在机器学习项目中,我们常常被各种炫酷的算法吸引注意力,却忽视了最基础也最重要的环节——数据预处理。从业多年,我见过太多项目因为数据质量问题而失败,也见证过那些看似简单的模型因为优质数据而大放异彩。
业界有个不争的事实:数据质量决定了模型性能的上限,而算法只是在逼近这个上限。就像一位米其林大厨,即使用最顶级的厨艺,也无法用腐烂的食材做出美味佳肴。在机器学习中,这个道理同样适用。
我曾在一次客户项目中遇到一个典型案例:客户抱怨他们的推荐系统效果不佳,投入大量时间调整模型参数却收效甚微。当我们检查原始数据时发现,用户行为日志中存在大量重复记录和异常时间戳,商品特征中有30%的缺失值。经过两周的数据清洗和特征工程后,使用同样的模型,推荐准确率直接提升了42%。
根据我的项目经验,一个完整机器学习项目的时间分配大致如下:
这个比例可能会让初学者感到惊讶,但确实反映了数据工作的重要性。好的数据科学家不是调参高手,而是数据"整形"专家。
拿到数据后的第一步不是急着清洗,而是全面了解数据的"健康状况"。这包括:
在Python中,我们可以使用以下代码快速完成这些检查:
python复制import pandas as pd
# 加载数据
df = pd.read_csv('titanic_train.csv')
# 基础信息
print(f"数据集形状: {df.shape}")
print("\n数据类型:")
print(df.dtypes)
# 缺失值统计
print("\n缺失值情况:")
print(df.isnull().sum())
# 数值型数据描述统计
print("\n数值特征统计:")
print(df.describe())
除了基础统计,可视化能帮助我们更直观地发现问题:
python复制import matplotlib.pyplot as plt
import seaborn as sns
# 设置绘图风格
sns.set(style="whitegrid")
# 年龄分布检查
plt.figure(figsize=(10,6))
sns.histplot(df['Age'].dropna(), kde=True, bins=30)
plt.title('Age Distribution')
plt.show()
# 票价箱线图(检查异常值)
plt.figure(figsize=(10,6))
sns.boxplot(x=df['Fare'])
plt.title('Fare Distribution')
plt.show()
提示:在这个阶段发现的问题应该记录下来,形成数据质量报告,为后续的清洗工作提供依据。
缺失值处理前需要先理解其产生原因:
根据不同的场景,我通常会采用以下策略:
删除处理
python复制df = df.drop('Cabin', axis=1)
python复制df = df.dropna(subset=['Embarked'])
填充处理
python复制# 中位数填充(抗异常值)
age_median = df['Age'].median()
df['Age'] = df['Age'].fillna(age_median)
# 均值填充
fare_mean = df['Fare'].mean()
df['Fare'] = df['Fare'].fillna(fare_mean)
python复制# 众数填充
embarked_mode = df['Embarked'].mode()[0]
df['Embarked'] = df['Embarked'].fillna(embarked_mode)
# 新增"缺失"类别
df['Cabin'] = df['Cabin'].fillna('Unknown')
高级方法
python复制from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df[['Age']] = imputer.fit_transform(df[['Age']])
python复制from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imputer = IterativeImputer(max_iter=10, random_state=42)
df[['Age']] = imputer.fit_transform(df[['Age']])
注意事项:测试集的缺失值填充必须使用训练集计算的统计量(均值、中位数等),避免数据泄露。
统计方法
python复制from scipy import stats
z_scores = stats.zscore(df['Fare'])
outliers = (abs(z_scores) > 3)
python复制Q1 = df['Fare'].quantile(0.25)
Q3 = df['Fare'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
可视化方法
删除法
python复制df = df[(df['Fare'] >= lower_bound) & (df['Fare'] <= upper_bound)]
盖帽法(Winsorization)
python复制df['Fare'] = df['Fare'].clip(lower_bound, upper_bound)
转换法
python复制df['Fare_log'] = np.log1p(df['Fare'])
python复制df['Fare_bin'] = pd.qcut(df['Fare'], q=5, labels=False)
经验分享:对于业务相关的异常值(如电商中的超高金额订单),不要盲目处理,应该先与业务方确认是否为真实数据。
标签编码(Label Encoding)
python复制from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['Pclass_encoded'] = le.fit_transform(df['Pclass'])
独热编码(One-Hot Encoding)
python复制df = pd.get_dummies(df, columns=['Sex', 'Embarked'], prefix=['Sex', 'Embarked'])
目标编码(Target Encoding)
python复制from category_encoders import TargetEncoder
encoder = TargetEncoder()
df['Cabin_encoded'] = encoder.fit_transform(df['Cabin'], df['Survived'])
标准化(Standardization)
python复制from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
df[['Age_std', 'Fare_std']] = scaler.fit_transform(df[['Age', 'Fare']])
归一化(Normalization)
python复制from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
df[['Age_norm', 'Fare_norm']] = scaler.fit_transform(df[['Age', 'Fare']])
鲁棒缩放(Robust Scaling)
python复制from sklearn.preprocessing import RobustScaler
scaler = RobustScaler()
df[['Age_robust', 'Fare_robust']] = scaler.fit_transform(df[['Age', 'Fare']])
专业建议:对于树模型(如随机森林、XGBoost),通常不需要做特征缩放,但对线性模型(如逻辑回归)和距离-based模型(如KNN)则非常重要。
在泰坦尼克号数据集中,我们可以创建:
python复制df['FamilySize'] = df['SibSp'] + df['Parch'] + 1
python复制df['Title'] = df['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
python复制df['Age_Pclass'] = df['Age'] * df['Pclass']
python复制from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
poly_features = poly.fit_transform(df[['Age', 'Fare']])
方差阈值
python复制from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.1)
X_selected = selector.fit_transform(X)
单变量统计检验
python复制from sklearn.feature_selection import SelectKBest, chi2
selector = SelectKBest(chi2, k=10)
X_new = selector.fit_transform(X, y)
python复制from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
estimator = LogisticRegression()
selector = RFE(estimator, n_features_to_select=5)
selector = selector.fit(X, y)
基于L1正则化的特征选择
python复制from sklearn.linear_model import Lasso
lasso = Lasso(alpha=0.1)
lasso.fit(X, y)
# 非零系数对应的特征被选中
树模型的特征重要性
python复制from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
model.fit(X, y)
importances = model.feature_importances_
python复制from sklearn.decomposition import PCA
pca = PCA(n_components=0.95) # 保留95%方差
X_pca = pca.fit_transform(X)
python复制from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda = LinearDiscriminantAnalysis(n_components=1)
X_lda = lda.fit_transform(X, y)
python复制from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# 数值型特征处理
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# 类别型特征处理
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# 组合处理器
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# 完整Pipeline
pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', LogisticRegression())
])
对于复杂的特征工程,可以创建自定义转换器:
python复制from sklearn.base import BaseEstimator, TransformerMixin
class TitleExtractor(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
return self
def transform(self, X):
return X['Name'].str.extract(' ([A-Za-z]+)\.', expand=False).to_frame()
python复制import joblib
# 保存
joblib.dump(pipeline, 'preprocessing_pipeline.pkl')
# 加载
loaded_pipeline = joblib.load('preprocessing_pipeline.pkl')
数据泄露问题
类别不平衡问题
高基数类别问题
大数据集处理
并行处理
内存优化
数据漂移检测
预处理版本控制
自动化测试
在真实项目中,我通常会建立一个数据预处理的标准操作流程(SOP),包含数据质量检查表、预处理方法选择指南和验证步骤。这不仅提高了工作效率,也确保了不同项目间的一致性。