数据探索与预处理:缺失值处理、异常值检测、数据标准化、分层抽样
做信用评分模型,最难的不是算法选型,而是数据质量。我早年接手过一个汽车贷款评分项目,源系统里缺失率高达 30%,异常值更是五花八门。省掉预处理直接建模?好家伙,模型上线后拒绝率飙升,被业务方追着骂了一周。从那以后,我每到一个项目,第一件事就是盯着原始数据发呆——不是摸鱼,是在想哪些坑必须提前踩掉。
这一章咱们聊四个核心步骤:缺失值、异常值、标准化、分层抽样。它们就像房子的地基,地基歪了,柱子再漂亮也白搭。
1. 缺失值处理:别小看那些空单元格
信用数据里缺失很常见。比如借款人没填收入,或者系统根本没录职业信息。遇到缺失,我一般分三步走:先看比例,再看机制,最后选方法。
缺失机制要搞明白
- 完全随机缺失(MCAR):比如系统随机丢包,最简单,直接删除或均值填充都行。
- 随机缺失(MAR):缺失和观测数据有关,比如高收入人群更愿意填收入字段。这时用中位数填充会低估收入,我习惯用模型预测(KNN、随机森林)来填。
- 非随机缺失(MNAR):缺失本身就和目标变量相关。曾有项目里“已逾期”的客户故意不填负债项,这种场景千万别填充,得单独标一个“缺失”类别。
常用填充方法对比
| 方法 | 适用场景 | 缺点 |
|---|---|---|
| 直接删除行 | 缺失率低 (≤5%) | 样本量减少,可能丢失模式 |
| 均值/中位数填充 | 数值型、MCAR | 降低方差,影响相关性 |
| 众数填充 | 分类型 | 频繁值被强化,稀有类别消失 |
| KNN 插补 | MAR 数值型 | 计算量大,K 值难调 |
| 多重插补(MICE) | 通用,尤其 MNAR | 实现复杂,耗时 |
我的经验是:信用评分场景,中位数填充比均值更稳健——收入分布通常右偏,均值会被高收入拉高,中位数反而更贴近中间人群。看一眼代码:
import pandas as pd
from sklearn.impute import SimpleImputer
# 中位数填充收入字段
imputer = SimpleImputer(strategy='median')
data['income'] = imputer.fit_transform(data[['income']])
# 分类字段用众数
imputer_cat = SimpleImputer(strategy='most_frequent')
data['occupation'] = imputer_cat.fit_transform(data[['occupation']])
2. 异常值检测:揪出那些“离谱”的数字
信用数据里异常值可不少:有人月收入填了 9999999,有人年龄写 200 岁。这些要么是录入错误,要么是恶意干扰。我见过一个风控团队直接拿这些数据训练,结果模型把“9999999”当成优质客户——因为高收入群体违约率低。你说冤不冤?
我常用的三种检测方法
- IQR 四分位法:简单粗暴,适合单变量。Q1 - 1.5×IQR 到 Q3 + 1.5×IQR 之外就算异常。适合收入和负债这类右偏变量。
- Z-score 法:假设正态分布,均值 ±3σ 之外为异常。注意如果数据本身有偏,Z-score 会误判很多正常值。我用它前会先做对数变换。
- DBSCAN 聚类:能把离群点自动标出来,适合多维场景。但参数 eps 和 min_samples 要反复试,我一般在小样本上试好了再放大。
import numpy as np
from scipy import stats
# IQR 方法
def detect_outliers_iqr(data, column):
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return data[(data[column] < lower) | (data[column] > upper)]
# 检测到的异常值建议:先查业务原因,再决定删除还是截尾(winsorize)
outliers = detect_outliers_iqr(data, 'loan_amount')
print(f'发现 {len(outliers)} 条异常贷款额')
3. 数据标准化:把不同量纲放在一个坐标系里
信用模型里用到的特征,量纲差距能差出几个数量级。月收入几千,可用额度几十万,贷款期限几百个月。如果不做标准化,那些量级大的特征会在模型里“一言九鼎”。
我主要用两种:
- Min-Max 归一化:把数据缩放到 [0,1]。适合特征值有明确上下界的,比如年龄 18-100 岁。但注意:如果新样本超出边界,结果会奇怪。
- Z-score 标准化:变成均值为 0,标准差为 1。适合分布接近正态、且模型对异常值不敏感的情景(比如逻辑回归)。
| 方法 | 公式 | 适用模型 | 注意点 |
|---|---|---|---|
| Min-Max | (x - min) / (max - min) | 神经网络、KNN、SVM | 对异常值敏感 |
| Z-score | (x - μ) / σ | 线性回归、逻辑回归、PCA | 要求数据近似正态 |
| RobustScaler | (x - 中位数) / IQR | 含大量异常值的场景 | 树模型不需要标准化 |
我个人偏好:先画箱线图看分布,如果异常值多就用 RobustScaler,否则用 Z-score。注意,树模型(随机森林、XGBoost)不需要标准化,分箱后直接喂就行。
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# Z-score 标准化
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data[['income', 'loan_amount']])
# Min-Max 归一化(用于部分特征)
scaler_mm = MinMaxScaler()
data[['age']] = scaler_mm.fit_transform(data[['age']])
4. 分层抽样:保证训练集和测试集“长得像”
信用评分里,正负样本通常极不平衡:好客户占 90%,坏客户只有 10%。如果随机抽样,极少数测试集里可能一个坏客户都没有——模型评估结果虚高,上线立马翻车。
分层抽样的核心就是:按目标变量 y 的类别比例,分别从每一类中随机抽取。说白了,就是让训练集和测试集中好/坏客户的比例保持一致。
from sklearn.model_selection import train_test_split
X = data.drop('default', axis=1)
y = data['default']
# 按目标变量分层,测试集占 30%
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=42
)
# 检查比例是否一致
print(y_train.value_counts(normalize=True))
print(y_test.value_counts(normalize=True))
这里有个细节:如果某个类别样本太少(比如坏客户不到 100 条),分层时可能抽不到代表性样本。我建议此时用 交叉验证 + 少数类过采样(比如 SMOTE)来处理。
5. 数据预处理完整流程(一张图说明)
从上图可以看得很清楚:预处理是串行流水线,每个步骤依赖前序结果。我一般在代码里把它封装成一个 Pipeline,保证训练和测试流程完全一致。测试集只做变换不拟合参数,避免数据泄露——这个话题后面会细聊。
最后说一句:预处理没有万能药方。我每次拿到数据都会先跑一个快速 EDA,看看缺失分布、异常值位置、特征量级,再决定具体参数。你照着这个流程走一遍,至少能过滤掉 80% 的数据脏坑。剩下的 20%,咱们在后续特征工程和模型调优时再慢慢啃。