第4章 数据预处理:缺失值处理、标准化、异常值剔除
数据预处理这事儿,说简单也简单,说复杂能让你调一整天。我早年做药物性质预测时,就被缺失值坑过一次——模型训练时跑得好好的,一上线预测就崩。后来一查,原来是测试集里混了几个空值,模型直接罢工。
所以这章,我会把数据预处理的三板斧讲透。你跟着走一遍,后面调参会省很多力气。
4.1 缺失值处理
说实话,我见过太多人一上来就直接把包含 NaN 的行删掉。运气好还行,运气差的话,数据本身就少,再一删,训练集都快没了。我个人习惯是:先看一下缺失比例,再决定策略。
4.1.1 删除法
什么情况下可以直接删?
- 缺失比例超过 50% 的特征,我会直接弃用。留着你也没用,模型学不到有效信息。
- 缺失行数极少(比如少于 5%),删除后不影响整体分布。
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('admet_data.csv')
# 删除缺失比例超过50%的列
threshold = len(df) * 0.5
df = df.dropna(thresh=threshold, axis=1)
# 删除缺失行(比例极小的情况)
df = df.dropna()
4.1.2 填充法
如果缺失比例在 10%-40% 之间,我会倾向填充。怎么填?这取决于特征类型。
| 特征类型 | 常用填充策略 | 适用场景 |
|---|---|---|
| 连续值(如 LogP、分子量) | 中位数 / 均值填充 | 分布较对称用均值,有偏态用中位数 |
| 离散值(如类别标签) | 众数填充 | 保留类别分布 |
| 时间序列相关 | 前向填充 / 插值 | 随时间变化的数据 |
# 均值填充连续值
df['logP'] = df['logP'].fillna(df['logP'].mean())
# 中位数填充(抗异常值)
df['mol_weight'] = df['mol_weight'].fillna(df['mol_weight'].median())
# 众数填充离散值
df['solubility_class'] = df['solubility_class'].fillna(df['solubility_class'].mode()[0])
4.2 数据标准化
你想想看,SVR 模型里用到的核函数(RBF 核、多项式核),都依赖样本之间的距离计算。如果特征之间的量级差太多——比如分子量动辄几百,而 LogP 才几——模型会完全被大数值特征主导。说白了,标准化就是让所有特征站在同一起跑线上。
4.2.1 Z-score 标准化
这个方法我用的最多。它会把数据变成均值为 0、标准差为 1 的分布。适合数据本身接近正态分布的情况。
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
4.2.2 Min-Max 标准化
如果你的数据有明确的上下界(比如某些物化性质有理论范围),用 Min-Max 可能更合适。它会把所有值缩放到 [0, 1] 区间。
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
4.2.3 怎么选?
| 方法 | 优点 | 缺点 | 我的选择偏好 |
|---|---|---|---|
| Z-score | 不受异常值极端影响 | 不保证 [0,1] 区间 | 默认首选 |
| Min-Max | 保留原始分布形状 | 对异常值敏感 | 数据范围已知时用 |
我个人在 ADMET 预测中,90% 的情况用 Z-score。因为它对异常值的鲁棒性更好,尤其是后面还要做异常值剔除时,两步配合起来很顺手。
4.3 异常值剔除
为什么需要剔除异常值?SVR 模型里有个 epsilon 参数,它控制着容忍误差的范围。但如果异常值太多,模型会被带歪,epsilon 怎么调都白搭。我遇到过最夸张的一次,一个分子量 5000 的化合物混进了训练集——其他样本普遍是 200-500——结果模型预测出来的 LogP 全是负数,完全不能用。
4.3.1 IQR 方法
这是最简单粗暴的方法。基于四分位数,把超出 1.5 倍 IQR 范围的数据视为异常。
def remove_outliers_iqr(df, column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return df[(df[column] >= lower_bound) & (df[column] <= upper_bound)]
# 对多个特征逐个剔除
for col in ['logP', 'mol_weight', 'TPSA']:
df = remove_outliers_iqr(df, col)
4.3.2 Z-score 方法
设定一个阈值(通常 3 或 4),如果某个样本的 Z-score 绝对值超过阈值,就视为异常值。
from scipy import stats
z_scores = np.abs(stats.zscore(df[['logP', 'mol_weight', 'TPSA']]))
threshold = 3
df = df[(z_scores < threshold).all(axis=1)]
4.4 本章知识结构
下面这张图,建议你存下来。每次做数据预处理前,扫一眼,流程就不会乱。
4.5 完整预处理流水线
最后,我给你一个可以直接复用的完整代码模板。你可以根据自己的数据集,微调里面的参数。
from sklearn.preprocessing import StandardScaler
from scipy import stats
import pandas as pd
import numpy as np
def preprocess_pipeline(df, feature_cols, missing_thresh=0.5, z_score_thresh=3):
"""
完整预处理流水线
"""
# 1. 缺失值处理
# 删除缺失比例超过阈值的列
df = df.dropna(thresh=len(df) * (1 - missing_thresh), axis=1)
# 剩余缺失值用中位数填充
for col in df.columns:
if col in feature_cols:
df[col] = df[col].fillna(df[col].median())
# 2. 标准化
scaler = StandardScaler()
df[feature_cols] = scaler.fit_transform(df[feature_cols])
# 3. 异常值剔除
z_scores = np.abs(stats.zscore(df[feature_cols]))
df = df[(z_scores < z_score_thresh).all(axis=1)]
return df, scaler
# 使用
features = ['logP', 'mol_weight', 'TPSA', 'HBD', 'HBA']
df_clean, scaler = preprocess_pipeline(df, features)