news 2026/10/18 22:00:16

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
信贷风控实战:从特征工程到模型调优的贷款违约预测指南

简介:面向金融科技与信贷风控场景的机器学习实践源码,聚焦贷款违约风险预测,适用于金融机构信用评估、数据分析与建模学习者。压缩包共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_col=0) print(train.shape) print(train.isnull().sum())

这里index_col=0是因为原始 CSV 第一列是样本 ID,不参与建模。大多数信贷数据集都会有不同程度的缺失,尤其是「逾期 30-59 天笔数」这种人工录入字段。先看缺失量再决定填充策略,而不是上来就 fillna(0),这是很多新手容易跳过的第一步。

2.2 样本不均衡:先算违约占比再定评估指标

y = train['SeriousDlqin2yrs'] print(y.value_counts(normalize=True))

违约样本占比通常在 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(upper=1.0)

年龄字段出现 0 或 120 显然不合理,直接过滤比让模型硬学更稳妥。信用卡额度使用率这类比例型字段,超过 1.0 的值通常是数据录入错误(额度清零但消费仍在),用clip(upper=1.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', axis=1) y = train['SeriousDlqin2yrs'] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) dtc = DecisionTreeClassifier( max_depth=5, min_samples_leaf=50, class_weight='balanced', random_state=42 ) dtc.fit(X_train, y_train) print('DTC AUC:', roc_auc_score(y_val, dtc.predict_proba(X_val)[:, 1]))

max_depth=5和min_samples_leaf=50是控制决策树复杂度的两个关键旋钮。前者限制树的层数,后者要求每个叶子节点至少包含 50 个样本,两者配合能有效防止树把训练集里的噪声也学进去。class_weight='balanced'用来应对样本不均衡——少样本类别的误分会被赋予更高惩罚。为什么不直接跑默认参数?默认决策树几乎必然过拟合,训练集 AUC 接近 1,验证集却惨不忍睹,这种「假完美」对后续模型对比没有参考价值。

4.2 随机森林:用 bagging 稳住方差

from sklearn.ensemble import RandomForestClassifier rfc = RandomForestClassifier( n_estimators=200, max_depth=8, min_samples_leaf=30, class_weight='balanced_subsample', n_jobs=-1, random_state=42 ) rfc.fit(X_train, y_train) print('RFC AUC:', roc_auc_score(y_val, rfc.predict_proba(X_val)[:, 1]))

随机森林在决策树基础上引入了样本随机采样和特征随机选择,所以max_depth可以适当放宽到 8,靠多棵树的平均来消化方差。class_weight='balanced_subsample'是随机森林特有的参数——它会在每次 bootstrap 采样时动态调整权重,比固定balanced更贴合 bagging 的机制。n_jobs=-1表示用满所有 CPU 核心,200 棵树在这个数据量级下几秒就能跑完。

4.3 梯度提升:用 boosting 压低偏差

from sklearn.ensemble import GradientBoostingClassifier gbc = GradientBoostingClassifier( n_estimators=120, max_depth=3, learning_rate=0.05, subsample=0.8, random_state=42 ) 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_rate=0.05控制每棵树对最终结果的贡献权重,学习率越低需要的树越多,但泛化能力往往更好。subsample=0.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( C=0.5, max_iter=500, class_weight='balanced', solver='liblinear' ) lr.fit(X_train_s, y_train) print('LR AUC:', roc_auc_score(y_val, lr.predict_proba(X_val_s)[:, 1]))

逻辑回归最容易被忽视,但它的优势是稳定性和可解释性。C=0.5是正则化强度的倒数,C 越小正则化越强,在特征间存在多重共线性时能有效压缩系数波动。solver='liblinear'适合中小数据集和二分类问题,它比默认的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_weight='balanced', random_state=42), param_grid, cv=5, scoring='roc_auc', n_jobs=-1 ) grid.fit(X_train, y_train) print('Best params:', grid.best_params_)

GridSearchCV 的scoring='roc_auc'指定用 AUC 作为搜索目标,cv=5做五折交叉验证。这里有一个很多人忽略的点:交叉验证的折数会影响参数选择结果,样本量小的时候用 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_depth=10时验证集 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_col=0) # 这里要做和训练集完全一样的预处理 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', index=False)

这段代码特别强调了一点:预处理逻辑必须和训练时完全一致,包括缺失值填充的数值(用训练集的统计量,而不是测试集自己的),包括异常值过滤的阈值。实际项目里这两个数据集往往来自不同时间段,分布可能有偏移,如果各自独立做归一化,预测结果就会失真。

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, method='isotonic', cv=5 ) calibrated.fit(X_train, y_train)

method='isotonic'用等渗回归做非参数校准,适合样本量充足的情况;样本少时用sigmoid更稳。校准后输出的概率才能和实际违约率对上——这一步决定了你后续定审批阈值时用的分数是否可信。我吃过一次亏:未经校准的模型在 0.7 分以上区间的实际违约率只有 30%,而模型认为有 70%,直接导致审批策略过于保守误杀了一批好客户。从那以后我每次做完模型都强制走一遍概率校准,把校准曲线画出来和 45 度线对比,偏差超过 5 个百分点就不上线。希望这些经验能帮你在信贷建模路上少踩几个坑,快速把这份源码跑成自己的 baseline。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 22:00:15

CSS hover父子联动原理与高性能实践指南

1. 项目概述&#xff1a;hover不是“悬停”那么简单&#xff0c;它是CSS中少有的父子联动开关你有没有试过&#xff0c;鼠标移到一个导航栏的菜单项上&#xff0c;不仅这个菜单项变色&#xff0c;它下面的二级下拉菜单也跟着展开&#xff1f;或者把鼠标移到一张商品卡片上&…

作者头像 李华
网站建设 2026/10/18 21:59:53

Linux磁盘挂载从入门到实战:分区、格式化、fstab与避坑指南

1. 为什么磁盘挂载是Linux运维里绕不开的活儿干过几年Linux运维或者自己折腾过服务器的人&#xff0c;估计都有这种经历&#xff1a;刚拿到一台机器&#xff0c;df -h一看&#xff0c;根目录就几十个G&#xff0c;数据盘呢&#xff1f;数据盘在哪&#xff1f;明明买的时候选了1…

作者头像 李华
网站建设 2026/10/18 21:59:51

2026储能十大品牌榜单深度解读:技术突围与场景深耕是关键

2026年储能十大品牌榜单出炉了。有人看到的是名字&#xff0c;我看到的是一条很清晰的分界线&#xff1a;储能行业的竞争&#xff0c;已经从“谁能把电芯造出来”进入“谁能把电芯、系统、场景一起跑通”的阶段。这篇文章不打算做标题党&#xff0c;也不复读官方宣传稿&#xf…

作者头像 李华
网站建设 2026/10/18 21:55:52

数据在内存中的存储:字节序、对齐与浮点精度的底层原理

“数据在内存中的存储”这个话题&#xff0c;是每个写代码的人都绕不开的底层命题。我在项目现场排查过一个特别诡异的问题&#xff1a;一段缓存数据在运行几个小时后出现乱码&#xff0c;追了两天才发现是有人把 int 按字节数组读取&#xff0c;而且完全没考虑字节顺序。后来我…

作者头像 李华