数据预处理:缺失值处理、标准化、归一化、数据划分

数据预处理这一步,说白了就是把原始数据洗干净,喂给模型之前先让它们「讲人话」。我做了这么久的ADMET预测,见过太多因为数据预处理没做好导致模型崩掉的案例。有时候结果不对,不是算法有问题,而是数据本身就有毛病。

ADMET特征工程关键步骤 数据预处理核心流程 缺失值处理 标准化 归一化 数据划分 常见方法: · 删除法(整行/整列) · 均值/中位数填充 · KNN/MICE插补 核心公式: Z = (X - μ) / σ 适用场景: 数据近似正态分布时 核心公式: X' = (X - X_min) / (X_max - X_min) 适用场景: 有界数据、需要保留分布形状 划分策略: · 随机划分(7:2:1) · 分层划分(保留类别比例) · 时序划分(时间序列) 关键原则:先划分,再归一化! 避免信息泄露,保证模型评估的公正性

缺失值处理:别让空数据砸了你的模型

原始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看着挺高实际上全在猜。后来用了分层采样,评估结果才靠谱。所以哪怕数据量再少,分层这一步不能省。

先划分,再归一化——这是铁律

这个错误我犯过,很多人也犯过。如果先对整个数据集做归一化,然后才划分训练集和测试集——这意味着测试集的信息早就被训练集知道了(因为归一化参数是全体数据算出来的)。

正确的做法是:

  1. 先把数据划分成训练集、验证集、测试集
  2. 只在训练集上计算归一化参数(均值和标准差,或最小值和最大值)
  3. 用训练集的参数去变换验证集和测试集
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数据的特性灵活处理。缺失值处理要看缺失机制,标准化和归一化要选对场景,数据划分必须分层且先划分后变换。这些基础工作做扎实了,后续的特征重要性分析才有意义。