2. 数据预处理:缺失值分析与填充策略归纳总结

2.1 缺失值:信用评分里的"隐形刺客"

做信用评分模型,最烦人的不是模型选不对,而是数据本身就不干净。我刚入行那会儿,拿到一份某平台的借贷数据,心想"这不直接建模嘛"。结果一跑代码,报警了一堆NaN。后来才发现——缺失值如果不处理,模型会偷偷把"不知道"当"没有",后果很严重。

说白了,缺失值就是样本里某些字段没录进去。比如客户年龄没填、收入字段空着、或者历史违约次数干脆是null。这和零值完全不是一回事:零表示"有,且为零";缺失表示"不知道,没记录"。模型可不会自动区分这两者,你得帮它辨别。

为什么会缺失?我总结了三种常见来源:

  • 系统原因:数据库导出的bug、采集设备故障、数据接口中途断了。我在一个项目中遇到过,某合作方提供的"近半年逾期天数"字段,三分之一都是空——后来发现是他们内部系统升级漏写了日志。
  • 用户原因:客户不愿意填敏感信息(比如年收入、职业);或者填表时选项里没有适合自己的(比如"其他"类)。
  • 逻辑原因:某些字段只有在特定条件下才有意义。比如"失业时长"只对有工作经历的人才有值,未婚人士没有"配偶收入"。

核心原则:缺失值处理不是"填了就完事"。你得先弄清楚它为什么缺失,再去选方法。不同原因对应的处理策略完全不同。

2.2 缺失值分析的三个步骤

我个人习惯按这个流程来——不绕弯子,直接说:

  1. 统计每列的缺失率:用df.isnull().mean()看一眼,心里有个数。
  2. 判断缺失机制:完全随机缺失(MCAR)、随机缺失(MAR)、非随机缺失(MNAR)。
  3. 决定处理策略:删除、填补、或者作为独立特征引入模型。

第2步最难。说实话,完全随机缺失在真实数据里很少见。更多是"高收入人群更不愿意填收入"这种选择性缺失。你想想看,如果模型自动补了一个平均收入,而实际上缺失的都是高收入样本——那预测就会严重偏倚。

下面这张图我常用在培训课上,把缺失值分析的决策路径画清楚:

缺失值分析决策流程 获取原始数据 统计每列缺失率 > 5%? 判断缺失机制 MCAR / MAR / MNAR 选择处理策略 删除 / 填补 / 特征化 评估填补后分布变化 纳入模型或回退调整

2.3 常见填充方法对比

处理缺失值,说白了就两类思路:删掉,或者填上。但怎么删、怎么填,里面门道不少。我整理了一个表格,把最常用的几种方法拎出来对比:

方法 适用场景 优点 缺点 Python实现
直接删除(行/列) 缺失率 < 5% 且随机缺失 简单快速,不引入偏差 样本量骤减,可能丢失信息 df.dropna()
均值/中位数填充 连续变量,低缺失率 保持均值不变 缩小方差,扭曲分布 df.fillna(df.median())
众数填充 分类变量 保持众数频率 可能引入偏态 df['col'].fillna(df['col'].mode()[0])
KNN插补 变量间有线性关系 利用相似样本信息 计算量大,需调K值 from sklearn.impute import KNNImputer
回归/模型预测填充 缺失变量与其他变量强相关 预测精度高 容易过拟合,需交叉验证 自定义模型 + predict()
多重插补(MICE) MCAR或MAR,需统计推断 考虑不确定性,结果稳健 实现复杂,不适用于海量数据 from sklearn.experimental import enable_iterative_imputer

我的小习惯:对于信用评分场景,我一般先看缺失率。如果某字段缺失超过40%,我会直接丢弃,因为填补的风险太大。如果缺失率在5%~30%之间,我会先用中位数或KNN试一下,再用影子特征(即新增一个0/1列标记是否原本缺失)来观察模型对填补值的依赖程度。

2.4 避坑指南:这些坑我替你踩过

坑1:用全局均值填充分组结构明显的变量

我曾经处理过一份"月收入"数据,低收入客户缺失率低,高收入客户缺失率高。用全样本均值去填——结果低收入客户的收入被拉高了,模型训练出来居然判定"收入越高违约风险越大",完全反了。后来我改成按职业分组的均值填充,才纠正过来。

坑2:分类变量用众数填充后忘记做独热编码

嗯,这个错误我犯过一次。众数填充后字段里仍然有缺失值转成的"其他"类别,但后续的独热编码把"其他"当成了新的一列,模型理解错了语义。正确做法是先填充再编码,或者把缺失本身作为一个独立类别。

坑3:缺失特征作为"负向信号"直接丢弃

有时缺失本身就是一个信息。比如,客户没填工作单位,可能暗示他工作不稳定。我个人习惯在填补的同时,生成一个is_missing的0/1标志列,让模型自己学是否应该重视这个"缺失"信号。

特别注意:不要在测试集上使用训练集的统计数据以外的信息来填补。正确做法是:先把训练集和测试集分开,在训练集上计算填充值(均值、中位数等),然后应用到测试集。否则会造成数据泄露(data leakage),模型评估结果虚高,上真实环境就崩。

2.5 代码实战:快速实现缺失值分析

这部分我直接给核心代码,你跑一遍就能看到效果:

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split

# 假数据示例
np.random.seed(42)
df = pd.DataFrame({
    'age': np.random.randint(20, 60, 1000).astype(float),
    'income': np.random.normal(50000, 15000, 1000).astype(float),
    'default': np.random.choice([0,1], 1000)
})
# 人为制造缺失
df.loc[df.sample(200).index, 'income'] = np.nan
df.loc[df.sample(100).index, 'age'] = np.nan

# 第一步:缺失率概览
missing_ratio = df.isnull().mean() * 100
print("缺失率(%):\n", missing_ratio)

# 第二步:按是否缺失分组看分布差异
df['income_missing'] = df['income'].isnull().astype(int)
print("\n收入缺失 vs 非缺失的违约率对比:")
print(df.groupby('income_missing')['default'].mean())

# 第三步:训练集上计算中位数,填充
train, test = train_test_split(df, test_size=0.2, random_state=42)
income_median = train['income'].median()
train['income_filled'] = train['income'].fillna(income_median)
test['income_filled'] = test['income'].fillna(income_median)

# 检查填充后分布
print("\n填充后的收入统计:")
print(train['income_filled'].describe())

这段代码跑完后,你会看到收入缺失的样本违约率往往更高——这正是我之前踩过的那个坑的雏形。如果直接填充均值而不保留缺失标志,模型就会丢失这个重要信号。

2.6 归纳总结:适合信用评分的填充策略

信用评分数据通常缺失率不高,但一旦缺失往往带有信息。我最后给个推荐方案:

  • 连续变量:先用中位数填充(稳健),再生成is_missing标志。如果缺失率>20%,考虑KNN或回归插补。
  • 分类变量:把缺失本身当作一个类别(如UNKNOWN),不特意填充。信用评分里"不确定"本身就有风险含义。
  • 如果有时间序列:使用前向填充(forward fill),但要注意业务逻辑是否允许。
  • 验证流程:一定要在训练集上计算填充参数,测试集只应用,不重新计算。

做缺失值分析,其实是在和"信息不完整"打交道。别怕花时间。我做了几年项目,发现最耗时的不是调模型,而是理解数据为什么缺失。等你把这一步做扎实了,后续特征筛选和建模才会顺。