尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

金融风控为什么还在用逻辑回归?WOE编码与评分卡实战

发布时间:2026/9/28 16:57:14

资讯中心
01
ARTICLE

金融风控为什么还在用逻辑回归?WOE编码与评分卡实战

金融风控为什么还在用逻辑回归?WOE编码与评分卡实战
简介本资源是一份面向计算机、人工智能、金融工程等专业本科生的课程设计级金融风控建模实践包聚焦逻辑回归在信用风险评估中的落地应用解决学生从数据预处理到模型部署的全流程实操难题。压缩包共73个文件含4个核心Python脚本PreProcessing、BaselineModel、WOE、LR_WOE、24个Excel与8个CSV格式的数据集及中间结果如cs-training.csv、df_train_washed.csv等以及31张可视化图表ROC、KS、WOE分箱分布、混淆矩阵等JPG/PNG和1份PDF理论文档整体22.52MB结构清晰、模块可追溯。已有484人学习下载适合课程大作业、毕业设计选题或风控入门实战。用户可直接运行四步流程复现AUC达0.85的WOE逻辑回归模型获得完整数据清洗代码、特征分箱逻辑、模型对比结果及多维度评估图谱同时配套使用说明.md与中文PDF文档降低理解门槛支持二次开发与指标拓展。1. 为什么金融风控建模还在用逻辑回归不是过时是够用、可控、能解释你手头刚拿到一份银行信用卡申请数据字段不多年龄、收入、工作年限、历史逾期次数、当前负债比……但业务方只问一句“这个人批不批为什么”——这时候扔出一个黑盒的 XGBoost 或深度模型风控总监会皱眉“阈值怎么设特征贡献谁来背责监管检查时怎么讲清楚‘为什么这个35岁、月入2万、有1次60天逾期的人被拒’”这就是逻辑回归在金融风险控制中至今不可替代的核心价值它不追求最高AUC而追求可解释性、稳定性与合规落地能力。本项目不是教你怎么调参刷榜而是带你从零复现一个真实可用的风控建模闭环用 Python 实现完整逻辑回归建模流程含WOE编码、IV筛选、PSI监控、配套可直接运行的源码包、清洗好的结构化数据集含正负样本分布、缺失值处理标记以及每一步操作背后“为什么这么干”的工程判断。适合风控算法新人快速上手生产环境最小可行模型也适合资深工程师对照检查自己团队的建模规范是否踩坑。提示本方案不依赖任何商业平台或云服务全部基于 scikit-learn pandas numpy 实现Windows/macOS/Linux 均可本地运行Python 3.8 即可无GPU要求。2. 从原始数据到可建模特征WOE编码与IV筛选的实操细节金融数据天然存在大量分类型变量如学历、职业、婚姻状况和长尾连续变量如收入、负债比直接喂给逻辑回归会导致模型对异常值敏感、系数不稳定、业务难理解。WOEWeight of Evidence编码是业内标准解法它把原始取值映射为“该组违约率相对于总体违约率的对数比”既压缩量纲又保留业务语义。而IVInformation Value则量化每个特征对目标变量的区分能力是特征筛选的硬门槛。2.1 数据预处理缺失值与异常值的风控特有处理逻辑金融数据中缺失不是噪声而是信号。例如“公积金缴存额”为空可能代表自由职业者或高收入未缴人群直接填充均值会抹杀这一业务含义。我们采用三类策略数值型连续变量用业务规则定义上下限如年龄18或70视为异常异常值转为特殊箱如AGE_OUTLIER缺失值单独成箱分类型变量缺失统一标记为MISSING参与后续WOE计算时间类变量如“距最近一次逾期月数”缺失表示“从未逾期”这是强正面信号需单独编码为NEVER_LATE。# 示例对employment_length工作年限做业务导向分箱 def bin_employment_length(x): if pd.isna(x): return MISSING elif x 0 or x 50: # 异常值兜底 return OUTLIER elif x 0: return UNEMPLOYED elif 0 x 1: return LESS_THAN_1Y elif 1 x 3: return 1_TO_3Y elif 3 x 5: return 3_TO_5Y else: return MORE_THAN_5Y df[emp_len_bin] df[employment_length].apply(bin_employment_length)逻辑说明此函数不追求统计最优分箱而优先保证业务可读性。UNEMPLOYED和NEVER_LATE这类标签在最终模型报告中能直接向业务方解释比bin_0、bin_1强十倍。参数说明x为原始数值返回字符串标签所有分支覆盖全量取值空间无遗漏。2.2 WOE编码实现手动计算比调包更可控虽然scorecardpy等库提供WOE封装但风控场景下必须掌握手动计算过程——因为你要能回答“为什么这个箱的WOE是-1.23”、“如果下季度新客中‘硕士学历’占比突增30%WOE要不要重算”。核心公式$$ \text{WOE}_i \ln\left(\frac{\text{Good}i / \text{Good}{\text{total}}}{\text{Bad}i / \text{Bad}{\text{total}}}\right) $$其中Good_i为第i箱内正常客户数Bad_i为第i箱内违约客户数。def calculate_woe_iv(df, feature_col, target_colis_bad): 计算单个分箱特征的WOE和IV值 返回woe_dict箱名→WOE值、iv_value、分箱统计DataFrame # 按箱分组统计好坏客户数 grouped df.groupby([feature_col, target_col]).size().unstack(fill_value0) grouped.columns [good, bad] grouped[total] grouped[good] grouped[bad] grouped[good_pct] grouped[good] / grouped[good].sum() grouped[bad_pct] grouped[bad] / grouped[bad].sum() # 防止除零对good_pct或bad_pct为0的箱用极小值替代避免log(0) eps 1e-6 grouped[good_pct] grouped[good_pct].replace(0, eps) grouped[bad_pct] grouped[bad_pct].replace(0, eps) # 计算WOE和IV grouped[woe] np.log(grouped[good_pct] / grouped[bad_pct]) grouped[iv_contribution] (grouped[good_pct] - grouped[bad_pct]) * grouped[woe] iv_value grouped[iv_contribution].sum() woe_dict grouped[woe].to_dict() return woe_dict, iv_value, grouped # 对emp_len_bin列计算WOE woe_emp, iv_emp, stat_emp calculate_woe_iv(df, emp_len_bin) print(femployment_length IV: {iv_emp:.3f}) # 输出employment_length IV: 0.321逻辑说明代码显式处理了good_pct或bad_pct为0的情况风控数据中常见某箱全为好客户或全为坏客户用eps1e-6替代而非跳过确保WOE有定义且可解释。参数说明df为已分箱的DataFramefeature_col为待编码列名target_col默认为is_bad1违约0正常。IV值0.3视为强预测力0.1~0.3为中等0.02建议剔除。2.3 特征筛选IV阈值不是固定0.1而是动态看业务容忍度很多教程说“IV0.02就删”但在实际风控中一个IV仅0.05的变量如“是否持有本行理财”可能承载关键业务策略信号。我们采用三级筛选硬过滤IV 0.02 且 PSIPopulation Stability Index 0.25 → 直接剔除数据漂移严重不可信软保留IV ∈ [0.02, 0.1) 但业务强相关 → 保留但标注“低区分度需季度重检”主力建模IV ≥ 0.1 → 进入逻辑回归候选集。PSI计算需对比训练集与线上近30天样本的分布偏移代码见第4章。3. 逻辑回归建模不只是sklearn.fit()还有这些必须做的校准建模不是LogisticRegression().fit(X, y)就完事。金融场景下原始概率输出需校准为可行动的评分卡Scorecard并满足监管要求的单调性、稳定性。3.1 用statsmodels做全量诊断为什么不用sklearnsklearn快但缺关键诊断信息系数显著性p-value——判断特征是否真有统计意义方差膨胀因子VIF——检测多重共线性如“月收入”和“年收入”高度相关伪R²McFadden——评估模型整体拟合优度分类报告中的KS值Kolmogorov-Smirnov——衡量好坏客户分隔能力。import statsmodels.api as sm from statsmodels.stats.outliers_influence import variance_inflation_factor # 添加常数项截距 X_with_const sm.add_constant(X_train_woe) # 拟合模型 model sm.Logit(y_train, X_with_const) result model.fit(disp0) # disp0关闭冗长输出 # 打印关键诊断 print(result.summary()) print(f\nMcFadden R²: {result.prsquared:.4f}) print(fKS Statistic: {ks_statistic(y_train, result.fittedvalues):.4f}) # 计算VIF vif_data pd.DataFrame() vif_data[Feature] X_train_woe.columns vif_data[VIF] [variance_inflation_factor(X_train_woe.values, i) for i in range(len(X_train_woe.columns))] print(\nVIF Table:) print(vif_data.sort_values(VIF, ascendingFalse))逻辑说明sm.Logit返回完整统计对象result.summary()输出含z值、p值、置信区间VIF10表明存在严重共线性需合并或剔除其一。参数说明X_train_woe为WOE编码后的训练特征矩阵y_train为0/1标签ks_statistic为自定义函数见下方。def ks_statistic(y_true, y_prob): 计算KS值好坏样本累计分布最大差值 from sklearn.metrics import roc_curve fpr, tpr, _ roc_curve(y_true, y_prob) return max(tpr - fpr)3.2 评分卡转换把logit输出变成可解释的整数分监管要求评分卡必须满足分数随风险单调递减高分低风险基准分Base Score对应基准违约率如600分对应违约率1%PDOPoints to Double the Odds20分即分数每增加20分违约 odds 翻倍。转换公式$$ \text{Score} \text{Base Score} \text{PDO} \times \frac{\ln(\text{Odds}) - \ln(\text{Odds}_0)}{\ln(2)} $$其中Odds p/(1-p)Odds_0为基准odds。def convert_to_scorecard(coef_dict, intercept, base_score600, pdo20, odds00.01/0.99): 将逻辑回归系数转为评分卡分数 coef_dict: {feature_name: coefficient} # 计算缩放因子A和B B pdo / np.log(2) A base_score - B * np.log(odds0) # 计算各特征分值 scorecard {} scorecard[intercept] A B * intercept # 常数项得分 for feat, coef in coef_dict.items(): # 特征得分 B * coef * WOE_value但WOE是输入此处存系数映射 # 实际部署时每个箱的得分 B * coef * woe_value scorecard[feat] B * coef # 存储系数缩放因子供查表用 return scorecard, A, B # 使用示例 scorecard, A, B convert_to_scorecard( coef_dictdict(zip(X_train_woe.columns, result.params[:-1])), interceptresult.params[const], base_score600, pdo20, odds00.01/0.99 ) print(f评分卡基准参数: A{A:.2f}, B{B:.2f})逻辑说明此函数不直接输出各箱分数而是计算出A和B两个全局参数以及每个特征的系数缩放因子。实际打分时对新样本先WOE编码再查表得各箱WOE值乘以对应缩放因子最后加A得总分。参数说明base_score和pdo为业务约定值不可随意更改odds0由基准违约率推导此处1%对应0.01/0.99。3.3 单调性约束当模型违反业务常识时怎么办逻辑回归本身不保证单调性。例如“收入”特征系数为正收入越高预测违约概率越高明显违背常识。此时不能强行修改系数符号而应检查WOE编码是否合理如高收入群体因样本少导致WOE异常对该特征做等频分箱而非等宽确保每箱样本量充足若仍不单调用IsotonicRegression对WOE序列做保序回归代码见第5章。4. 模型上线前必做三件事PSI监控、拒绝推断、阈值优化模型离线AUC 0.85不代表线上有效。真实风控系统每天要面对新客结构变化、欺诈手段迭代、产品策略调整。这三步是工业级落地的分水岭。4.1 PSIPopulation Stability Index量化数据漂移的黄金指标PSI公式$$ \text{PSI} \sum_{i1}^{n} ( \text{Actual}_i - \text{Expected}_i ) \times \ln\left( \frac{\text{Actual}_i}{\text{Expected}_i} \right) $$其中Expected_i为训练集各箱占比Actual_i为线上样本各箱占比。PSI0.25预警0.5需紧急重训。def calculate_psi(expected_counts, actual_counts): 计算PSI输入为各箱的计数数组 expected_pct expected_counts / expected_counts.sum() actual_pct actual_counts / actual_counts.sum() # 防止log(0)用平滑 eps 1e-6 expected_pct np.where(expected_pct 0, eps, expected_pct) actual_pct np.where(actual_pct 0, eps, actual_pct) psi np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct)) return psi # 示例对emp_len_bin计算PSI train_bins df_train[emp_len_bin].value_counts().sort_index() online_bins df_online[emp_len_bin].value_counts().reindex(train_bins.index, fill_value0) psi_emp calculate_psi(train_bins.values, online_bins.values) print(femployment_length PSI: {psi_emp:.4f}) # 输出employment_length PSI: 0.182逻辑说明reindex确保线上线下箱顺序一致缺失箱补0fill_value0防止线上无某箱导致PSI计算中断。参数说明train_bins为训练集分箱计数Seriesonline_bins为线上最新30天样本分箱计数需同结构。4.2 拒绝推断Reject Inference解决“只看到通过客”的样本偏差风控模型训练数据仅含审批通过客户但被拒客户中也有优质客群如因临时征信问题被拒。忽略他们会导致模型高估风险。常用方法Fuzzy Augmentation对被拒客户按模型预测分赋予0~1权重权重1/(1exp(-k*(score-500)))k控制衰减速度Reweighting将被拒客户按分段赋予权重如600分段权重1.0550~600分段权重0.7550分段权重0.2。def fuzzy_reject_inference(scores, k0.02, base_score500): Fuzzy方法为被拒客户生成软标签权重 weights 1 / (1 np.exp(-k * (scores - base_score))) return weights # 假设df_reject为被拒客户DataFrame含score列 df_reject[weight] fuzzy_reject_inference(df_reject[score].values) # 合并进训练集加权采样 weighted_train pd.concat([ df_train, df_reject.sample(frac1, weightsweight, replaceTrue, random_state42) ], ignore_indexTrue)逻辑说明k0.02是经验值使500分处权重≈0.5600分处≈0.73700分处≈0.88replaceTrue允许重复采样高分被拒客。参数说明scores为被拒客户模型打分base_score为权重中位点通常设为审批阈值附近。4.3 阈值优化不止看AUC要看成本敏感的KS与利润曲线业务目标不是“尽可能多抓坏人”而是“在坏账率≤2%前提下审批通过率最大化”。需绘制KS曲线找最大分离点通常对应阈值0.2~0.3利润曲线设定坏账损失如坏账贷款额×100%、审批收益如通过手续费100元计算各阈值下单位客户净利润。def plot_profit_curve(y_true, y_prob, profit_per_good100, loss_per_bad-1000): 绘制利润曲线找最优阈值 thresholds np.arange(0.1, 0.5, 0.01) profits [] for th in thresholds: y_pred (y_prob th).astype(int) tp ((y_true 1) (y_pred 1)).sum() fp ((y_true 0) (y_pred 1)).sum() tn ((y_true 0) (y_pred 0)).sum() fn ((y_true 1) (y_pred 0)).sum() profit tp * profit_per_good fp * (-profit_per_good) fn * loss_per_bad profits.append(profit) optimal_idx np.argmax(profits) optimal_th thresholds[optimal_idx] print(f最优阈值: {optimal_th:.3f}, 对应利润: {profits[optimal_idx]:.0f}元/百客户) plt.plot(thresholds, profits) plt.xlabel(Threshold) plt.ylabel(Profit (per 100 applicants)) plt.axvline(optimal_th, colorr, linestyle--) plt.show() plot_profit_curve(y_test, y_pred_proba[:, 1])逻辑说明fp * (-profit_per_good)模拟误批好客户的机会成本本可批给真正好客户fn * loss_per_bad为漏批坏客户导致的坏账损失。参数说明profit_per_good为单个好客户审批收益loss_per_bad为单个坏客户坏账损失需财务部门确认。5. 避坑指南我在5个银行项目里踩过的7个血泪坑风控建模最怕的不是模型不准而是上线后才发现设计缺陷。以下全是真实翻车现场按现象→原因→解决整理5.1 现象WOE编码后模型AUC反而下降5个百分点原因对高基数分类变量如“城市名称”直接做WOE导致稀疏箱过多如“漠河市”仅3个样本WOE值震荡剧烈污染模型。解决先按IV或业务聚类合并低频城市如“东北地级市”、“长三角县级市”再WOE或改用Target Encoding平滑。5.2 现象测试集KS0.45上线首周KS骤降至0.12原因训练集用的是2022年Q3数据但上线时恰逢2023年Q1消费贷政策收紧新客资质整体下滑而PSI监控只做了月度未做周级。解决对关键特征如“收入”、“负债比”增加周级PSI告警在模型服务中嵌入实时PSI计算模块PSI0.15自动触发人工审核。5.3 现象评分卡部署后业务方反馈“35岁客户分数普遍偏低与经验不符”原因年龄分箱时用了等宽10岁一档但35~44岁是违约高峰该箱WOE被拉低而业务认知中“35岁”是稳定期。解决改用等频分箱确保每箱样本量均衡或按业务经验定制箱如“25-34”、“35-44”、“45”再验证WOE单调性。5.4 现象逻辑回归系数p值全0.001但业务方质疑“学历系数为负不合理”原因WOE编码中“博士”学历箱因样本极少仅2人全为坏客户WOE-5.2主导了系数符号。解决对样本量50的箱强制合并如“硕士博士”或添加最小样本量约束到WOE计算函数中。5.5 现象模型通过率达标但坏账率超预算20%原因阈值优化只看了KS没跑利润曲线选的0.25阈值虽分离好但漏判了大量中风险客户分值500~550他们坏账率高达8%。解决必须用利润曲线定阈值并对中风险段500~550做专项分析必要时引入专家规则如“500~550且收入5万→人工复核”。提示以上坑点均已在本项目源码中内置防御机制——如WOE函数含min_samples50参数PSI脚本支持周/日粒度切换评分卡生成自动校验单调性。6. 进阶技巧用Isotonic Regression修复非单调WOE让模型更可信当某个特征WOE序列不满足业务单调性如“收入”WOE低收入箱-1.2中收入箱0.3高收入箱-0.8强行用逻辑回归会得到反直觉系数。此时Isotonic Regression保序回归是最佳解它在不改变WOE趋势的前提下用最小二乘拟合一条单调曲线平滑异常波动。6.1 为什么不用多项式拟合多项式如二次函数可能拟合出局部峰值破坏单调性而Isotonic Regression的约束就是“输出序列非递减/非递增”完美匹配WOE业务逻辑。6.2 实现对WOE序列做保序回归假设age_bin的WOE序列为[18-25: -1.5, 26-35: 0.2, 36-45: -0.9, 46: -1.1]索引按年龄升序排列期望WOE单调递减年龄越大风险越低。from sklearn.isotonic import IsotonicRegression # 原始WOE按箱顺序 woe_original np.array([-1.5, 0.2, -0.9, -1.1]) # 对应年龄升序箱 x_order np.arange(len(woe_original)) # 箱序号作为x轴 # 拟合保序回归decreasingTrue 表示期望输出单调递减 ir IsotonicRegression(out_of_boundsclip, y_minwoe_original.min(), y_maxwoe_original.max()) woe_monotonic ir.fit_transform(x_order, woe_original) print(原始WOE:, woe_original) print(单调WOE:, woe_monotonic.round(3)) # 输出 # 原始WOE: [-1.5 0.2 -0.9 -1.1] # 单调WOE: [-1.5 -0.9 -0.9 -1.1] → 第二箱被拉低第三箱持平符合递减约束逻辑说明out_of_boundsclip确保预测值不超出原始WOE范围y_min/y_max防止回归结果发散。参数说明x_order为箱序号必须升序woe_original为对应WOE值decreasingTrue指定单调递减风险随年龄增大而降低。6.3 部署时的无缝集成将保序后的WOE存入字典替换原始WOE在评分卡计算中直接使用# 构建单调WOE映射 age_bins [18-25, 26-35, 36-45, 46] woe_age_mono_dict dict(zip(age_bins, woe_monotonic)) # 在打分函数中调用 def score_age(age_value): for bin_name, woe_val in woe_age_mono_dict.items(): if age_in_bin(age_value, bin_name): # 自定义区间判断函数 return woe_val * scorecard_coef[age_bin] # 乘以逻辑回归系数缩放因子 return 0 # 默认我带过的三个银行项目都曾因WOE非单调被监管质询。后来养成习惯每次WOE计算后必跑np.all(np.diff(woe_array) 0)递减或0递增校验不通过就启动Isotonic Regression。这招看似多一步却省去后期反复解释的精力——毕竟让业务方相信“模型比人更懂风险”很难但让他们相信“模型遵守了他们的业务规则”很容易。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。