从银行贷款审批实战出发:5分钟搞懂机器学习里的数据清洗(缺失值处理篇)
从银行贷款审批实战出发:5分钟搞懂机器学习里的数据清洗(缺失值处理篇)
银行风控建模的第一步,往往不是选择最炫酷的算法,而是处理那些让人头疼的缺失值。想象一下,你刚拿到一份银行贷款审批数据,准备构建预测模型时,却发现"年收入"一栏有15%的记录是空的——这就像厨师准备做菜时发现食材缺斤少两。本文将带你从银行业务视角,重新审视那些教科书式的填充方法。
1. 为什么银行数据清洗如此特殊?
在电商推荐系统中填充缺失值,和在银行贷款审批中处理缺失值,本质上是两种完全不同的游戏。前者可能影响用户体验,后者直接关系到金融公平与风险控制。
银行数据最显著的特征是 强监管属性 和 高价值密度 。每个缺失值背后可能隐藏着:
- 客户故意隐瞒的高负债情况
- 数据采集时的系统漏洞
- 敏感信息的合规性过滤
以常见的收入字段缺失为例,直接使用均值填充可能导致:
- 低估高净值客户的实际风险
- 虚增低收入群体的信用评分
- 扭曲不同职业群体的收入分布
# 银行数据典型特征检查代码示例
def check_missing_pattern(df):
missing_report = {
'column': [],
'missing_rate': [],
'corr_with_approval': [] # 缺失是否与审批结果相关
}
for col in df.columns:
missing_rate = df[col].isna().mean()
# 检查缺失是否与贷款审批结果系统性相关
corr = df[col].isna().astype(int).corr(df['y'])
missing_report['column'].append(col)
missing_report['missing_rate'].append(missing_rate)
missing_report['corr_with_approval'].append(corr)
return pd.DataFrame(missing_report)
提示:在金融领域,缺失本身可能就是重要特征。某些客户群体可能更倾向于隐瞒真实财务状况。
2. 数值型字段的处理艺术:超越简单均值
当面对x1-x6这类数值型变量(如收入、负债比、资产总值)时,专业数据分析师会考虑以下填充策略:
| 策略 | 适用场景 | 银行业务风险 | 实现代码示例 |
|---|---|---|---|
| 分位数填充 | 数据存在极端值时 | 避免异常值影响 |
df['income'].fillna(df['income'].quantile(0.3))
|
| 条件均值 | 字段间存在明显相关性 | 保持业务逻辑一致 |
df['income'] = df.groupby('job_title')['income'].transform(lambda x: x.fillna(x.mean()))
|
| 预测填充 | 高价值字段且缺失率低 | 计算成本较高但精确 | 使用随机森林回归预测缺失值 |
| 标记+中位数 | 缺失可能有特殊含义 | 保留缺失模式信息 |
df['income_missing'] = df['income'].isna()
df['income'] = df['income'].fillna(df['income'].median())
|
负债收入比(DTI)的典型处理流程:
- 计算非缺失客户的DTI分布百分位
- 分析缺失DTI客户的其他特征(职业、资产等)
- 根据客户分组的中位数进行有条件填充
- 添加缺失标记作为新特征
# 基于业务规则的条件填充示例
def fill_dti_smart(df):
# 第一步:标记原始缺失情况
df['dti_missing'] = df['dti'].isna().astype(int)
# 第二步:按职业和资产分组填充
fill_values = df.groupby(['job_category', 'has_mortgage'])['dti'].median()
df['dti'] = df.apply(
lambda row: fill_values.loc[row['job_category'], row['has_mortgage']]
if pd.isna(row['dti']) else row['dti'],
axis=1
)
return df
3. 分类变量的智能处理:当众数不够用时
处理x7-x15这类分类变量(如职业、教育程度、居住城市)时,情况变得更加复杂。教育水平"未知"和"高中以下"在风控模型中的含义可能截然不同。
高级处理技巧包括:
- 业务映射填充 :将缺失的职业映射为"自由职业"而非简单的众数
- 概率抽样填充 :按现有分布概率随机填充,保持整体分布
- 构建"缺失"类别 :特别是当缺失率超过5%时
- 多层分组填充 :先按城市分组,再按收入水平填充教育程度
教育程度填充的典型错误与正确做法对比:
# 不推荐 - 简单使用全局众数
df['education'].fillna(df['education'].mode()[0], inplace=True)
# 推荐 - 考虑年龄分组的众数
age_bins = [20, 30, 40, 50, 60]
df['age_group'] = pd.cut(df['age'], bins=age_bins)
fill_values = df.groupby('age_group')['education'].agg(lambda x: x.mode()[0])
df['education'] = df.apply(
lambda row: fill_values[row['age_group']]
if pd.isna(row['education']) else row['education'],
axis=1
)
注意:对于婚姻状况等敏感字段,法律可能限制特定填充方式。在某些地区,不能将缺失的婚姻状况默认填充为"已婚"。
4. 从数据清洗到模型公平性:一个完整的案例
某城商行在消费贷审批模型中,最初对所有缺失的信用卡数量字段填充为0(假设没有信用卡)。上线后发现:
- 年轻客户群体审批通过率异常升高
- 农村地区客户被系统性低估信用
- 模型无法识别"无信用卡记录"与"确实没有信用卡"的区别
改进后的处理流程:
graph TD
A[原始数据] --> B{信用卡字段缺失?}
B -->|是| C[检查其他银行产品持有情况]
B -->|否| D[保留原值]
C --> E{有储蓄账户?}
E -->|是| F[填充为同年龄段中位数]
E -->|否| G[标记为"新客户"特殊类别]
F --> H[添加"was_missing"标记]
G --> H
D --> H
调整后的模型效果对比:
| 指标 | 原始填充方式 | 优化后方案 |
|---|---|---|
| 年轻客户逾期率 | 4.2% | 3.1% |
| 农村客户通过率 | 58% | 67% |
| 模型AUC | 0.72 | 0.78 |
| 客户投诉量 | 23件/月 | 7件/月 |
这个案例揭示了一个关键认知: 数据清洗不是单纯的数学问题,而是业务逻辑的编码过程 。银行风控模型中的每个填充决策,都应该能够通过"业务合理性测试"——即能够向监管机构合理解释为什么这样处理是公平且审慎的。
5. 实战工具箱:Python中的进阶技巧
超越简单的Scikit-learn Imputer,现代Python生态系统提供了更灵活的工具:
Pandas进阶填充方法:
# 基于时间序列的填充(适合信用卡还款记录)
df['payment_history'] = df.groupby('customer_id')['payment_history'].ffill()
# 使用插值法处理有序分类变量
df['credit_rank'] = df['credit_rank'].interpolate(method='polynomial', order=2)
# 多重填充(高级技巧)
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imp = IterativeImputer(max_iter=10, random_state=0)
df[['income', 'assets']] = imp.fit_transform(df[['income', 'assets']])
处理特殊业务场景的函数封装:
def financial_imputer(df, num_strategy='median', cat_strategy='business_rules'):
"""
针对金融数据特点的智能填充器
参数:
num_strategy: 数值型字段策略 ('median', 'grouped_mean', 'quantile')
cat_strategy: 分类型字段策略 ('business_rules', 'probabilistic')
"""
# 数值型处理
if num_strategy == 'grouped_mean':
for col in NUMERIC_COLS:
df[col] = df.groupby('occupation')[col].transform(
lambda x: x.fillna(x.mean()))
# 分类变量处理
if cat_strategy == 'business_rules':
df['education'] = df.apply(fill_education_by_age, axis=1)
df['marital_status'] = df['marital_status'].fillna('Unknown')
# 添加缺失标记
for col in df.columns:
if df[col].isna().sum() > 0:
df[f'{col}_was_missing'] = df[col].isna()
return df
在Jupyter Notebook中进行填充效果验证的实用代码块:
# 填充前后分布对比可视化
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
df['income'].plot(kind='kde', ax=axes[0], title='Before Imputation')
df_filled['income'].plot(kind='kde', ax=axes[1], title='After Imputation')
for ax in axes:
ax.set_xlabel('Income')
plt.tight_layout()
plt.show()
# 填充值与实际值差异分析
mask = df_original['income'].notna() & df_filled['income_was_missing']
errors = df_original.loc[mask, 'income'] - df_filled.loc[mask, 'income']
print(f"平均填充误差: {errors.abs().mean():.2f}")
print(f"最大高估: {errors.max():.2f}")
print(f"最大低估: {errors.min():.2f}")
数据清洗的质量直接影响模型效果。在一次实际项目中,经过优化的填充策略使KS值从0.32提升到了0.41,而整个过程中使用的仍然是相同的XGBoost算法和特征工程流程。这印证了业界常说的那句话:"垃圾进,垃圾出"——再先进的算法也无法弥补低质量的数据准备。
更多推荐
所有评论(0)