信用风险等级评分系统实战:从极度不平衡数据到A-E等级输出
简介这份资源面向银行、消费金融与互联网金融领域的数据分析及风控从业者也适合具备一定机器学习基础、希望系统实践信用评分卡的学习者用于解决信用卡申请环节中高风险客户识别与审批决策效率问题。压缩包共4个文件约18.19MB包含可交互运行的Jupyter Notebook、核心Python脚本、训练数据CSV与说明文档覆盖从数据读取到建模评估的完整链路。内容围绕数据预处理与特征工程展开自动区分数值与类别变量借助toad完成标签编码与连续变量分箱并给出坏账率分布等单变量分析及多变量交叉探索模型部分支持逻辑回归、随机森林、XGBoost等主流分类器输出准确率、召回率、F1、AUC及混淆矩阵、ROC、KS曲线便于诊断调优。读者可据此理解评分卡构建流程与可解释性分析思路并迁移到自有信贷数据中。已有150人学习关注。1. 信用评分系统从一份“看起来都像好人”的客户名单说起手里拿到一份三万条的客户申请数据逾期标签只有不到 4%绝大多数样本都是“好人”。如果直接拿准确率当指标模型把所有客户都判成“不违约”准确率也能到 96%但业务上一分钱风险都没挡住。这就是信用风险等级评分系统真正要解决的问题不是判断“谁是好客户”而是在极度不平衡的样本里把少数高风险客户按等级排出来让风控策略能分层定价、分层授信。这个项目适合两类人一类是想跑通一套完整机器学习流程的开发者从特征工程、样本切分、模型训练到评分卡映射走一遍另一类是金融风控方向的同学想看清楚评分等级是怎么从概率值落到 A 到 E 的。下面按“能干什么、怎么跑、怎么改”的顺序拆。2. 项目定位与技术选型为什么是这套组合2.1 系统到底解决什么问题信用风险等级评分系统的核心输出不是“违约/不违约”的二分类标签而是一个离散等级。业务侧拿到等级后才能做差异化策略A 级客户给高额度低利率D 级客户降额或者要求补充材料E 级直接拒绝。所以整个系统的链路是原始申请数据 → 特征工程 → 违约概率预测 → 概率到分数的映射 → 分数到等级的切分。这条链路里模型只是中间一环前后两端的处理才决定系统能不能落地。从项目标题和关键词看这套系统覆盖的是信用评分、风险评估、金融风控三个方向。实际工程里它通常包含数据预处理模块、特征衍生模块、模型训练与评估模块、评分映射模块以及一个供业务调用的评分接口。数据规模一般在几万到几十万条申请记录特征维度在几十到上百之间包含年龄、收入、职业、历史逾期次数、负债收入比这类结构化字段。2.2 技术栈与依赖选择这类项目最常见的组合是 Python pandas scikit-learn模型侧用逻辑回归做基线再用梯度提升树LightGBM 或 XGBoost做对比。选逻辑回归不是因为它强而是因为评分卡需要可解释性每个特征的系数能直接转成 WOE 分箱后的分值业务和合规都能看懂。梯度提升树用来验证“非线性关系有没有被漏掉”如果树模型 AUC 明显高于逻辑回归说明特征里有交互项没被线性模型捕捉到这时候再考虑加特征或者换模型。依赖清单大致如下版本按常见稳定组合写实际以本地环境为准# 核心依赖建议用虚拟环境隔离 python3.9 pandas1.5.3 numpy1.23.5 scikit-learn1.2.2 lightgbm3.3.5 imbalanced-learn0.10.1 # 处理样本不平衡 joblib1.2.0 # 模型持久化提示金融风控项目对可复现性要求高依赖版本最好锁死别用 latest否则换台机器跑出来的分数可能对不上。2.3 目录结构长什么样一个能跑起来的项目目录不会太花哨但每个文件职责要清楚。常见结构是这样credit_risk_score/ ├── data/ │ ├── raw/ # 原始申请数据 │ └── processed/ # 清洗和特征衍生后的数据 ├── src/ │ ├── config.py # 路径、分箱数、阈值等配置 │ ├── data_loader.py # 数据读取与字段校验 │ ├── preprocessing.py # 缺失值、异常值、WOE 分箱 │ ├── feature_engineer.py # 衍生特征如负债收入比 │ ├── train.py # 模型训练与交叉验证 │ ├── evaluate.py # AUC、KS、混淆矩阵 │ └── score_mapping.py # 概率转分数、分数转等级 ├── models/ # 训练好的模型文件 ├── notebooks/ # 探索性分析不参与生产 └── requirements.txt这个结构的好处是训练和评分分离train.py只负责产出模型文件score_mapping.py只负责把概率翻译成业务等级改等级切分点的时候不用重新训练模型。3. 核心模块拆解从原始数据到等级输出3.1 数据加载与字段校验第一步不是急着训练而是把字段类型和取值范围卡住。信用数据里最常见的坑是“收入”字段混进了字符串、负数或者空值直接进模型会报错或者产出离谱分数。加载模块要做三件事读文件、检查必需字段是否存在、对数值字段做基础范围校验。import pandas as pd REQUIRED_COLUMNS [ age, income, debt_ratio, late_count_12m, employment_years, loan_amount, target ] def load_credit_data(path: str) - pd.DataFrame: df pd.read_csv(path) # 缺字段直接失败别让脏数据流到下游 missing [c for c in REQUIRED_COLUMNS if c not in df.columns] if missing: raise ValueError(f缺少必需字段: {missing}) # 数值字段强制转换非法值变 NaN 后续统一处理 numeric_cols [age, income, debt_ratio, late_count_12m, employment_years, loan_amount] for col in numeric_cols: df[col] pd.to_numeric(df[col], errorscoerce) # 年龄和收入做硬边界明显异常的直接置空 df.loc[(df[age] 18) | (df[age] 75), age] None df.loc[df[income] 0, income] None return df这段代码的关键在errorscoerce它把无法转换的值变成 NaN 而不是抛异常保证批量数据能继续往下走。年龄边界 18 到 75 是常见业务约束收入小于等于 0 视为无效。参数path指向原始 CSV字段名要和REQUIRED_COLUMNS对齐换数据集时先改这个列表。3.2 特征工程与 WOE 分箱评分卡的核心是 WOEWeight of Evidence分箱。它把连续变量切成若干段每段用一个值代替既处理了非线性又让逻辑回归的系数有业务含义。分箱数量一般控制在 5 到 8 段太少丢信息太多容易过拟合。下面是一个简化版的分箱和 WOE 计算import numpy as np import pandas as pd def woe_binning(df, feature, target, bins5): # 等频分箱避免某段样本过少 df df.copy() df[bin] pd.qcut(df[feature], qbins, duplicatesdrop) grouped df.groupby(bin)[target].agg([count, sum]) grouped.columns [total, bad] grouped[good] grouped[total] - grouped[bad] # 加 0.5 平滑防止某段坏样本为 0 导致 log 无穷 grouped[bad_rate] (grouped[bad] 0.5) / (grouped[total] 1) grouped[good_rate] (grouped[good] 0.5) / (grouped[total] 1) grouped[woe] np.log(grouped[good_rate] / grouped[bad_rate]) # 用 WOE 值替换原始分箱 woe_map grouped[woe].to_dict() df[feature _woe] df[bin].map(woe_map).astype(float) return df, groupedqcut做等频分箱duplicatesdrop处理重复边界值。平滑项 0.5 是血泪经验某一段如果坏样本为 0log(0)直接炸掉加平滑后数值稳定。返回的grouped表要留着后面做评分映射时每个分箱对应的分值就从这里来。参数bins建议从 5 开始试看每段的坏样本率是否单调不单调就调整分箱数或者换分箱方式。3.3 模型训练与不平衡处理样本不平衡是这类项目的常态。常见做法是给正负样本加权重而不是直接过采样因为过采样容易让模型记住重复样本。逻辑回归里用class_weightbalanced树模型里用scale_pos_weight。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score import joblib def train_scorecard_model(df, feature_cols, target_coltarget): X df[feature_cols].fillna(0) y df[target_col] # 分层切分保证训练集和测试集坏样本比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) model LogisticRegression( class_weightbalanced, # 自动按样本比例加权 max_iter1000, C1.0, # 正则强度越小越保守 solverlbfgs ) model.fit(X_train, y_train) proba model.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, proba) print(f测试集 AUC: {auc:.4f}) joblib.dump(model, models/scorecard_lr.pkl) return model, X_test, y_test, probastratifyy是必须的否则切分后测试集可能坏样本极少AUC 波动很大。class_weightbalanced让少数类在损失函数里权重更高相当于告诉模型“漏掉一个坏客户的代价更大”。C控制正则化金融场景一般偏保守可以从 0.1 到 1.0 之间调。训练完把模型存成 pkl评分服务直接加载不用每次重训。3.4 概率转分数与等级切分模型输出的是 0 到 1 的概率业务看不懂。评分卡用“基础分 各特征分值”的方式转成分数常见设定是基准分 600、好坏比翻倍增减 20 分。简化实现如下import numpy as np def proba_to_score(proba, base_score600, base_odds50, pdo20): # odds good / bad概率是坏样本概率 odds (1 - proba) / np.maximum(proba, 1e-6) factor pdo / np.log(2) offset base_score - factor * np.log(base_odds) score offset factor * np.log(odds) return np.clip(score, 300, 850) # 常见分数区间 def score_to_grade(score): # 等级切分点按业务坏账容忍度定不是拍脑袋 if score 750: return A elif score 680: return B elif score 620: return C elif score 560: return D return Ebase_odds50表示基准分 600 对应的好坏比是 50:1pdo20表示好坏比翻倍分数加 20。这两个参数要和业务对齐改的时候整个分数分布会平移。np.clip把分数卡在 300 到 850防止极端概率产出离谱分数。等级切分点不是固定的要看每个等级的实际坏账率是否落在业务容忍范围内这个后面在改造章节细说。4. 跑起来环境、训练与评分验证4.1 环境准备与数据放置先把虚拟环境建好依赖装齐。数据文件放到data/raw/下命名成application.csv字段名和REQUIRED_COLUMNS对齐。如果手头没有真实数据可以先用脚本造一批模拟数据验证流程字段分布按业务常识设年龄 20 到 60收入对数正态逾期次数 0 到 5 偏多。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python -c import pandas, sklearn, lightgbm; print(依赖就绪)这一步别省依赖没装齐后面报错很难定位。装完先跑一行 import 验证比直接跑训练脚本更容易看出是环境问题还是代码问题。4.2 训练与评估的完整命令训练入口放在src/train.py跑之前确认config.py里的路径对。常见跑法cd src python train.py --data ../data/raw/application.csv --model lr --output ../models/脚本内部会依次做加载、分箱、训练、评估、保存。评估除了 AUC还要看 KS因为风控更关心“好坏样本的分数分布有没有拉开”。KS 在 0.3 以上算可用0.4 以上算不错。如果 AUC 高但 KS 低说明模型排序能力有但区分度不够可能是分数分布太集中需要检查分箱是否合理。4.3 评分接口的本地验证模型训练完用score_mapping.py对几条样本做端到端验证确认概率、分数、等级三者对得上import joblib import pandas as pd from score_mapping import proba_to_score, score_to_grade model joblib.load(../models/scorecard_lr.pkl) sample pd.DataFrame([{ age_woe: 0.12, income_woe: -0.35, debt_ratio_woe: 0.48, late_count_12m_woe: 0.91, employment_years_woe: -0.10, loan_amount_woe: 0.22 }]) proba model.predict_proba(sample)[0, 1] score proba_to_score(proba) print(f违约概率{proba:.4f}, 分数{score:.1f}, 等级{score_to_grade(score)})注意这里喂给模型的是 WOE 后的特征不是原始值。如果直接传原始年龄和收入模型系数对不上分数会完全失真。这是新手最容易翻车的地方训练用 WOE评分用原始值两边特征口径不一致。5. 避坑与排查五个真实踩过的坑5.1 分箱后某段样本为空导致报错现象跑 WOE 分箱时报ValueError: Bin edges must be unique。原因某个特征取值高度集中等频分箱切不出 5 段。解决qcut加duplicatesdrop同时把分箱数降到 3 到 4 段或者改用卡方分箱。分完检查每段样本数少于总样本 5% 的段考虑合并。5.2 训练集和测试集分数分布差异大现象训练集 AUC 0.82测试集只有 0.65。原因切分前做了全局分箱或全局缺失值填充测试集信息泄漏到训练过程。解决所有拟合类操作分箱边界、均值填充、WOE 映射只在训练集上 fit再 transform 到测试集。用 sklearn 的 Pipeline 能强制这个顺序。5.3 等级切分后 E 级客户太少现象按固定分数切分E 级只占 1%业务上没法做拒绝策略。原因切分点拍脑袋定的没看实际分布。解决先输出分数分布的分位数按业务想要的拒绝比例反推切分点。比如想拒绝 8% 的客户就取第 8 百分位作为 E 级下界再往上按比例切 A 到 D。5.4 模型上线后分数整体偏移现象同一批客户隔一个月再评分分数普遍降了 30 分。原因特征分布漂移比如收入字段的缺失率从 5% 涨到 20%填充值拉低了 WOE。解决评分前加特征分布监控缺失率和均值超出训练集阈值就告警别等分数异常了才回头查。5.5 用准确率评估模型现象模型准确率 95%但坏客户一个没抓出来。原因样本不平衡下准确率没有意义全判好人也能到 96%。解决评估指标固定用 AUC、KS 和分等级后的坏账率别再看 accuracy。混淆矩阵要看召回率风控场景漏判坏客户的代价远大于误判好客户。6. 二次开发把等级切分点变成可调参数这套系统最值得改的地方不是模型而是等级切分逻辑。模型换一次成本高切分点调一次只要几分钟但业务效果立竿见影。我一般会把切分点从硬编码改成配置驱动放在config.py里GRADE_THRESHOLDS { A: 750, B: 680, C: 620, D: 560, # 低于 560 归 E } def score_to_grade_configurable(score, thresholdsGRADE_THRESHOLDS): for grade in [A, B, C, D]: if score thresholds[grade]: return grade return E改完之后验证方法不是看代码有没有报错而是跑一遍历史数据输出每个等级的样本占比和实际坏账率做成一张对照表等级样本占比实际坏账率业务容忍上限A22%0.8%1.5%B31%2.1%3.0%C25%4.5%5.0%D15%8.2%9.0%E7%15.6%18.0%这张表出来切分点合不合理一眼就能看出来。如果 C 级坏账率贴着上限说明 C 级下界可以再往上提一点把一部分客户压到 D 级。如果 E 级占比太高但坏账率没超说明拒绝策略可以放松放一部分回 D 级做降额而不是直接拒。另一个改造方向是加模型对比。逻辑回归跑通后用 LightGBM 跑同一套特征比较 AUC 和 KS。如果树模型明显更好别急着替换先看特征重要性把树模型发现的重要交互项手工加到逻辑回归里既保住可解释性又提升效果。我见过太多项目直接上树模型结果合规评审过不了回头重做评分卡。从那以后我每次调等级切分点都强制先跑一遍历史数据的等级分布和坏账率对照表不看完这张表不改配置。模型可以慢慢迭代切分点改错一次业务侧就是真金白银的损失。希望这套拆解能帮你把项目跑起来少走几个我走过的弯路。本文还有配套的精品资源点击获取