第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])
注意:千万别用均值去填充离散特征。我有一次图省事,把 "CYP_inhibitor" 这种 0/1 变量用均值填了,结果模型出来后,预测值全是小数,根本没法解释。

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)]
我常用的流程:先用 IQR 粗筛一遍,再用 Z-score 做二次确认。两个方法同时标记为异常的,我才删。为什么?因为有些特征本身分布就偏,单用 IQR 可能会误删。

4.4 本章知识结构

下面这张图,建议你存下来。每次做数据预处理前,扫一眼,流程就不会乱。

数据预处理流程 原始数据 缺失值处理 删除法(缺失 > 50%) 填充法(均值/中位数/众数) 数据标准化 Z-score 标准化 Min-Max 标准化 异常值剔除 干净数据集 建议:按流程依次执行,每步骤后检查特征分布,避免累积误差

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)
避坑提示:我曾经把标准化和异常值剔除的顺序搞反过——先剔再标准化。结果标准化后的分布变了,原本算好的 Z-score 阈值全都不对。正确的顺序是:先标准化、再剔除。你记住就行。