☰
基于机器学习的信用风险等级评分系统:从概率到A-E五级分数的工程实践
2026/10/27 18:16:31 网站建设 项目流程

简介:面向银行、消费金融与互联网金融风控场景的机器学习信用风险等级评分实践包,适合具备一定Python与建模基础、希望系统掌握信用卡申请审批评分卡流程的学习者与从业者。资源围绕申请人历史数据展开,覆盖变量类型自动识别、类别变量标签编码与连续变量分箱等特征工程环节,并提供单变量坏账率分布与多变量交叉的探索性分析,帮助理解数据中的风险信号。模型部分支持逻辑回归、随机森林、XGBoost等主流分类器,输出准确率、召回率、F1值与AUC等指标,并绘制混淆矩阵、ROC曲线和KS曲线,便于诊断与调优;同时借助toad工具衔接专业评分卡构建,兼顾可解释性与监管合规需求。压缩包共4个文件,含可交互运行的Notebook、核心Python脚本、训练数据CSV与说明文档,整体约18.19MB,已有150人学习。读者可据此复现从数据预处理到模型评估的完整链路,并可按业务自定义分箱策略与评估参数,快速筛选高风险客户、提升审批自动化水平。

1. 从一张拒贷工单说起:这套评分系统到底要解决什么

某消费金融团队的风控负责人给我看过一张工单:客户月收入稳定、无逾期记录,却被系统直接拒贷,人工复核发现是评分卡把「近三个月查询次数」这一项权重放得过大,把正常比价行为误判成了资金饥渴。这类问题在信用风险等级评分场景里非常典型——模型不是不准,而是不可解释、不可调、不可验收。这套基于机器学习的信用风险等级评分系统,要解决的就是把「评分」从黑箱变成一个可拆解、可复现、可交付的工程件:输入借款人结构化特征,输出 A 到 E 五级风险等级与对应分数,同时给出每个等级的概率依据和关键特征贡献。它适合正在做金融风控方向毕设或课程设计的学生,也适合需要快速搭一套评分原型验证思路的从业者。关键词信用评分、风险评估、金融风控,落到工程上其实是三件事:特征怎么来、模型怎么选、等级怎么切。

2. 需求拆解与数据口径:评分系统的边界先划清楚

2.1 业务需求转成可计算指标

信用评分系统最容易翻车的地方不在模型,而在需求没转成可计算的口径。业务方说「要识别高风险客户」,这句话在工程上必须拆成:违约定义是什么、观察期多长、表现期多长、样本从哪个时间窗口取。我一般会先定一张需求映射表,把模糊描述逐条落到字段和阈值上,避免后面模型训练完了才发现标签口径和业务理解对不上。

业务诉求可计算口径对应字段/规则
识别短期违约风险放款后 6 个月内逾期 30 天以上标签字段 label,1 为违约
控制审批通过率目标通过率约 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(periods=3) # 与3个月前做差,正值表示使用率上升 ) return df

这段代码的关键在diff(periods=3),它用三个月前的值做基准算变化量,比直接取均值更能反映趋势。参数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_size=0.3, stratify=y, # 按标签分层,避免测试集违约样本过少 random_state=42 ) # 训练时通过 class_weight 处理不平衡,比过采样更可控 # model = LGBMClassifier(class_weight="balanced", ...)

stratify=y是这里最不能省的参数,少了它测试集可能只有个位数违约样本,评估指标全是噪声。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_estimators=300) model.fit(X_train, y_train, eval_set=[(X_test, y_test)], eval_metric="auc", callbacks=[lgb.early_stopping(30)]) # 30轮不提升则早停

num_leaves和min_child_samples是一对需要联调的参数,叶子数大而最小样本小,模型会把噪声学进去,评分分布出现尖刺。early_stopping(30)是防止树数量拍脑袋的后悔药,验证集 AUC 连续 30 轮不涨就停,比手动设n_estimators靠谱。class_weight="balanced"让少数类权重自动放大,配合 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, base=600, pdo=50): """违约概率转信用分数,分数越高风险越低""" 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) # 截断到分数区间

pdo=50表示好坏比翻一倍分数加 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 out

fit里存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], index=X_sample.columns) top3 = contrib.abs().sort_values(ascending=False).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 贡献图讲一个具体样本。这三个问题答顺了,基本盘就稳了。

还有一个实操技巧:答辩前把整个流程用一条命令跑通,从数据加载到评分输出,中间不手工干预。我见过太多演示时因为路径写死或者中间文件缺失卡住的场面。把入口脚本固定下来,数据路径用相对路径,输出目录自动创建,这些细节比模型调参更能决定演示成败。

从那以后我每次交付评分类项目,都会在验收前强制走一遍「换台机器从零跑通」的流程,把环境依赖、文件路径、版本匹配全部暴露出来。这套习惯帮我挡掉过好几次现场翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询