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 异常值检测流程总览
下图展示了从原始数据到清洗后数据的完整异常值处理链路:
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σ)保存为配置文件,便于模型上线后的一致性复现。