数据预处理:打好地基,才能建高楼

做化合物有效性评估,最怕什么?数据本身有问题。图表再漂亮,底层的数字是脏的,那就是「垃圾进,垃圾出」。我记得刚入行时接手过一个抗肿瘤项目,活性数据看起来挺漂亮,结果一复查——三分之一样本漏加溶剂,缺失值直接用了0填补。嗯,那批图表后来全废了。

所以数据预处理这步,不能省,不能糊。我个人习惯把这项工作拆成三个核心动作:缺失值处理、标准化、异常值检测。今天咱们一个一个聊。

一、缺失值处理——别让“坑”毁了你的图

现实中的数据几乎都有缺失。可能是实验记录漏了,或是检测仪器临时故障。处理方法就那么几种,但用错了会埋雷。

常见路子:

  • 直接删除:简单粗暴,适用于缺失比例很小(< 5%)且随机缺失的情况。
  • 填充:用均值、中位数、众数、邻近值或模型预测值补上。
  • 插值:时间序列数据常用线性或样条插值。
  • 标记为特殊值:比如-1,但在标准化前要小心计算。

我在项目中遇到过最坑的事——某药物浓度数据有30%缺失,团队直接用全体均值填充。你想想看,浓度随时间下降的趋势全被抹平了,后续图表完全看不出药代动力学特征。后来我用分组(按给药剂量)中位数填充,才救回来。

核心原则:先判断缺失机制(随机/非随机),再选方法。不要无脑均值填充。

代码示范(Python + pandas):

import pandas as pd
import numpy as np

# 读取数据
df = pd.read_csv("compound_data.csv")

# 查看缺失情况
print(df.isnull().sum())

# 删除法:缺失率低于5%的列直接删行
df_clean = df.dropna(subset=['IC50', 'LogD'])

# 填充法:按分组用中位数填充
df['Solubility'].fillna(
    df.groupby('CompoundClass')['Solubility'].transform('median'),
    inplace=True
)

# 插值法:时间序列用前向后向填充
df['Concentration'].interpolate(method='linear', inplace=True)

方法对比表:

方法优点缺点适用场景
直接删除简单、不影响分布浪费样本、可能引入偏倚缺失率低、随机缺失
均值/中位数填充保留样本量缩小方差、扭曲相关性缺失率低、非敏感特征
KNN/模型预测利用特征间关系计算开销大缺失率较高、特征相关性强
标记法保留缺失信息部分模型不兼容决策树/随机森林等

二、标准化——不同尺度,统一量纲

化合物评估数据里,浓度范围可能是0.001~1000 µM,而毒性评分是1~10。你直接把这两列扔进聚类或距离计算的图表里,浓度铁定把毒性压死。标准化,说白了就是让各个特征站在同一起跑线上。

两种主流方式:

  • Z-score 标准化:(x - mean)/ std。结果均值0,标准差1。适合数据近似正态分布。
  • Min-Max 标准化:(x - min)/(max - min)。结果缩放到[0,1]。适合有明确边界的数据。

代码示例(sklearn):

from sklearn.preprocessing import StandardScaler, MinMaxScaler

# Z-score
scaler_z = StandardScaler()
df[['IC50', 'LogD']] = scaler_z.fit_transform(df[['IC50', 'LogD']])

# Min-Max
scaler_mm = MinMaxScaler()
df[['Solubility', 'Toxicity']] = scaler_mm.fit_transform(df[['Solubility', 'Toxicity']])
如果数据中存在离群值,Min-Max 会把正常值压缩到很小的区间。这时候用 Z-score 更稳健。我个人习惯先做异常值检测,再决定标准化方式。

两种方法对比:

方法输出范围对离群值保留分布形状
Z-score理论上无界相对稳健保留原始分布(仅平移缩放)
Min-Max[0, 1]受极端值影响大保留相对顺序,但压缩范围

三、异常值检测——揪出数据里的“捣蛋鬼”

有一次我帮客户分析一组靶点结合数据,图表上看有一个样本的活性值比其他样本高出两个数量级。团队差点把它当神药推进候选,后来发现是操作员加样时重复加了两倍……嗯,从那以后我再也不信一眼惊艳的数据点了。

常用检测手段:

  • 箱线图(IQR规则):Q1 - 1.5*IQR 和 Q3 + 1.5*IQR 之外即为异常。
  • Z-score法:|z| > 3 通常视为异常(样本量小时可用2.5)。
  • DBSCAN 聚类:密度低且孤立点判为异常。
  • 隔离森林(Isolation Forest):随机切割,快速识别离群点。

代码示范 IQR 检测:

Q1 = df['IC50'].quantile(0.25)
Q3 = df['IC50'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['IC50'] < lower_bound) | (df['IC50'] > upper_bound)]
print(f"检测到 {len(outliers)} 个异常值")
异常值不等于错误值!有些异常可能是新发现——比如某个化合物活性异常高,可能是新的骨架。先分析来源,再决定剔除还是保留。

四、完整处理流程

把上面三件事串起来,才是稳妥的预处理管线。我个人习惯的顺序:先处理缺失值 → 再检测并处理异常值 → 最后标准化。为什么异常值检测不能放在标准化之后?举个例:如果你先用 Min-Max 标准化,离群值会把正常数据压缩到边缘小范围,后续的异常检测反而失灵了。

下面这张图把整体流程画出来了,你在设计图表前可以照着走一遍。

化合物预处理流程 原始数据 缺失值处理 异常值检测 标准化 干净数据 注:缺失值与异常值检测可并行处理;标准化必须放在异常值处理后,避免离群值污染缩放参数。

走完这三步,你的数据才算真正“干净”。接下来画图表、做建模,心里才有底。下次我会跟你聊怎么选择合适的图表类型——但那是另一章的事了。