缺失值处理:别让“空”毁了你的模型

做RNA-seq分析,缺失值是个绕不开的坑。我当年刚接触转录组数据时,拿到表达矩阵第一件事就是直接丢给机器学习模型跑——结果准确率惨不忍睹。后来一查,发现矩阵里飘着一堆NaN和0。嗯,并不是基因不表达,而是测序没测到。你想想看,模型看到一堆缺失值,能不懵吗?

这一章我就聊聊缺失值怎么来的,以及我常用的几种填补策略。坦白说,没有万能药,但选对了方法能让你的模型至少提升5-10个点。

基因表达数据为什么会有缺失?

很多人以为表达矩阵里出现0或NaN就是基因没表达。其实大部分缺失是技术原因造成的,跟生物学无关。我总结了几个常见“凶手”:

  • 测序深度不足:低表达基因的read count本来就少,抽样时可能恰好没抽到。这就是所谓的“dropout事件”。
  • 低表达基因:表达量太低,低于检测阈值,仪器报不出来。
  • 批次效应:不同实验批次、不同试剂、不同操作员——我曾在一个项目中,两批样本同一基因的缺失率差了30%,原因竟然是换了一台测序仪。
  • PCR扩增偏好:高GC含量的区域难扩增,容易漏掉。
  • 比对质量问题:低质量read被过滤掉,对应基因就空了。
我的习惯:拿到表达矩阵后,先画一张缺失值热图,按行(基因)和列(样本)统计缺失比例。如果某个样本的缺失率超过50%,我建议直接剔除——你想想看,一半都是空值,怎么填都像在猜谜。

常见填补策略:从粗暴到精细

填补方法很多,但核心就两个字:推和猜。推是用统计量替代,猜是利用其他基因或样本的关系来预测。下面几个策略我都在真实项目里踩过坑,一个一个说。

均值/中位数填补(最简单,但小心翻车)

把基因在所有样本中的表达值取平均(或中位数),填到空位置上。代码就两行:

from sklearn.impute import SimpleImputer
import numpy as np

imputer = SimpleImputer(strategy='median')
matrix_filled = imputer.fit_transform(expression_matrix)

我在处理一个乳腺癌数据集时试过均值填补,结果聚类分析直接把所有样本分成了两团——后来发现是因为均值被几个高表达样本带偏了。换成中位数就好多了。我个人习惯:有异常值用中位数,否则用均值。

翻车警告:如果缺失比例超过20%,均值/中位数填补会严重压缩表达值的变异度,导致后续差异基因分析全错。我曾经因此漏掉了3个真正的biomarker。

KNN填补(利用“邻居”帮忙)

找k个与缺失基因表达模式最相似的基因(按非缺失位置算欧氏距离),用它们的加权平均来填。sklearn直接支持:

from sklearn.impute import KNNImputer

imputer = KNNImputer(n_neighbors=5, weights='distance')
matrix_filled = imputer.fit_transform(expression_matrix)

效果比均值好很多,但有两个坑:一是计算量大——如果你有2万个基因,跑一次KNN要几分钟;二是k值怎么选?我试过5、10、15,在TCGA的肝癌数据上k=10效果最好。建议用交叉验证来调。

随机森林填补(迭代预测,效果最稳)

先用均值初始化,然后每次选一个含缺失值的基因作为“目标变量”,其他基因做特征,训练随机森林来预测缺失值。迭代个10轮,结果就收敛了。sklearn的IterativeImputer就是这个逻辑:

from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor

imputer = IterativeImputer(
    estimator=RandomForestRegressor(n_estimators=100, max_depth=10),
    max_iter=10,
    random_state=42)
matrix_filled = imputer.fit_transform(expression_matrix)

这个是我最常用的方法,尤其在要建预测模型时。随机森林本身对噪声不敏感,而且能捕捉基因之间的非线性关系。不过迭代10次的话,2万基因×500样本的数据集可能要跑30分钟。嗯,时间换精度,值不值你自己掂量。

方法对比:一张表说清楚

方法 速度 适用场景 我踩过的坑
均值/中位数 极快 缺失率<10%,且数据分布对称 异常值会把均值带偏;中位数更扛噪
KNN填补 中等 缺失率10%-30%,样本量适中 k选不好,邻居里全是空的就翻车
随机森林 慢 缺失率30%-50%,追求模型精度 迭代次数多了过拟合,10轮足够

核心流程:一张SVG图讲清楚

下面我画了一个流程图,概括了从缺失诊断到策略选择再到效果验证的完整思路。你可以把它贴在工位旁边,做RNA-seq分析时心里有个谱:

缺失值处理核心流程 原始表达矩阵 缺失诊断:原因+比例 缺失<10% → 均值/中位数 10%-30% → KNN 30%-50% → 随机森林 填补执行 + 迭代优化 效果验证(分布/聚类/模型) 完整表达矩阵 说明: 缺失率超过50%建议直接 剔除基因或样本,不要硬填。

避坑指南:这些年我交过的学费

填补不是终点,填完一定要验证。我分享几个真实教训:

  • 别对所有基因无差别填补:我曾图省事,把缺失率95%的基因也用KNN填了——结果模型把那些基因当作重要特征了。后来发现,填出来的值基本是噪声。建议先过滤掉缺失率>50%的基因。
  • 填完后检查分布:均值填补会让表达分布出现一个尖锐的峰,峰顶正好是均值。你画个核密度图就能看出来。如果看到这种“假峰”,赶紧换方法。
  • 不要忽视零值:有些软件把0当作缺失,有些当作真实表达。我就被坑过一次——用DESeq2时把0当作缺失填了,结果差异基因分析全错了。先弄清楚你数据里0的意义。

我的经验总结:如果最终目的是建分类模型,优先用随机森林填补;如果是做差异分析或降维,KNN更安全。但无论选哪个,**先用原始非缺失数据做一次小实验**——随机挑10%的基因人为“造缺失”,然后看你的填补方法能恢复多少。这个步骤花10分钟,能帮你省下未来几周的重做时间。