机器学习特征工程中的缺失值处理方法
机器学习特征工程:缺失值处理方法完全指南
概述
在真实世界的数据集中,缺失值是普遍存在的现象。如何妥善处理缺失值,是特征工程中最基础也最关键的环节之一。不当的处理可能引入偏差、丢失信息甚至导致模型性能显著下降。本教程将从零开始,系统地讲解缺失值处理的核心方法论与 Python 实践,帮助你建立完整、实用的知识体系。
理解缺失值
缺失值的产生机制
在动手处理之前,必须先理解数据“为何缺失”。根据缺失机制,我们可以将缺失分为三类:
- 完全随机缺失 (MCAR):缺失的发生与数据本身完全无关。例如,实验设备随机故障导致某次测量未记录。这种情况下,直接删除缺失样本通常不会引入明显偏差。
- 随机缺失 (MAR):缺失的发生与数据中其他已观测到的变量有关,但与缺失值本身无关。例如,男性更不愿意填写收入调查中的“收入”字段,但收入缺失的概率仅依赖于已观测的“性别”,而非收入值本身。
- 非随机缺失 (MNAR):缺失的发生与缺失值本身有关。例如,高收入人群更倾向于隐藏自己的收入,此时缺失值与收入的高低直接相关。这类情况最为复杂,需要谨慎处理。
理解缺失机制有助于选择更合理的处理方法。
识别缺失值
在 Python 中,我们使用 pandas 快速发现缺失值。常见的缺失标记包括 None、NaN、空字符串等。
import pandas as pd
import numpy as np
# 示例数据
df = pd.DataFrame({
'age': [25, np.nan, 35, 40, np.nan],
'income': [50000, 60000, np.nan, 80000, 92000],
'gender': ['M', 'F', None, 'F', 'M']
})
# 查看每个特征的缺失数量
print(df.isnull().sum())
# 全局缺失比例
print(df.isnull().sum() / len(df))
缺失值处理方法分类
处理缺失值的方法大致可分为以下四类:
- 删除法:直接移除含缺失值的样本或特征。
- 单变量填充法:利用该特征自身的统计量(均值、中位数、众数等)进行填充。
- 多变量填充法:利用其他特征的信息预测缺失值(KNN、回归、多重插补等)。
- 标记与模型融合:保留缺失状态作为新特征,结合填充值共同进入模型。
下面逐一展开。
删除法
删除含缺失值的样本
- 操作:
df.dropna()或df.dropna(axis=0) - 适用场景:缺失比例极小(如 < 5%),且缺失机制为 MCAR,删除后样本量仍足够充裕。
- 风险:若缺失非完全随机,会导致样本分布偏差;大量删除会损失宝贵信息。
删除含缺失值的特征
- 操作:
df.dropna(axis=1) - 适用场景:某一特征的缺失比例过高(如 > 50%),且该特征对模型贡献有限。
- 风险:可能丢弃有潜在价值的特征。
# 删除任何包含缺失值的行
df_dropped_rows = df.dropna()
# 删除缺失值超过一定阈值的列
threshold = len(df) * 0.5 # 保留缺失比例不超过50%的列
df_cleaned = df.dropna(axis=1, thresh=threshold)
单变量填充法
简单统计填充
利用特征的集中趋势进行填充:
- 均值填充:适用于近似正态分布的连续特征。
- 中位数填充:适用于存在离群值的连续特征,更具稳健性。
- 众数填充:适用于离散/分类特征。
from sklearn.impute import SimpleImputer
# 均值填充数值特征
imputer_mean = SimpleImputer(strategy='mean')
df['age'] = imputer_mean.fit_transform(df[['age']])
# 中位数填充
imputer_median = SimpleImputer(strategy='median')
df['income'] = imputer_median.fit_transform(df[['income']])
# 众数填充分类特征(需转换为数值型)
imputer_mode = SimpleImputer(strategy='most_frequent')
df['gender'] = imputer_mode.fit_transform(df[['gender']])
常数值填充
使用自定义的常数(如 -1、"unknown")填充所有缺失值。这种方法可以显式标记缺失状态,但可能引入错误默认值。
df['age'].fillna(-1, inplace=True)
多变量填充法
KNN 填充
基于 K 近邻算法,用与缺失样本最相似的 K 个邻居的加权平均来估计缺失值。该方法充分利用了特征间的关系,但计算量较大,且需要对数据进行标准化。
from sklearn.impute import KNNImputer
from sklearn.preprocessing import StandardScaler
# 假设 df 只包含数值特征
scaler = StandardScaler()
df_scaled = scaler.fit_transform(df)
imputer_knn = KNNImputer(n_neighbors=5)
df_imputed = imputer_knn.fit_transform(df_scaled)
# 记得逆标准化以便解释
df_imputed = scaler.inverse_transform(df_imputed)
插值法
用于时间序列或有序数据。通过相邻观测值进行线性、多项式或样条插值。
# 假设 df 具有时间索引且按时间排序
df['sensor'] = df['sensor'].interpolate(method='linear', limit_direction='both')
多重插补
多重插补是处理复杂缺失(尤其是 MNAR)的黄金标准。它通过多次迭代创建多组完整数据,分析时综合考虑填充的不确定性。IterativeImputer 是 sklearn 提供的便捷实现。
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imputer_iter = IterativeImputer(max_iter=10, random_state=0)
df_imputed_iter = imputer_iter.fit_transform(df)
标记缺失与模型融合
有时缺失本身也含有信息。我们可以创建一个新的二值特征来标记原特征是否缺失,然后对原特征进行填充(如均值填充)。这样既保留了“缺失”这个信息,又保证了特征完整。
# 为 'age' 创建缺失指示器
df['age_missing'] = df['age'].isnull().astype(int)
# 对原始特征进行均值填充
df['age'].fillna(df['age'].mean(), inplace=True)
在一些模型(如 XGBoost、LightGBM)中,甚至可以不做填充,由模型自动处理缺失值。但进行标记后填充的方式对传统模型(如线性模型、N N)更为友好。
方法选择的核心原则
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 删除 | 简单、不引入人工偏见 | 损失信息,易偏差 | 缺失极少、MCAR |
| 均值/中位数 | 简单、计算快 | 扭曲分布,低方方差 | 快速基线、数据量大 |
| KNN | 利用特征关系,精度较高 | 计算开销大,需标准化 | 特征相关性较强 |
| 多重插补 | 统计合理,处理不确定性 | 调试复杂,计算慢 | 严谨分析、高价值决策 |
| 标记缺失 | 保留缺失状态信息 | 增加特征数量 | 缺失本身具有意义 |
实战建议:始终保留一份原始数据,在一个科学划分的训练/测试集上验证不同处理方法对模型性能的影响,选择交叉验证得分最优的策略。避免在未见过的数据上使用填充统计量计算时发生数据泄露——填充所用的均值、中位数等必须从训练集计算,并应用到测试集。
完整流程示例
以下是一个端到端的处理流水线,使用 Pipeline 确保填充逻辑不与测试数据泄露。
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# 假定 X_train, X_test, y_train, y_test 已划分
num_cols = ['age', 'income']
cat_cols = ['gender']
# 数值特征流水线
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy='median')),
# 可继续添加标准化等步骤
])
# 分类特征流水线
cat_pipeline = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
# 可继续添加编码步骤
])
from sklearn.compose import ColumnTransformer
preprocessor = ColumnTransformer([
('num', num_pipeline, num_cols),
('cat', cat_pipeline, cat_cols)
])
# 完整模型流水线
model = Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier())
])
# 训练与评估
model.fit(X_train, y_train)
print(model.score(X_test, y_test))
总结
缺失值处理并非“补上就完事”,而是一个需要结合数据理解、业务场景和模型特性的系统工程。从简单的删除与统计填充,到复杂的多重插补与缺失标记,没有银弹。核心原则是:不要盲目填充,先分析缺失机制;在交叉验证指导下选择方法;始终防范数据泄露。掌握这些方法后,你的特征工程能力将向上迈出扎实一步。