
简介这份资源面向金融风控方向的学习者与数据科学从业者聚焦用Python实现申请评分卡模型帮助理解从原始申请数据到信用评分的完整建模链路。包内共14个文件以pkl序列化中间产物、py建模脚本、csv原始数据集及lr_classweight权重文件为主压缩包约9.22MB涵盖数据预处理、特征筛选、模型训练与评分规则生成等环节所需的素材。目前已有1696人学习下载适合希望动手复现评分卡流程、理解WOE与分箱逻辑的读者。借助其中的application.csv与配套脚本可依次完成缺失值处理、One-Hot编码、基于卡方检验或互信息的特征选择并对比逻辑回归、GBDT、DNN等算法的表现同时借助类别权重文件应对违约样本偏少的不平衡问题。整体结构便于按模块拆解学习可作为信贷风控入门与模型评估练习的实操参考。1. 申请评分卡模型到底交付了什么从一份 Python 数据集说起很多人第一次接触风控建模是从一份 Excel 里的逾期标签开始的。你拿到手的所谓“python的申请评分卡模型 数据集”通常不是单个 csv而是一整套能跑通 WOE 分箱、IV 筛选、逻辑回归训练到评分映射的工程包原始申请样本、字段字典、好坏样本定义、以及一份可复现的建模脚本。它解决的核心问题是——把“这个人会不会逾期”变成一个 300 到 850 之间的整数分数让审批策略有阈值可卡。适合谁刚转风控的数据分析同学、要做贷前审批策略的产品、以及需要给面试官展示完整评分卡 pipeline 的求职者。下面我按自己拆包的顺序把这份资源从字段到分数讲透中间该抄的代码、该避的坑一个不落。2. 拆开数据集字段、标签与好坏样本定义2.1 申请评分卡的数据长什么样一份标准的申请评分卡数据集行是申请件列分四类主键与时间apply_id、apply_date、人口属性age、marital_status、education、征信与行为query_3m、credit_card_utilization、loan_amount、以及标签列label 或 dpd30。标签一般是二值的1 代表坏样本观察期内逾期 30 天以上0 代表好样本。常见做法是把观察点定在放款后 6 到 12 个月表现期不足的样本直接剔除否则标签噪声会让你后面 IV 值虚高。我一般拿到数据先做三件事看缺失率、看好坏比、看字段类型。好坏比低于 1:20 的样本逻辑回归容易把截距拟合到极端这时候要么降采样好样本要么上样本权重。字段类型决定后面分箱方式——连续变量走卡方或等频分箱类别变量直接按类别 WOE 编码。这一步不做后面分箱脚本报错你都不知道错在哪。import pandas as pd import numpy as np df pd.read_csv(apply_scorecard.csv) # 基础体检缺失率、好坏比、类型分布 print(df.isnull().mean().sort_values(ascendingFalse).head(10)) print(好坏比:, df[label].value_counts(normalizeTrue).to_dict()) print(df.dtypes.value_counts())这段代码不复杂但顺序有讲究。先看缺失率是因为缺失率超过 60% 的字段基本可以直接丢省得后面分箱时被空值卡住。好坏比输出的是比例字典方便你判断要不要调权重。类型分布让你一眼看出哪些是 object 需要转数值。参数上没什么可调的但head(10)建议保留字段多的时候全打印会刷屏。2.2 好坏样本定义与观察期、表现期的边界这是整个评分卡最容易被忽略、又最致命的地方。观察期observation window是你采集特征的时间范围表现期performance window是你追踪是否逾期的窗口。常见设定是观察期取申请前 6 个月的行为表现期取放款后 12 个月。两个窗口不能重叠否则特征里混入了未来信息模型离线 AUC 能到 0.85上线直接崩。数据集里如果带了apply_date和loan_date一定要自己算一遍表现期是否够。我见过有人直接拿全量数据训练结果表现期不足 3 个月的样本标签全是 0模型学出来“人人都是好人”。处理办法很简单按放款日切一刀只保留表现期完整的申请件。# 假设数据里有放款日和最后观察日 df[loan_date] pd.to_datetime(df[loan_date]) df[observe_end] pd.to_datetime(df[observe_end]) # 表现期至少 12 个月 df[perf_months] (df[observe_end] - df[loan_date]).dt.days / 30 df df[df[perf_months] 12].copy() print(剩余样本量:, len(df))这里perf_months是算出来的表现期月数阈值 12 是行业里比较稳的取值短了标签不可靠长了样本量掉得快。copy()不能省否则后面赋值会触发 SettingWithCopyWarning虽然不影响结果但看着烦。切完之后样本量如果掉到原来的三成以下说明你的数据时间跨度不够得考虑缩短表现期或者换数据集。3. 从原始字段到 WOE分箱、IV 与筛选的完整链路3.1 卡方分箱与等频分箱怎么选分箱是评分卡的灵魂。连续变量直接进逻辑回归系数解释不了业务也不稳定。分箱的目的就是把非线性关系切成单调的段再转成 WOE。常见两种做法等频分箱保证每箱样本量接近卡方分箱保证箱间标签差异最大。我一般先用等频粗分 20 箱再用卡方合并到 5 到 8 箱兼顾稳定性和区分度。分箱数量不是越多越好。箱数超过 10WOE 曲线容易抖动IV 虚高箱数少于 4信息损失大。经验值是 5 到 8 箱且每箱坏样本占比不低于 5%。如果某个箱坏样本太少合并到相邻箱别硬留着。def woe_iv(df, feature, target, bins5): # 等频分箱 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_rate] grouped[bad] / grouped[total] grouped[woe] np.log( (grouped[bad] / grouped[bad].sum()) / (grouped[good] / grouped[good].sum()) ) iv ((grouped[bad] / grouped[bad].sum()) - (grouped[good] / grouped[good].sum())) * grouped[woe] return grouped, iv.sum() grouped, iv woe_iv(df, age, label) print(grouped) print(IV:, iv)pd.qcut做等频分箱duplicatesdrop处理重复边界值不然会报 Bin edges must be unique。WOE 公式里分子是坏样本占比、分母是好样本占比取对数后正负都有。IV 是 WOE 的加权和一般 IV 大于 0.02 才有预测力0.1 到 0.5 算强。这段代码返回分组明细和 IV 值方便你逐字段看单调性。如果 WOE 不单调说明分箱有问题回去调整箱数或换卡方分箱。3.2 IV 筛选与多重共线性处理算完所有字段的 IV下一步是筛选。常见阈值是 IV 低于 0.02 直接丢高于 0.5 要警惕——可能是标签泄漏或者字段本身是强规则。我一般保留 IV 在 0.02 到 0.5 之间的字段再两两算相关系数超过 0.7 的留 IV 高的那个。多重共线性在逻辑回归里会让系数符号反直觉比如“收入越高逾期越高”这时候模型没法解释。处理办法除了删字段还可以用 VIF 检验VIF 大于 10 的字段考虑剔除。数据集字段不多的话直接看相关矩阵热力图最快。iv_dict {} for col in [age, income, query_3m, credit_util]: _, iv_val woe_iv(df, col, label) iv_dict[col] iv_val iv_series pd.Series(iv_dict).sort_values(ascendingFalse) selected iv_series[(iv_series 0.02) (iv_series 0.5)] print(selected) # 相关性检查 corr df[selected.index].corr().abs() upper corr.where(np.triu(np.ones(corr.shape), k1).astype(bool)) to_drop [c for c in upper.columns if any(upper[c] 0.7)] print(建议剔除:, to_drop)iv_dict收集每个字段的 IVselected做区间过滤。相关矩阵用np.triu取上三角避免重复计算对角线。to_drop是相关系数超 0.7 的字段列表实际删之前建议人工看一眼有时候两个字段业务含义不同但数值相关删哪个得结合场景。这段跑完你的入模字段基本就定了。4. 逻辑回归训练与评分映射把概率变成 300 到 850 分4.1 WOE 转换与逻辑回归拟合字段筛完把每个字段的 WOE 值映射回原数据得到一张全 WOE 的训练集。逻辑回归用penaltyl2、C1.0起步solverlbfgs对中小数据集够用。训练完看系数符号是否符合业务预期AUC 一般落在 0.7 到 0.8 之间算正常超过 0.85 要查标签泄漏。from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score # 假设 X_woe 是 WOE 转换后的特征矩阵 model LogisticRegression(penaltyl2, C1.0, solverlbfgs, max_iter1000) model.fit(X_woe, y) prob model.predict_proba(X_woe)[:, 1] print(AUC:, roc_auc_score(y, prob)) print(系数:, dict(zip(X_woe.columns, model.coef_[0])))C是正则化强度的倒数越小正则越强系数越保守。max_iter调到 1000 是防止不收敛警告。AUC 只是参考真正要看的是系数符号和 KS 值。系数为正说明 WOE 越高逾期概率越高如果某个字段系数为负但业务上应该为正回去查分箱单调性。4.2 评分映射PDO、基准分与方向评分卡不是直接输出概率而是映射成整数分。核心参数三个基准分base score、基准 odds、PDOpoints to double the odds。常见设定是 base 600、odds 50、PDO 20意思是 odds 每翻一倍分数加 20。公式是score base PDO / ln(2) * ln(odds) - PDO / ln(2) * ln(prob / (1 - prob))。import math base_score 600 base_odds 50 pdo 20 factor pdo / math.log(2) offset base_score - factor * math.log(base_odds) def prob_to_score(prob): odds prob / (1 - prob) return round(offset factor * math.log(odds)) df[score] prob_to_score(prob) print(df[score].describe())factor是 PDO 换算成自然对数的系数offset是偏移量。prob_to_score把概率转成分数分数越高代表违约概率越低。跑完看分数分布正常应该集中在 500 到 700 之间如果全挤在 300 或 850说明基准参数设错了。这一步的输出就是审批策略能直接用的分数列。5. 避坑与排查评分卡落地最常见的五个翻车点5.1 现象IV 值高得离谱AUC 却很低原因通常是标签泄漏。某个字段在表现期内才产生比如“逾期后催收次数”它跟标签强相关但不是预测变量。解决逐字段核对产生时间凡是表现期内产生的字段一律剔除。5.2 现象WOE 不单调系数符号反直觉原因是分箱边界不合理或者类别变量合并太粗。解决连续变量改卡方分箱类别变量把坏样本占比接近的类别合并再重算 WOE。5.3 现象训练集 AUC 0.85测试集掉到 0.65原因是过拟合或样本时间穿越。解决按时间切训练测试集别随机切检查是否有字段在训练期和测试期分布差异过大做 PSI 检验PSI 大于 0.1 的字段要重新分箱。5.4 现象分数分布全挤在一段原因是基准分或 PDO 设错或者概率输出本身区分度不够。解决先看概率分布是否集中在 0.5 附近如果是回查特征区分度如果概率正常调整 base_score 和 PDO 让分数拉开。5.5 现象缺失值填充后 IV 虚高原因是把缺失当成一个类别而缺失本身跟标签相关。解决缺失率低于 5% 的字段直接删样本高于 5% 的把缺失单独设为一箱观察 WOE 是否合理不合理就填充众数或中位数。6. 进阶技巧用 PSI 监控评分卡上线后的稳定性模型上线不是终点。我习惯每月跑一次 PSIPopulation Stability Index看分数分布和入模字段分布有没有漂移。PSI 小于 0.1 算稳定0.1 到 0.25 要警惕大于 0.25 就得考虑重新训练。下面这段代码可以直接抄。def psi(expected, actual, bins10): # 按预期分布分箱 breakpoints np.percentile(expected, np.linspace(0, 100, bins 1)) breakpoints[0] -np.inf breakpoints[-1] np.inf exp_counts np.histogram(expected, binsbreakpoints)[0] / len(expected) act_counts np.histogram(actual, binsbreakpoints)[0] / len(actual) # 防止除零 exp_counts np.where(exp_counts 0, 0.0001, exp_counts) act_counts np.where(act_counts 0, 0.0001, act_counts) return np.sum((act_counts - exp_counts) * np.log(act_counts / exp_counts)) # 假设 train_score 是训练期分数oot_score 是上线后分数 print(分数 PSI:, psi(train_score, oot_score))breakpoints用训练集的分位数切保证两期分箱一致。np.where把零频替换成极小值避免 log 报错。PSI 输出的是标量直接跟阈值比就行。字段级 PSI 同理把分数换成字段值即可。我一般把 PSI 脚本挂到调度上每月自动跑超阈值发邮件。从那以后我每次上线新模型都强制走一遍 PSI 和 KS 的月度监控再也没出现过分数突然崩掉却没人发现的情况。希望帮到你。本文还有配套的精品资源点击获取