简介:面向计算机相关专业毕业设计及机器学习实战学习者,这是一套银行客户逾期行为预测的完整Python实现,包含全部源码与训练/测试数据,可用于毕设、课程设计或期末大作业。压缩包共10个文件,以CSV数据、Jupyter Notebook建模分析文档、Excel字段说明及使用说明为主,整体约127.3MB;其中Notebook覆盖数据探索EDA与XGBoost+LR、GBDT+LR两类组合模型,可对照学习特征工程与模型调优流程。包内另附Python脚本、字段说明与运行指导,项目经过严格调试,具备完整可复现的实验链路,方便直接部署或在此基础上拓展改进。目前已有229人学习下载,适合需要快速搭建金融风控预测项目的初中级学习者。
1. 机器学习银行客户逾期行为预测:这份毕业设计源码到底能跑到什么程度
银行客户逾期行为预测,本质就是给你一堆客户历史行为和还款记录,让你在放款前判断“这个人未来会不会逾期”。听起来像风控专家的黑匣子,但这份 python 毕业设计源码把全流程摊开了:从原始 train.csv/test.csv 开始,先做探索性数据分析,再用 XGBoost+LR、GBDT+LR 两套组合模型做预测,最后输出 submission.csv。它不是只有一堆代码文件,而是带着字段说明、数据字典和能直接运行的 notebook,很适合计算机专业做毕业设计、课程设计或者想练机器学习完整流程的同学。
我拆这份资源时的第一感受是:它的价值不在模型本身有多高大上,而在“数据处理到提交结果”这条链路是完整闭环的,训练集、测试集、提交样例、字段说明全齐,下载下来改改特征和参数就能跑通。适用人群很具体——需要应付毕设开题、中期检查和最终答辩的学生,以及想从“只懂调库”过渡到“能解释特征为什么有效”的初学者。下面我按实际动手顺序,讲讲这份源码里最值得关注的地方。
2. 读懂 data 目录:train.csv、test.csv 与字段说明的配合逻辑
2.1 字段说明.xlsx 是一切的起点
有些同学拿到项目先看模型代码,其实顺序反了。这个资源里字段说明.xlsx才是真正的钥匙。它告诉你 train.csv 里每一列到底代表什么业务含义,这直接决定你后续怎么做特征工程。一般来说,银行逾期数据集包含客户基本信息(年龄、收入、职业)、历史借贷记录(贷款金额、期限、历史逾期次数)、还款行为(最近还款情况、账户状态)等。
我建议拿到文件先做三件事:第一,打开字段说明,把每个字段的“业务含义”和“数据类型”列出来;第二,对应到 train.csv 的实际列名,检查有没有语义不明确的字段;第三,标记出哪些字段可能是“事后变量”,也就是逾期结果已经发生后才产生的数据,这类字段在预测时要特别注意,否则会造成数据穿越。
2.2 训练集与测试集的划分规则
train.csv 和 test.csv 的划分逻辑,决定了你能不能正确复现提交结果。常见做法是按时间切分——前面的月份做训练,后面的月份做测试,这样才符合银行风控的实际场景:用历史数据训练,预测未来客户。但很多初学者拿到数据就直接用 train_test_split 随机切分,这会导致模型“偷看”未来信息,线上分数会比本地验证好看,真实场景却翻车。
我在 notebook 的 EDA 部分先确认了数据的行数、列数、数据类型分布,然后检查是否有明显的重复样本和全空列。用info()和describe()先做快速体检,这个动作不要跳过,它能帮你发现字段说明里没提到的脏数据。
2.3 EDA notebook:三个必看的分析点
用户逾期_EDA.ipynb 里有一批值得抄作业的可视化分析,我拆的时候重点看了三块:
第一块是目标变量分布。逾期样本占比多少、是否严重不平衡。这个数字直接决定你要不要用 scale_pos_weight、SMOTE 或者自定义评估指标。银行场景里逾期率通常很低,可能只有百分之几,这种情况下准确率是无意义的,必须看 AUC 或 PRAUC。
第二块是数值型特征的分布和缺失率。train.isnull().mean()这个操作虽然基础,但能快速列出每个字段的缺失比例,我一般会把缺失率超过 30% 的字段单独拎出来判断是要删除还是做特殊填充。
第三块是特征与目标的单变量关系。比如年龄、收入这些字段分组后计算逾期率,看是否存在明显的单调趋势。这一步能给你后续特征工程提供方向,不是随便找几个字段扔进模型就完事。
3. 从 EDA 到建模输入:数据清洗与特征工程的可复现操作
3.1 缺失值处理:不同字段区别对待
从 EDA 进入建模前,缺失值处理是必经之路。不是所有字段都适合用均值填充。以银行逾期数据为例,收入字段的缺失可能代表客户无固定收入,直接填均值反而掩盖了这个信号。我一般按字段业务含义分组处理。
import pandas as pd import numpy as np train = pd.read_csv('data/train.csv') # 查看每个字段的缺失率和数据类型 missing_report = pd.DataFrame({ 'dtype': train.dtypes, 'missing_count': train.isnull().sum(), 'missing_ratio': train.isnull().mean() }) print(missing_report[missing_report['missing_ratio'] > 0].sort_values('missing_ratio', ascending=False)) # 数值型字段按中位数填充,避免极值影响 num_cols = train.select_dtypes(include=[np.number]).columns for col in num_cols: if train[col].isnull().any(): train[col] = train[col].fillna(train[col].median()) # 类别型字段用众数填充,同时保留缺失标记 cat_cols = train.select_dtypes(include=['object']).columns for col in cat_cols: if train[col].isnull().any(): train[col] = train[col].fillna('missing')这段代码的意图很清楚:先输出缺失报告,让你知道每个字段脏到什么程度,再分别用中位数和众数填充。为什么数值型用中位数而不是均值?因为收入、贷款金额这类字段容易有长尾分布,均值会被极值拉偏,中位数更稳健。类别型填missing而不是直接填空字符串,是为了让模型能识别“这一列原本缺失”这个信息,LightGBM 和 XGBoost 都能直接利用这个模式。
3.2 特征构造的常见思路
这个项目里最值得学的不是调参,而是特征构造的思路。我从源码里梳理出几条适用性很强的方向:
第一是时间类特征。如果原始数据里有申请日期、还款日期,可以构造“距今天数”“历史逾期距上次逾期的间隔”这类特征。逾期行为往往有短期重复性,间隔越短风险越高。
第二是聚合统计特征。按客户 ID 分组,计算历史借款次数、平均借款金额、最大逾期天数等。银行客户的数据结构通常是“一个客户多条借款记录”,如果不做聚合,模型学不到客户维度的整体画像。
第三是交叉特征。把“收入区间和贷款金额的比值”“年龄和贷款期限的组合”这类特征显式构造出来。虽然树模型天然能做特征分裂,但显式的比值特征往往能让分裂更有业务解释性。
# 构造客户维度的聚合特征 customer_group = train.groupby('customer_id').agg({ 'loan_amount': ['count', 'sum', 'mean', 'max'], 'overdue_days': ['max', 'mean', 'sum'] }) customer_group.columns = ['loan_count', 'loan_total', 'loan_mean', 'loan_max', 'overdue_max', 'overdue_mean', 'overdue_sum'] customer_group = customer_group.reset_index() # 合并回训练集 train = pd.merge(train, customer_group, on='customer_id', how='left')这里说明一下:groupby().agg()的用法是同时算多个统计量,减少重复代码。loan_count是客户借款次数,overdue_max是历史最大逾期天数,这些特征捕获了客户的整体风险偏好。合并时用how='left'保证保留训练集的所有行,不会因为某些客户在聚合表里不存在而丢失。
3.3 类别特征编码:不踩 LabelEncoder 的坑
很多教程直接用 LabelEncoder 处理所有类别特征,这在树模型里风险不大,但如果是类别取值特别多的字段容易出问题。我更习惯先看字段的类别数再做决定。类别数少(10 个以内)用 one-hot;类别数多(几十上百)用频率编码或 target encoding。项目里如果只是树模型,LabelEncoder 也可以用,但热编码更稳。
# 检查类别特征的唯一值数量 for col in cat_cols: n_unique = train[col].nunique() print(f'{col}: {n_unique} unique values') # 低频类别合并,防止类别数过多导致过拟合 value_counts = train['employment_type'].value_counts() rare_categories = value_counts[value_counts < 100].index train['employment_type'] = train['employment_type'].replace(rare_categories, 'rare')低频类别合并这一步很实用:有些类别只出现几次,模型根本学不到稳定规律,还会在测试集上遇到根本没见过的类别时直接报错或随机分裂。设置阈值 100 的意思是样本少于 100 的类别统一归为rare,测试集里出现新类别时也按这个逻辑处理。
4. XGBoost+LR 与 GBDT+LR 的实现细节:两套组合模型怎么选、怎么调
4.1 为什么要做模型融合:单模型上限在这里
银行逾期数据通常是非线性关系复杂、特征维度高、样本不平衡的场景。单棵决策树太弱,RandomForest 能缓解方差但偏差不一定够。XGBoost+LR 和 GBDT+LR 的核心思路,是用树模型自动做特征交叉和筛选,把原始特征转换成稀疏的“叶子节点指示向量”,再喂给 LR 做最终分类。这样既利用了树的非线性拟合能力,又继承了 LR 的稳定性和可解释性。
一句话概括:树模型负责找到特征之间的复杂互动规律,LR 负责在树模型输出的高阶特征上做最后的决策边界。这个套路在 Kaggle 金融风控类比赛中是久经考验的方案。
4.2 XGBoost 部分的关键参数解读
源码的 XGBoost+LR.ipynb 里,关键参数集中在树的复杂度控制和防止过拟合上。我挑几个实际起作用的说:
max_depth:控制单棵树深度,太深容易记住噪声,浅了又学不到复杂规律。常见范围 4~8,风控数据通常不会太深。learning_rate:每棵树对最终结果的贡献权重,调小后需要更多弱学习器,但泛化更稳。常用 0.01~0.1。subsample和colsample_bytree:控制样本采样和特征采样的比例,相当于给每棵树提供不同的视角,降低过拟合。scale_pos_weight:处理正负样本不平衡的关键参数,一般设为负样本数除以正样本数的比例。
import xgboost as xgb from sklearn.linear_model import LogisticRegression # 将训练数据转为 DMatrix 格式 dtrain = xgb.DMatrix(X_train, label=y_train) dtest = xgb.DMatrix(X_test, label=y_test) # 树模型参数 xgb_params = { 'max_depth': 6, 'learning_rate': 0.05, 'subsample': 0.8, 'colsample_bytree': 0.8, 'scale_pos_weight': (y_train == 0).sum() / (y_train == 1).sum(), 'eval_metric': 'auc', 'objective': 'binary:logistic', 'nthread': 4, 'seed': 42 } # 训练并输出最佳迭代次数 bst = xgb.train(xgb_params, dtrain, num_boost_round=500, evals=[(dtrain, 'train'), (dtest, 'test')], early_stopping_rounds=50, verbose_eval=False)逻辑说明:scale_pos_weight设置正负样本比例,让模型在训练时加大对少数类的惩罚力度。eval_metric用auc而不是logloss,因为逾期预测关心的不是概率绝对值,而是排序能力。early_stopping_rounds=50的意思是连续 50 轮验证集 AUC 不提升就停止训练,防止白白迭代浪费算力。
4.3 从 XGBoost 叶子节点到 LR 输入
树模型训练完成后,把原始特征矩阵转成叶子节点指示向量。这一步是 XGBoost+LR 融合的关键桥梁。它的原理是:每个样本经过一棵树,会落在某个叶子节点上,我们用 0/1 表示这个样本是否落在每个叶子节点上。多棵树组合起来就得到一个稀疏的高维特征向量。
# 获取每棵树叶子节点的位置 leaf_preds_train = bst.predict(dtrain, pred_leaf=True) leaf_preds_test = bst.predict(dtest, pred_leaf=True) # 对叶子位置做 one-hot 编码 from sklearn.preprocessing import OneHotEncoder enc = OneHotEncoder(handle_unknown='ignore') X_leaf_train = enc.fit_transform(leaf_preds_train) X_leaf_test = enc.transform(leaf_preds_test) # 用 LR 在叶子特征上做最终分类 lr = LogisticRegression(C=0.1, max_iter=500, solver='liblinear') lr.fit(X_leaf_train, y_train) y_pred_lr = lr.predict_proba(X_leaf_test)[:, 1]代码里注意两点:一是handle_unknown='ignore',测试集可能出现训练集中没见过的叶子组合,忽略而不是报错;二是 LR 的正则化系数C=0.1,这里设小一点是让正则更强,因为叶子特征维度比较高,过拟合风险也随之增大。
4.4 GBDT+LR 与 XGBoost+LR 的差异
GBDT+LR 用的树模型是lightgbm的LGBMClassifier,我在对比两个 notebook 后认为主要区别在工作效率:LightGBM 用直方图算法,训练速度快,内存占用小。如果数据量几万行,差异不大;如果几十万行,LightGBM 能省下大量时间。
import lightgbm as lgb lgb_model = lgb.LGBMClassifier( n_estimators=300, max_depth=5, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, scale_pos_weight=(y_train == 0).sum() / (y_train == 1).sum(), random_state=42 ) lgb_model.fit(X_train, y_train) leaf_train = lgb_model.predict(X_train, pred_leaf=True) leaf_test = lgb_model.predict(X_test, pred_leaf=True) # 后续 LR 训练逻辑与 XGBoost 版本一致注意predict(X, pred_leaf=True)返回的是二维数组,形状为(样本数, 树数量),而 XGBoost 的pred_leaf=True返回的是(样本数, 树的叶子索引)。两者输出维度含义略有不同,合并进 OneHotEncoder 前要先确认数组形状。
5. 避坑指南:这份源码运行中常见的四个翻车点
5.1 缺失值填充后模型效果反而变差
现象:不填充缺失值还能跑到 0.7 的 AUC,用中位数填充后掉到 0.68,而且特征重要性排序变得很奇怪。
原因:某些字段的缺失本身是业务信号,比如“收入缺失”很可能代表无稳定收入来源,这类客户恰恰是高风险群体。强制填充抹掉了这个强信号。
解决:先按缺失率分桶,缺失率高的字段单独新增一列is_missing标记列,原字段填充中位数。然后在模型里确认is_missing列是否进入高重要性区间。如果进入,说明缺失信息确实有效,必须保留。
5.2 pred_leaf 编码后 LR 维度爆炸
现象:训练 300 棵树,每棵树 30 个叶子,OneHotEncoder 后特征维度直接到 9000 多,LR 训练时间暴涨且结果不稳定。
原因:每棵树贡献一个字段的 one-hot,字段数量随着树的数量线性增长,而不是随样本量自适应。
解决:缩减树的数量到 100~200 棵,或者限制max_depth到 4~5 减少叶节点数。更实用的做法是只取前 N 棵树(比如重要性排序前 80 棵)的特征做融合,其余树丢弃。这样能在保持信息的前提下控制维度。
5.3 训练集与测试集特征不一
现象:训练集有 35 列,测试集只有 33 列;或者训练集测试集列名相同但顺序不同,合并时报 ValueError。
原因:特征工程阶段对训练集做了单独的列拼接,测试集没执行同样的代码。最常见的是pd.merge时键字段在测试集里被误删。
解决:把特征工程封装成函数,训练集和测试集走同一套流程。代码里明确列出必须保留的键字段,合并完成后用assert list(X_train.columns) == list(X_test.columns)强制校验。
def build_features(df): # 所有特征构造步骤 ... return feature_df X_train = build_features(train) X_test = build_features(test) assert list(X_train.columns) == list(X_test.columns), '特征列不一致,检查特征工程'5.4 提交格式不匹配导致评分报错
现象:submission.csv 的行数和测试集不一致,或者 ID 列存在重复和缺失,提交到评测平台后显示格式错误。
原因:测试集在数据清洗时删除过重复行,而提交模板仍然是原始测试集的行数;或者在reset_index后没有把索引列正确输出为 ID。
解决:提交前严格对照原始 submission.csv 的结构,只改预测概率列,ID 列完全复用。并且保证预测数组的行顺序与测试集一致,导出行前按 ID 升序排列。
6. 验证模型质量的三个技巧:从本地 AUC 到提交前检查
本地验证的逻辑和线上评分逻辑必须同步。这个项目里训练集带标签、测试集无标签,本地验证只能用切分训练集模拟。我习惯先固定随机种子,再按时间或按客户 ID 做分层切分,避免同一个人同时出现在训练和验证里造成信息泄漏。
第一,切分方式直接影响验证可信度。随机切分看到的 AUC 偏高,因为模型“记住了”一批相近样本的分布;时间切分更贴近真实场景,但样本量不够时方差大。我一般两种都跑,记录下差异幅度。如果差异超过 0.03,说明模型过拟合或者特征里有时间漂移,需要回头看特征。
第二,计算 PRAUC 比单纯看 AUC 更稳。逾期样本占比通常低于 5%,AUC 可能很好看但实际正样本召回率很低。PRAUC 对正样本占比变化更敏感,能反映少数类的分辨能力。
from sklearn.metrics import roc_auc_score, average_precision_score # 假设 y_val 是标签,y_pred 是 LR 输出的概率 auc = roc_auc_score(y_val, y_pred) pr_auc = average_precision_score(y_val, y_pred) print(f'AUC: {auc:.4f}, PRAUC: {pr_auc:.4f}')第三,提交前做一次完整的预测结果检查。对比提交模板的 ID 顺序,确认概率值范围在 0 到 1 之间,确认没有 NaN。最简单的方法是把 submission.csv 读回来,用isnull().sum()检查残差,再跑一次len(submission) == len(test)。
这套项目我拆过不少类似的,真正拉开分数的往往不是模型选型,而是特征工程的一致性和验证的严谨度。从那以后我每次做这类数据的整理,都强制走一遍“字段说明 → EDA → 特征工程封装 → 验证一致性检查”的流程,漏掉任何一步都会在提交阶段补课。希望这一套思路帮到你,把脱了线的代码跑成自己的东西。
本文还有配套的精品资源,点击获取