基于toad的Python评分卡完整实战:分箱WOE到逻辑回归落地 简介一份面向金融风控与数据分析初学者的Python信用评分卡示例基于toad库实现完整覆盖特征选择、分箱、WOE转换、模型评估和评分卡生成等核心环节。压缩包共2个文件含一个可直接运行的Python脚本和一个结果说明文档包体约5.8MB数据与代码一次配齐。目前已有271人学习下载。示例不仅提供代码骨架还附带用于训练和验证的数据集读者可对照资源描述中的配套博文逐行理解每一步输入输出快速搭建自己的评分卡实验。运行后可掌握toad库在变量筛选、最优分箱、IV/KS评估以及分数映射中的典型用法理清从原始样本到最终信用分数的完整链路适合智能风控入门、金融建模课程设计或相关项目预研参考。 评分卡不是新东西但每次新项目我还是要把它从头到尾跑一遍。原因很简单只要做信贷审批、信用卡额度授信、消费分期这类业务业务方和监管最认的往往不是黑盒模型而是能把“为什么给这个客户600分而不是550分”讲清楚的评分卡。这篇我直接用Python生态里的toad库从一个公开数据集出发把完整的示例代码和数据集准备方法写清楚跑通一版真正能落地的评分卡流程。toad这个库在国内风控圈用得挺多了专门用来做评分卡流程从数据清洗、特征筛选、分箱、WOE转换到逻辑回归训练和评分映射基本上把传统评分卡的脏活都包圆了。相比自己写几百行分箱代码toad能省下大量时间关键是分箱结果和评分逻辑都可控、可解释。下面我把整个流程拆开讲最后会给一份可以直接复制的完整脚本。1. 为什么做评分卡还要用toad这类工具1.1 手撸评分卡的问题出在哪先说说我自己早年做评分卡的经历。那时候拿到数据第一步先写个函数统计缺失率第二步写循环算IV第三步用卡方分箱分箱的轮子也得自己造第四步手动做WOE编码第五步塞进逻辑回归最后还要写评分映射公式把概率转成分数。一整套下来代码量轻轻松松上千行而且分箱边界、WOE计算方向、空箱处理这些细节只要有一个地方不一致整个项目的评分结果都可能出问题。更麻烦的是评分卡流程里有很多“潜规则”。比如分箱后每个箱子的样本占比不能太低否则上线后箱子很容易空心特征一波动分数就乱跳再比如WOE转换后特征和target的关系要尽量单调这样业务方才能理解“年龄越大分数越高”这种逻辑。这些约束如果全靠手写代码会越来越臃肿最后变成只有自己能看懂的“祖传代码”。1.2 toad在整个流程里干了哪些活toad的设计思路就是按评分卡流程分模块每个环节都给你一个现成的高质量工具。我用一张表对比一下手撸和用toad的差别环节传统手撸方式toad方式工作量对比数据质量报告逐个字段写统计函数toad.detect()、toad.quality()从半天缩到1分钟缺失值、异常值清洗自己写fillna和clip逻辑toad.clean()从一篇代码缩到一行单变量特征筛选手写IV计算循环toad.selection.select()从百行缩到一行分箱自己写卡方分箱或等频分箱toad.transform.Combiner()从几百行缩到四五行WOE转换手写WOE计算和映射toad.transform.WOETransformer()从几十行缩到一行评分映射手推公式写映射函数手动公式或toad.ScoreCard可选这表里最打动我的其实是quality和selection。做评分卡的人都知道特征筛选是前期最耗时的事尤其遇到几百个变量的宽表光算IV再按阈值筛掉低质量特征就能让人崩溃。toad把这套逻辑标准化了虽然它不代表完全不用思考但至少能保证基线的稳定性。2. 数据准备与初始体检拿到评分卡项目数据我先看这三样2.1 示例数据集怎么选公开数据和模拟数据两条路很多读者看到“完整示例代码和数据集”第一反应是想拿一份现成数据直接跑。我给两个方案一个用真实公开数据集一个用模拟数据两条路都能跑通代码。真实公开数据集我推荐UCI的German Credit也就是德国信用数据集。它大约1000条样本20个特征包含年龄、性别、工作状态、贷款金额、贷款时长、历史逾期情况等目标变量是信用好坏二分类。数据量不大但特征类型丰富有数值型也有类别型非常适合演示分箱和WOE过程。搜“UCI German Credit”或者“german credit data csv”就能找到网上很多版本字段名可能略有差异注意把好坏客户标签处理成0和1就行。如果你不想折腾下载可以直接用我下面这段模拟数据代码。它生成的数据不包含真实信息只做技术演示但字段设计贴近真实信贷场景年龄、年收入、负债收入比、近6个月逾期次数、信用卡使用率、近6个月贷款查询次数。坏客户比例通过调节截距项控制在10%左右后续分箱和建模效果都会比较清晰。import numpy as np import pandas as pd def make_simulated_data(n20000, random_state42): np.random.seed(random_state) n int(n) age np.random.randint(18, 70, sizen) income np.random.lognormal(mean10, sigma0.6, sizen) debt_ratio np.clip(np.random.beta(2, 5, sizen), 0, 0.95) overdue_6m np.random.poisson(0.6, sizen) credit_util np.clip(np.random.beta(2, 6, sizen), 0, 1) query_count_6m np.random.poisson(1.2, sizen) logit ( -4.8 - 0.02 * (age - 35) - 0.4 * (np.log(income) - 10) 3.0 * debt_ratio 0.8 * overdue_6m 1.5 * credit_util 0.4 * query_count_6m np.random.normal(0, 0.6, sizen) ) p 1 / (1 np.exp(-logit)) target np.random.binomial(1, p) df pd.DataFrame({ age: age, income: income, debt_ratio: debt_ratio, overdue_6m: overdue_6m, credit_util: credit_util, query_count_6m: query_count_6m, target: target }) return df data make_simulated_data() print(data.shape) print(data[target].mean())跑完这段你会得到一个约2万行、7列的DataFrame坏客户占比在10%上下。字段不够复杂但演示整套评分卡流程完全够了。2.2 detect和quality一分钟看清数据质量拿到数据第一件事不是建模而是先看数据质量。toad自带的两个函数能把这件事压缩成两行代码import toad # 检查字段类型、缺失率、唯一值等 toad.detect(data) # 计算每个字段的IV、缺失率、唯一值等质量指标 toad.quality(data, targettarget)我自己在真实项目里的习惯是先跑detect了解哪些字段是数值型、哪些是类别型有没有字段唯一值占比高得离谱再跑quality看IV和缺失率。quality输出的表里每一行是一个特征列包含IV、缺失率、唯一值个数之类重点看两件事。第一缺失率。缺失率超过70%的特征通常直接剔除因为不管怎么填充它在分箱后样本量都不够上线后稍有一点波动就会出空箱。第二IV值。IV低于0.02的特征基本属于“没什么区分度”后面特征筛选中可以直接过滤掉。这一步我特别提醒一点toad.detect()的合理使用方式是看每一个字段的分布是否符合业务认知。比如模拟数据里的overdue_6m是个偏态分布多数客户逾期次数是0少数几次极少数很多次。这种零膨胀的字段后面分箱时必须单独处理0值不能简单等频分段否则0值客户会被打到各个箱子里业务解释性会很差。3. 特征筛选、分箱与WOE转换toad全自动背后的判断逻辑3.1 分箱不是越细越好分箱这一步是整个评分卡的精髓也是toad最值得讲清楚的地方。为什么要分箱主要有三个原因。第一处理异常值和极端值。信贷数据里收入、负债这种长尾分布字段经常有极端值直接进回归模型会被拉得很离谱分箱后极端值就落到最后一个箱子里影响可控。第二拟合非线性关系。年龄和违约率往往不是线性关系年轻人违约高中年低老年又高一点分箱后每个箱给一个WOE值逻辑回归就能拟合这种非线性。第三业务展示需要。评分卡最终要给业务看“年龄在30到40岁之间加15分”这种展示方式必须依赖分箱。toad的分箱器用法很简单combiner toad.transform.Combiner() combiner.fit( data[feas [target]], ytarget, methodchi, min_samples0.05, max_n_bins5 ) bins combiner.export() print(bins)这里methodchi是卡方分箱它会把相邻且违约率差异不显著的区间合并使得最终箱内差异小、箱间差异大。min_samples0.05意味着每个箱子至少要有5%的样本防止分箱过细导致上线后某箱空心max_n_bins5限制最大箱数让评分表不至于太啰嗦。这两个参数我建议在真实项目中盯着调样本量越小min_samples要放得越大否则分箱结果会非常不稳定。分箱器跑完export()返回的是一个字典key是特征名value是分箱边界列表。你可以直接看每个特征的切分点是否合理比如年龄如果被切成18-25、25-35、35-50、50-70这种边界业务上就很容易解释如果切出19.8-26.7这种带小数的边界就要怀疑是不是分箱参数没调好或者数据有异常。3.2 WOE转换与IV筛选的联动逻辑分箱之后是WOE转换。WOE全称是Weight of Evidence计算方式是一个箱子里坏客户占比除以好客户占比再取对数WOE_i ln(坏客户占比_i / 好客户占比_i)这个值的直观含义是当前箱子的风险相对于全量平均水平是高还是低。WOE大于0说明这个箱子的坏客户比例偏高分值应该低WOE小于0说明这个箱子比较安全分值应该高。IV值就是每个箱子的WOE按好坏占比加权求和衡量整个特征对好坏的区分能力。toad的WOE转换只需要一行代码trans toad.transform.WOETransformer() data_woe trans.fit_transform(data[feas [target]], data[target])转换之后原始字段变成WOE值例如age字段原来的“28岁”会变成年龄落在对应箱子后给的一个WOE数值。这一步做完后进行特征筛选selected toad.selection.select( data_woe, targettarget, empty0.7, iv0.02, corr0.7, return_dropTrue ) final_feas selected[0]筛选逻辑按顺序依次执行先剔缺失率超过70%的特征再剔IV低于0.02的特征最后做相关性分析两个特征相关性高于0.7时保留IV较高的那个。这套阈值是评分卡项目里比较通用的基线但我建议不要死记样本量小的时候IV阈值可以降到0.01特征特别多的宽表可以提到0.05关键看最终入模特征是否稳定、是否可解释。这里有一个需要特别强调的细节WOE方向。不同版本、不同库对WOE的定义方向可能不同有的库定义为ln(坏/好)有的定义为ln(好/坏)。建模后一定要检查逻辑回归系数符号和业务直觉是否一致。我习惯在分箱后打印每个箱子的bad_rate和WOE一起看如果bad_rate越高WOE也越高说明当前WOE定义是ln(坏/好)那后面评分映射里系数符号就要按这个方向走。4. 逻辑回归建模与评分映射从概率到600分的过程4.1 为什么评分卡要用逻辑回归而不是XGBoost模型选择上评分卡行业默认用逻辑回归这在今天看起来有点“old school”但绝对不是守旧。我做过很多树模型和神经网络单论AUC确实经常能压逻辑回归一头但评分卡场景里最核心的需求不是极致的区分度而是稳定和可解释。逻辑回归输出的是违约概率的对数发生比logit它是线性的logit β0 β1*WOE1 β2*WOE2 ... βk*WOEk这个形式让每个特征对最终分数的贡献可以拆成独立的加分项。业务方问“为什么这个人分低”你可以直接回答因为他的“近6个月查询次数”落在高风险箱里这一项扣了35分。换成XGBoost就很难做到这种精确归因。稳定性方面逻辑回归方差小对特征分布波动不敏感上线后分数分布不容易漂移。对信贷这种需要长期监控的模型来说这比微小的AUC提升重要得多。所以在代码实现里模型验证部分我不建议只盯AUCKS才是风控更关心的指标。KS衡量的是好坏客户累计分布的最大差异能更直观反映模型把好人和坏人分开的能力。我用toad内置的KS计算函数看一下就行from sklearn.metrics import roc_auc_score from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression x_train, x_test, y_train, y_test train_test_split( data_woe[final_feas], data_woe[target], test_size0.3, random_state42 ) lr LogisticRegression(C0.1, class_weightbalanced, max_iter500) lr.fit(x_train, y_train) auc roc_auc_score(y_test, lr.predict_proba(x_test)[:, 1]) ks toad.metrics.KS(lr.predict_proba(x_test)[:, 1], y_test) print(AUC:, round(auc, 4)) print(KS:, round(ks, 4))C0.1是我在评分卡里常用的正则化强度。评分卡入模特征都是WOE分箱转换后的变量本身信息密度高但特征间可能还有残留相关性稍微强一点的正则能压住系数的波动线上表现更稳。4.2 评分映射公式的推导与落地代码从逻辑回归的概率到600分这个标准分数中间有个固定公式我每次都会手推一遍防止符号搞反。我们定义好坏比odds为好客户概率与坏客户概率的比值odds (1 - p) / p分数由这两个参数决定Score A - B * logit这里logit ln(p/(1-p))所以logit越大票价风险越高分数越低公式里用负号。常数A和B怎么定业界常用的设定是好本文还有配套的精品资源点击获取