好的,我们直接进入第四章正文。

第 4 章:缺失值处理

说实话,做数据预处理这么多年,缺失值处理是最基础但也最容易被搞砸的环节。你想想看,原始组学数据里,缺失值几乎是不可避免的——机器打盹、过程出错、样本量不够,总有各种理由让你的矩阵里出现 NaN。

我个人习惯是先把问题简单化。说白了,处理缺失值就三大流派:删掉它、填个值、猜个值。听起来很直白对吧?但每一条路都有不少坑。我刚开始做项目时,随手用了平均值填充,结果模型性能直接崩了。

这一章,我们就把这三种方法掰开揉碎讲清楚。

缺失值处理三大方法对比 原始数据含缺失值 删除法 均值/中位数填充 模型预测填充 行删除 / 列删除 优点:简单、完全 缺点:损失信息 适用于:缺失极少 统计量填充 优点:快捷、可控 缺点:可能偏离分布 适用于:特征值稳定 KNN / 回归预测 优点:精度高 缺点:计算成本高 适用于:缺失较多 清洗后的数据集

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 填那些敏感特征。怎么组合,完全取决于你对数据的理解。