数据预处理:缺失值处理、异常值处理、数据类型转换
数据预处理这一步,说白了就是给数据"洗澡"。我记得刚入行时接过一个信贷数据集,缺失率超过40%,我当时直接删了半张表……结果模型KS只有0.08,领导把我叫去谈了半小时。后来才明白,预处理不是简单的"删"或"填",每一步都得有道理,尤其是在信用评分卡里,特征分箱的质量很大程度上取决于预处理阶段的功夫。
下面这张图是我自己总结的预处理流程,你可以当个路线图用。每个环节做什么、怎么衔接,看完心里就有数了。
1. 缺失值处理:别急着删,先看缺失逻辑
信用评分卡里,缺失值太常见了。年龄、收入、工作年限……总有一些字段是空的。我个人习惯第一步先算缺失率,分三类对待:
- 缺失率 < 5%:直接删除样本,对整体影响不大。
- 缺失率 5% - 30%:用中位数或均值填充。收入这类偏态分布用中位数,年龄用均值。
- 缺失率 > 30%:别随便填了。我会把"是否缺失"单独做成一个二值特征,再配合模型预测填充。
我在项目里遇到过一个问题:一个"负债率"特征缺失率35%,我直接用均值填充,结果分箱后坏样本率曲线出现了奇怪的"驼峰"。后来排查发现,缺失的样本其实是"收入为0"的隐性失业人群,跟有收入的人群根本不是一回事。你想想看,这种场景下缺失本身就是信息。
# 缺失值处理典型代码
import pandas as pd
import numpy as np
# 1. 查看缺失率
missing_rate = df.isnull().mean()
print(missing_rate[missing_rate > 0])
# 2. 按缺失率分类处理
for col in missing_rate.index:
rate = missing_rate[col]
if rate < 0.05:
df.dropna(subset=[col], inplace=True)
elif rate < 0.30:
if df[col].skew() > 1: # 偏态分布用中位数
df[col].fillna(df[col].median(), inplace=True)
else:
df[col].fillna(df[col].mean(), inplace=True)
else:
# 高缺失率:生成缺失标记 + 模型预测填充
df[col + '_missing'] = df[col].isnull().astype(int)
# 后续可用简单模型预测填充,此处略
pass
2. 异常值处理:别让"极端值"带偏了分箱边界
异常值对分箱的影响,比缺失值更隐蔽。我之前做过一个项目,收入特征里有个值是980万,明显是录入错误。我没处理直接分箱,结果最后一箱的边界被拉到500万以上,整张评分卡就废了。
检测异常值,我常用这三种方法:
- IQR 规则:下界 Q1 - 1.5×IQR,上界 Q3 + 1.5×IQR。适用于偏态分布。
- Z-Score 方法:|Z| > 3 视为异常。适用于近似正态分布。
- 业务阈值:比如负债率不可能超过100%,年龄不可能超过120岁。这个最靠谱。
处理方式上,我倾向于"截尾"而不是"删除"。你想想看,删除样本会损失信息,尤其是信用评分卡这种正负样本本来就稀缺的场景。把异常值拉到上下边界,既保留了样本量,又不让极端值影响分箱。
# 异常值检测与截尾处理
def cap_outliers(df, col, method='iqr'):
if method == 'iqr':
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
elif method == 'zscore':
mean, std = df[col].mean(), df[col].std()
lower = mean - 3 * std
upper = mean + 3 * std
else:
# 业务阈值需手动传入
raise ValueError("请指定业务阈值")
df[col] = df[col].clip(lower=lower, upper=upper)
return df
# 使用示例
df = cap_outliers(df, 'income', method='iqr')
df = cap_outliers(df, 'age', method='zscore')
3. 数据类型转换:分箱前必须做好的"格式清理"
数据类型转换看起来简单,但最容易翻车。信用评分卡里的特征,来源五花八门:前端录入、后端数据库、第三方数据源……类型不一致是家常便饭。
我把常见的数据类型转换场景整理成了下面这个表,你直接对照着用:
| 原始类型 | 目标类型 | 转换方法 | 信用评分卡典型特征 |
|---|---|---|---|
| object(字符串数字) | float / int | pd.to_numeric() |
收入、年龄、负债率 |
| object(类别) | category | astype('category') + LabelEncoder |
职业、教育程度、婚姻状况 |
| object(日期) | datetime | pd.to_datetime() |
申请日期、逾期日期 |
| int(类别编码) | category | astype('category') |
性别编码、区域编码 |
这里有个我反复踩过的坑:日期格式不统一。有的系统用"2024-01-15",有的用"2024/01/15",还有用"20240115"的。直接用 pd.to_datetime() 会报错,必须先指定格式或做归一化。
# 数据类型转换示例
# 1. 字符串转数值
df['income'] = pd.to_numeric(df['income'], errors='coerce')
# 2. 类别编码
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['education'] = le.fit_transform(df['education'].astype(str))
# 3. 日期统一解析
df['apply_date'] = pd.to_datetime(df['apply_date'], format='%Y-%m-%d', errors='coerce')
# 4. 检查转换后的类型
print(df.dtypes)
关于数据类型转换,我想再叮嘱一句:转换完成后,务必用 df.info() 和 df.isnull().sum() 再检查一遍。因为转换过程中 errors='coerce' 会把解析失败的变成 NaN,如果你没注意,这些 NaN 就会在后续分箱时悄悄捣乱。
预处理做完,数据基本就"干净"了。下一步就是进入特征分箱环节——但那是后面几章的事。你现在只需要记住:预处理的质量,决定了分箱的天花板。我见过太多人急着分箱,结果被缺失值和异常值坑得反复返工。打好基础,后面才能走得快。