贷款违约预测实战:信贷风控建模全流程指南
2026/9/16 3:48:54 网站建设 项目流程

做信贷风控的朋友应该都有体会,贷款违约预测是风控建模里最经典、也最能锻炼数据分析功底的任务之一。不管是银行信用卡中心、持牌消金公司,还是互联网金融平台的信贷部门,核心问题永远只有一个:借出去的钱,哪些人大概率还不上。这篇实战文章就是围绕这个问题展开的,我会用一套完整的数据分析和数据挖掘流程,带着你从原始数据出发,一步步构建一个可用的违约预测模型。配套的代码和数据集思路都是实际项目中沉淀下来的做法,适合刚入门风控建模的读者,也适合那些做数据分析想往金融方向转型的朋友。

这篇博文会把重心放在三件事上:怎么理解业务并把问题转成建模任务,怎么做特征工程和样本处理,怎么训练模型并评估效果。过程中涉及的数据处理代码、建模代码我都会贴出来,你完全可以照着跑一遍。整个项目我会用一个经典的信贷场景数据集来做演示,数据字段包含用户收入、负债、逾期历史、贷款用途等信息,目标是预测借款人是否会在未来一段时间内发生违约。

1. 贷款违约预测到底在预测什么

1.1 先从业务角度理解风控建模的目标

普通人听到“贷款违约预测”,第一反应可能是“预测一个人会不会还钱”。这个理解方向对,但在实际风控体系里还不够精确。信贷机构真正关心的,是借款人在未来某个时间窗口内(比如未来12个月)是否会出现“逾期多少天以上”的违约行为。这里的“逾期多少天”通常是30天、60天或90天,具体看产品的风险容忍度。把这个问题定义清楚了,模型的目标变量(label)才会清晰。

举个例子,在个人信贷场景中,一个客户借了钱之后,前3个月还款正常,第4个月开始不还了,一直拖到第6个月。如果我们定义“违约”为逾期超过90天,那么这个客户在第4、5、6个月其实还没达到违约状态,但到了第7个月就会被标记为违约样本。所以,时间窗口和逾期口径的定义,直接决定了标签的准确性和模型的可解释性

从数据分析的角度看,这个任务本质上是一个二分类问题:根据借款人的历史行为数据、资质数据、征信数据,预测其违约概率。模型输出的不是简单的“会/不会”,而是一个0到1之间的概率值。这个概率值后续会被映射成评分卡分数,配合风控策略中的额度、利率、拒绝规则一起使用。

1.2 正负样本不平衡问题是怎么产生的

信贷场景里,真正违约的人永远是少数。以银行信用卡业务为例,如果整体不良率控制在2%上下,那就意味着模型看到的数据集里,98%的人是正常还款的,只有2%的人会违约。这种极端不平衡的数据分布,会让很多初学者在建模时掉坑里。

我之前带过不少新人,他们拿到数据后第一件事就是用逻辑回归跑了一下,发现准确率高达97%以上,非常兴奋。但实际上这个模型等于什么都没学,只需要把所有样本都预测为“不违约”,准确率就是98%。所以在风控建模里,准确率这个指标基本没什么参考价值,我们更依赖AUC、KS、召回率、精确率这些指标来评估模型效果。

针对样本不平衡问题,常见的处理手段有三种:欠采样、过采样和代价敏感学习。在实际项目中,我更推荐先尝试调阈值和用代价敏感的方式,而不是一上来就疯狂过采样。因为SMOTE这类方法会人为构造样本,构造出来的样本分布如果和真实分布偏差太大,上线之后效果反而会打折扣。这个后面讲模型评估的时候我会展开说。

2. 数据准备与预处理才是建模的重头戏

2.1 数据集字段说明与初步探查

很多初学者喜欢一上来就写模型代码,但真正有经验的数据挖掘工程师,会把大部分时间花在数据理解和清洗上。这里我用一个公开的信贷违约数据集来做演示,核心字段包括:

字段名含义类型
person_income借款人年收入数值型
person_home_ownership房屋所有权状况分类型
loan_amnt贷款金额数值型
loan_int_rate贷款利率数值型
loan_percent_income贷款金额占收入比例数值型
cb_person_cred_hist_length征信历史长度(年)数值型
loan_status违约标签(0正常,1违约)分类型

处理这些数据的第一步,不是直接填充缺失值,而是先用describe()info()方法做快速体检,看看数据量、缺失情况、分布形态。实际业务数据里经常会出现工资收入动不动上千万的异常值,或者贷款金额为0的脏数据。如果这些不处理干净,后面特征工程全白做。

import pandas as pd import numpy as np # 读取数据 data = pd.read_csv('loan_data.csv') print(data.shape) print(data.info()) print(data.describe(percentiles=[0.01, 0.25, 0.5, 0.75, 0.99]))

这段代码输出的信息量很大。percentiles参数特别值得注意,因为我们不仅要看均值、最大最小值,还要看1%分位和99%分位的分布。信贷数据的分布通常都是右偏的,大多数人是工薪阶层收入在10万左右,少数高收入人群会把均值拉得很高,直接看均值容易误判。

2.2 缺失值处理与异常值截断

缺失值处理在风控项目里非常讲究,不是简单用均值或者中位数填充就完事。因为缺失本身可能就包含了信息。举个例子,一个客户在填写贷款申请时,如果故意不填工作单位信息,或者征信报告里某段逾期记录缺失,这些缺失背后的含义和完全随机的缺失是完全不同的。

实际处理时,我会先把缺失率大于30%的字段拎出来单独看,不急着填。缺失率不高的情况下,数值型特征一般用中位数填充,因为中位数不容易受异常值影响;分类型特征用众数填充,或者单独增加一个“未知”类别。

# 缺失率统计 missing_rate = data.isnull().sum() / len(data) print(missing_rate[missing_rate > 0]) # 数值型特征用中位数填充,分类型特征用众数填充 num_cols = ['person_income', 'loan_amnt', 'loan_int_rate'] for col in num_cols: data[col] = data[col].fillna(data[col].median()) cat_cols = ['person_home_ownership'] for col in cat_cols: data[col] = data[col].fillna(data[col].mode()[0])

异常值处理方面,我的习惯是用分位数截断,而不是直接删除样本。仍然以上面的收入字段为例,如果直接用3倍标准差来判定异常值,在收入严重右偏的分布下,很多真实的高收入群体会被误杀。更合理的做法是,把大于99%分位的值挤压(clip)到99%分位,把小于1%分位的值挤压到1%分位。这样做既保留了样本数量,又降低了极端值对模型的干扰。

2.3 特征工程:从原始字段中挖出更有价值的特征

特征工程是整个项目中我认为最有意思的部分,也是数据分析能力体现得最明显的地方。原始字段之间往往存在隐藏关系,需要通过加减乘除、分箱、交叉等方式去挖掘。

在这份信贷数据里,一个很关键的特征是loan_percent_income,也就是贷款金额占年收入的比例。直觉上,一个人如果年收入10万,却贷款了50万,还款压力会非常大,违约概率自然高。这个字段本身就是现成的,但我们可以进一步构造一些更细化的特征,比如贷款金额在年收入中的负担水平分级。

# 构造收入负债比的分级特征 data['income_load_level'] = pd.cut( data['loan_percent_income'], bins=[0, 0.1, 0.2, 0.3, 0.5, 1, float('inf')], labels=['极低', '低', '中等', '偏高', '高', '极高'] ) # 构造利率和贷款金额的交叉特征 data['loan_amnt_int_rate'] = data['loan_amnt'] * data['loan_int_rate'] # 征信历史长度分箱 data['cred_hist_bin'] = pd.cut( data['cb_person_cred_hist_length'], bins=[0, 2, 5, 10, 100], labels=['0-2年', '2-5年', '5-10年', '10年以上'] )

分类型特征处理上,贷款用途、房屋所有权状况这些字段,直接用pd.get_dummies()做独热编码就好了。不过要注意一个问题:如果类别特别多,独热编码会造成维度爆炸。比如贷款用途有几十种,那就要先对类别频次做统计,把出现次数很少的类别合并成“其他”。

提示:训练集和测试集必须用同样的特征工程逻辑。如果你在训练集上做了get_dummies,测试集上也要做一模一样的处理,否则特征数量对不上模型就报错了。这个细节很多初学者会忽略。

3. 模型训练:从逻辑回归到集成学习

3.1 为什么风控建模首推逻辑回归

金融行业对模型的可解释性要求极高。监管在审查的时候,会明确要求机构解释清楚:为什么给这个客户拒绝了贷款?是哪些因素导致了高风险判断?这种情况下,逻辑回归这样的线性模型有着天然优势,因为每个特征的权重系数都能直接解释为对违约概率的贡献方向。

逻辑回归输出的概率值可以用下面的公式表示:

[ P(y=1) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 x_1 + \cdots + \beta_n x_n)}} ]

其中(\beta_i)就是每个特征的权重系数。如果(\beta_i)是正数,说明该特征值越大,违约概率越高;反之如果是负数,说明该特征值越大,违约概率越低。这种透明性,是随机森林、XGBoost这些黑盒模型很难替代的。

当然,逻辑回归也有明显的局限性,它处理非线性关系的能力较弱。所以实际业务中,对连续变量做WOE分箱再入模是更常规的做法。分箱之后,变量和目标变量之间的关系通过WOE值来刻画,模型的表达能力会增强,同时也保持了可解释性。不过WOE编码的完整流程比较长,我们这篇文章先用逻辑回归处理标准化后的数值特征,后续进阶文章再专门讲WOE与评分卡。

3.2 训练集测试集划分与数据标准化

建模之前,数据的划分方式很关键。金融时序数据里有一个容易犯的错误,就是随机划分训练集和测试集。如果数据本身有明确的时间先后顺序,比如2020年的样本和2023年的样本混在一起随机切分,相当于让模型“偷看”了未来的信息,评估出来的效果会虚高。

处理方法很简单,如果有时间字段,就按时间排序后切分:前70%做训练,后30%做测试。如果数据集没有明显时间顺序,退而求其次可以使用随机划分,但要设置固定的random_state,保证实验可复现。

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler features = ['person_income', 'loan_amnt', 'loan_int_rate', 'loan_percent_income', 'cb_person_cred_hist_length', 'loan_amnt_int_rate'] X = data[features] y = data['loan_status'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 标准化 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

注意stratify=y这个参数,它是用来做分层采样的,保证训练集和测试集里的正负样本比例和原始数据一致。在正负样本不平衡的情况下,如果不加这个参数,可能会出现测试集里一个违约样本都没有的极端情况,那评估就完全失真了。

3.3 训练逻辑回归模型并评估

逻辑回归在Sklearn里的实现非常简洁,核心就是调用LogisticRegression,然后fitpredict_proba。值得一提的是class_weight='balanced'这个参数,它会让模型在训练时自动给少数类样本更高的权重。这是对付样本不平衡最轻量、最不容易过拟合的手段之一。

from sklearn.linear_model import LogisticRegression # class_weight='balanced' 自动调整正负样本权重 lr_model = LogisticRegression(class_weight='balanced', max_iter=1000, random_state=42) lr_model.fit(X_train_scaled, y_train) y_pred_proba_lr = lr_model.predict_proba(X_test_scaled)[:, 1]

接下来是评估环节。上面说过,准确率在这里意义不大,所以要同时计算AUC、KS、召回率和精确率。

from sklearn.metrics import roc_auc_score, precision_score, recall_score, roc_curve # 用默认阈值0.5 y_pred_lr = (y_pred_proba_lr >= 0.5).astype(int) auc_lr = roc_auc_score(y_test, y_pred_proba_lr) precision_lr = precision_score(y_test, y_pred_lr) recall_lr = recall_score(y_test, y_pred_lr) print(f'逻辑回归 AUC: {auc_lr:.4f}') print(f'逻辑回归 精确率: {precision_lr:.4f}') print(f'逻辑回归 召回率: {recall_lr:.4f}')

第一次建模跑出来的结果通常不会太惊艳,因为还没做大量的特征优化和调参。比较典型的数值大概是AUC在0.75到0.85之间,这个水平在风控场景里已经具备一定区分能力了。真正专业的做法不是纠结这个初始数值,而是看后续迭代有没有稳定的提升。

3.4 XGBoost与逻辑回归的效果对比

实际业务里,XGBoost这类梯度提升树模型是逻辑回归之外最常见的补充选项。它的优势在于能自动捕捉特征之间的复杂交互关系,不需要做太多手工特征工程,在纯机器学习算法里精度通常是最高的那档。

from xgboost import XGBClassifier # scale_pos_weight 同样用于处理样本不平衡 xgb_model = XGBClassifier( n_estimators=200, max_depth=4, learning_rate=0.05, scale_pos_weight=sum(y_train == 0) / sum(y_train == 1), random_state=42 ) xgb_model.fit(X_train, y_train) y_pred_proba_xgb = xgb_model.predict_proba(X_test)[:, 1] auc_xgb = roc_auc_score(y_test, y_pred_proba_xgb) print(f'XGBoost AUC: {auc_xgb:.4f}')

需要特别说明的是,XGBoost是树模型,它对特征的量纲不敏感,所以不需要做标准化,直接用原始特征就行。而逻辑回归是线性模型,特征的尺度直接影响梯度下降的速度和收敛效果,所以必须标准化。这两个模型在训练前对数据预处理要求的差异,是很多初学者容易搞混的地方。

从业务落地角度看,我不建议把逻辑回归和XGBoost看作非此即彼的关系,它们各有各的适用场景。逻辑回归胜在稳定、可解释、易上线,适合做拒绝原因解释和监管合规;XGBoost胜在精度高、能处理复杂模式,适合做反欺诈识别和贷中预警。成熟的风控体系往往是两者并行,一个做基础评分,一个做补充判别。

4. 模型评估与调优的关键细节

4.1 别被AUC骗了,从KS和PR曲线看真实能力

AUC是风控建模里最常用的指标,但它描述的是模型在所有可能阈值下的综合排序能力,并不直接对应某个具体阈值下的业务表现。更贴近实际业务的是KS值,它衡量的是模型把正负样本区分开的最大程度,计算方式是累计正样本比例和累计负样本比例的最大差距。

from sklearn.metrics import roc_curve # 计算KS值 fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba_lr) ks_value = max(tpr - fpr) ks_threshold = thresholds[np.argmax(tpr - fpr)] print(f'逻辑回归 KS值: {ks_value:.4f}, 最佳阈值: {ks_threshold:.4f}')

在信贷风控的实战经验里,KS大于0.3说明模型有较好的区分能力,大于0.4说明模型表现优秀,超过0.6则要警惕是否发生过拟合或者数据泄露。AUC和KS本质上是同一套逻辑衍生出来的指标,但KS更直观地告诉你在哪个分数段人分得最开。

另外,在正负样本极度不平衡的场景里,PR曲线(Precision-Recall曲线)比ROC曲线更值得关注。因为ROC曲线的横纵坐标都受负样本影响较大,正样本很少的时候,ROC看上去依然漂亮,但PR曲线会直接暴露模型在正样本上的表现。我给你一个现实场景:业务方可能会问“审批通过的人里,有多少比例最后会违约”,这就是精确率的含义;而“真正违约的人中,模型抓到了多少”,则是召回率的含义。两者存在此消彼长的关系,需要结合业务偏好去选择阈值。

4.2 阈值的选择与业务成本挂钩

很多初学者默认所有模型的判断阈值都是0.5,但在风控场景里,0.5这个阈值几乎没有意义。违约样本的占比如果只有2%,那模型预测出的违约概率普遍都会很低,用0.5做阈值很可能一个违约客户都抓不出来。

阈值到底怎么定,取决于业务成本。如果模型是用来做贷前审批的,拒绝一个违约客户带来的损失是把钱借给TA之后收不回来;但误拒一个好人,损失的是这笔贷款本该产生的利息收入。两边的成本结构不一样,最优阈值也就不一样。实际项目中,我们会根据KS曲线找到区分度最高的阈值点,再结合业务利润测算去做微调。

# 精确率、召回率随阈值变化的曲线 from sklearn.metrics import precision_recall_curve precisions, recalls, thresholds_pr = precision_recall_curve(y_test, y_pred_proba_lr) # 展示不同阈值下的表现 for th in [0.3, 0.4, 0.5, 0.6, 0.7]: y_pred_temp = (y_pred_proba_lr >= th).astype(int) p = precision_score(y_test, y_pred_temp) r = recall_score(y_test, y_pred_temp) print(f'阈值={th:.1f}, 精确率={p:.4f}, 召回率={r:.4f}')

这种阈值扫描的操作我建议每个做风控建模的人都养成习惯。不要拘泥于书本上的默认值,一定要从业务视角去反推模型参数。

4.3 特征重要性分析:让模型告诉你哪些变量最有用

不管是做风控报告,还是和业务方解释模型逻辑,特征重要性分析都是绕不开的一个环节。逻辑回归的系数大小可以部分反映特征重要性,但要注意系数绝对值大小受特征尺度影响,所以必须基于标准化后的特征来分析。

# 查看逻辑回归系数 coefficients = pd.DataFrame({ 'feature': features, 'coef': lr_model.coef_[0] }).sort_values(by='coef', ascending=False) print(coefficients)

如果是XGBoost,直接调用feature_importances_就可以拿到特征的重要性分数。两者的结论通常会有差异,这很正常。XGBoost偏向选择那些在实际分裂中带来最大信息增益的特征,逻辑回归则反映的是特征和目标之间线性关系的强弱。

从我的实际经验来看,loan_percent_income(贷款金额占收入比例)在多个信贷数据集里都是最核心的预测变量之一,loan_int_rate(利率)往往也高度重要,因为利率本身就和借款人的风险等级挂钩,风险高的用户通常要接受更高的利率才能通过审批。当你发现重要性排名和业务直觉严重不符的时候,第一反应应该是检查特征里是否混入了和目标变量存在直接逻辑关系的字段,那叫做“数据泄露”。

5. 常见坑与调优经验

5.1 数据泄露:模型效果虚高的头号元凶

特征工程的时候最容易不知不觉引入数据泄露。什么叫数据泄露?就是模型在做预测时,用到了在真实业务场景中根本不可能提前获取的信息。

举一个典型的例子,如果数据里包含贷款发放后的还款行为字段,而你拿它来预测贷款会不会违约,那AUC可以轻松到0.99,看起来无敌。但这个模型上线之后会发现:等你观测到用户还款行为时,早就该知道TA逾期了,这还预测什么?所以特征选择时,一定要确保所有特征在预测时点之前就能获取到。

另一个隐蔽的场景是数据预处理阶段的泄露。比如你在训练集和测试集合并的数据上做了标准化或者填充,而不是先拆分再分别处理,那测试集的信息就已经悄悄进入了训练过程。正确做法是:先把训练集和测试集拆开,用训练集的统计量(均值和标准差)去转换测试集。我在前面的代码里特意先fit_transform训练集再transform测试集,就是为了避免这个坑。

5.2 欠拟合与过拟合的实战判断

逻辑回归很容易欠拟合,因为模型复杂度不够,难以捕捉特征间的非线性关系。XGBoost则恰恰相反,树模型能力过强,如果不加限制,很容易把训练集背得滚瓜烂熟,测试集上一塌糊涂。

判断过拟合最简单的方法是对比训练集和测试集的AUC差异。如果训练集AUC是0.95,测试集AUC只有0.75,那大概率是过拟合了。这个时候优先降低模型复杂度,XGBoost里可以采取下面这些调整措施:

调整方向参数操作
降低单棵树复杂度max_depth从6降到3或4
增加正则化reg_lambdareg_alpha调大,比如设为1.0或更高
增加随机性subsamplecolsample_bytree设为0.7-0.9
降低学习率learning_rate从0.1降到0.05,同时增加n_estimators

我个人在调XGBoost时有个习惯:先把n_estimators设大一些(比如300到500),配上较小的learning_rate,然后看早停机制下的最优迭代次数。这样可以避免因为迭代次数不足导致的欠拟合,也避免盲目迭代导致的过拟合。

from xgboost import XGBClassifier xgb_model = XGBClassifier( n_estimators=500, max_depth=3, learning_rate=0.03, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, scale_pos_weight=sum(y_train == 0) / sum(y_train == 1), random_state=42, use_label_encoder=False, eval_metric='auc' )

5.3 样本不平衡的进阶处理思路

前面说过,class_weight='balanced'scale_pos_weight是最轻量的不平衡处理手段。但如果正负样本比例实在太悬殊,比如低于1:20,光靠调权重往往不够,需要考虑组合策略。

先做一次欠采样,把负样本降到正样本的5到10倍,再在这个降采样后的数据集上训练模型。预测的时候不需要做任何调整,因为模型输出的概率经过predict_proba之后依然具有可比性。这类方法的缺点是会损失大量负样本里的信息。

另一个思路是分群建模。把客户按照征信记录的有无分成有征信和无征信两个群体,分别构建两套模型。因为这两个客群的变量构成差异很大,无征信人群的数据维度少、缺失高,强行混在一起建模反而互相干扰。这个方法的落地成本更高,但效果往往更好,在消金公司里用得非常多。

提醒:处理样本不平衡时,优先调整权重或阈值,不要一上来就大动干戈做复杂采样。每引入一步额外处理,都是在增加上线后的维护成本和不确定性。

5.4 常见问题速查表

问题可能原因排查方法解决方案
训练集AUC很高,测试集很低过拟合对比训练/测试AUC差异降低模型复杂度、增加正则、使用早停
AUC在0.5附近特征与标签无关或数据处理出错检查特征是否标准化、字段是否对齐重新梳理特征工程,检查标签是否错位
模型预测全为0样本不平衡且未做任何处理查看预测概率分布设置class_weight或降低判断阈值
特征数量对不上训练集和测试集独热编码不一致比较训练/测试维度先合并特征列再处理,或统一用同样的类别列表
KS异常高(超过0.6)可能存在数据泄露审查特征是否用到未来信息剔除泄露特征,重新训练

上面这些坑,说实话每一个我都踩过。尤其是数据泄露这个问题,刚入行的时候完全没概念,以为特征越多越好,结果模型在回测时惊艳全场,上线后立刻翻车。后来养成了一个习惯:每做一次特征工程,都要问自己一句“这个字段在预测时点真的能拿到吗?”这个问题,希望你从第一天就开始问自己。

6. 从模型到业务:后续还能做什么

有些读者跑完上面的代码,看到AUC的数值就认为项目结束了。但从真实业务的角度看,模型落地才是一切的开始。这篇文章用经典数据集演示了贷前违约预测的完整流程,从数据探查、特征工程、模型训练到评估方法,每一步在真实的风控架构里都有对应的系统角色。

代码和数据集你可以自己复现,跑通之后可以再往这几个方向深挖:第一个方向是评分卡,把逻辑回归的系数直接转换成标准化的评分卡分数,这也是银行体系里最通用的形式;第二个方向是模型融合,用逻辑回归和XGBoost的输出做Stacking,往往能再带来几个百分点的AUC提升;第三个方向是特征工程自动化,用Featuretools这样的工具自动生成大量衍生特征,再配合特征筛选来做降维。

根据我个人的体会,贷款违约预测这个项目适合反复做,每做一遍都会有新的理解。第一次做,你可能把重心放在跑通代码上;第二次做,你会开始关心特征背后的业务含义;第三次做,你会主动思考模型上线后的监控和回退机制。等到你开始操心这些事了,说明你已经不再是单纯写代码的初级数据分析师,而是真正在往风控模型专家的方向成长了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询