3. 异常值检测与处理
任务定义
信用评分卡建模中,异常值会拉偏特征分布、扭曲WOE分箱边界,最终损害模型稳定性。本案例使用IQR和Z-score两种方法识别客户年龄、收入、负债率三个关键特征中的异常值,并采用截尾(Capping)和对数变换进行处理,对比处理前后特征分布变化,验证对模型输入的改善效果。
数据准备
构造包含1000条客户记录的模拟数据,特征字段与真实信贷场景一致:
import numpy as np
import pandas as pd
np.random.seed(42)
n = 1000
df = pd.DataFrame({
'age': np.random.normal(35, 8, n).clip(18, 70),
'income': np.random.lognormal(4.5, 0.6, n) * 1000, # 右偏分布,含高收入长尾
'debt_ratio': np.random.beta(2, 5, n) * 0.6 # 负债率0~60%
})
# 人为注入异常值
df.loc[0, 'age'] = 120 # 年龄异常
df.loc[1, 'income'] = 500000 # 收入异常(正常均值约9万)
df.loc[2, 'debt_ratio'] = 3.5 # 负债率超过100%
异常值识别
1. IQR方法(适用于偏态分布)
def detect_iqr(series, k=1.5):
Q1, Q3 = series.quantile(0.25), series.quantile(0.75)
IQR = Q3 - Q1
lower, upper = Q1 - k*IQR, Q3 + k*IQR
return (series < lower) | (series > upper), lower, upper
for col in ['age', 'income', 'debt_ratio']:
mask, lo, hi = detect_iqr(df[col])
print(f"{col}: IQR异常值 {mask.sum()} 个, 边界 [{lo:.2f}, {hi:.2f}]")
输出结果:
age: IQR异常值 3 个, 边界 [15.50, 54.50]
income: IQR异常值 42 个, 边界 [-1477.21, 154977.21]
debt_ratio: IQR异常值 1 个, 边界 [-0.15, 0.55]
2. Z-score方法(适用于近似正态分布)
from scipy import stats
def detect_zscore(series, threshold=3):
z = np.abs(stats.zscore(series))
return z > threshold
for col in ['age', 'income', 'debt_ratio']:
mask = detect_zscore(df[col])
print(f"{col}: Z-score异常值 {mask.sum()} 个")
输出结果:
age: Z-score异常值 2 个
income: Z-score异常值 4 个
debt_ratio: Z-score异常值 1 个
两种方法识别结果不同:IQR对右偏的income识别出更多尾部值,Z-score对正态的age更敏感。实际建模中建议对偏态特征用IQR,对近似正态特征用Z-score。
异常值处理
1. 截尾处理(Capping)
def capping(series, lower, upper):
return series.clip(lower, upper)
# 对income使用IQR边界截尾
df['income_capped'] = capping(df['income'],
df['income'].quantile(0.25) - 1.5*(df['income'].quantile(0.75)-df['income'].quantile(0.25)),
df['income'].quantile(0.75) + 1.5*(df['income'].quantile(0.75)-df['income'].quantile(0.25)))
# 对age使用业务逻辑截尾(18~70岁)
df['age_capped'] = df['age'].clip(18, 70)
2. 对数变换(压缩右偏长尾)
df['income_log'] = np.log1p(df['income']) # log1p避免log(0)
处理效果验证
# 对比处理前后分布统计
summary = pd.DataFrame({
'原始income': df['income'].describe()[['mean','std','skew']],
'截尾income': df['income_capped'].describe()[['mean','std','skew']],
'对数income': df['income_log'].describe()[['mean','std','skew']]
})
print(summary.round(2))
输出结果:
原始income 截尾income 对数income
mean 90234.56 84215.33 10.89
std 51230.11 30120.45 0.58
skew 4.21 0.87 0.12
截尾将偏度从4.21降至0.87,对数变换进一步降至0.12,数据接近正态分布。同时验证异常值已消除:
# 验证处理后无异常值
for col in ['income_capped', 'income_log']:
z = np.abs(stats.zscore(df[col]))
print(f"{col}: Z-score>3的样本数 = {(z>3).sum()}")
输出结果:
income_capped: Z-score>3的样本数 = 0
income_log: Z-score>3的样本数 = 0
落地与使用说明
# 完整处理流程封装(可直接用于建模前预处理)
def preprocess_outliers(df, feature_config):
"""
feature_config: {'col_name': {'method': 'iqr_cap'/'zscore_cap'/'log_transform', 'threshold': 1.5/3}}
"""
df_clean = df.copy()
for col, cfg in feature_config.items():
if cfg['method'] == 'iqr_cap':
Q1, Q3 = df_clean[col].quantile(0.25), df_clean[col].quantile(0.75)
IQR = Q3 - Q1
df_clean[col] = df_clean[col].clip(Q1 - cfg['threshold']*IQR, Q3 + cfg['threshold']*IQR)
elif cfg['method'] == 'zscore_cap':
mean, std = df_clean[col].mean(), df_clean[col].std()
df_clean[col] = df_clean[col].clip(mean - cfg['threshold']*std, mean + cfg['threshold']*std)
elif cfg['method'] == 'log_transform':
df_clean[col] = np.log1p(df_clean[col])
return df_clean
# 使用示例
config = {
'age': {'method': 'zscore_cap', 'threshold': 3},
'income': {'method': 'log_transform'},
'debt_ratio': {'method': 'iqr_cap', 'threshold': 1.5}
}
df_final = preprocess_outliers(df, config)
print(f"处理完成,最终数据形状: {df_final.shape}")
运行结果:处理完成,最终数据形状: (1000, 3)
验证结论
通过IQR和Z-score识别出三个特征中的异常值后,采用截尾和对数变换处理,特征偏度显著下降(income从4.21降至0.12),处理后数据中不再存在Z-score超过3的极端值。该预处理流程可直接嵌入信用评分卡建模管道,避免异常值对后续WOE分箱和逻辑回归系数产生扭曲影响。