资讯动态

基于机器学习的信用风险等级评分系统:从概率到A-E五级分数的工程实践

发布时间:2026/10/27 18:16:44 来源:尧图企业网站定制
简介面向银行、消费金融与互联网金融风控场景的机器学习信用风险等级评分实践包适合具备一定Python与建模基础、希望系统掌握信用卡申请审批评分卡流程的学习者与从业者。资源围绕申请人历史数据展开覆盖变量类型自动识别、类别变量标签编码与连续变量分箱等特征工程环节并提供单变量坏账率分布与多变量交叉的探索性分析帮助理解数据中的风险信号。模型部分支持逻辑回归、随机森林、XGBoost等主流分类器输出准确率、召回率、F1值与AUC等指标并绘制混淆矩阵、ROC曲线和KS曲线便于诊断与调优同时借助toad工具衔接专业评分卡构建兼顾可解释性与监管合规需求。压缩包共4个文件含可交互运行的Notebook、核心Python脚本、训练数据CSV与说明文档整体约18.19MB已有150人学习。读者可据此复现从数据预处理到模型评估的完整链路并可按业务自定义分箱策略与评估参数快速筛选高风险客户、提升审批自动化水平。1. 从一张拒贷工单说起这套评分系统到底要解决什么某消费金融团队的风控负责人给我看过一张工单客户月收入稳定、无逾期记录却被系统直接拒贷人工复核发现是评分卡把「近三个月查询次数」这一项权重放得过大把正常比价行为误判成了资金饥渴。这类问题在信用风险等级评分场景里非常典型——模型不是不准而是不可解释、不可调、不可验收。这套基于机器学习的信用风险等级评分系统要解决的就是把「评分」从黑箱变成一个可拆解、可复现、可交付的工程件输入借款人结构化特征输出 A 到 E 五级风险等级与对应分数同时给出每个等级的概率依据和关键特征贡献。它适合正在做金融风控方向毕设或课程设计的学生也适合需要快速搭一套评分原型验证思路的从业者。关键词信用评分、风险评估、金融风控落到工程上其实是三件事特征怎么来、模型怎么选、等级怎么切。2. 需求拆解与数据口径评分系统的边界先划清楚2.1 业务需求转成可计算指标信用评分系统最容易翻车的地方不在模型而在需求没转成可计算的口径。业务方说「要识别高风险客户」这句话在工程上必须拆成违约定义是什么、观察期多长、表现期多长、样本从哪个时间窗口取。我一般会先定一张需求映射表把模糊描述逐条落到字段和阈值上避免后面模型训练完了才发现标签口径和业务理解对不上。业务诉求可计算口径对应字段/规则识别短期违约风险放款后 6 个月内逾期 30 天以上标签字段 label1 为违约控制审批通过率目标通过率约 70%评分阈值按分位数切等级可解释每个等级有明确分数区间A-E 五级分数 300-850特征可追溯每个入模特征有来源说明特征字典表这张表的价值在于它把「风险评估」从一句口号变成了可以写进验收文档的条目。学生做毕设时经常忽略这一步直接拿一份公开数据集开跑最后答辩被问「你的违约定义是什么」就答不上来。先把口径写死后面所有模块都围绕它展开。2.2 数据字段与特征分层设计特征设计我习惯分三层基础属性层、行为表现层、衍生统计层。基础属性层放年龄、收入、职业类型这类相对静态的字段行为表现层放近 6 个月还款记录、查询次数、额度使用率衍生统计层放趋势类指标比如近 3 个月额度使用率的斜率、查询次数的环比变化。分层的目的是让特征工程有结构后面做特征筛选和解释时能按层归因而不是一堆字段平铺。# 特征分层定义示例字段名按实际数据字典替换 feature_groups { base: [age, monthly_income, occupation_type, marital_status], behavior: [repay_ontime_cnt_6m, query_cnt_3m, credit_util_rate], derived: [util_rate_trend_3m, query_cnt_mom, income_debt_ratio] } # 衍生特征计算额度使用率近3个月斜率用简单差分近似趋势 def build_trend_feature(df): df df.sort_values([customer_id, month]) df[util_rate_trend_3m] ( df.groupby(customer_id)[credit_util_rate] .diff(periods3) # 与3个月前做差正值表示使用率上升 ) return df这段代码的关键在diff(periods3)它用三个月前的值做基准算变化量比直接取均值更能反映趋势。参数periods要根据观察窗口调整窗口是 6 个月就取 3 或 6别拍脑袋。衍生特征一定要在划分训练集和测试集之前按客户维度算好否则会出现同一客户跨集合泄漏的问题这是血泪经验。2.3 样本划分与类别不平衡处理信用违约样本天然不平衡违约率往往在 5% 以下。直接训练会让模型偏向多数类评分分布挤在高分段等级切分失去意义。通常的做法是分层抽样保证训练集和测试集违约比例一致再在训练阶段用类别权重或过采样处理不平衡。我倾向用类别权重而不是 SMOTE因为过采样在特征维度高时容易造出不符合业务逻辑的合成样本解释性会变差。from sklearn.model_selection import train_test_split # 分层抽样保证违约比例在训练/测试集一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, # 按标签分层避免测试集违约样本过少 random_state42 ) # 训练时通过 class_weight 处理不平衡比过采样更可控 # model LGBMClassifier(class_weightbalanced, ...)stratifyy是这里最不能省的参数少了它测试集可能只有个位数违约样本评估指标全是噪声。random_state固定是为了让结果可复现答辩时能对上号。类别权重的思路是让少数类样本在损失函数里占更大比重具体权重可以按正负样本比例反比设置也可以交给balanced自动算。3. 模型选型与评分卡构建从概率到等级的三步走3.1 为什么选树模型而不是逻辑回归传统评分卡用逻辑回归加 WOE 编码优势是解释性强、监管友好树模型优势是能自动捕捉非线性交互在特征工程不充分时表现更稳。这套系统定位是原型验证和教学演示我选 LightGBM 做主模型同时保留逻辑回归作为基线对照。理由很实际树模型对缺失值和异常值容忍度高学生拿到的数据往往脏逻辑回归需要精细的 WOE 分箱工作量翻倍还容易出错。但要注意树模型输出的是概率不是分数中间必须加一层分数映射这一步不能省。import lightgbm as lgb params { objective: binary, # 二分类输出违约概率 metric: auc, # 评估用AUC对不平衡不敏感 learning_rate: 0.05, # 学习率调小配合较多树数量 num_leaves: 31, # 叶子数控制复杂度别一上来就调大 min_child_samples: 50, # 叶子最小样本防止过拟合小样本 class_weight: balanced, # 处理类别不平衡 verbose: -1 } model lgb.LGBMClassifier(**params, n_estimators300) model.fit(X_train, y_train, eval_set[(X_test, y_test)], eval_metricauc, callbacks[lgb.early_stopping(30)]) # 30轮不提升则早停num_leaves和min_child_samples是一对需要联调的参数叶子数大而最小样本小模型会把噪声学进去评分分布出现尖刺。early_stopping(30)是防止树数量拍脑袋的后悔药验证集 AUC 连续 30 轮不涨就停比手动设n_estimators靠谱。class_weightbalanced让少数类权重自动放大配合 AUC 指标整体评估不会被多数类带偏。3.2 概率到分数的映射公式模型输出的是 0 到 1 的违约概率业务要的是 300 到 850 的分数且分数越高风险越低。标准做法是用评分卡刻度变换设定基准分和翻倍分把概率的对数几率线性映射到分数区间。公式是score base factor * ln((1-p)/p)其中factor PDO / ln(2)PDO 是 odds 翻倍需要的分数。这套变换的好处是分数和 odds 保持单调关系等级切分时阈值有业务含义。import numpy as np def prob_to_score(prob, base600, pdo50): 违约概率转信用分数分数越高风险越低 factor pdo / np.log(2) # 每翻倍odds对应的分数增量 offset base - factor * np.log(1) # 基准点校准 odds (1 - prob) / np.clip(prob, 1e-6, 1 - 1e-6) # 好坏比 score offset factor * np.log(odds) return np.clip(score, 300, 850) # 截断到分数区间pdo50表示好坏比翻一倍分数加 50 分这个值要和业务方确认不是技术参数。np.clip防止极端概率把分数打到区间外1e-6的下限是避免除零。映射完一定要画分数分布图看 A 到 E 各等级占比是否合理如果 E 级占了 40%说明阈值切分有问题得回头调。3.3 等级切分的阈值确定方法等级切分不是等分而是按分数分位数结合业务通过率来定。我一般先看分数分布再按目标通过率反推阈值。比如目标通过率 70%就把分数从高到低排序第 70 百分位作为 A/B 分界参考再结合违约率曲线微调。切分完要验证每个等级的违约率是否单调递增如果 B 级违约率比 C 级还高说明切分点选错了得重新看分布。# 按分位数切分等级阈值需结合业务通过率调整 quantiles [0.2, 0.4, 0.6, 0.8] thresholds np.quantile(scores, quantiles) # 分数越高越好等级从高到低对应 A-E def assign_grade(score): if score thresholds[3]: return A elif score thresholds[2]: return B elif score thresholds[1]: return C elif score thresholds[0]: return D else: return E分位数只是起点真正定阈值时要看每个等级的违约率是否随等级下降而上升。如果 D 和 E 的违约率差不多说明这两级区分度不够可以考虑合并或调整切点。这一步建议做成可配置的阈值表答辩时能现场演示改阈值看等级分布变化比讲一堆理论有说服力。4. 系统模块实现特征、训练、评分、解释四条链路4.1 特征处理流水线特征流水线要保证训练和线上评分用同一套逻辑否则会出现训练时特征分布和推理时不一致的经典翻车。我的做法是把特征处理封装成一个类fit阶段算统计量并存下来transform阶段复用线上加载同一份参数文件。缺失值填充用中位数而不是均值因为信用特征长尾明显均值容易被极端值拉偏。from sklearn.base import BaseEstimator, TransformerMixin import pandas as pd class CreditFeaturePipeline(BaseEstimator, TransformerMixin): def __init__(self): self.fill_values {} self.feature_cols [] def fit(self, df): self.feature_cols [c for c in df.columns if c ! label] # 中位数填充抗长尾记录填充值供线上复用 self.fill_values df[self.feature_cols].median().to_dict() return self def transform(self, df): out df[self.feature_cols].copy() out out.fillna(self.fill_values) # 用训练期统计量填充 return outfit里存fill_values是关键线上推理时直接transform就能保证口径一致。feature_cols也存下来避免线上多传字段或少传字段导致列顺序错乱。这个类可以序列化成 pickle和模型文件一起版本管理回滚时一起回滚。4.2 训练与评估模块训练模块除了跑模型还要输出评估报告AUC、KS、各等级违约率、分数分布。KS 在信用风控里比 AUC 更常被业务方问它衡量好坏样本累计分布的最大差距一般 KS 大于 0.3 算可用。评估报告要落成结构化文件方便后面做模型对比和答辩展示。from sklearn.metrics import roc_auc_score, roc_curve def evaluate_model(y_true, y_prob): auc roc_auc_score(y_true, y_prob) fpr, tpr, _ roc_curve(y_true, y_prob) ks max(tpr - fpr) # KS统计量 return {auc: round(auc, 4), ks: round(ks, 4)}roc_curve返回的tpr - fpr最大值就是 KS不用额外装库。评估结果建议同时存 JSON 和打印到日志训练脚本跑完能直接看到关键指标。如果 KS 低于 0.2先别调模型回头查特征有没有泄漏或者标签口径错了模型本身很少是主因。4.3 评分服务接口设计评分服务对外暴露一个接口输入客户特征 JSON输出分数、等级、违约概率和 Top 特征贡献。接口设计要注意两点一是入参校验缺字段直接返回明确错误而不是静默填充二是响应里带模型版本号方便追溯。下面是一个最小可用的 Flask 接口示例。from flask import Flask, request, jsonify import pickle app Flask(__name__) model pickle.load(open(model.pkl, rb)) pipeline pickle.load(open(pipeline.pkl, rb)) app.route(/score, methods[POST]) def score(): payload request.get_json() df pd.DataFrame([payload]) X pipeline.transform(df) # 复用训练期处理逻辑 prob model.predict_proba(X)[0, 1] score prob_to_score(prob) return jsonify({ score: round(float(score), 1), grade: assign_grade(score), prob: round(float(prob), 4), model_version: v1.0 })pipeline.transform保证线上和训练口径一致model_version是排查问题时最有用的一行。入参校验可以再加一层字段白名单缺字段返回 400 而不是让transform抛异常。这个接口够毕设演示用生产环境还要加鉴权、限流和日志但结构不用变。4.4 特征贡献解释模块树模型的可解释性靠 SHAP它能给出每个样本每个特征的贡献值正好补上「黑箱」这个短板。评分结果里带 Top 3 贡献特征业务方能看到「为什么这个人被评了 C」答辩时也能讲清楚模型不是瞎猜。import shap explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_sample) # 取单个样本的Top3贡献特征 contrib pd.Series(shap_values[1][0], indexX_sample.columns) top3 contrib.abs().sort_values(ascendingFalse).head(3)shap_values[1]取的是违约类的贡献索引别搞反。abs()排序是为了看影响大小正负号表示推高还是拉低风险。SHAP 计算在样本多时慢线上可以只对需要解释的请求算或者预计算缓存。5. 避坑与排查评分系统最容易翻车的五个点5.1 特征穿越导致离线指标虚高现象是离线 AUC 到 0.9 以上上线后效果断崖。原因多半是特征里混入了放款后才产生的字段比如「实际还款次数」被当成入模特征。解决办法是建特征字典时标注每个字段的可得时点训练前做一次时点校验凡是放款后才知道的字段一律剔除。我一般会在流水线里加一个available_at_apply白名单不在名单里的字段直接报错。5.2 训练与线上特征口径不一致现象是同一客户离线打分和线上打分差几十分。原因通常是缺失值填充方式不同、类别编码顺序不同或者衍生特征计算窗口不一致。解决办法是把特征处理封装成可序列化的 pipeline训练和线上加载同一份对象禁止两边各写一套逻辑。上线前用一批样本做离线线上比对分数差异超过阈值就拦住。5.3 等级阈值拍脑袋导致分布畸形现象是 A 级占了大半、E 级几乎没人或者中间等级挤成一团。原因是阈值直接等分或者照搬别人的切点。解决办法是先看分数分布直方图再按目标通过率反推切点切完验证各等级违约率单调性。阈值做成配置项别写死在代码里调整时不用改代码重新部署。5.4 类别不平衡处理过度导致误杀现象是通过率骤降大量正常客户被拒。原因是过采样比例设得过高或者类别权重给得太猛模型对违约信号过度敏感。解决办法是优先用类别权重而非过采样权重按实际比例反比设置后做一轮验证看通过率和违约率的平衡点。评估不能只看 AUC要同时看各等级的通过率和违约率。5.5 模型文件与特征文件版本错配现象是服务启动报维度不匹配或者打分结果莫名其妙。原因是模型 pickle 和 pipeline pickle 不是同一次训练产出的。解决办法是把模型、pipeline、阈值配置、特征字典打成一个版本包用同一个版本号命名加载时校验版本一致。回滚时整包回滚不允许单独换其中一个文件。6. 验收与答辩用一张对照表把方案讲圆验收这套系统我习惯用一张「设计目标—实现方式—验证证据」对照表答辩时按表讲评委问什么都能落到具体条目上。表里每一行都要有可展示的证据不能只写「已实现」。设计目标实现方式验证证据输出五级风险等级概率映射分数后按分位数切分等级分布图 各等级违约率表模型区分能力达标LightGBM 类别权重AUC、KS 指标报告评分可解释SHAP 特征贡献单样本 Top3 贡献示例线上线下一致统一 pipeline 序列化离线线上比对差异表阈值可调整阈值配置化现场改阈值演示分布变化答辩时最容易被问的三个问题提前准备好答案第一「你的违约定义和观察期是什么」答口径表第二「为什么用树模型不用逻辑回归」答非线性捕捉和缺失值容忍同时说明保留了逻辑回归基线做对照第三「模型可解释性怎么保证」直接打开 SHAP 贡献图讲一个具体样本。这三个问题答顺了基本盘就稳了。还有一个实操技巧答辩前把整个流程用一条命令跑通从数据加载到评分输出中间不手工干预。我见过太多演示时因为路径写死或者中间文件缺失卡住的场面。把入口脚本固定下来数据路径用相对路径输出目录自动创建这些细节比模型调参更能决定演示成败。从那以后我每次交付评分类项目都会在验收前强制走一遍「换台机器从零跑通」的流程把环境依赖、文件路径、版本匹配全部暴露出来。这套习惯帮我挡掉过好几次现场翻车。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑