3. 数据清洗:缺失值处理
数据清洗这件事,其实是整个特征工程里最枯燥、但又最关键的环节。我做了这么多年信用评分项目,发现一个规律:模型好不好,往往不取决于算法多高级,而取决于你数据修得干不干净。说得直白点,缺失值处理不好,后面再怎么调参都是白搭。
3.1 先摸清家底——统计缺失率
拿到一份原始数据,我的第一个动作不是跑模型,而是跑一个缺失率统计。你想想看,如果你的收入字段有 80% 是空的,还拿它做特征,那不是自己骗自己吗?
一般我会按字段粒度统计缺失比例,然后分类处理:
| 缺失率范围 | 处理策略 | 我的个人经验 |
|---|---|---|
| < 5% | 直接填均值/中位数,或者直接删行 | 信用评分样本量通常大,删几行无所谓,我一般直接删 |
| 5% ~ 30% | 考虑均值/中位数/众数填充 | 这里是填充策略的主要战场,下面会细说 |
| 30% ~ 60% | 模型预测填充,或做"是否缺失"二值特征 | 我曾经踩过坑,30%以上用简单填充会把分布搞崩 |
| > 60% | 基本可以放弃该字段 | 除非是强业务特征,否则我直接丢弃 |
统计缺失率的代码很简单,但效果很好:
import pandas as pd
def missing_rate(df):
miss = df.isnull().sum()
miss_pct = (miss / len(df)) * 100
miss_df = pd.DataFrame({'缺失数': miss, '缺失率%': miss_pct})
return miss_df[miss_df['缺失数'] > 0].sort_values('缺失率%', ascending=False)
# 用法示例
# miss_report = missing_rate(credit_data)
# print(miss_report)
3.2 简单填充:均值、中位数、众数
做信用评分时,很多字段是连续型的,比如年收入、负债比、征信查询次数。对这些字段,我一般这样选填充策略:
- 均值填充:适合正态分布的数据。但信用评分的收入往往右偏,用均值会被高收入拉偏。我很少直接用均值填充收入,除非数据集非常大。
- 中位数填充:我个人的首选。它对异常值不敏感,特别适合信用评分这种长尾分布。我曾经对比过,用中位数填充比均值填充在AUC上能高出0.02~0.03。
- 众数填充:仅用于类别型字段,比如"住房类型""教育程度"。这一点要注意——众数填充可能导致某个类别占比过高,所以我会加一个"缺失是否"标识。
看代码:
# 中位数填充连续字段
credit_data['年收入'].fillna(credit_data['年收入'].median(), inplace=True)
# 众数填充类别字段
credit_data['住房类型'].fillna(credit_data['住房类型'].mode()[0], inplace=True)
嗯,这里要注意,mode() 返回的是 Series,一定要取第一个值,否则会报错。我之前就忘记取 [0],结果填充进去一个 Series,排查了好久。
3.3 模型预测填充——当简单方法不够用时
当某个特征的缺失率超过 30% 时,简单填充会严重扭曲数据分布。这时候我建议用模型预测填充。说白了,就是把缺失的字段作为目标变量,用其他完整字段来预测它。
我在项目中常用的做法:
- 把原始数据分成两份:一份无缺失(训练集),一份有缺失(预测集)。
- 用无缺失部分训练一个回归模型(常用随机森林,因为它不用太多调参)。
- 对缺失部分做预测,把预测值填进去。
当然,这个方法的代价是计算量大。如果你的样本量上百万,用随机森林可能会跑很久。我会退而求其次,用 KNN Imputer 或者 Iterative Imputer(scikit-learn 里都有)。
代码示例:
from sklearn.ensemble import RandomForestRegressor
# 分离数据
train = credit_data[credit_data['负债比'].notna()]
predict = credit_data[credit_data['负债比'].isna()]
# 选择特征(排除目标列)
features = ['年收入', '年龄', '征信查询次数']
X_train = train[features]
y_train = train['负债比']
X_predict = predict[features]
# 训练并预测
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
predicted_values = model.predict(X_predict)
# 填入缺失位置
credit_data.loc[credit_data['负债比'].isna(), '负债比'] = predicted_values
3.4 整体流程图
下面这张图总结了我处理缺失值的完整决策路径,你可以把它贴在工位上,每次遇到缺失值照着走一遍:
3.5 别忘了标记——"缺失"本身也是信息
最后,我想跟你分享一个我自己的经验。很多人做完填充就完事了,其实缺失本身有时候比填充值更有价值。在信用评分的场景里,某个字段缺失往往暗示着这个人的信息不完整,可能风险偏高。
我个人习惯:无论用什么方式填充,我都会额外生成一个二值特征,比如 年收入_缺失标志,缺失时为1,否则为0。这个标志在某些模型(尤其是树模型)里经常被选中作为重要特征。
代码实现:
# 为每个有缺失的字段生成标志
for col in ['年收入', '负债比', '住房类型']:
credit_data[f'{col}_缺失标志'] = credit_data[col].isna().astype(int)
好了,关于缺失值处理,核心就这么几招。你先按缺失率分档,再选合适的填充策略,千万别忘了做缺失标志。把这一步做好,后面的特征工程才能站得住脚。