ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

银行客户逾期预测:机器学习风控建模实战项目

银行客户逾期预测:机器学习风控建模实战项目 简介这是一份基于机器学习的银行客户逾期行为预测毕业设计资源面向计算机相关专业准备毕设、课程设计或期末大作业的学生以及希望练习金融风控实战项目的学习者。压缩包共10个文件包含3个CSV数据文件、1个Excel字段说明、3个Jupyter Notebook分析/建模脚本、1个Python辅助脚本、使用说明TXT和配置Git忽略文件总大小127.3MB。数据侧涵盖训练集、测试集与提交样例字段说明清晰建模侧覆盖用户逾期EDA探索、XGBoostLR、GBDTLR三套完整Notebook可直接运行复现也能作为毕设核心代码扩展。项目经严格调试下载即可运行目前已有229人学习浏览适合用来快速搭建完整机器学习预测流程并撰写毕设文档。1. 银行客户逾期预测一个能直接跑通的风控机器学习入门项目做过信贷风控的人都知道最烧脑的问题不是把历史数据整理得多漂亮而是回答“这个客户下个月到底会不会逾期”。这个标题里的“银行客户逾期行为预测”就是一个标准的二分类问题用 Python 的 pandas 做数据清洗和特征工程用 sklearn 的逻辑回归和 LightGBM 建模型再用 AUC、KS 这些指标衡量模型能不能把坏客户从好客户里挑出来。它能解决的问题很具体在客户真正逾期之前提前圈定高风险名单让催收资源往该去的地方投。这个项目特别适合刚学完机器学习基础、想拿真实结构化数据练手的人也适合想从评分卡切入信贷风控的从业者。依赖只有 pandas、numpy、sklearn、lightgbm源码和全部数据都在压缩包里照着跑一遍比啃十遍理论更有用。2. 逾期预测本质是二分类先定义标签再决定用哪些字段很多人拿到数据的第一件事就是跑模型结果在验证集上漂亮得不得了一到新数据就翻车。原因多半是标签没定义清楚或者字段里混进了未来信息。这一章先把标签口径、时间窗口、字段选择这三件事定死后面建模才不会白忙。2.1 逾期标签怎么定义M0/M1/滚动逾期率别把口径用混银行说的“逾期”不是简单的一天没还而是有严格分期的。M0 表示未逾期M1 是逾期 1 到 30 天M2 是逾期 31 到 60 天以此类推。做行为预测时我们要预测的是“未来会不会进入 M1”而不是“当前是不是 M1”。如果直接用当前逾期状态当标签模型学到的是“他已经逾期了”毫无预判意义。常见的做法是用滚动逾期率或者未来 N 期是否出现 M1 作为正样本。比如用第 T 期的客户特征去预测第 T1 期是否出现至少一次逾期 30 天以上的行为。数据表里如果有多期账单就需要按客户 ID 聚合把“未来有没有逾期”打成一个标签。下面这段代码是标签构造的基本形态import pandas as pd # 假设原始数据里有这两个日期字段 # due_date: 账单应还日期 # repay_date: 实际还款日期 df[overdue_days] (df[repay_date] - df[due_date]).dt.days # M1: 逾期天数 30 记为 1 df[is_m1] (df[overdue_days] 30).astype(int) # 按客户聚合未来任何一个账单周期出现 M1这个客户就是正样本 label df.groupby(customer_id)[is_m1].max().rename(label) # 如果原始表是按月份拆成不同行的注意只用“未来”月份来打标签 train df[df[month] 2022-06] train train.merge(label, oncustomer_id, howleft)逻辑说明是overdue_days通过两个日期字段相减得到这是最直接的逾期天数is_m1是把数值转成二分类标志最后用groupby取最大值表示该客户在未来任意一期只要发生过 M1就算正样本。参数说明groupby(customer_id)[is_m1].max()的意思是“存在即正样本”适合逾期率不那么高的数据如果逾期很多可以改成“至少出现两次M1”这种更严的口径否则模型会把大量偶发逾期的人识别成高风险业务上不好解释。2.2 字段画像哪些原始字段直接入模哪些要加工银行客户表通常包含两类字段。一类是静态属性年龄、性别、职业、婚姻、学历、住房类型另一类是动态行为近三个月消费金额、信用卡额度使用率、历史最长逾期天数、近半年查询次数、账户余额、月收入等。静态字段大多可以直接入模但像“月收入”这种经常有缺失和极值需要做截断或分箱。动态字段里“额度使用率”比“消费金额”更有区分度因为它抵消了不同客户授信额度的规模差异。我一般会先做一个字段画像清单标注每个字段的类型、缺失率、唯一值个数、是否含未来信息。比如“当月还款金额”如果有实际到账的还款日期那么它出现在训练特征里就有问题——因为我们要预测的是未来不能用已经发生的结果去预测结果。把这类字段挑出来直接删除是建模前最重要的一步。2.3 训练集与时间窗口用 T 月特征预测 T1 月杜绝时间穿越时间窗口是风控建模最容易翻车的地方。如果直接把所有月份的数据随机切成训练集和测试集那么训练集里可能包含测试集客户的未来信息或者同一个客户在不同月份的数据同时出现在两边导致验证分数虚高。正确做法是严格按时间切分用历史某个月份的特征训练预测后面一个时间段的逾期情况。# 按月份划分训练集用T月验证集用T1月 train_month 2022-06 valid_month 2022-07 train feature_df[feature_df[month] train_month] valid feature_df[feature_df[month] valid_month] # 注意标签已经是“未来是否逾期”所以同一个月份的特征对应的目标在下一期 y_train train[label] X_train train.drop([label, customer_id, month], axis1) y_valid valid[label] X_valid valid.drop([label, customer_id, month], axis1)这里的核心是特征从2022-06提取而标签表示2022-07是否逾期两者之间天然隔了一个月的观察期。参数说明drop([label, customer_id, month], axis1)里的customer_id不能当特征否则模型会记住客户 ID 而不是学习规律month也不能入模因为未来的测试月份它可能从未出现过。3. 特征工程与数据清洗把原始客户表变成能喂给模型的 X特征工程做得好不好比模型选择更影响结果。LightGBM 能处理缺失值但那不代表缺失值不用管逻辑回归则对缺失和量纲极其敏感。这一章给出完整的数据清洗和特征筛选流程每一步都可以直接抄。3.1 缺失率超 40% 直接砍掉剩余字段用中位数和众数填充银行数据缺失很常见不是因为系统没记录而是字段本身在部分客户身上不适用。比如“配偶收入”对未婚客户天然缺失。处理原则是分两级缺失率超过 40% 的字段直接放弃因为补出来的信息噪声远大于信号缺失率在 5% 到 40% 之间的按数据类型填充——连续变量用中位数避免被极端值拉偏离散变量用众数保持分布形态。import pandas as pd import numpy as np # 计算缺失率 missing_rate df.isnull().mean() # 删除缺失率 0.4 的列 drop_cols missing_rate[missing_rate 0.4].index df df.drop(columnsdrop_cols) # 连续变量用中位数填充离散变量用众数填充 num_cols df.select_dtypes(include[int64, float64]).columns cat_cols df.select_dtypes(include[object]).columns for col in num_cols: df[col] df[col].fillna(df[col].median()) for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0])逻辑说明df.isnull().mean()得到每个字段的缺失比例然后按 0.4 阈值筛选。select_dtypes(include[int64, float64])是为了把数值列从 object 列里分出来避免对字符串做中位数。参数说明阈值 0.4 是经验值如果样本量特别大比如超过 100 万可以放宽到 0.5如果样本量只有几千条建议收紧到 0.3否则填充会导致大量噪声。mode()[0]取众数时如果出现并列pandas 会默认返回第一个最好先dropna()再取众数。3.2 离散变量做 WOE 编码还是 one-hot样本量少时优先用 WOE离散变量入模有两种主流做法one-hot 和 WOE 编码。one-hot 简单但遇到类别特别多的字段比如“职业”有几十种会生成几十列稀疏特征逻辑回归容易过拟合。WOE 编码把每个类别的坏样本占比与好样本占比的比值转成一个数值既能表达单调性又不会增加维度。WOE 的计算公式是ln(坏样本占比 / 好样本占比)。实际应用中先分箱再对每一箱计算。下面这段代码实现了一个简化版def woe_encode(df, feature, target): # 先按特征取值分组统计好、坏样本数 grouped df.groupby(feature)[target].agg( totalcount, badsum ) grouped[good] grouped[total] - grouped[bad] # 加平滑项避免除零 grouped[bad_pct] (grouped[bad] 1) / (grouped[bad].sum() 1) grouped[good_pct] (grouped[good] 1) / (grouped[good].sum() 1) grouped[woe] np.log(grouped[bad_pct] / grouped[good_pct]) # 把 WOE 映射回原表 return df[feature].map(grouped[woe]) df[occupation_woe] woe_encode(df, occupation, label)逻辑说明groupby(feature)[target].agg(totalcount, badsum)统计每个类别下的总样本数和坏样本数good是好样本数。平滑项1防止某个类别没有坏样本或好样本时算出无穷大。参数说明WOE 值越大表示该类别坏样本占比越高也就是风险越高这个方向性在后续建模时要注意如果把 WOE 直接喂给逻辑回归那正系数就对应高风险解释起来很方便。LightGBM 这类树模型对单调性不敏感但 WOE 编码依然能减少维度提升训练速度。3.3 用 IV 值和相关性过滤特征控制入模维度不是数越多越好特征不是越多越好。相关性过高的特征会让逻辑回归的系数不稳定IV信息价值过低的特征对区分好坏样本没有贡献。IV 值可以直接由 WOE 计算而来每个分组的(坏占比 - 好占比) * WOE加起来就是 IV。一般经验是IV 小于 0.02 的字段直接丢弃0.02 到 0.1 之间弱预测力0.1 到 0.3 中等大于 0.3 说明有强预测力但也容易过拟合。def cal_iv(df, feature, target): grouped df.groupby(feature)[target].agg(badsum, totalcount) grouped[good] grouped[total] - grouped[bad] grouped[bad_pct] grouped[bad] / grouped[bad].sum() grouped[good_pct] grouped[good] / grouped[good].sum() grouped[woe] np.log((grouped[bad_pct] 1e-6) / (grouped[good_pct] 1e-6)) grouped[iv] (grouped[bad_pct] - grouped[good_pct]) * grouped[woe] return grouped[iv].sum() # 对所有离散特征计算 IV candidate_cols [gender, education, occupation, has_house] iv_dict {col: cal_iv(df, col, label) for col in candidate_cols} keep_cols [col for col, iv_val in iv_dict.items() if iv_val 0.02]逻辑说明IV 的计算依赖 WOE它衡量的是“这个特征所有分组区分好坏的累计能力”。1e-6是为了数值稳定。参数说明iv_val 0.02是一个可调的阈值如果入模特征还是太多可以把阈值提高到 0.05如果业务上特别看重某个特征比如“征信查询次数”即使 IV 只有 0.018也可以保留但要接受一点点噪声。4. 用 LightGBM 和逻辑回归搭一个 baseline代码与参数说明这一章直接给两套可跑的模型代码一套是逻辑回归作为可解释性基线一套是 LightGBM作为精度基线。两套模型的结果可以互相印证如果差异过大大概率是特征或标签出了问题。4.1 逻辑回归作为可解释性基线先跑通再谈优化逻辑回归的优点是系数直接对应每个特征的风险方向风控业务方最爱看。缺点是它对特征线性关系敏感而且不能自动处理非线性。所以用逻辑回归时连续变量最好先做分箱或标准化离散变量用 WOE 编码。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import roc_auc_score # X 已经是 WOE 编码和数值填充好的特征集 # 标准化让所有特征在同一个量纲下 scaler StandardScaler() X_scaled scaler.fit_transform(X_train) # 注意使用 class_weight缓解样本不平衡 lr LogisticRegression(C1.0, class_weightbalanced, max_iter1000) lr.fit(X_scaled, y_train) # 验证集同样用训练集的 scaler 转换 X_valid_scaled scaler.transform(X_valid) y_pred_prob lr.predict_proba(X_valid_scaled)[:, 1] auc roc_auc_score(y_valid, y_pred_prob) print(fLR AUC: {auc:.4f})逻辑说明StandardScaler把连续特征变成均值 0 方差 1逻辑回归的梯度下降更快收敛系数也可以直接比较相对大小。class_weightbalanced让模型自动给少数类更高的权重这个参数在正样本占比低于 20% 时基本必开。参数说明C1.0是正则化强度的倒数C 越小正则越强如果验证集 AUC 比训练集低很多比如差 0.1 以上说明过拟合把 C 调到 0.1 或更小。max_iter1000是预防默认迭代次数不够导致收敛失败。4.2 LightGBM 做精度基线关键参数一次说清逻辑回归拟合的是线性关系而 LightGBM 是梯度提升树能自动捕捉交互效应和非线性。模型精度通常比逻辑回归高几个点但需要调参。下面这份参数是我常用的快速配置适合万级到百万级样本。import lightgbm as lgb from lightgbm import early_stopping lgb_params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, max_depth: -1, min_child_samples: 20, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 1.0, verbosity: -1, seed: 42, } train_data lgb.Dataset(X_train, labely_train) valid_data lgb.Dataset(X_valid, labely_valid, referencetrain_data) model lgb.train( lgb_params, train_data, num_boost_round1000, valid_sets[valid_data], callbacks[early_stopping(stopping_rounds100)], ) y_pred_prob_lgb model.predict(X_valid, num_iterationmodel.best_iteration) auc_lgb roc_auc_score(y_valid, y_pred_prob_lgb) print(fLGB AUC: {auc_lgb:.4f})逻辑说明objectivebinary表示二分类metricauc让训练过程直接关注 AUC。learning_rate0.05控制每棵树贡献的缩放越小越稳但需要更多树。num_leaves31是树模型的复杂度核心值越大越容易过拟合对于几万样本可以降到 15。feature_fraction0.8表示每棵树随机选取 80% 的特征类似随机森林的行采样能防止特征间强相关导致的过拟合。early_stopping(stopping_rounds100)让模型在验证集 AUC 连续 100 轮不提升时停止避免跑满 1000 轮后过拟合。参数说明bagging_fraction和bagging_freq1配合使用表示每轮迭代前随机抽 80% 样本这在大样本下能明显提升泛化能力lambda_l21.0是 L2 正则和逻辑回归里的 C 作用类似。4.3 样本不平衡处理过采样、欠采样、class_weight 到底选哪个逾期行为天然是少数正样本占比常常不到 10%甚至只有 3%。如果直接建模模型会学成“反正预测不逾期也能有 97% 准确率”从而把所有人都判成好客户。解决方式有三种调整样本权重、欠采样、过采样。我一般第一选择是class_weightbalanced或者 LightGBM 里的scale_pos_weight因为它不改变样本分布只是在损失函数里放大少数类的错误代价。当正样本占比在 5% 到 20% 之间时这个方法足够。如果正样本占比极低比如 1%光调权重不够需要结合欠采样把好样本随机抽到坏样本的 4 到 5 倍。过采样比如 SMOTE在风控场景里我不是很推荐因为银行客户数据噪声大人为生成合成样本会引入业务上不存在的客户组合反而让模型学到虚假规律。# LightGBM 里常见的做法根据正负样本比例设置权重 scale_pos_weight len(y_train[y_train 0]) / len(y_train[y_train 1]) lgb_params[scale_pos_weight] scale_pos_weight # 或者直接对样本进行欠采样 from sklearn.utils import resample neg_data X_train[y_train 0] pos_data X_train[y_train 1] # 控制正负样本比 1:5 n_samples min(len(pos_data) * 5, len(neg_data)) neg_data_sampled resample(neg_data, n_samplesn_samples, random_state42) X_train_bal pd.concat([pos_data, neg_data_sampled]) y_train_bal pd.concat([y_train[y_train 1], y_train[y_train 0]] * 0) # 示意逻辑说明scale_pos_weight的值是负样本数除以正样本数比如负样本占 90%正样本占 10%这个值就是 9意味着模型把每个正样本的错误权重放大 9 倍从而更关注少数类。resample是 sklearn 提供的无放回采样工具这里用的思路是只对多数类做欠采样。参数说明正负比控制在 1:5 到 1:10 之间比较稳妥如果欠采样太狠比如 1:1模型容易过拟合到少数类线上坏客户抓到很多但好客户被误杀的比例也会急剧上升。5. 避坑与常见问题不平衡、过拟合、时间穿越是三个最大翻车点这个项目的源码和数据包我见过很多人跑完说“AUC 0.95”结果拿到新数据只有 0.7。下面这几条踩坑记录每一条都是真实发生过的问题按“现象、原因、解决”来写希望能帮你少走弯路。5.1 模型 AUC 高得离谱原因是用到了未来信息现象训练集和验证集深分都超过 0.95但上线后预测的逾期名单基本是乱的。原因特征里混进了目标月份之后才产生的字段。最常见的是把“还款记录”放进特征或者标签和特征来自同一个月的数据模型实际上是在用已经发生的结果预测同一个结果。解决严格按时间切分特征和标签。特征用 T 月月末的存量信息标签用 T1 月的新增逾期事件。检查每个特征字段的统计口径凡是需要“未来”才能计算出来的字段一律删除。如果数据集是跨月多期的还要按客户 ID 做去重避免同一个人出现在不同时间的训练集和验证集里。5.2 准确率 97%但坏客户一个都没抓出来现象模型在测试集上准确率 97%业务方很开心结果把预测为不逾期的客户翻出来看里面有大量实际逾期的人召回率不到 20%。原因正负样本严重不平衡。模型全部预测为“不逾期”也能拿到 97% 准确率所以它选择了最简单的路径。解决不要只看准确率要看 AUC、召回率、精准率。用class_weightbalanced或者scale_pos_weight强制模型关注少数类。如果采用了欠采样要在验证集上保持原始的正负样本比例否则验证集上的精确率是虚高的。5.3 训练 1000 轮后 AUC 下降越训越差现象LightGBM 随着迭代轮数增加验证集 AUC 先升后降训练集 AUC 一直涨。原因过拟合。树模型学习能力太强把训练集中的噪声也记住了。解决使用early_stopping设置stopping_rounds100让它在验证集指标不再上升时停下来。同时降低num_leaves比如从 31 降到 15减少单棵树的复杂度把learning_rate从 0.05 降到 0.01配合更多迭代轮数通常能提升稳定性。5.4 换一个随机种子AUC 波动超过 0.03现象同一个数据集只是把random_state从 42 改成 2024AUC 就变了 0.03 甚至更多。原因样本量不够或者训练集和验证集划分不稳定。在几万样本下正样本本身就只有几千条随机切分时正样本分布不均匀。解决改用时间切分而不是随机切分这样每个 seed 下的训练集和验证集都是固定的。如果必须随机切分就用分层抽样stratifyy保证正负样本比例一致。还可以做多次 seed 融合把不同 seed 下训练模型的预测概率取平均效果通常比单模型稳定。5.5 验证集 AUC 不错但按月份回测时波动巨大现象整体测试集 AUC 0.83但拆开每个月单独测AUC 从 0.75 到 0.89 乱跳。原因部分月份受季节、政策、催收策略调整影响客户还款行为发生了变化。模型在 A 月学到的规律放到 B 月可能不再适用。解决不要只看整体 AUC按月份输出 KS 和 AUC 曲线观察模型稳定性的漂移方向。如果某个月的 AUC 特别低检查该月特征分布是否异常比如大促、节假日导致消费金额陡增。上线后期监控 PSI群体稳定性指数超过 0.1 就要重新训练模型。6. 上线前必做的验证KS、回溯测试和解释性核对模型训练完只是第一步真正能上线还需要回答三个问题能不能把坏人排到前面、换一批客户还稳不稳、业务解释是否合理。这一章给出三个必须完成的验证动作。6.1 用 KS 和 PSI 判断排序能力与稳定性阈值按业务定AUC 衡量整体排序能力但信贷业务更关心在某个分数段上的区分度。KS 统计量是累计好样本比例与累计坏样本比例的最大差值一般超过 0.3 就认为模型可用超过 0.5 要警惕过拟合。PSI 是衡量新老数据特征分布偏移的指标PSI 小于 0.1 代表稳定0.1 到 0.25 代表轻度偏移大于 0.25 则说明特征分布发生显著变化模型大概率要失效。用 sklearn 的roc_curve可以轻松画出 KS 的取值点。6.2 回溯测试用历史月份滚动模拟上线不要只做一次训练集、验证集的切分要模拟上线后的真实节奏。比如用 1 到 6 月的数据训练预测 7 月的逾期再把 1 到 7 月的数据训练预测 8 月滚动跑 6 次。每次记录 AUC、KS、前 10% 客户的逾期率。这能看出模型是否随着时间推移而衰减。如果后几个月的指标明显下降说明特征或标签口径在变上线前要重新做特征筛选。6.3 用 Shapley 值核对单个客户的风险解释业务方一定会问为什么给他高分这时候黑匣子的 LightGBM 也要给出解释。shap.TreeExplainer可以为每个客户的预测值分解出每个特征贡献的正负。我一般会挑选预测概率最高的 20 个客户看它们的特征归因如果发现有“年龄大反而风险贡献高”这种不符合业务经验的逻辑要么特征有问题要么模型学到了偶然相关。这个步骤不需要对每个客户做但至少抽查高风险和低风险各 20 个样本保证模型方向和业务常识不冲突。我的习惯是模型上线前先做一轮滚动回测回测时不看整体 AUC只看每个月的坏客户捕获率排名。只有每个月的表现都过得去才敢放上线。这个过程很繁琐但能帮你躲过无数个“当时看着挺好第二天被业务骂回来”的深夜。希望这篇笔记能帮你把这个逾期预测项目稳稳跑通少踩几个坑。本文还有配套的精品资源点击获取
返回列表