简介面向银行客户逾期行为预测这一典型风控场景提供一套可直接运行的Python机器学习毕业设计源码与完整数据。资源聚焦信用风险评估中的二分类问题通过客户历史交易与属性数据构建预测模型适合计算机、数据科学等相关专业学生用于毕业设计、课程设计、期末大作业也适合希望掌握机器学习项目完整流程的实战学习者。压缩包内含10个文件主要由CSV格式的训练集、测试集与提交样例xlsx字段说明文档以及3个可执行的Jupyter Notebook组成——分别完成用户逾期数据EDA探索、XGBoostLR与GBDTLR模型构建此外还提供Python辅助脚本和txt使用说明帮助用户快速了解数据结构、运行顺序与输出格式。整体压缩包约127.3MB项目已经导师指导并通过严格调试解压后按说明即可运行。当前已有229人学习对准备答辩或沉淀建模经验者而言是一份涵盖数据探索、特征工程、模型对比与结果提交的完整参考。1. 银行客户逾期行为预测不是堆模型而是先解决“标签和特征”两件事银行客户逾期行为预测是风控领域出现频率最高的二分类问题之一。很多人拿到一份“python实现基于机器学习的银行客户逾期行为预测源码全部数据.zip”时第一反应往往是赶紧跑通模型、看AUC但真正决定这套方案能不能落地价值的是标签怎么定义、特征怎么对齐时间窗口、样本不均衡怎么处理。我在做信贷风控项目时见过太多空有高AUC的模型上线后效果崩盘也见过用简单逻辑回归配合好特征打平复杂模型的案例。下面的内容面向两类人想用Python上手机器学习的初学者以及需要把预测结果解释给业务方的分析师。我会按数据、特征、建模、评估、避坑的顺序把实现路径和踩过的坑讲透。2. 数据清洗与特征工程逾期预测的胜负手在数据不在算法2.1 拿到原始数据先做什么缺失值、时间戳和标签构造一份典型的银行客户逾期数据通常包含客户ID、申请时间、年龄、收入、负债、历史还款记录、逾期次数、征信查询次数以及一个“是否逾期”的标记。但直接把原始表丢给模型是跑不出好效果的。我一般先查三件事缺失值分布、日期字段格式、标签的统计口径。先看一段最小数据清洗代码import pandas as pd import numpy as np # 读取原始数据gbk 是银行导出数据的常见编码 df pd.read_csv(bank_customer_data.csv, encodinggbk, enginec) # 检查缺失率超过 80% 的列先移出特征候选 missing_ratio df.isnull().mean() drop_cols missing_ratio[missing_ratio 0.8].index.tolist() df.drop(columnsdrop_cols, inplaceTrue) # 日期字段统一转成 datetime后续特征都基于真实时间偏移 df[apply_time] pd.to_datetime(df[apply_time]) df[last_overdue_time] pd.to_datetime(df[last_overdue_time]) df[overdue_flag] df[overdue_flag].astype(int) print(f数据量 {df.shape[0]}剩余特征数 {df.shape[1]}逾期率 {df[overdue_flag].mean():.4f})这段代码解决三件事把缺失率过高的列先剔掉避免后续填充制造噪声把日期字段转成标准格式因为计算“申请时间距上次逾期的时间差”依赖它把标签统一成整数。参数上encodinggbk是银行导出的常见编码报错就换成utf-8或检查分隔符enginec能加速大文件解析遇到混合编码时需要换回python引擎。提示如果标签列直接用“历史上有没有逾期”模型学到的就是过去而不是未来务必先和业务方确认逾期观察窗口。这里要强调一个常见误区逾期行为预测里的“标签”不能简单等于“历史是否逾期”。更常见的做法是设定某个观察点看该点未来30天或90天是否发生逾期。如果原始数据的标签字段已经固化你需要先重算。# 原始数据里包含 obs_date观察点和 overdue_date逾期发生日期 df[overdue_in_30d] ((df[overdue_date] - df[obs_date]).dt.days 30).astype(int) # 观察点后超过30天的逾期不算当前窗口 df.loc[df[overdue_date] df[obs_date] pd.Timedelta(days30), overdue_in_30d] 0这段逻辑看着简单却是整个逾期预测里最容易泄漏的地方如果你用“已发生的逾期”去做特征模型学到的其实是结果而不是预测。我一般动手前会先问业务方逾期是首期逾期、滚动逾期还是自然人逾期口径不一样标签和特征就完全不一样。2.2 特征工程把信贷行为转成机器学习能吃的数值特征工程是银行客户逾期预测收益最高的部分。常见做法是把特征分成客户基本属性、收入负债、历史行为、外部数据几类。对风控来说比单列数值更有效的是“比率”和“差值”因为额度、收入这些原始数字在不同客户间量纲差异太大。# 基础特征额度使用率、收入负债比 df[credit_utilization] df[loan_balance] / df[credit_limit] df[debt_to_income] df[total_debt] / (df[annual_income] 1e-6) # 历史行为特征距上次逾期天数、近6个月逾期次数 df[days_since_last_overdue] (df[obs_date] - df[last_overdue_time]).dt.days df[overdue_count_6m] df[[overdue_m1, overdue_m2, overdue_m3, overdue_m4, overdue_m5, overdue_m6]].sum(axis1) # 离散特征学历、地区、申请渠道直接转 category cat_cols [education, region, channel] df[cat_cols] df[cat_cols].astype(category)这些特征有效是因为它们反映真实的偿债压力和行为稳定性额度使用率高的人往往现金流已经绷紧距上次逾期时间越短说明不良行为越近近6个月多次逾期的人大概率是“习惯性逾期”。参数上1e-6用来防除零也可以用np.finfo(float).eps。sum(axis1)在列名匹配上要小心一旦原始表里某个月份的列缺失结果会静默偏小。特征构造完我会做一次“未来信息审查”同一客户的多条申请记录里如果obs_date不同特征中不能混入后续发生的信息。否则就会制造机器学习的噪声数据不是随机噪声而是带有未来信息的伪特征训练集指标好得出奇上线立刻失灵。2.3 样本不均衡处理逾期样本太少时不要急着上SMOTE银行客户逾期率通常只有2%到10%这意味着训练集里正样本非常少。很多人第一反应是上SMOTE过采样但实际做下来SMOTE不是默认首选。过采样会让模型看到更多“人为凑出来的正样本”在真实分布上未必有效尤其在特征空间复杂、样本量少时合成样本会放大噪声。from sklearn.model_selection import StratifiedKFold from imblearn.over_sampling import SMOTE # 先在原始数据上建立分层交叉验证窗口 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) # 只在训练折内做SMOTE验证折保持真实分布 for train_idx, valid_idx in skf.split(X, y): X_train, X_valid X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid y.iloc[train_idx], y.iloc[valid_idx] smote SMOTE(random_state42, k_neighbors5) X_train_res, y_train_res smote.fit_resample(X_train, y_train)这段代码的关键点有两个用StratifiedKFold保证每一折正负样本比例与总体一致SMOTE只作用于训练折验证折绝对不能重采样。如果你先对全量数据做SMOTE再切分验证集里混入合成样本指标虚高部署时立刻现原形。参数上k_neighbors5是默认值正样本很少时调到3避免跨类别噪声样本被强行连接。那么什么情况下才适合SMOTE我自己的标准是正样本数量低于1000或者模型在原始数据上几乎学不到正例特征时先试过采样但一定要同时观察精确率和召回率的平衡而不是只看AUC。这个坑我在第5章还会展开。2.4 用时间序列切分代替随机切分避免未来信息泄漏银行逾期数据本质上带时间戳随机切分会把“同一时间段的人”同时分到训练和测试里模型看到的是同一时期的客户画像而上线后面对的是未来客户。如果宏观经济或政策变化模型就会水土不服。所以处理数据阶段就要决定切分方式。# 按 obs_date 排序取前70%作训练、后30%作测试 df df.sort_values(obs_date).reset_index(dropTrue) train_size int(len(df) * 0.7) train df.iloc[:train_size] test df.iloc[train_size:]切分后特征、标签的提取要严格分开feature_cols [c for c in df.columns if c not in [overdue_in_30d, overdue_date, obs_date]] X_train train[feature_cols] y_train train[overdue_in_30d] X_test test[feature_cols] y_test test[overdue_in_30d]这里最容易被忽略的是填充缺失值。如果你在切分前用全量mean()填充测试集信息就泄漏给了训练集。正确做法是# 先用训练集计算填充值再分别应用到训练集和测试集 fill_values X_train.median() X_train X_train.fillna(fill_values) X_test X_test.fillna(fill_values)按时间切分后前面70%客户训练后面30%客户模拟未来。训练集上计算的median()本质上来自历史测试集只是拿到历史中位数去填充新样本这才是合规的。3. 建模与训练从逻辑回归到提升树跑通逾期预测的主流程3.1 先建逻辑回归基线理解系数比追求精度更重要在搞复杂模型之前先跑逻辑回归有两个好处一是能快速发现特征泄漏逻辑回归对极端特征很敏感二是业务方需要“可解释的分数”逻辑回归的系数能直接讲故事。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler # 数值列单独做标准化类别列交给后续 one-hot num_cols [credit_utilization, debt_to_income, days_since_last_overdue, age, annual_income] scaler StandardScaler() X_train_num scaler.fit_transform(X_train[num_cols]) X_test_num scaler.transform(X_test[num_cols]) # 用逻辑回归训练基线 lr LogisticRegression(max_iter1000, C0.1, solverliblinear) lr.fit(X_train_num, y_train)这里C0.1对应更强的正则化对高维稀疏特征和噪声特征有抑制作用。solverliblinear在样本量大、特征维度不高时更快而且对L1/L2都支持。训练完我会把系数和业务逻辑对照一下。如果某个特征系数是负数但业务上正相关那就要考虑是不是存在特征泄漏或特征间互相矛盾。当然逻辑回归的问题也很明显特征之间的非线性交互能力弱比如“高额度使用率”和“近6个月逾期多次”同时出现时风险不是简单相加。这时候需要更灵活的树模型。3.2 用LightGBM提升效果核心参数与早停逻辑LightGBM是银行风控里最常见的机器学习模型之一速度快且能直接处理类别特征。下面是典型训练流程import lightgbm as lgb # 构造 Dataset指定类别特征 lgb_train lgb.Dataset(X_train, y_train, categorical_featurecat_cols) lgb_valid lgb.Dataset(X_test, y_test, categorical_featurecat_cols, referencelgb_train) params { objective: binary, metric: auc, learning_rate: 0.03, num_leaves: 31, max_depth: -1, min_child_samples: 20, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l1: 1.0, lambda_l2: 5.0, verbose: -1 } model lgb.train( params, lgb_train, num_boost_round1000, valid_sets[lgb_valid], callbacks[lgb.early_stopping(stopping_rounds100), lgb.log_evaluation(100)] )参数说明learning_rate0.03配合num_boost_round1000是常规搭配让模型每步走小步靠早停避免过拟合。num_leaves31是单棵树的复杂度对中小样本够用min_child_samples20防止叶子节点样本过少feature_fraction和bagging_fraction是列采样和行采样能显著减少过拟合lambda_l1/lambda_l2是正则化在银行小样本场景下建议都设上否则特征重要性会被高维离散特征带偏。训练完要看特征重要性但不要只看默认的split次数。我一般把gain重要性排序后和业务经验对照。比如“距最近一次逾期天数”排第一很合理但如果obs_date本身排进前五那说明时间泄漏了——观察点字段不应该成为预测因子。3.3 用五折交叉验证替代直接训练测试OOF分数更可信直接使用默认参数训练LightGBM可以跑出不错效果但要让模型稳定我会用5折交叉验证来评估参数组合。原因是单次训练测试切分的结果方差很大正样本少时某一折的坏样本分布偏移会误导你。from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) oof np.zeros(len(X_train)) for fold_idx, (tr_idx, va_idx) in enumerate(skf.split(X_train, y_train)): X_tr, X_va X_train.iloc[tr_idx], X_train.iloc[va_idx] y_tr, y_va y_train.iloc[tr_idx], y_train.iloc[va_idx] d_tr lgb.Dataset(X_tr, y_tr, categorical_featurecat_cols) d_va lgb.Dataset(X_va, y_va, categorical_featurecat_cols, referenced_tr) fold_model lgb.train( params, d_tr, num_boost_round1000, valid_sets[d_va], callbacks[lgb.early_stopping(50), lgb.log_evaluation(200)] ) oof[va_idx] fold_model.predict(X_va, num_iterationfold_model.best_iteration) print(fOOF AUC: {roc_auc_score(y_train, oof):.4f})这段代码输出的是 out-of-fold 预测结果oof把每一折验证集的预测拼在一起模拟“模型在没见过的数据上”的表现。后面第6章你会发现OOF分数还能用来做阈值选择、计算业务指标。调参时不要每次只看测试集AUC而要比较OOF AUC否则你在多次调参中等于在偷偷用测试集做选择最终评估会偏乐观。注意这里的np.zeros(len(X_train))需要提前保证X_train.index是连续的如果前面有过筛选建议先reset_index(dropTrue)再开始否则oof[va_idx]会错位。3.4 调参顺序先定学习率再看叶子数和正则化如果不追求极致精度上面的参数已经够用。但如果你还想压榨一下模型我建议按这个顺序调而不是直接网格搜索全部参数。第一步固定learning_rate0.05跑num_boost_round1000看最优迭代数落在哪里第二步调num_leaves好的范围是 16~63和max_depth联动num_leaves增大的时候正则化也要跟着加大第三步调feature_fraction和bagging_fraction它们对过拟合影响最直接最后才动min_child_samples和lambda_l2。每个参数单独调不要一上来就并行网格搜否则你根本不知道是谁在起作用。4. 模型评估与可解释性不要只看AUC要算业务账模型训练完很多人习惯只看AUC哪边高就选哪边。但银行风控模型是要算钱的一个线上决策错误可能意味着几十万坏账。所以这一章我会逐项拆开混淆矩阵、KS、SHAP、阈值和PSI。每个都对应着一个上线前必须回答的问题。4.1 混淆矩阵与KS风控场景更该看哪些指标在风控界AUC不是业务最关心的。一个逾期率3%的产品如果模型把几乎所有客户都预测为“不逾期”AUC也可能有0.7以上但真正能过滤坏客户的阈值完全没有。所以我会先看混淆矩阵再看KS、精确率、召回率。from sklearn.metrics import confusion_matrix # 拿到LightGBM在测试集上的预测概率 prob model.predict(X_test, num_iterationmodel.best_iteration) # 先看默认阈值0.5下的表现 pred_label (prob 0.5).astype(int) cm confusion_matrix(y_test, pred_label) tn, fp, fn, tp cm.ravel() precision tp / (tp fp) recall tp / (tp fn) print(f精确率: {precision:.3f}召回率: {recall:.3f})这里有一个非常关键的认知在逾期率只有5%的数据上0.5阈值几乎没有意义。你需要根据前5%、前10%的坏样本占比去选阈值。KS统计量是累计正例比例和累计负例比例的差能直观反映模型区分好坏客户的能力。一般风控认为KS在0.3以上可用0.4以上算优秀但要结合逾期率看逾期率极低时KS很难冲高。计算KS的代码很短但要注意排序逻辑def ks_statistic(y_true, y_prob): df pd.DataFrame({y: y_true, prob: y_prob}).sort_values(prob) df[cum_bad] (df[y] 1).cumsum() / (df[y] 1).sum() df[cum_good] (df[y] 0).cumsum() / (df[y] 0).sum() return (df[cum_bad] - df[cum_good]).max() print(fKS: {ks_statistic(y_test, prob):.3f})sort_values(prob)默认升序累积差最大值出现的点就是模型区分能力最强的分数切点。如果 KS 很低先不要调参回去检查特征和标签口径十有八九是特征没有对齐观察点时点。4.2 用SHAP解释模型为什么这个客户被判逾期银行风控模型上线后一定会被问“为什么拒绝这个客户”答案不能是“模型分数低”。SHAP是一种解释树模型的方法能给出每个特征对单个样本预测的贡献。处理“黑匣子”问题关键是先让业务方看到特征方向是否合理。import shap # LightGBM模型用TreeExplainer explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test[:1000]) # 取一个被预测为高风险的样本做解释 idx 5 shap.force_plot(explainer.expected_value, shap_values[idx], X_test.iloc[idx])TreeExplainer速度快但对超大规模数据要小心内存。如果你只需要解释单个样本可以直接传入一行数据不必对所有测试集计算。SHAP值加起来等于模型的输出概率经过logistic转换因此能说人话“该客户年收入5万贡献了0.12的逾期概率近6个月逾期3次贡献了0.25总评分从基准0.08上升到0.45。”注意shap_values在某些LightGBM版本里返回的是一个 list类别数2如果你取shap_values[1]才是正类方向的贡献。新版 shap 更推荐直接用shap_values explainer(X_test)对象接口。4.3 阈值选择从模型分到决策规则的落地方法模型输出概率不是最终决策。你要和业务方一起定一条线线之上拒绝客户线之下通过客户。此时要算两笔账漏过的坏账损失误拒的好客户流失收益。下面是一个简化版本的成本计算。from sklearn.metrics import confusion_matrix # 每个逾期客户的平均坏账损失 cost_bad # 每个好客户的平均利润 profit_good # 在网格上搜索最优阈值让净收益最大 thresholds np.arange(0.05, 0.5, 0.05) best_t, best_profit 0, -np.inf for t in thresholds: pred (prob t).astype(int) tn_, fp_, fn_, tp_ confusion_matrix(y_test, pred).ravel() net_profit tp_ * profit_good - fp_ * profit_good - fn_ * cost_bad if net_profit best_profit: best_t, best_profit t, net_profit print(f最优阈值: {best_t:.2f}净收益: {best_profit:.2f})这里我故意把公式简化通过的好客户产生的收益去抵拒绝错杀的损失逾期客户直接按坏账扣除。实际项目里你应该把产品利率、催收成本、资金成本都放进去。关键是别直接拿0.5当决策线而是在概率上搜索业务收益最大的线。搜索步长0.05够用样本量大时可以加密到0.01。4.4 上线前最后一道检查PSI群体稳定性指标模型上线后不是一劳永逸。银行客群会随宏观环境变化特征分布也会漂移。PSI人群稳定性指数衡量训练集和上线后实际客群的特征分布差异。当PSI超过0.25时就该重训模型。# 简化版PSI计算按十分位数分组 def psi(expected, actual, bins10): expected_percent np.histogram(expected, binsbins, range(0, 1))[0] / len(expected) actual_percent np.histogram(actual, binsbins, range(0, 1))[0] / len(actual) psi_value np.sum((actual_percent - expected_percent) * np.log(actual_percent / expected_percent 1e-6)) return psi_value # 用训练集OOF概率作为expected最新一批客群预测结果作为actual psi_score psi(oof, prob_new_data) print(fPSI: {psi_score:.3f})注意log里加1e-6避免某一区间概率为0时爆掉。实际项目中我会对每个关键特征单独算PSI而不是只算分数PSI。如果一个特征分箱跨月份剧烈变化即使模型总AUC没跌也要排查是不是数据采集口径变了。这一步能提前发现模型悄悄失效的苗头。5. 逾期预测最容易交学费的5个坑现象、原因与解决办法下面这5个坑每一个都有人在这类项目里真金白银交过学费。我按现象、原因、解决三步写方便你直接对照排查。5.1 训练集AUC很高上线后却失灵现象训练集AUC 0.95OOF AUC也有0.9但上线后每天的实际逾期命中率只有原来的三分之一。原因最常见的是时间泄漏。比如原始数据同时存在“申请时刻的负债”和“申请之后才发生的还款记录”特征工程时把后者也当成已知值或者标签定义里直接把未来30天逾期结果当成了特征筛选依据。解决回到特征构造阶段逐列问一句话“这个字段在观察点obs_date当天能不能查到”。不能查到的列全部删掉。然后用按时间排序的切分重新训练并用最新一批客群做回测。排查时可以先打印特征重要性和相关系数# 检查关键泄漏嫌疑字段和标签的相关性 leak_cols [repay_amount_latest, account_balance, total_overdue] for col in leak_cols: if col in df.columns: corr df[col].corr(df[overdue_in_30d]) print(f{col}: {corr:.3f}) # 如果相关性超过0.3大概率是泄漏需要回到特征表确认时点一个正常行为特征和标签的相关性很少能到0.3除非它本身就是未来结果。比如“已还款金额”和“未来是否逾期”高度负相关这在逻辑上就不对因为观察点当天你不可能知道客户后面还了多少钱。如果时间回溯困难至少用obs_date做分组切分避免同一时期客户同时落在训练和验证里。5.2 SHAP解释里出现了“未来余额”这种变量现象业务方看SHAP图时突然说“不对啊这个客户的余额是上个月才变的模型怎么用到了”原因特征工程把多个时间点的记录横向展开了但没对齐观察点。比如客户最新余额取的是整张表的最新一条记录而不是obs_date当天及之前的最后一条于是未来信息混进特征。解决在构造“最新值”类特征时要按obs_date做截断。用 pandas 的merge_asof可以处理# 使用 merge_asof 找到每个客户在 obs_date 当天的最新余额 merged pd.merge_asof( df.sort_values(obs_date), balance_df.sort_values(date), left_onobs_date, right_ondate, bycustomer_id, directionbackward )merge_asof的directionbackward表示取观察点之前最近的一条记录天然排除了未来数据。这个逻辑最好写成一个通用的“特征时点对齐函数”把每一张流水表都在obs_date处截断而不是简单取最新。过程虽然麻烦但能避免绝大多数黑匣子翻车。上线前再抽查10个被拒绝客户的SHAP图让业务方一起确认特征方向比事后解释诚实得多。5.3 SMOTE后在真实场景反而变差现象用了SMOTE训练集精确率漂亮但在测试集上精确率直接掉一半而且是莫名其妙地掉。原因先在全量数据上做SMOTE再切分或者对验证集也做了过采样。SMOTE合成样本在特征空间里连接了两个相邻样本如果验证集里也有合成样本模型等于“见过答案”。解决只对训练折做SMOTE验证折保持真实分布。更简单的是先不用SMOTE改用scale_pos_weight。在LightGBM里这样设置params[scale_pos_weight] (y_train 0).sum() / (y_train 1).sum()这个参数的含义是让模型在计算损失时把少数类的权重放大到与多数类接近不需要人为生成样本。实际项目里scale_pos_weight的效果通常比SMOTE稳定因为它不会改变特征空间的真实分布。如果用了SMOTE也要用原始分布的数据做OOF验证否则你看到的所有指标都不可信。5.4 LightGBM训练速度极慢而且每次结果波动大现象同一个params换一次随机种子特征重要性前几名就变了训练时间也从10分钟跳到40分钟。原因num_leaves调得太大min_child_samples太小导致树又深又碎或者类别特征没有声明成categoryLightGBM还在按数值处理白白增加计算负担。解决先用小数据集跑通固定随机种子。给一组保守参数params { learning_rate: 0.05, num_leaves: 31, min_child_samples: 50, feature_fraction: 0.7, bagging_fraction: 0.7, bagging_freq: 1, random_state: 42, }类别特征在lgb.Dataset里用categorical_feature显式声明并且确保类别列是全数值编码比如0、1、2而不是字符串。字符串类别会让LightGBM报错或者在自动转换时变成无意义的数值。调参时一次只动一个参数每轮都看OOF AUC和训练时间免得两个参数互相影响最后分不清是谁在起作用。5.5 模型分数和业务通过率对不上现象模型预测的逾期概率平均是0.08但实际业务里逾期率只有0.03模型分数明显抬高。原因训练集的标签口径和业务口径不一致比如训练集把“宽限期后还款”也标成逾期而业务方的逾期定义是“逾期90天以上”或者训练集来自一个高风险的测试客群和目标客群分布不同。解决和业务方重对齐标签口径重新筛选训练样本。然后计算分数PSI和关键特征PSI定位哪一个特征分布偏移最严重。下面是一个快速PSI核对方法def psi(expected, actual, bins10, min_value1e-6): expected_percent np.histogram(expected, binsbins, range(0, 1))[0] / len(expected) actual_percent np.histogram(actual, binsbins, range(0, 1))[0] / len(actual) return np.sum((actual_percent - expected_percent) * np.log((actual_percent min_value) / (expected_percent min_value))) # 对比训练集OOF分数和10月份上线客群分数 month_score_psi psi(oof, october_scores) print(f10月客群PSI: {month_score_psi:.3f})如果分数PSI大于0.25基本可以确认客群变了模型必须重训。如果只有某个特征PSI高比如收入分布变了可以只对这个特征做分箱校准不一定全量重训。这里的分箱要和训练时保持一样否则算出来的PSI没有可比性。6. 用OOF分数做最后的验证三个动作和一个调参技巧训练阶段的oof分数不要扔它是你最后一个能诚实的评估依据。我先做三件小事用OOF选阈值而不是测试集做特征稳定性检查最后用全量数据加早停迭代轮数再训练几轮。# 用OOF分数选阈值避免反复使用测试集导致评估失真 best_t select_threshold_by_profit(oof, y_train, thresholdsthresholds) print(fOOF最优阈值{best_t:.2f}) # 全量数据上继续训练轮数在早停最优迭代基础上增加20 final_model lgb.train( params, lgb.Dataset(X_train, y_train, categorical_featurecat_cols), num_boost_roundmodel.best_iteration 20 )这段代码里select_threshold_by_profit是你在4.3节里写的那个函数直接用OOF结果调用。为什么不用测试集选阈值因为你一旦在多次调参中用测试集选阈值测试集就不再代表“没见过的数据”最终上线评估会偏乐观。OOF是从5折交叉验证拼出来的每个样本的预测都来自没有见过该样本的模型用它选阈值更稳。第二个动作是特征稳定性检查。上线前把训练集按月份拆成两半分别计算每个特征的均值和方差再和最近一批线上数据对比。如果某个特征在线上缺失率超过训练时的5%多半是上游数据源出了问题要在规则里先拦截。第三个动作是固定随机种子LightGBM的bagging_fraction和feature_fraction依赖随机数不固定random_state你每次训练得到的特征重要性排序都不同业务方会怀疑模型不稳定。最后一个技巧是在全量训练时用交叉验证得到的最佳迭代数加20到50轮。全量数据比验证折更多模型还能再收敛一点但加太多就会过拟合。我一般加30轮然后在测试集上验证一次如果没有明显掉点就保留。我个人的血泪教训是在逾期预测项目里真正拉开差距的往往不是模型选了LightGBM还是XGBoost而是标签口径和特征时点。源码和数据包只能让你跑通流程能不能落到真实场景取决于你有没有花足够时间做时间序列切分和PSI校验。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?