
简介这份基于机器学习的银行客户逾期行为预测项目提供完整源码与配套数据专为计算机相关专业即将开展毕业设计、课程设计或需要项目实战的Python学习者打造。项目已通过导师指导并获高分经严格调试后可直接运行能够显著降低环境配置和代码修改成本。压缩包内共有十个文件整体大小约一百二十七兆字节主要包含三个数据表文件、三个交互式分析脚本、一个字段说明表、一个主程序脚本、一个使用说明文档以及版本控制忽略文件覆盖从数据探索、特征分析到模型训练与预测比较的完整流程。目前已有二百二十九人学习资源附带数据字典和可复现的实验笔记读者可据此理解银行客户逾期风险的关键影响因素并掌握极端梯度提升与逻辑回归、梯度提升树与逻辑回归等经典集成模型的实战调用方法。1. 银行客户逾期预测这套 python 源码加全量数据实际解决什么问题打开“python实现基于机器学习的银行客户逾期行为预测源码全部数据.zip”这个项目包第一反应通常不是“模型怎么调”而是“逾期到底按哪个口径定义、好坏样本占比多少、数据能不能按时间切干净”。这套源码加全部数据解决的正是从原始客户字段到可上线的二分类模型之间的完整链路数据清洗、特征构造、模型训练、效果评估与上线前验证而不是某一小段演示代码。适合两类人刚接触风控建模的开发者可以借此建立完整流程观有信贷业务经验但没写过 python 建模的分析师可以把业务直觉翻译成可执行的脚本。下面按实际落地顺序把数据、特征、建模、避坑和上线前验证一条线讲透。2. 数据准备与标签定义先处理三个决定模型生死的细节银行客户逾期行为预测这类项目数据往往不是一个“干净的表”在那里等你。拿到压缩包里的全部数据后第一件事不是跑模型而是搞清楚字段含义、时间跨度和标签口径。这三个细节如果没处理好后面所有结果都白搭。2.1 读入全部数据先看字段类型再让 pandas 干活我一般先用 pandas 读入数据同时把字段类型、日期列和可能存在的压缩格式一并处理掉。如果项目包里的数据是 zip 压缩read_csv 能直接读但要注意路径和编码。import pandas as pd df pd.read_csv( data/loan_data.csv, parse_dates[issue_date, obs_date], dtype{ loan_amnt: float32, annual_inc: float32, term: category, int_rate: float32, emp_length: float32, }, ) print(df.info()) print(df.head())读入后先看 df.info()它会把每列的非空数量和数据类型列出来这一步能快速发现哪些列被读成了 object、哪些字段缺失严重。dtype 参数里把金额、利率这类连续数值压成 float32能减少内存占用term 这类固定类别字段声明成 category后续 groupby 会更快。parse_dates 指定日期列避免后续排序时再去 to_datetime。这里最容易翻车的是把主键列或类别列误读成数值比如客户 ID 超长被读成 float所以读取后最好再检查一下 dtypes。2.2 缺失值处理策略三类字段不能共用一种填法逾期预测数据里的缺失值往往不是“随机没记”而是带有业务语义。直接看缺失率分布miss df.isnull().mean().sort_values(ascendingFalse) print(miss[miss 0.1])得到缺失率之后我会把字段分成三类分别处理。第一类是连续数值型如利率、收入、循环额度使用率缺失用中位数填充第二类是额度上限、账户余额类缺失填 0表示“没有这个账户”或“没有使用记录”第三类是工作经验、居住年限这类语义型字段缺失填 -1把“不披露”和“无工作经验”分开。# 连续数值字段中位数填充避免极端值影响 df[int_rate] df[int_rate].fillna(df[int_rate].median()) df[revol_util] df[revol_util].fillna(df[revol_util].median()) # 额度类字段缺失填 0同时保留缺失标记 df[total_rev_hi_lim] df[total_rev_hi_lim].fillna(0) df[is_rev_lim_missing] df[total_rev_hi_lim].isnull().astype(int) # 语义型字段缺失填 -1单独成类 df[emp_length] df[emp_length].fillna(-1)注意这里额度字段先填 0 再生成缺失标记因为后面构造“额度使用率”特征时缺失和真正为 0 的含义完全不同。如果一律用中位数填充等于把“没有额度记录”和“额度使用率处于平均水平”混为一谈模型会被误导。这种字段级别的填充策略就是项目里“全部数据”相比玩具数据集更值钱的地方缺失模式本身携带风险信息。2.3 正负样本统计与不平衡评估先算账再建模逾期预测几乎一定面临样本不平衡。先量化地看一下pos_rate df[target].mean() print(逾期样本占比: {:.2%}.format(pos_rate)) print(样本总量:, len(df)) print(坏客户绝对量:, int(df[target].sum()))如果坏样本占比低到 3% 以下这不仅是“准确率没意义”的问题而是模型能不能学到足够坏客户模式的问题。此时先不要急着 SMOTE 或随机欠采样我通常先做两件事第一记录原始好坏比后面设置 scale_pos_weight 或 class_weight 时要用第二按时间窗口切分验证集因为风控数据有很强的时间效应随机切分会让验证结果虚高。一句话总结这个阶段的重点别在不理解字段的情况下做任何填充更别拿到不平衡数据就盲目过采样。先弄清楚每个缺失值背后的业务含义再决定填什么这个顺序不能反。3. 特征工程把还款行为转成模型认得出的可靠信号建模之前特征工程决定上限。银行逾期预测的数据里最值钱的信号不是客户自己申报的收入和职位而是账户行为轨迹。把行为信息组织成模型能直接吃下的形态这一步做扎实LightGBM 随便跑几轮 AUC 就能上去做不好调参再猛也是原地打转。3.1 行为类特征为什么比静态申报字段更可靠客户在申请表上填写的收入、工作年限、学历属于静态申报信息它的特点是更新慢、可包装而且不同客户之间的可比性差。真正反映逾期风险的是行为数据循环额度用了多少、近几个月还款金额波动大不大、最近一期有没有逾期。行为类特征反映的是“客户当下有没有资金压力”这种压力在行为上会先于逾期出现所以对预测任务更可靠。这也是为什么拿到源码后不要先冲着一大堆原始列做特征选择而是先把行为字段找齐。常见可用的原始字段包括revol_bal 循环账户余额、revol_lim 循环额度上限、repay_amt 每期还款金额、delinq_2yrs 过去两年逾期次数、最近一次逾期距离现在的月份数。特征工程就是把它们组合成风险含义更强的指标。3.2 构造额度使用率与还款波动系数代码与参数额度使用率是信贷风控里最经典的行为特征代表客户对循环资金的依赖程度。还款波动系数则刻画还款行为是否忽高忽低资金链紧张的人往往还款金额不稳定。import numpy as np eps 1e-6 # 额度使用率当前余额 / 额度上限clip 到 0-1 df[utilization] df[revol_bal] / (df[revol_lim] eps) df[utilization] df[utilization].clip(0, 1) # 还款波动系数最近 6 期还款金额的标准差 / 均值 def volatility(x): tail x.tail(6) return tail.std() / (tail.mean() eps) df[repay_volatility] ( df.groupby(loan_id)[repay_amt] .transform(volatility) ) df[repay_volatility] df[repay_volatility].replace([np.inf, -np.inf], np.nan) df[repay_volatility] df[repay_volatility].fillna(0)加 eps 是为了避免除零clip 把额度使用率限制在 0-1 是因为临时额度调整可能让原始比值超过 1但这些离群点对模型没有边际贡献。tail(6) 的窗口按“月度还款”设定代表最近半年如果数据是按周更新的窗口换成 26 更合理这个参数要跟着数据频次走。replace 和 fillna 处理的是某段窗口内所有还款金额相同导致的零方差情况这类客户反而是低风险所以填 0 比填均值更合适。3.3 滚动统计特征用过去 3、6、12 期数据生成“最近状态”单期行为只能看到当下模型更需要“趋势”。滚动统计特征能把最近几个账龄窗口的变化趋势提炼出来。注意这里必须先按客户和观测时间排序再滚动否则窗口会把未来信息带进来。# 排序很关键同一客户的多期观测必须按时间递增 df df.sort_values([loan_id, obs_date]).reset_index(dropTrue) g df.groupby(loan_id) for w in [3, 6, 12]: df[frepay_amt_mean_{w}] ( g[repay_amt].transform(lambda x: x.rolling(w, min_periods1).mean()) ) df[frepay_amt_std_{w}] ( g[repay_amt].transform(lambda x: x.rolling(w, min_periods1).std()) )窗口 3、6、12 在月度数据里分别对应一个季度、半年、一年。min_periods1 让早期样本也有统计值但代价是早期窗口方差大后面建模时可以观察特征重要性判断是否需要设更高的 min_periods。滚动统计的 std 特征特别容易产生缺失因为长度为 1 的窗口标准差是 NaN需要统一 fillna否则 LightGBM 能处理缺失但逻辑回归会直接报错。滚动特征最大的坑是排序错误。如果直接用原始表顺序做 rolling等于把同一个客户不同时期的记录交错处理窗口里混入其他客户的数据特征完全失去意义。我习惯在排序后打印前几行确认 loan_id 和 obs_date 的排列顺序再做聚合这一步值得花两分钟。4. 建模与调参从逻辑回归基准到 LightGBM 的完整跑通特征造好后进入建模环节。很多教程喜欢直接上 LightGBM但实际落地时我会先跑一个逻辑回归当基准再用梯度提升树做主力。原因有两个逻辑回归能快速验证特征管道有没有 bug同时给业务方一个可解释性基线LightGBM 则在特征工程到位后把效果往上抬。4.1 选型理由逻辑回归当基准LightGBM 当主力逻辑回归在逾期预测里的角色被严重低估。它训练快、权重可解释还能用系数方向检查特征是否符合业务直觉。比如额度使用率的系数应该是正的如果训练出来是负的那大概率是特征构造或样本切分出了问题。但逻辑回归对非线性关系和特征交互不敏感在原始字段上很难把逾期客户和正常客户彻底分开。LightGBM 这类梯度提升树能自动处理特征交互对缺失值有原生支持训练效率也高十万级样本几分钟就能跑完。缺点是树模型的可解释性弱而且调参不当容易过拟合。所以我的做法是逻辑回归先跑通流程确认数据没有明显错误再切换 LightGBM 做最终模型。4.2 最小实现训练、预测、评价一套代码走完from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler import lightgbm as lgb feature_cols df.drop(columns[target, loan_id, obs_date]).columns.tolist() X df[feature_cols] y df[target] # 逻辑回归前需要标准化树模型不需要 scaler StandardScaler() X_scaled pd.DataFrame( scaler.fit_transform(X[feature_cols]), columnsfeature_cols, indexdf.index, ) X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.3, stratifyy, random_state42 ) lr LogisticRegression(max_iter2000, class_weightbalanced) lr.fit(X_train, y_train)train_test_split 里的 stratifyy 保证划分后好坏样本比例和全量一致class_weightbalanced 让少数的逾期样本获得更高误分类代价这是逻辑回归缓解不平衡最直接的手段。注意这里必须做标准化否则量纲差异大时逻辑回归收敛很慢max_iter 要相应调大。先跑通这个基准记录 AUC之后对照 LightGBM 的提升幅度。LightGBM 部分则不需要标准化直接用原始特征X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) ratio y_train.mean() / (1 - y_train.mean()) model lgb.LGBMClassifier( n_estimators800, learning_rate0.05, num_leaves31, min_child_samples20, scale_pos_weightratio, random_state42, ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], eval_metricauc, callbacks[lgb.early_stopping(100)], )scale_pos_weight 取训练集中负样本数除以正样本数这是和 class_weightbalanced 等价的 LightGBM 写法。eval_metricauc 让训练过程直接显示验证集 AUC早停 patience 设 100意思是连续 100 轮验证集指标不提升就停止避免无效训练。注意树模型用的是原始 X_train不需要标准化标准化对树模型没有帮助。4.3 调参顺序先修类别不平衡再动树参数很多人在 LightGBM 上一来就调 num_leaves、min_child_samples结果验证集提升不明显。我建议的调参顺序是固定的先确认 scale_pos_weight 或 class_weight 已经按样本比例设置这是解决类别不平衡的第一步再看 learning_rate 和 n_estimators 的组合把学习率降到 0.05 以下同时把迭代次数放宽到 800 以上最后才动 num_leaves 和 min_child_samples。常用参数范围和调整方向learning_rate 0.01-0.1越低越稳但训练轮数要增加num_leaves 15-63值越大模型越复杂也越容易过拟合min_child_samples 10-50限制每个叶子最少样本数防止叶子节点完全过拟合到个别样本feature_fraction 0.7-1.0每棵树随机抽特征比例略微降低训练集表现但能提高验证集泛化能力。每一步只动一个参数观察验证集 AUC 变化不要同时改两三个参数否则出了问题都不知道是哪个改坏的。5. 银行逾期预测最容易翻车的 5 个坑现象、原因与解决这一章写的都是实际操作里反复出现的问题每一条都是“现象先行”的复盘记录。已经跑过几个模型的老手看这些坑会很有共鸣。5.1 坑一标签泄漏——训练集 AUC 0.97验证集却只有 0.51现象模型在训练集上 AUC 高得离谱但一到时间切分的验证集上就塌方特征重要性排序第一的是某个名字看起来很像“逾期天数”的字段。原因有些字段本身是“逾期发生后”才会产生的信息比如当期逾期天数、催收记录、已还金额占应还比例。如果这些字段和 target 在同一个时间点生成模型等于直接看到了答案。这种情况下特征工程看似做得很好实际上是把未来信息偷偷塞进了训练数据。解决建立严格的时间边界。规则很简单预测观测日当月的逾期风险只能用观测日之前已经产生的字段。我在跑特征管道时会把每个特征标记一个“可计算时间点”任何特征的可计算时间都不能晚于样本的观测日。识别出可疑字段后直接丢弃重新训练再看验证集表现。5.2 坑二只用准确率评估被“好客户”带偏现象业务方拿着模型报告问准确率 93%为什么实际使用还是抓不住逾期客户。翻开混淆矩阵才发现逾期客户几乎全被预测成了正常客户准确率高完全是因为正常客户占比本来就大。原因逾期样本占比可能只有 5%-8%模型全部预测为 0 也能有 92% 准确率。准确率在这个场景里是一个会骗人的指标。解决换评估指标核心看 AUC、KS 和逾期客户的召回率。AUC 衡量排序能力不依赖阈值召回率衡量在固定阈值下能抓到多少坏客户。对业务方沟通时用“坏客户召回率”而不是准确率比如“在验证集上前 10% 的高风险客户覆盖了 65% 的逾期样本”这句话比准确率有意义得多。5.3 坑三随机切分代替时间切分验证集结果虚高现象同一套代码随机切分验证集 AUC 能到 0.80改成按时间切分后掉到 0.65。模型策略审阅时被打回理由是没有满足“样本外验证”。原因随机切分把不同时期的客户混在一起模型相当于偷看了未来一段时间的宏观趋势。信贷数据有强烈的时间效应客群构成、市场环境、审批政策都会随时间变化随机切分让“未来”的信息混入训练集。解决按时间排序后切分用前 80% 时间段训练后 20% 时间段验证。cutoff df[obs_date].quantile(0.8) train df[df[obs_date] cutoff] test df[df[obs_date] cutoff] print(train 时间范围:, train[obs_date].min(), train[obs_date].max()) print(test 时间范围:, test[obs_date].min(), test[obs_date].max())这个坑最隐蔽的地方是调参时如果一直用随机切分模型参数会过拟合到一个并不存在的“跨时间规律”上上线后效果必然打折。5.4 坑四缺失值一律填 0把“没有记录”和“值就是 0”搅在一起现象额度使用率特征出现大量值为 1 的样本模型把这一类客户全部判成高风险但实际业务里这些客户可能只是没有循环额度账户。原因额度上限缺失时被填成 0再用当前余额除以额度上限就得到了无穷大clip 之后变成 1。模型学到的是“额度使用率1 是高危”却不知道这个 1 是缺失造成的假象。解决缺失填充必须区分语义。额度类字段缺失填 0 的同时增加一个 is_missing 标记列让模型自己判断“没有额度记录”和“额度为 0”是不是不同的风险含义。数值型连续字段的缺失用中位数填充并保留缺失标记列。凡是被填充过的字段都应该考虑要不要补充一个缺失标记。5.5 坑五同一客户的多次贷款记录同时出现在训练集和验证集现象客户维度上有重复记录比如同一个客户有多笔贷款随机切分按行切导致同一客户的部分记录在训练集、部分在验证集。验证集 AUC 虚高到不可信。原因随机切分默认按样本行切忽略了数据之间的客户关联。同一客户的多次贷款行为高度相关模型等于在验证集里见到了“类似客户”评估结果偏乐观。解决先按客户 ID 分组再切分。cust_ids df[customer_id].unique() train_ids, test_ids train_test_split(cust_ids, test_size0.2, random_state42) train df[df[customer_id].isin(train_ids)] test df[df[customer_id].isin(test_ids)]业务上如果预测的是“客户未来是否会逾期”这个切分方式才是正确的如果预测的是“某笔贷款是否会逾期”按客户切分也比按行切分更保守可靠。判断标准是验证集里不能出现训练集里见过的客户。6. 模型上线前的体检用 KS 与 PSI 确认预测能力是否可靠模型在验证集上 AUC 不错不代表可以直接上线。落地前我还会做两个检查KS 看区分度是否真实PSI 看特征分布会不会随客群变化而失效。6.1 计算 KS 值一眼看穿模型区分度KS 衡量模型把好坏客户分开的能力比 AUC 更直观。计算方法是按预测分数从高到低排序累加好客户和坏客户比例的差值取最大差值。def ks_statistic(y_true, y_score): df pd.DataFrame({y: y_true, score: y_score}) df df.sort_values(score, ascendingFalse).reset_index(dropTrue) df[cum_bad] df[y].cumsum() / df[y].sum() df[cum_good] (1 - df[y]).cumsum() / (1 - df[y]).sum() return (df[cum_bad] - df[cum_good]).abs().max()训练集 KS 和验证集 KS 差距如果超过 0.1通常说明过拟合严重。我一般会同时打印训练集和验证集 KS如果训练集 0.45、验证集 0.42这个模型比较健康如果训练集 0.5、验证集 0.3就需要回头调参或检查时间泄漏。6.2 用 PSI 检查特征分布漂移上线前最后一道关卡模型上线一段时间后效果下降最常见原因是客群结构变了。PSI 可以量化这种漂移。def psi_calc(score_train, score_now, bins10): train_hist, edges np.histogram(score_train, binsbins, densityTrue) now_hist, _ np.histogram(score_now, binsedges, densityTrue) psi 0 for exp, act in zip(train_hist 1e-6, now_hist 1e-6): psi (act - exp) * np.log(act / exp) return psiPSI 小于 0.1 说明分布变化不大0.1-0.25 需要关注超过 0.25 基本可以判断发生了明显漂移。我在实践中会对每个入模特征都算一遍 PSI而不只是算模型分数如果发现单个特征漂移严重上线后的监测报表会优先提示这个字段。这类检查做完模型才算真正具备上线状态。我现在拿到任何 python 风控源码第一件事都不会急着调参而是先把时间窗口、客户 ID 和特征生成时间线画出来。这三条链路理清了后面可以避开大半的翻车事故。这也是这套源码里最值得学习的地方数据本身不会说话但特征工程和验证方式会决定你能否听到正确的信号。希望帮到你。本文还有配套的精品资源点击获取