简介:基于Python的贷款违约预测项目面向金融风控零基础人群和高校课程设计场景,围绕信贷业务中的借款人违约二分类问题,完整覆盖数据清洗、探索性数据分析、特征工程、模型训练与评估全流程。项目集成LightGBM、XGBoost、CatBoost以及多模型融合的集成学习思路,可以帮助读者快速建立金融风控基线方案。压缩包共十个文件,包含六个Python脚本,分别负责特征生成、单一模型训练、融合预测与特征重要性输出;另有一个Jupyter Notebook用于EDA可视化、一份Word格式的项目说明手册、一张特征重要性图以及配置文件,整体仅380KB,轻量易用。项目已获导师指导并达到九十七分高分,代码保证一键可运行,下载后无需任何修改即可作为课程设计或期末大作业直接提交。目前共有一百三十一人学习,适合希望系统掌握借贷违约预测流程、理解模型对比与集成方法的入门学习者。
1. 零基础入门金融风控:贷款违约预测到底在解决什么问题
拿到“零基础入门金融风控-基于python的贷款违约预测源码+项目说明.zip”这个资源,很多初学者第一反应是“这不就是个二分类任务吗,训练一个模型预测用户会不会还钱”。但真在信贷场景里做过一遍的人会告诉你,事情远没有那么简单——脏数据、极端不平衡的标签、特征穿越、阈值怎么定,每一步都能让模型从“准确率99%”的幻觉跌回现实。这篇文章就围绕这个项目,把从原始数据到可用模型的完整链路拆开讲清楚:你要做什么、每步代码为什么这么写、参数怎么调、坑在哪里。
这个方向适合两类人:一是刚入门机器学习、想找一个不那么玩具级的实战项目练手的学生;二是已经在做数据分析、想往金融风控或信贷模型方向转的从业者。不管哪类,目标都一样——不是跑通一个demo,而是理解一套能应对真实信贷数据的建模流程,包括特征工程、不平衡处理、模型评估和上线前的验证。下面所有内容默认你手上这份源码包里有完整的数据和代码,我们直接按一个标准建模项目的顺序来拆。
2. 数据准备与特征工程:贷款违约预测的胜负手
2.1 先看数据结构:拿到表格后第一件事不是建模
任何信贷风控项目,第一步永远是读数据、看字段、理解每一列的业务含义。常见做法是用 pandas 先做一个快速体检,而不是急着调库训练。某开放信贷数据集(模拟数据)通常包含申请人的年龄、收入、负债、逾期历史、借款用途等字段,目标变量是是否违约。拿到手的第一步代码长这样:
import pandas as pd import numpy as np # 读取训练数据 df = pd.read_csv('loan_data.csv') print(df.shape) # 样本量和特征数 print(df.head()) # 前五行 print(df.info()) # 字段类型和缺失情况 print(df.describe()) # 数值字段分布 print(df['is_default'].value_counts()) # 标签分布这里df.shape让你知道有多少笔贷款记录、多少个特征字段;df.info()直接暴露缺失值和数据类型问题;value_counts()看标签分布,这一步就会看到一个大问题——违约样本通常只占 5% 到 20%,这个不平衡比例决定了后面所有策略。建议把字段按业务含义归个类:身份信息类、收入负债类、历史行为类、借款属性类,后面做特征工程时按类别处理,思路会清楚很多。
2.2 缺失值处理:不是所有空值都该填同一个数
信贷数据缺失是常态,但处理方式要看字段类型和业务含义。收入字段缺失可能代表申请人没有稳定收入,而家庭住址字段缺失可能只是录入遗漏,这两者隐含的风险完全不同。常见做法是把缺失单独做成一个标志位,而不是简单填充。
# 按缺失率筛选字段 missing_ratio = df.isnull().mean().sort_values(ascending=False) print(missing_ratio[missing_ratio > 0.3]) # 缺失超过30%的字段 # 对关键字段做分类型处理 df['income_missing'] = df['annual_income'].isnull().astype(int) df['annual_income'] = df['annual_income'].fillna(df['annual_income'].median())这里income_missing这个新字段很关键——它把“收入是否缺失”这件事本身变成了特征。逻辑是:收入缺失的人,风险特征可能和收入极低的人相似,模型需要有机会学到这个模式。缺失率超过 30% 的字段一般建议直接删除,因为填充带来的噪声远大于信号。分类型字段用众数填充,连续型字段用中位数填充,这是默认做法,后续可以再用模型填补做对比。
2.3 特征衍生:从原生字段里挖出业务含义
信贷风控里,单纯用原始字段建模效果通常一般,真正有用的是衍生特征。比如单一收入数字意义有限,但“收入 / 借款金额”这个比率直接反映还款压力;逾期次数除以总贷款笔数,反映的是逾期频率而非简单次数。这些衍生特征才是风控模型里真正有区分度的部分。
# 还款能力类衍生特征 df['debt_to_income'] = df['total_debt'] / (df['annual_income'] + 1e-6) df['loan_to_income'] = df['loan_amount'] / (df['annual_income'] + 1e-6) # 历史行为类衍生特征 df['overdue_ratio'] = df['overdue_count'] / (df['total_loans'] + 1e-6) df['credit_utilization'] = df['total_debt'] / (df['credit_limit'] + 1e-6) # 对极端值做截断处理 for col in ['debt_to_income', 'loan_to_income', 'overdue_ratio']: upper = df[col].quantile(0.99) df[col] = df[col].clip(upper=upper)代码里到处是+1e-6,这是防止除零的小技巧,属于写风控代码的基本功。加1e-6的代价是当分母真为 0 时,这个比率会变成一个很大的值,但这恰好表示“没有收入却借了钱”,本身就是强风险信号。对比率类特征做 99 分位数截断,是为了防止个别极端样本把模型权重带偏——这在信贷数据里很常见,曾有测试样本收入填的是几百万,不截断的话整个模型都得看它脸色。
3. 模型选型与训练:从逻辑回归到梯度提升树
3.1 为什么风控项目默认先跑逻辑回归
金融风控领域有个反直觉的现象:深度学习模型效果不一定比逻辑回归好,但逻辑回归一定是最先被接受的。原因在于信贷模型有强监管要求,模型可解释性比精度更优先——你得能说清楚为什么拒绝某个申请人,而不是甩一句“神经网络说的”。所以入门这个项目,逻辑回归是必须跑通的第一个模型,它同时是后续复杂模型的基准线。
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 特征列 feature_cols = [col for col in df.columns if col not in ['is_default']] X = df[feature_cols].values y = df['is_default'].values # 切分训练集和测试集,注意 stratify X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 标准化后用逻辑回归训练 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) model_lr = LogisticRegression(C=1.0, max_iter=1000, class_weight='balanced') model_lr.fit(X_train_scaled, y_train) print('LR train acc:', model_lr.score(X_train_scaled, y_train)) print('LR test acc:', model_lr.score(X_test_scaled, y_test))stratify=y保证切分前后正负样本比例一致,这个细节在新手代码里经常被漏掉,但漏掉的后果是模型评估结果完全失真。class_weight='balanced'是处理不平衡的第一步——它在损失函数里给少数类(违约样本)更高的惩罚权重,让模型不敢轻易把所有样本都预测成“不违约”。做完这一步你会发现测试集准确率可能从 95% 掉到 80% 左右,别慌,这恰恰说明模型开始真正学习违约模式了,而不是躺平。
3.2 梯度提升树:为什么它是风控领域的事实标准
逻辑回归做完了基准,下一步必上梯度提升树。XGBoost 或 LightGBM 在结构化信贷数据上几乎总是优于逻辑回归,因为它们能自动捕捉特征之间的非线性交互——比如“收入低但历史还款好”和“收入低且历史逾期多”这两类用户的风险完全不同,逻辑回归只能线性逼近这种差异,树模型可以直接切分出来。LightGBM 在训练速度上比 XGBoost 快得多,对零基础入门来说更友好,这个项目源码里大概率也是用它做最终模型。
import lightgbm as lgb model_lgb = lgb.LGBMClassifier( n_estimators=300, # 树的数量 learning_rate=0.05, # 学习率 max_depth=5, # 单棵树深度 num_leaves=31, # 叶子节点数 subsample=0.8, # 样本采样比例 colsample_bytree=0.8, # 特征采样比例 random_state=42, class_weight='balanced' ) model_lgb.fit( X_train, y_train, eval_set=[(X_test, y_test)], eval_metric='auc', callbacks=[lgb.early_stopping(50), lgb.log_evaluation(50)] ) print('LGB test auc:', model_lgb.score(X_test, y_test))这里eval_metric='auc'而不是'accuracy',是风控建模必须习惯的思维转变——准确率在不平衡数据上没有意义,预测 100% 不违约就能有 95% 准确率,但业务上毫无用处。AUC 衡量的是模型把违约样本排在不违约样本前面的概率,不依赖阈值,是风控模型的主评估指标。early_stopping(50)是防止过拟合的关键机制:测试集 AUC 连续 50 轮不提升就停止训练,这样树的数量不是拍脑袋定的,而是数据自己说了算。
3.3 特征重要性:模型训练完先看它靠什么做决策
模型训完之后,第一件事不是调参,而是看特征重要性。这一步有两个目的:一是验证特征工程做的是否有效,如果辛苦构造的衍生特征排在最前面,说明方向对了;二是为后续特征筛选提供依据——信贷模型特征数量不是越多越好,冗余特征反而会降低模型稳定性和可解释性。
# 提取特征重要性并排序 importance = pd.DataFrame({ 'feature': feature_cols, 'importance': model_lgb.feature_importances_ }).sort_values('importance', ascending=False) print(importance.head(15)) # 筛选重要性较高的特征,重新训练轻量模型 top_features = importance.head(20)['feature'].tolist() print('选中的特征:', top_features)LightGBM 的feature_importances_默认用的是分裂次数(split count),对连续型特征有点吃亏,更精确的做法是用gain类型看特征带来的信息增益。但入门阶段分裂次数够用了,看个相对排序没问题。如果发现某个人口统计学字段(比如年龄)重要性远超所有衍生特征,通常是特征工程没做好,回去重新审视衍生特征的构造逻辑,而不是直接接受这个结果。
4. 样本不平衡与阈值策略:贷款违约预测的核心难点
4.1 不平衡问题不是想当然就能处理的
信贷场景违约率通常在 5% 到 15% 之间,意味着一个模型只要预测全部“正常还款”,就能拿到 90% 以上的准确率。这就是不平衡问题的本质:准确率指标完全失真,模型优化方向被多数类绑架。解决思路分两条路:数据层面重采样,或算法层面调整权重。前面已经用了class_weight='balanced',这属于算法层面;数据层面最常用的是 SMOTE 过采样。两条路可以叠加,但要小心过采样后再配合复杂模型容易导致过拟合。
from imblearn.over_sampling import SMOTE from collections import Counter # 只看训练集重采样 smote = SMOTE(random_state=42, k_neighbors=5) X_train_res, y_train_res = smote.fit_resample(X_train_scaled, y_train) print('重采样前:', Counter(y_train)) print('重采样后:', Counter(y_train_res)) # 用重采样后的数据重新训练逻辑回归 model_lr_smote = LogisticRegression(C=1.0, max_iter=1000) model_lr_smote.fit(X_train_res, y_train_res)SMOTE 的原理是在少数类样本的 k 近邻之间做线性插值,生成新的合成样本,而不是简单复制。k_neighbors=5是默认值,如果样本量太少需要调低。但注意:SMOTE 只能在训练集上做,绝不能碰测试集——测试集必须保留真实分布,否则评估结果是假的。这是新手最容易犯的错误,一旦测试集也被重采样了,模型上线后的真实表现会远低于你的预期。那条“训练集 0.95 AUC、测试集 0.89 AUC”的差距里,有一部分就是这么来的。
4.2 模型输出的是概率,不是判决
风控建模里最容易被忽视的一个事实是:模型最终输出的不是“违约/不违约”标签,而是一个违约概率(0 到 1 之间的分数)。要不要拒绝一个申请人,取决于这个概率是否超过某个阈值。这个阈值不能默认取 0.5——在违约率只有 5% 的数据集上,0.5 的阈值意味着模型几乎会把所有人都判为“不违约”,因为它的训练数据里本来就没有那么多人违约概率超过 0.5。
from sklearn.metrics import precision_recall_curve # 获取测试集违约概率 y_prob = model_lgb.predict_proba(X_test)[:, 1] # 计算不同阈值下的精确率和召回率 precision, recall, thresholds = precision_recall_curve(y_test, y_prob) # 找出 F1 最高的阈值,或者按业务要求找召回率达标的最低阈值 f1_scores = 2 * precision * recall / (precision + recall + 1e-6) best_idx = f1_scores.argmax() best_threshold = thresholds[best_idx] print('F1最优阈值:', best_threshold) print('对应精确率:', precision[best_idx]) print('对应召回率:', recall[best_idx])precision_recall_curve把每个可能阈值对应的精确率和召回率全算出来,然后你再做决策。F1 最优是一个纯数据层面的选择;但真实业务场景里,阈值选择往往要考虑成本——拒掉一个好人损失的是这笔贷款的利息收入,放掉一个坏人损失的是这笔贷款的本金。通常坏账本金远大于利息收入,所以阈值会偏向提高召回率、牺牲精确率。这个权衡是风控建模的灵魂工作。
4.3 用 KS 和 AUC 评估区分度,不只看排名变化
除了 AUC,信贷风控里另一个核心指标是 KS(Kolmogorov-Smirnov),它衡量的是违约和正常两组样本在预测分数分布上的最大差距。AUC 看的是整体排序能力,KS 看的是最大区分点在哪。两个指标结合才能判断模型质量。在建模早期,AUC 从 0.7 涨到 0.75 是非常容易的,但从 0.8 涨到 0.85 就需要在特征工程上狠下功夫。
from sklearn.metrics import roc_auc_score, roc_curve # AUC 计算 auc_score = roc_auc_score(y_test, y_prob) print('测试集 AUC:', auc_score) # KS 计算 fpr, tpr, _ = roc_curve(y_test, y_prob) ks = max(tpr - fpr) print('测试集 KS:', ks)这里tpr - fpr的最大值就是 KS 值,通常认为 KS 大于 0.3 模型可上线,大于 0.5 就算非常优秀。但注意:KS 高不代表业务效果好,因为它同样不包含成本和收益的信息。所以完整的评估框架应该是:AUC 看整体排序能力、KS 看最大区分度、分类报告看特定阈值下的精确率召回率、业务测算看成本和收益。缺哪块补哪块,不要只盯着一个数字自嗨。
5. 避坑指南:零基础做贷款违约预测最容易翻车的 5 个细节
现象一:测试集准确率很高但 AUC 只有 0.6 左右,模型完全没用。
原因是目标标签在测试集和训练集分布不一致,或者特征里混入了未来信息。解决方法是先检查train_test_split是否用了stratify=y,再检查特征列里是否包含“放款后才知道的信息”,比如放款后额度使用率——这类特征在训练时有效,上线后根本拿不到。信贷建模有一条铁律:特征必须是在决策时点能拿到的信息,否则就是在用未来预测过去。
现象二:训练集 AUC 0.95、测试集 AUC 0.75,过拟合严重。
原因是特征数量太多、模型太复杂,而样本量不够。解决方法有梯度:先看特征重要性,砍掉排名靠后的特征;然后用早停机制重新训练,减少树的数量;最后把max_depth调小、num_leaves调小。LightGBM 里对这类问题最有效的是调大min_child_samples(默认 20 调到 50 甚至 100),强制每个叶子节点至少有足够多的样本,可以有效抑制模型死记硬背单个样本。
现象三:代码报错ValueError: Input contains NaN,但数据已经填充过了。
原因是特征工程过程中新构造的特征里又产生了 NaN。常见位置:除法分母为 0(比如credit_limit为 0 时相除)、对数运算对 0 或负数报错、类别编码后某些类别只出现在测试集。解决方法是训练前跑一遍assert np.isfinite(X_train).all(),把问题卡在源头,不要等模型报错了才排查。我一般习惯在每做完一步特征工程就检查一次 NaN 数量,而不是最后统一处理——排查成本会小一个量级。
现象四:逻辑回归训练时警告ConvergenceWarning或者损失不下降。
原因是数据没做标准化,或者学习率/迭代次数不匹配。逻辑回归对特征尺度敏感,收入几万和年龄几十在同一个模型里,权重收敛极慢。解决方法是前面提到的StandardScaler先标准化,然后把max_iter从默认的 100 调到 1000。如果还不行,把C值调小(正则化增强),让模型更新步长更稳定。
现象五:SMOTE 之后训练集指标暴涨,但测试集崩了。
原因是只在训练集做 SMOTE、测试集没动,模型学的分布和真实分布产生了偏移,这在零基础项目里几乎必犯。解决方法是建立一条纪律线:所有数据预处理逻辑(填充、标准化、重采样)都必须先fit训练集,再用训练集的参数transform测试集。这个“先 fit 再 transform”的习惯是专业和业余的分水岭,务必从第一个项目就开始养成。
现象六:同一份数据,换了个random_state结果差异巨大,模型不稳定。原因是数据集本身样本量小,切分方式对结果影响大。解决方法是做交叉验证而不是单次切分,或者把random_state固定为多个值跑多次取平均。这类问题在信贷数据里尤其常见——因为违约样本本来就少,切分时稍微抖一下,测试集里的违约样本数量差几个,KS 就差 0.05。
6. 从模型到可解释:用 SHAP 让贷款违约预测模型不再是黑匣子
模型训练完、评估做完,工作其实只完成了一半。真正的风控项目里,模型需要向业务部门、风控审批团队,甚至监管机构解释“为什么这个客户被拒绝了”。这时候就需要 SHAP(SHapley Additive exPlanations),它能把黑匣子模型拆成每个特征的贡献值,告诉你这个客户之所以被判为高风险,主要是收入低贡献了 0.3,逾期次数贡献了 0.2,负债比贡献了 0.15。
import shap # 用训练好的 LightGBM 模型计算 SHAP 值 explainer = shap.TreeExplainer(model_lgb) shap_values = explainer.shap_values(X_test) # 看整体特征重要性 shap.summary_plot(shap_values, X_test, feature_names=feature_cols) # 看单个样本的解释 shap.force_plot(explainer.expected_value, shap_values[0], X_test[0], feature_names=feature_cols)summary_plot是所有样本的 SHAP 值分布图——横轴是 SHAP 值(对模型输出的影响方向和大小),纵轴是特征名,按重要性排序。你能一眼看出哪些特征把违约概率往高推、哪些往低拉。force_plot是单个样本的解释,蓝色代表把概率往下拉的因素,红色代表往上推的因素,这个图能直接打印出来给审批人员看。
真实场景里我会额外写一个“拒绝原因报告”函数:取被判为违约的样本,按 SHAP 值从大到小排序取前 5 个特征,输出成“该客户违约概率 78%,主要风险因素:收入负债比过高(贡献+0.31)、历史逾期次数 5 次(+0.22)、近 6 个月查询次数 12 次(+0.18)”。这个动作不仅让业务信服,更是模型上线时绕不开的可解释性要求。
从零基础入门到能交付一个完整的贷款违约预测项目,路径很清晰:理解数据 → 特征工程 → 基准模型 → 复杂模型 → 处理不平衡 → 选阈值 → 评估 → 解释。每一步都有对应的代码、指标和常见的坑。刚上手时你会觉得踩坑是最耗时的,但回头看那些坑恰恰是你对“金融风控”这四个字理解最深的地方。做信贷建模,敬畏数据、敬畏业务、敬畏不确定性,这是我最想告诉你的经验。希望帮到你。
本文还有配套的精品资源,点击获取