第 4 章:缺失值处理
说实话,做数据预处理这么多年,缺失值处理是最基础但也最容易被搞砸的环节。你想想看,原始组学数据里,缺失值几乎是不可避免的——机器打盹、过程出错、样本量不够,总有各种理由让你的矩阵里出现 NaN。
我个人习惯是先把问题简单化。说白了,处理缺失值就三大流派:删掉它、填个值、猜个值。听起来很直白对吧?但每一条路都有不少坑。我刚开始做项目时,随手用了平均值填充,结果模型性能直接崩了。
这一章,我们就把这三种方法掰开揉碎讲清楚。
4.1 删除法:干脆利落,但有代价
删除法是最直接的路子。看到 NaN,直接删掉那一行或那一列。听起来很粗鲁,但在某些场景下确实管用。
- 行删除(Listwise Deletion):删掉任何包含缺失值的样本。整行扔掉。
- 列删除(Column Deletion):如果某一列缺失率太高,直接抛弃这个特征。
我在做客户数据清洗时,用过一次行删除。一下子删掉了 20% 的数据。模型是跑通了,但我心里总有点发虚——那 20% 的客户可能是高价值群体。结果上线后模型在顶级用户的预测上疯狂翻车。
我曾经在几个小样本生物信息学数据集上用了行删除,样本量直接降到原来的三分之一。模型的置信区间宽得吓人,最后评审直接被拒。关键点:当缺失率超过 5% 时,行删除要格外谨慎;如果数据本身就不足千条,最好别删。
列删除的决策标准其实很清晰。我一般会这么判断:
| 缺失率 | 处理策略 |
|---|---|
| <1% | 通常可以行删除,影响极小 |
| 1% - 5% | 根据样本量决定,样本充足时行删除 |
| 5% - 20% | 考虑填充或模型预测,不建议直接删除 |
| >20% | 推荐列删除,或者重新考量该特征的价值 |
4.2 均值/中位数填充:最常用的“救火队”
如果不愿意丢数据,那填一个值呗。最朴素的想法就是用该特征的平均值或中位数去填补 NaN。
- 均值填充:适合正态分布或近似正态分布的数据。
- 中位数填充:数据有偏斜,或者有异常值的时候,中位数更稳健。
这两个哪个更好?你想想看,如果特征是「收入」,大部分人月薪一万,某几个大佬月薪百万。这时候均值会被大佬拉得很高,填进去反而引入偏差。中位数才是那个靠谱的选择。
填充前先画个分布直方图。如果是钟形曲线(正态),放心用均值。如果是长尾分布,铁定选中位数。这步花不了 30 秒,但能避免很多后续麻烦。
用 scikit-learn 实现很简单:
from sklearn.impute import SimpleImputer
import numpy as np
# 假设 data 是 NumPy 数组,列方向有缺失
# 均值填充
imputer_mean = SimpleImputer(strategy='mean')
data_filled_mean = imputer_mean.fit_transform(data)
# 中位数填充
imputer_median = SimpleImputer(strategy='median')
data_filled_median = imputer_median.fit_transform(data)
这里有一点要注意:SimpleImputer 默认是按列(特征)计算的。如果你的数据是按行组织样本,按列组织特征,这没问题。但如果你的矩阵方向理解错了,填进去的就是错的值。
在时间序列或流式数据中,用全局均值填充缺失值其实是危险的。因为你用到了未来的数据(后续时间点)来估计过去的缺失。正确的做法是用滚动窗口的值,或者向前填充、向后填充。千万别在线上推理时用离线算好的均值。
4.3 模型预测填充:用 AI 填补 AI 的坑
如果你觉得填一个简单统计量太粗放,那不如用模型来「猜」缺失值。我管这个叫「用 AI 填补数据」。说白了,就是拿没有缺失的样本作为训练集,把缺失的特征当标签来预测。
常用的模型有两类:
- K 近邻(KNN Imputer):找到缺失样本的 K 个最近邻,用他们的值做加权平均。
- 回归模型:用其他完整特征作为自变量,缺失特征作为因变量,训练线性回归或随机森林来预测。
举个例子,用 KNN Imputer:
from sklearn.impute import KNNImputer
import numpy as np
# 假设 data 有缺失值
imputer_knn = KNNImputer(n_neighbors=5)
data_filled_knn = imputer_knn.fit_transform(data)
这段代码会自动计算欧氏距离,选取 5 个最相似的完整样本,然后取均值填充。很方便,但它背后有个前提——距离计算本身不能依赖缺失值。不过 scikit-learn 已经帮你处理好了。
KNN Imputer 中 n_neighbors 的取值很关键。太小(比如 k=1)容易过拟合噪声;太大(比如 k=50)则会让预测趋近于全局均值,失去模型的优势。我一般在 [3, 10] 之间用交叉验证来选。另外,数据量不大时,距离计算很快;如果数据有百万行,KNN 的复杂度会成为负担。
至于回归模型填充,它更灵活。你可以先把没有缺失的行拿出来,用其他列做特征训练一个随机森林或 XGBoost,然后用这个模型去预测缺失值。
from sklearn.ensemble import RandomForestRegressor
import numpy as np
# 假设第 2 列有缺失
# 找出第 2 列没有缺失的行作为训练集
train_mask = ~np.isnan(data[:, 1])
X_train = data[train_mask, [0, 2, 3]] # 用其他列做特征
y_train = data[train_mask, 1] # 第 2 列是标签
# 训练模型
model = RandomForestRegressor(n_estimators=100)
model.fit(X_train, y_train)
# 预测缺失部分
test_mask = np.isnan(data[:, 1])
X_test = data[test_mask, [0, 2, 3]]
predicted = model.predict(X_test)
data[test_mask, 1] = predicted
这个方法的优势是精度高,能捕捉到特征之间的复杂关系。但缺点你也看到了——如果缺失率太高,训练集的有效样本不够,模型本身的预测就不靠谱。我曾经接过一个信贷评分项目,某特征缺失率达到 40%,我用随机森林预测填充,结果模型在验证集上的 R² 只有 0.2。填进去的值基本等于随机噪声。
模型预测填充不适合所有场景。我建议只在缺失率 10%–30% 之间、且其他特征与缺失特征明显相关时使用。如果相关性太弱,模型猜出来的值还不如简单用中位数。
最后总结一下三种方法的适用场景:
| 方法 | 适用场景 | 我最常踩的坑 |
|---|---|---|
| 删除法 | 缺失率极低(<1%);样本量很大 | 忘记检查删除后样本分布是否偏移 |
| 均值/中位数填充 | 缺失率不高;特征取值稳定 | 在时间序列上用了全局均值导致未来信息泄露 |
| 模型预测填充 | 缺失率中等;特征间关系紧密 | 缺失率过高或特征相关弱时,模型预测变垃圾 |
嗯,这三种方法是大部分缺失值预处理的主干。在实际项目中,你可能会把它们组合使用:先用列删除扔掉垃圾特征,用中位数填充普通特征,最后用 KNN 填那些敏感特征。怎么组合,完全取决于你对数据的理解。