
简介这是一套面向计算机、人工智能、自动化等专业学生与从业者的深度学习实战项目源码围绕个人贷款违约预测展开可作为期末课程设计、课程大作业或毕业设计的完整参考方案也适合基础较好的学习者在此基础上修改扩展功能。资源包共28个文件约16.37MB包含5个Python脚本、4个Jupyter Notebook、8个CSV数据集、5个XML配置及xlsx、md、docx等文档覆盖数据读取、预处理、独热编码、金额清洗与可视化分析等环节代码注释详尽并附运行教程。项目已通过调试测试答辩评审分达95分读者可据此掌握从原始交易、账户、信用卡等数据到违约预测建模的完整流程理解特征工程与数据构建思路并借助排错说明快速跑通代码。目前已有673人学习下载适合需要系统实践深度学习与金融风控场景的学习者参考借鉴。1. 从一份带注释的 Python 源码说起个人贷款违约预测到底在做什么打开招聘网站搜“风控建模”十个岗位里有八个在提“违约预测”。但真正动手做过的人都知道这件事的难点从来不是把模型跑起来而是搞清楚数据长什么样、特征怎么造、模型输出怎么解释给业务听。一份标注齐全的 Python 源码之所以有价值不是因为它能直接上线而是它把“从原始借贷记录到违约概率”这条链路完整摊开给你看——每一步在算什么、参数为什么这么设、哪里容易翻车。个人贷款违约预测本质是一个二分类问题给定一笔贷款申请或一笔存量贷款预测借款人在未来某个时间窗口内是否会违约。输入是借款人属性、贷款属性、还款历史等结构化字段输出是一个 0 到 1 之间的违约概率。听起来简单但实际数据里正负样本极度不平衡违约率往往只有个位数百分比模型很容易“全预测为不违约”就能拿到 90% 以上的准确率——这种模型在业务上毫无价值。这份源码适合两类人一是刚学完深度学习基础、想找一个完整项目练手的在校生或转行者二是已经在做规则风控、想往模型风控方向转的从业者。它不要求你有 GPU 集群一台普通笔记本就能跑通全流程。接下来我会按“数据怎么处理 → 模型怎么搭 → 怎么训练和评估 → 怎么避坑 → 怎么进阶”的顺序把这条链路拆开讲清楚。2. 数据准备与特征工程把原始借贷记录变成模型能吃的张量2.1 先搞清楚你的数据里有什么个人贷款违约预测常用的公开数据集字段结构大同小异。常见的有几类借款人基本信息年龄、职业、收入水平、婚姻状况、贷款信息贷款金额、期限、利率、用途、信用历史历史逾期次数、信用查询次数、已有负债、还款行为当前逾期天数、过去 12 个月还款状态。这些字段里数值型、类别型、有序型混杂不能直接丢进神经网络。我一般会先做一轮字段盘点把每个字段的类型、缺失率、取值分布列出来。缺失率超过 40% 的字段直接砍掉除非业务上确认它极其重要。取值分布极度偏斜的类别字段比如某个类别占比超过 95%也要警惕它可能带来噪声而不是信息。import pandas as pd import numpy as np # 读取原始数据假设是 CSV 格式 df pd.read_csv(loan_data.csv) # 字段盘点类型、缺失率、唯一值数 field_summary pd.DataFrame({ dtype: df.dtypes, missing_rate: df.isnull().mean().round(4), nunique: df.nunique() }) print(field_summary.sort_values(missing_rate, ascendingFalse)) # 查看目标变量分布确认不平衡程度 print(df[default].value_counts(normalizeTrue))这段代码做的是最基础的数据体检。missing_rate帮你快速定位哪些字段需要填充或丢弃nunique帮你区分类别型和数值型value_counts(normalizeTrue)让你对正负样本比例心里有数。如果违约样本占比低于 5%后面训练时必须做重采样或调损失函数权重否则模型会偏向多数类。2.2 缺失值填充和异常值处理的具体做法数值型字段的缺失我一般用中位数填充而不是均值因为金融数据里长尾分布太常见均值容易被极端值拉偏。类别型字段的缺失单独归为“未知”一类不要用众数填充——众数填充会人为放大某个类别的权重让模型学到虚假的相关性。异常值处理要分情况。年龄出现 200 岁、收入出现负数这种明显是录入错误直接按业务规则截断或置为缺失。但“收入很高”不一定是异常值可能是真实的高净值客户不能一刀切用 3σ 原则删掉。我的习惯是先做业务合理性检查年龄 18-70、收入大于 0再做统计分布检查分位数截断两步都过了才保留。# 数值型缺失用中位数填充 num_cols df.select_dtypes(include[np.number]).columns.tolist() num_cols.remove(default) # 目标变量不填充 for col in num_cols: median_val df[col].median() df[col] df[col].fillna(median_val) # 类别型缺失归为Unknown cat_cols df.select_dtypes(include[object]).columns.tolist() for col in cat_cols: df[col] df[col].fillna(Unknown) # 业务合理性截断年龄限制在18-70 df[age] df[age].clip(18, 70) # 收入取1%和99%分位数截断避免极端值干扰 income_low df[income].quantile(0.01) income_high df[income].quantile(0.99) df[income] df[income].clip(income_low, income_high)clip函数在这里做的是截断而不是删除好处是保留了样本量同时把极端值拉回合理区间。分位数选 1% 和 99% 是经验值样本量大时可以放宽到 0.5% 和 99.5%样本量小就收紧到 2% 和 98%。填充和截断的顺序不能反——先填充再截断否则截断后的分位数会受缺失值影响。2.3 类别字段编码和数值字段标准化类别字段编码有两种常见做法独热编码和嵌入层。字段取值少比如性别、婚姻状况取值不超过 5 个用独热编码就够了取值多比如职业类别有几十种用嵌入层更合适让模型自己学类别之间的关系。这份源码里我建议先用独热编码跑通后面再换嵌入层对比效果。数值字段标准化用 StandardScaler 或 MinMaxScaler 都行但要注意标准化参数只能从训练集计算然后应用到验证集和测试集。如果全量数据一起标准化验证集的信息会泄露到训练过程导致评估结果虚高。这是血泪教训很多人第一次做的时候都会踩这个坑。from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 区分数值列和类别列 num_features [age, income, loan_amount, interest_rate, debt_ratio] cat_features [gender, marital_status, occupation, loan_purpose] # 构建预处理管道 preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), num_features), (cat, OneHotEncoder(handle_unknownignore), cat_features) ]) # 注意fit_transform只在训练集上做 X_train preprocessor.fit_transform(X_train_raw) X_val preprocessor.transform(X_val_raw) X_test preprocessor.transform(X_test_raw)handle_unknownignore这个参数很关键。验证集或测试集里可能出现训练集没见过的类别取值如果不设这个参数编码器会直接报错。设成ignore后未见过的类别会被编码成全零向量模型至少不会崩。fit_transform和transform的区分是防止数据泄露的核心操作训练集用fit_transform验证集和测试集只能用transform。3. 深度学习模型搭建用 PyTorch 写一个能跑通的违约预测网络3.1 为什么选全连接网络而不是 LSTM 或 Transformer个人贷款违约预测的数据是结构化表格数据不是序列数据也不是图像数据。全连接网络MLP对这种表格数据的建模能力已经足够而且训练快、调参简单、可解释性相对好。LSTM 和 Transformer 更适合处理时序关系或长距离依赖用在表格数据上不仅收益有限还会大幅增加训练成本和调参难度。我见过有人拿 Transformer 做信贷违约预测最后效果和三层 MLP 差不多但训练时间翻了十倍。除非你的数据里确实有明确的时序结构比如逐月的还款记录序列否则不要为了“用上新技术”而过度设计。这份源码里用的是经典的三层全连接网络输入维度等于预处理后的特征数隐藏层用 ReLU 激活输出层用 Sigmoid 把值压到 0 到 1 之间。3.2 网络结构定义和关键参数说明import torch import torch.nn as nn class DefaultPredictor(nn.Module): def __init__(self, input_dim, hidden_dims[128, 64, 32], dropout_rate0.3): super(DefaultPredictor, self).__init__() layers [] prev_dim input_dim for hidden_dim in hidden_dims: layers.append(nn.Linear(prev_dim, hidden_dim)) layers.append(nn.BatchNorm1d(hidden_dim)) # 加速收敛稳定训练 layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout_rate)) # 防止过拟合 prev_dim hidden_dim layers.append(nn.Linear(prev_dim, 1)) layers.append(nn.Sigmoid()) # 输出违约概率 self.network nn.Sequential(*layers) def forward(self, x): return self.network(x) # 假设预处理后特征维度为 45 model DefaultPredictor(input_dim45) print(model)hidden_dims控制隐藏层宽度和深度。128-64-32 这个配置在几千到几万条样本的数据集上比较稳样本量更大可以加到 256-128-64样本量小就降到 64-32。dropout_rate设 0.3 是常用起点过拟合严重就调到 0.5欠拟合就降到 0.1。BatchNorm1d放在线性层之后、激活函数之前能显著加快收敛速度但如果 batch size 太小比如小于 16BatchNorm 的效果会不稳定这时候可以换成 LayerNorm。3.3 损失函数选择处理样本不平衡的关键二分类最常用的损失函数是 BCEWithLogitsLoss 或 BCELoss。但违约预测里正负样本极度不平衡直接用普通 BCE 会让模型偏向多数类。解决办法有两个一是给正样本更高的权重二是用 Focal Loss。我一般先用带权重的 BCE 跑一版如果效果不够再换 Focal Loss。# 计算正负样本权重 neg_count (y_train 0).sum() pos_count (y_train 1).sum() pos_weight torch.tensor([neg_count / pos_count], dtypetorch.float32) # 带权重的BCE损失 criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) # 如果要用Focal Loss可以这样定义 class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0): super(FocalLoss, self).__init__() self.alpha alpha self.gamma gamma def forward(self, inputs, targets): bce_loss nn.functional.binary_cross_entropy_with_logits( inputs, targets, reductionnone) pt torch.exp(-bce_loss) focal_loss self.alpha * (1 - pt) ** self.gamma * bce_loss return focal_loss.mean()pos_weight设为负样本数除以正样本数意思是让正样本的损失放大这么多倍。如果违约率是 5%pos_weight就是 19模型会更关注正样本。Focal Loss 的gamma参数控制对难分类样本的关注程度gamma2是原论文推荐值实际用的时候可以在 1 到 3 之间调。注意用BCEWithLogitsLoss时模型输出层不要加 Sigmoid因为损失函数内部已经做了用BCELoss时才需要加 Sigmoid。这个细节搞错会导致损失值异常训练完全不收敛。3.4 训练循环和验证策略训练循环里要做的几件事前向传播、计算损失、反向传播、更新参数、记录指标。验证集用来监控过拟合如果验证集损失连续几个 epoch 不下降就提前停止。学习率用 Adam 的默认值 1e-3 起步如果损失震荡就降到 1e-4。from torch.utils.data import DataLoader, TensorDataset # 构建DataLoader train_dataset TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train.values)) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5, factor0.5) best_val_loss float(inf) patience_counter 0 for epoch in range(100): model.train() for batch_X, batch_y in train_loader: optimizer.zero_grad() outputs model(batch_X).squeeze() loss criterion(outputs, batch_y) loss.backward() optimizer.step() # 验证阶段 model.eval() with torch.no_grad(): val_outputs model(torch.FloatTensor(X_val)).squeeze() val_loss criterion(val_outputs, torch.FloatTensor(y_val.values)) val_preds (torch.sigmoid(val_outputs) 0.5).float() val_acc (val_preds torch.FloatTensor(y_val.values)).float().mean() scheduler.step(val_loss) print(fEpoch {epoch}: val_loss{val_loss:.4f}, val_acc{val_acc:.4f}) # 早停逻辑 if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_model.pth) else: patience_counter 1 if patience_counter 10: print(Early stopping triggered) breakbatch_size64是常用起点显存不够就降到 32 或 16。ReduceLROnPlateau在验证损失不下降时自动降低学习率patience5表示等 5 个 epochfactor0.5表示每次降一半。早停的patience_counter设 10意思是验证损失连续 10 个 epoch 不改善就停。注意验证阶段要加model.eval()和torch.no_grad()前者关闭 Dropout 和 BatchNorm 的训练行为后者节省显存并加速计算。4. 模型评估与业务落地AUC 高不代表能赚钱4.1 为什么准确率在违约预测里几乎没用前面提过违约率通常只有个位数百分比。如果模型把所有样本都预测为“不违约”准确率能到 95% 以上但这个模型一个违约客户都抓不到。所以评估违约预测模型准确率只能作为参考核心指标是 AUC、KS 和召回率。AUC 衡量的是模型对正负样本的排序能力取值 0.5 到 1.00.5 等于随机猜1.0 是完美排序。信贷场景里 AUC 到 0.75 以上就算可用0.8 以上算不错。KS 值衡量的是模型区分正负样本的最大差距通常要求大于 0.3。召回率在违约预测里尤其重要——漏掉一个违约客户的代价远大于误拒一个正常客户。from sklearn.metrics import roc_auc_score, roc_curve, recall_score, precision_score model.eval() with torch.no_grad(): test_outputs model(torch.FloatTensor(X_test)).squeeze() test_probs torch.sigmoid(test_outputs).numpy() # AUC auc roc_auc_score(y_test, test_probs) print(fAUC: {auc:.4f}) # KS fpr, tpr, thresholds roc_curve(y_test, test_probs) ks max(tpr - fpr) print(fKS: {ks:.4f}) # 在某个阈值下的召回率和精确率 threshold 0.3 # 根据业务需求调整 test_preds (test_probs threshold).astype(int) recall recall_score(y_test, test_preds) precision precision_score(y_test, test_preds) print(fThreshold{threshold}: Recall{recall:.4f}, Precision{precision:.4f})阈值的选择是业务问题不是技术问题。如果业务目标是“尽量不漏掉违约客户”阈值就调低比如 0.2牺牲精确率换召回率。如果业务目标是“尽量减少误拒正常客户”阈值就调高比如 0.5。我一般会画一条阈值-召回率-精确率的曲线让业务方自己选一个平衡点。4.2 用 SHAP 做特征归因让风控部门能看懂深度学习模型常被诟病“黑匣子”但违约预测场景里业务方需要知道“为什么这个人被判定为高风险”。SHAP 是目前最常用的特征归因工具它能给出每个特征对单条预测结果的贡献值。import shap # 用训练集的一个子集作为背景数据 background torch.FloatTensor(X_train[:100]) explainer shap.DeepExplainer(model, background) # 解释测试集前50个样本 test_samples torch.FloatTensor(X_test[:50]) shap_values explainer.shap_values(test_samples) # 可视化单个样本的特征贡献 shap.initjs() shap.force_plot(explainer.expected_value, shap_values[0], feature_namesall_feature_names)DeepExplainer适合深度学习模型background用训练集子集就行100 条足够。shap_values的维度是样本数特征数正值表示该特征推高了违约概率负值表示拉低了违约概率。业务方看到“历史逾期次数 0.15”这样的解释比单纯看到一个 0.82 的概率值要信服得多。4.3 模型上线前必须做的三件事第一时间外验证。用较早时间段的数据训练用较晚时间段的数据测试模拟真实上线场景。如果时间外 AUC 比随机划分的 AUC 低很多说明模型学到了时间相关的噪声不能上线。第二压力测试。把某些特征人为推到极端值比如收入设为 0、负债比设为 10看模型输出是否合理。如果收入为 0 的人违约概率反而很低说明模型学到了错误的因果关系。第三公平性检查。检查模型对不同年龄段、不同性别群体的预测是否存在系统性偏差。如果某个群体的违约率被显著高估或低估需要调整特征或加约束。5. 避坑指南训练违约预测模型时最容易翻车的五个地方5.1 数据泄露验证集信息偷偷溜进了训练过程现象验证集 AUC 高得离谱0.95 以上但上线后效果一塌糊涂。原因最常见的是标准化或编码时用了全量数据。比如先对全量数据做 StandardScaler再划分训练集和验证集验证集的均值和方差信息就泄露到了训练过程。另一种情况是特征里包含了“未来信息”比如用“当前逾期天数”预测“是否会违约”但“当前逾期天数”本身就是违约后的结果。解决严格按“先划分、再预处理”的顺序操作。训练集fit_transform验证集和测试集只transform。特征筛选时逐列检查确认每个特征在预测时间点都是已知的。5.2 样本不平衡处理过度SMOTE 把噪声也放大了现象用了 SMOTE 过采样后训练集 AUC 很高但验证集 AUC 反而下降。原因SMOTE 通过在正样本之间插值生成新样本但如果正样本本身噪声很大插值生成的样本会把噪声放大。而且 SMOTE 只能在训练集上做如果在划分前做合成样本会同时进入训练集和验证集造成泄露。解决先试带权重的损失函数如果效果不够再试 SMOTE。用 SMOTE 时只在训练集上做且k_neighbors不要设太大默认 5 就行。另一种更稳的做法是欠采样多数类虽然损失了一些信息但不会引入合成噪声。5.3 阈值设成 0.5业务目标和技术指标脱节现象模型 AUC 0.82但业务方说“不好用”。原因0.5 是默认阈值但业务上可能更关心“在召回率 80% 的前提下精确率能到多少”。如果业务目标是抓出尽可能多的违约客户0.5 的阈值可能漏掉大量高风险客户。解决和业务方确认目标——是“宁可错杀”还是“宁可放过”。然后画阈值-召回率-精确率曲线让业务方选一个可接受的平衡点。阈值确定后用验证集确认在该阈值下的业务指标比如坏账率下降多少。5.4 特征重要性不稳定换一批数据排名就变了现象这次训练“历史逾期次数”是最重要特征下次训练“负债比”排到了第一。原因特征之间高度相关时模型对重要性的分配会不稳定。比如“月收入”和“年收入”高度相关模型可能这次偏向月收入下次偏向年收入。另外样本量太小也会导致特征重要性波动。解决做特征聚类把高度相关的特征合并或只保留一个。用 SHAP 的全局解释而不是单次训练的特征重要性SHAP 对相关特征的处理更稳定。如果样本量小于 5000特征重要性的解释要谨慎。5.5 模型更新频率拍脑袋三个月一更新可能太慢现象模型上线半年后AUC 从 0.82 降到 0.71。原因信贷市场环境变化快经济周期、政策调整、客群迁移都会让模型失效。如果更新频率太低模型会逐渐偏离当前数据分布。解决建立监控指标每月计算一次上线模型的 AUC 和 KS。如果 KS 下降超过 20%触发重新训练。重新训练时用最近 12 个月的数据但保留一部分历史数据做时间外验证。更新频率根据监控结果动态调整不要固定“三个月一更新”。6. 从跑通到跑好三个让违约预测模型真正可用的进阶技巧6.1 用多模型融合替代单模型调参单模型调参的收益是有上限的。与其花一周时间把 MLP 的 AUC 从 0.80 调到 0.81不如用同样的时间搭一个融合模型。我常用的组合是MLP XGBoost Logistic Regression三个模型的预测概率做加权平均。权重可以用验证集上的 AUC 来定也可以直接用等权重。from sklearn.linear_model import LogisticRegression import xgboost as xgb # 训练XGBoost xgb_model xgb.XGBClassifier( n_estimators200, max_depth5, learning_rate0.05, scale_pos_weightneg_count/pos_count, eval_metricauc) xgb_model.fit(X_train, y_train) # 训练逻辑回归 lr_model LogisticRegression(class_weightbalanced, max_iter1000) lr_model.fit(X_train, y_train) # 获取三个模型的预测概率 mlp_probs test_probs # 前面MLP的输出 xgb_probs xgb_model.predict_proba(X_test)[:, 1] lr_probs lr_model.predict_proba(X_test)[:, 1] # 加权融合 ensemble_probs 0.5 * mlp_probs 0.3 * xgb_probs 0.2 * lr_probs ensemble_auc roc_auc_score(y_test, ensemble_probs) print(fEnsemble AUC: {ensemble_auc:.4f})融合的关键是模型之间要有差异性。MLP 擅长捕捉非线性关系XGBoost 擅长处理特征交互逻辑回归提供稳定的线性基准。如果三个模型都是 MLP融合收益很小。权重分配上我一般给表现最好的模型 0.5另外两个各 0.25但具体比例要在验证集上试。6.2 用概率校准让输出概率更可靠深度学习模型输出的概率往往不是真实概率。比如模型说“这个人违约概率 0.3”但实际统计下来这个群体违约率只有 0.15。概率校准就是修正这种偏差常用方法是 Platt Scaling 和 Isotonic Regression。from sklearn.calibration import CalibratedClassifierCV # 用验证集做概率校准 calibrated_model CalibratedClassifierCV( model, methodisotonic, cvprefit) calibrated_model.fit(X_val, y_val) # 校准后的概率 calibrated_probs calibrated_model.predict_proba(X_test)[:, 1]methodisotonic适合样本量大的情况methodsigmoidPlatt Scaling适合样本量小的情况。cvprefit表示用已经训练好的模型不再重新训练。校准后的概率可以直接用于业务决策比如“违约概率大于 0.2 的拒绝放贷”这个 0.2 才是真实可信的。6.3 建立监控闭环模型上线只是开始模型上线后我会建三个监控指标特征分布漂移、预测分布漂移、实际表现衰减。特征分布漂移用 PSIPopulation Stability Index衡量PSI 大于 0.2 说明特征分布发生了显著变化。预测分布漂移看模型输出的概率分布是否偏移。实际表现衰减看 AUC 和 KS 的月度变化。def calculate_psi(expected, actual, buckets10): 计算PSI衡量两个分布的差异 breakpoints np.percentile(expected, np.linspace(0, 100, buckets 1)) expected_counts np.histogram(expected, binsbreakpoints)[0] / len(expected) actual_counts np.histogram(actual, binsbreakpoints)[0] / len(actual) # 避免除零 expected_counts np.where(expected_counts 0, 0.0001, expected_counts) actual_counts np.where(actual_counts 0, 0.0001, actual_counts) psi np.sum((actual_counts - expected_counts) * np.log(actual_counts / expected_counts)) return psi # 对每个特征计算PSI for col in num_features: psi calculate_psi(train_data[col], recent_data[col]) if psi 0.2: print(fWarning: {col} PSI{psi:.4f}, distribution shifted)PSI 小于 0.1 说明分布稳定0.1 到 0.2 之间需要关注大于 0.2 就要触发调查。如果多个特征同时出现高 PSI说明客群发生了系统性迁移模型需要重新训练。这套监控闭环建起来之后模型维护就从“拍脑袋更新”变成了“数据驱动更新”。我做了这么多年风控模型最大的教训就是模型效果好不好七分靠数据两分靠特征一分靠调参。很多人一上来就研究网络结构、调学习率但真正决定成败的是数据质量、特征设计和业务理解。把这份源码跑通只是起点后面每一步都需要结合具体业务场景去打磨。希望帮到你。本文还有配套的精品资源点击获取