4. 数据清洗实战:缺失值与异常值处理

在额度模型与评分卡构建中,原始数据几乎不可能完美无缺。缺失值会破坏模型训练的完整性,异常值则会扭曲统计分布、拉偏回归系数。本章聚焦两类核心清洗操作:缺失值填充与异常值检测截尾,并给出可直接复用的 Python 实现。

4.1 缺失值处理策略

缺失值处理没有银弹,需根据变量类型、缺失比例、业务含义选择合适方法。下表列出三种常用填充方式及其适用场景:

填充方法 适用变量类型 优点 缺点
均值填充 连续型(近似正态) 计算简单,保持均值不变 压缩方差,削弱相关性
中位数填充 连续型(偏态分布) 不受极端值影响 忽略分布形状信息
众数填充 离散型 / 分类型 保留类别分布 可能引入偏差
模型预测填充 任意类型 利用变量间关系,精度高 计算成本高,需避免过拟合
实操建议:缺失率低于 5% 时,均值/中位数填充足够;缺失率在 5%~20% 时,优先使用模型预测填充;缺失率超过 50% 的变量建议直接删除或构造“是否缺失”哑变量。

4.1.1 均值 / 中位数 / 众数填充代码

import pandas as pd
import numpy as np

# 示例数据
df = pd.DataFrame({
    'income': [5000, 6000, np.nan, 5500, 7000, np.nan, 4800],
    'age': [25, 30, 35, np.nan, 28, 32, np.nan],
    'education': ['本科', '硕士', '本科', '本科', '硕士', np.nan, '本科']
})

# 均值填充(income 近似正态)
df['income_filled_mean'] = df['income'].fillna(df['income'].mean())

# 中位数填充(age 可能有偏态)
df['age_filled_median'] = df['age'].fillna(df['age'].median())

# 众数填充(education 分类变量)
mode_edu = df['education'].mode()[0]
df['education_filled_mode'] = df['education'].fillna(mode_edu)

print(df[['income_filled_mean', 'age_filled_median', 'education_filled_mode']].head())

4.1.2 模型预测填充(KNN / 回归)

模型预测填充利用非缺失变量作为特征,预测缺失值。以 KNN 为例:

from sklearn.impute import KNNImputer

# 构造数值型矩阵(分类变量需先编码)
df_numeric = df[['income', 'age']].copy()

# KNN 填充
imputer = KNNImputer(n_neighbors=3)
df_imputed = imputer.fit_transform(df_numeric)

df['income_knn'] = df_imputed[:, 0]
df['age_knn'] = df_imputed[:, 1]

print(df[['income', 'income_knn', 'age', 'age_knn']])
避坑提醒:模型预测填充时,务必使用训练集拟合 imputer,再用同一 imputer 转换验证集和测试集,防止数据泄露。

4.2 异常值检测与截尾处理

异常值会严重干扰额度模型的稳定性。常用的检测方法有 IQR 法和 Z-score 法,处理方式通常为截尾(Winsorization)或删除。

4.2.1 IQR 法(四分位距法)

IQR 法基于箱线图原理,将超出 Q1 - 1.5×IQR 或 Q3 + 1.5×IQR 的值视为异常。该方法对偏态分布鲁棒性较好。

def iqr_capping(series, multiplier=1.5):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    return series.clip(lower=lower_bound, upper=upper_bound)

# 示例
df['income_capped_iqr'] = iqr_capping(df['income_knn'])
print('原始 income 描述:\n', df['income_knn'].describe())
print('截尾后 income 描述:\n', df['income_capped_iqr'].describe())

4.2.2 Z-score 法(标准差法)

Z-score 法假设数据近似正态分布,将 |Z| > 3 的样本标记为异常。该方法对极端值敏感,适合对称分布。

from scipy import stats

def zscore_capping(series, threshold=3):
    z_scores = np.abs(stats.zscore(series.dropna()))
    upper_bound = series.mean() + threshold * series.std()
    lower_bound = series.mean() - threshold * series.std()
    return series.clip(lower=lower_bound, upper=upper_bound)

df['income_capped_z'] = zscore_capping(df['income_knn'])
print('Z-score 截尾后范围:', df['income_capped_z'].min(), '~', df['income_capped_z'].max())
对比总结:IQR 法不依赖正态假设,适合金融风控中常见的偏态变量(如收入、负债比);Z-score 法适合对称变量(如年龄、信用分)。建议对每个连续变量分别绘制分布图后选择方法。

4.3 异常值检测流程总览

下图展示了从原始数据到清洗后数据的完整异常值处理链路:

异常值检测与截尾处理流程 原始数据 分布形态判断 IQR 法截尾 Z-score 法截尾 清洗后数据 偏态分布 → IQR 法;近似正态分布 → Z-score 法;截尾后需重新验证分布稳定性。

4.4 完整清洗流水线示例

将缺失值填充与异常值截尾组合为可复用的流水线函数:

def clean_pipeline(df, numeric_cols, cat_cols, outlier_method='iqr'):
    """
    综合清洗流水线
    - numeric_cols: 连续变量列表
    - cat_cols: 分类变量列表
    - outlier_method: 'iqr' 或 'zscore'
    """
    df_clean = df.copy()

    # 1. 分类变量众数填充
    for col in cat_cols:
        df_clean[col] = df_clean[col].fillna(df_clean[col].mode()[0])

    # 2. 连续变量中位数填充 + 异常值截尾
    for col in numeric_cols:
        # 中位数填充
        df_clean[col] = df_clean[col].fillna(df_clean[col].median())
        # 异常值截尾
        if outlier_method == 'iqr':
            df_clean[col] = iqr_capping(df_clean[col])
        else:
            df_clean[col] = zscore_capping(df_clean[col])

    return df_clean

# 使用示例
df_cleaned = clean_pipeline(df, numeric_cols=['income', 'age'], cat_cols=['education'])
print('清洗后数据形状:', df_cleaned.shape)
生产环境建议:将填充参数(均值/中位数/众数)和截尾阈值(1.5×IQR 或 3σ)保存为配置文件,便于模型上线后的一致性复现。