数据清洗错误:缺失值处理不当、异常值误判、重复数据未去重

大家好,我是你们这堂课的讲师。

咱们做ADMET预测模型,有个残酷的现实——模型性能的天花板,往往不在算法,而在数据。你想想看,算法再花哨,配方再惊艳,喂进去的数据是脏的,结果能好到哪去?

我自己踩过太多坑了。以前刚入行那会儿,拿到一份体外代谢数据,看着R²挺漂亮,兴奋得不行。结果一检查,好家伙,将近三分之一的样本是重复的,没去重。

今天咱们就好好聊聊数据清洗里最容易翻车的三个大坑:缺失值、异常值和重复数据。

数据清洗三大陷阱流程图 原始ADMET数据 缺失值处理不当 异常值误判 重复数据未去重 策略选择 均值/中位数/模型插补 科学判断 IQR/z-score临界值 去重策略 结构+实验双重匹配 干净的建模数据集 三个错误相互关联:一个环节出错,后续模型验证全都白做

一、缺失值处理不当

说实话,缺失值这个问题,看着简单,实际里面门道很深。我见过有人不管三七二十一,看到NaN就直接删行。行数少了,数据也歪了。还有人一股脑全用均值填充,结果把分布全搞乱了。

为什么会这样?因为缺失值不是生来平等的。你得先搞清楚它是哪种缺失。

1. 缺失类型怎么判断

我一般把缺失分为三类,工作中得区别对待:

类型 定义 举个ADMET的例子 推荐处理方式
完全随机缺失 缺失和观测值没关系 仪器偶尔故障,随机丢了一个孔的数据 直接删除不会引入偏差,但行数少时用插补
随机缺失 缺失跟其他已知变量有关 高LogP化合物的水溶性测不出来 用其他相关变量做插补
非随机缺失 缺失值由变量本身决定 毒性太强的化合物测不出IC50 别删也别简单填充,考虑加入一个“无法测量”的标记

我的经验:拿到数据后,先做一个缺失矩阵图。一眼就能看出哪些特征经常一起缺失,哪些化合物结构域缺失严重。这比看统计数字快得多。

2. 常见的错误操作

  1. 全部删除:把含缺失的行全丢掉。数据量本来就不大,再删就没法建模了。
  2. 统一用均值填充:这会把预测变量之间的相关性压扁,模型的区分能力跟着下降。
  3. 填充后不记录:做完填充完全没有标记,后续分析都不知道哪些是原来的值,哪些是填进去的。

3. 修复策略对比

方法 适用场景 注意点
均值/中位数填充 刚好缺失率低于5%,且是连续变量 会让方差变小,谨慎使用
KNN插补 有相似样本存在 我常用,但注意k值别设太小,5-10之间
多重插补 缺失率较高(20%以上) 计算量大,但结果更可靠
模型预测插补 缺失特征与其他特征强相关 小心过拟合,最好用轻量模型
# 举个例子,KNN插补的代码实现
from sklearn.impute import KNNImputer

imputer = KNNImputer(n_neighbors=5, weights="distance")
X_imputed = imputer.fit_transform(X)
# 记得在管道里做,别在训练集上单独做

二、异常值误判

这个坑我栽得最惨。有一次我拿到一批Caco-2渗透性数据,看到两个点在10⁻⁵级别,远远偏离群组,想都没想就直接删了。后来再细看文献,那两化合物是P-gp底物,值就那么低。

误判异常值的后果:把正常值当异常删了,模型就学不到真实的边缘情况,预测标准差被压缩,对新化合物会过于自信。

1. 异常值不是你想删就能删的

我个人判断异常值的流程是这样的:

  1. 先用统计方法找出来:比如z-score超过3,或者IQR法标出的点。这些都是"疑似"。
  2. 然后去查原始实验记录:是不是仪器故障?是不是有人操作失误?
  3. 最后问化学:这个结构本身是不是就比较特殊?

千万别干的事:只看统计分布就下结论。我一个同事就因为IQR法把几个高活性化合物标成异常给删了,模型训练完后对高活性区间的预测能力一塌糊涂。

2. 常见误判场景

场景 统计表现 化学解释
强效先导化合物 活性值远低于均值 它是这个系列里面最优的,不是异常
代谢极差的底物 半衰期异常长 它本来就是代谢慢的典型结构
实验批次误差 一组数据整体偏移 可能是过板或孵育条件问题

3. 修复建议

我建议你建立起一个"异常值审查日志",每条被判为异常的数据都得登记:

异常值审查模板(我自己的习惯):
--------------------------------
化合物ID: CPD-0042
目标值: Caco-2 Papp = 3.2e-6 cm/s
统计标记: z-score = 3.8(严重偏高)
化学家意见: 该化合物含胍基,带正电,渗透低是正常的
处理结果: 保留,不删除
审查人: 李博士
日期: 2024-03-15

三、重复数据未去重

你觉得重复数据有什么影响?嗯,说出来你可能不信,它比缺失值还隐蔽。缺失值是丢了信息,重复数据是偷着给某些样本加权重。模型会认为这个结构出现了两次、三次,它的信号就被放大了。

1. 重复数据的常见来源

  • 不同实验批次的同一化合物——多批次数据合库时经常发生
  • 异构体或类似结构——InChIKey不同但实际结构几乎一样
  • 同一化合物的不同盐形式——做ADMET时通常当不同记录处理
  • 手动录入重复——Excel里复制粘贴多了行

我曾经接手过一个模型,发现每次训练在验证集上的R²都很高,但外部预测惨不忍睹。一查数据,多个核心骨架的化合物被重复录入了3-4遍,模型相当于在"背诵"这些样本。

2. 去重的正确操作

先按结构去重,再按实验值去重。这两步不能反过来。

# 按标准InChI去重
df["std_inchi"] = df["smiles"].apply(lambda x: Chem.MolToInchi(Chem.MolFromSmiles(x)))
df_deduplicated = df.drop_duplicates(subset=["std_inchi", "assay_name"], keep="first")

# 检查同一结构的实验值一致性
df.groupby("std_inchi")["logD"].agg(["mean", "std", "count"]).reset_index()

3. 碰到同结构不同值怎么办?

同一个结构,两批次测出来的logD相差0.8个单位。你留哪个?

我的做法是:

  1. 先排查是不是批次差异——两个实验是不是用了不同pH缓冲液?
  2. 不是批次问题——取中位数,或者取更近批次的值
  3. 保留不确定性——给这个化合物分配一个更大的置信权重

终极建议:数据清洗不是一次性工作。每次你从外部接新数据、合并新批次,都要重新走一遍缺失值、异常值、去重这三关。我习惯用一个版本化的清洗脚本,每次跑完会输出报告,列清楚:删了多少行异常、填充了多少缺失、去掉了多少重复。这样项目复盘的时候,谁也说不了闲话。

好了,数据清洗这三个坑,你看明白了吗?下次拿到新的ADMET数据,先问问自己:缺失值我搞清楚类型了吗?异常值我确认过化学意义了吗?重复数据我完整去重了吗?这三关过了,后面的建模才有意义。