☰
个贷违约预测:AUC口径、交叉验证防泄漏与WOE/IV特征工程
2026/11/5 4:08:10 网站建设 项目流程

简介:面向数据竞赛与机器学习实战学习者,这份压缩包围绕CCF大数据与计算智能比赛中的个贷违约预测任务,提供从数据理解、特征构造到模型训练与结果提交的完整参考资料,适合具备一定Python与建模基础、希望系统复盘赛题流程的读者。包内共18个文件,以8个Python脚本和5个Jupyter Notebook为主要载体,辅以2个CSV数据与提交样例、1个数据压缩包及README说明,整体约40MB,目录按基线、特征、模型和配置等环节组织,便于检索学习。内容覆盖信用违约场景的数据清洗、缺失值与异常值处理、特征工程、LightGBM与TabNet等模型尝试、基线代码和提交示例,也保留特征管理、训练入口与配置拆分等工程化做法,并涉及交叉验证、超参数调优和模型集成等常见竞赛流程。已有569人学习,读者可据此对照复现实验、理解赛题建模思路,并积累表格数据竞赛中的特征迭代与模型调优经验。

1. 个贷违约预测:决定名次的关键是验证口径

个贷违约预测这类赛题,最容易出现反直觉情况的地方在于:模型越调越复杂,榜单反而往下掉。它真正考的不是你会不会调 LightGBM,而是你能不能把「线下可信」这件事做对——AUC 口径怎么对齐、交叉验证怎么切、特征有没有偷偷用到未来信息。赛题给的是脱敏后的借贷行为数据,违约样本通常只占个位数百分比,硬套 accuracy 能刷到 95% 以上,但排序能力一点没涨。所以考点分布大致是:数据理解与验证策略占四成,特征工程占三成,模型与融合占两成,剩下的是对指标的业务解读。这篇写给已经能跑通完整流程、却卡在榜单中游的人。

2. 赛题考点地图:AUC 口径、数据画像与四类必答任务

赛题的第一层门槛不在算法,而在「你交上去的东西到底在衡量什么」。很多人在这一步就没有对齐:线下用一套切分算出漂亮的分数,线上提交后直接掉两个档。先把指标和数据两张底图认清楚,后面的调参才有意义。

2.1 评估指标:AUC 为什么对不平衡「免疫」

AUC 的定义是:随机抽一个正样本和一个负样本,模型给正样本的分数更高的概率。注意这句话里没有阈值,也没有正负样本的比例。所以它天然是个排序指标——你把所有预测值同时乘以 2 或者取对数,AUC 一点不变。这也解释了为什么在违约率只有 3% 的数据上,全预测为「不违约」能拿到 97% 的准确率,但 AUC 只有 0.5,等于随机猜。

KS 是另一个绕不开的指标,定义是max(TPR - FPR),衡量「最优的那一刀能切多开」。风控业务里更爱看 KS,因为它可以直接对应通过率、拒绝率这类决策语言。但 KS 对切点敏感,线下 CV 算出来的 KS 往往比线上乐观,不能直接拿去定业务阈值。

指标衡量什么对不平衡敏感对阈值敏感常见误用
AUC全局排序能力不敏感不敏感拿它当概率校准好坏的证据
KS最优切点的区分度中等敏感直接用 CV 的 KS 定业务阈值
Accuracy整体判对比例极敏感敏感不平衡数据刷到 0.96 就以为成了
F1正类精确率与召回率的调和中等敏感不调阈值直接算

提示:提交前确认赛题要的是正类概率还是 0/1 标签。要求概率时交了标签,AUC 会直接掉到 0.5 附近,这是每年都有人犯的错。

2.2 数据画像:三张表的字段语义与缺失的含义

信贷赛题的数据通常分几块:申请信息(金额、期限、利率、渠道)、征信与行为记录(查询次数、逾期次数、还款状态),有的赛题还会带行为埋点。字段多、稀疏、缺失率高,这是常态。

这里有个特别容易踩的坑:缺失在信贷场景里往往不是「没有值」,而是「没有发生过」。某类查询次数为空,很可能是这个人从来没查过征信,本身就是一种风险信号。直接fillna(-1)会把这个语义和真实的「值为 -1」混在一起。更稳的做法是先造一列缺失指示(is_null),再单独填一个哨兵值,让树模型自己去学分岔。

金额类字段先看分位数再决定截断,99 分位以外的极端值往往来自数据录入或者特殊产品,保留会拉偏分裂点。类别字段还要防一手:测试集里可能出现训练集没见过的取值,编码时留一个unknown桶,别让模型在推理阶段报错。

2.3 真题演练:线下 CV 涨了、线上掉了,怎么答

这是赛题复盘和面试里出现频率最高的一问,几乎每个做过这类赛题的人都会被追问。答的时候别急着甩「过拟合」三个字,那是结论不是过程。按下面四步讲,面试官会觉得你真跑过。

第一步看切分。随机 KFold 有没有把同一个用户的不同期记录分到训练和验证两边?如果有,模型在验证折上见到的是「熟人的另一面」,分数天然偏高。这类数据必须按用户分组切。

第二步看特征。WOE、目标编码、各类统计量,是不是在全量数据上 fit 的?如果是,验证折的标签信息已经通过特征的均值漏进去了。正确做法是只在训练折上 fit,验证折只做 transform。

第三步看时间。训练集和测试集是不是同一时间段。如果测试集更靠后,用户的整体行为分布会漂移,尤其是额度、利率这类受外部环境影响的字段。

第四步才轮到模型复杂度。树深、叶子数、迭代轮数是不是靠 early stopping 在线下「榨」出来的,把验证集当成了调参集。四步走完基本能定位,而前两步才是这类赛题里最真实的失分点。

3. 特征工程:从原始流水到 WOE/IV 的可复现链路

赛题的分数上限八成由特征决定。模型换来换去 AUC 差 0.005,特征做对一组能差 0.03。这一章讲三件事:时间窗口怎么切、分箱和 WOE 为什么是逻辑回归的必需品、以及一段能直接套用的实现。

3.1 时间窗口聚合:7/30/90/180 天怎么切

行为类数据几乎都是流水表,一人多行。要把它们压成一人一行的宽表,靠的是分组聚合。常见的组合是:按用户 ID 分组,先按时间窗口筛出近 7 天、30 天、90 天、180 天的记录,再对每个窗口做count / sum / mean / max / min / std / nunique。

窗口不是越多越好。7 天反映短期急迫性,30 天和 90 天反映中期行为习惯,180 天覆盖一个完整的还款周期。再往上加 365 天,样本覆盖率会掉得厉害,很多人半年前根本没有记录,聚合出来的全是缺失。

聚合特征有两个细节值得单独说。一是「次数类」和「比率类」要成对出现,光有「近 30 天查询次数」不够,还要有「近 30 天查询次数 / 总查询次数」,否则模型无法区分一个总查询 3 次、近 30 天查了 3 次的人,和一个总查询 100 次、近 30 天查了 3 次的人。二是分组聚合的键要包含时间戳,做完窗口筛选后再 groupby,不要先 groupby 再在里面写时间条件,那样容易把窗口外的记录也带进来。

3.2 分箱与 WOE/IV:逻辑回归为什么非要这一步

WOE 的全称是权重证据,公式是ln((bad_i / bad_total) / (good_i / good_total)),本质是把「这个箱里坏样本占比相对于整体坏样本占比的倍数」取对数。IV 则是(bad_i/bad_total - good_i/good_total) * WOE按箱求和,用来衡量一个变量整体的区分能力。

逻辑回归必须走 WOE,原因有三个。第一,逻辑回归是线性模型,要求特征和 log-odds 呈线性关系,连续变量直接扔进去几乎必然违背这个假设,分箱后单调的 WOE 把非线性掰直了。第二,WOE 把每个箱映射到同一个尺度上,极端值和量纲问题一并解决。第三,WOE 天然处理缺失——把缺失单独作为一个箱,它自己会学到一个 WOE 值,不用你拍脑袋填。

IV 的经验区间可以直接背:小于 0.02 基本没有预测力;0.02 到 0.1 是弱;0.1 到 0.3 是中等;0.3 到 0.5 是强;超过 0.5 先别高兴,优先怀疑泄漏或者分箱过细。

3.3 代码演练:一份能直接套用的 WOE/IV 实现

下面这段只依赖 numpy 和 pandas,能在任何一折训练集上跑。关键点是:只能在训练折 fit,验证折用映射表 transform。

import numpy as np import pandas as pd def fit_woe_iv(df, feature, target, bins=10): """ df : 训练折数据,绝不能是全集 feature : 待编码字段名 target : 标签列,1 表示违约 bins : 连续变量的分箱数,样本少时调小 返回 : 映射表 mapping(箱 -> woe)与变量总 IV """ tmp = df[[feature, target]].copy() # 连续变量走等频分箱;离散变量直接按取值分组 if tmp[feature].dtype != "object" and tmp[feature].nunique() > bins: tmp["bin"] = pd.qcut(tmp[feature], q=bins, duplicates="drop") else: tmp["bin"] = tmp[feature].astype(str) g = tmp.groupby("bin", observed=True)[target].agg(["count", "sum"]) g.columns = ["total", "bad"] g["good"] = g["total"] - g["bad"] total_bad, total_good = g["bad"].sum(), g["good"].sum() # 加 0.5 平滑,防止某个箱 bad 为 0 时 log 溢出 g["woe"] = np.log( ((g["bad"] + 0.5) / total_bad) / ((g["good"] + 0.5) / total_good) ) g["iv"] = ( ((g["bad"] + 0.5) / total_bad) - ((g["good"] + 0.5) / total_good) ) * g["woe"] return g["woe"].to_dict(), float(g["iv"].sum()) def transform_woe(series, mapping, default=0.0): """验证折 / 测试集只做映射,不重新 fit""" if pd.api.types.is_numeric_dtype(series) and len(mapping) > 0: keys = list(mapping.keys()) # 数值型映射表依赖分箱边界,实际使用时保存 bin 边界再 cut return series.map(mapping).fillna(default) return series.astype(str).map(mapping).fillna(default)

这段代码有三个地方值得在答题时主动提。pd.qcut用等频分箱而不是等宽,是为了保证每箱样本量接近,避免某一箱只有几十条记录导致 WOE 剧烈波动。+0.5是拉普拉斯平滑,正样本极少时某些箱的 bad 计数为 0,不平滑会直接算出负无穷。fit和transform拆成两个函数,是为了强制你在交叉验证里守住「训练折 fit、验证折 transform」这条线——所有数据泄漏的翻车,几乎都出在这条线上。

数值型字段的映射要注意,qcut产生的是区间对象,线上单条推理时得先把分箱边界存下来,用pd.cut复现。这一步不做,本地跑得通,换一份数据就报错。

3.4 面试题:IV 值高就一定要进模型吗

不一定,而且高 IV 恰恰是最该警惕的一类。回答可以分三层。

第一层,IV 只衡量单变量区分度,不衡量稳定性。一个变量在训练期 IV 是 0.4,在验证期掉到 0.05,说明它学到的是特定时间段的巧合,进模型只会拖累线上。所以要看跨期 IV 的衰减幅度,掉一半以上的直接砍掉。

第二层,高 IV 可能来自分箱过细。每个箱只有几十条样本时,WOE 的方差极大,IV 是被噪声堆上去的。检查方法是把分箱数从 10 降到 5,如果 IV 掉得厉害,说明原来的高 IV 不牢靠。

第三层,也是赛题里最要命的:IV 异常高的变量,很多时候本身就是标签的衍生。比如某个字段是「最近一次还款状态」,那它和违约几乎是一回事,训练时 AUC 飞起,线上根本拿不到这个字段。判断方法很简单,问一句「这个字段在放款决策的那一刻能不能拿到」,拿不到就是未来信息。

4. 模型与验证:LightGBM 调参和线上一致性的取舍

特征定了,模型这块的分差其实不大,但验证策略的分差很大。同样是 LightGBM,切分方式换一下,线下 AUC 能差 0.02,而线下和线上的差距往往就藏在这 0.02 里。

4.1 从 LR 基线到 GBDT 主力:两套模型各自的分工

逻辑回归在风控里不是备胎,是合规基线。它要求 WOE 编码、缺失填充、标准化,可解释性强,每个特征的系数方向能直接拿去写规则。赛题里先跑一个 LR 有两个好处:一是验证特征方向有没有反(比如「逾期次数越多越安全」这种明显错误的 WOE 映射),二是给自己一个兜底分数,后面 LightGBM 万一调崩了还有东西交。

LightGBM 的优势在于不用编码、能直接吃原始值和缺失、自动做非线性和特征交叉。代价是可解释性差,容易在噪声特征上过拟合。几个关键参数的作用和常用区间如下:

参数作用赛题常用区间调大后的后果
learning_rate每棵树的贡献0.02–0.05收敛快但容易欠稳
num_leaves单树复杂度31–63过拟合风险上升
min_child_samples叶子最小样本数20–100太大则欠拟合
feature_fraction列采样比例0.6–0.9太小则学不动
bagging_fraction行采样比例0.7–0.9需配合 bagging_freq
lambda_l1 / lambda_l2正则强度1e-3 到 10太大则分裂点变钝
early_stopping_rounds早停耐心值100–200太大则白跑轮数

调参顺序建议固定下来:先把学习率压到 0.05,n_estimators给一个大数(比如 5000)配早停,找到合适的迭代轮数;再调num_leaves和min_child_samples控制单树复杂度;最后加feature_fraction、bagging和正则。反过来的顺序会让每一轮都白调,因为学习率一变,最优轮数全变。

4.2 StratifiedKFold、GroupKFold 与时间外验证的区别

这三种切分对应三种不同的假设,选错了线下分数就是自欺欺人。

StratifiedKFold保证每折正负比例和整体一致,适合样本独立同分布的场景。但它随机打乱,同一个用户的多条记录可能同时出现在训练折和验证折,这在信贷数据里几乎必然发生。

GroupKFold按用户 ID 分组,保证同一用户只出现在一折里。这是最接近「新客户」上线场景的验证方式,线下分数会比随机切低一截,但那才是真话。

TimeSeriesSplit按时间外推,前段训练、后段验证,模拟「用历史预测未来」。如果赛题的测试集在时间上更靠后,这才是最该用的切法。

from sklearn.model_selection import StratifiedKFold, GroupKFold, TimeSeriesSplit # 方案 A:随机分层五折,只适合用户不重复的样本 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) # 方案 B:按用户分组,防止同一用户跨折造成泄漏 gkf = GroupKFold(n_splits=5) for tr_idx, va_idx in gkf.split(X, y, groups=user_id): X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] # 方案 C:时间外推,最接近线上真实分布 tss = TimeSeriesSplit(n_splits=5) for tr_idx, va_idx in tss.split(X_sorted_by_time): ...

选择依据写清楚:如果赛题明确说了测试集时间更靠后,优先 C,用 B 做辅助;如果没提时间,但样本里有重复用户,用 B;只有在完全匿名、无用户标识、无时间戳的情况下才退回 A。groups参数必须传用户 ID,传错了等于没分组。

4.3 真题演练:类别不平衡该不该重采样

这题的标准错误答案是「用 SMOTE 过采样到 1:1」。正确的答法是先问一句:评估指标是什么。

如果是 AUC,重采样基本没用。AUC 只看排序,你把正样本复制十遍,正负样本的相对顺序没变,AUC 自然不变。反而 SMOTE 插值出来的合成样本不代表任何真实用户,在信贷这种强业务约束的场景里,合成出来的「查询次数 3.7 次、逾期 0.4 次」毫无意义。

更稳的处理顺序是:先不动,直接跑,看 baseline;如果模型完全学不到正类(比如树的分裂点全被负样本主导),再用scale_pos_weight或者class_weight给正样本加权,这个操作不改变样本本身,只改变损失函数的惩罚力度;只有在需要输出「真实违约概率」时,才考虑做概率校准,用等渗回归或者 Platt scaling 把分数拉回真实比例。

还有一个细节:如果非要做重采样,必须在交叉验证的每一折内部做,先切分再采样。先对全集 SMOTE 再切 CV,合成样本会跨折泄漏,线下分数虚高得离谱,这是赛题复盘里最常见的翻车点之一。

4.4 模型融合:加权平均、rank 平均与 Stacking 的边界

融合的前提是模型之间要有差异。两个都是 LightGBM、只是随机种子不同,融了等于没融。有效的组合通常是:一个 GBDT(LightGBM 或 CatBoost)、一个线性模型(LR 走 WOE)、一个对特征尺度不敏感的模型(比如 ExtraTrees),三者的错误相关性才够低。

加权平均最直接,但要求各模型输出在同一尺度上。如果 LR 输出的是校准过的概率、LightGBM 输出的是未校准的分数,直接加权会被尺度大的那个主导。权重可以在 OOF 预测上用网格搜索按 AUC 找。

rank 平均先把每个模型的预测转成排名再平均,对尺度完全不敏感,适合模型之间分布差异大的情况。缺点是丢掉了概率信息,如果赛题要求提交概率并且按概率算指标,得谨慎。

Stacking 的二层通常用 LR,输入必须是 OOF 预测。用训练集上的 in-sample 预测做二层输入,是最典型的泄漏,二层模型会学到一个虚假的映射关系。赛题里的实际经验是:单模调好 + rank 平均,往往比三层 stacking 更稳,尤其是在样本量不大、正样本又少的情况下。

5. 易错点:五个让分数大幅下降的操作

这一章按「错在哪 → 为什么错 → 怎么答对」写,每一条都在真实赛题复盘里反复出现。看的时候不妨对照自己的代码过一遍。

5.1 在全量数据上 fit WOE 或目标编码

错在哪:把训练集和测试集拼起来算 WOE,或者先对全体数据做 target encoding,再切分训练验证。

为什么错:WOE 和 target encoding 的计算式里都含标签。用全量数据算,等于让验证折的标签参与了特征构造,验证分数必然虚高,线上立刻打回原形。这类泄漏最阴险的地方在于它不报错,代码跑得通,分数还很好看。

怎么答对:把编码器当成模型的一部分,写进 pipeline。每一折内部fit只用训练折,transform作用于验证折;测试集只transform不fit。面试时被问到特征工程,主动补一句「所有含标签的编码我都放在折内做」,比讲十个特征都有用。

5.2 随机 KFold 切穿了用户和时间

错在哪:拿到数据直接StratifiedKFold(shuffle=True),既不看用户 ID 是否重复,也不看时间戳是否有序。

为什么错:同一用户的多条记录高度相关,一条在训练折、一条在验证折,模型实际上是在「认人」而不是「学规律」。时间维度同理,用未来的行为预测过去,线下当然准。这是线下 0.80、线上 0.72 这类断崖的典型成因。

怎么答对:先做数据体检,统计用户 ID 的重复率、时间戳的跨度。重复率高就用GroupKFold,时间跨度大就用TimeSeriesSplit,两者都有就先用时间切、再在时间块内按用户分组。答题时把「为什么这么切」讲出来,比报一个高分更有说服力。

5.3 把 SMOTE 当成提分按钮

错在哪:一看到正样本占比 3%,先跑一遍 SMOTE 把比例拉到 1:1,然后发现 AUC 没动甚至略降,还以为是参数没调好。

为什么错:AUC 是排序指标,过采样不改变正负样本的相对顺序,理论上就不该有提升。更糟的是,如果在切分之前做 SMOTE,合成样本会同时出现在训练折和验证折,制造出一批「几乎一样」的泄漏样本,线下分数虚高。

怎么答对:先明确指标。AUC 场景下优先用样本权重(scale_pos_weight),不动样本本身。确实需要重采样时,只在训练折内部做,并且清楚说明这一步的目的是让模型别完全忽略正类,而不是为了提 AUC。能把这层因果关系讲清楚,这一题就稳了。

5.4 用 AUC 去挑阈值、把概率当违约率

错在哪:在验证集上扫一遍阈值,挑 AUC 最高的那个;或者直接把模型输出的 0.3 解释成「这个人有 30% 的概率违约」。

为什么错:AUC 对阈值不敏感,同一组预测值在任何阈值下 AUC 都一样,扫阈值挑 AUC 是个伪操作。而 GBDT 输出的原始分数没有校准过,0.3 只代表相对排序位置,不代表真实频率。样本经过重采样或加权之后,这个偏差会更大。

怎么答对:阈值要按业务目标定,比如在 Recall 不低于 0.6 的前提下最大化 Precision,或者按通过率倒推。要真实概率就先做校准,用等渗回归或 Platt scaling 在验证集上拟合,再报概率。答这题时把「排序」和「校准」两个词分开讲,基本不会被追问倒。

5.5 拿特征重要性当因果结论

错在哪:跑完 LightGBM 看 gain 排名,直接说「近 30 天查询次数是最重要的违约原因」,甚至按重要性砍掉一半特征再跑。

为什么错:gain 衡量的是这个特征在所有分裂中带来的不纯度下降总和,它受特征取值个数、分箱粒度、与其他特征的相关性影响。一个与其他强特征高度相关的变量,重要性会被稀释;一个取值极多的 ID 类字段,重要性会虚高。按 gain 硬砍特征,很容易砍掉那些单独看不强、但组合起来有效的变量。

怎么答对:重要性只作为参考,筛选特征优先用 IV 加跨期稳定性,再看模型去掉该特征后的 AUC 变化。讲业务含义时说「相关」不说「导致」。这一条在面试里是加分项,因为它说明你知道模型的边界在哪。

6. 复盘口径:把一套赛题解法讲成面试答案

赛题做完只是完成一半,能不能把过程讲成一段让面试官信服的答案,是另一半。面试官问「你做过什么项目」的时候,最想听的不是 AUC 多少,而是你怎么知道这个数字可信。

可以把回答固定成三段。第一段讲目标和约束:这是一个二分类的违约预测问题,正样本占比低,评估用 AUC,数据里有重复用户和时间跨度,所以验证必须按用户分组加时间外推。第二段讲方法和取舍:LR 走 WOE 做基线和可解释性,LightGBM 吃原始特征做主力,时间窗口聚合做了 7/30/90/180 四档,含标签的编码全部放在折内 fit。第三段讲发现:我最初用随机 KFold,线下 0.81、线上 0.75,换成按用户分组后线下掉到 0.77,和线上对齐了,说明之前那 0.04 是泄漏带来的。

这三段里,第三段最值钱。它证明你有能力识别「好得不像真的」的分数,而不是被分数牵着走。

押题方向也给几个。指标类:AUC 和 KS 的区别、AUC 对不平衡为什么不敏感、概率校准什么时候必须做。特征类:WOE 的作用、IV 的经验区间、目标编码怎么防泄漏、时间窗口怎么选。模型类:LightGBM 和 XGBoost 的实现差异(直方图算法、leaf-wise 生长、单边梯度采样)、正则参数怎么起作用。工程类:线上线下不一致怎么排查、模型上线后效果衰减怎么办。这几类覆盖了近九成的追问。

提示:被问到「你这个模型能上线吗」,别只答能。补一句「需要补一个跨期监控,看特征分布的 PSI 有没有漂移」,面试官会知道你考虑过模型的后半生。

我第一次做这类赛题时,线下 AUC 冲到 0.86,激动得连夜提交,线上 0.71。查了两天才发现是目标编码在全量数据上 fit 的,那一版的代码到现在我还留着当反面教材。从那以后我每次切分之前都强制走一遍三个检查:有没有重复用户、有没有时间戳、含标签的操作在不在折内。这三问问完,能省掉大部分后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询