个人信用评估与机器学习:从WOE分箱到评分卡开发的完整实践指南
简介面向机器学习初学者和课程设计学生这套个人信用评估Python实现方案以阿里天池贷款违约预测赛题为背景完整覆盖数据清洗、特征编码、降维、网格搜索调参与模型评估等环节适合作为课程设计或入门数据挖掘的实战参考。资源包共82个文件压缩后约5.16MB核心包含13个Python脚本、62张PNG可视化分析图、2个CSV数据文件、1份Word设计报告及说明文档其中脚本按模块组织可视化图则覆盖变量分布、相关性热力图、主成分分析等结果设计报告详细记录实验步骤与结论。目前已有741人学习。通过这套资料读者既可复现贷款违约预测全流程也能学习到信用评分场景中特征工程与模型调优的通用方法代码结构清晰便于迁移到类似风险预测任务中复用。1. 个人信用评估机器学习能替人工规则省下多少事个人信用评估这个场景几乎每个做过信贷风控的人都不陌生申请人的年龄、收入、负债、历史还款记录摆在那里让审批员凭经验打分既慢又不一致。机器学习解决的就是这件事——用历史样本训练一个模型把“会不会违约”转成一个可解释的概率或分数。刚入门的朋友拿它当练习项目非常合适很多机器学习课程设计选题也会落在信用评估上而真正在银行、消费金融公司做风控的日常干的也是同一套流程。反直觉的一点是这个任务里模型算法不是门槛数据样本定义、特征处理才是决定模型能不能用的关键。2. 信用评估的数据从哪来公开数据集与清洗流程2.1 个人信用评估常用数据集German Credit 与 Give Me Some Credit做个人信用评估第一步不是选模型而是先解决数据。常见做法是先用公开数据集把流程跑通再迁移到自己的业务数据上。入门最常用的是 UCI 的 German Credit 数据集1000 条样本、20 个变量标签是好客户或坏客户。样本量小适合拿来理解特征工程和评分卡流程几分钟就能跑完一轮实验。另一个更接近真实业务的是 Kaggle 上的 Give Me Some CreditGMSC约 15 万条样本12 个特征标签字段是借款人未来两年内是否出现财务困境。这个数据集样本量够大能真实反映出“坏样本比例很低”带来的问题我一般建议用这个做完整流程演练。拿到数据后先别急着建模。个人信用评估的样本单位是“人”而不是“行为”所以一个客户只能对应一条记录如果同一个客户有多条借贷记录必须先在客户维度做聚合否则训练集和测试集之间会泄露信息。这个步骤在公开数据集里已经被处理好了但真实业务里极其常见。2.2 用 pandas 把原始表洗干净缺失值、异常值和标签处理机器学习中的数据处理是什么说白了就是让原始表变成能直接喂给监督学习算法的干净 DataFrame。信用数据里最常见的四个问题缺失值、异常值、重复值、标签比例失衡。GMSC 里 MonthlyIncome 字段缺失率大约 20%DebtRatio 偶尔会出现几十甚至上百的极端值。下面是我惯用的清洗流程import pandas as pd import numpy as np # 读入原始数据先看结构和缺失情况 df pd.read_csv(cs-training.csv) print(df.info()) # MonthlyIncome 缺失率不低初版用中位数填充 # 不用 mean 是因为收入分布右偏个别高收入会把均值拉得很高 df.loc[:, MonthlyIncome] df[MonthlyIncome].fillna( df[MonthlyIncome].median() ) # 如果有全空行直接丢弃 df df.dropna(subset[NumberOfDependents]) # 年龄小于等于 0 属于脏数据用中位数替换而不是删除 df.loc[df[age] 0, age] df[age].median() # DebtRatio 超过 1 说明月负债超过月收入现实中基本是录入错误 df df[df[DebtRatio] 1] # 检查标签分布这决定了后面所有参数怎么调 print(df[SeriousDlqin2yrs].value_counts(normalizeTrue))这里几个参数值得说清楚。fillna 用中位数而不是均值是因为收入分布长尾严重均值会被少数高收入样本带偏中位数更稳健。DebtRatio 截断到 1 是业务常识负债超过收入的人可能仍会被审批通过但比率几十倍只有脏数据才做得出来。最后一行的 value_counts(normalizeTrue) 是必做动作如果坏样本占比低于 10%后面模型训练就必须处理类别不平衡否则精度再高也没有意义。提示公开数据集清洗固然重要但真实信贷数据里还有“观察期”和“表现期”的时间窗口问题建议一开始就在工程文档里把这两个窗口定义清楚。3. WOE 分箱与 IV 筛选信用评估特征工程的核心步骤3.1 为什么信用评分不用原始数值直接进模型很多从普通机器学习入门的朋友会问年龄、收入这些特征直接标准化后扔进逻辑回归不就行了吗在信用评估里还真不行。原因有两个。第一年龄和违约概率不是线性关系——20 岁和 30 岁可能差异很大但 40 岁和 50 岁差异很小让原始数值充当线性输入等于强行假设每增加一岁风险变化相同这个假设在业务上站不住脚。第二逻辑回归本身要求输入与输出呈线性关系而信用评估中连续变量与违约概率通常呈“U 型”或“阶梯型”关系。机器学习线性回归实验里那套“直接标准化 回归”的做法在信用评估里会带来解释性和稳定性双重问题。行业常见解法是分箱 WOE 编码。分箱就是把连续变量切成几段WOEWeight of Evidence证据权重则把每一段转换成“这一箱里好客户占比与坏客户占比之比的对数”。经过转换后输入变量与违约概率的对数几率的线性关系才能成立逻辑回归系数也才可解释。3.2 等频分箱与 WOE 编码可复现的 Python 实现分箱方式有很多种最常见的是等频分箱也就是让每一箱的样本量大致相等。代码实现比较直接import pandas as pd import numpy as np def woe_bin(df, feat, target, bins5): # 等频分箱qcut 遇到重复分位点会报错duplicatesdrop 是必带参数 df_cp df.copy() df_cp[bin] pd.qcut(df_cp[feat], qbins, duplicatesdrop) grouped df_cp.groupby(bin)[target].agg([mean, sum, count]) grouped[good] grouped[count] - grouped[sum] # sum 是坏样本数 bad_total grouped[sum].sum() good_total grouped[good].sum() # WOE 公式ln(好样本占比 / 坏样本占比) grouped[woe] np.log( (grouped[good] / good_total) / (grouped[sum] / bad_total) ) # IV 是每个箱的贡献累加用于衡量变量区分度 grouped[iv] ( (grouped[good] / good_total - grouped[sum] / bad_total) * grouped[woe] ) return grouped result woe_bin(train, age, SeriousDlqin2yrs, bins8) print(result)这段代码的核心逻辑是groupby 掉每个分箱后用 sum 数出坏样本数count 减 sum 得到好样本数然后按 WOE 公式逐箱计算。注意 qcut 在数据存在大量重复值时会报“Bin edges must be unique”之类的错误duplicatesdrop 会自动合并重复分位点代价是实际箱数可能会小于设置的 bins这属于可接受的行为。bins 参数在信用评估里有讲究。我一般初版设置为 5~10 箱太少会丢失梯度信息太多则每一箱样本量过小WOE 值被噪声主导。一个经验标准是每箱至少要有 30 个坏样本如果做不到就降低箱数或合并相邻箱。等频分箱是最容易上手的方案但未必是最优的。卡方分箱会在建模时自动合并类别分布相似的相邻箱生成的 WOE 更平滑但实现复杂度也更高。建议先把等频分箱跑通再看结果决定要不要升级。3.3 IV 值筛选特征留下真正有区分度的变量WOE 算完之后紧接着就是 IVInformation Value信息价值筛选。IV 本质上是 WOE 的加权求和衡量一个变量对好坏的区分能力。行业里通行的判断标准是IV 区间预测能力处理建议 0.02几乎无预测力直接剔除0.02 ~ 0.1较弱可保留或结合业务判断0.1 ~ 0.3中等优先保留 0.3较强保留但需排查是否泄漏这里有一个容易被忽略的坑IV 超过 0.5 的变量要格外警惕它可能泄露了标签信息。比如“当前逾期次数”在一个“未来两年是否违约”的模型里IV 一定高得离谱但它在业务上属于“过去状态”还是“未来结果”取决于你定义观察期的方式。所以 IV 筛选只做排名真正的取舍必须由业务人员确认变量含义。特征筛选完成后把所有变量的 WOE 值按样本拼成一张新表原始数值就不再进模型了。这一步做完才到模型训练环节。4. 逻辑回归训练与评分卡转换把概率翻译成分数4.1 为什么信用评估场景首选逻辑回归而不是随机森林很多刚入门的人会问随机森林、XGBoost 的 AUC 明显更高为什么信用评估还在用逻辑回归这个问题的答案不在精度而在“可解释性”和“可落地性”。信贷审批涉及监管合规和客户申诉。客户被拒贷时金融机构必须能解释为什么拒逻辑回归的系数可以直白地告诉业务人员“年龄每增加一箱分数变化多少”。而随机森林的决策路径极其复杂连开发者自己都说不清单个样本为什么被分到坏客户那一类。另外逻辑回归天然输出概率配合评分卡转换后分数可以沿用银行已经运行多年的阈值体系替换成本低。这并不意味着机器学习算法层面只能选逻辑回归。实际项目中常见的做法是逻辑回归做底稿和评分卡XGBoost 做对照模型验证变量的重要性和稳定性。如果 XGBoost 里某个变量的重要性极高但逻辑回归里系数不显著说明这个变量存在非线性关系需要回头检查分箱是否合理。4.2 训练逻辑回归类别不平衡与正则化参数怎么设用 WOE 矩阵替代原始特征后训练代码就很常规了from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # 用 WOE 矩阵替代原始特征进模型 features [age_woe, income_woe, debt_ratio_woe] X train_woe[features] y train_woe[SeriousDlqin2yrs] # stratifyy 保证训练集和测试集的坏样本比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) model LogisticRegression( C0.1, # 正则化强度越小惩罚越重 penaltyl2, # L2 让系数分布更平滑 class_weightbalanced, # 自动按类别比例反向加权 solverliblinear, max_iter500 ) model.fit(X_train, y_train) train_auc roc_auc_score(y_train, model.predict_proba(X_train)[:, 1]) test_auc roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) print(训练集 AUC:, train_auc) print(测试集 AUC:, test_auc)C 值在信用评估里是一个需要反复试的参数。老版本 scikit-learn 默认 C1.0在几千条样本上经常过拟合训练集 AUC 比测试集高出 0.05 以上。我一般从 C0.05 开始试到 0.2 之间画一条 AUC 曲线选择测试集 AUC 最高且与训练集差距最小的点。class_weightbalanced 是最快的缓解坏样本过少的做法但注意它改变的是损失函数里的样本权重对系数绝对值也会有影响。如果后续要转评分卡业务方可能会对“某个变量的系数为什么这么大”有疑问那时候需要回到分箱环节调整。换用 SMOTE 过采样或对好样本下采样也可以但初版用 class_weight 最省事。solvers 选 liblinear 是因为小样本二分类场景它收敛最稳lbfgs 在高维稀疏矩阵上更快但这里样本量不大差别不明显。4.3 从概率到评分卡offset 与 factor 的换算模型训练完成后业务方真正要的不是违约概率而是一个 300~900 分之间的整数分数。评分卡转换公式业界是通用的Score offset - factor * ln(odds)其中 odds p / (1-p)p 是模型输出的违约概率。factor 和 offset 由两个业务参数决定基准分对应的 odds以及分数每升高多少分 odds 翻倍。import numpy as np # 业务设定600 分对应 odds 1/10坏好比 1:10 # 分数每升高 20 分odds 翻倍即客户变好的可能性翻倍 factor 20 / np.log(2) offset 600 - factor * np.log(1/10) def prob_to_score(p): odds p / (1 - p) score offset - factor * np.log(odds) return round(score) test_score [prob_to_score(p) for p in model.predict_proba(X_test)[:, 1]]这段换算本身不难真正的难点在业务参数怎么定。基准分对应的 odds 和翻倍分值是风控委员会和业务方共同拍板的不是建模的人自己定的。常见取值范围是基准分 500~700odds 在 1:5 到 1:20 之间翻倍分值在 15~30 分之间。换一组参数分数的绝对含义就变了所以评分卡转换的第一步永远是先和业务对齐这两个参数。转换完成后正常的流程是对每个 WOE 变量计算其对应分数贡献形成一张“评分卡”表格。这张表才是真正给业务审批系统用的东西每个分箱区间对应多少分加总后加基准分就是最终信用评分。5. 个人信用评估的 5 个典型踩坑现象、原因与解决5.1 坏样本比例太低模型精度虚高现象是 Accuracy 高达 96%但 KS 和 AUC 都很低模型完全没有区分能力。原因很简单坏样本占比通常只有 3%~5%模型只要全预测成好客户精度就已经 95% 以上但一个只会说“都通过”的模型在风控里毫无价值。解决方法是把评估指标从 Accuracy 换成 AUC、KS 和 Recall。训练时用 class_weightbalanced 或者对训练集做下采样让好坏样本比例别太悬殊。最后选阈值时不要默认 0.5而是看 KS 最大点或者业务能接受的召回率/误杀率来定阈值。这点在机器学习模型评估那一套流程里属于最基础但最容易犯的错。注意只看 Accuracy 判断模型好坏在信贷场景里基本等于自欺欺人。5.2 特征泄漏把“未来”和“当前”混进训练集现象是测试集 AUC 高得惊人超过 0.85还以为是模型效果好结果上线后立刻翻车。原因通常是特征里面混进了“未来才知道的信息”。比如用客户“当前逾期次数”去预测“未来两年是否违约”如果观察期定义不严格这个变量就包含了结果本身的信息。更隐蔽的是在特征构造时用了全量数据的统计量比如把整个数据集上客户均收入作为特征。解决方法所有特征必须在观察期结束时点就已确定。特征生成代码要在时间戳层面做校验数据集划分按时间切分而不是随机切分。WOE 分箱边界只能在训练集上计算测试集和上线后的新样本只能对已经生成的箱子做映射不能重新分箱。5.3 坏样本定义不统一模型目标漂移现象是同一个数据集两个人各建一个模型KS 差一大截而且谁都说服不了谁。原因是坏样本标签的定义不统一。有人把逾期 30 天当成坏客户M1有人坚持逾期 90 天以上才算坏M3还有人把“历史曾经核销”当坏客户。标签定义不同模型学习的目标就已经不同。这个坑的解决方式靠流程规范项目启动前写一份样本定义文档明确观察期多长、表现期多长、逾期到第几天记为坏样本、逾期不足但多次发生的怎么算。整个机器学习应用流程里这一步几乎决定了后面所有工作的价值文档写清楚之后后面的人拿到数据不需要再猜。5.4 分箱后 WOE 不单调逻辑回归系数不稳定现象是模型系数符号和业务直觉相反比如收入越高违约概率越高。原因很可能是分箱不合理某几个箱子坏样本占比波动大WOE 值上下跳动逻辑回归为了拟合这种波动把系数搞偏了。等频分箱在样本量不足时尤其容易出现这种问题。解决方法是合并相邻箱。WOE 值不单调时把相邻的箱合并到一起让每个箱的坏占比稳定下来。如果合并后还是不单调那就换一个分箱方法比如卡方分箱或者考虑这个变量本身就不适合进入模型。变量筛选的目的是留下方向一致、解释合理的输入不是为了凑 AUC。5.5 模型上线后 KS 快速衰减变量分布漂移现象是回测表现很好的模型上线一个月后 KS 掉到原来的三分之一。原因大多不是模型训练出了问题而是客群结构变了。比如业务主推了新客群年龄和收入分布整体迁移或者市场环境变化导致某些行业客户收入骤降。机器学习模型只能学到训练集分布下的规律分布一变原模型就失效。解决方法是上线前先设好监控体系每个变量的 PSI 按月计算超过 0.1 预警超过 0.25 就要重新训练模型评分整体分布同样按 PSI 监控。这里需要提醒的是PSI 的分箱边界必须用训练集的分位点固定下来不能拿当期数据重新切箱否则漂移永远测不出来。6. 上线前的验证KS、PSI 与滚动回测的落地做法模型训练完了评分卡也转了真正考验人的是上线前的验证。信用评估不像别的机器学习项目随便拿一组留出集跑个 AUC 就能上线它必须回答两个问题模型在时间维度上稳不稳定上线后分布漂移了能不能及时发现。先说滚动回测。把历史数据按月份切成多段比如用第 1~12 个月的样本训练第 13~18 个月验证再把训练窗口向后推 6 个月第 7~18 个月训练第 19~24 个月验证。这样做能看到 KS 在时间维度上的波动。如果每个窗口的 KS 都接近模型才是真的稳如果第一个窗口 KS 0.35第二个窗口掉到 0.2说明样本定义或特征里混了一些只在特定时间段成立的信号。很多入门项目只做一次随机切分KS 很好看一上时间序列验证就露馅。再说 PSI 的计算。一个可复现的 PSI 函数如下import numpy as np import pandas as pd def psi(score_train, score_now, bins10): # 以训练集分位点作为固定边界不能用当期数据重新分箱 cuts pd.qcut(score_train, qbins, retbinsTrue)[1] train_pct np.histogram(score_train, binscuts)[0] / len(score_train) now_pct np.histogram(score_now, binscuts)[0] / len(score_now) # 避免分母为 0加上极小值 now_pct np.where(now_pct 0, 1e-6, now_pct) return np.sum((now_pct - train_pct) * np.log(now_pct / train_pct))这段代码里最关键的就是分箱边界固定逻辑。train_pct 和 now_pct 用的是同一组 cuts如果每期都用当期数据重新算分位数分布漂移就被“动态归一化”掩盖了PSI 永远显示稳定。PSI 的判断标准是小于 0.1 视为稳定0.1~0.25 需要监测并排查原因大于 0.25 建议触发模型重训。我自己的习惯是模型上线前先制定一个“监控三件套”评分分布的 PSI、每个特征变量的 PSI、按月滚动的 KS 报表。前两项可以用上面这个函数每天跑批第三项每月重算一次。上线后前三个月是重点观察窗口评分分布出现整体右移或左移第一时间找业务要当期客群画像做对照。以前我做信用评估模型把 AUC 刷到 0.82 就觉得项目完工了结果上线一个月 PSI 直接跳到 0.3评分分布整体右移才发现是业务在那个月集中推了一批高收入低负债的客群。后来养成的习惯是不管模型多简单先写样本定义文档再做时间切分验证最后把 PSI 监控挂上。这三件事做完模型上线才不会提心吊胆。希望帮到你。本文还有配套的精品资源点击获取