尧图精选

基于机器学习的信用评分卡搭建:WOE分箱、模型选型与分数映射

🕒 发布时间:2026/10/2 5:21:39 📁 来源:尧图网络
简介面向金融风控与数据建模人员的信用风险评分系统完整实现基于机器学习对信用卡申请历史数据进行清洗、特征工程与建模输出信用风险等级评估帮助银行、消费金融等机构高效筛选高风险客户提升审批效率与自动化水平。资源包为Zip压缩格式共4个文件涵盖说明文档、脚本、交互式笔记本及数据文件压缩包整体约18.19MB。已有149人学习下载。读者可借助交互式笔记本完整复现数据分箱、标签编码、模型训练与评估流程并结合准确率、召回率、F1值、AUC等指标及混淆矩阵、ROC曲线与KS曲线进行诊断优化基于toad工具构建评分卡脚本与数据可直接修改适配自定义场景附带说明文档便于快速上手适合具备一定Python基础的建模初学者及业务分析师参考。1. 信用评分系统为什么要转向机器学习先想清楚再动手「基于机器学习信用风险等级评分系统」这个名字听起来很大但我交付过的机器学习项目最终都收敛到一个朴素事实离线 AUC 最高的模型不一定能上线能上线的是「分数解释得清、阈值调得动、出问题找得到原因」的模型。这套系统本质上不是炫技而是把借款人的逾期概率折算成一个 300 到 900 分的分数和一个风险等级再交给信审策略决定通过、拒绝还是人工复核。适合谁做信贷审批、消费金融、小微企业授信的建模工程师和策略同学。痛点很具体全靠经验卡额度客群一变就得重调只堆复杂模型又过不了合规解释和客诉核查。所以这里要的不是黑匣子而是一条能把「为什么给这个分、为什么调到这个等级」讲清楚的决策链路。开局第一道选择题就来了主模型用逻辑回归还是梯度提升树这道题没有标准答案但会决定后面所有特征工程和分数映射的写法。我先把问题留到第 4 章从数据口径讲起。2. 数据口径先于模型好客户怎么定义、观察期与表现期怎么切建模翻车的原因里目标变量定义错误排在第一位。很多人拿到数据后第一件事是跑模型这是机器学习应用流程里最常见的误区。信用评分项目里好客户、坏客户、观察期、表现期这四个词如果没对齐后面所有 AUC 都是自欺欺人。2.1 好坏客户定义别拿逾期 90 天一刀切「坏客户」不是拍脑袋决定的它直接决定坏样本率和模型的学习目标。常见定义有以下几类定义方式坏样本率区间适用场景表现期内最大逾期天数 ≥ 90 天M33% - 8%消费金融默认口径逾期 60 天且逾期金额超过 500 元5% - 10%小额短期产品核销或进入诉讼流程0.5% - 2%大额长期贷款表现期内任意一笔逾期 30 天10% - 20%现金贷短周期产品关键不是哪个定义「正确」而是它与你后续的额度损失校准是否一致。如果目标变量定义过松模型学到的「坏」更多是还款意愿波动而不是真实违约定义过严坏样本太少模型分数整体偏移。我一般先跑一遍 Vintage账龄分析再定口径把放款月份作为横轴、账龄作为纵轴看逾期率曲线什么时候进入平台期。表现期至少要覆盖曲线从爬升到平台期的长度消费贷通常 6 到 12 个月长期贷款建议 12 到 24 个月。表现期太短的直接后果是最近几个放款月份的客户还没走完风险暴露期就被标成了「好客户」标签噪声变大。这个阶段做扎实后面「坏样本不足」的问题能缓解一大半。2.2 观察期、表现期与样本切分一段可复制的 Pandas 代码数据切分的常见做法是「观察期 6 个月 表现期 12 个月」观察期用来取特征表现期用来定标签。下面的代码按放款日期切样本并生成 0/1 标签。import pandas as pd loan pd.read_csv(loan_sample.csv, parse_dates[loan_date, first_overdue_date]) obs_start pd.Timestamp(2022-01-01) # 观察期起点 obs_end pd.Timestamp(2022-06-30) # 观察期终点 perf_end pd.Timestamp(2023-06-30) # 表现期截止 train loan[(loan[loan_date] obs_start) (loan[loan_date] obs_end)].copy() # 标签表现期内最大逾期天数 90 记为坏客户 train[label] ((train[first_overdue_date] perf_end) (train[max_dpd] 90)).astype(int) # observe_window 只用于样本切分和时间外验证不能进特征 train[observe_window] (train[loan_date] - obs_start).dt.days这段代码有几个容易忽略的点。label的规则必须同时满足「逾期发生日在表现期截止前」和「最大逾期天数达标」只满足一个都不算坏。observe_window是样本在观察期内的位置很多人顺手把它当特征结果模型学到了「放款越晚风险越高」这种时间伪规律上线后立刻失效。另外训练集、验证集、测试集要用时间顺序切分不要用随机切分。随机切分会把同一时期的客户分散到两边相当于变相使用了未来信息做验证上线后分数表现大概率缩水。如果数据历史不够 12 个月我一般会缩短表现期到 6 个月同时把坏定义放宽到 M260 天逾期而不是硬凑一个 12 个月表现期。模型可以迭代标签口径不对才没有后悔药。2.3 缺失值和极端值先做「是否缺失」再看分布信贷数据的缺失率普遍偏高工作单位、月收入、学历、居住状态经常大面积缺失。我的处理顺序是缺失率超过 30% 的候选变量先放一边但「是否缺失」本身保留为新的 0/1 特征——填了和没填风险差别往往很大。不要用均值填充信用场景里均值填充会把「缺失信号」抹掉。极端值处理也尽量不做主观截断。收入为 0 的、月进件次数异常的直接交给后一阶段的分箱去吸收但有一个前提分箱时要把「0 收入」和「低收入」区分开否则坏率会被平均掉。特征工程做完后每一箱都要检查坏率是否单调不单调的箱要回头查业务含义。数据口径这部分是后面 WOE 分箱的基础下一章开始给可复现的代码。3. 特征工程WOE 分箱与 IV 筛选的落地代码和参数拿到干净样本后进入特征工程。信用评分领域最经典的做法是 WOE 分箱 IV 筛选。为什么不把年龄、收入这些原始值直接喂给机器学习模型这里面的道理值得先说清楚。3.1 为什么信用评分要先做 WOE 变换WOEWeight of Evidence证据权重的直观含义是每个分箱里坏样本分布与好样本分布的比值取对数。WOE 越高该箱的坏浓度越高。原始年龄、收入与风险的关系经常是非单调的年龄很小风险高、中年风险低、老年风险又上升。直接用线性模型拟合这种 U 型关系效果很差分箱后再做 WOE 变换变量与 log-odds 之间就近似线性了。WOE 变换的第二个好处是天然处理缺失值把缺失单独作为一个分箱缺失本身的风险由数据说话而不是拍脑袋填一个值。第三个好处是量纲统一逻辑回归这类线性模型对输入尺度敏感WOE 变换后所有变量的取值都在一个可比较的区间里。树模型如 XGBoost理论上可以不依赖 WOE但我在同一个项目里习惯把候选变量统一算成 WOE这样逻辑回归和 XGBoost 可以在同一特征空间下横向对比后面切分数刻度时也只用一套映射逻辑。3.2 分箱、WOE 与 IV 的计算代码下面这段 Python 代码可以直接跑通单变量的分箱、WOE 和 IV 计算。import numpy as np import pandas as pd def compute_woe_iv(x, y, max_bins10, min_bin_pct0.05): df pd.DataFrame({x: x, y: y}).dropna(subset[y]) # 缺失值单独装箱统一填充到 -999 df[x] df[x].fillna(-999) # 初始分箱用分位数duplicatesdrop 避免重复分位点报错 n_bins min(max_bins, df[x].nunique()) df[bin] pd.qcut(df[x], n_bins, duplicatesdrop) grouped df.groupby(bin, observedTrue)[y].agg([sum, count]) grouped.columns [bad, total] grouped[good] grouped[total] - grouped[bad] bad_total grouped[bad].sum() good_total grouped[good].sum() # 坏分布 / 好分布 grouped[bad_dist] grouped[bad] / bad_total grouped[good_dist] grouped[good] / good_total grouped[woe] np.log(grouped[bad_dist] / grouped[good_dist]) grouped[iv] (grouped[bad_dist] - grouped[good_dist]) * grouped[woe] return grouped.reset_index()代码逻辑分四步先对缺失值占位让缺失样本落到独立分箱再用pd.qcut做分位数分箱然后按箱聚合好样本数和坏样本数最后按公式算出 WOE 和 IV。两个参数需要注意max_bins控制分箱粒度我一般取 8 到 10箱数太多会让每箱样本量不足min_bin_pct是最小箱占比这段简化代码没有强制执行它生产环境里我会在分箱后检查每箱数量占比是否低于 5%低于就合并相邻箱。这段代码还有一个边界坑如果某箱的good或bad为 0bad_dist / good_dist会变成 0 或无穷np.log直接报错。我处理这类零箱的方式是先把空箱合并到相邻箱再计算而不是手动加一个极小值平滑——加平滑会改变 WOE 的方向性后面解释分数时容易翻车。IV 的计算公式是(坏分布 - 好分布) * WOE求和。单变量 IV 的参考阈值小于 0.02 基本没有区分能力直接丢弃0.02 到 0.1 是弱变量可以保留备选0.1 到 0.3 是有效变量大于 0.5 要警惕大概率用了未来信息或者分箱过度拟合。3.3 IV 筛选与相关性去重留下的变量还得过业务直觉完成所有候选变量的 WOE 计算后按以下顺序筛选剔除 IV 小于 0.02 的变量。计算两两相关系数绝对值大于 0.7 时保留 IV 更高的那个。比如「月收入」和「年收入」相关性通常超过 0.9只留 IV 高者。对每个高分箱做业务检查分箱坏率是否符合直觉箱内样本量是否足够。这里最容易犯的错是「唯 IV 论」。我见过一个变量 IV 高达 0.4分箱后发现 40 岁以上样本的坏率骤增但该箱只有不到 2% 的样本完全是噪声拟合出来的。遇到这种情况哪怕 IV 再高我也先剔除。另一个常见坑是把「落选变量」彻底删掉其实它只是当前分箱方式下区分能力弱换一种分箱方式可能就有效了。我会把落选变量存到单独的文件里下一轮迭代再评估。做完特征筛选变量数量一般控制在 15 到 30 个。接下来是建模和分数映射这是整个机器学习项目里最体现工程细节的部分。4. 建模与分数映射把模型输出折算成 300-900 分的完整链路信用评分的分数不是模型的概率本身而是概率经过刻度变换后的结果。这一步决定你交付给业务方的到底是「一个概率数字」还是「一套能被理解和使用的评分系统」。4.1 逻辑回归与 XGBoost怎么选怎么配合逻辑回归的优势是可解释性每个变量有明确的系数能直接还原成「年龄扣 5 分、负债比扣 12 分」这样可解释的加减分项。XGBoost 的优势是非线性拟合能力强AUC 通常更高但分数解释困难监管问询和客诉核查时很难交代。我一般并行跑两套模型XGBoost 用于特征探索和拒绝样本的排序实验逻辑回归用于线上主评分卡。如果团队没有强合规压力且坏样本率极低低于 2%可以以 XGBoost 为主模型但必须配一套概率校准否则分数刻度会失真。实际选型可以参考下面这张表业务场景建议主模型监管问询多、需要逐项解释扣分逻辑回归样本量小于 5 万逻辑回归坏样本率极低小于 2%XGBoost 概率校准特征间非线性强、样本量大XGBoost 为主逻辑回归做对照逻辑回归训练时特征必须是 WOE 变换后的值而不是原始值。WOE 在逻辑回归里还有一个额外价值分箱后的 WOE 值天然带方向性如果某个变量的 WOE 方向和业务直觉相反在系数表里一眼就能看出来。4.2 评分映射公式从 logit 到分数的推导与代码分数映射的通用的公式是score offset factor * ln(odds)这里odds定义为好/坏good/bad分数越高风险越低。两个刻度参数factor是斜率由 PDOodds 翻倍需要的分数增量决定offset是截距由基准分和基准 odds 决定。import numpy as np PDO 20 # odds 每翻一倍分数增加 20 分 base_score 600 # 基准分数 base_odds 19 # 基准好坏比 good/bad 19:1 factor PDO / np.log(2) offset base_score - factor * np.log(base_odds) def score_from_logit(logit): # logit ln(坏概率 / 好概率)越大表示风险越高 log_odds_good_bad -logit return offset factor * log_odds_good_bad # 逻辑回归logit intercept sum(coef * woe_i) logit_lr 0.35 print(score_from_logit(logit_lr)) # XGBoost用 predict_proba 的坏样本概率还原 logit再套同一公式 p_bad 0.3 logit_xgb np.log(p_bad / (1 - p_bad)) print(score_from_logit(logit_xgb))注意这里有一个符号约定代码里的logit是坏概率的对数几率数值越大风险越高所以score_from_logit内部取负把好/坏的对数几率还原出来。逻辑回归的decision_function返回的就是 logit系数与 WOE 相乘再求和后套用这个函数即可。XGBoost 则要先predict_proba拿到坏样本概率再转成 logit。参数怎么设取决于业务敏感度。PDO 取 20 是行业常见值表示风险每翻一倍分数变化 20 分。想让分数对风险更敏感PDO 可以取到 30但我不建议低于 15否则分数跳动太频繁客服和信审每天都要处理大量「为什么上个月 720 这个月 680」的疑问。基准分 600、基准 odds 19:1 是常见起点如果目标客群的基础逾期率就是 5%基准 odds 可以调到 19 对应的水平让分数分布落在 300 到 900 之间。4.3 风险等级阈值切分把分数切到 A/B/C/D分数映射完成后还要切成等级。示例决策矩阵如下等级分数区间建议策略A≥ 720自动通过标准额度B650 - 719通过额度打折或降额C580 - 649转人工复核或补充资产证明D 580拒绝阈值不是拍出来的而是根据通过率目标和坏账容忍度倒推。上线初期目标通过率 60%就把阈值放到通过率刚好 60% 的位置再看该位置对应的坏账率是否在容忍范围内。具体做法是先在验证集上画分数分布直方图和 KS 曲线找区分度最高的分数段再按通过率目标左右平移阈值。提示上线首月阈值宁可保守一点多拒绝几个边界客户也不要让坏账率冲高。评分卡上线后有的是机会调阈值但第一波坏账表现会直接影响业务方对整个机器学习项目的信心。5. 避坑信用评分项目里最常见的 4 个翻车现场信用评分项目的坑和普通机器学习项目不太一样很多问题不在算法层面而在「时间」和「样本」层面。下面 4 个都是我在实际项目中踩过或复盘过的每条按现象、原因、解决三步写。5.1 样本不均衡让分数整体偏移现象坏样本占比低于 2% 时模型输出的 logit 整体偏小分数普遍偏高按固定阈值切分通过率虚高坏账率超预期。原因逻辑回归对正负样本比例敏感大量好样本把截距拉低导致所有客户的 logit 都被压缩分数分布整体上移。排序能力受影响较小所以只看 AUC 看不出问题。解决训练时给坏样本加权重class_weightbalanced或手动设置权重比然后用验证集上真实的坏样本比例重新校准阈值。不要直接调阈值去「校正」分数偏移那样只是掩盖了模型问题。用 SMOTE 等合成样本要格外慎重合成的坏样本可能与真实坏账模式不符反而污染 WOE 分箱。5.2 时间穿越特征里混进了表现期的信息现象离线 AUC 高达 0.85上线后 KS 掉到 0.2 以下业务方直接质疑模型质量。原因最常见的是分箱时用了全量样本或者特征里混进了「放款日后才发生」的变量比如催收次数、放款后的还款记录、以及 2.2 节提到的observe_window。这类未来信息在离线验证里表现极好上线后完全不成立。解决观察期特征只取放款日之前发生的数据分箱边界只 fit 训练集验证集和测试集沿用训练集的边界不能重新分箱。上线前对每个特征做时点可用性审查列出每个变量的数据采集时间字段无法确认采集时点的变量一律不进模型。5.3 拒绝推断没做模型对拒绝人群是黑的现象模型在通过人群上表现不错但在拒绝边界附近的客群上分层能力差被拒绝的人如果当初被通过实际违约率可能远高于模型预估。原因好/坏标签只在被通过的人身上观测得到被拒绝的样本没有表现期模型学不到这部分人群的真实风险规律。这是信贷场景特有的样本选择偏差。解决常见做法是两阶段模型或软标签法。先用一个宽松模型或现有高分卡对历史拒绝样本打伪标签再并入训练集资源不够时至少做一轮拒绝样本回捞实验把原本拒绝但被人工捞回的样本纳入观察才能估计偏差方向。跳过这一步的项目上线后通常会在阈值附近集中翻车。5.4 分数上线后表现大幅下跌现象上线首月 KS 比离线验证低 40% 以上分数分布整体偏移。原因训练样本只取了最近 3 个月或客群发生了结构性变化比如进件渠道换了一批、合作方导流人群变了。这是「柜台前后两套样」的典型表现。解决训练样本至少要覆盖 12 到 18 个月并且保留最后 3 个月做时间外验证OOT不能用随机划分代替。上线后按周监控分数分布和 PSI群体偏移指数PSI 超过 0.1 就要开始查客群变化超过 0.25 强烈建议重训。注意区分短期波动和模型失效先看样本量再看偏移方向不要因为一两周的数据波动就急着重训。6. 上线前的最后一公里分桶回测、决策矩阵复核与配置版本化这一章讲三个验证技巧都能直接用在项目收尾阶段也是我每次交付前必做的三步。6.1 分桶坏账率回测把验证集分数按分位数切成 20 桶从高到低逐一检查每桶的真实坏账率。一个可靠的评分系统坏账率应该随分数升高单调下降。def bucket_check(score, y, n_buckets20): df pd.DataFrame({score: score, y: y}) df[bucket] pd.qcut(df[score], n_buckets, duplicatesdrop) table df.groupby(bucket, observedTrue)[y].agg([mean, count]) table[bad_rate] table[mean] return table.sort_index(ascendingFalse)如果最高分桶的坏账率反而上升说明分数在高端区间失真优先检查 WOE 方向是否写反或者特征里混入了过拟合变量。6.2 决策矩阵人工复核自动化的指标再漂亮也要过一遍人工。我会把 A/B 等级边界和 C/D 等级边界上下 10 分内的样本各抽 100 个让策略同事不看分数、只看原始资料先给出「你认为该不该拒」再和模型等级对拍。不一致率超过 5%说明某个变量的业务含义和模型学到的方向有偏差需要补变量或重新审视分箱。6.3 把变量字典、分箱边界、系数、阈值、等级表统一存成配置这是我被坑过一次之后养成的习惯。曾有一次回测用的分箱边界和线上配置不一致导致分数对不上排查了三天最后发现是发布时配置文件版本落后了一版。后来所有评分卡在发布时都带同一个版本配置文件线上评分和线下回测读同一份 JSON{ version: 2024-03-01, model: lr_woe_v7, params: {offset: 515.0, factor: 28.85}, bins: {age: [25, 35, 45], income: [3000, 8000, 20000]}, grades: {A: 720, B: 650, C: 580} }每次迭代只改版本号旧版本保留可回溯。这个习惯帮我省掉了大量「线上分数和线下对不上」的排查时间也希望帮你在上线前少走一段弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →