ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

贷款违约预测实战:从数据探索到模型评估的完整流程

2026/9/16 6:18:52 拓冰建站 浏览量
贷款违约预测实战:从数据探索到模型评估的完整流程 做贷款违约预测这个项目我前后折腾了大半个月。最初只是想拿一份公开数据集练手结果越做越觉得有意思从最开始的简单逻辑回归到后面加入特征工程、处理样本不平衡、调参对比整个链路跑下来对数据分析在金融风控里的落地方式有了非常直观的理解。这篇文章我把整个项目完整拆开从数据探索、特征工程到模型训练和评估代码和思路都会给到数据集我也会说明从哪里拿、怎么处理。不管你是刚入门数据分析还是想了解风控建模的大致流程这份实战记录应该都能给你一些参考。1. 项目核心思路与方案选型1.1 贷款违约预测到底在解决什么问题贷款违约预测说白了就是金融机构在放贷之前根据借款人的历史数据判断这个人未来会不会还不上钱。这个问题的业务价值非常直接——如果模型能把“高风险用户”提前识别出来机构就可以拒绝放款、降低额度或者要求增加担保措施从而减少坏账损失。从技术角度拆解其实是一个典型的二分类监督学习问题标签是“是否违约”特征是借款人的年龄、收入、负债率、历史逾期次数等。你需要训练一个分类器输入特征输出违约概率。但和普通的分类任务比如图片分类、垃圾邮件识别相比风控场景有几个明显特点样本极度不平衡真实场景里违约用户通常只占5%以下如果模型把所有用户都判为“正常”准确率也能到95%以上但这个模型毫无用处。错误代价不对等把“坏人”放进来误放和把“好人”挡出去误杀代价完全不同。前者直接亏钱后者损失的是业务量和声誉。所以评估指标不能用准确率要看AUC、KS、召回率等。特征可解释性要求高银行风控部门用模型不仅要知道“谁违约”还要知道“为什么违约”这样才能满足监管要求也方便业务人员理解模型行为。所以这个项目虽然名字叫“贷款违约预测”但做的时候不能只把它当一个机器学习练习得带着业务逻辑去思考每个特征的含义和每个指标的选择。1.2 技术方案选型为什么用Python sklearn这套组合技术选型上我用了Python搭配pandas、scikit-learn、imbalanced-learn这几个库。先说为什么不用深度学习。贷款违约预测的特征通常是表格型数据几十个到几百个特征样本量在几万到几十万量级。这种场景下传统机器学习模型逻辑回归、随机森林、XGBoost效果已经非常好训练速度快可解释性强而且部署成本低。深度神经网络在图像、文本、语音这类非结构化数据上有优势但在表格数据上优势并不明显反而容易过拟合调参也麻烦。再用逻辑回归作为基线模型原因有三个。第一逻辑回归输出的是概率值天然适合做信用评分第二模型权重可以直接解释为特征对违约风险的影响方向和大小第三训练极快几秒钟就能跑完适合作为后续复杂模型的对比基准。随机森林/梯度提升树作为对比模型是因为它们在处理非线性关系、特征交互时表现更好而且能输出特征重要性帮助做特征筛选。金融风控里常用的评分卡模型很多就是从逻辑回归衍生出来的而XGBoost、LightGBM则是Kaggle竞赛里的常胜将军实际业务里用得也越来越多。1.3 数据集说明从哪里拿数据、字段长什么样模型训练离不开数据。我这次用的是公开的德国信用数据集German Credit Dataset这个数据集在UCI机器学习库上可以直接下载也可以从Kaggle找到格式通常是csv。它包含1000条样本、20个特征标签是“信用好/信用差”其中信用差违约样本300条违约率30%——这个比例比真实业务场景高很多但作为教学案例反而方便处理。主要字段包括字段名含义类型age年龄数值sex性别类别job工作类型有序类别housing住房类型类别saving_accounts储蓄账户余额类别checking_account活期账户余额类别credit_amount贷款金额数值duration贷款期限月数值purpose贷款用途类别risk标签1违约0正常类别如果你不想用这份数据也可以去Kaggle搜“Lending Club Loan Data”那个数据集更大几十万条字段更丰富但需要更多的清洗工作。初次练手的话德国信用数据集更合适字段少、逻辑清晰半天时间就能跑通全流程。我后面的代码也基于这份数据你拿到csv之后放到项目目录下就能复现。2. 数据探索与特征工程实操2.1 第一步先别急着建模把数据“看”明白拿到数据第一件事不是跑模型而是做EDA探索性数据分析。我见过不少新手上来就train_test_split然后直接model.fit结果模型效果差还不知道为什么。数据里的异常、缺失、分布偏态、类别不均衡都会直接影响模型效果你不看数据根本发现不了。我习惯先做这几步import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 读取数据 df pd.read_csv(german_credit.csv) # 1. 查看基本结构 print(df.shape) # (1000, 21) print(df.info()) # 查看每列的dtype和非空值数量 print(df.describe()) # 数值列的统计描述 # 2. 检查缺失值 missing df.isnull().sum() print(missing[missing 0])德国信用数据集的原始版本缺失值不多但在网上流传的csv版本里Saving accounts和Checking account两个字段经常是NaN因为这些字段本身表示“账户余额类型”没有账户的人可能是真的没有存款、也可能是数据没采集到这两种情况对业务含义来说其实可以合并处理。遇到缺失值处理方式我会在后面专门讲。接下来看标签分布和目标变量的比例# 标签分布 print(df[Risk].value_counts()) print(df[Risk].value_counts(normalizeTrue))如果正常样本占70%、违约占30%那这个比例还算能接受但真实业务里如果违约率只有3%~5%就要考虑采样策略或者用类别权重了。2.2 缺失值和异常值处理基本原则与实操缺失值处理没有放之四海皆准的方法要看业务含义和数据量级。我总结的经验是缺失率低于5%可以直接删除对应样本或者用均值/中位数填充。缺失率在5%~20%优先用中位数填充数值特征用众数填充类别特征也可以根据其他特征做简单预测填充。缺失率超过20%这个特征可能本身就不可靠要么删除要么单独作为一列“是否缺失”的特征加入模型让模型自己去学。德国信用数据集里如果Saving accounts和Checking account存在NaN我倾向于填充为“无账户”这个类别因为从业务角度讲没有储蓄账户和没有储蓄信息对风险评估的指向性是相近的。异常值方面数值型特征里credit_amount贷款金额和duration贷款期限可能会出现极端值。我的处理思路是先画箱线图看看分布然后对超过3倍IQR的值做截尾处理而不是直接删除——因为贷款金额大本身可能不是错误只是少数情况删除会损失信息截尾可以缓解极端值对模型的干扰。# 用IQR识别异常值并做截尾处理 def cap_outliers(series): Q1 series.quantile(0.25) Q3 series.quantile(0.75) IQR Q3 - Q1 lower Q1 - 3 * IQR upper Q3 3 * IQR return series.clip(lower, upper) for col in [credit_amount, duration, age]: df[col] cap_outliers(df[col])2.3 特征编码类别变量怎么喂给模型德国信用数据集里大部分特征是类别变量比如住房类型、贷款用途、储蓄账户状态。机器学习模型只吃数值所以需要编码。两种常用方式标签编码Label Encoding适用于有序类别比如job工作类型1、2、3、4本身有等级含义。独热编码One-Hot Encoding适用于无序类别比如purpose贷款用途“买车”和“买家电”没有大小关系用独热编码才不会引入错误的顺序信息。我用pd.get_dummies做独热编码同时注意把标签列先拆出来。# 分离特征和标签 X df.drop(Risk, axis1) y df[Risk] # 数值列和类别列分开处理 num_cols [age, credit_amount, duration] cat_cols [c for c in X.columns if c not in num_cols] # 独热编码 X_encoded pd.get_dummies(X, columnscat_cols, drop_firstTrue) print(X_encoded.shape)drop_firstTrue是为了避免共线性——如果不drop独热编码会产生“虚拟变量陷阱”逻辑回归里会出现完全多重共线性影响参数解释。2.4 样本不平衡不能忽视的“隐藏杀手”如果说这个项目里有一个最容易被新手忽略的坑那就是样本不平衡。逻辑回归这种模型在正负样本比例悬殊时会倾向于预测多数类导致召回率很低。德国信用数据集30%的违约率还好但真实风控数据经常是5%以下。我做了两种处理第一种是class_weightbalanced让模型自动调整类别权重代价小、代码简单适合快速验证。from sklearn.linear_model import LogisticRegression model_lr LogisticRegression(class_weightbalanced, max_iter1000)第二种是SMOTE过采样用imbalanced-learn库生成少数类的合成样本。SMOTE的原理是在少数类样本之间插值生成新样本而不是简单复制能有效缓解过拟合风险。from imblearn.over_sampling import SMOTE smote SMOTE(random_state42, sampling_strategyauto) X_resampled, y_resampled smote.fit_resample(X_encoded, y) print(X_resampled.shape, y_resampled.value_counts().values)需要注意SMOTE一定要在划分训练集和测试集之后做只能对训练集过采样绝对不能对测试集做。否则模型评估时看到的“违约样本”是生成的假样本评估结果就没有意义了。这个问题我后面还会在避坑清单里详细说。3. 模型构建与评估全流程3.1 划分数据集为什么不能用默认的train_test_split划分训练集、验证集、测试集基本原则是所有预处理步骤都在训练集上学习然后应用到测试集。这里我特别强调一件事先切分再做特征工程和采样。from sklearn.model_selection import train_test_split # 先用原始数据切分 X_train, X_test, y_train, y_test train_test_split( X_encoded, y, test_size0.2, random_state42, stratifyy )stratifyy是保持训练集和测试集的类别比例一致这样即使数据不平衡两个集合里的违约率也接近。如果不加这个参数极端情况下可能测试集里一个违约样本都没有那模型评估就完全失真了。切分完之后再用SMOTE对X_train做处理测试集保持原始数据不动。3.2 基线模型先用逻辑回归快速跑通先写一个训练函数统一管理模型训练和评估逻辑后续换模型就非常方便。from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, roc_curve, confusion_matrix, classification_report ) def evaluate_model(model, X_train, y_train, X_test, y_test, model_name): model.fit(X_train, y_train) y_pred model.predict(X_test) y_pred_proba model.predict_proba(X_test)[:, 1] print(f {model_name} ) print(准确率:, round(accuracy_score(y_test, y_pred), 4)) print(精确率:, round(precision_score(y_test, y_pred), 4)) print(召回率:, round(recall_score(y_test, y_pred), 4)) print(F1:, round(f1_score(y_test, y_pred), 4)) print(AUC:, round(roc_auc_score(y_test, y_pred_proba), 4)) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred)) return model逻辑回归加上class_weightbalanced跑出来的结果AUC大概在0.75~0.78左右这个水平不算高但作为基线足够用了。关键是把评估流程跑通后面换更好的模型才能有对比。3.3 进阶模型随机森林与前向逐步验证随机森林是集成学习里的Bagging代表它训练多个决策树然后投票抗过拟合能力强不用做太多特征缩放对类别特征也不敏感非常适合作为第二梯队模型。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators200, max_depth8, min_samples_split10, min_samples_leaf5, random_state42, class_weightbalanced ) evaluate_model(rf, X_train, y_train, X_test, y_test, RandomForest)随机森林的AUC通常能到0.80以上。这里有几个超参数值得注意n_estimators树的数量太少了模型不够稳定太多了训练慢200是一个性价比不错的起点。max_depth限制树的深度防止过拟合。风控数据特征数量一般不多深度5~10就够。min_samples_leaf叶子节点的最小样本数建议设置大一点比如5~20避免模型学到过于罕见的模式。如果你还想测试XGBoost或LightGBM逻辑上也完全一样只是接口略有差异。我后面会提到怎么把这段代码简单改成XGBoost的版本。3.4 模型评估指标风控场景不看准确率那看什么很多人第一次做分类任务习惯性先看准确率。但在风控场景里准确率真的会骗人。举一个例子如果数据集里95%是正常用户、5%是违约用户模型把所有用户都预测为“正常”准确率是95%看起来很高但它一个违约用户都没发现这种模型放到业务里就是灾难。所以风控建模里常用的指标是AUCROC曲线下面积衡量模型把违约用户排在正常用户前面的能力。0.5是随机猜测0.7~0.8有可用价值0.8以上算优秀。AUC不依赖于具体阈值适合做模型间的横向对比。召回率Recall所有真实违约用户里模型抓到了多少。风控场景里召回率低了意味着大量坏人漏网。精确率Precision模型预测为违约的用户里有多少是真的违约。精确率低了意味着误杀太多影响正常业务。KS值风控领域非常看重的指标衡量模型对好坏用户的区分度一般大于0.3就算可用大于0.5非常优秀。我通常会同时看AUC和召回率AUC看整体排序能力召回率看实际抓人的效果两者结合才能对模型有全面的判断。# 画ROC曲线 import matplotlib.pyplot as plt def plot_roc(model, X_test, y_test, model_name): y_pred_proba model.predict_proba(X_test)[:, 1] fpr, tpr, _ roc_curve(y_test, y_pred_proba) auc_value roc_auc_score(y_test, y_pred_proba) plt.figure(figsize(6, 5)) plt.plot(fpr, tpr, labelf{model_name} (AUC{auc_value:.4f})) plt.plot([0, 1], [0, 1], k--) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(ROC Curve) plt.legend() plt.show()3.5 特征重要性分析模型的“解释权”很重要风控模型不只是拿来做预测的还要能解释。比如银行审批一笔贷款被拒客户来投诉你得能说清楚为什么被拒——是负债率太高还是历史逾期太严重。这个时候特征重要性分析就很有用。随机森林可以直接输出特征重要性# 特征重要性 importances pd.Series(rf.feature_importances_, indexX_train.columns) top20 importances.nlargest(20) plt.figure(figsize(10, 6)) top20.sort_values().plot.barh() plt.title(Top 20 Feature Importance) plt.show()从业务角度理解这些特征也很合理checking_account活期账户余额通常排在首位因为它直接反映借款人当前的资金流动性duration贷款期限和credit_amount贷款金额也很重要期限越长、金额越大不确定性越高。你看到这个结果会自然理解模型不是“黑箱”它的判断逻辑和我们业务常识是一致的。4. 常见问题与避坑技巧实录4.1 数据泄漏最隐蔽、也最致命的错误数据泄漏是建模里最常见的低级错误但很多人踩了坑还不自知。我举几个典型场景场景一先标准化/SMOTE再划分训练集和测试集。标准化时要计算均值和标准差如果拿全部数据计算测试集的信息就已经泄漏到训练阶段了。SMOTE也是同理如果先对全量数据过采样再切分训练集里可能会有测试集样本的“合成版本”模型评估结果虚高。场景二用全量数据做特征选择再切分训练集。比如你先用所有数据计算特征和标签的相关性筛掉一部分特征再切分数据集。这个相关性计算本身就用了测试集标签的信息同样属于泄漏。正确的做法是先切分再在训练集上做标准化、特征选择、过采样。实在不确定先后顺序就记住一个原则——“任何从数据中学到的参数都只能从训练集学”。4.2 类别不平衡处理过头反而让模型失真SMOTE不是万能的。我之前有一版实验把SMOTE的sampling_strategy设为1.0让正负样本完全1:1结果模型在测试集上的AUC反而降了。原因很简单过采样比例太高模型学到了太多合成样本的模式反而偏离了真实分布。我的经验是先尝试class_weightbalanced如果召回率还是不理想再上SMOTE但sampling_strategy不要超过0.5~0.8也就是让少数类占比到总样本的30%~40%就够。别追求完全均衡要的是模型在真实分布上表现好不是在平衡分布上好。4.3 随机森林过拟合了怎么办随机森林虽然抗过拟合能力强但不代表不会过拟合。当你发现训练集AUC接近1测试集AUC只有0.7的时候基本可以判断过拟合了。我的排查顺序是先看max_depth是不是太大降到5~10。调大min_samples_leaf让叶子节点至少包含5~10个样本。适当减少n_estimators如果500棵树和100棵树表现差不多没必要用500。必要时用交叉验证选超参数不要手动瞎调。另外一个技巧是看特征重要性如果模型把某个特征的权重给得特别高但你对这个特征的业务含义不太确定要怀疑是不是数据本身有偏差。4.4 评估指标不能只看一个数不同模型的AUC可能很接近比如随机森林0.82、XGBoost 0.815但它们的业务表现可能差异很大。我会同时看混淆矩阵关注误放和误杀的具体数字。一个经验是如果模型在保持高AUC的同时还能把违约召回率做到60%以上并且误杀率控制在20%以内就已经是业务上可用的模型了。另外风控模型上线前基本都会做“分数阈值校准”——把模型输出的概率映射成整数分数比如300~850分再设定一个审批阈值。这部分属于评分卡建模的范畴后续可以单独写一篇细讲。5. 从代码到业务模型之外还要想什么这个项目做到这里已经完整跑通了数据清洗、特征工程、模型训练、效果评估的整个链路。但如果你真的把模型用在金融业务里还有几件事是代码之外必须要考虑的。第一模型的时效性。信贷用户的还款行为会随着经济环境、政策变化而变化。今年训练出来的模型明年可能就失效了。所以一定要建立模型监控机制定期用最新数据做回测看看AUC有没有明显下降KS是否变差必要时做模型迭代。第二特征稳定性。有些特征短期内有区分度但随着时间推移分布会漂移比如某个年龄段的用户突然增多或者减少。上线后要注意监控特征分布的变化遇到异常波动要及时排查原因。第三可解释性与合规。在金融行业直接用一个深度神经网络做信贷审批除非你能解释清楚每个决策的原因否则很难过监管这一关。这也是为什么逻辑回归、评分卡这类可解释性强的模型在传统金融机构里仍然占据主流。就算用了随机森林或XGBoost也应该配合SHAP、LIME等工具做解释分析。从实操角度来看我建议你把这个项目作为起点然后朝这几个方向扩展一是换更大的数据集比如Lending Club的公开数据体验处理几十万条数据时性能优化的重要性二是加入特征工程的高级技巧比如WOE编码、IV值筛选这是评分卡建模的核心内容三是尝试XGBoost或LightGBM对比不同模型的效果差异。这样一路做下来你对风控建模的理解会非常立体。我个人在实际跑这套代码时体会最深的一点是模型效果差不要急着换更复杂的算法先把数据质量打牢——缺失值处理是否合理特征编码是否引入多余信息样本不平衡是否处理得当这些基础工作做到位哪怕用一个简单的逻辑回归也能拿到及格线以上的结果。数据决定模型的上限算法只是去逼近这个上限这句话在风控场景里体现得尤其明显。