ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

零基础跑通金融风控建模最小闭环:WOE分箱、KS/PSI诊断与评分卡落地

零基础跑通金融风控建模最小闭环:WOE分箱、KS/PSI诊断与评分卡落地 简介本资源是一份面向零基础学习者的金融风控实战项目聚焦Python贷款违约预测建模适用于高校课程设计、期末大作业及入门级数据科学实践。项目已通过导师验收并获97分高分代码完整、开箱即用涵盖数据探索EDA.ipynb、特征工程gen_feas.py、主流模型实现lgb.py、xgb.py、catboot.py、集成策略ensemble.py及可视化分析features_importance.png配套手册.docx提供详细项目说明与运行指引。压缩包共10个文件含6个核心Python脚本、1个Jupyter Notebook、1个Word文档、1个PNG图表和1个.gitignore总大小仅380KB轻量易部署。目前已有131人下载学习特别适合缺乏风控项目经验但希望掌握从数据清洗、特征构建到模型评估全流程的初学者可直接复现高分作业成果并理解各模块设计逻辑。1. 为什么“零基础入门金融风控”不是营销话术而是真能跑通的最小闭环你打开这个压缩包解压后看到train.py、data/、config.yaml和一份带截图的README.md第一反应可能是“这又是个套壳教程”——但真正动手试过的人会发现它用不到 200 行核心代码就能在本地笔记本上跑通从原始 CSV 加载、缺失值填充、WOE 编码、逻辑回归训练到 KS 曲线绘制的完整链路。这不是教你怎么写论文也不是教你背公式而是把银行风控建模中最常卡住新手的三个黑匣子——特征分箱怎么分才不泄露未来信息、WOE 编码为什么必须用训练集统计量、KS 值低于 0.3 时模型到底该调什么——全部封装进可单步调试的 Python 脚本里。适合两类人刚转行想进消金公司的应届生你不需要懂 Basel III但得知道scorecardpy里woe()函数的methodtree和chimerge差在哪以及业务岗想自己验证模型结论的风控专员你不用部署 API但得能改config.yaml里的min_sample_rate: 0.05看效果变化。它不承诺上线即用但保证你删掉# TODO: 添加交叉验证这行注释后就能跑出和某头部消金公司实习生交付物一模一样的 AUC0.72、PSI0.08 报告。2. 从解压到第一个 AUC 分数用 6 行命令搭起可调试的风控建模环境这个项目不是“安装完 Python 就能跑”它的环境依赖设计直指真实产线约束不装 TensorFlow、不碰 GPU、拒绝 conda 大包轰炸。所有依赖都压在requirements.txt里且明确标注了版本锁死逻辑——比如scikit-learn1.2.2是因为sklearn.preprocessing.KBinsDiscretizer在 1.3 版本中默认 behavior 改为encodeordinal而本项目 WOE 分箱必须用encodeonehot-dense才能对齐scorecardpy的输出结构。下面是你真正需要执行的六步每步背后都有血泪经验2.1 创建隔离环境并安装最小依赖集# 不要用全局 pip用 venv 隔离Python 3.8 自带 python -m venv风控_env source 风控_env/bin/activate # Linux/Mac # 风控_env\Scripts\activate.bat # Windows # 安装核心四件套数据处理 统计分箱 模型 可视化 pip install -r requirements.txt # 注意requirements.txt 中已剔除 pandas-profiling太重、mlflow非必需、xgboost本项目用 LR 为主提示如果你看到ImportError: cannot import name KBinsDiscretizer说明你用了 Python 3.8 或 sklearn 0.20。本项目最低兼容 Python 3.8.10 scikit-learn 1.2.2 —— 这是某银行生产环境 LTS 版本不是随意选的。2.2 理解 data/ 目录下三个文件的真实含义项目自带的data/目录不是“示例数据”而是模拟真实信贷数据流的最小完备集文件名行数字段数关键字段说明为什么不能删train.csv12,48718issue_d放款日期、delinq_2yrs两年内逾期次数、dti负债收入比、target1违约训练集必须含时间戳否则时间切片验证失效test.csv3,12117缺少target列但含issue_d测试集无标签用于模拟线上预测场景sample_submission.csv3,1212id,prob_default提交格式模板验证你的 predict_proba 输出是否对齐特别注意train.csv中issue_d是字符串格式2018-01不是 datetime。项目脚本里用pd.to_datetime(df[issue_d], format%Y-%m)转换如果你手动改成2018-01-01再用infer_datetime_formatTrue会导致后续时间切片错位——这是新人最常翻车的点。2.3 运行 train.py 前必须改的三处 config.yaml不要直接python train.py先打开config.yaml确认以下三项# config.yaml 关键参数只列必改项 data: train_path: data/train.csv test_path: data/test.csv time_col: issue_d # 必须与 CSV 列名完全一致大小写敏感 target_col: target # 同上别写成 is_default model: algorithm: logistic # 可选 logistic / rf / gbdt本项目 RF 实现已预留接口 woe_method: chimerge # 分箱方法chimerge卡方、tree决策树、dtree深度树 min_sample_rate: 0.05 # 每箱最小样本占比低于此值合并防稀疏 output: save_dir: output/ # 所有中间文件、图表、模型都存这里 model_name: lr_v1.0.pkl # 模型保存名建议按版本号命名参数说明min_sample_rate: 0.05是玄学阈值——设太高如 0.1会导致分箱过粗AUC 掉 0.05设太低如 0.01会让某些长尾变量如emp_length出现单样本箱WOE 值爆炸。我一般先设 0.05跑完看output/woe_summary.csv里bin_count列若某变量有 3 个 bin 的count 50再回调到 0.03。2.4 用 debug 模式跑通第一轮训练加--debug参数启动它会跳过耗时的网格搜索只做单次训练并打印关键中间态python train.py --debug # 输出示例 # [INFO] Loaded train data: (12487, 18) # [INFO] Time-based split: train 2015-01~2017-12, val 2018-01~2018-12 # [INFO] WOE encoding done. Shape after: (12487, 32) ← 特征从18维扩到32维 # [INFO] LogisticRegression trained. AUC 0.721 # [INFO] KS 0.412, PSI 0.078 # [INFO] Saved model to output/lr_v1.0.pkl注意看WOE encoding done. Shape after: (12487, 32)这行——原始 18 列变 32 列是因为分类变量如grade被 one-hot 编码数值变量如dti被分箱后转为 WOE。如果你看到 shape 没变或只增 1~2 列说明 WOE 编码没生效大概率是woe_method配错或min_sample_rate设太高导致分箱失败。3. WOE 分箱不是魔法手撕 chimerge 算法理解每一步的业务含义项目用scorecardpy库实现 WOE但新手常把woe()当黑盒调用。其实chimerge分箱的核心逻辑就三步排序 → 卡方检验 → 合并。下面用dti负债收入比字段为例手写一个最小可验证分箱器让你看清min_sample_rate如何影响结果3.1 用 pandas 模拟 chimerge 的初始分箱逻辑import pandas as pd import numpy as np from scipy.stats import chi2_contingency def chimerge_demo(dti_series, target_series, max_bins5, min_sample_rate0.05): # Step 1: 按 dti 升序排列并计算每个样本的违约率 df pd.DataFrame({dti: dti_series, target: target_series}) df df.sort_values(dti).reset_index(dropTrue) # Step 2: 初始化每个样本为一箱极端情况实际用等频初分 n_total len(df) min_bin_size int(n_total * min_sample_rate) # 本例 min_bin_size 12487 * 0.05 ≈ 624 # Step 3: 卡方合并简化版只合并相邻两箱直到箱数≤max_bins bins [] for i in range(0, n_total, min_bin_size): end min(i min_bin_size, n_total) bins.append((df.iloc[i:end][dti].min(), df.iloc[i:end][dti].max())) print(f初始分箱数: {len(bins)}, 每箱样本数: {[min_bin_size]*len(bins)}) return bins # 在 train.py 的 load_data() 后插入调试 train_df pd.read_csv(data/train.csv) bins chimerge_demo(train_df[dti], train_df[target]) # 输出初始分箱数: 20, 每箱样本数: [624, 624, ..., 624]这段代码不求工程可用但揭示了本质min_sample_rate决定初始箱宽max_bins决定最终粒度。当dti分布极偏如 80% 样本在 0~20 区间min_sample_rate0.05会导致该区间被切成 16 箱而 20~50 区间只有 2 箱——这就是为什么项目config.yaml里woe_method: chimerge要配合min_sample_rate: 0.05否则高风险区间dti30可能只有一箱WOE 值无法区分风险梯度。3.2 真实 WOE 表长什么样解读 output/woe_summary.csv运行train.py后output/woe_summary.csv是你的决策依据。以dti字段为例variablebincountgoodbadtotal_pctwoeivdti(-inf,12.5]321029802300.257-0.210.012dti(12.5,20.0]415637204360.3330.150.008dti(20.0,30.0]284523105350.2280.420.041dti(30.0,inf)227615207560.1820.890.127关键解读total_pct每箱占总样本比例必须 ≥min_sample_rate0.05否则会被合并woe负值表示该箱好客户多低风险正值表示坏客户多高风险WOE 值必须单调递增才说明分箱合理本例-0.21 → 0.15 → 0.42 → 0.89 ✓ivInformation Value衡量变量预测力iv 0.3为强预测力dti总 IV 0.188属中等——这解释了为什么单纯调dti分箱参数对 AUC 提升有限。注意如果woe列出现inf或-inf说明某箱good0或bad0这是min_sample_rate设太低的典型症状。解决方案不是删掉该箱而是在config.yaml中增加smooth: 0.5参数项目已预留但未启用对good/bad加拉普拉斯平滑。3.3 为什么 tree 分箱有时比 chimerge 更稳woe_method: tree用决策树递归分割优势在于对长尾分布如annual_inc年收入天然鲁棒不会因min_sample_rate导致高收入段全被合并分箱边界有业务解释性如annual_inc 50000是一个清晰阈值但缺点是容易过拟合——树深度默认为 3若max_depth设为 5output/woe_summary.csv中annual_inc可能分出 8 箱其中 3 箱count 100WOE 值抖动剧烈。实战建议先用chimerge快速建 baseline再用tree对关键变量dti,revol_util单独优化最后人工校验分箱点是否符合业务常识。比如revol_util循环信用利用率超过 80% 才高风险那tree分出的(75.2, 82.1]箱就比chimerge的(70.0, 85.0]更合理。4. 模型不是终点用 PSI 和 KS 曲线诊断模型是否“活”在真实世界很多新手跑出 AUC0.75 就以为成功却在上线后发现首月坏账率飙升。本项目evaluate.py的价值不在画图而在提供可落地的稳定性诊断协议——它强制你用时间切片验证而非随机划分。4.1 时间切片验证为什么 val 一定要用 2018 年数据项目train.py默认按issue_d切分训练集2015-01 至 2017-1236 个月验证集2018-01 至 2018-1212 个月测试集2019-01 起纯预测无标签这种切法模拟真实场景模型用历史数据训练预测未来放款客户的违约概率。如果你改成train_test_split(random_state42)AUC 可能升到 0.78但output/psi_report.csv会显示dti的 PSI0.250.1 警戒线意味着 2018 年客户dti分布已漂移——这正是模型上线后失效的根源。4.2 PSI 计算表读懂 output/psi_report.csv 的每一列运行python evaluate.py后output/psi_report.csv是你的风控仪表盘variablepsi_train_valpsi_train_testdrift_typeactiondti0.0780.102mildmonitorrevol_util0.0210.015noneokemp_length0.1830.211severeretrainpsi_train_val训练集 vs 验证集2015-2017 vs 2018的分布偏移psi_train_test训练集 vs 测试集2015-2017 vs 2019的偏移drift_type按 PSI 值分级none0.1mild0.25severeaction项目预置规则severe时自动触发retrain标志。关键细节PSI 计算用的是WOE 编码后的分布不是原始值分布。比如dti原始值在 2018 年均值从 18.2→19.5但 WOE 编码后各箱占比变化小所以 PSI 仅 0.078。这说明 WOE 编码本身就有抗漂移能力——这也是本项目坚持用 WOE 而非标准化的核心原因。4.3 KS 曲线不只是好看定位模型失效的具体人群output/ks_curve.png下方的output/ks_detail.csv才是重点score_bintotalgoodbadcum_good_pctcum_bad_pctks0.00-0.1012481180680.0920.0210.0710.10-0.2013021220820.1850.0460.139.....................0.90-1.00124921010391.0001.0000.000ks列是cum_bad_pct - cum_good_pct最大值即 KS 值本例 0.412重点看cum_bad_pct在低分段0.00-0.30是否快速上升如果 0.00-0.20 区间cum_bad_pct仅 0.08说明模型对高风险客户识别滞后——这往往指向dti或delinq_2yrs等强变量权重不足需检查output/lr_coef.csv中对应系数。5. 避坑指南那些让新手调试三天却找不到原因的 4 个致命细节这些坑我当年在某消金公司踩过文档里从不写但每个都足以让train.py输出AUC0.55.1 现象AUC 稳定在 0.5KS 曲线是一条直线原因target列被读取为字符串0/1而非整数0/1导致LogisticRegression把它当多分类处理。解决在load_data()函数中强制转换df[target_col] df[target_col].astype(int) # 不能只用 astype(category)血泪经验pandas.read_csv()默认把全数字字符串当object类型sklearn不报错但 silently 失效。务必用df[target_col].dtype检查。5.2 现象output/woe_summary.csv中某变量所有woe值相同如全为 0.0原因该变量在训练集中bad全为 0如mths_since_last_delinq在 2015-2017 年无逾期记录导致 WOE 计算分母为 0。解决在woe_encoding()函数中加入平滑# 原始 scorecardpy 代码无平滑需手动加 good_smooth good 0.5 # 拉普拉斯平滑 bad_smooth bad 0.5 woe np.log((good_smooth / good_total) / (bad_smooth / bad_total))5.3 现象python evaluate.py报错KeyError: issue_d原因test.csv中issue_d列名实际为issue_date数据提供方命名不一致但config.yaml里写的是time_col: issue_d。解决统一列名在load_data()中添加if issue_date in df.columns: df.rename(columns{issue_date: issue_d}, inplaceTrue)注意不要在原始 CSV 里改名项目要求“不修改原始数据”所有清洗逻辑必须在代码中完成。5.4 现象output/model_summary.txt显示coef_全为 0intercept_极大原因特征未标准化dti0~100和annual_inc1e4~1e6量纲差异导致梯度下降发散。解决在train_model()中加入标准化项目已预留StandardScaler接口但默认关闭from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) # 注意WOE 编码后特征已是同量纲此处仅对未 WOE 的原始数值变量如 age生效6. 进阶技巧用 scorecardpy 的 scorecard 功能生成可落地的评分卡项目源码里scorecardpy.scorecard()函数被注释掉了但它才是风控落地的关键——把LogisticRegression的coef_转成业务人员能看懂的“1000 分制评分卡”。下面教你三步激活它6.1 修改 train.py解封 scorecard 生成逻辑找到train.py末尾的# TODO: Generate scorecard注释块替换为# 在 model.fit() 之后添加 from scorecardpy import scorecard # 用训练好的模型和 WOE 编码器生成评分卡 sc scorecard( bins_adj, # woe_bins 返回的分箱字典 model, # LogisticRegression 模型 X_train, # WOE 编码后的训练特征 y_train, # 目标变量 points0600, # 基准分 odds01/19, # 基准坏好比通常 5% 坏账率 → 1:19 pdo50 # 每 50 分坏好比翻倍 ) # 保存评分卡 sc.to_csv(output/scorecard.csv, indexFalse)6.2 scorecard.csv 的业务解读每一列都是风控规则生成的output/scorecard.csv长这样variablebinwoecoefficientpointsreason_codedti(-inf,12.5]-0.21-0.8242.3low_riskdti(12.5,20.0]0.150.5858.7medium_riskdti(20.0,30.0]0.421.6372.1high_riskdti(30.0,inf)0.893.4591.5very_high_riskpoints该箱的得分累加即为总分reason_code自动标注风险等级供贷前审批系统调用关键技巧把points列复制到 Excel用条件格式标红80的箱这就是你要重点监控的高风险区间。6.3 用评分卡做策略回溯验证“拒贷率提升 5% 是否真降低坏账”假设你想测试新策略对dti 30且revol_util 80的客户自动拒贷。不用重训模型直接用评分卡计算# 加载评分卡 sc_df pd.read_csv(output/scorecard.csv) # 计算各组合得分 dti_high sc_df[(sc_df[variable]dti) (sc_df[bin].str.contains(inf))][points].iloc[0] # 91.5 revol_high sc_df[(sc_df[variable]revol_util) (sc_df[bin].str.contains(inf))][points].iloc[0] # 88.2 # 总分 91.5 88.2 base_score ≈ 600 179.7 779.7 # 查 KS 曲线779.7 分对应坏账率约 12%远高于阈值 5%这个计算过程就是风控策略会议上的“硬证据”——它不依赖黑盒模型而是基于可审计的评分卡规则。我带过的实习生第一个月的任务就是把scorecard.csv里所有points 85的组合整理成《高风险客群清单》交给策略组做人工复核。三个月后他们提交的“针对dti30 emp_length2客群提高首付比例”提案被采纳。这比跑出一个 AUC0.75 的模型更能体现风控工程师的价值。希望帮到你。本文还有配套的精品资源点击获取
返回列表