简介:针对LendingClub数据集的金融反欺诈集成学习项目资料包,面向金融风控、数据挖掘方向学习者和从业者,解决P2P借贷场景下欺诈样本不平衡、模型泛化能力不足等实际问题。资源共3个文件,压缩包大小15.64MB,包含一个Jupyter Notebook代码文件、一个HTML格式的完整分析报告以及一份LoanStats_2017Q3原始CSV数据,可配套用于复现从数据清洗、特征工程到模型融合的全流程。内容围绕Bagging、Boosting、Stacking等集成方法展开,涵盖数据划分、弱预测器选择、超参数调优及ROC/AUC等性能评估,并针对欺诈检测中样本不平衡问题给出加权与AdaBoost等处理策略。已有1296人学习下载,通过Notebook代码与HTML报告,读者可快速掌握如何构造还款意愿、风险等级等衍生特征,并构建具备较强鲁棒性的反欺诈模型,适合作为金融数据挖掘实战的参考范例。
1. 用 LendingClub 数据集做反欺诈,为什么集成学习是首选?
信贷风控里,欺诈检测和信用评分是两条完全不同的技术路线。信用评分看的是“这个人未来还不还”,欺诈检测看的是“这笔申请本身是不是假的”。LendingClub 数据集在公开领域里是少有的、带完整申请信息和还款表现的信贷数据集,很多人拿它练手信用评分,但真正有区分度的用法,是把它改造成一个二分类任务,用集成学习去抓那些“看起来正常、实际上坏账”的样本。这个思路在线下风控团队里非常常见,因为 LendingClub 的字段里藏着大量可做交叉验证的特征,比如贷款目的、年收入、就业年限、负债率,这些字段组合起来,欺诈和正常的分布差异远比单特征明显。
本文要解决的是四件事:这个数据集到底怎么理解、怎么把原始 CSV 改造成监督学习样本、集成学习里三个主力模型(随机森林、XGBoost、LightGBM)怎么调参、以及实际跑起来之后最容易翻车的五个坑。适合两类人看:一类是刚入行想做风控建模的算法工程师,另一类是已经在做传统评分卡、想引入集成方法提升区分度的风控分析师。说实话,LendingClub 的数据分布相对“干净”,真实业务里的欺诈率比它高得多,但流程和方法是通用的——你在 LendingClub 上学到的特征工程和模型验证方法,换到自营信贷数据上依然成立。
2. 理解 LendingClub 数据集的字段与欺诈标签构造
2.1 原始字段拆解:哪些列是特征,哪些列必须删
LendingClub 的公开数据集从 2007 年到 2018 年,每个季度一个 CSV,字段数在 100 个以上。但绝对不是所有字段都能直接喂给模型,第一道工序就是分清楚字段的“出身”。我一般把字段分成五类:申请时点就确定的静态信息(比如 annual_inc、emp_length、home_ownership)、贷款本身的属性(loan_amnt、term、int_rate、installment)、借款人信用画像(delinq_2yrs、inq_last_6mths、open_acc)、贷款目的与用途描述(purpose、title、addr_state)、以及贷后才有的事实(loan_status、last_pymnt_amnt、recoveries、total_pymnt)。
这里有个新手最容易犯的错:把贷后字段直接当特征。比如 last_pymnt_amnt 和 total_pymnt,这两个字段在建模时点根本不存在,它们本身就是结果的一部分。如果你把它们放进特征,训练出来的模型区分度会虚高到 0.99 以上,但上线之后完全失效,因为真实预测时这些字段是空的。这属于典型的标签泄漏,我们在第三节会专门展开。
字段处理的另一个关键点是 id 和 member_id 这类唯一标识列,它们对泛化没有任何贡献,必须最先丢掉。title 字段和 purpose 字段高度冗余,title 是自由文本、purpose 是标准化枚举,经验上直接保留 purpose 把 title 丢掉更稳,因为自由文本的脏数据太多,比如大小写不一致、拼写错误,清洗成本高且收益不稳定。
2.2 标签构造:从 loan_status 到二分类欺诈标签
LendingClub 的 loan_status 字段不是现成的二分类,它包含 Current、Fully Paid、Charged Off、Late (31-120 days)、In Grace Period、Default 等多种取值。做反欺诈模型时,最常见的做法是只保留两类样本:Fully Paid 视为正常,Charged Off 或 Default 视为欺诈/坏账,其余中间状态直接丢弃。
这个映射听起来简单,但有一个业务陷阱:Charged Off 不代表一定是欺诈,它可能只是纯粹的信用恶化。真实业务里你会把欺诈和坏账分开建模,但 LendingClub 数据没有给“is_fraud”这个直接标签,所以退而求其次用 Charged Off 做代理标签(proxy label)。做的时候要记住:这是一个代理目标,模型学习到的是“这笔贷款最终变坏”的模式,而不一定是“申请材料造假”的模式。你可以通过特征重要性去观察,如果排名靠前的特征是 delinq_2yrs、inq_last_6mths 这类信用历史变量,说明模型更多学到了信用风险;如果排名靠前的是 employment_length、annual_inc 这类申请信息,说明更接近欺诈信号。
代码实现如下:
import pandas as pd df = pd.read_csv("loan.csv", low_memory=False) # 只保留明确的好与坏 df = df[df["loan_status"].isin(["Fully Paid", "Charged Off"])].copy() # 构造二分类标签 df["is_bad"] = (df["loan_status"] == "Charged Off").astype(int) # 删除标签泄漏字段和标识字段 leak_cols = ["last_pymnt_amnt", "total_pymnt", "total_rec_prncp", "total_rec_int", "total_rec_late_fee", "recoveries", "collection_recovery_fee", "id", "member_id"] df = df.drop(columns=leak_cols) print(df["is_bad"].value_counts(normalize=True))这段代码的逻辑非常明确:先过滤出好坏样本,再把坏样本标记为 1,最后删除所有贷后字段。打印出来的分布就是你的建模基线——LendingClub 全量数据上坏账率通常在 15% 到 20% 之间,如果你只拿某一年份子集,这个比例会浮动。注意 low_memory=False 这个参数,因为原始 CSV 列很多、类型混杂,Pandas 默认的按块读取方式可能把某些列自动推断成错误类型,关掉它一次性读取更稳。
2.3 特征工程:从单字段到交叉特征与 WOE 分箱
反欺诈模型的提升往往不在模型算法,而在特征工程。LendingClub 数据集里,最值得做的一类特征是“偿还能力与借款额度的匹配度”。比如 installment(月还款额)除以 annual_inc(年收入)得到还款压力比,dti(负债收入比)本身就在原始字段里,但它是申请时算好的,你可以用 loan_amnt 和 installment 重新构造一个“真实负担系数”。
另一个有价值的交叉特征是就业年限与收入的组合。原本 emp_length 是一个枚举值(<1 年、1 年、2 年……10 年+),可以直接转成数值,然后和 annual_inc 做比值,表示“每一年的工作经验对应多少收入”。这个特征的意义在于识别那些收入与职业资历明显不匹配的申请,通常这类是欺诈的高发区。还有一个做法是把 addr_state 和 purpose 交叉起来做 groupby 统计,得到“该州该贷款目的的历史坏账率”,这相当于给模型注入了区域风险先验,效果比单纯 one-hot 编码好得多。
WOE 分箱在信用评分领域是标准做法,虽然集成学习对线性关系不敏感,但 WOE 有两个额外好处:一是能把缺失值单独分到一箱,天然处理了空值;二是能把异常值压缩到有限的箱内,避免极端值干扰树模型的切分点。我用 WOE 编码通常只对连续变量做,比如 annual_inc、dti、revolving_util,离散变量直接保留原始枚举或者做 label encoding 就够了。
import numpy as np # 还款压力比 df["inst_income_ratio"] = df["installment"] / (df["annual_inc"] + 1) # 就业年限转数值 df["emp_length_num"] = df["emp_length"].str.extract(r"(\d+)").astype(float) df["emp_length_num"] = df["emp_length_num"].fillna(0) # 收入与经验匹配度 df["income_per_emp_year"] = df["annual_inc"] / (df["emp_length_num"] + 1) # 交叉统计特征:州 + 目的的历史坏账率 df["st_purpose_bad_rate"] = df.groupby(["addr_state", "purpose"])["is_bad"].transform("mean")这里的 +1 是平滑项,防止除零。income_per_emp_year 对失业或新入职的人会是一个很大的数,但后续树模型能自动找到合理切分点,不必提前做截断。st_purpose_bad_rate 是目标编码的一种,它用了全量数据的标签信息,在训练集内部计算会带来轻微过拟合,稳妥的做法是在训练集内用 KFold 交叉统计,但这个在样本量足够大(几十万行)时影响有限,一般直接 transform 也能接受。我建议你在做正式实验时用 KFold 版本,避免被人挑战数据泄漏。
3. 三个集成学习模型:随机森林、XGBoost 与 LightGBM 的选型与训练
3.1 为什么不用逻辑回归:非线性交互与自动特征选择
传统信用评分卡用逻辑回归加 WOE 转换,优势是可解释性极强,每一个特征的系数都能直接翻译成业务规则。但放到 LendingClub 这个场景下,逻辑回归有两个显著短板:第一,欺诈模式是高度非线性的,比如“低收入+高贷款额+短期就业”这种组合信号,逻辑回归需要手动构造交互项才能捕捉,而树模型天然能通过分裂找到这种组合;第二,逻辑回归对特征共线性敏感,而我们的交叉特征和原始特征之间大量相关,需要做 VIF 筛选,非常耗时。
集成学习在校准概率方面也不一定比逻辑回归差。XGBoost 和 LightGBM 都支持输出原始分数和概率,只要你不追求严格的概率校准,用 rank 排序做风控决策是完全够用的。实际落地时,风控系统通常只需要一个分数阈值,而不是一个精确的概率估计,这一点让集成模型的实用性大大增强。
选择哪个集成模型,取决于三个现实约束:训练时间、推断延迟、以及调参成本。随机森林是最稳的基线,几乎不用调参就能达到不错的效果;XGBoost 在中小数据集上表现最好,但对超参数敏感;LightGBM 训练速度最快,在几十万行数据上可以达到分钟级训练,适合快速迭代特征。我这里不是教你选一个“最好的”,而是建议三个模型都跑一遍,用验证集 AUC 和 KS 去选最终模型。
3.2 数据划分:时间序列切分比随机切分更接近真实风控
LendingClub 数据是按季度发布的,隐含了时间轴。反欺诈建模必须使用按时间切分的方式,而不是随机切分,因为样本之间存在时间依赖,随机切分会把未来信息泄漏给训练集,导致验证集表现虚高。
具体做法:按 issue_d(贷款发放月份)排序,前 80% 作为训练集,后 20% 作为验证集。如果你想做更严谨的验证,可以按时间滚动做 Walk-Forward Validation,比如用 2015 年全年训练、2016 年 Q1 验证,然后滑动窗口逐步前移。这个方法能评估模型在不同经济周期下的稳定性,对于信贷风控模型尤其重要。
df = df.sort_values("issue_d").reset_index(drop=True) split_idx = int(len(df) * 0.8) train = df.iloc[:split_idx].copy() valid = df.iloc[split_idx:].copy() feature_cols = [c for c in train.columns if c not in ["loan_status", "issue_d", "url", "desc"]] X_train = train[feature_cols] y_train = train["is_bad"] X_valid = valid[feature_cols] y_valid = valid["is_bad"]注意这里有一个细节:issue_d 本身不能作为特征,因为它在预测时点虽然已知,但它和样本的“时间先后”强相关,模型会学到“最近发放的贷款还没坏”这种假象。数据预处理阶段必须在 sort 之后、特征选择之前把 issue_d 剔出 feature_cols。另一个容易漏掉的是 url 和 desc 字段,desc 是借款人的自述文本,虽然可以做文本挖掘,但大多数业务场景下清洗成本高,我直接排除,url 是 LendingClub 页面链接,完全无意义。
3.3 训练代码:XGBoost 与 LightGBM 的参数解读与实测
XGBoost 和 LightGBM 在 LendingClub 这个规模的数据集上,训练脚本相对简单,重点在参数上。先看 XGBoost:
import xgboost as xgb xgb_model = xgb.XGBClassifier( n_estimators=500, max_depth=5, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_alpha=1.0, reg_lambda=2.0, scale_pos_weight=4.0, eval_metric="auc", early_stopping_rounds=50, random_state=42 ) xgb_model.fit( X_train, y_train, eval_set=[(X_valid, y_valid)], verbose=False )这里最关键的参数是 scale_pos_weight。LendingClub 数据中坏样本只有 15%-18%,正负样本比例严重失衡。如果你不设置这个参数,模型会倾向把所有样本预测为负类,AUC 可能还行,但召回率很低。scale_pos_weight 设为负样本比例的倒数,通常在 3 到 5 之间比较合理,参数不是越大越好,过大会导致误杀率升高。
max_depth 和 learning_rate 是另外一个平衡:max_depth=5 配合 learning_rate=0.05 通常比 max_depth=8 配合 learning_rate=0.3 更稳,因为深度越深模型越容易记住训练集中的噪声,而小学习率配合多棵树能慢慢逼近目标并且有早停兜底。colsample_bytree=0.8 给每棵树随机抽 80% 特征,这能提升多样性,尤其在特征之间相关性较高时效果明显。
LightGBM 的写法类似,但有几个参数不一样:
import lightgbm as lgb lgb_model = lgb.LGBMClassifier( n_estimators=500, max_depth=-1, num_leaves=31, learning_rate=0.05, subsample=0.8, subsample_freq=1, colsample_bytree=0.8, reg_alpha=0.5, reg_lambda=2.0, scale_pos_weight=4.0, min_child_samples=50, random_state=42 ) lgb_model.fit( X_train, y_train, eval_set=[(X_valid, y_valid)], eval_metric="auc", callbacks=[lgb.early_stopping(50)] )LightGBM 的 max_depth=-1 表示不限深度,控制复杂度靠 num_leaves。num_leaves=31 已经比 XGBoost 的默认 6 层可以表达的复杂度高得多,对 LendingClub 这个规模足够。min_child_samples=50 是防止叶节点样本过少导致过拟合的关键参数,在 LightGBM 里比 max_depth 更重要。subsample_freq=1 表示每轮都重新采样,默认是 0 即不进行行采样,如果你不设置 subsample_freq,subsample 参数是不生效的——这个坑很多老手都会踩。
3.4 随机森林做基线:n_estimators 与 class_weight 如何设
随机森林在这个项目里的定位是基线模型,作用是给你一个“最差也能达到”的下限。它的最大优势是几乎不需要调参,唯一必须调整的是 class_weight 参数,否则同样的失衡问题会让模型偏向多数类。
from sklearn.ensemble import RandomForestClassifier rf_model = RandomForestClassifier( n_estimators=300, max_depth=15, min_samples_leaf=50, class_weight="balanced_subsample", n_jobs=-1, random_state=42 ) rf_model.fit(X_train, y_train) print("RF train AUC:", roc_auc_score(y_train, rf_model.predict_proba(X_train)[:, 1])) print("RF valid AUC:", roc_auc_score(y_valid, rf_model.predict_proba(X_valid)[:, 1]))class_weight 的取值有三种:balanced、balanced_subsample、以及自定义字典。balanced_subsample 比 balanced 更适合随机森林,因为它在每个 bootstrap 样本上重新计算权重,与随机森林的采样机制更搭。n_estimators=300 在几十万行数据上已经足够收敛,继续增大只会拖慢速度,收益微乎其微。min_samples_leaf=50 是过拟合控制的核心,它强制每个叶子至少包含 50 个样本,这样学到的规则不会太碎片化,泛化更稳。
随机森林的训练时间明显比 XGBoost 和 LightGBM 长,这里用 n_jobs=-1 让所有 CPU 核并行,在数据量大时能节省不少时间。跑完三个模型后,对比它们的验证集 AUC,你会发现随机森林通常比 XGBoost 低 0.01 到 0.02,但优势是训练稳定、不太受种子影响。如果你需要快速验证一个新特征是否有效,先用随机森林跑一遍,它的波动小,特征的重要度排名也更稳定。
4. 模型评估与调优:AUC、KS、混淆矩阵与特征重要性
4.1 风控语境下的评估指标:不只盯着 AUC
很多初学建模的人只报一个 AUC,这在风控业务里是远远不够的。AUC 衡量的是排序能力,但它不告诉你阈值选在哪、不同阈值下的误杀率是多少。风控里最常用的两个指标是 KS 和 Recall@Precision,前者衡量模型对好坏样本的区分度,后者直接对应业务成本。
KS 的计算方式是对所有样本按预测分数排序,然后计算累计好样本比例与累计坏样本比例的最大差值。KS 的常见取值范围是这样:小于 0.2 模型基本不可用,0.2 到 0.4 可以上线并逐步优化,0.4 以上已经很好。LendingClub 数据上做一个合理的集成模型,KS 一般在 0.4 到 0.5 之间,如果超过 0.6,我建议你检查一下是否发生了标签泄漏。
混淆矩阵的价值在于让你直观看到误杀率。假设你的验证集有 6 万样本,坏样本约占 15%,模型在 30% 的通过率上捕获了 70% 的坏样本,这意味着你要拒绝 30% 的申请来换 70% 的召回。这个数值是否可接受,完全取决于业务对“错杀好人”和“漏掉坏人”的容忍度,本身没有标准答案。
from sklearn.metrics import roc_auc_score, confusion_matrix, classification_report y_pred_proba = xgb_model.predict_proba(X_valid)[:, 1] print("Valid AUC:", roc_auc_score(y_valid, y_pred_proba)) # 按分数阈值做决策 threshold = 0.5 y_pred = (y_pred_proba >= threshold).astype(int) print(confusion_matrix(y_valid, y_pred)) # 尝试更高阈值,提升召回 threshold_high = 0.3 y_pred_high = (y_pred_proba >= threshold_high).astype(int) print(confusion_matrix(y_valid, y_pred_high))阈值的选择不是从模型里学出来的,而是从业务约束里倒推的。你先把验证集按预测分数分桶,画出每个桶的坏账率,然后找到“坏账率超过业务容忍线”的分数点,把阈值定在那个位置。这个做法比直接用 0.5 要合理得多,因为集成模型的输出概率并不严格等于真实违约概率,它只保证排序是对的。
4.2 特征重要性:从默认 gain 到业务可解释的维度
XGBoost 和 LightGBM 都提供了两种特征重要性:gain 表示该特征在所有分裂中带来的平均信息增益,weight 表示该特征被用于分裂的次数。在 LendingClub 场景下,我建议你用 gain 为主。weight 容易被高基数特征带偏,比如 addr_state 有 50 个取值,树会频繁拿它做分裂,但它对整体模型的增益可能并不高。
importance_df = pd.DataFrame({ "feature": X_train.columns, "gain": xgb_model.feature_importances_ }).sort_values("gain", ascending=False) print(importance_df.head(20))跑完这个代码之后,你会看到排序靠前的通常是 dti、revolving_util、inst_income_ratio、delinq_2yrs 这类直接反映负债和还款压力的特征。如果 st_purpose_bad_rate 排进前十,说明区域风险先验确实有效;如果 emp_length_num 排得很靠后,不代表它没用,只是它的信息被 income_per_emp_year 覆盖了。这时候你可以考虑把冗余特征删掉,再对比一次 AUC,如果差异在 0.002 以内,就可以放心精简特征集,降低上线时的特征采集成本。
4.3 过拟合判定:训练集与验证集 AUC 差距多少算危险
经验上有一个粗略的判定标准:训练集 AUC 减去验证集 AUC,如果差距在 0.05 以上,说明过拟合明显,需要加强正则。你可以通过调整 max_depth、min_child_samples、reg_lambda 三个参数来控制。还有一个更快的诊断方法是画学习曲线——把训练集按 10%、20%……100% 分批,观察验证集 AUC 是否还在上升,如果到 60% 之后验证集 AUC 就平台了,说明数据量已经接近上限,继续加模型复杂度只会让训练集 AUC 继续涨而验证集不动。
我遇到的一个真实翻车案例是:有人在特征里加了一个“借款描述长度”,结果验证集 AUC 涨了 0.01,非常兴奋,结果上线之后完全不 work。原因很简单:借款描述长度在历史样本里可以统计出来,但在真实申请时点,这个特征还没被清洗完,实际传到模型里的值是空值。这种情况下,模型的线上表现和离线评估完全不同。所以“特征在建模时点是否可得”这个 checklist 比任何调参都重要。
5. 避坑与排查:LendingClub 反欺诈建模的 5 个常见大坑
5.1 标签泄漏:把贷后结果字段当成特征
现象:模型训练完后,验证集 AUC 高达 0.98,特征重要性里排第一的是最后一笔还款金额。原因:总还款额、回收金额、逾期天数这些字段只在贷款生命周期结束时才有值,它们本身就是坏账结果的一部分。解决:在特征列表里彻底删除所有贷后字段,包括 total_pymnt、last_pymnt_amnt、recoveries、collection_recovery_fee、total_rec_late_fee。判断方法很简单:如果一个字段你在申请评分时点拿不到它的值,它就不能进模型。
5.2 随机切分导致时间泄漏
现象:模型验证集 AUC 比按时间切分高 0.03,看起来效果很好,但上线后衰减严重。原因:LendingClub 数据有明确的时间轴,随机切分会把同一年代的样本同时放进训练集和验证集,模型“见过”了相似时间段的数据分布,验证结果虚高。解决:用 issue_d 排序后按 8:2 切分,或者做时间窗口滚动验证。这个坑在学术竞赛里可以忽略,但生产环境必须按时间切分。
5.3 类别特征乱用 Label Encoding
现象:模型能跑通,但 feature_importance 里 addr_state 排名极高,验证集表现不稳定。原因:Label Encoding 给 50 个州赋予了 0 到 49 的数值,树模型会认为 49 和 48 是相邻的,而实际上加州和德州的欺诈模式可能完全不同。解决:对高基数类别特征用目标编码(用坏账率做编码)或 one-hot。在 LendingClub 数据上,addr_state 用目标编码效果更好,因为 one-hot 会让树模型做出很多零碎的分裂,而目标编码能把区域风险压缩成一个连续变量。
5.4 对缺失值直接填 0 或均值
现象:训练集和验证集的缺失值分布不同,模型在验证集上表现尚可,但换了一批新数据后效果大幅下跌。原因:LendingClub 的某些缺失值(比如 emp_length、annual_inc)并不是随机缺失的,缺失本身可能与申请人的风险特征相关。解决:为缺失值单独加一个指示变量,比如 emp_length_missing 列,值为 1 表示缺失;或者直接让树模型原样处理缺失值。XGBoost 和 LightGBM 原生支持缺失值,会自动寻找缺失值的最佳分裂方向,你不应该手动 fillna(0) 去干扰它们。
5.5 正负样本比例严重失衡时不设 scale_pos_weight
现象:验证集 AUC 0.4x 看起来还行,但看混淆矩阵发现,模型把几乎所有验证集样本都预测成了负样本。原因:负样本占 82%,模型发现全部预测为负类就能有 82% 的准确率,而 AUC 对这类情况不敏感。解决:设置 scale_pos_weight 为正负样本比例倒数,或者用 class_weight="balanced"。另一个做法是对负样本做下采样,但 LendingClub 数据量足够大,用 scale_pos_weight 既能保持信息完整,又不用额外采样。
6. 进阶技巧:用集成模型的概率做分数校准与群体稳定性验证
模型训练完之后,还有一个很关键的环节:把集成模型的输出概率转换成一个业务可直接使用的分数,并验证这个分数在不同时间段是否稳定。这里介绍一个我常用的做法,叫单调分箱校准。原理是:直接把模型输出的概率按分位数分成 10 箱,统计每一箱的真实坏账率。如果模型有区分度,坏账率应该从第 1 箱到第 10 箱单调递增;如果出现局部倒挂,说明那一段区间的排序能力不稳定,需要对模型进行局部修正。
import pandas as pd score_df = pd.DataFrame({ "prob": y_pred_proba, "label": y_valid.values }) score_df["bucket"] = pd.qcut(score_df["prob"], q=10, duplicates="drop") bucket_stats = score_df.groupby("bucket", observed=True).agg( count=("label", "count"), bad_rate=("label", "mean") ).reset_index() bucket_stats["score"] = (bucket_stats["bad_rate"] * 600 + 300).round(2) print(bucket_stats)这里的分数映射公式是我在业务里常用的:把坏账率乘以 600 再加 300,让分数落在 300 到 900 之间,分数越高代表风险越高。这个映射不是唯一的,你可以根据业务的接受度调整斜率和截距。我在实际项目中通常还会加一个校准步骤:用 Isotononic Regression 或者 Platt Scaling 对概率做校准,让模型输出的概率在绝对意义上更接近真实坏账率。这个步骤不影响 AUC,但能让分数阈值更稳定。
群体稳定性验证是风控模型上线前必须做的评估,核心指标是 PSI(Population Stability Index)。它的计算方式是对比训练集和线上最新样本的分数分布差异,PSI 小于 0.1 表示稳定,0.1 到 0.25 表示有漂移需要监控,超过 0.25 表示分布变化过大,模型需要重新训练。LendingClub 数据因为跨度长,2015 年和 2018 年之间的风险分布差异很大,如果你只拿其中一年训练、另一年验证,PSI 很容易超标。这个现象提醒我们:在真实风控业务里,模型不仅要每年重训,还要定期用 PSI 监控特征分布漂移。
最后说一个我的习惯:每次跑完一个模型版本,我都会把特征列表、参数配置、验证集 AUC/KS、PSI 一起存成一个版本记录文件。因为反欺诈模型迭代非常频繁,如果没有版本记录,三个月后你根本说不清现在的线上模型是用哪批特征、哪个阈值训练出来的。这个习惯救了我很多次——有次线上模型出了问题,我靠版本记录半小时内定位到是特征上线顺序错乱导致的。希望这些方法和踩坑记录能帮你在用 LendingClub 练手时少走几趟弯路。
本文还有配套的精品资源,点击获取