数据预处理:缺失值处理、标准化、归一化、数据划分
数据预处理这一步,说白了就是把原始数据洗干净,喂给模型之前先让它们「讲人话」。我做了这么久的ADMET预测,见过太多因为数据预处理没做好导致模型崩掉的案例。有时候结果不对,不是算法有问题,而是数据本身就有毛病。
缺失值处理:别让空数据砸了你的模型
原始ADMET数据里,缺失值几乎避不开。实验条件不同、仪器故障、记录遗漏都会导致特征值缺失。我见过有人直接删掉所有含缺失的行——这样会把样本量削掉一大半,数据分布都变了。
核心原则:先看缺失比例,再决定处理策略。低于5%可以直接删除,5%-30%可以考虑填充,高于30%最好重新思考这个特征有没有用。
常用的缺失值处理方法有三类,我列在下面:
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| 删除法 | 缺失比例 < 5%,或随机缺失 | 可能会引入偏差,删除前先检查分布 |
| 均值/中位数填充 | 连续型特征,缺失比例不高 | 会降低特征方差,影响模型区分能力 |
| KNN插补 | 特征间有关系,缺失模式较复杂 | 计算量较大,但效果好于简单填充 |
我个人习惯先用missingno可视化缺失模式,再动手处理。你看代码:
import pandas as pd
import missingno as msno
import matplotlib.pyplot as plt
# 读数据
data = pd.read_csv('admet_data.csv')
# 看缺失情况
print(data.isnull().sum() / len(data) * 100)
# 可视化
msno.matrix(data)
plt.show()
我曾经吃过亏:有一次做CYP抑制预测,特征缺失率高达40%,我用均值填充后模型AUC直接降了0.15。后来发现这个特征缺失与实验批次强相关,不是随机缺失。所以填充前一定要搞清缺失机制。
标准化:让不同尺度的特征站上同一跑道
ADMET特征尺度差异太大了。分子量可能上千,logP只有几个点,而某些指纹特征值只有0或1。这些不同量纲的数据直接喂进模型,数值大的特征天然占优势,模型会跑偏。
标准化的本质就是让每个特征变成均值为0、标准差为1的分布。我在项目中经常这么写:
from sklearn.preprocessing import StandardScaler
# 初始化
scaler = StandardScaler()
# 只对数值型特征做标准化
numeric_cols = ['MW', 'logP', 'HBD', 'HBA', 'TPSA']
data[numeric_cols] = scaler.fit_transform(data[numeric_cols])
我的一个小建议:标准化适用于数据近似正态分布的情况。如果你发现特征值有明显的偏态,可以试试先做log变换再标准化。我在做口服吸收率预测时就遇到过这种情况—logP分布很漂亮,但分子量分布偏向大分子,先取log再标准化效果就好很多。
归一化:把数据压缩到[0,1]之间
归一化和标准化不一样。归一化是把数据缩放到一个固定区间,最常见的是[0,1]区间。公式很简单:X' = (X - min) / (max - min)。
你想想看,什么时候该用归一化?当特征有明确的上下界,而且你希望保留原始分布形状时。比如分子量一般就在0-1000之间,logP在-2到10之间——这种情况下归一化就很自然。
from sklearn.preprocessing import MinMaxScaler
# 初始化
minmax_scaler = MinMaxScaler()
# 归一化
data[numeric_cols] = minmax_scaler.fit_transform(data[numeric_cols])
标准化和归一化怎么选?直接看这张表:
| 对比维度 | 标准化 | 归一化 |
|---|---|---|
| 输出范围 | 无固定范围,多为[-3, 3] | 固定[0,1]或[-1,1] |
| 对异常值敏感性 | 较敏感(受均值和标准差影响) | 非常敏感(受极值影响极大) |
| 适合的模型 | SVM、PCA、线性回归 | 神经网络、KNN |
| 分布要求 | 近似正态分布 | 无严格要求 |
实际经验:我做ADMET预测时,如果特征来源复杂、分布各异,我就优先选择标准化。但如果数据直接来自实验测量范围(比如溶解度、渗透性这些有生理上下限的特征),归一化往往更直观。
数据划分:训练集、验证集、测试集
数据划分这一步很容易被忽视,但出了问题后面的结果全废。说白了,划分不科学就等于模型作弊——你拿测试集的信息去训练,得出的评估结果根本不可信。
from sklearn.model_selection import train_test_split
# 先分特征和标签
X = data.drop('CYP_inhibition', axis=1)
y = data['CYP_inhibition']
# 第一次切分:分出训练集和临时集
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# 第二次切分:从临时集中分出验证集和测试集
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
)
print(f'训练集: {X_train.shape}')
print(f'验证集: {X_val.shape}')
print(f'测试集: {X_test.shape}')
我个人习惯用7:1.5:1.5的比例。但要注意一个关键点:分类问题必须用分层采样。比如你的CYP抑制数据中,正样本只占20%,如果不分层,随机切分可能把正样本全切到测试集里,训练集全是负类,模型学个啥?
我曾经踩过的坑:有一次做hERG毒性预测,数据量只有几百条,我没注意分层就直接随机划分。结果验证集里只有2个阳性样本,模型评估时AUC看着挺高实际上全在猜。后来用了分层采样,评估结果才靠谱。所以哪怕数据量再少,分层这一步不能省。
先划分,再归一化——这是铁律
这个错误我犯过,很多人也犯过。如果先对整个数据集做归一化,然后才划分训练集和测试集——这意味着测试集的信息早就被训练集知道了(因为归一化参数是全体数据算出来的)。
正确的做法是:
- 先把数据划分成训练集、验证集、测试集
- 只在训练集上计算归一化参数(均值和标准差,或最小值和最大值)
- 用训练集的参数去变换验证集和测试集
from sklearn.preprocessing import StandardScaler
# 正确的做法
scaler = StandardScaler()
# 只在训练集上拟合
X_train_scaled = scaler.fit_transform(X_train)
# 用训练集的参数变换验证集和测试集
X_val_scaled = scaler.transform(X_val)
X_test_scaled = scaler.transform(X_test)
嗯,这一步看着简单,但真正在项目里严格执行的人不多。我记得有一次帮同事排查模型性能异常,翻代码发现他把标准化放在数据划分前面——修正之后模型表现正常了,还挨了他一顿感谢夜宵。
总结一下:数据预处理没有标准答案,需要根据ADMET数据的特性灵活处理。缺失值处理要看缺失机制,标准化和归一化要选对场景,数据划分必须分层且先划分后变换。这些基础工作做扎实了,后续的特征重要性分析才有意义。