尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python申请评分卡模型实战:从数据集准备到分数校准全流程
简介本资源面向金融风控与数据挖掘方向的学习者提供一套用Python实现申请评分卡模型的完整数据集与配套代码帮助理解从原始申请数据到信用评分的全流程。压缩包共14个文件约9.22MB包含1个application.csv原始数据集、5个Python脚本覆盖A_Card主流程、测试、GBDT与DNN建模、7个pkl序列化文件存放训练/测试数据、WOE字典、分箱字典、编码字典及逻辑回归模型以及1个LR_classweight权重文件便于直接复现数据预处理、特征选择、模型训练与评估、打分规则创建等环节。目前已有1696人学习下载适合希望掌握评分卡原理、练习sklearn建模与分箱编码技巧的初中级数据从业者也可作为信贷风控项目的参考方案。1. 申请评分卡模型到底在算什么从一份逾期名单说起手里拿到一份三万行的信贷申请记录字段包括年龄、月收入、负债比、历史逾期次数、近半年查询次数还有一列label——0 表示正常还款1 表示逾期超过 90 天。业务方要的不是一份报表而是一个能在新客户进件时实时吐出 0 到 1000 分的打分函数分数越高越安全低于某个阈值直接拒件。这就是申请评分卡模型Application Scorecard要解决的事而python的申请评分卡模型 数据集这个组合本质是在问用 Python 把一份带标签的申请数据训练成一张可解释、可上线、可监控的评分卡数据集该怎么准备、模型该怎么搭、分数该怎么映射。它适合三类人做风控策略但想自己跑通建模全流程的从业者、手里有业务数据想验证评分卡可行性的数据同学、以及被要求「用 Python 出一版评分卡」但不确定从哪下手的工程师。不适合指望调个库就出分的人——评分卡的坑大多不在模型本身而在数据切分、WOE 分箱和分数校准这三段。2. 数据集准备申请评分卡的数据长什么样、怎么切2.1 申请评分卡数据集的字段结构与标签定义一份能直接用于申请评分卡的数据集通常不是单一文件而是「申请主表 行为衍生表」的组合。主表一行一个申请件包含申请时点可获取的信息身份属性年龄、婚姻、学历、收入负债月收入、月负债、负债收入比、申请行为申请金额、期限、渠道、征信查询近 3 个月查询次数、近 6 个月查询次数。行为衍生表则是从历史还款记录里算出来的比如近 12 个月最大逾期天数、历史最长逾期期数、当前未结清贷款笔数。标签列一般叫label或target取值 0/11 代表「坏」。这里有个容易被忽略的点坏的定义必须和业务口径一致。有的机构把 M1逾期 1 期就算坏有的要 M3逾期 90 天以上才算坏。口径不同同一份数据训出来的分数分布完全不同。我一般会在数据字典里明确写一行label1 定义为观察点后 6 个月内出现 90 天以上逾期。观察点observation point和表现期performance window是申请评分卡数据集的两个时间锚点。观察点是你采集申请信息的时点表现期是观察点之后用来判定好坏的时间长度。常见做法是观察点后 6 到 12 个月。如果表现期太短很多坏样本还没暴露标签会偏乐观太长则数据新鲜度下降模型上线后衰减快。提示如果数据集里没有明确的时间列先别急着建模。没有申请时间就无法做时间外样本验证评分卡上线后的稳定性无从谈起。2.2 用 Python 做时间外切分而不是随机切分申请评分卡最忌讳随机切分训练集和测试集。随机切分会让同一时期的样本同时出现在训练和测试里模型「见过」那个时期的客群特征评估结果虚高。正确做法是按申请时间切用较早时间段做训练较晚时间段做验证和测试。import pandas as pd import numpy as np # 假设 df 已加载包含 apply_date 列和 label 列 df[apply_date] pd.to_datetime(df[apply_date]) # 按时间排序 df df.sort_values(apply_date).reset_index(dropTrue) # 取 70% 时间点作为训练集截止15% 验证15% 测试 n len(df) train_end int(n * 0.7) valid_end int(n * 0.85) train df.iloc[:train_end].copy() valid df.iloc[train_end:valid_end].copy() test df.iloc[valid_end:].copy() print(f训练集 {train.shape}坏样本率 {train[label].mean():.4f}) print(f验证集 {valid.shape}坏样本率 {valid[label].mean():.4f}) print(f测试集 {test.shape}坏样本率 {test[label].mean():.4f})这段代码的逻辑是按申请时间排序后顺序切分保证训练集的时间早于验证集验证集早于测试集。参数上70/15/15 是常见比例但如果数据量小可以改成 60/20/20。切完后一定要打印三段的坏样本率如果测试集坏样本率比训练集高出一倍以上说明客群随时间发生了明显迁移模型上线后需要更频繁地监控。切分之后还要检查一件事训练集和测试集的特征分布是否一致。常用做法是对每个数值特征做 PSIPopulation Stability Index计算PSI 大于 0.25 说明分布差异较大这个特征在时间外样本上可能不稳定。def calc_psi(base, compare, bins10): # 基于 base 分箱统计 compare 的分布偏移 breakpoints np.percentile(base, np.linspace(0, 100, bins 1)) breakpoints np.unique(breakpoints) base_counts np.histogram(base, binsbreakpoints)[0] / len(base) compare_counts np.histogram(compare, binsbreakpoints)[0] / len(compare) # 避免除零 base_counts np.where(base_counts 0, 1e-6, base_counts) compare_counts np.where(compare_counts 0, 1e-6, compare_counts) psi np.sum((compare_counts - base_counts) * np.log(compare_counts / base_counts)) return psi for col in [age, monthly_income, debt_ratio, query_3m]: psi_val calc_psi(train[col].dropna(), test[col].dropna()) print(f{col} 的 PSI {psi_val:.4f})PSI 的计算逻辑是以训练集的分位点为切分点分别统计训练集和测试集落在每个区间的比例再用比例差乘以对数比求和。参数bins10是经验值数据量大时可以加到 20。PSI 小于 0.1 说明稳定0.1 到 0.25 需要关注大于 0.25 建议在建模时对这个特征做特殊处理比如只保留粗分箱或直接剔除。3. 从原始字段到 WOE 分箱评分卡可解释性的来源3.1 为什么申请评分卡必须做 WOE 分箱申请评分卡和普通二分类模型最大的区别是可解释性要求。业务方要能回答「这个客户为什么被拒」监管也要能审查模型是否使用了歧视性变量。WOEWeight of Evidence分箱把连续变量切成若干段每段用一个 WOE 值替代原始值WOE 的计算公式是WOE ln(该箱坏样本占比 / 该箱好样本占比)WOE 越大说明这个箱里的坏样本比例越高风险越大。IVInformation Value则是各箱 WOE 的加权和用来衡量一个变量的整体区分能力。IV 小于 0.02 基本没用0.02 到 0.1 弱0.1 到 0.3 中等0.3 到 0.5 强大于 0.5 要警惕过拟合。分箱的另一个好处是处理缺失值和异常值。月收入缺失可以单独成一箱月收入 999999 这种异常值也可以归到最高箱不需要额外做填充。def calc_woe_iv(df, feature, target, bins5): # 等频分箱 df df[[feature, target]].dropna().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] # 防止除零 grouped[bad] grouped[bad].replace(0, 0.5) grouped[good] grouped[good].replace(0, 0.5) grouped[bad_pct] grouped[bad] / grouped[bad].sum() grouped[good_pct] grouped[good] / grouped[good].sum() grouped[woe] np.log(grouped[bad_pct] / grouped[good_pct]) grouped[iv] (grouped[bad_pct] - grouped[good_pct]) * grouped[woe] iv_total grouped[iv].sum() return grouped, iv_total for col in [age, monthly_income, debt_ratio, query_3m, history_overdue]: _, iv calc_woe_iv(train, col, label, bins5) print(f{col} 的 IV {iv:.4f})这段代码用等频分箱把连续变量切成 5 段然后计算每段的 WOE 和 IV。参数bins5是起步值实际项目中会根据 IV 调整通常 3 到 8 箱比较常见。replace(0, 0.5)是为了避免某个箱里没有坏样本或好样本导致对数无穷大这是血泪经验——不加这一步代码会在某些稀疏特征上直接报错。3.2 分箱单调性检查与手动调整自动分箱出来的结果不一定单调。比如年龄分箱可能出现「青年风险高、中年风险低、老年风险又高」的 U 型这种非单调关系在评分卡里很难解释业务方会质疑「为什么 55 岁比 35 岁风险高」。常见做法是合并相邻箱直到 WOE 单调。def merge_to_monotonic(grouped): # 按分箱顺序检查 WOE 是否单调不单调则合并相邻箱 grouped grouped.reset_index() while True: woe grouped[woe].values # 检查是否单调递增或递减 inc all(woe[i] woe[i1] for i in range(len(woe)-1)) dec all(woe[i] woe[i1] for i in range(len(woe)-1)) if inc or dec: break # 找到第一个违反单调的位置合并该箱与下一箱 for i in range(len(woe)-1): if (woe[i] woe[i1] and not dec) or (woe[i] woe[i1] and not inc): grouped.loc[i, total] grouped.loc[i1, total] grouped.loc[i, bad] grouped.loc[i1, bad] grouped.loc[i, good] grouped.loc[i1, good] grouped grouped.drop(i1).reset_index(dropTrue) break # 重新计算 WOE grouped[bad_pct] grouped[bad] / grouped[bad].sum() grouped[good_pct] grouped[good] / grouped[good].sum() grouped[woe] np.log(grouped[bad_pct] / grouped[good_pct]) return grouped合并逻辑是只要 WOE 序列不是单调递增也不是单调递减就找到第一个破坏单调性的相邻对把它们合并重新计算 WOE直到单调为止。这个函数在实际项目里能省掉大量手工调箱的时间。但要注意合并后箱数可能变得很少如果少于 3 箱这个特征的区分度可能已经不够了需要考虑是否保留。分箱完成后把每个特征的 WOE 值映射回原始数据形成 WOE 矩阵。这个矩阵就是后续逻辑回归的输入。def apply_woe_mapping(df, feature, grouped): # 根据分箱边界把原始值映射为 WOE df df.copy() bins grouped[bin].cat.categories if hasattr(grouped[bin], cat) else grouped[bin] # 用 pd.cut 按相同边界切分 df[bin] pd.cut(df[feature], binsbins, duplicatesdrop) woe_map dict(zip(grouped[bin], grouped[woe])) df[feature _woe] df[bin].map(woe_map).fillna(0) return df映射时要注意训练集的分箱边界必须保存下来测试集和线上新样本都用同一套边界。如果每次都用新数据重新分箱线上分数会漂移。我一般会把分箱边界和 WOE 映射表存成 JSON 或 pickle和模型文件放在一起。4. 逻辑回归建模与分数校准从概率到 0-1000 分4.1 用 WOE 矩阵训练逻辑回归并检查系数申请评分卡的主流模型是逻辑回归不是因为它效果最好而是因为它可解释、系数方向明确、上线后容易监控。用 WOE 矩阵作为输入逻辑回归的系数就代表了每个特征对风险的贡献方向。from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, roc_curve # 假设 woe_cols 是 WOE 特征列名列表 woe_cols [c for c in train.columns if c.endswith(_woe)] X_train train[woe_cols].fillna(0) y_train train[label] X_test test[woe_cols].fillna(0) y_test test[label] model LogisticRegression( penaltyl2, C1.0, class_weightbalanced, max_iter1000, solverlbfgs ) model.fit(X_train, y_train) # 输出系数 coef_df pd.DataFrame({ feature: woe_cols, coef: model.coef_[0] }).sort_values(coef) print(coef_df) # 评估 train_pred model.predict_proba(X_train)[:, 1] test_pred model.predict_proba(X_test)[:, 1] print(f训练集 AUC {roc_auc_score(y_train, train_pred):.4f}) print(f测试集 AUC {roc_auc_score(y_test, test_pred):.4f})参数上class_weightbalanced是因为申请评分卡的坏样本率通常很低5% 到 15%不加权的话模型会偏向预测好样本。C1.0是正则化强度的倒数C 越小正则化越强如果系数绝对值普遍偏大可以降到 0.1 试试。max_iter1000是防止迭代不收敛WOE 特征之间相关性较高时容易不收敛。系数检查是评分卡建模的关键一步。所有系数应该为负——WOE 越大代表风险越高逻辑回归里 WOE 的系数应该为负这样 WOE 增大时预测为坏的概率也增大。如果某个特征的系数为正说明它和标签的关系反了要么是分箱有问题要么是这个特征在业务上就不该用。4.2 把概率映射成 0-1000 分的标准做法逻辑回归输出的是概率业务方要的是分数。标准做法是用「基点 倍数」的线性映射score A - B * ln(odds)其中 odds p / (1-p)p 是坏样本概率。A 和 B 由两个锚点决定某个基准分数对应某个基准 odds以及 odds 翻倍时分数下降多少。import numpy as np # 设定锚点基准分数 600 对应 odds1:20即坏好比 1/20 # 分数每降低 50 分odds 翻倍 base_score 600 base_odds 1 / 20 pdo 50 # points to double the odds B pdo / np.log(2) A base_score B * np.log(base_odds) def prob_to_score(prob): # prob 是坏样本概率 prob np.clip(prob, 1e-6, 1 - 1e-6) odds prob / (1 - prob) score A - B * np.log(odds) return np.round(score).astype(int) test_scores prob_to_score(test_pred) print(f测试集分数范围{test_scores.min()} - {test_scores.max()}) print(f测试集分数均值{test_scores.mean():.1f})这段代码的逻辑是先根据锚点算出 A 和 B再把每个样本的坏概率转成 odds最后用线性公式得到分数。参数base_score600、base_odds1/20、pdo50是行业常见设定但不同机构会调整。比如有的机构用 500 分对应 odds1:50pdo40。关键是上线前和业务方确认好这套映射因为分数阈值直接决定通过率。分数映射完之后要做一次分数分布对比训练集和测试集的分数分布是否接近。如果测试集分数整体偏低说明新客群风险在上升可能需要调整通过阈值。import matplotlib.pyplot as plt plt.figure(figsize(10, 5)) plt.hist(prob_to_score(train_pred), bins50, alpha0.5, labeltrain, densityTrue) plt.hist(prob_to_score(test_pred), bins50, alpha0.5, labeltest, densityTrue) plt.xlabel(score) plt.ylabel(density) plt.legend() plt.title(Score Distribution: Train vs Test) plt.show()如果两条分布曲线错位明显先别急着上线回头检查测试集的时间段是不是客群发生了结构性变化或者某个特征在测试集上出现了训练集没见过的取值。5. 避坑与排查申请评分卡建模中最容易翻车的 5 个地方5.1 现象AUC 很高但上线后坏账率没降原因随机切分导致数据泄漏。同一时期的样本同时出现在训练和测试里模型记住了那个时期的客群特征时间外样本上表现大幅下降。解决强制按时间切分并且用 PSI 检查每个特征在训练集和测试集上的分布差异。PSI 大于 0.25 的特征要么剔除要么只保留粗分箱。5.2 现象某个特征的逻辑回归系数为正原因WOE 分箱时把好坏样本比例算反了或者这个特征本身和标签的关系就是正的比如「历史逾期次数」越多风险越高但 WOE 计算时坏样本占比反而下降。解决先检查 WOE 计算公式确认分子是坏样本占比、分母是好样本占比。如果公式没错检查这个特征的分箱是否合理必要时手动调整分箱边界。如果调整后系数仍为正考虑剔除该特征。5.3 现象模型在测试集上 AUC 0.75但分数分布和训练集错位严重原因训练集和测试集的坏样本率差异大或者某个关键特征在测试集上缺失率高。解决先对比两段的坏样本率如果差异超过 50%说明客群迁移明显需要重新审视观察点和表现期的设定。然后检查每个特征在两段的缺失率缺失率差异大的特征要做特殊处理。5.4 现象分箱后某些箱的样本量极少原因等频分箱在数据分布不均匀时会切出样本量差异很大的箱极端情况下某个箱只有几十个样本。解决分箱后检查每箱样本量少于总样本 5% 的箱要和相邻箱合并。合并后重新计算 WOE 和 IV。如果合并后箱数少于 3考虑这个特征是否值得保留。5.5 现象上线后分数整体漂移通过率异常波动原因线上新样本的 WOE 映射和训练时不一致或者某个特征的取值超出了训练集的分箱边界。解决把训练时的分箱边界和 WOE 映射表固化下来线上用同一套映射。对于超出边界的取值归到最近的箱不要重新分箱。同时建立分数监控每天统计分数分布和通过率发现漂移及时排查。6. 进阶技巧用评分卡做通过率模拟与阈值选择模型训完、分数映射完最后一步是选阈值。阈值不是拍脑袋定的要用通过率模拟来选。做法是把测试集按分数从高到低排序设定不同的分数阈值计算每个阈值下的通过率和通过样本的坏账率画出一条权衡曲线。def simulate_threshold(scores, labels, thresholds): results [] for t in thresholds: approved scores t approval_rate approved.mean() if approved.sum() 0: bad_rate 0 else: bad_rate labels[approved].mean() results.append({ threshold: t, approval_rate: approval_rate, bad_rate: bad_rate, approved_bad_count: labels[approved].sum() }) return pd.DataFrame(results) thresholds np.arange(400, 800, 20) sim_df simulate_threshold(test_scores, y_test.values, thresholds) print(sim_df)这段代码的逻辑是对每个候选阈值计算通过率分数大于等于阈值的样本占比和通过样本的坏账率。业务方通常会给一个目标坏账率比如「通过样本的坏账率控制在 3% 以内」然后从模拟结果里找到满足条件的最高阈值这样通过率最大。参数上thresholds的范围要根据实际分数分布调整。如果测试集分数集中在 500 到 700就从 500 扫到 700步长 10 或 20。步长太粗会漏掉最优阈值太细则计算量大但收益有限。除了通过率模拟还有一个进阶做法是分段授信。不同分数段给不同额度分数高的给高额度分数低的给低额度。这需要把额度也纳入模拟计算每个分数段的预期损失。预期损失 通过率 × 坏账率 × 平均损失金额。这个计算需要业务方提供平均损失金额的估计通常用历史坏账的平均未还本金来近似。我自己的习惯是每次做完评分卡都会把分箱边界、WOE 映射表、模型系数、分数映射参数、阈值模拟结果这五样东西打包成一个版本目录目录名带上日期和 AUC。上线后每周跑一次分数分布监控每月跑一次 PSI 和 AUC 回溯。这套流程跑顺了评分卡就不是一次性的建模项目而是一个可以持续迭代的风控组件。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Blender 遇到的一个问题:Rigify 报错 metarig 与 spine.004 的排查思路

Blender 遇到的一个问题:Rigify 报错 metarig 与 spine.004 的排查思路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/10 17:38:46
Xmanager 之 Xbrowser 访问 Linux 远程桌面:从连接失败到稳定会话的排查与配置

Xmanager 之 Xbrowser 访问 Linux 远程桌面:从连接失败到稳定会话的排查与配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/10 17:38:46
CSS设置链接鼠标(失效)不能点样式:TaoToken 统一 Key 通道下的前端调试与验证

CSS设置链接鼠标(失效)不能点样式:TaoToken 统一 Key 通道下的前端调试与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/10 17:38:46
MORE NEWS

更多资讯

📰

医疗品牌全案咨询与超级符号IP

医疗品牌全案与超级符号IP:美奥口腔如何携手奇正沐古构筑口腔健康消费信任心智在消费医疗赛道,不少具备设备、医师资源的连锁机构普遍存在认知误区:把经营重心放在门诊服务、获客投放,低估品牌全案、IP 与符号设计对用户心智建设的…

📰

冷藏车温度探头装哪儿才准?探头位置与送风回风区别

冷藏车温度探头装哪儿才准?探头位置与送风回风区别很多人看冷藏车温度,只看温控器上的数字,不问探头装在哪。探头贴送风口和放在回风区,读数能差出一截,位置不对,显示达标也不代表货物周围达标。这篇讲探头…

📰

Cursor vs VSCode:主要区别与优势分析,附 TaoToken 统一 Key 接入配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

GitHub 3万星的 1Panel:Linux 管理面板如何用 Docker 跑通 AI 应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

MCP+A2A协议如何推动AI智能体进化为超级分布式网络:TaoToken统一通道下的多智能体协作实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

连分数:密码学中从公开参数恢复私钥的数学X光机

我第一次真正意识到连分数能干什么,是在复现某类密钥风险分析的时候。当时手里只有一对公开参数,其中一个私密的小量被藏得很深,理论文档里有一句话:当私密量小于某个平方根量级时,它会在公开参数连分数展开的一组收敛…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬