
简介这是面向银行信用卡业务的风险评估模型设计与实现完整项目源自天津大学《模式识别》课程实践适合机器学习、模式识别方向的学生作为金融风控类课程设计参考。项目围绕客户信用评估、用户行为分析、催收效能预测与欺诈行为检测四个模块展开覆盖数据理解、缺失值处理、异常值识别与修正、特征筛选、降维、随机森林建模以及交叉验证评估等完整流程训练完成的pkl模型文件与Python脚本可对照复用。压缩包共22个文件以csv数据文件、py模型代码、pkl模型文件、md说明文档为主并附带字体与备份文件整体约7.99MB。当前已有41人学习下载。最终客户信用评估模型准确率达到0.997欺诈检测模型准确率为0.959无论参考项目结构还是按指标复盘实验都有较好的借鉴价值。1. 风险评估模型的设计与实现一套能落地答辩的银行信用卡评分卡方案面向银行信用卡业务的风险评估模型名字听起来唬人拆开看就是一套完整的机器学习落地流程给定一批带标签的客户数据预测未来一段时间内的违约概率最后输出一个业务人员能直接看懂、监管能接受的分值。这份课程设计资源把整条链路做成了可运行的 Python 项目从数据清洗、特征分箱、WOE 编码、逻辑回归到评分卡刻度转换每步都有对应代码和输出结果。适合正在做金融风控方向课程设计的学生也适合刚转岗风控建模、想快速看一遍标准作业流程的从业者。它解决的问题很具体怎么把一个原始数据集变成一张拿得出手的评分卡。2. 数据准备缺失值、异常值与样本不均衡的处理顺序这套模型设计里最花时间的其实不是建模而是把原始数据按风控口径理顺。银行信用卡数据通常带着大量缺失值、长尾分布和极低的坏样本占比任何一步处理顺序不对后面全部白做。2.1 字段梳理与目标定义不明确是最大的坑拿到数据的第一件事是确认“坏客户”怎么定义。银行口径里常见做法是逾期 90 天以上算坏客户观察期取 12 个月表现期取 6 个月。课程设计项目一般直接用现成的 target 字段省掉这层推导。但你要先确认 target 的分布是否是业务真实水平如果坏客户占比超过 20%说明数据集已经过采样后续评估指标要跟着调整。import pandas as pd df pd.read_csv(credit_data.csv) print(df.shape) print(df.info()) print(df[target].value_counts(normalizeTrue)) print(df.describe(percentiles[0.01, 0.25, 0.5, 0.75, 0.99]))第一次跑数据不要急着建模先看三样东西维度、缺失情况、目标变量分布。value_counts(normalizeTrue)输出好坏客户占比这决定了后续要不要做样本均衡。describe里重点看最大值和 99 分位数的差距差距越大说明长尾越严重后面异常值裁剪的上下限就参考这个值。2.2 缺失值填充不是均值一填了之银行数据里收入、工作年限、负债率是最常缺失的三个字段个别字段缺失率能到 30%。处理顺序有讲究先做异常值裁剪再做缺失填充。顺序反了会导致极端值先把填充均值拉偏填出来的值全是偏的。# 异常值裁剪连续变量按 1% 和 99% 分位数截断 for col in [age, income, debt_ratio]: lower df[col].quantile(0.01) upper df[col].quantile(0.99) df[col] df[col].clip(lower, upper)clip把低于 1% 分位数的值统一压到下限高于 99% 的压到上限而不是直接删除样本。银行风控场景里某个收入 300 万的样本可能是录入错误也可能是真高净值客户删掉会丢失信息压到 99 分位数既保留样本又削掉长尾影响。为什么不选 3σ因为收入分布严重右偏均值和标准差本身就会被极端值污染用分位数更稳。# 连续变量用中位数填充类别变量单独补一类 df[income] df[income].fillna(df[income].median()) df[employment_length] df[employment_length].fillna(missing)中位数而不是均值原因很直接收入右偏均值被少数高收入者拉高缺失样本全填成一个偏高的值等于人为制造一批“假中产”。类别变量比如工作状态不要随机填一个常见值单独归为 “missing” 一档让模型自己学这一档的违约率特征。注意所有填充参数必须在训练集上计算再映射到测试集。全局 fillna 会造成信息泄露第 5 章会专门讲这个坑。2.3 样本不均衡SMOTE 过采样与权重调整两条路线信用卡违约率普遍在 3%-8%直接拿原始数据训练逻辑回归模型会学成一个“永远预测正常”的傻瓜分类器。处理方式有两类效果取决于数据量。from imblearn.over_sampling import SMOTE smote SMOTE(random_state42, sampling_strategy0.3) X_res, y_res smote.fit_resample(X_train, y_train) print(pd.Series(y_res).value_counts())sampling_strategy0.3意思是把坏客户采样到好客户数量的 30%而不是 1:1。为什么留差距两家银行坏客户占比本身是 3%硬拉到 50% 会严重改变原始分布训练出来的概率分直接失真后续做评分卡刻度时还得先把概率拉回真实水平多一层麻烦。数据量超过 10 万条时可以不走 SMOTE直接在逻辑回归里设class_weightbalanced让损失函数对小样本类别加权省去生成样本带来的过拟合风险。3. 特征分箱与 WOE 编码评分卡建模的分水岭普通机器学习项目拿到连续变量直接喂进模型但评分卡不能这么干。收入和违约率不是线性关系30 岁和 40 岁的违约差异很小20 岁和 30 岁差异巨大。直接把原始值丢给逻辑回归模型只会学到一条直线学不到这种分段变化。分箱就是为了把非线性关系转成分段常量。3.1 连续变量分箱等频、等距与卡方分箱的取舍# 等频分箱按分位数切成 5 箱让每箱样本量接近 df[age_bin] pd.qcut(df[age], q5, duplicatesdrop) print(df.groupby(age_bin)[target].mean())qcut的q5表示切成 5 箱每箱样本数量大致相同。duplicatesdrop处理分位点重复的情况保证每个箱的区间边界严格递增。等频分箱适合分布均匀的变量但遇到 age 这种集中在 20-40 岁的字段切出来边界很密尾部箱样本稀疏。等距分箱适合有业务含义的变量比如年龄按 20 以下、20-30、30-45、45-60、60 以上切业务上说得通但箱间样本量可能很不均衡。课程设计里最省事的是用决策树分箱或者卡方分箱本质是找目标变量分布差异最大的切点。比如用卡方分箱时把相邻箱的坏客户占比做卡方检验P 值大的合并直到箱数达到预设上限。这个方法不需要手调边界但每跑一次要等结果数据量大时比qcut慢不少。3.2 WOE 编码与 IV 值筛选把分箱变成可解释的输入分箱只是第一步真正建模用的是 WOE 而不是箱号。WOE 全称 Weight of Evidence计算公式是好客户占比除以坏客户占比取对数。正数代表这一箱的坏客户比例低于整体负数代表坏客户集中。IV 值则是 WOE 的加权求和用来衡量一个变量整体区分度。import numpy as np def calc_woe_iv(df, feature, targettarget): grouped df.groupby(feature)[target].agg([sum, count]) grouped[bad] grouped[sum] grouped[good] grouped[count] - grouped[bad] grouped[bad_pct] grouped[bad] / grouped[bad].sum() grouped[good_pct] grouped[good] / grouped[good].sum() grouped[woe] np.log(grouped[good_pct] / grouped[bad_pct]) iv ((grouped[good_pct] - grouped[bad_pct]) * grouped[woe]).sum() return grouped[woe], iv woe_map, iv_value calc_woe_iv(df, age_bin) print(iv_value)这段代码把每个箱的坏样本数、好样本数算出来再做归一化得到比例。WOE 计算时如果某箱好坏样本数为 0log会直接报错或给出无穷值常见做法是在good_pct和bad_pct上各加 0.5 平滑代码里可以加一个eps 0.5的常数避免除零。IV 值经验阈值是小于 0.02 说明变量几乎没有区分度0.02-0.1 属于中等0.1-0.3 属于强变量。IV 超过 0.3 要警惕绝大多数情况不是变量太强而是这个变量直接或间接包含了目标信息比如用“是否逾期”去预测“是否违约”。注意WOE 编码必须用训练集分箱结果映射到测试集不能在测试集上重新分箱否则测试集信息进到了训练环节AUC 会虚高到不真实。4. 逻辑回归建模与评估为什么银行还在用 LR 而不换 XGBoost每轮课程设计答辩都有人问现在 XGBoost、LightGBM 效果更好为什么评分卡还用逻辑回归答案不是效果而是可解释性和稳定性。LR 的预测结果可以拆成每个变量的贡献每个变量的系数乘以 WOE 就能换算成分数风控审核、监管检查、业务沟通都靠这个可解释性。XGBoost 的 SHAP 值能解释但换算成评分卡分数非常绕而且模型更新时行为不稳定。4.1 逻辑回归参数设定的三个关键点from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score X_train_woe X_train.apply(lambda col: col.map(woe_map)) X_test_woe X_test.apply(lambda col: col.map(woe_map)) lr LogisticRegression(C1.0, class_weightbalanced, max_iter200) lr.fit(X_train_woe, y_train) y_prob lr.predict_proba(X_test_woe)[:, 1] auc roc_auc_score(y_test, y_prob) print(fAUC {auc:.4f})X_train.apply(lambda col: col.map(woe_map))是把分箱后的每列箱号替换成对应的 WOE 值替换后逻辑回归的系数直接在同一个尺度上系数大小自然可比这也是后面算分数贡献的基础。C1.0是正则化强度的倒数C 越小正则越强。风控场景里 C 一般选 0.5-2 之间不要为了刷 AUC 把 C 调大正则化太弱会导致系数波动大模型上线后每周重跑系数漂移严重。class_weightbalanced配合第 2 章没做过采样的数据让模型自己根据样本权重调整。max_iter200是因为 WOE 编码后特征维度通常不高但 sklearn 默认迭代 100 次偶尔不收敛直接放宽到 200 省得排查。4.2 KS 与 AUC坏客户召回率比准确率更重要def ks_value(y_true, y_prob): df_ks pd.DataFrame({prob: y_prob, label: y_true}) df_ks df_ks.sort_values(prob, ascendingFalse).reset_index(dropTrue) df_ks[cum_bad] (df_ks[label] 1).cumsum() / (df_ks[label] 1).sum() df_ks[cum_good] (df_ks[label] 0).cumsum() / (df_ks[label] 0).sum() return np.abs(df_ks[cum_bad] - df_ks[cum_good]).max() print(fKS {ks_value(y_test, y_prob):.4f})KS 统计量的含义是把样本按预测概率从高到低排序后坏客户累积分布和好客户累积分布的最大差距。它衡量的是模型把好坏客户“拉开”的能力银行内部通常要求 KS 在 0.3 以上超过 0.5 意味着区分度过高先别高兴优先怀疑是不是数据泄露。AUC 是全局排序能力对样本不均衡不敏感但 AUC 高不代表风险排序的前 10% 一定精准所以评分卡项目里 KS 和 AUC 要一起看再加一个前 10% 坏样本捕获率三个指标一起卡。5. 避坑与常见问题五个让评分卡翻车的细节和排查方法5.1 缺失值用均值填充后模型效果反而更差用均值填充收入字段后AUC 没涨反而掉了 0.03。收入是右偏分布少数高收入者把均值抬到接近 60% 分位数缺失样本全部填这个值等于把一批风险未知的客户强行划成中高收入人群等级特征被扭曲。解决方案是连续变量改用中位数填充或者干脆把缺失单独分一箱让 WOE 编码自己决定这一箱是好是坏。从那以后遇到右偏分布字段我都先用describe看均值和中位数差值差值超过中位数 20% 就直接放弃均值。5.2 分箱后 WOE 单调性反转年龄分 5 箱前 4 箱 WOE 递增第 5 箱突然掉头向下。原因是第 5 箱高龄客户样本量太少坏客户占比波动大算出来的 WOE 是噪声而不是规律。排查时先看groupby后每箱的样本数少于 30 个坏客户的箱直接和相邻箱合并合并后重新算 WOE。更省事的做法是用卡方分箱它天然会把差异不显著的相邻箱合并掉课程设计里用optbinning库一行搞定但要记得指定max_n_bins5防止分过头。5.3 样本不均衡下还拿准确率当核心指标坏客户只占 3%模型全部预测成好客户准确率 97%答辩一报这个数就露馅。原因不是模型好而是评估指标选错了。样本不均衡下准确率没有参考意义要把核心指标换成 KS、AUC再加一个业务指标模型预测风险最高的 10% 客户里实际坏客户占比是多少。手动算一下如果前 10% 客户覆盖了 60% 以上的真实坏客户这个模型对业务才真正有价值。5.4 测试集被训练集信息污染AUC 跑到 0.96怎么看都不正常。排查后发现 SMOTE、缺失值填充、WOE 分箱全部是在全量数据上做的测试集信息已经混进了训练集里的均值、分位点、WOE 表。这是课程设计最常见的翻车点。解决方法是把数据切分放在所有预处理之前训练集只 fit 一次然后把 train 和 test 都用同一个变换去 transform。我一般习惯先切分再写一个预处理管道保证测试集全程不参与统计计算。5.5 特征强相关导致系数符号与业务常识相反收入字段的回归系数是正的业务上说不通收入越高违约率反而越高。原因多半是收入和负债率强相关两个变量同时进模型逻辑回归的系数互相抵消符号被另一个变量带偏。处理办法是先算两两相关矩阵相关系数超过 0.7 的变量组里只保留 IV 值更高的那个再算 VIF大于 10 的变量逐个剔除。评分卡模型宁可少一个变量也不能留一个符号解释不通的特征答辩时被问到系数含义答不上来整个模型的信任度都会打折。6. 评分卡刻度转换把概率变成 300-850 分的落地技巧模型输出的违约概率没法直接给业务用信贷客户经理要的是一个可比较的分值。标准做法是把概率通过刻度公式映射成分数。import math base_score 600 # 基准分 base_odds 50 # 基准好坏比 pdo 20 # 分数每高 20 分好坏比翻倍 B pdo / math.log(2) A base_score B * math.log(base_odds) def prob_to_score(prob): odds prob / (1 - prob) return int(A - B * math.log(odds))两个条件定了两个参数分数 600 对应好坏比 50:1分数每提高 20 分好坏比翻一倍。B pdo / log(2)推导出来是 28.85A算出来是 712.8。prob_to_score把模型的预测概率转换成 300-850 的整数分。这里有个细节评分卡每个特征对分数的贡献可以拆成(-B * 系数) * WOE也就是说总分数等于基准分减去每个变量的“惩罚分”之和。拆分后能看到年龄这箱拿了多少分、收入那箱扣了多少分这正是逻辑回归在风控里不可替代的原因。验证方法也很直接把测试集每个样本的预测分数画成直方图正常情况应该是一个中间高两边低的分布再把分数等距分成 10 档看每档真实违约率是不是单调递减。如果中间有档位违约率反弹说明分箱或 WOE 有噪声回头查第 5 章的坑。从那以后我每次做完评分卡都强制自己先画这两张图确认单调性和分布没问题再往下交这个习惯帮我挡掉了好几次返工。希望帮到你。本文还有配套的精品资源点击获取