4、特征工程:WOE编码、IV值计算、变量分箱
特征工程这步,我个人的经验是——它往往决定模型的天花板。算法调得再好,喂进去的变量如果是垃圾,出来也是垃圾。今天咱们就聊三个核心操作:WOE编码、IV值计算、变量分箱。这三兄弟在信用评分模型里天天见,少一个都不行。
一句话总结:分箱是把连续变量切成几段,WOE是把每段变成有业务含义的数字,IV则是告诉你这个变量到底有没有用。
4.1 变量分箱——把连续值砍成几段
为什么要分箱?说白了,信用模型里很少直接把年龄、收入这种连续值塞进去。一来模型容易过拟合,二来业务解释性差。你想想看,跟业务方说“年龄每增加一岁,风险下降0.03”,对方一脸懵。但你说“年龄在25岁以下,属于高风险段”,他就懂了。
我在项目中常用的分箱方法有这几种:
| 方法 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 等距分箱 | 按值范围平均切 | 简单 | 样本分布不均时容易出空箱 |
| 等频分箱 | 按样本量平均分 | 每箱样本均衡 | 可能把相近值切到不同箱 |
| 最优分箱 | 基于卡方或决策树 | 业务解释性强 | 计算量稍大 |
| 自定义分箱 | 按业务经验切 | 最符合业务逻辑 | 需要领域知识 |
我曾经踩过的坑:有一次做等频分箱,把贷款金额切成五等份。结果中间那箱包含了好几个极端值,导致该箱的坏账率异常高。后来我改成先用箱线图去掉极端值,再分箱——效果立马正常了。所以分箱前,一定要看一眼分布。
4.2 WOE编码——给变量赋予业务含义
分箱之后,每箱需要一个数值去代表它。直接用好坏样本的比例差?可以。但信用评分领域更流行用WOE(Weight of Evidence,证据权重)。公式是这样的:
WOE_i = ln( (好样本占比_i) / (坏样本占比_i) )
其中好样本占比 = 该箱好样本数 / 所有好样本数,坏样本占比同理。WOE为正,说明该箱好样本比例高于整体,风险低;WOE为负,说明风险高。这个方向跟业务直觉一致:分数越高越安全。
我个人习惯在分箱后马上计算WOE,然后检查单调性。如果WOE在整个变量上不是单调的(比如先升后降),说明分箱边界可能有问题,或者变量本身是非线性的。嗯,这时候就要回头调整分箱了。
4.3 IV值——衡量变量预测力
IV(Information Value,信息量)是WOE的加权求和。公式:
IV = Σ ( (好占比_i - 坏占比_i) * WOE_i )
IV越大,变量区分好坏的能力越强。业内通常这么看:
| IV范围 | 预测力 | 处理建议 |
|---|---|---|
| < 0.02 | 几乎无预测力 | 可考虑删除 |
| 0.02 – 0.10 | 较弱 | 保留但谨慎使用 |
| 0.10 – 0.30 | 中等 | 可用 |
| 0.30 – 0.50 | 较强 | 优先使用 |
| > 0.50 | 可疑(可能过强) | 检查是否泄露了标签 |
注意:IV高于0.5不代表变量好,反而可能是数据泄漏。比如“是否逾期90天”这个变量,在建模时已经包含了目标信息——这种变量一定不能用。我见过有人把“实际利率”当特征,结果IV高达0.7,后来发现是因为高风险客户被收了高利率,属于事后信息。所以IV高不一定就是宝。
4.4 完整流程:从原始变量到最终特征
下面这张图我把流程串起来了。从左到右,一步一步看。
4.5 实战中的避坑清单
光知道公式不够,落地才是硬道理。我整理了几条常踩的坑:
- 分箱后WOE不单调。 原因可能是分箱边界选错了,或者变量和好坏关系本来就不是线性的。我一般先用决策树分箱,再手动微调边界。
- IV极高但逻辑不通。 比如“近三个月查询次数”IV值0.6,查一下是不是跟逾期有后向关联——查询发生在逾期之前才算有效特征。
- WOE编码后的特征维度爆炸。 每个分箱变成一列?别。WOE编码本质上是把类别变量映射成一个数值,只生成一列就够了。多元WOE那种做法只在特定场景下用。
- 缺失值处理。 分箱前先看缺失率。缺失较少可以用单独一箱,缺失很多就用“缺失WOE”单独处理。我习惯把缺失作为一个独立箱,单独算WOE,不要硬插。
小技巧:计算IV时,如果某个分箱的好占比或坏占比为0,会导致WOE变成无穷。这时候可以加一个平滑项:好样本加0.5,坏样本加0.5,或者所有箱统一加一个极小值。我一般用“好样本数+0.5,坏样本数+0.5”的处理方式,稳定且不影响排序。
4.6 代码实现——快速上手
下面是我在项目中用过的一个分箱+WOE+IV计算的核心片段。代码用Python,纯手写不用现成库,方便理解原理:
import pandas as pd
import numpy as np
def woe_iv_calc(df, feature, target):
# 分箱(这里简化成等频分箱)
df['bin'] = pd.qcut(df[feature], q=5, duplicates='drop')
# 统计每箱好坏样本数
grouped = df.groupby('bin')[target].agg(['count', 'sum'])
grouped.columns = ['total', 'bad']
grouped['good'] = grouped['total'] - grouped['bad']
# 计算好占比、坏占比(加平滑)
total_good = grouped['good'].sum() + 0.5
total_bad = grouped['bad'].sum() + 0.5
grouped['good_pct'] = (grouped['good'] + 0.5) / total_good
grouped['bad_pct'] = (grouped['bad'] + 0.5) / total_bad
# WOE
grouped['woe'] = np.log(grouped['good_pct'] / grouped['bad_pct'])
# IV
grouped['iv'] = (grouped['good_pct'] - grouped['bad_pct']) * grouped['woe']
iv_total = grouped['iv'].sum()
return grouped[['total','bad','good','good_pct','bad_pct','woe','iv']], iv_total
实际用的时候,我建议把分箱逻辑单独抽成函数,方便调整分箱策略。代码里用了加平滑的方法,避免0除错。
记住:WOE编码后的特征可以直接喂给逻辑回归,但如果是树模型(XGBoost、LightGBM),WOE编码并不一定更好。树模型对原始值分箱或保留原值都行。我自己的经验是:逻辑回归一定要用WOE,树模型可以省掉这步,直接用分箱后的编号特征或原始值。