
简介这份资源面向金融风控从业者、数据科学学习者及信贷场景开发者提供一套基于机器学习的信用卡申请风险评分系统用于评估申请人信用风险等级辅助银行与消费金融机构提升审批效率与自动化水平。压缩包共4个文件包含1个py脚本、1个ipynb交互式笔记、1个csv训练数据集和1个md说明文档整体约18.19MB覆盖从数据读取到模型落地的完整链路。系统功能涵盖数据预处理与特征工程、探索性数据分析、模型训练与评估及金融风控适配支持逻辑回归、随机森林、XGBoost等主流分类模型并输出混淆矩阵、ROC曲线、KS曲线等可视化结果同时借助toad工具支持评分卡构建流程。目前已有149人学习下载适合希望掌握信用评分卡开发、模型可解释性分析与风控建模实战的读者参考。1. 从一份信用风险评分系统源码说起它到底能跑出什么结果打开这份基于机器学习信用风险等级评分系统的源码包第一眼看到的不是模型文件而是一堆 CSV 和几个 Python 脚本。这恰恰是它最实在的地方——它把「金融风控里怎么把一个人的信用分成 A/B/C/D 等级」这件事从数据清洗一路铺到模型评估中间没有藏着掖着的黑匣子。你拿到的是一套能跑通、能改参数、能换数据集的完整流程而不是一个只告诉你「用 XGBoost 效果很好」的 PPT。这套东西解决的核心问题是给定一批带标签的客户数据比如历史违约记录、收入、负债率、查询次数怎么训练出一个模型输出每个客户的违约概率再按概率切分成不同信用等级。适合谁做金融风控策略的工程师、需要交课程设计的学生、想从零搭一套评分卡原型的开发者。它不承诺生产级高可用但能让你在本地把「数据进、等级出」这条链路完整走一遍知道每个环节的参数动了之后结果会怎么变。2. 数据管道与特征工程评分卡的第一道生死线2.1 原始数据长什么样先做一次体检拿到数据别急着往模型里灌。我一般会先跑一遍结构探查看看缺失率、类别分布和异常值。这份源码里通常带一个data/目录里面是 CSV 格式的客户记录。下面这段代码是我习惯用的体检脚本你可以直接抄import pandas as pd import numpy as np # 读取原始数据注意编码金融数据常见 gbk 或 utf-8 df pd.read_csv(data/credit_data.csv, encodingutf-8) # 基础信息行数、列数、每列类型 print(df.info()) # 缺失率排序超过 60% 的列直接考虑丢弃 missing_rate df.isnull().sum() / len(df) print(missing_rate.sort_values(ascendingFalse).head(20)) # 标签分布看是否极度不平衡 if target in df.columns: print(df[target].value_counts(normalizeTrue)) # 数值列描述统计重点看 min/max 有没有离谱值 print(df.describe().T[[min, max, mean, std]])逻辑说明info()让你知道哪些列是 object 类型需要编码哪些是数值型。缺失率排序是决定「填充还是丢弃」的依据金融数据里查询次数、负债率这类字段缺失往往有业务含义不能无脑填 0。标签分布直接决定后面要不要用class_weight或过采样。describe()里的 min/max 能抓出年龄 999、收入负数这种脏数据。参数说明encoding根据实际文件调整读不进来就试gbk或latin1。缺失率阈值我一般设 0.6高于这个值的列除非业务强依赖否则先拿掉。标签分布如果正负比超过 1:10后面训练必须处理不平衡。2.2 分箱、WOE 和 IV为什么不用原始值直接喂模型信用评分领域有一个绕不开的传统连续变量先分箱再算 WOEWeight of Evidence和 IVInformation Value。这不是玄学是因为逻辑回归这类线性模型对非线性关系不敏感分箱能把「年龄 25 和 45 的风险差异」变成离散的、单调的区间。即便你用 XGBoost分箱后的特征往往也更稳因为极端值被区间吸收了。源码里一般会有一个feature_engineering.py或类似文件。核心步骤是对每个连续变量用卡方分箱或等频分箱切成 510 箱计算每箱的 WOE再用 IV 筛选特征。IV 的经验阈值小于 0.02 预测力太弱0.020.1 弱0.10.3 中等0.30.5 强大于 0.5 要怀疑数据泄漏。def calculate_woe_iv(df, feature, target): 计算单个特征的 WOE 和 IV df: 包含特征和目标列的数据框 feature: 特征列名 target: 目标列名1 代表违约0 代表正常 # 按特征值分组统计好坏样本 grouped df.groupby(feature)[target].agg([count, sum]) grouped.columns [total, bad] grouped[good] grouped[total] - grouped[bad] # 防止除零加一个极小值 grouped[bad_rate] grouped[bad] / grouped[bad].sum() 1e-10 grouped[good_rate] grouped[good] / grouped[good].sum() 1e-10 # WOE ln(坏样本占比 / 好样本占比) grouped[woe] np.log(grouped[bad_rate] / grouped[good_rate]) # IV sum((坏样本占比 - 好样本占比) * WOE) grouped[iv] (grouped[bad_rate] - grouped[good_rate]) * grouped[woe] iv_value grouped[iv].sum() return grouped, iv_value逻辑说明WOE 衡量的是某个分箱里坏样本与好样本的比例相对于整体比例的偏离程度。WOE 为正说明该箱坏样本占比高于平均风险高为负则风险低。IV 是 WOE 的加权和用来量化整个特征的预测能力。这段代码里1e-10是防止某个分箱没有坏样本或好样本导致除零。参数说明target列必须是 0/1 编码1 代表违约。分箱后的特征列应该是类别型比如age_bin取值 07。如果 IV 大于 0.5先别高兴检查一下这个特征是不是包含了未来信息比如「最近一次还款状态」这种贷后字段。2.3 特征筛选与数据集切分别让未来数据混进训练集特征算完 IV 之后按 IV 从高到低排序通常保留 IV 0.02 且不在黑名单里的特征。黑名单包括客户 ID、时间戳、贷后行为字段。然后切分训练集和测试集。这里有一个血泪经验金融数据时间性强随机切分会导致「未来样本」进入训练集线下 AUC 虚高上线就翻车。正确做法是按时间切比如前 70% 时间做训练后 30% 做测试。from sklearn.model_selection import train_test_split # 假设已经按时间排序time_col 是时间列 df df.sort_values(apply_date) # 按时间点切分前 70% 训练 split_idx int(len(df) * 0.7) train_df df.iloc[:split_idx] test_df df.iloc[split_idx:] # 如果数据量小至少也要保证测试集里好坏样本都有 print(train_df[target].value_counts()) print(test_df[target].value_counts())逻辑说明sort_values保证时间顺序iloc按位置切分。打印两边的标签分布是为了确认测试集里没有出现「全是好样本」或「全是坏样本」的极端情况否则 AUC 没法算。参数说明切分比例 0.7/0.3 是常见起点数据量少可以 0.8/0.2。如果测试集坏样本少于 50 个评估指标波动会很大考虑交叉验证或延长数据窗口。3. 模型训练与调参逻辑回归、树模型怎么选参数怎么定3.1 逻辑回归做基线可解释性是风控的硬需求在信用评分场景逻辑回归不是「过时」而是「合规」。很多金融机构要求评分卡必须能解释每个特征对最终分数的贡献树模型在这方面天然吃亏。源码里通常会把逻辑回归作为 baseline用 WOE 转换后的特征训练。from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, roc_curve import matplotlib.pyplot as plt # 假设 X_train 是 WOE 转换后的特征矩阵y_train 是标签 lr LogisticRegression( penaltyl2, # L2 正则防止过拟合 C1.0, # 正则强度倒数越小正则越强 class_weightbalanced, # 自动按类别频率加权 solverlbfgs, max_iter1000, random_state42 ) lr.fit(X_train, y_train) # 预测概率并算 AUC y_pred_proba lr.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, y_pred_proba) print(fTest AUC: {auc:.4f}) # 画 ROC 曲线 fpr, tpr, _ roc_curve(y_test, y_pred_proba) plt.plot(fpr, tpr, labelfLR (AUC{auc:.3f})) plt.plot([0, 1], [0, 1], k--) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.legend() plt.show()逻辑说明class_weightbalanced在不平衡数据里很关键它让模型对少数类违约样本的损失更敏感。C控制正则强度C 越小模型越保守系数被压缩得越厉害。AUC 是排序指标衡量模型把坏样本排在好样本前面的能力风控里比准确率更有意义。参数说明penalty选 l2 还是 l1 取决于你要不要做特征选择l1 会把不重要的特征系数压到 0。max_iter设 1000 是为了避免「未收敛」警告如果还报错就继续加大或换saga求解器。random_state固定住保证每次跑结果一致。3.2 树模型提效果XGBoost 的参数怎么调不翻车如果逻辑回归的 AUC 不够看下一步通常是上 XGBoost 或 LightGBM。树模型能自动捕捉特征交互和非线性但参数多调不好容易过拟合。源码里一般会有一个train_xgb.py核心参数就那么几个max_depth、learning_rate、n_estimators、subsample、colsample_bytree。import xgboost as xgb from sklearn.metrics import roc_auc_score # 构建 DMatrixXGBoost 的高效数据格式 dtrain xgb.DMatrix(X_train, labely_train) dtest xgb.DMatrix(X_test, labely_test) # 参数设置先保守再逐步放开 params { objective: binary:logistic, # 二分类输出概率 eval_metric: auc, # 评估指标用 AUC max_depth: 4, # 树深风控场景建议 36 learning_rate: 0.05, # 学习率小一点更稳 subsample: 0.8, # 行采样防过拟合 colsample_bytree: 0.8, # 列采样防过拟合 min_child_weight: 5, # 叶子最小样本权重和越大越保守 scale_pos_weight: 3, # 正负样本权重比根据不平衡程度调 seed: 42 } # 训练带早停 evals [(dtrain, train), (dtest, eval)] bst xgb.train( params, dtrain, num_boost_round500, evalsevals, early_stopping_rounds30, # 30 轮没提升就停 verbose_eval50 ) # 用最优迭代次数预测 y_pred bst.predict(dtest, iteration_range(0, bst.best_iteration 1)) print(fXGBoost Test AUC: {roc_auc_score(y_test, y_pred):.4f})逻辑说明max_depth控制模型复杂度风控数据噪声大树太深会记住噪声。learning_rate和n_estimators是一对学习率小就需要更多树但泛化更好。subsample和colsample_bytree是随机采样让每棵树看到不同的数据子集降低方差。scale_pos_weight是不平衡数据的另一种处理方式值约等于负样本数除以正样本数。参数说明early_stopping_rounds是后悔药防止树越加越多反而过拟合。iteration_range确保预测时只用最优的那几棵树而不是全部 500 棵。如果 AUC 在训练集上 0.9、测试集上 0.7说明过拟合了先把max_depth降到 3再把min_child_weight加到 10。3.3 模型评估与分数映射AUC、KS 和等级切分训练完模型风控里看的不只是 AUC。KS 值衡量的是好坏样本累计分布的最大差值通常 KS 0.3 才算可用。然后要把概率映射成信用等级常见做法是按分数分位切或者用pd.cut按业务阈值切。from scipy.stats import ks_2samp def calculate_ks(y_true, y_pred_proba): 计算 KS 值 good y_pred_proba[y_true 0] bad y_pred_proba[y_true 1] ks_stat, _ ks_2samp(good, bad) return ks_stat ks calculate_ks(y_test, y_pred) print(fKS: {ks:.4f}) # 按概率分位切分成 A/B/C/D 四个等级 import pandas as pd result pd.DataFrame({prob: y_pred, target: y_test}) result[grade] pd.qcut(result[prob], q4, labels[A, B, C, D]) print(result.groupby(grade)[target].agg([count, mean]))逻辑说明ks_2samp直接算两组分布的 KS 统计量。pd.qcut按分位数切保证每个等级样本量接近。打印每个等级的违约率均值理想情况下 A 到 D 的违约率应该单调递增如果 D 的违约率反而比 C 低说明模型排序有问题。参数说明q4是切四段业务上常用 57 个等级。如果某个等级样本太少可以改成按业务阈值切比如概率 0.1 为 A0.10.3 为 B以此类推。4. 避坑与排查评分系统落地时最容易翻车的五个地方4.1 现象线下 AUC 0.85上线一周坏账率没降原因训练集和测试集随机切分未来数据泄漏进了训练集。比如「最近 6 个月查询次数」这个特征在训练时用了全量数据计算但上线时只能拿到历史数据分布完全变了。解决按时间切分数据集所有特征计算只能使用当前时间点之前的数据。如果已经上线先做特征分布稳定性监控PSI 大于 0.2 的特征直接下线。4.2 现象模型预测概率集中在 0.5 附近区分度极差原因特征没有做 WOE 转换或者 WOE 计算时用了全量数据导致过拟合。另外逻辑回归的class_weight设了balanced但没调整阈值概率被压缩。解决检查 WOE 计算是否只在训练集上拟合然后应用到测试集。逻辑回归输出概率后用验证集找最佳阈值而不是固定 0.5。树模型可以调scale_pos_weight或直接用predict_proba的排序结果。4.3 现象某个特征 IV 高达 0.8但业务方说这个字段上线时拿不到原因用了贷后字段或未来信息。比如「最后一次还款状态」「当前逾期天数」这些字段在申请时点根本不存在。解决维护一个特征黑名单所有贷后行为字段、ID 类字段、时间戳字段一律排除。IV 过高反而是警报先查数据来源再决定要不要用。4.4 现象训练时 AUC 0.9测试时 0.6调参也没用原因过拟合。树模型深度太大、叶子节点样本太少或者特征数量远大于样本量。解决先降max_depth到 34加min_child_weight加subsample和colsample_bytree。如果还不行做特征选择只保留 IV 排名前 20 的特征。样本量少于 5000 时逻辑回归往往比树模型更稳。4.5 现象等级切分后 D 级违约率比 C 级还低原因概率排序和实际风险排序不一致通常是模型没有单调性约束或者某个特征和标签的关系是非单调的。解决在逻辑回归里检查每个 WOE 特征的系数符号如果某个特征的系数为正但业务上应该是负相关说明 WOE 计算反了。树模型可以加单调性约束XGBoost 支持monotone_constraints参数。5. 进阶技巧用 PSI 监控模型衰减把评分系统跑成长期工程模型上线不是终点。信用风险评分系统最怕的是「训练时好好的三个月后突然不准了」。这时候需要 PSIPopulation Stability Index来监控特征分布和分数分布的偏移。PSI 小于 0.1 说明稳定0.10.25 需要关注大于 0.25 就要考虑重新训练。def calculate_psi(expected, actual, buckets10): 计算 PSIexpected 是训练集分布actual 是新数据分布 # 按 expected 的分位数切桶 breakpoints np.percentile(expected, np.linspace(0, 100, buckets 1)) breakpoints[0] -np.inf breakpoints[-1] np.inf expected_counts np.histogram(expected, binsbreakpoints)[0] / len(expected) actual_counts np.histogram(actual, binsbreakpoints)[0] / len(actual) # 防止除零 expected_counts np.where(expected_counts 0, 1e-6, expected_counts) actual_counts np.where(actual_counts 0, 1e-6, actual_counts) psi np.sum((actual_counts - expected_counts) * np.log(actual_counts / expected_counts)) return psi # 对每个特征和最终分数算 PSI for col in feature_cols: psi_val calculate_psi(train_df[col], test_df[col]) if psi_val 0.25: print(f警告{col} PSI{psi_val:.4f}分布偏移严重)逻辑说明PSI 的本质是比较两个分布在每个分桶里的占比差异取对数后加权求和。分桶按训练集的分位数切保证每桶样本量均匀。1e-6是防止某个桶里没有样本导致 log 无穷大。参数说明buckets10是常用值样本量少可以降到 5。PSI 阈值 0.25 是经验值不同业务可以调整。除了特征 PSI还要对最终评分做 PSI 监控分数 PSI 超过 0.1 就说明模型输出分布变了。我自己的习惯是每次重新训练模型之前先把上一版模型的 PSI 报告拉出来看看是哪些特征漂移最厉害。如果某个特征的 PSI 连续三个月上升不管 AUC 掉没掉我都会把它放进观察名单下次迭代优先处理。这套流程跑下来评分系统就不是一个「交完课程设计就扔」的脚本而是一个能持续跟踪、持续修正的风控组件。希望帮到你。本文还有配套的精品资源点击获取