我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

信贷风控实战:从特征工程到模型调优的贷款违约预测指南

信贷风控实战:从特征工程到模型调优的贷款违约预测指南 简介面向金融科技与信贷风控场景的机器学习实践源码聚焦贷款违约风险预测适用于金融机构信用评估、数据分析与建模学习者。压缩包共23个文件约13.77MB包含7个Python源码、2个CSV训练测试数据集、11个PNG图表以及Excel、说明文档和LICENSE许可文件源码按数据分析、模型训练、预测等模块划分。已有560人学习下载。项目中分别实现随机森林、决策树、梯度提升等多种算法通过data_analysis进行数据预处理与探索train_use系列脚本完成模型训练与对比predict.py可用于新样本违约风险预测PNG图表直观呈现学习曲线与特征重要性辅助理解模型性能与调优方向。配套文档提供操作说明便于快速复现实验流程可直接参考或扩展为信贷审批、风险控制等实际业务方案。1. 贷款违约预测一份能直接跑通的机器学习实践源码做信贷风控的人大概都有过这种体验调了一周模型上线后指标却不如规则引擎最后发现是数据泄漏埋了雷。这份基于 Python 的贷款违约预测源码包把「数据清洗 → 特征探索 → 四类模型训练 → 性能对比 → 新样本预测」整条链路完整串了起来训练集、测试集、数据字典一应俱全。它适合两类人一是准备入门信用评分建模的开发者想看看真实信贷数据长什么样、决策树 / 随机森林 / 梯度提升在这类任务上谁更稳二是已经在做风控但想快速搭一套 baseline 的从业者用来对照自己的特征工程和调参思路。整套代码结构清晰能改能扩展不是那种只能跑个 demo 的半成品。2. 先看清数据信贷数据集里的字段含义与缺失值处理2.1 数据字典里藏着建模的起点解压后最先要打开的不是代码而是Data Dictionary.xls。这份数据字典定义了每个字段的业务含义我通常会把字段分成三类身份类年龄、家庭成员数、收入负债类年收入、负债率、历史行为类逾期次数、信贷额度使用率。这三类的特征工程思路完全不同——收入类字段大概率要做分箱或对数变换历史行为类字段往往是缺失值重灾区而且和违约标签的相关性最强。import pandas as pd train pd.read_csv(cs-training.csv, index_col0) print(train.shape) print(train.isnull().sum())这里index_col0是因为原始 CSV 第一列是样本 ID不参与建模。大多数信贷数据集都会有不同程度的缺失尤其是「逾期 30-59 天笔数」这种人工录入字段。先看缺失量再决定填充策略而不是上来就 fillna(0)这是很多新手容易跳过的第一步。2.2 样本不均衡先算违约占比再定评估指标y train[SeriousDlqin2yrs] print(y.value_counts(normalizeTrue))违约样本占比通常在 6%-8% 之间这意味着直接用准确率评估会得到虚高的结果——全预测为「不违约」也能有 93% 的正确率。我一般会在这一阶段就确定用 AUC 作为主评估指标同时记录召回率因为风控场景里漏掉一个违约客户的代价远高于误杀一个正常客户。这个判断会直接影响后面模型训练时的参数选择。3. 数据探索与特征工程从缺失值填充到异常值截断3.1 缺失值的两种处理策略train[MonthlyIncome] train[MonthlyIncome].fillna( train[MonthlyIncome].median() ) train[NumberOfDependents] train[NumberOfDependents].fillna(0)月收入用中位数填充因为它右偏严重均值会被高收入样本拉高家属人数填 0 则符合业务语义——缺失往往意味着「无家属」。这段代码逻辑很简单但背后是两类填充思路的取舍连续型字段用稳健统计量离散型字段用业务默认值。如果你拿到的数据缺失率超过 30%就得考虑性别、年龄分组填充而不是全局填一个值。3.2 异常值截断把「年龄 0 岁」这类脏数据挡在模型外train train[train[age] 18] train train[train[age] 100] train[RevolvingUtilizationOfUnsecuredLines] train[ RevolvingUtilizationOfUnsecuredLines ].clip(upper1.0)年龄字段出现 0 或 120 显然不合理直接过滤比让模型硬学更稳妥。信用卡额度使用率这类比例型字段超过 1.0 的值通常是数据录入错误额度清零但消费仍在用clip(upper1.0)截断而不是删除可以保留样本量。这里有一个血泪经验先做异常值处理再做缺失值填充不然截断操作会把填充后的数据再次弄脏。3.3 数据探索可视化训练前的必要检查data_analysis.py里会输出各特征的分布图其中percentage.png展示违约与不违约群体在各特征上的差异。我建议你在跑模型前先盯一眼「逾期次数」这类特征在两组样本上的分布——如果违约组的中位数明显更高说明这个特征有区分度后面做特征重要性排序时它大概率排在前面。这一步不做后面模型跑完你都不知道哪些特征在起作用调参就成了盲调。4. 四类模型训练对比决策树、随机森林、梯度提升与逻辑回归4.1 决策树先跑一个不过拟合的基线from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split X train.drop(SeriousDlqin2yrs, axis1) y train[SeriousDlqin2yrs] X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.3, random_state42, stratifyy ) dtc DecisionTreeClassifier( max_depth5, min_samples_leaf50, class_weightbalanced, random_state42 ) dtc.fit(X_train, y_train) print(DTC AUC:, roc_auc_score(y_val, dtc.predict_proba(X_val)[:, 1]))max_depth5和min_samples_leaf50是控制决策树复杂度的两个关键旋钮。前者限制树的层数后者要求每个叶子节点至少包含 50 个样本两者配合能有效防止树把训练集里的噪声也学进去。class_weightbalanced用来应对样本不均衡——少样本类别的误分会被赋予更高惩罚。为什么不直接跑默认参数默认决策树几乎必然过拟合训练集 AUC 接近 1验证集却惨不忍睹这种「假完美」对后续模型对比没有参考价值。4.2 随机森林用 bagging 稳住方差from sklearn.ensemble import RandomForestClassifier rfc RandomForestClassifier( n_estimators200, max_depth8, min_samples_leaf30, class_weightbalanced_subsample, n_jobs-1, random_state42 ) rfc.fit(X_train, y_train) print(RFC AUC:, roc_auc_score(y_val, rfc.predict_proba(X_val)[:, 1]))随机森林在决策树基础上引入了样本随机采样和特征随机选择所以max_depth可以适当放宽到 8靠多棵树的平均来消化方差。class_weightbalanced_subsample是随机森林特有的参数——它会在每次 bootstrap 采样时动态调整权重比固定balanced更贴合 bagging 的机制。n_jobs-1表示用满所有 CPU 核心200 棵树在这个数据量级下几秒就能跑完。4.3 梯度提升用 boosting 压低偏差from sklearn.ensemble import GradientBoostingClassifier gbc GradientBoostingClassifier( n_estimators120, max_depth3, learning_rate0.05, subsample0.8, random_state42 ) gbc.fit(X_train, y_train) print(GBC AUC:, roc_auc_score(y_val, gbc.predict_proba(X_val)[:, 1]))梯度提升的套路和随机森林正好相反——每棵树都在拟合前一棵的残差所以max_depth通常压到 3-4 层就够了太深很容易在残差里学到噪声。learning_rate0.05控制每棵树对最终结果的贡献权重学习率越低需要的树越多但泛化能力往往更好。subsample0.8是行采样每棵树只用 80% 的样本训练这既减少过拟合也让模型对个别异常样本不那么敏感。4.4 逻辑回归一个容易被低估的对手from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_val_s scaler.transform(X_val) lr LogisticRegression( C0.5, max_iter500, class_weightbalanced, solverliblinear ) lr.fit(X_train_s, y_train) print(LR AUC:, roc_auc_score(y_val, lr.predict_proba(X_val_s)[:, 1]))逻辑回归最容易被忽视但它的优势是稳定性和可解释性。C0.5是正则化强度的倒数C 越小正则化越强在特征间存在多重共线性时能有效压缩系数波动。solverliblinear适合中小数据集和二分类问题它比默认的lbfgs收敛更快。特征缩放对逻辑回归是必须的——不像树模型对尺度不敏感逻辑回归的梯度下降在未缩放的特征上会震荡得厉害。4.5 学习曲线判断模型是欠拟合还是过拟合dtc_learn_curve.py会绘制随训练样本量增加时训练集和验证集 AUC 的变化曲线。具体看两个点如果两条曲线最终靠得很近但 AUC 都不高比如都在 0.7 以下说明模型容量不够属于欠拟合应该增加max_depth或换更强的模型如果训练集 AUC 远高于验证集而且随着样本量增加差距不缩小这就是过拟合需要加强正则化或增大min_samples_leaf。学习曲线解决的是「该往哪个方向调参」的问题比盲目网格搜索高效得多。5. 参数调优与避坑那些容易翻车的细节5.1 用 GridSearchCV 找最优参数组合from sklearn.model_selection import GridSearchCV param_grid { max_depth: [4, 6, 8], min_samples_leaf: [20, 50, 100], n_estimators: [100, 200] } grid GridSearchCV( RandomForestClassifier(class_weightbalanced, random_state42), param_grid, cv5, scoringroc_auc, n_jobs-1 ) grid.fit(X_train, y_train) print(Best params:, grid.best_params_)GridSearchCV 的scoringroc_auc指定用 AUC 作为搜索目标cv5做五折交叉验证。这里有一个很多人忽略的点交叉验证的折数会影响参数选择结果样本量小的时候用 5 折比 10 折更稳定。另外搜索完最优参数后要用全部训练数据重新训练一遍模型而不是直接拿grid.best_estimator_去预测——因为交叉验证时的模型只见过部分数据。5.2 避坑记录一类别不平衡导致 AUC 虚高现象训练出来的模型 AUC 达到 0.9 以上但看混淆矩阵发现违约样本几乎全被预测成「不违约」。原因样本中违约比例只有 6%模型只要把所有样本预测为多数类准确率就能到 94%AUC 在这个场景下会被「容易区分的大多数」拉高掩盖少数类上的糟糕表现。解决改用precision_recall_curve计算 PR AUC或者直接观察召回率。我一般固定「违约样本召回率 ≥ 60%」作为硬约束再看哪个模型的 AUC 最高而不是反过来。5.3 避坑记录二max_depth 从 10 调到 50训练集 AUC 升但验证集不动现象决策树max_depth10时验证集 AUC 约 0.82调到 50 后训练集 AUC 涨到 0.97验证集反而跌到 0.80。原因树太深把训练集里的个体噪声当成规律学进去了验证集上没有重复的噪声规律所以表现不升反降。解决用学习曲线确认过拟合区间然后把max_depth固定在最优点附近。默认参数从来不是好选择——DecisionTreeClassifier默认不限制深度这在信贷数据上几乎必过拟合。5.4 避坑记录三缺失值填充顺序影响最终结果现象先用中位数填充了月收入再做异常值过滤结果发现部分样本的月收入被填进了「年龄为 0」的脏样本里过滤后这些填充值又影响了其他字段的统计量。原因数据处理的先后顺序错了。异常值过滤应该在缺失值填充之前执行否则脏样本会带着填充值参与后续统计计算。解决固定的处理流程是「先过滤异常行 → 再填充缺失值 → 最后做特征缩放」。这个顺序我在每个项目里都会强制走一遍没有例外。5.5 避坑记录四特征泄漏——把未来信息当特征用现象跑完模型发现某个特征的权重异常高AUC 逼近 1.0但上线后效果一落千丈。原因数据里某些字段比如当前逾期金额天然携带了违约结果的信息模型学到的其实是「用结果预测结果」。信贷场景里最典型的泄漏源是「事后统计」类特征例如贷款发放后才产生的逾期记录。解决做特征筛选时逐个确认每个特征在业务上的时间戳。规则是建模时点之前可获得的信息才能入模建模时点之后产生的字段一律剔除。6. 预测与进阶验证模型可解释性与概率校准6.1 用 predict.py 跑通新样本预测模型训练完成后predict.py负责对测试集cs-test.csv输出违约概率import pandas as pd from sklearn.ensemble import RandomForestClassifier test pd.read_csv(cs-test.csv, index_col0) # 这里要做和训练集完全一样的预处理 test[MonthlyIncome] test[MonthlyIncome].fillna( train[MonthlyIncome].median() ) test test[train.columns.drop(SeriousDlqin2yrs)] prob rfc.predict_proba(test)[:, 1] result pd.DataFrame({id: test.index, prob: prob}) result.to_csv(predict_result.csv, indexFalse)这段代码特别强调了一点预处理逻辑必须和训练时完全一致包括缺失值填充的数值用训练集的统计量而不是测试集自己的包括异常值过滤的阈值。实际项目里这两个数据集往往来自不同时间段分布可能有偏移如果各自独立做归一化预测结果就会失真。6.2 用 SHAP 打破黑匣子import shap explainer shap.TreeExplainer(rfc) shap_values explainer.shap_values(X_val) shap.summary_plot(shap_values[1], X_val)shap_values[1]取出的是「违约」类别的贡献值。summary plot 的横轴是 SHAP 值正值为推动违约纵轴按特征重要性排序每一行一个样本点颜色表示特征值高低。这张图能回答两个业务问题哪些特征对违约判断影响最大某个特征的高值推动违约还是抑制违约信贷审批场景里可解释性不是可选项监管要求必须说清楚拒绝客户的理由SHAP 是目前和树模型配合最顺手的工具。6.3 概率校准模型输出的分不一定能直接用随机森林输出的概率存在系统性偏差——它倾向于输出接近 0 或接近 1 的极端值实际违约率并没有那么高。如果要把预测概率直接映射成信用评分需要做校准from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV( rfc, methodisotonic, cv5 ) calibrated.fit(X_train, y_train)methodisotonic用等渗回归做非参数校准适合样本量充足的情况样本少时用sigmoid更稳。校准后输出的概率才能和实际违约率对上——这一步决定了你后续定审批阈值时用的分数是否可信。我吃过一次亏未经校准的模型在 0.7 分以上区间的实际违约率只有 30%而模型认为有 70%直接导致审批策略过于保守误杀了一批好客户。从那以后我每次做完模型都强制走一遍概率校准把校准曲线画出来和 45 度线对比偏差超过 5 个百分点就不上线。希望这些经验能帮你在信贷建模路上少踩几个坑快速把这份源码跑成自己的 baseline。本文还有配套的精品资源点击获取
返回列表