数据清洗错误:缺失值处理不当、异常值误判、重复数据未去重
大家好,我是你们这堂课的讲师。
咱们做ADMET预测模型,有个残酷的现实——模型性能的天花板,往往不在算法,而在数据。你想想看,算法再花哨,配方再惊艳,喂进去的数据是脏的,结果能好到哪去?
我自己踩过太多坑了。以前刚入行那会儿,拿到一份体外代谢数据,看着R²挺漂亮,兴奋得不行。结果一检查,好家伙,将近三分之一的样本是重复的,没去重。
今天咱们就好好聊聊数据清洗里最容易翻车的三个大坑:缺失值、异常值和重复数据。
一、缺失值处理不当
说实话,缺失值这个问题,看着简单,实际里面门道很深。我见过有人不管三七二十一,看到NaN就直接删行。行数少了,数据也歪了。还有人一股脑全用均值填充,结果把分布全搞乱了。
为什么会这样?因为缺失值不是生来平等的。你得先搞清楚它是哪种缺失。
1. 缺失类型怎么判断
我一般把缺失分为三类,工作中得区别对待:
| 类型 | 定义 | 举个ADMET的例子 | 推荐处理方式 |
|---|---|---|---|
| 完全随机缺失 | 缺失和观测值没关系 | 仪器偶尔故障,随机丢了一个孔的数据 | 直接删除不会引入偏差,但行数少时用插补 |
| 随机缺失 | 缺失跟其他已知变量有关 | 高LogP化合物的水溶性测不出来 | 用其他相关变量做插补 |
| 非随机缺失 | 缺失值由变量本身决定 | 毒性太强的化合物测不出IC50 | 别删也别简单填充,考虑加入一个“无法测量”的标记 |
我的经验:拿到数据后,先做一个缺失矩阵图。一眼就能看出哪些特征经常一起缺失,哪些化合物结构域缺失严重。这比看统计数字快得多。
2. 常见的错误操作
- 全部删除:把含缺失的行全丢掉。数据量本来就不大,再删就没法建模了。
- 统一用均值填充:这会把预测变量之间的相关性压扁,模型的区分能力跟着下降。
- 填充后不记录:做完填充完全没有标记,后续分析都不知道哪些是原来的值,哪些是填进去的。
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. 异常值不是你想删就能删的
我个人判断异常值的流程是这样的:
- 先用统计方法找出来:比如z-score超过3,或者IQR法标出的点。这些都是"疑似"。
- 然后去查原始实验记录:是不是仪器故障?是不是有人操作失误?
- 最后问化学:这个结构本身是不是就比较特殊?
千万别干的事:只看统计分布就下结论。我一个同事就因为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个单位。你留哪个?
我的做法是:
- 先排查是不是批次差异——两个实验是不是用了不同pH缓冲液?
- 不是批次问题——取中位数,或者取更近批次的值
- 保留不确定性——给这个化合物分配一个更大的置信权重
终极建议:数据清洗不是一次性工作。每次你从外部接新数据、合并新批次,都要重新走一遍缺失值、异常值、去重这三关。我习惯用一个版本化的清洗脚本,每次跑完会输出报告,列清楚:删了多少行异常、填充了多少缺失、去掉了多少重复。这样项目复盘的时候,谁也说不了闲话。
好了,数据清洗这三个坑,你看明白了吗?下次拿到新的ADMET数据,先问问自己:缺失值我搞清楚类型了吗?异常值我确认过化学意义了吗?重复数据我完整去重了吗?这三关过了,后面的建模才有意义。