恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于深度学习的个人贷款违约预测系统:Python源码实现与避坑指南
首页
资讯中心
/
基于深度学习的个人贷款违约预测系统:Python源码实现与避坑指南
基于深度学习的个人贷款违约预测系统:Python源码实现与避坑指南
发布时间:2026/10/3 23:18:04
简介这份资源是面向计算机、人工智能、自动化等专业学生与从业者的深度学习实战项目包以个人贷款违约预测为主题可用于课程设计、大作业或毕业设计参考。项目代码经过调试测试注释详尽并附有运行教程文档适合零基础学习者上手也便于进阶者在此基础上修改扩展。压缩包共28个文件约16.37MB包含csv数据集、py源码、ipynb笔记本、xml配置、xlsx商业取数逻辑表及md运行说明等覆盖数据读取、预处理、可视化分析与特征编码等完整流程其中数据构建脚本与多个练习脚本可帮助理解贷款违约预测的数据处理逻辑。目前已有673人学习下载资源提供了从数据到模型构建的完整方案配合详细注释与教程能有效降低深度学习项目入门门槛适合需要快速搭建预测系统原型的读者参考借鉴。1. 个人贷款违约预测系统从一份 Python 源码到能跑通的模型拿到「基于深度学习的个人贷款违约预测系统设计与实现python源码」这个标题多数人第一反应是去搜一份能直接跑的代码然后卡在环境配置、数据字段对不上、模型跑出来 AUC 只有 0.5 这几步上。这个方向真正要解决的问题很具体给定一批贷款申请人的结构化信息收入、负债、历史还款、贷款金额等预测这个人未来会不会违约输出一个概率值再按业务阈值切成「通过/拒绝/人工复核」。它适合两类人一类是课程设计或毕设需要完整跑通一套流程的学生另一类是信贷风控方向想用深度学习替代逻辑回归基线的一线从业者。下面这套东西我按「数据怎么处理、模型怎么搭、参数怎么调、坑在哪」的顺序讲清楚你照着能复现也能判断这套方案值不值得往生产环境推。2. 违约预测的数据底座字段、清洗与不平衡处理深度学习在结构化信贷数据上能不能打赢 XGBoost八成取决于数据这一层而不是网络结构。个人贷款违约数据集通常是「宽表 标签」的形态一行一个申请人一列一个特征最后一列是是否违约0/1。常见公开数据集的特征维度在 10 到 30 之间样本量从几千到几十万不等。真正动手前先把字段分成四类分类方式直接决定后面怎么编码。2.1 字段分四类编码方式各不同连续数值型年龄、月收入、负债收入比、贷款金额、利率直接标准化即可有序类别学历、职级用序号编码保留大小关系无序类别职业类型、贷款用途、担保方式必须做独热或嵌入还有一类是「看似数值实则类别」的比如地区编码、邮编直接当数值喂进去是典型的翻车点模型会学出「邮编越大越容易违约」这种毫无业务意义的规律。我一般会先跑一遍字段画像把每列的取值个数、缺失率、分布偏度打出来再决定编码。取值个数小于 15 的当类别处理大于 15 且业务上连续的当数值处理介于两者之间的看业务含义。import pandas as pd import numpy as np def profile_columns(df, target_col): 输出每列的类型建议、缺失率、唯一值数 rows [] for col in df.columns: if col target_col: continue nunique df[col].nunique(dropnaTrue) missing df[col].isna().mean() # 取值少且非浮点倾向类别取值多且是数值倾向连续 if nunique 15: suggest category elif pd.api.types.is_numeric_dtype(df[col]): suggest numeric else: suggest high_card_category rows.append({ column: col, dtype: str(df[col].dtype), nunique: nunique, missing_rate: round(missing, 4), suggest: suggest }) return pd.DataFrame(rows).sort_values(missing_rate, ascendingFalse) # 用法profile profile_columns(df, target_coldefault_flag)这段代码的逻辑是先看唯一值数量再看数据类型最后给出编码建议。nunique 15这个阈值不是死的业务上如果某个类别字段有 20 个取值但分布极度集中也可以当类别处理。missing_rate排序是为了让你优先处理缺失严重的列缺失率超过 60% 的字段除非业务上极其关键否则直接删掉比费劲填补更划算。2.2 缺失值填补别用全局均值一刀切信贷数据的缺失往往不是随机的。收入字段缺失的人可能是无固定职业者本身违约率就偏高。用全局均值填补会把这个信号抹平。我的做法是数值型按目标变量的分组中位数填补类别型单独设一个「缺失」类别。这样缺失本身成了一个特征。def fill_missing(df, num_cols, cat_cols, target_col): df df.copy() for col in num_cols: # 按标签分组的中位数填补保留缺失与违约的关联 median_map df.groupby(target_col)[col].median() df[col] df.apply( lambda r: median_map[r[target_col]] if pd.isna(r[col]) else r[col], axis1 ) for col in cat_cols: df[col] df[col].fillna(MISSING).astype(str) return dfgroupby(target_col)[col].median()得到的是「违约人群的收入中位数」和「正常人群的收入中位数」各自填补对应群体。这里有个数据泄漏的隐患如果是在训练集上算中位数再应用到验证集验证集的填补逻辑必须复用训练集的映射不能重新算。生产环境里这一步要固化成一个fill_params.json训练时存下来推理时加载。2.3 类别不平衡过采样、欠采样还是改损失函数违约样本通常只占 5% 到 20%极端情况低于 2%。这时候模型全预测「不违约」也能有 95% 以上的准确率但业务上毫无价值。处理方式有三条路SMOTE 过采样、随机欠采样、以及直接用带类别权重的损失函数。我的经验是样本量大于 5 万时优先用类别权重小于 1 万时用 SMOTE欠采样只在正负样本都极其充足时考虑。from sklearn.utils.class_weight import compute_class_weight import numpy as np def get_class_weight(y): classes np.unique(y) weights compute_class_weight( class_weightbalanced, classesclasses, yy ) return dict(zip(classes, weights)) # 训练时传给模型的 class_weight 参数 # 例如违约样本占 8%则违约类权重约为 6.25正常类约为 0.54compute_class_weight的balanced模式按n_samples / (n_classes * np.bincount(y))计算样本越少的类权重越高。这个权重直接乘在交叉熵损失上让模型对少数类的错误更敏感。注意权重不是越大越好如果违约类权重设到 20 以上模型会变得极度保守把大量正常客户也判成违约业务上同样不可接受。一般控制在 10 以内配合后面的阈值调优一起用。3. 深度学习模型怎么搭从 MLP 到嵌入层的结构化网络结构化数据的深度学习模型核心就两件事类别特征怎么变成低维稠密向量数值特征怎么和这些向量拼接后送进全连接层。别一上来就上 Transformer在几千到几十万样本的表格数据上一个设计合理的 MLP 加嵌入层效果和调参成本都更可控。3.1 嵌入层维度怎么定经验公式与业务约束类别特征的嵌入维度常见经验是min(50, (n_categories 1) // 2)其中n_categories是该字段的取值个数。但这个公式在高基数类别上会给出过大的维度比如一个 1000 个取值的职业编码按公式会得到 50 维参数量爆炸。我一般会加一个上限嵌入维度不超过 16且不超过n_categories的平方根。import torch import torch.nn as nn class LoanDefaultNet(nn.Module): def __init__(self, cat_dims, num_dim, hidden_dims(256, 128, 64), dropout0.3): cat_dims: list of int, 每个类别特征的取值个数 num_dim: int, 数值特征数量 super().__init__() # 为每个类别特征建一个嵌入表 self.embeddings nn.ModuleList([ nn.Embedding(n_cat, min(16, int(n_cat ** 0.5) 1)) for n_cat in cat_dims ]) emb_total sum(min(16, int(n ** 0.5) 1) for n in cat_dims) input_dim emb_total num_dim layers [] prev input_dim for h in hidden_dims: layers [ nn.Linear(prev, h), nn.BatchNorm1d(h), nn.ReLU(), nn.Dropout(dropout) ] prev h layers.append(nn.Linear(prev, 1)) self.mlp nn.Sequential(*layers) def forward(self, x_cat, x_num): # x_cat: (batch, n_cat_features) 长整型 embeds [emb(x_cat[:, i]) for i, emb in enumerate(self.embeddings)] x torch.cat(embeds [x_num], dim1) return self.mlp(x).squeeze(-1)这段网络的定义里nn.Embedding为每个类别字段维护一张查找表输入是类别索引输出是稠密向量。BatchNorm1d放在线性层之后、激活之前能显著加快收敛尤其在数值特征量纲差异大的时候。Dropout设 0.3 是结构化数据的常用值超过 0.5 容易欠拟合。输出层是单个神经元配合BCEWithLogitsLoss使用不要在模型里加 Sigmoid损失函数内部会做。3.2 训练循环早停、学习率与验证指标训练循环里最容易出问题的是验证指标的选择。违约预测是不平衡二分类准确率没有参考价值要看 AUC 和 KS。KS 在风控里比 AUC 更常用它衡量的是好坏样本累计分布的最大差值直接对应业务上能区分出多少违约客户。from sklearn.metrics import roc_auc_score, roc_curve import numpy as np def ks_score(y_true, y_prob): fpr, tpr, _ roc_curve(y_true, y_prob) return max(tpr - fpr) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for x_cat, x_num, y in loader: x_cat, x_num, y x_cat.to(device), x_num.to(device), y.to(device) optimizer.zero_grad() logits model(x_cat, x_num) loss criterion(logits, y) loss.backward() # 梯度裁剪防止嵌入层梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * len(y) return total_loss / len(loader.dataset) # 验证阶段 def evaluate(model, loader, device): model.eval() probs, labels [], [] with torch.no_grad(): for x_cat, x_num, y in loader: x_cat, x_num x_cat.to(device), x_num.to(device) logits model(x_cat, x_num) probs.extend(torch.sigmoid(logits).cpu().numpy()) labels.extend(y.numpy()) auc roc_auc_score(labels, probs) ks ks_score(labels, probs) return auc, ksclip_grad_norm_的max_norm5.0是经验值嵌入层参数量大梯度容易在早期爆炸裁剪后训练稳定很多。早停的耐心值设 5 到 10 个 epoch监控指标用验证集 KSKS 连续 5 轮不提升就停。学习率用 1e-3 配 Adam如果 loss 震荡明显降到 3e-4。batch size 在 256 到 1024 之间太小训练慢太大泛化差。3.3 数值特征标准化树模型不用神经网络必须做神经网络对输入尺度敏感数值特征不做标准化收入这种量级上万的字段会直接主导梯度。标准化要在训练集上算均值和标准差然后应用到验证集和测试集。注意如果用了按标签分组填补缺失标准化要在填补之后做。from sklearn.preprocessing import StandardScaler import joblib scaler StandardScaler() # 只在训练集上 fit X_num_train scaler.fit_transform(X_num_train) X_num_val scaler.transform(X_num_val) X_num_test scaler.transform(X_num_test) # 保存 scaler 供推理使用 joblib.dump(scaler, num_scaler.pkl)fit_transform和transform的区别是血泪教训如果验证集也调fit_transform均值和标准差会包含验证集信息造成数据泄漏验证指标虚高上线后直接翻车。生产推理时加载num_scaler.pkl用transform处理新样本。4. 避坑与排查跑不通、指标假高、上线崩的典型场景这一章列的每一条都是我或身边人真实踩过的按「现象 → 原因 → 解决」写你对照自己的情况排查。4.1 训练 loss 不降AUC 卡在 0.5现象模型训练几十个 epochloss 在 0.69 附近震荡二分类随机水平验证 AUC 约 0.5。原因通常是标签列没对齐或者类别特征索引从 1 开始而嵌入层期望从 0 开始。解决先检查y的取值是不是只有 0 和 1再检查类别编码后的最小值是不是 0。用LabelEncoder重新编码确保索引从 0 连续到n_categories - 1。如果标签是「是/否」字符串先映射成 1/0。4.2 验证集 AUC 0.95测试集掉到 0.6现象验证指标漂亮得不像话换一批数据就崩。原因几乎都是数据泄漏标准化、缺失填补、类别编码在划分训练测试之前就做了或者用了未来信息。解决把数据划分放在所有预处理之前用train_test_split先切分再在训练集上 fit 所有变换器验证集和测试集只 transform。时间序列数据要按时间切不能随机切。4.3 违约样本召回率极低阈值 0.5 一刀切现象模型 AUC 有 0.75但按 0.5 阈值分类违约样本几乎全漏。原因是类别不平衡下模型输出的概率整体偏低0.5 不是最优阈值。解决在验证集上画 KS 曲线或 PR 曲线找 KS 最大或 F1 最优的阈值。风控场景通常把阈值设在违约概率的 70% 到 90% 分位数附近宁可错杀不可放过。阈值要固化到配置文件推理时读取。4.4 嵌入层维度设太大显存爆了现象模型刚初始化就 OOM或者 batch size 只能设到 32。原因是高基数类别特征的嵌入表参数量过大比如 10 万个取值 × 50 维 500 万参数光嵌入层就占几百 MB。解决高基数类别先做频次截断出现次数少于 100 的取值统一归为「其他」把基数降到几千以内。嵌入维度按min(16, sqrt(n_categories))控制别用固定 50。4.5 推理时预处理和训练不一致现象离线指标 0.78上线后监控显示 KS 只有 0.3。原因是推理服务的预处理逻辑和训练脚本各写各的缺失填补的中位数、标准化的均值、类别编码的映射表都对不上。解决把预处理固化成一个Preprocessor类或sklearn的Pipeline训练和推理共用同一份代码参数存成文件。每次模型更新预处理参数一起版本化。5. 从跑通到能用阈值调优、模型解释与部署前检查模型跑出 AUC 只是起点真正决定这套系统能不能用的是阈值怎么定、业务方信不信、以及上线前有没有做一致性校验。这一章讲三个具体技巧都是我实际项目里验证过的。5.1 用 KS 曲线定阈值而不是拍脑袋KS 曲线横轴是阈值纵轴是 KS 值。找到 KS 最大的点对应的阈值就是统计上最优的切分点。但业务上还要考虑通过率如果阈值定得太高违约拦截率上去了但通过率可能从 70% 掉到 40%业务量撑不住。我的做法是画一张双轴图左轴 KS右轴通过率找两者的平衡点。import numpy as np from sklearn.metrics import roc_curve def find_best_threshold(y_true, y_prob, min_pass_rate0.5): 在保证最低通过率的前提下找 KS 最大的阈值 fpr, tpr, thresholds roc_curve(y_true, y_prob) ks tpr - fpr # 通过率 预测为正常的比例 pass_rate 1 - (y_prob[:, None] thresholds[None, :]).mean(axis0) valid pass_rate min_pass_rate if not valid.any(): return thresholds[np.argmax(ks)] ks_valid np.where(valid, ks, -1) best_idx np.argmax(ks_valid) return thresholds[best_idx], ks[best_idx], pass_rate[best_idx]min_pass_rate是业务约束比如要求至少 50% 的申请人能通过。pass_rate的计算用广播比较得到每个阈值下预测为正常的比例。这个函数返回最优阈值、对应 KS 和通过率直接写进配置文件。注意阈值是在验证集上定的测试集只用来最终评估不能反复调。5.2 用 SHAP 做单样本解释让风控方敢用深度学习模型被诟病最多的是黑匣子风控审批人员不敢直接信一个说不清理由的分数。SHAP 能给每个样本输出每个特征的贡献值正贡献推高违约概率负贡献拉低。对表格数据KernelExplainer或DeepExplainer都能用但DeepExplainer对嵌入层模型支持更好。import shap import torch # 取 100 个背景样本 background next(iter(train_loader)) explainer shap.DeepExplainer(model, [background[0], background[1]]) # 解释一个测试样本 test_sample (x_cat_test[:1], x_num_test[:1]) shap_values explainer.shap_values(test_sample) # shap_values 的形状对应每个输入特征可以映射回原始字段名 # 正数表示推高违约概率负数表示拉低DeepExplainer的背景样本数量影响解释稳定性和速度100 到 500 之间比较合适。解释结果可以做成审批界面上的「主要影响因素」列表比如「负债收入比过高0.12、历史逾期次数多0.08、收入稳定-0.05」。这一步做了业务方的接受度会明显不一样。5.3 上线前的三项一致性检查模型部署前我会固定做三件事。第一用同一批测试数据分别走离线脚本和推理服务对比输出的概率值差异超过 1e-4 就要查预处理。第二检查推理服务的输入字段顺序和训练时是否一致字段错位是上线后指标暴跌的常见原因。第三压测推理延迟单样本推理超过 50ms 就要考虑模型蒸馏或量化信贷审批通常要求实时返回。# 一致性检查示例 def check_consistency(offline_probs, online_probs, tol1e-4): diff np.abs(np.array(offline_probs) - np.array(online_probs)) max_diff diff.max() if max_diff tol: bad_idx np.where(diff tol)[0] print(f不一致样本数: {len(bad_idx)}, 最大差异: {max_diff:.6f}) print(f前 5 个不一致索引: {bad_idx[:5]}) return False print(一致性检查通过) return True这套流程走下来从一份 Python 源码到一个能解释、能监控、能上线的违约预测系统中间的差距主要就在数据预处理的一致性、阈值调优和解释性这三块。我自己的习惯是每次模型更新先把预处理参数和阈值一起版本化再跑一遍一致性检查确认无误才推生产。希望帮到你。本文还有配套的精品资源点击获取