
简介这套基于Python的贷款违约预测机器学习实践源码面向金融科技学习者、数据科学家及金融机构风控人员聚焦信贷审批中的违约风险识别与信用评估问题完整演示了从数据清洗、特征工程、模型训练到结果预测的标准化流程。压缩包共23个文件、13.77MB含7个Python核心源码、11张训练可视化PNG图、2个CSV数据集、1个Excel预测结果文件及说明文档。多个py脚本分别对应随机森林、决策树、梯度提升等算法训练与对比并配套学习曲线、特征重要性等可视化图表便于直观评估模型效果。已有560人学习下载。通过该项目可掌握贷款违约预测的完整建模思路获得可直接运行的分类模型代码、数据处理脚本和图表生成逻辑还可借助readme与Markdown文档快速复现实验适合用于课程设计、竞赛备练或金融风控入门实践。1. 贷款违约预测在解决什么问题一本能跑的机器学习源码而不是模型拼盘贷款违约预测是机器学习在金融风控里为数不多能直接产生业务价值的落地场景。用Python把它做成一套完整源码意味着不再依赖Excel规则表和人工判断而是让历史借贷数据自己说话——坏客户有哪些共同特征、哪些字段最能区分风险。这篇笔记把基于Python的贷款违约预测机器学习实践源码拆开讲从特征工程讲到模型评估标出哪些代码能直接抄、哪些参数必须改。这道题解决三个实际问题一是放款前的审批决策预测每个申请人的违约概率二是存量客户额度管理把概率换算成额度调整依据三是给毕业设计或转行简历提供一个完整可讲述的机器学习项目。适合三类人金融风控从业者、学完机器学习想练真实项目的开发者、正在找课设题目的学生。需要说明的是这套方案的重心不在算法有多华丽而在于把数据、特征、模型、评估串成一条能反复运行、能跟业务对齐的流水线。先把Python 3.9以上装好、vscode配好解释器再把依赖装齐就能跟着跑。2. 数据处理与特征工程把借贷流水变成模型能用的表格从原始数据到第一版特征表通常占整个项目六成时间。周志华的《机器学习》里讲数据预处理时强调一个观点算法消耗的是特征里的信息量原始表里90%的列直接喂给模型反而拖后腿。这套源码建议按工程方式组织而不是把所有处理堆在一个notebook里。下面按数据加载、缺失值、类别编码三段拆开讲每一段都能直接落成脚本。2.1 源码目录划分与数据加载先把入口和依赖理清楚我一般会把一个能交作业的贷款违约预测源码工程拆成五个模块数据、特征、模型、评估、配置。业务方和答辩老师看目录结构就能知道你做了哪几步比一个几百行的main.py有说服力得多。常见结构如下loan_default_predict/ ├── data/raw/ # 原始数据只读不写 ├── data/processed/ # 清洗后的特征表 ├── features/ # 特征工程脚本 ├── models/ # 模型训练脚本 ├── eval/ # 评估结果与图表 ├── main.py # 训练入口 └── config.yaml # 参数配置目录划分的逻辑是让每个阶段能单独重跑。数据清洗变了不需要重训模型模型调参了不需要重新算特征。main.py只做编排不写具体逻辑。配置单独放是因为特征工程的缺失率阈值、模型的正则强度、切分比例这些参数以后都要调硬编码在脚本里改起来容易翻车。数据加载的第一步是统一列名和检查目标分布。不同渠道拿到的数据可能叫loan_status也可能叫is_default先归一化再往下走import pandas as pd from pathlib import Path RAW_PATH Path(data/raw/loan_data.csv) PROCESSED_PATH Path(data/processed/) df pd.read_csv(RAW_PATH, parse_dates[apply_date]) df.rename(columns{loan_status: is_default}, inplaceTrue) df[apply_date] pd.to_datetime(df[apply_date], errorscoerce) print(数据规模:, df.shape) print(目标分布:\n, df[is_default].value_counts(normalizeTrue))这里有两个关键点。一是parse_dates在读取时就把时间列转成datetime后续按时间切分才能排序。二是errorscoerce会把格式异常的时间解析成NaT之后统一处理不至于让整行数据因为一个日期格式问题被丢弃。打印目标分布是为了确认类别不平衡程度——如果违约率只有5%第4章的评估陷阱几乎必然出现。2.2 缺失值处理别让空值悄悄变成一列特征贷款数据里缺失值不是噪声反而是信号。一个客户没填收入、没填工作单位、没填居住地址本身就说明他的资质可能有问题。所以处理缺失不能一删了之要分成三档来对待。missing_ratio df.isnull().mean().sort_values(ascendingFalse) drop_cols missing_ratio[missing_ratio 0.6].index.tolist() df df.drop(columnsdrop_cols) num_cols df.select_dtypes(include[float64, int64]).columns num_cols [c for c in num_cols if c not in [is_default, loan_id]] for col in num_cols: df[col _is_missing] df[col].isnull().astype(int) df[col] df[col].fillna(df[col].median()) cat_cols df.select_dtypes(include[object]).columns for col in cat_cols: df[col] df[col].fillna(__MISSING__)第一档是缺失率超过60%的列直接删除——特征缺失太多模型学到的只是“是否缺失”这个信号缺失本身没有区分度。第二档是数值列填充中位数而不是均值因为收入、贷款金额这类金融字段是长尾分布均值会被极端值拉偏中位数更稳健。同时额外生成一列_is_missing标记把“曾经缺失”这个事实保留给模型。第三档是类别列填充一个特殊字符串让模型自己决定这个类别怎么用。缺失率阈值60%不是固定值我在config.yaml里把它配成参数。数据量大时可以放宽到70%数据量小时建议收紧到40%否则删掉太多列会让后续特征工程没有素材。判断依据很简单删除后模型AUC下降超过0.02说明删多了。注意填充必须在切分训练集之前完成但填充值的计算只能用训练集的数据。中位数如果用了全量数据计算验证集的信息已经泄漏进训练过程了。2.3 类别特征编码与WOE把分类变量转成模型能理解的数值贷款数据里的类别字段比想象中多职业、学历、居住地、产品类型、渠道来源。逻辑回归不能直接吃字符串常见的做法是独热编码或标签编码。但风控场景还有一个更常用的方案——WOE编码它能把类别字段和违约率的关系直接编码成一个单调数值。import numpy as np def woe_encode(series, target, bins5, min_pct0.05): df_tmp pd.DataFrame({x: series, y: target}) df_tmp[x] pd.qcut(df_tmp[x], qbins, duplicatesdrop) grouped df_tmp.groupby(x, observedTrue)[y].agg([sum, count]) grouped[bad] grouped[sum] grouped[good] grouped[count] - grouped[sum] grouped[bad_rate] grouped[bad] / grouped[bad].sum() grouped[good_rate] grouped[good] / grouped[good].sum() grouped[woe] np.log(grouped[good_rate] / grouped[bad_rate]) grouped[iv] (grouped[good_rate] - grouped[bad_rate]) * grouped[woe] return grouped[woe], grouped[iv].sum()WOE的公式是ln(好样本占比 / 坏样本占比)值越大代表这个分箱里的客户越优质。IV值是WOE按占比加权的总和用来衡量特征的预测力IV大于0.1的特征值得保留低于0.02的基本是噪声。min_pct0.05保证每个分箱至少占5%的样本防止某个箱子只有几十个人导致WOE剧烈波动。对类别字段做WOE编码时先把每个类别的违约率算出来再按违约率排序后当成数值列分箱。这样既保留了类别信息又让逻辑回归系数具有业务解释性——某个职业的WOE为负说明该职业违约率高于整体水平审核时值得多看一眼。特征工程做完后把结果表统一存成parquet格式后续模型训练直接从processed目录读取避免每次重跑都从头清洗。3. 模型选型与训练逻辑回归当基准树模型负责提分特征表就绪后进入建模核心原则是先打基准再上复杂模型。我见过太多项目一上来就跑XGBoost最后既说不清特征重要性也解释不了业务规则。正确的顺序是先用逻辑回归跑通全流程记下AUC和KS再尝试随机森林或XGBoost看提升了多少、提升来自哪些特征。下面的代码都能直接替换进源码的models目录。3.1 逻辑回归可解释性好适合做风控审核依据逻辑回归在贷款违约预测里没有被淘汰反而适合当第一版上线模型。它的系数直接对应风险方向审核人员能看懂“收入越高违约概率越低”这类结论模型也容易过监管审查。训练前必须做标准化因为逻辑回归对特征尺度敏感L2正则会把量级大的特征系数压得更小。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression feature_cols [c for c in df.columns if c not in [loan_id, apply_date, is_default]] X df[feature_cols] y df[is_default] pipe_lr Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(class_weightbalanced, max_iter1000, C0.1)) ]) pipe_lr.fit(X_train, y_train) coef_df pd.DataFrame({ feature: feature_cols, coef: pipe_lr.named_steps[lr].coef_[0] }).sort_values(coef, keyabs, ascendingFalse) print(coef_df.head(10))class_weightbalanced是这里最关键的参数它会按类别频率自动放大少数类的损失权重让模型不只会预测“不违约”。C0.1是L2正则强度的倒数C越小正则越强系数越稳定如果训练集只有几千条样本C可以再降到0.05。标准化的位置必须放在Pipeline里而不是预先改数据这样预测新样本时会自动套用同一套均值方差不会因为忘记缩放导致上线翻车。3.2 XGBoost用早停和网格搜索把AUC往上顶逻辑回归跑通后用树模型提升精度。XGBoost在表格数据上是默认选择对特征缩放不敏感能自动处理缺失值非线性交互也学得动。吴恩达的课里反复强调验证集要独立这里更是如此——树模型太容易过拟合早停是必需品。import xgboost as xgb from sklearn.model_selection import GridSearchCV param_grid { max_depth: [4, 6], learning_rate: [0.05, 0.1], subsample: [0.8, 1.0], colsample_bytree: [0.8, 1.0], } model_xgb xgb.XGBClassifier( n_estimators500, eval_metricauc, early_stopping_rounds50, use_label_encoderFalse, ) grid GridSearchCV(model_xgb, param_grid, cv3, scoringroc_auc, n_jobs-1) grid.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse) print(最优参数:, grid.best_params_) print(验证集最佳AUC:, grid.best_score_)每个参数都有明确的调整方向。max_depth控制在4到6之间太深必过拟合信贷数据里几乎没有需要深度超过8的交互。learning_rate0.05配合n_estimators500模型会学得慢但更稳如果数据量超过10万行可以把learning_rate调到0.1节省训练时间。subsample0.8让每棵树只用80%的样本colsample_bytree0.8让每棵树只用80%的特征两个参数一起给模型注入随机性是防止树模型翻车的最有效手段。early_stopping_rounds50的含义是验证集AUC连续50轮不提升就停止训练这样n_estimators500只是上限而不是最终迭代次数。网格搜索的cv3就够金融数据通常按时间切分用5折交叉验证反而会引入未来信息。3.3 按时间切分训练集与测试集金融数据不能随机打散这条是金融建模里最容易踩、代价也最高的坑。常规机器学习项目用train_test_split随机切分没问题但贷款数据有严格的时间先后关系——用2020年的数据训练、2021年的数据测试才符合“用过去预测未来”的真实场景。随机切分会把同一时期、同一批客群的数据同时塞进训练集和测试集AUC虚高0.05到0.1很常见。df df.sort_values(apply_date).reset_index(dropTrue) train_end int(len(df) * 0.7) val_end int(len(df) * 0.85) train_df df.iloc[:train_end] val_df df.iloc[train_end:val_end] test_df df.iloc[val_end:] X_train, y_train train_df[feature_cols], train_df[is_default] X_val, y_val val_df[feature_cols], val_df[is_default] X_test, y_test test_df[feature_cols], test_df[is_default]这里和量化策略回测是同一个道理策略在历史数据上表现好不代表未来能赚钱因为回测样本和实盘样本没有时间重叠。贷款违约预测也一样线上场景永远面对的是“未来”的客户验证集必须晚于训练集。切分比例70/15/15在样本量超过5万时够用数据少时可以改成60/20/20。如果只想省事用sklearn的TimeSeriesSplit做滚动验证也可以但第一版源码我建议直接按日期排序切分逻辑最直观答辩也最容易讲清楚。4. 贷款违约预测的高频踩坑类别不平衡、数据泄露与评估陷阱这个项目最容易让人误以为成功的不是模型选错而是评估口径错了。违约率通常只有3%到8%在这个背景下准确率是一个会骗人的指标。下面四条坑我都在真实数据上踩过每一条都按现象、原因、解决的顺序写清楚。4.1 类别不平衡准确率95%可能是假象现象模型预测结果打印准确率高达95%看起来表现很好但业务方根本不买账。追问发现模型把所有客户都预测成了“不违约”——因为违约样本只占5%全预测为不违约就已经有95%的准确率但一个坏客户都没识别出来。原因默认分类阈值0.5在类别严重不平衡时失效。模型输出的违约概率可能集中在0.1到0.3之间以0.5为界划分会把绝大多数真实违约样本划到不违约一侧。只看准确率等于默认了“不违约”这个预测结果的价值和“违约”预测结果相同但风控场景里漏掉一个坏客户的损失远大于误伤一个好客户。解决换评估指标看AUC、KS、召回率和精确率同时调整阈值而不是死守0.5。AUC只看排序能力不受阈值影响KS衡量好坏样本区分度这些都是类别不平衡下的稳定指标。阈值选择用下面的方法扫描from sklearn.metrics import confusion_matrix pred_proba pipe_lr.predict_proba(X_test)[:, 1] for threshold in [0.2, 0.3, 0.4, 0.5]: pred_label (pred_proba threshold).astype(int) tn, fp, fn, tp confusion_matrix(y_test, pred_label).ravel() precision tp / (tp fp) recall tp / (tp fn) print(fthreshold{threshold:.1f} precision{precision:.3f} recall{recall:.3f})扫描之后你会发现阈值从0.5降到0.3时召回率可能从30%涨到60%精确率只是小幅下降。具体选哪个点取决于业务审批阶段宁可错杀不可漏过就选召回率高的低阈值贷后催收资源有限就选精确率更高的高点。4.2 SMOTE过采样切分前做等于把答案塞进验证集现象用SMOTE过采样后验证集AUC从0.72暴涨到0.89往上一提交心里窃喜。结果做时间切分复测AUC掉回0.74过采样带来的提升几乎不存在。原因SMOTE在切分前做了全量数据的过采样合成样本会和原始样本在验证集里形成“近亲”。SMOTE的插值逻辑是在同类样本之间制造新样本验证集里一旦存在某个原始样本它的合成邻居也大概率在验证集里模型等于见过答案再考试。解决先按时间切分再只在训练集内过采样。写成代码就是顺序不能反from imblearn.over_sampling import SMOTE # 错误先过采样再切分数据泄漏 # X_res, y_res SMOTE().fit_resample(X_all, y_all) # 正确先切分训练集内部过采样 X_train_res, y_train_res SMOTE(random_state42).fit_resample(X_train, y_train)过采样只作用于训练集验证集和测试集必须保持原始的违约率。random_state42固定住合成样本的生成过程保证每次复现结果一致。另一个替代方案是不做SMOTE直接给树模型设置scale_pos_weight参数效果接近且没有泄漏风险。4.3 特征里混进了未来数据AUC 0.99先别高兴现象某个特征的加入让AUC直接冲到0.99模型几乎完美。检查特征列表发现里面有一列叫“放款后逾期次数”这显然是在违约发生之后才能观测到的数据——模型学习到的不是预测规律而是结果本身。原因这叫数据泄露在贷款预测里最常见的来源有三个特征表里混入了放款后才产生的行为数据用全量数据算特征统计量导致目标信息进特征对时间窗口处理不当比如用未来12个月的还款记录预测当前违约。解决建立特征血缘清单标注每个特征的数据截止时点绘制特征表时强制检查。我在源码的features目录里放了一个feature_audit.csv维护特征名、数据来源、可用的最早时间戳三列。AUC超过0.95就要警惕先跑一遍特征重要性排序把排名靠前但业务上解释不通的特征逐个删掉重训。4.4 拒绝样本缺失训练集和线上人群不是一回事现象离线验证AUC稳定在0.78上线后实际区分度明显变差坏客户识别率远低于测试集表现。原因模型训练只用了历史已放款客户的数据这些客户是经过审批规则筛选的“幸存者”。被拒绝的申请人没有标签他们当中可能包含大量潜在坏客户也可能包含因误拒错过的优质客户。训练集和真实申请人群的分布不一致这叫样本选择偏差在信贷场景里无法完全消除。解决离线评估看相对排序而不是绝对概率。模型的概率值是相对排序高分客户的违约风险确实比低分客户高但0.7这个绝对数值在线上和线下含义可能不同。严肃的做法是引入拒绝推断用规则模型给被拒绝样本打伪标签再训练第一版项目至少要做到两件事在评估报告里写明训练集只覆盖已放款客群承认bad rate被低估只对通过审批的客户区间内评估模型可用性不把概率绝对值直接给业务当硬指标。5. 从能跑源码到能上线阈值校准、概率输出与特征监控模型训练完不等于项目结束。可用的贷款违约预测源码最后一步是让业务方真正能用起来。我有两个习惯第一模型只输出概率不输出0/1标签把决策权交还给业务第二阈值用约登指数自动寻找而不是固定在0.5。约登指数是ROC曲线上敏感度与特异性之差的最大点数学上等价于让误判成本最小的近似解from sklearn.metrics import roc_curve def choose_threshold(y_true, y_proba): fpr, tpr, thresholds roc_curve(y_true, y_proba) youden tpr - fpr best_idx np.argmax(youden) return thresholds[best_idx] best_th choose_threshold(y_test, pred_proba) print(f推荐阈值: {best_th:.4f})上线后的监控同样重要。每个特征都会随时间偏离训练分布我一般用PSI群体稳定性指标按月监控PSI超过0.25说明特征分布变化过大需要重训或回滚。计算公式不复杂把当前月份特征分布和建模时分布分箱逐箱计算占比差异乘对数差异后求和。特征监控脚本放在eval目录下每天定时跑和模型训练解耦。这套方案值不值得投入如果你手头只有一个原始的贷款数据集我建议先把第2章的特征工程做扎实用逻辑回归跑通全流程再考虑上XGBoost。我最早做这个项目时也在随机切分上翻过车AUC虚高到让人膨胀后来所有金融模型一律按时间切分阈值从0.5改成约登点之后业务方终于认可这个模型能用。希望帮到你。本文还有配套的精品资源点击获取