第4章 数据预处理:缺失值处理与异常值处理

真实世界的信贷数据,从来不会像教科书那么干净。我做了这么多年信用评分,拿到新数据的第一件事不是建模,而是先看看数据里有多少「坑」。说白了,这一步做不好,后面花里胡哨的特征工程都是空中楼阁。

这一章咱们就聊聊两个最常碰到的「坑」——缺失值和异常值。怎么发现它们?怎么处理?哪些方法能用在信用评分上?我踩过的坑也一并说给你听。

4.1 缺失值处理

缺失值在信贷数据里太常见了。比如申请人的职业信息没填、房产评估价缺失、甚至部分还款记录丢失。为什么会这样?可能是系统字段设计问题,也可能是申请人主动不填。但不管什么原因,大多数模型不能直接吃缺失值。

4.1.1 先搞清楚缺失的类型

我个人习惯先分三类,处理方式完全不同:

缺失类型 含义 典型场景 处理建议
MCAR(完全随机缺失) 缺失与任何变量无关 录入时随机漏填 可直接删除或简单填充
MAR(随机缺失) 缺失依赖其他已知变量 高收入人群更可能不填收入 可用回归或模型预测填充
MNAR(非随机缺失) 缺失依赖缺失值本身 亏损客户故意不填负债 需引入外部信息或谨慎建模

嗯,你在实际项目中没法严格验证类型,但可以根据业务知识判断。比如「教育程度」缺失,往往集中在部分群体,这更像 MAR。

4.1.2 常用处理方法

我简单列几个常用做法,按推荐度排序:

  1. 直接删除——缺失占比很小(比如<5%)且随机时可用。但信用评分里样本很珍贵,一般不轻易删。
  2. 均值/中位数填充——最简单,但会降低方差,扭曲分布。我曾经在「收入」字段用均值填充,结果模型对高收入群体完全失效。
  3. 固定值填充——比如用-1或9999代表缺失。适合树模型,因为它能识别这个特殊值。
  4. 模型预测填充——用其他字段预测缺失值。比如用年龄、职业、学历来推算收入。效果好,但计算成本高。
  5. 多重插补——生成多组填充值,建模时取平均。学术界推崇,工程上用得少,因为太麻烦。
我的建议: 在信用评分里,优先用「固定值填充 + 缺失指示列」的组合。举个例子——对「收入」字段,缺失的填-1,同时生成一个二值列「收入是否缺失」。这样模型既学习了缺失本身的信息,又保留了分布特征。

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)

下面这张图帮你理清整体流程。从左到右:拿到原始数据后,先判定缺失率和异常可能性,分叉处理,最后合并成干净数据。

缺失值与异常值处理流程 原始数据集 缺失率 < 5% ? 是 否 删除 / 均值填充 固定值 + 缺失指示列 异常值检测(IQR/Z-score) 截断(Winsorize) 分箱 / 视为缺失 删除(仅明显错误) 干净可用的数据集 流程说明:先判断缺失严重程度,选择策略;再检测异常值,根据业务场景选择截断、分箱或删除。 注意:缺失指示列在后续特征工程中往往能提供额外信用风险信号。

4.4 实战避坑指南

我曾经犯过的错: 有一回做个人信贷评分模型,训练集里「月收入」字段有10%的缺失,我直接用了均值填充。结果模型上线后,对自由职业者群体(他们收入波动大且常不填收入)打分严重偏高,导致坏账率上升。后来我把缺失单独编码为-1并加缺失指示列,模型才稳定下来。

另外要注意:千万别在特征工程之前就做大规模异常删除。信用数据里,极端值往往对应高风险客户,删掉它们等于把最有价值的信号扔掉了。我建议先做缺失处理,再做异常截断,最后检查分布是否合理。

实用小建议: 如果时间紧张,可以直接用「固定值填充缺失 + Winsorize截断异常」这个组合拳。对大多数信用评分场景,这个组合能快速产出可用的干净数据,效果不差。

好了,数据预处理就聊到这儿。记住一句话:垃圾进,垃圾出。花80%的时间把数据弄干净,后面20%的建模工作会轻松很多。