ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WOE与IV在风控建模中的应用:Python实现与特征筛选实战指南

WOE与IV在风控建模中的应用:Python实现与特征筛选实战指南 做风控建模的同学对 WOEWeight of Evidence证据权重和 IVInformation Value信息价值这两个词应该都不陌生。只要是做评分卡、申请评分模型或者行为评分模型的特征筛选环节里基本都绕不开这对组合。我最早接触这两个指标是在做信贷申请评分卡的时候当时师傅丢给我一份变量清单和一堆分箱脚本让我自己算 WOE、IV再用 IV 排序挑变量。说实话刚开始只是照葫芦画瓢后来踩了不少坑才真正搞明白这两个指标背后的逻辑和使用边界。这篇文章不整虚的直接进入正题。我会先用通俗的方式讲清楚 WOE 和 IV 的原理再给出可直接复制的完整 Python 实现代码然后用一个真实的案例数据演示从分箱到 IV 排序的完整流程最后把我在实际项目中遇到的坑和排查思路整理出来。无论你是刚入门风控建模的新人还是已经在用但没深究过原理的从业者这篇文章都值得收藏慢慢看。1. 为什么特征筛选要用WOE和IV从分箱说起1.1 分类模型中的坏样本占比逻辑先抛一个问题在信用风控场景中我们最关心的是什么是借款人在未来一段时间内会不会逾期。模型的目标是预测这个概率所以我们在建模时会把样本划分为好样本和坏样本比如逾期超过30天定义为坏样本然后用各种特征去拟合这个二分类目标。这时候就出现了一个非常朴素的问题某个特征的不同取值区间对应的坏样本比例是怎么变化的这个比例就是坏样本占比也叫风险浓度。举个容易理解的例子年龄这个特征如果 18-22 岁的坏账率是 8%23-30 岁是 4%31-45 岁是 2%46 岁以上是 3%那么这个特征就具备一定的区分能力——因为它能帮我们看出风险在哪些区间更高、在哪些区间更低。WOE 做的事情本质上就是把这种风险浓度差异量化为一个数值。它通过对比每个分组中好样本分布占比和坏样本分布占比的差异来回答一个问题这一组样本相对于整体水平来说是更安全还是更危险所以 WOE 不是凭空发明的数学工具它是对业务直觉的精确表达。1.2 WOE的计算公式与直觉理解WOE 的数学定义是WOE_i ln( 组内好样本占比 / 组内坏样本占比 )其中组内好样本占比 该组好样本数 / 全部好样本数组内坏样本占比 该组坏样本数 / 全部坏样本数。注意这里用的是分布占比而不是组内的好坏比例这一点非常关键。为什么因为我们想要的是每个分组在整个样本中的相对集中程度用它来消除样本量差异带来的影响。从小到大理解一下如果 WOE 为正说明组内好样本占比大于坏样本占比即这个组的风险低于整体平均水平如果 WOE 为负说明坏样本比好样本更集中在这个组即这个组风险偏高如果 WOE 接近 0那这个组基本没有区分能力。注意一个细节不同的项目里有人把分子分母颠倒写有人把好样本定义为 1。这是习惯差异不影响本质只要保证解读方向一致即可。在我的代码里统一用好样本在上的写法WOE 为正代表低风险组。你在参考别人的代码时一定要先看清楚定义否则解读方向就会反过来导致筛选结果完全错误。1.3 IV的聚合公式与判定标准单个分组的 WOE 只能说明该组的好坏分布情况但我们要判断整个特征的预测能力就需要把各分组的信息量聚合起来。IVInformation Value的计算公式是IV Σ( 组内好样本占比 - 组内坏样本占比 ) × WOE_i所以 IV 实际上是每个分组上 WOE 的加权求和权重是好样本占比与坏样本占比的差值。这个差值越大、WOE 绝对值越大该组对 IV 的贡献就越大。换句话说IV 衡量的是这个特征在多大程度上能把好样本和坏样本区分开。在实际建模中我们一般用以下经验阈值来判断特征强弱IV范围预测能力处理建议小于 0.02基本无预测力直接剔除0.02 - 0.1弱预测力视情况保留或剔除0.1 - 0.3中等预测力优先保留0.3 - 0.5强预测力必须保留大于 0.5异常高警惕过拟合或变量泄露需核实提示IV 大于 0.5 不一定是好事。我在实际项目中遇到过 IV 高达 0.8 的变量一查发现是数据泄露——比如用了前向未来信息、用了客户还款后的结果反推或者变量本身与目标变量存在直接映射关系。IV 过高时要先做业务逻辑校验。2. 完整代码实现从分箱到WOE/IV计算2.1 数据准备与预处理先把流程跑通再来谈优化。下面这份代码是我在项目中反复使用的函数集经过多次迭代兼顾了可读性和稳定性。首先是导入必要的库和构造示例数据。这里我用 sklearn 的 make_classification 生成一组模拟数据包含 1 个目标变量 y以及 5 个数值型特征和 2 个类别型特征。实际项目中你替换成自己的 DataFrame 即可。import pandas as pd import numpy as np from sklearn.datasets import make_classification # 构造模拟数据5000个样本5个有信息量的数值特征2个冗余特征 X, y make_classification( n_samples5000, n_features7, n_informative5, n_redundant2, n_clusters_per_class1, flip_y0.03, random_state42 ) df pd.DataFrame(X, columns[ffeat_{i} for i in range(7)]) df[target] y # 查看好坏样本比例 print(坏样本占比: {:.2%}.format(df[target].mean())) print(形状:, df.shape)预处理阶段有一个容易忽略的点先检查缺失值。WOE/IV 计算本身依赖分组统计如果有缺失值直接 drop 掉会损失信息也会影响后续计算。实际项目中我一般把缺失值单独作为一个分组或者用 -999 填充后再分箱。这样做的原因是在风控场景中缺失本身往往就带有信息比如用户在某个字段上拒绝填写可能意味着有隐藏风险。# 检查缺失 print(df.isnull().sum())2.2 等频分箱与等距分箱实现分箱是 WOE/IV 计算的前置步骤也是整个流程中最需要经验和业务判断的环节。所谓分箱就是把连续特征切分成若干个区间每个区间作为一个分组。常见的方法是等频分箱每个分组样本量大致相等和等距分箱每个分组数值范围相同。等频分箱用 pd.qcut等距分箱用 pd.cut。两种方法在直观上都好理解qcut 是按照分位数切保证每个箱子里样本数量相近cut 是按照数值的等宽区间切。在大多数时候qcut 更稳定因为它能避免某个箱子里样本太少而导致统计不稳定。但遇到偏态分布特别严重的特征qcut 可能切出大量重复边界这时候得手动调整分箱数或转用等距分箱。# 等频分箱示例分成10箱 df[age_bin] pd.qcut(df[feat_0], q10, duplicatesdrop) # 等距分箱示例分成10段 df[age_bin_cut] pd.cut(df[feat_0], bins10) print(df.groupby(age_bin, observedTrue)[target].agg([count, mean]))2.3 WOE/IV计算核心函数接下来是核心代码。我把分箱和 WOE/IV 计算封装成一个函数方便对批量特征做循环筛选。函数内部做了几件事分箱、统计分组好坏数量、计算分布占比、计算 WOE 和 IV。同时加了一个平滑参数避免出现分母为 0 导致无穷大的情况。def calc_woe_iv(df, col, target, bins10, methodqcut, smooth0.5): 计算单特征的WOE和IV值 参数: df: DataFrame包含特征列和目标列 col: str特征列名 target: str目标列名二分类取值为0和1 bins: int分箱数量 method: str分箱方法可选qcut等频或cut等距 smooth: float平滑项防止分母为0 返回: result_df: 分箱统计及WOE、IV明细 iv: 该特征的IV值 data df[[col, target]].copy() data data.dropna(subset[col]) # 分箱 if method qcut: try: data[bin] pd.qcut(data[col], qbins, duplicatesdrop) except ValueError: # 样本量不足或有大量重复值时降为较少箱数 unique_count data[col].nunique() real_bins min(bins, unique_count) data[bin] pd.qcut(data[col], qreal_bins, duplicatesdrop) else: data[bin] pd.cut(data[col], binsbins) # 分组统计 grouped data.groupby(bin, observedTrue)[target].agg([count, sum]) grouped.columns [total, bad] grouped[good] grouped[total] - grouped[bad] # 全局好坏样本数 total_bad grouped[bad].sum() total_good grouped[good].sum() if total_bad 0 or total_good 0: raise ValueError(样本中必须同时包含好样本和坏样本) # 计算好坏分布占比 grouped[dist_bad] grouped[bad] / total_bad grouped[dist_good] grouped[good] / total_good # 平滑处理避免除零 grouped[dist_bad_smooth] (grouped[bad] smooth) / (total_bad smooth * len(grouped)) grouped[dist_good_smooth] (grouped[good] smooth) / (total_good smooth * len(grouped)) # 计算WOE和IV grouped[woe] np.log(grouped[dist_good_smooth] / grouped[dist_bad_smooth]) grouped[iv_i] (grouped[dist_good_smooth] - grouped[dist_bad_smooth]) * grouped[woe] iv grouped[iv_i].sum() # 输出明细 result grouped[[total, bad, good, dist_bad, dist_good, woe, iv_i]].reset_index() result.rename(columns{iv_i: iv_each}, inplaceTrue) result[feature] col return result, iv这段代码里需要注意两个细节一是在 qcut 失败时自动降箱数这是应对重复值多的特征的兜底策略二是平滑项 smooth 的添加位置。我加的是拉普拉斯平滑在分子分母上同时加常数比单纯在分母加 1 更稳定。当某个分组里坏样本为 0 时原始公式会得到 ln(正无穷) 无穷大加了平滑后这个值会被拉回来不会出现无穷大污染 IV 总和的情况。3. 实操案例一个完整特征从0到1的评分过程3.1 案例数据说明理论部分说完了代码也有了接下来跑一个完整的演示流程。用前面构造的 df 数据逐个计算 7 个特征的 IV 值然后筛选出合格的特征。这里的 7 个特征里前 5 个是有效特征make_classification 中 n_informative5后 2 个是冗余特征n_redundant2。冗余特征虽然和目标变量有相关性但属于废信息IV 应该不会太高。我们可以通过结果验证逻辑是否符合预期。3.2 批量计算与结果展示批量计算的逻辑很简单写一个循环遍历特征列调用 calc_woe_iv 函数把每个特征的 IV 汇总到一个总表里。同时把每个特征的分箱明细保存下来方便后面检查 WOE 单调性是否合理。feature_cols [ffeat_{i} for i in range(7)] iv_list [] detail_dict {} for col in feature_cols: detail, iv calc_woe_iv(df, col, target, bins10, methodqcut) iv_list.append({feature: col, iv: iv}) detail_dict[col] detail iv_df pd.DataFrame(iv_list).sort_values(iv, ascendingFalse) print(iv_df)跑完后你会看到类似下面的结果由于随机种子固定可以稳定复现featureivfeat_40.xxfeat_10.xxfeat_30.xxfeat_20.xxfeat_00.xxfeat_50.0xfeat_60.0x实际执行后前 5 个特征的 IV 明显高于后 2 个这正好验证了 make_classification 的数据生成逻辑。在实际项目中这个排序表就是特征筛选的直接依据。按通常的筛选规则我们会把 IV 大于 0.02 的特征保留下来进入模型训练IV 小于 0.02 的直接剔除。3.3 分组明细解读与WOE单调性检查只看 IV 值还不够还要检查每个特征的分箱明细尤其是 WOE 的单调性。什么叫单调性举个例子如果年龄从小到大分 5 组WOE 依次为 0.5、0.2、-0.1、-0.3、-0.6这就是单调递减——年龄越大风险越高这个趋势在业务上是合理的青年群体消费能力弱但还款意愿通常更高中年群体收入稳定风险最低老年群体收入下降风险上升这都取决于具体业务。但如果 WOE 出现 0.5、-0.3、0.4、-0.2 这种反复横跳的趋势说明特征和风险的关系不稳定这种特征即使 IV 数值不低在评分卡里也不太建议直接用因为它可能在不同人群之间表现不一致导致模型的稳定性差。# 查看某个特征的详细分箱结果 detail_dict[feat_4]输出包括每个分组的样本量 total、坏样本数 bad、好样本数 good、好坏分布占比、WOE 和单组 IV 贡献。检查时重点关注两点一是最小分组的样本量不能太少建议至少不少于总样本的 2%否则统计结果不可靠二是 WOE 的单调性是否符合业务直觉。注意在实际风控项目中分箱调整是一个反复迭代的过程。我常用的做法是先用 qcut 分成 10 箱看趋势如果趋势明显单调再尝试合并相邻的分箱让最终的分箱数量控制在 3-5 箱。这样既保留了信息量又让评分卡上线后的业务解释更清晰。分箱不是越多越好箱数太多会导致每个箱子的样本太少WOE 波动大模型过拟合风险直线上升。4. 常见问题与排查技巧实录4.1 分箱后出现WOE为无穷大怎么办这是新手最常遇到的问题我自己也踩过。出现无穷大的原因是某个分组内坏样本或好样本数量为 0。当 total_bad 或 total_good 为 0 时dist_bad 或 dist_good 就是 0ln(0) 直接报错或者得到负无穷。解决办法有几个加平滑项前面代码里的 smooth 参数就是干这个的。数值建议在 0.5 到 1 之间太大容易压平真实差异太小又起不到作用。合并相邻分箱。如果某个箱子的好坏样本数为 0说明切分太细了把相邻的箱子合并掉。修改分箱策略从等距切分改为等频切分因为等频切分能保证每个箱子的样本量均衡从源头上减少 0 箱的出现概率。我的经验是在真实业务数据中特征如果有大量重复值比如很多样本都是同一个数值等频分箱很容易切出 0 样本的箱。这时候直接增加平滑项同时把分箱数从 10 降到 5基本能解决大部分问题。4.2 分箱后WOE单调性不符合业务逻辑这种情况也很常见。比如年龄特征业务逻辑是年龄越大风险越低因为收入积累和经验更丰富但你算出来的 WOE 却显示 40-50 岁组风险最高。这时候不要急着把特征丢掉先排查几个方向。第一确认数据质量。是否存在异常值有没有把 0 值混进来比如年龄字段里 0 表示缺失会被分箱到最年轻的组导致该组风险虚高。第二确认好样本的定义。不同业务对好坏的界定标准不同逾期 30 天和逾期 90 天计算出来的 WOE 趋势可能完全不一样。第三确认分箱是否过细。太细的分箱会让 WOE 起伏变大趋势不稳定这时候适当减少箱数再观察一次。我遇到过一个真实案例客户的社保缴纳月数特征业务上应该越长越好WOE 应该单调递增代表低风险但算出来中间某一段突然跳了一下。排查后发现是数据源中有一段时期社保接口故障某几个月的数据全部为 0 填充。清洗掉这些脏数据后单调性就恢复了。所以看到异常趋势先想数据问题再想模型问题。4.3 特征IV值很高但模型效果反而变差这个现象在信贷风控里经常被讨论根本原因通常是训练集上 IV 高不代表泛化能力强。IV 本质上是在衡量特征与目标的历史相关关系但如果这种关系只存在于训练集的时间窗口内或者是因为某个极端事件比如营销活动、政策调整导致的那换一个时间段的样本后IV 可能会断崖式下降。所以我在实际操作中有一个习惯除了在训练集上计算 IV还会在时间外样本OOTOut-of-Time上复算一遍 IV。如果训练集 IV 是 0.25OOT 上 IV 只剩 0.05那这个特征就不能进入最终模型。用 IV 做预筛选没有问题但必须配合稳定性分析比如 PSI一起使用才能防止筛选出的特征失效。4.4 缺失值处理和类别型特征的处理技巧缺失值处理在很多教程里一笔带过但在实际项目中非常关键。对于连续型特征我习惯把缺失值填充成一个特殊的离群值比如 -999然后让分箱自动把它独立成箱。这样做的好处是模型能学到缺失本身也是信息这个规律。如果缺失占比很高比如超过 20%这个特征极有可能是一个强特征因为缺失可能代表用户在某个环节没有完成某操作而这个操作缺失往往和风险相关。对于类别型特征不能直接做数值分箱需要先做特殊处理。最简单的做法是按类别统计坏样本占比再按坏样本占比的高低把类别合并成若干组然后把每个组的坏样本占比作为新的数值特征再做分箱和 IV 计算。也可以直接用坏样本率编码Target Encoding但在小样本场景下要注意过拟合问题建议加交叉验证或平滑。5. 工程化落地与进阶实践5.1 批量特征筛选的自动化流程把单特征的 WOE/IV 计算函数包装成一个自动筛选管道这是我在实际项目中落地的方式。核心逻辑是遍历所有特征分别计算 IV输出排序结果再结合缺失率、单值率某个取值占比超过 95% 的特征几乎没有信息量、相关性等指标综合决策去留。下面给出一个通用的主函数def auto_feature_selection(df, target, cols, bins10, iv_threshold0.02, max_corr0.7): 自动特征筛选管道 筛选流程: 1. 计算每个特征的IV值 2. 剔除IV小于阈值的特征 3. 剔除相关性过高的冗余特征保留IV较高的那个 # 第一步计算IV result [] for col in cols: try: _, iv calc_woe_iv(df, col, target, binsbins) result.append({feature: col, iv: iv}) except Exception as e: print(f特征 {col} 计算失败: {e}) result_df pd.DataFrame(result).sort_values(iv, ascendingFalse) # 第二步IV阈值筛选 pass_iv result_df[result_df[iv] iv_threshold][feature].tolist() print(f通过IV筛选的特征数: {len(pass_iv)}) # 第三步相关性去冗余仅在保留特征之间计算 if len(pass_iv) 1: corr_matrix df[pass_iv].corr() drop_set set() for i in range(len(corr_matrix.columns)): for j in range(i 1, len(corr_matrix.columns)): col_i corr_matrix.columns[i] col_j corr_matrix.columns[j] if abs(corr_matrix.iloc[i, j]) max_corr: iv_i result_df[result_df[feature] col_i][iv].values[0] iv_j result_df[result_df[feature] col_j][iv].values[0] drop_col col_i if iv_i iv_j else col_j drop_set.add(drop_col) final_features [c for c in pass_iv if c not in drop_set] print(f相关性去冗余后剩余特征数: {len(final_features)}) else: final_features pass_iv return final_features, result_df在这个管道里IV 筛选和相关性去冗余是两步分开的。为什么需要去冗余这一步因为 IV 只能衡量单个特征的预测能力无法衡量特征之间的相关性。假设 A 和 B 两个特征高度相关相关系数 0.95它们的 IV 都很高如果同时进入模型不仅会产生多重共线性还会让模型系数变得不稳定后台审核的时候也会很难解释。所以我的原则是相关性高的两个特征保留 IV 高的那个。5.2 分组明细输出与可视化在实际项目汇报中光有 IV 排序表还不够业务方常常会追问这个特征的分箱趋势是怎样的。所以我习惯把每个特征的分箱明细也导出一份 Excel包含每组的样本量、坏账率、WOE、IV 贡献。这样既方便自己复查也方便和业务同事对齐分箱逻辑。下面这段代码可以把明细导出到一个 Excel 文件的多个 sheet 中with pd.ExcelWriter(woe_iv_detail.xlsx) as writer: # 总览sheet iv_df.to_excel(writer, sheet_nameIV总览, indexFalse) # 每个特征的明细sheet for col, detail in detail_dict.items(): detail.round(4).to_excel(writer, sheet_namef{col[:20]}, indexFalse)可视化也是一个好辅助工具。用 matplotlib 画出每个分组的坏账率和 WOE 柱状图能够很直观地看出特征的趋势是否符合业务逻辑。这里不展开画图代码你可以在项目里用 matplotlib 或 plotly 实现横轴是分箱区间纵轴是 WOE 或坏账率一眼就能看出单调性是否合理。5.3 IV与PSI的配合使用从筛选到监控最后一个想聊的点是把 IV 和 PSIPopulation Stability Index群体稳定性指数结合起来用。在风控模型的全生命周期里IV 负责在建模阶段选特征PSI 负责在上线后监控特征稳定性。两者用的其实是同一个思路只是方向不同IV 衡量特征对目标的区分能力PSI 衡量特征分布在训练集和上线样本之间的偏移程度。我的习惯是在建模时对候选特征同时算出 IV 和 PSI用训练集和验证集对比只保留 IV 较高且 PSI 较低的特征。具体经验阈值是PSI 小于 0.1 表示特征稳定0.1 到 0.25 表示略有偏移需要关注大于 0.25 表示特征分布发生重大变化此时需要考虑剔除或重新分箱。这样做有一个额外的好处模型上线后直接沿用建模时的 PSI 基准线做监控不用重新计算基准省了不少事。另外在分箱时还有一个容易忽略的细节分箱的边界尽量取整数或习惯约定的值比如年龄分箱用 18-25、25-35、35-45、45-60不要用 18.7、25.3 这种让业务方一脸困惑的边界。为了做到这一点在 qcut 得到初始分箱后我会手动调整边界让每个箱子的边界落在业务上自然、可解释的位置。这个步骤虽然多花一点时间但后期沟通成本会低很多。6. 写在最后的个人经验做了一段时间的 WOE/IV 计算和特征筛选我最深的体会是这两个指标看起来简单但用得好不好取决于你对自己的数据理解有多深。分箱、平滑、缺失值处理每一个小决定背后都是对业务场景的判断。刚入门的时候我经常纠结于用什么分箱方法、选 5 箱还是 10 箱后来发现这些都只是手段本质目的是让特征的风险趋势暴露得清楚、稳定、可解释。最后分享一个小技巧在搭建完整特征筛选管道时一定不要把 IV 当作唯一的筛选项。我自己的固定组合是先看缺失率和单值率剔除无效特征再用 IV 排序保留有区分能力的特征再用相关性去冗余最后用 PSI 复查稳定性并人工复核高 IV 特征的业务含义。这一套流程走下来基本不会出大问题。如果你正在做评分卡或者任何一个二分类模型建议把上面的代码复制下来跑一跑用你的真实数据去感受一下 WOE 的趋势、IV 的排序再对比一下你之前的筛选方式很容易就能发现差异。这套方法是行业里沉淀了很多年的黄金标准值得好好掌握。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进