简介一份面向机器学习初学者与金融风控从业者的贷款违约预测实践源码基于Python实现完整建模流程。项目包含随机森林、决策树、梯度提升等多种模型训练与对比附带数据分析与预测脚本覆盖数据预处理、特征探索、模型评估等关键环节适合用于信用评分与信贷风险管理场景。压缩包共23个文件由7个Python源码、11张PNG可视化图、2个CSV数据集、1个Excel字典文件及说明文档组成整体约13.77MB目录结构清晰便于按模块复现实验。已有560人学习下载。通过该资源可掌握贷款数据清洗、模型调参与结果解读的整套思路既可作为课程设计参考也能为金融机构的风险评估提供可扩展的建模基线。1. 贷款违约预测的机器学习项目到底在解决什么问题银行和消费金融公司每天都要回答同一个问题这笔钱借出去对方还会不会还传统做法是人审加规则评分但人工审核覆盖不了海量进件规则分数卡又难以捕捉借款人复杂的行为模式。贷款违约预测就是把这个决策过程交给机器学习用历史借贷数据训练一个分类器在放款前给出违约概率。这个任务和一般分类问题的最大区别在于样本极不平衡——好客户通常占九成以上坏客户可能只有百分之二三欠得少的短期逾期和彻底坏账在业务上又是两回事。所以项目里的每一项设计从特征构造、样本切分到阈值选择都要围绕这个不平衡展开。适合阅读这篇文章的读者是刚接触信贷风控的数据分析师、想系统走一遍机器学习应用流程的Python工程师以及需要从零搭建一个可解释风控Demo的团队。标题里提到的源码常见做法就是把特征工程、模型训练、评估和决策输出拆成模块下面按这一套方案逐步展开。2. 贷款违约预测的特征工程分箱、WOE编码与IV筛选2.1 为什么贷款违约预测不直接喂原始特征信贷数据里既有年龄、收入这类连续值也有婚姻状况、职业、住房类型这类类别变量。直接把原始数值丢给机器学习模型并不是不可以但可解释性和稳定性都会打折扣极端值会让逻辑回归系数失真类别出现频率不均时one-hot会产生大量稀疏列。更现实的问题在于很多字段是缺失的直接丢弃会损失信息。业界常见的做法是做分箱加WOE编码。WOEWeight of Evidence衡量的是每个分箱内坏客户分布与好客户分布的差异本质上是把原始特征转换成对数胜算比让特征与目标之间呈现单调的线性关系。这在评分卡场景里有几十年的成熟应用逻辑回归输入WOE值后每一项的系数乘以WOE再求和就能还原成一张加加减减的评分表。IVInformation Value则用来衡量特征整体预测力IV小于0.02通常视为无用变量0.1以上才有保留价值。2.2 分箱代码与参数设置import pandas as pd import numpy as np def equal_freq_bin(series, bins5, targetNone): 等频分箱让每个箱内的样本量尽量接近 try: # qcut按分位数切分duplicatesdrop处理切分点重复 cut, bins_edges pd.qcut(series, qbins, retbinsTrue, duplicatesdrop) except ValueError: # 分位数相同导致切分失败时退化为等宽分箱 cut, bins_edges pd.cut(series, binsbins, retbinsTrue, include_lowestTrue) df pd.DataFrame({feature: series, bin: cut, label: target}) grouped df.groupby(bin, observedFalse)[label].agg([sum, count]) grouped.columns [bad, total] grouped[good] grouped[total] - grouped[bad] # 加平滑项避免除零 grouped[bad_rate] (grouped[bad] 0.5) / (grouped[total] 1) return grouped, bins_edges def calc_woe_iv(grouped): 根据分箱统计计算WOE和IV total_bad grouped[bad].sum() total_good grouped[good].sum() grouped[bad_dist] grouped[bad] / total_bad grouped[good_dist] grouped[good] / total_good # WOE ln(坏客户分布 / 好客户分布) grouped[woe] np.log((grouped[bad_dist] 1e-6) / (grouped[good_dist] 1e-6)) # IV Σ(坏分布 - 好分布) * WOE grouped[iv] (grouped[bad_dist] - grouped[good_dist]) * grouped[woe] return groupedqcut是等频分箱的核心按分位数切分能让每个箱内样本数大致相等避免某个箱只有零星几条导致WOE波动剧烈。duplicatesdrop是必填的因为当特征有大量重复值时分位数会返回相同边界不处理直接报错。groupby(..., observedFalse)是pandas 2.0以上针对类别型分组的新默认行为防止切分后出现空箱告警。落在bad_rate里的0.5平滑项来自贝叶斯估计避免某些箱坏样本为0时算出的WOE无穷大。2.3 特征筛选的数值尺子这里有个常见误区IV越好越要保留。实际上IV过高超过0.5的变量要警惕它往往意味着字段本身包含了目标信息比如“是否被法院执行”这种强制度变量在线上申请时根本拿不到同源数据训练时放进模型会造成严重的样本偏差。我一般会先看IV排序再逐个核对字段的获取时点只有申请时点之前就能拿到的变量才有资格进入模型。IV区间预测力处理建议 0.02几乎没有直接剔除0.02 ~ 0.1较弱结合业务判断保留0.1 ~ 0.3中等优先保留0.3 ~ 0.5较强保留并做相关性检查 0.5异常查数据来源警惕未来函数还有一类变量不在上表里缺失率超过70%的字段无论IV多高都要先跟业务确认缺失原因。如果缺失本身是随机发生的可以考虑单独用“是否缺失”作为一个二值特征交给模型学如果缺失代表着某种拒绝策略这个字段就直接废弃。2.4 类别特征与时间特征的处理类别特征的处理需要单独提出因为贷款场景里的类别和图像分类的类别不同它是“低频但重要”的。婚姻状态、学历这种高基数类别学历有几十种学校类型时要小心不适合直接one-hot。常见做法是把类别合并成“好客户占比”的统计编码然后和WOE做一次线性变换。实操上我会额外构造一个“该类别在训练集出现次数”的字段用来抑制冷门类别的过拟合这在机器学习分类器里是比单纯编码更稳的技巧。时间特征容易被忽略但信贷数据里时间就是风险本身。借款期限、历史平均使用额度的时间窗口长度、距离上次申请的天数这些特征决定了模型能不能识别出“突然缺钱”的信号。连续值特征全部做分箱再WOE编码之后逻辑回归会变得异常稳定这也是为什么很多金融公司仍然没有放弃逻辑回归的原因——特征工程吸收非线性模型只负责线性加权出了坏账还能追查是哪个分箱出了问题。3. 贷款违约预测的模型选型与训练逻辑回归还是XGBoost3.1 分类器选择的两个极端在贷款违约预测这个具体问题里模型选型通常在两极之间摇摆。一端是逻辑回归简单透明系数直接对应风险因子监管审计时能讲清楚“为什么拒绝这个人”另一端是梯度提升树如XGBoost、LightGBM非线性拟合能力强在相同特征下通常比LR高出几个点的AUC。中间地带的随机森林在信贷场景里反而少见因为它的预测是大量树投票的结果解释性弱于LR精度又难和梯度提升拉开差距调试成本却不低。我的建议是两条腿走路先用逻辑回归搭一个基线跑通全流程和特征有效性验证再用XGBoost做精度上限测试。如果XGBoost相对LR的提升不足1%的AUC生产环境就继续用LR毕竟线上稳定性和排查问题的成本都更低。反之如果差距明显就要考虑上树模型并在解释层面对特征做SHAP归因。3.2 训练集与验证集的切分原则贷款违约预测有一个有别于普通机器学习任务的切分原则不能随机打乱切分。信贷模型训练的是“过去预测未来”如果随机抽样训练集里混入未来时间段的数据等于提前泄露了宏观环境变化的信息。正确做法是按时序切分用历史24个月的数据训练最近3个月做验证。更严格一点还需要把验证集再按放款月份分层分别看每个月的KS值是否稳定。from sklearn.model_selection import TimeSeriesSplit from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score import numpy as np import pandas as pd # 假设df已按申请日期排序且特征已经完成WOE编码 X df[feature_columns].values y df[is_default].values # TimeSeriesSplit保证训练集永远在验证集之前 tscv TimeSeriesSplit(n_splits4) auc_scores [] for train_index, val_index in tscv.split(X): X_train, X_val X[train_index], X[val_index] y_train, y_val y[train_index], y[val_index] # class_weightbalanced缓解正负样本不平衡 lr LogisticRegression( C1.0, solverliblinear, class_weightbalanced, max_iter200 ) lr.fit(X_train, y_train) y_pred_proba lr.predict_proba(X_val)[:, 1] auc_scores.append(roc_auc_score(y_val, y_pred_proba)) print(f各折AUC: {np.round(auc_scores, 4)}) print(f平均AUC: {np.mean(auc_scores):.4f})TimeSeriesSplit不是简单地把数据切成几份而是保证第n折的训练集包含前n-1折的所有数据验证集永远在时间上更靠后。class_weightbalanced让逻辑回归内部的损失函数自动放大少数类样本的惩罚对于违约率只有3%的数据集来说这是性价比最高的不平衡处理方案比上采样更不容易过拟合。solverliblinear是二分类小数据集的首选坐标下降法对L2正则的收敛比lbfgs更稳。3.3 XGBoost的超参数搜索与早停XGBoost在这个任务上要想打败逻辑回归关键不在树的数量而在以下几个参数import xgboost as xgb from sklearn.model_selection import GridSearchCV xgb_model xgb.XGBClassifier( objectivebinary:logistic, eval_metricauc, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, n_estimators500, reg_alpha0.1, reg_lambda1.0, random_state42 ) param_grid { max_depth: [3, 4, 5], min_child_weight: [1, 3, 5], learning_rate: [0.01, 0.05, 0.1] } grid GridSearchCV( xgb_model, param_grid, scoringroc_auc, cv3, n_jobs-1, verbose0 ) grid.fit(X_train, y_train) print(f最佳参数: {grid.best_params_})max_depth限制在5以内是刻意为之。信贷特征在分箱WOE编码后是平滑的过深的树容易记住分箱边界上的噪声。min_child_weight在XGBoost里是“叶子节点所需的最小样本权重和”调大它等于强制每个叶子有足够的样本支撑对抑制违约样本稀疏导致的过拟合效果显著。learning_rate配合n_estimators要一起看学习率降到0.01时树的数量通常要上千才能收敛训练时间和收益不成正比所以0.05是一个折中点。实际调参时我还会固定一个评估集开启XGBoost自带的早停机制找到最优迭代轮数后再用这个轮数重新在完整训练集上拟合这比盲调n_estimators更省时间。网格搜索在这个场景里别把范围开太大三组参数、每组三个取值足够覆盖最优区间再多的组合只是把时间浪费在相同精度的区域。3.4 样本不平衡是否要做重采样不少教程会把SMOTE、随机过采样当作贷款违约预测的标配但信贷场景里要谨慎。违约样本的生成机制和普通分类不同——违约是“一段时间后的结果”不是“当下的属性”。对少数类做过采样相当于人为重复了某些借款人的记录模型会把这些重复样本的模式当作普遍规律一旦这批人在时间上是聚类的过采样就会放大周期性风险。提示在金融风控里优先考虑调整class_weight和决策阈值重采样只在AUC提升明显且经过时间外样本验证后才引入。实践中的经验是SMOTE用在AUC提升上通常不超过0.005而它带来的分布偏移风险却是实实在在的。如果坚持要用务必把过采样放在TimeSeriesSplit的每一折内部去执行让训练集和验证集保持真实的时序隔离。4. 贷款违约预测的结果评估与阈值选择不只是看准确率4.1 为什么准确率在这里没有参考价值假设违约率是3%一个把所有借款人都预测为“好客户”的模型准确率高达97%。但这个模型没有任何风控价值。贷款违约预测关注的是能否把有限的坏账损失降低评估指标必须聚焦在排序能力和区分度上。AUC是常用的排序指标它衡量“随机抽一个好客户和一个坏客户模型把坏客户排在前面”的概率不受阈值影响适合初选模型。但AUC也有盲区它在整体排序上打分如果模型在头部1%的区间段区分度差AUC可能依然好看。所以要叠加KS值。KS是风控领域最常用的指标计算方式是把样本按预测分值从低到高排列后分成十等份在每一档上计算累计好客户占比和累计坏客户占比的差取最大差值。KS在0.2以上模型可用0.4以上属于优秀。它比AUC更直观的地方在于KS直接告诉你模型在哪个分数区间区分能力最强。4.2 阈值选择的代价敏感分析模型输出的是违约概率但放款决策只有“通过”和“拒绝”两种。阈值的选定不是拍脑袋定一个0.5而是要结合业务成本。拒掉一个好客户损失的是这笔贷款的利息收入放过一个坏客户损失的是本金加利息。两边成本不一样阈值就应该往成本高的一边偏移。# 代价矩阵假设好客户收益 贷款金额 * 利率差 # 坏客户损失 贷款金额 * (1 - 回收率) loan_amount 10000 # 单笔贷款金额 interest_margin 0.12 # 年化净息差 loss_given_default 0.6 # 违约损失率即坏账收回后净损失比例 # 预测概率和真实标签 # y_val为验证集真实标签1违约y_pred_proba为模型输出概率 results pd.DataFrame({ true_label: y_val, pred_proba: y_pred_proba }) # 在0.1到0.5之间扫描阈值 thresholds np.arange(0.10, 0.51, 0.01) profit_records [] for thr in thresholds: results[pred_label] (results[pred_proba] thr).astype(int) # 放行且未违约赚利息 good_approved (results[pred_label] 0) (results[true_label] 0) # 放行且违约损失本金 bad_approved (results[pred_label] 0) (results[true_label] 1) # 拒绝且违约本来会损失现在规避 bad_rejected (results[pred_label] 1) (results[true_label] 1) total_profit ( good_approved.sum() * loan_amount * interest_margin - bad_approved.sum() * loan_amount * loss_given_default ) profit_records.append({threshold: thr, total_profit: total_profit}) profit_df pd.DataFrame(profit_records) best_idx profit_df[total_profit].idxmax() print(profit_df.loc[best_idx])这段代码的核心逻辑是把“通过”和“拒绝”拆成四种业务结果分列计算然后把阈值当作一个业务参数去优化而不是统计参数。loss_given_default在真实业务里不是一个固定值不同担保方式、不同抵押物会有很大差异要按产品线分开测算。我在实际项目中会把利润率甚至资金占用成本都折算进代价矩阵做完这步之后模型的输出就不再是一个抽象概率而是一张“放款边界评分卡”。4.3 机器学习模型的稳定性和可解释性验证阈值选定之后模型上线前还要过稳定性验证这一关。常用的是PSIPopulation Stability Index它衡量模型打分在训练样本和上线后新样本之间的分布偏移程度。PSI在0.1以内表示稳定0.1到0.25需要注意超过0.25就说明客群发生结构性变化模型需要重新训练。逻辑回归的场景下还可直接计算各个特征的PSI定位具体是哪个变量发生了偏移这比只看整体分数要有用得多。可解释性验证则要看特征系数方向是否符合业务直觉。比如“收入”的WOE系数必须为正“历史逾期次数”的系数必须为负如果方向反了大概率是特征编码出了问题而不是模型发现了一个新规律。这个过程虽然不产出一个量化的指标但在模型评审时是最常被追问的部分。5. 用Python组织贷款违约预测源码从脚本到可复现工程5.1 源码目录怎么拆分最合理很多第一次写这类项目的开发者会把所有逻辑堆在一个Jupyter Notebook里训练一次跑一遍全部流程。Notebook适合探索不适合落地因为特征工程、模型训练、参数调优、阈值选择混在一起任何一步重跑都会产生不同的中间状态。我会把源码按职责拆成四个模块数据读取与清洗、特征工程、模型训练与评估、决策与输出。loan_default_prediction/ ├── config/ │ └── config.yaml # 参数配置与阈值设置 ├── data/ │ ├── raw/ # 原始数据不落库 │ └── processed/ # 中间特征 ├── src/ │ ├── data_loader.py # 数据读取与缺失值处理 │ ├── feature_engineering.py # 分箱、WOE、IV计算 │ ├── model_training.py # LR与XGBoost训练入口 │ ├── model_evaluation.py # AUC、KS、PSI计算 │ └── decision.py # 阈值优化与放款决策 ├── models/ # 训练产物按日期命名 └── main.py # 流水线入口配置和代码分离是这套结构里最关键的设计。阈值、代价矩阵、分箱数、模型超参全部写进config.yaml改参数不需要动代码重跑时也能通过配置文件追溯当时的实验条件。数据文件不放进源码仓库原始数据按日期归档在独立存储里processed中间表只保留最新的避免磁盘膨胀。5.2 可复现训练的三件套随机种子、实验记录、模型版本贷款违约预测的模型需要定期重新训练如果每次跑出来的结果对不上后续监控和对比都无从谈起。固定随机种子是最基本的要求但只固定Python内置的random.seed(42)是不够的NumPy和XGBoost各自维护独立的随机数生成器需要分别设置。import random import numpy as np import xgboost as xgb def set_seed(seed42): 统一固定各库随机种子 random.seed(seed) np.random.seed(seed) # XGBoost在sklearn接口里通过random_state传入 xgb.set_config(verbosity0, use_rmmTrue)注意xgb.set_config里的use_rmm只在启用RAPIDS内存管理时生效浅层函数不设置也不影响真正要固定的是传入XGBClassifier(random_stateseed)的参数。实验记录可以简化为一个CSV每跑一次训练就往里追加一行日期、数据版本、特征列表、模型类型、超参JSON、AUC、KS、阈值、PSI。不要小看这个表模型上线三个月后出了问题唯一能快速定位问题范围的就是这份记录。模型文件本身的命名规则建议带时间戳和指标后缀例如lr_20250115_auc0831.pkl避免用“final”这种覆盖式命名。5.3 完整流水线的一次性运行# main.py 简化版 import yaml import joblib import pandas as pd from src.data_loader import load_data from src.feature_engineering import build_features from src.model_training import train_lr, train_xgb from src.model_evaluation import evaluate_model from src.decision import optimize_threshold def main(): with open(config/config.yaml, r) as f: config yaml.safe_load(f) df load_data(config[data_path]) features build_features(df, config[feature_params]) X_train, X_val, y_train, y_val features[train_valid_split] which_model config[model][name] if which_model lr: model train_lr(X_train, y_train, config[model][params]) elif which_model xgb: model train_xgb(X_train, y_train, config[model][params]) metrics evaluate_model(model, X_val, y_val) threshold optimize_threshold(model, X_val, y_val, config[cost_matrix]) joblib.dump(model, fmodels/{which_model}_{config[data_version]}.pkl) pd.Series(metrics).to_csv(models/metrics.csv, modea, headerFalse) if __name__ __main__: set_seed() main()这个入口函数的职责只是串流程不包含任何业务逻辑。load_data返回的DataFrame在进入特征工程前必须保证字段齐全且类型已被校验一个常见的坑是日期列被读成字符串导致分箱前的排序错乱所以在data_loader.py里要先做pd.to_datetime()类型转换。5.4 关于源码开源的三个补全建议如果在公开渠道发布这套源码有三处细节需要额外打磨。第一是脱敏特征字段名不能直接暴露真实业务字段常见做法是改成f1, f2, f3并附一个字段说明映射表让使用者能理解特征含义但不触碰敏感信息。第二是替换数据真实借款数据绝对不能随源码分发用make_classification或公开数据集生成一个结构类似的模拟数据即可跑通全流程。第三是补充一个快速开始文档把环境依赖用requirements.txt锁死版本特别是pandas、scikit-learn和xgboost这三者的版本组合跨大版本升级会导致WOE分箱边界变化复现结果对不上。最后再补一个验证技巧模型落地前在decision.py里加一个“拒绝推断”的回测逻辑把模型拒绝掉的那部分样本标记出来在下一个时间周期观察实际表现。如果被拒样本的违约率明显低于模型预测的违约率说明阈值定得过于激进模型对尾部风险的估计偏低这时就要重新校正概率校准曲线。这一步虽然不直接改进AUC但能帮你在真实业务里少交学费。本文还有配套的精品资源点击获取