简介这份资源围绕银行客户逾期行为预测这一典型风控场景提供了完整的机器学习毕设项目目标用户是计算机相关专业正在做毕业设计、课程设计或期末大作业的学生也适合需要项目实战练习的入门及进阶学习者。项目中包含全部源码与数据从数据探索、特征工程到模型构建与效果对比均有清晰代码实现。压缩包共10个文件涵盖3个csv训练与测试数据、3个ipynb分析建模笔记本含用户逾期EDA、XGBoostLR、GBDTLR、1个xlsx字段说明、1个txt使用说明以及1个Python脚本整体大小127.3MB目录结构简洁便于快速上手。目前已有228人学习。项目经过严格调试下载后按说明即可运行可帮助读者理清风控建模思路、掌握主流集成模型实战流程同时为毕业设计答辩或求职作品提供完整可复现的参考实现。1. 银行客户逾期行为预测一份能跑通的机器学习源码包做银行信贷风控的都知道客户逾期预测本质上是事前拦截放款前先估算违约概率概率高的那批要么直接拒掉要么提高利率覆盖风险。这份基于 python 的机器学习银行客户逾期行为预测源码包恰好就是一条能跑的完整链路——数据、EDA、XGBoostLR、GBDTLR、提交文件都配齐了。train.csv、test.csv、submission.csv 在 data 目录下三个 notebook 按顺序跑完就能出预测结果。它不追新模型但把特征、训练、验证、提交这一整套动作串齐了适合毕设、课程设计、期末大作业也适合想把风控建模全流程走一遍的练习者。这也是我把它拆开讲透的理由。2. 先看数据长什么样EDA 与字段理解2.1 三个 notebook 与数据文件的分工打开压缩包以后先别急着跑模型。这套项目里最容易忽略的是 data/字段说明.xlsx它把所有列名的含义列清楚了没有它后面所有特征处理都像在黑匣子里猜。文件作用data/train.csv训练数据含目标列data/test.csv测试数据不含目标列需要预测概率data/submission.csv提交样例列名和格式参照它字段说明.xlsx每个字段的业务含义用户逾期_EDA.ipynb数据探查分布、缺失、相关性XGBoostLR.ipynb组合模型一XGBoost 提特征 LR 分类GBDTLR.ipynb组合模型二GBDT 提特征 LR 分类通常这类信贷数据里会包含客户基础属性、收入资产、申请金额、历史还款表现这几类字段具体列名以字段说明.xlsx 为准。我会先读 train.csv 把 shape 和列名打印出来再对着字段说明逐列过一遍这一步大约花 10 分钟但能省掉后面大量的返工时间。import pandas as pd import numpy as np train pd.read_csv(data/train.csv) test pd.read_csv(data/test.csv) print(train shape:, train.shape) print(test shape:, test.shape) print(train columns:, train.columns.tolist())这段代码的作用很直接确认 train.csv 和 test.csv 在列数上是否一致、目标列是否在 train 里被漏读。如果两者列数对不上通常说明数据文件本身有问题或者字段说明里的列需要做列名对齐这时候要先去查使用说明.txt 里有没有额外交代而不是急着往下写特征工程。2.2 目标分布与缺失值先看正样本占比再决定评估方式逾期预测和二分类教科书最不一样的地方在于样本不均衡。逾期客户在真实数据里往往只占个位数百分比所以第一步就要看目标列占比这直接决定后面用 accuracy 还是 AUC、要不要调正样本权重。# 目标列名以字段说明为准这里兼容常见命名 target_col is_overdue if is_overdue in train.columns else train.columns[-1] print(target column:, target_col) print(train[target_col].value_counts(normalizeTrue))跑完这段你会看到类似 0.9x / 0.0x 的分布前者是正常客户后者是逾期客户。如果逾期占比低于 5%后面模型评估就不要再盯着 accuracy它会把所有样本预测成 0 照样拿到高正确率却没有实际风控价值。正确做法是看 AUC 和 PR-AUC这两个指标不受正样本绝对量影响。接下来看缺失值。信贷数据里缺失不是单纯的数据脏很多字段缺失本身就有业务含义比如某项资产没填可能客户压根没有这项资产也可能是渠道不同漏采了。missing train.isnull().mean().sort_values(ascendingFalse) missing missing[missing 0] print(missing)这里的 mean() 算的是每列缺失比例把缺失比例超过 20% 的列单独挑出来后面特征工程阶段统一处理而不是在 EDA 阶段就匆忙填值。常见的错误是看到缺失就立刻 fillna(0)这会把缺失这个信息抹掉尤其收入和资产类字段填 0 会让模型学出低收入等于逾期的假规则。缺失列后面可以加 is_missing 标志位再做填充这一点在特征工程章节展开。EDA 里还有一步是相关性分析。对这个项目我更推荐分块看先看数值列之间的相关性矩阵再单独看每个特征和目标列的关系而不是一张大热力图糊上去。import seaborn as sns import matplotlib.pyplot as plt num_cols train.select_dtypes(include[np.number]).columns.tolist() num_cols [c for c in num_cols if c ! target_col] corr train[num_cols].corr() plt.figure(figsize(12, 10)) sns.heatmap(corr, cmapRdBu_r, center0, vmin-1, vmax1, fmt.2f) plt.title(Correlation Matrix) plt.show()这段代码把数值特征之间的相关性画出来了。看它不是为了找哪个特征和逾期强相关因为这种强相关在信贷数据里很少见而是为了抓多重共线性——如果两个特征相关系数超过 0.8比如年收入和月收入高度相关那 XGBoost 能扛住但后面接的 LR 会受影响系数估计不稳定。处理办法很简单留一个删一个优先留业务语义更直接的字段。3. 特征工程与样本切分防数据泄漏是关键3.1 特征构造三个常做的基础衍生字段多数信贷数据原始的列是数据库直出的直接喂给模型也能出结果但效果一般。原因是模型要从原始字段里自己学交互而数据量不够时交互学不好。常见的做法是构造业务含义明确的衍生字段我一般会先做这三个。第一个是收入和借款的比例关系。银行风控有一个朴素逻辑月收入相对借款金额越高还款压力越小逾期概率越低。单独看月收入和借款金额模型很难直接学到这个比例所以构造一个收入借款比字段。第二个是年龄分箱年龄和逾期是非线性关系年轻用户预期损失高、老年用户风险高中间段反而低直接喂连续值会让树模型不停尝试切分点分箱后模型就能直接利用这个非线性。第三个是历史逾期次数的聚合值如果原始数据里有多次借款记录按客户 ID 聚合出历史逾期次数和历史借款笔数再算一个逾期率。def build_features(df): feat df.copy() # 1. 收入借款比比值越高还款压力越小预期逾期概率越低 if monthly_income in feat.columns and loan_amount in feat.columns: feat[income_loan_ratio] feat[monthly_income] / (feat[loan_amount] 1) # 2. 年龄分箱年龄段与逾期的关系是非线性的 if age in feat.columns: feat[age_bin] pd.cut(feat[age], bins[0, 25, 35, 45, 60, 100], labels[0, 1, 2, 3, 4]).astype(int) # 3. 缺失标志位缺失本身可能是业务信息单独留一个字段 missing_cols [monthly_income, loan_amount] for col in missing_cols: if col in feat.columns: feat[col _is_missing] feat[col].isnull().astype(int) return feat这里啰嗦两句参数。收入借款比的分母加 1 是为了防止除零loan_amount 为 0 的样本不会产生 inf。年龄分箱的边界是通用做法25 岁以下、25-35、35-45、45-60、60 岁以上这五档和信贷生命周期大致对得上但如果你手里的数据年龄分布偏老年用户可以把边界往右挪。缺失标志位加的是 monthly_income 和 loan_amount 两个字段实际做的时候建议对缺失比例高的列都过一遍而不是只挑这两列。3.2 样本切分按时间切片防泄漏这一节是整个项目里最容易翻车的地方。逾期预测虽然用的是分类模型但因果本质上是时间序列今天的特征预测未来某段时间里是否逾期。如果切分训练集和验证集时用随机切分等于把未来的数据混进了训练集模型偷看了未来验证集 AUC 会虚高一大截上线后立刻打回原形。判断这份数据能不能按时间切分先看有没有申请日期或者放款日期字段。如果有正确做法是按时间排序前 80% 训练、后 20% 验证。# 申请日期字段名以字段说明为准常见叫 apply_date / loan_date date_col loan_date if loan_date in train.columns else apply_date train train.sort_values(date_col).reset_index(dropTrue) split_idx int(len(train) * 0.8) train_set train.iloc[:split_idx].reset_index(dropTrue) valid_set train.iloc[split_idx:].reset_index(dropTrue) print(train period:, train_set[date_col].min(), -, train_set[date_col].max()) print(valid period:, valid_set[date_col].min(), -, valid_set[date_col].max())按时间切割后训练集和验证集的分布会有轻微差异这是正常的甚至可以说是好事——它模拟了模型上线后的真实环境。如果验证集 AUC 明显低于随机切分时的水平别慌这是数据防泄漏生效了真实线下评估本来就该更悲观一点。还要注意特征工程必须在切分之后做至少 fit 的过程必须在训练集上完成。比如年龄分箱的边界、缺失值填充用的中位数这些参数都只能用训练集算出来再作用到验证集和测试集否则又会引入泄漏。常见误区是先对整个 train 做标准化和填充再切分看起来没什么问题实际上验证集的信息已经通过统计量流进了训练集。4. XGBoostLR 与 GBDTLR两套组合模型实现4.1 为什么组合模型比单一模型稳这套项目里选 XGBoostLR 和 GBDTLR不是随便凑模型。前者是 boosting 树模型后者是线性模型两个模型特点正好互补。树模型的优势是自动处理非线性、缺失值和特征交互不需要太多人工特征工程弱点是容易过拟合、对特征尺度不敏感、输出概率校准一般。LR 的优势是简单、稳定、可解释、概率校准好弱点是表达力有限做不了非线性原始特征直接喂给它效果很差。组合模型的思路是让树模型先对原始特征做一次非线性变换再把变换结果喂给 LR等于树模型负责理解数据LR 负责做最终决策。这个组合方案在信贷风控里被验证了很多年稳定性和可解释性都够这也是毕设答辩时最好讲清楚的一个点。实现上有两种路径。一种是用树模型的叶子节点索引作为离散特征经过 one-hot 后喂给 LR这是经典的 stacking 思路另一种是直接把树模型的输出概率作为 LR 的一个特征简单但提升有限。这份项目里的写法是第一种也是效果更有保障的。读者如果只是交作业跑通第一种就够用了。4.2 XGBoostLR提取叶子节点做二次训练先看 XGBoost 部分。训练参数是按风控场景的通用配置写的线上这类数据通常几千到几万行、十几到几十个特征这个量级下 max_depth6、n_estimators300 是稳妥起点。import xgboost as xgb from sklearn.linear_model import LogisticRegression feature_cols [c for c in train_set.columns if c not in [target_col, date_col]] X_train train_set[feature_cols] y_train train_set[target_col] X_valid valid_set[feature_cols] y_valid valid_set[target_col] # 第一阶段XGBoost 训练 xgb_model xgb.XGBClassifier( n_estimators300, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, eval_metricauc, use_label_encoderFalse ) xgb_model.fit(X_train, y_train, eval_set[(X_valid, y_valid)], verboseFalse)参数说明n_estimators 是树的数量300 在万级样本下不容易欠拟合learning_rate0.05 配合 300 棵树学习率越低单棵树贡献越小、整体越稳但训练时间变长subsample0.8 表示每棵树随机抽 80% 样本训练防止过拟合colsample_bytree0.8 表示每棵树随机抽 80% 特征同样是防过拟合。eval_metricauc 是对验证集计算 AUC用来观察模型是否在某一轮开始过拟合。如果你装的 xgboost 版本较新use_label_encoder 这个参数可以直接删掉。接下来是关键一步用训练好的 XGBoost 对每个样本输出叶子节点索引然后做 one-hot再训练 LR。# 提取叶子节点索引返回形状 (n_samples, n_trees) train_leaf xgb_model.apply(X_train) valid_leaf xgb_model.apply(X_valid) print(train leaf shape:, train_leaf.shape) # 叶子索引是离散类别one-hot 后喂给 LR from sklearn.preprocessing import OneHotEncoder enc OneHotEncoder(handle_unknownignore) X_train_leaf_oh enc.fit_transform(train_leaf) X_valid_leaf_oh enc.transform(valid_leaf) # 第二阶段LR 在叶子编码特征上做最终分类 lr LogisticRegression(C1.0, max_iter1000) lr.fit(X_train_leaf_oh, y_train) val_prob lr.predict_proba(X_valid_leaf_oh)[:, 1]这里的逻辑大家可以仔细体会一下。xgb_model.apply() 返回的是每个样本落在每棵树的哪个叶子节点数值上是一个个整数索引但它不代表任何顺序或大小关系所以不能直接当数值特征用。one-hot 编码之后每个叶子节点变成一个独立的 0/1 特征LR 才能正确学习每个叶子节点对应的逾期概率偏置。C1.0 是 LR 的正则化强度C 越小正则越强风控场景下如果验证集 AUC 不稳定可以先把 C 降到 0.5 试试。4.3 GBDTLR 对照实现GBDTLR 的思路和 XGBoostLR 完全一样只是基学习器换成 sklearn 自带的 GradientBoostingClassifier。这一步用来做对照实验答辩时能说明不同树模型对组合结果的影响这是一个加分项。from sklearn.ensemble import GradientBoostingClassifier gbdt GradientBoostingClassifier( n_estimators200, max_depth4, learning_rate0.06, subsample0.8 ) gbdt.fit(X_train, y_train) # sklearn 的 apply 返回三维数组取最后一维 train_leaf_gbdt gbdt.apply(X_train)[:, :, 0] valid_leaf_gbdt gbdt.apply(X_valid)[:, :, 0]注意这里和 xgboost 的一个差异sklearn 的 apply 返回 (n_samples, n_estimators, n_iterations) 三维数组第三维只有一个值所以用 [:, :, 0] 取出来变成二维。如果你直接用二维索引形状会报错这是 sklearn 版本差异导致的常见翻车点。拿到的叶子特征走同样的 one-hot LR 流程。因为上一节的 enc 是在 XGBoost 叶子特征上 fit 的这一节要重新 fit 一个新的 OneHotEncoder不要复用。两个模型的验证集 AUC 对比一下通常 XGBoostLR 略高一点点但 GBDTLR 在解释性上报给老师更好讲因为它来自 sklearn训练过程可以平滑地画出 feature_importance。两个 notebook 就是干这件事的跑完之后把 AUC 和 PR-AUC 记下来做成一张对比表写进论文。5. 训练与提交常见问题五个坑一个对策表5.1 随机切分导致验证集 AUC 虚高现象第一次跑模型验证集 AUC 到了 0.9 以上大家都以为模型很强了。原因用了 train_test_split 随机切分训练集里混进了未来样本模型通过时间相关性偷看了答案。解决改成按时间排序后切分重新训练后 AUC 回落到 0.7-0.8 区间才是真实水平。这件事特别容易发生在用 sklearn 默认随机切分的同学身上不对比时间切分结果根本发现不了。5.2 正样本太少模型全预测 0现象accuracy 96%模型像是躺赢了但看到的 AUC 只有 0.5等于随机猜。原因正样本占比一个位数LR 和 XGBoost 默认优化目标是整体准确率全预测 0 损失最小。解决评估指标换成 AUC 和 PR-AUC训练时给 XGBoost 加 scale_pos_weight 参数值约等于负样本数除以正样本数LR 那边用 class_weightbalanced。改完之后模型才会真正尝试区分正样本。5.3 叶子节点特征直接拼 LR 效果反而变差现象加了叶子节点特征之后 AUC 不仅没涨反而跌了跟论文里组合模型效果更好的说法对不上。原因叶子索引是整数研究者直接把它当数值特征喂给 LR模型学到一个叶子编号越大越容易逾期的假规律实际上编号只代表树里的位置没有任何大小含义。解决对叶子索引做 one-hot 编码确保每个叶子节点是独立的类别特征再喂 LR。做完这一步 AUC 通常能比单模型高出 2-3 个点。5.4 submission.csv 提交格式不对现象本地验证一切正常提交时提示 ID 对不上或者评分直接报错。原因测试集在特征工程里排序过、reset_index 之后和原始 test.csv 的行顺序不一致提交时把原始 ID 弄丢了。解决预测之前单独保存一份 test 原始 ID最后提交时用 pd.merge 把概率和 ID 对齐再保存成和 submission.csv 完全一致的列名。提交前用 pd.read_csv 打印前五行对照一下。注意submission.csv 里往往只保留两列——样本 ID 列和预测概率列。概率列名不一定要叫 pred以原文件列名为准但样本顺序必须严格对齐。5.5 缺失值填 0 带来的分布偏移现象特征工程把收入和资产字段的缺失值填充成 0训练集 AUC 正常但验证集分布一变化模型就崩。原因缺失填充成 0 之后模型学到的规则是月收入为 0 的客户逾期概率高等到真实场景里缺失被填成其他值时规则立刻失效。解决先加缺失标志位再用训练集统计量填充中位数更稳妥填充和标志位一起喂给模型。这两个特征配合起来模型才能同时利用缺失与否和缺失时的基准值两层信息。6. 验证与提交阈值选择与结果检查习惯模型训练完之后最后一个动作是把概率转成 0/1 类别但阈值未必是 0.5。逾期预测里正样本少0.5 通常不是最优切分点需要看 PR 曲线来找。from sklearn.metrics import precision_recall_curve # 用 XGBoost 单独模型的验证集概率来选阈值 y_prob xgb_model.predict_proba(X_valid)[:, 1] precision, recall, thresholds precision_recall_curve(y_valid, y_prob) f1_scores 2 * precision[:-1] * recall[:-1] / (precision[:-1] recall[:-1] 1e-9) best_idx np.argmax(f1_scores) best_thr thresholds[best_idx] print(best threshold:, round(best_thr, 4)) print(best F1:, round(f1_scores[best_idx], 4))这里用 F1 最大来找阈值只是一个默认策略。如果项目要求宁可误杀也不能漏掉逾期那阈值往低调比如 0.3如果要求宁可放过也不能打扰好客户阈值往高调。这个选择应该在答辩时交代清楚是体现业务理解的地方。阈值定好之后输出提交文件。我见过太多人在这里翻车所以固定了一套检查流程每一步都不过脑子地执行完已经成了习惯。test[pred_prob] xgb_model.predict_proba(test[feature_cols])[:, 1] sub pd.DataFrame({ id: test[id].tolist(), # ID 列名以原 submission.csv 为准 pred: test[pred_prob].tolist() }) sub.to_csv(submission_my.csv, indexFalse) # 强制检查三步前五行、行数、概率范围 check pd.read_csv(submission_my.csv) print(check.head()) print(total rows:, len(check)) print(prob range:, check[pred].min(), -, check[pred].max())检查的三样东西对应三个常见翻车点前五行确认列名和样例一致行数确认测试集样本一条没丢概率范围确认没有 NaN 混进结果。任一环节异常就回头查不要提交之后再后悔。从那以后我每次做完一份风控项目提交前都会强制走一遍这套检查列名对齐、样本数、概率范围、验证集 AUC、正样本占比五样齐全才算完。这不是什么高大上的技巧就是笨办法但确实帮我在答辩前拦下了好几次翻车。希望帮到你。本文还有配套的精品资源点击获取