数据预处理:打好地基,才能建高楼
做化合物有效性评估,最怕什么?数据本身有问题。图表再漂亮,底层的数字是脏的,那就是「垃圾进,垃圾出」。我记得刚入行时接手过一个抗肿瘤项目,活性数据看起来挺漂亮,结果一复查——三分之一样本漏加溶剂,缺失值直接用了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']])
两种方法对比:
| 方法 | 输出范围 | 对离群值 | 保留分布形状 |
|---|---|---|---|
| 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 标准化,离群值会把正常数据压缩到边缘小范围,后续的异常检测反而失灵了。
下面这张图把整体流程画出来了,你在设计图表前可以照着走一遍。
走完这三步,你的数据才算真正“干净”。接下来画图表、做建模,心里才有底。下次我会跟你聊怎么选择合适的图表类型——但那是另一章的事了。