数据探索与预处理:缺失值处理、异常值检测、数据标准化、分层抽样

做信用评分模型,最难的不是算法选型,而是数据质量。我早年接手过一个汽车贷款评分项目,源系统里缺失率高达 30%,异常值更是五花八门。省掉预处理直接建模?好家伙,模型上线后拒绝率飙升,被业务方追着骂了一周。从那以后,我每到一个项目,第一件事就是盯着原始数据发呆——不是摸鱼,是在想哪些坑必须提前踩掉。

这一章咱们聊四个核心步骤:缺失值、异常值、标准化、分层抽样。它们就像房子的地基,地基歪了,柱子再漂亮也白搭。

1. 缺失值处理:别小看那些空单元格

信用数据里缺失很常见。比如借款人没填收入,或者系统根本没录职业信息。遇到缺失,我一般分三步走:先看比例,再看机制,最后选方法。

先看比例:缺失率 < 5% 直接删除行(简单粗暴,对样本量影响小);缺失率 5%-20% 用填充;超过 40%,我会建议特征直接砍掉——信息量太少,还容易引入偏差。

缺失机制要搞明白

  • 完全随机缺失(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']])
我的小技巧:填充后一定要加一列“是否原始缺失”的标签特征(0/1)。有时候缺失本身就是一种信号——比如拒填收入的人,还款概率可能更低。

2. 异常值检测:揪出那些“离谱”的数字

信用数据里异常值可不少:有人月收入填了 9999999,有人年龄写 200 岁。这些要么是录入错误,要么是恶意干扰。我见过一个风控团队直接拿这些数据训练,结果模型把“9999999”当成优质客户——因为高收入群体违约率低。你说冤不冤?

我常用的三种检测方法

  • IQR 四分位法:简单粗暴,适合单变量。Q1 - 1.5×IQR 到 Q3 + 1.5×IQR 之外就算异常。适合收入和负债这类右偏变量。
  • Z-score 法:假设正态分布,均值 ±3σ 之外为异常。注意如果数据本身有偏,Z-score 会误判很多正常值。我用它前会先做对数变换。
  • DBSCAN 聚类:能把离群点自动标出来,适合多维场景。但参数 eps 和 min_samples 要反复试,我一般在小样本上试好了再放大。
我曾经踩过的坑:在信用卡交易数据里用 Z-score 检测异常金额,结果把双十一大额消费全标成异常了。后来我改用“部门化”处理——先按客户分组,再在该组内用 IQR,才滤掉那些真正的欺诈交易。
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 的类别比例,分别从每一类中随机抽取。说白了,就是让训练集和测试集中好/坏客户的比例保持一致。

我常用的原则:训练集 70%-80%,测试集 20%-30%,并且分层时最好固定随机种子(random_state=42),这样结果可复现。业务方问起来,直接甩参数截图,省得扯皮。
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,保证训练和测试流程完全一致。测试集只做变换不拟合参数,避免数据泄露——这个话题后面会细聊。

避坑指南:我曾经把标准化器的参数在全体数据上fit了,然后切分训练集测试集。结果测试集“偷看”了全集的均值和方差,模型得分虚高一大截,上线后直接崩盘。切记:先切分,再fit。

最后说一句:预处理没有万能药方。我每次拿到数据都会先跑一个快速 EDA,看看缺失分布、异常值位置、特征量级,再决定具体参数。你照着这个流程走一遍,至少能过滤掉 80% 的数据脏坑。剩下的 20%,咱们在后续特征工程和模型调优时再慢慢啃。