第4章 数据预处理:缺失值处理与异常值处理
真实世界的信贷数据,从来不会像教科书那么干净。我做了这么多年信用评分,拿到新数据的第一件事不是建模,而是先看看数据里有多少「坑」。说白了,这一步做不好,后面花里胡哨的特征工程都是空中楼阁。
这一章咱们就聊聊两个最常碰到的「坑」——缺失值和异常值。怎么发现它们?怎么处理?哪些方法能用在信用评分上?我踩过的坑也一并说给你听。
4.1 缺失值处理
缺失值在信贷数据里太常见了。比如申请人的职业信息没填、房产评估价缺失、甚至部分还款记录丢失。为什么会这样?可能是系统字段设计问题,也可能是申请人主动不填。但不管什么原因,大多数模型不能直接吃缺失值。
4.1.1 先搞清楚缺失的类型
我个人习惯先分三类,处理方式完全不同:
| 缺失类型 | 含义 | 典型场景 | 处理建议 |
|---|---|---|---|
| MCAR(完全随机缺失) | 缺失与任何变量无关 | 录入时随机漏填 | 可直接删除或简单填充 |
| MAR(随机缺失) | 缺失依赖其他已知变量 | 高收入人群更可能不填收入 | 可用回归或模型预测填充 |
| MNAR(非随机缺失) | 缺失依赖缺失值本身 | 亏损客户故意不填负债 | 需引入外部信息或谨慎建模 |
嗯,你在实际项目中没法严格验证类型,但可以根据业务知识判断。比如「教育程度」缺失,往往集中在部分群体,这更像 MAR。
4.1.2 常用处理方法
我简单列几个常用做法,按推荐度排序:
- 直接删除——缺失占比很小(比如<5%)且随机时可用。但信用评分里样本很珍贵,一般不轻易删。
- 均值/中位数填充——最简单,但会降低方差,扭曲分布。我曾经在「收入」字段用均值填充,结果模型对高收入群体完全失效。
- 固定值填充——比如用-1或9999代表缺失。适合树模型,因为它能识别这个特殊值。
- 模型预测填充——用其他字段预测缺失值。比如用年龄、职业、学历来推算收入。效果好,但计算成本高。
- 多重插补——生成多组填充值,建模时取平均。学术界推崇,工程上用得少,因为太麻烦。
4.1.3 代码示例:缺失值统计与填充
import pandas as pd
import numpy as np
# 假设 df 是信用数据
# 统计缺失比例
missing_ratio = df.isnull().mean().sort_values(ascending=False)
print('缺失率超过50%的字段:')
print(missing_ratio[missing_ratio > 0.5])
# 固定值填充 + 缺失指示列
def fill_with_missing_indicator(df, col, fill_value=-1):
df[col + '_miss'] = df[col].isnull().astype(int)
df[col].fillna(fill_value, inplace=True)
return df
# 对收入字段应用
df = fill_with_missing_indicator(df, 'income', fill_value=-1)
4.2 异常值处理
异常值在信贷数据里更危险。一个极端的收入值(比如年薪1亿)或一个离谱的年龄(比如200岁),可能直接把模型带偏。你想想看,逻辑回归对异常值非常敏感,树模型虽然鲁棒一些,但也会被分裂点影响。
4.2.1 怎么发现异常值?
我个人习惯三步走:
- 业务规则过滤: 年龄必须0~120,收入必须大于0,贷款金额不能超过房产价值。这是最可靠的。我曾经接到一个数据集,年龄字段有9999,明显是空值被错误编码了。
- 统计方法: 箱线图用IQR(四分位距)判断,或使用Z-score(假设正态分布)。信用数据往往偏态严重,Z-score不太合适,我更喜欢IQR。
- 可视化探查: 画直方图和箱线图,一眼就能看出尾巴上的离群点。
4.2.2 处理方式对比
| 方法 | 做法 | 适用场景 | 对模型的影响 |
|---|---|---|---|
| 截断(Winsorize) | 将极端值替换为分位数边界值 | 连续变量,分布偏态 | 保留样本量,降低极端影响 |
| 删除 | 直接剔除异常样本 | 明显录入错误,数量极少 | 样本减少,可能损失信息 |
| 分箱 | 将连续变量离散化 | 业务上不需要精确数值 | 损失精度,但提升稳定性 |
| 视为缺失 | 标为缺失,再用缺失处理 | 异常值可能是数据错误 | 需配合缺失处理策略 |
嗯,这里有个重要经验:信用评分里,异常值不一定是错误。比如一个客户有大量资产但突然违约,他的高收入可能就是真正的「异常但有效」。所以我建议你先做业务判断,再下刀。
4.2.3 代码示例:IQR截断处理
def winsorize_series(s, lower_quantile=0.01, upper_quantile=0.99):
lower = s.quantile(lower_quantile)
upper = s.quantile(upper_quantile)
return s.clip(lower, upper)
# 对'loan_amount'进行Winsorize处理
df['loan_amount_w'] = winsorize_series(df['loan_amount'])
# 也可以按分位数直接替换极端值
q99 = df['debt_ratio'].quantile(0.99)
df.loc[df['debt_ratio'] > q99, 'debt_ratio'] = q99
4.3 缺失值与异常值的处理流程(SVG)
下面这张图帮你理清整体流程。从左到右:拿到原始数据后,先判定缺失率和异常可能性,分叉处理,最后合并成干净数据。
4.4 实战避坑指南
我曾经犯过的错: 有一回做个人信贷评分模型,训练集里「月收入」字段有10%的缺失,我直接用了均值填充。结果模型上线后,对自由职业者群体(他们收入波动大且常不填收入)打分严重偏高,导致坏账率上升。后来我把缺失单独编码为-1并加缺失指示列,模型才稳定下来。
另外要注意:千万别在特征工程之前就做大规模异常删除。信用数据里,极端值往往对应高风险客户,删掉它们等于把最有价值的信号扔掉了。我建议先做缺失处理,再做异常截断,最后检查分布是否合理。
好了,数据预处理就聊到这儿。记住一句话:垃圾进,垃圾出。花80%的时间把数据弄干净,后面20%的建模工作会轻松很多。