IEEE-CIS欺诈检测实战:从数据拆解到模型调优的完整指南
简介:面向数据科学竞赛学习者与风控建模初学者的IEEE-CIS欺诈检测赛题EDA资源包,聚焦二分类场景下用户欺诈与否的判别问题,帮助读者在正式建模前完成数据理解与特征梳理。包内共6个文件,以Python脚本与Jupyter Notebook为主,辅以说明文档和配置项,压缩包约1.02MB,体积轻便易于本地运行。其中Notebook承载深入的探索性分析流程,脚本文件分别覆盖数据清洗、特征工程与可视化报告等环节,模块划分清晰,便于按需调用或二次改造。已有599人学习,适合希望借鉴成熟EDA组织方式、快速搭建赛题分析框架的读者参考,也可作为风控类项目前期数据探查的实践样例。
1. IEEE-CIS-Fraud-Detection:一份被反复引用的风控赛题,到底该怎么拆开做
第一次拿到 IEEE-CIS-Fraud-Detection 这个数据集的人,十有八九会先被它的表结构劝退:一张交易主表、一张身份表,两张表靠一个键拼起来,字段名全是C1到C14、V1到V339这种匿名编码,连业务含义都不给你。它来自一场公开的欺诈检测竞赛,任务很直白——判断一笔线上交易是不是欺诈。但真正做过的人都知道,难点从来不在模型选型,而在数据本身:极端不平衡、时间强相关、匿名特征无法解释、身份表大面积缺失。这篇笔记就按我实际复现这套方案的顺序,把数据怎么读、特征怎么造、验证怎么切、模型怎么调、坑在哪,一层层讲清楚。适合已经会写 sklearn 和 LightGBM、但还没系统做过风控建模的从业者,也适合想拿这套数据练手、又不想在验证集上自欺欺人的同学。
2. 先把数据读对:表结构、缺失模式与时间轴
2.1 两张表的关系与合并策略
这套数据的主表是交易流水,每行一笔交易,带TransactionID、isFraud标签、金额、产品编码、卡信息等;身份表是部分交易的身份补充信息,带设备类型、浏览器、邮箱域名等。两张表通过TransactionID关联,但身份表只覆盖了一部分交易,合并后必然出现大量缺失。常见做法是左连接主表,保留全部交易,身份字段缺失就让它缺失,不要急着填充——缺失本身在风控里就是信号,很多欺诈交易恰恰是身份信息不全的。
import pandas as pd train_trx = pd.read_csv('train_transaction.csv') train_id = pd.read_csv('train_identity.csv') # 左连接:以交易为主,身份信息能补则补 df = train_trx.merge(train_id, on='TransactionID', how='left') print(df.shape) print(df['isFraud'].mean()) # 正样本比例,通常不到 4% print(df['id_01'].isna().mean()) # 身份表缺失比例,往往超过 70%这段代码的关键在how='left'。如果改成 inner join,你会直接丢掉大部分交易,而丢掉的很可能正是身份信息缺失的那批,模型学到的分布就偏了。合并后先看两个数:正样本比例和身份字段缺失率。前者决定你后面要不要做重采样,后者决定身份特征能不能直接用。参数上没什么可调的,但这一步的顺序不能反——先合并再看缺失,而不是先填充再合并。
2.2 时间列是隐藏的主键
TransactionDT不是真实时间戳,而是一个从某个起点开始的秒级偏移量。它不能直接当特征喂给模型,但它是你做验证集切分、构造时间窗口特征、判断数据泄漏的唯一依据。我一般先把它换算成「天」,看看数据覆盖了多长时间,再决定按时间切还是按比例切。
# 换算成天,观察时间跨度 df['day'] = df['TransactionDT'] // (24 * 60 * 60) print(df['day'].min(), df['day'].max()) print(df.groupby('day')['isFraud'].mean().head(20))跑完你会发现两件事:数据大概覆盖半年左右;欺诈率在不同天之间波动明显,不是均匀分布。这就意味着随机切分验证集会高估模型效果,因为随机切分让模型在训练时「见过」未来时间段的模式。正确做法是按时间切,用前面的时间训练、后面的时间验证,模拟真实上线场景。这一步做错,后面所有调参都是在优化一个虚假的指标。
2.3 匿名特征的处理边界
C系列和V系列特征都是匿名化的,官方没给含义。C系列通常是类别型,取值不多;V系列是数值型,很多是计数或排序类特征。对C系列,我一般直接转 category 交给 LightGBM 处理,不自己做 one-hot,因为类别多、稀疏,one-hot 会把维度撑爆。对V系列,先看分布,偏态严重的做 log1p 变换,但不要无脑标准化——树模型对单调变换不敏感,标准化对它没意义,反而增加计算量。
提示:匿名特征不要试图反推业务含义去「讲一个故事」,那是浪费时间。你要做的是让模型能利用它们的统计结构,而不是解释它们。
3. 特征工程:金额、计数与时间窗口怎么造
3.1 金额类特征的变换与聚合
金额是这套数据里最有业务感的字段。原始金额分布极度右偏,直接喂给模型效果一般。我一般做三件事:取 log1p 压偏态、算金额与卡片历史均值的比值、按卡片聚合出消费频次和金额统计量。比值特征往往比原始金额更有区分度,因为欺诈交易常常表现为「这张卡突然出现一笔远超日常的消费」。
import numpy as np df['amt_log'] = np.log1p(df['TransactionAmt']) # 按卡片聚合:该卡的历史交易次数与金额均值 card_agg = df.groupby('card1')['TransactionAmt'].agg(['count', 'mean']).rename( columns={'count': 'card_txn_cnt', 'mean': 'card_amt_mean'}) df = df.merge(card_agg, on='card1', how='left') # 金额相对卡片均值的比值 df['amt_ratio'] = df['TransactionAmt'] / (df['card_amt_mean'] + 1)这里的聚合是在全量数据上做的,严格来说有轻微泄漏风险,因为验证集的信息也参与了均值计算。更严谨的做法是只在训练时间段内算聚合,再映射到验证集。实操中如果时间切分做得干净,这个泄漏影响很小,但你要知道它存在。+1是为了防止除零,别省。
3.2 计数特征与 UID 构造
风控里最有效的一类特征是「这个实体出现了多少次」。单看一笔交易看不出问题,但同一张卡、同一个邮箱域名在短时间内高频出现,就很可疑。常见做法是把几个关键字段拼成一个 UID,比如卡号加地址加邮箱域名,然后统计这个 UID 的交易次数、涉及的不同设备数等。
# 构造组合 UID,统计出现频次 df['uid'] = df['card1'].astype(str) + '_' + df['addr1'].astype(str) + '_' + df['P_emaildomain'].astype(str) df['uid_cnt'] = df.groupby('uid')['TransactionID'].transform('count') # 同一 UID 下不同设备数的近似(用设备信息字段去重计数) df['uid_device_nunique'] = df.groupby('uid')['DeviceInfo'].transform('nunique')transform('count')和groupby().count()的区别要搞清楚:前者把聚合结果广播回每一行,保持行数不变,后者会压缩成每个 UID 一行。特征工程里几乎都用transform。UID 的字段组合可以多试几组,卡号加邮箱是一组,卡号加地址是另一组,不同组合捕捉不同的欺诈模式。但别拼太多字段,拼得越细,每个 UID 的样本越少,统计量越不可靠。
3.3 时间窗口特征:把「最近」量化出来
时间窗口特征是这套数据的分水岭。核心思路是:对每笔交易,看它之前一段时间内同一实体的行为。比如「这张卡在过去 1 天内的交易笔数」「这个 UID 在过去 6 小时内的金额总和」。这类特征计算量大,但区分度极高。
# 简化示例:按卡片统计过去 1 天内的交易笔数 df = df.sort_values('TransactionDT').reset_index(drop=True) window = 24 * 60 * 60 # 1 天,单位秒 def rolling_count(group): times = group['TransactionDT'].values counts = [] for i, t in enumerate(times): # 统计当前时刻往前 window 秒内的交易数 counts.append(((times >= t - window) & (times <= t)).sum()) return pd.Series(counts, index=group.index) df['card_txn_1d'] = df.groupby('card1', group_keys=False).apply(rolling_count)上面这段是教学版,逐行循环在几十万行数据上会慢到无法接受。生产里我一般用排序加双指针,或者用pd.merge_asof配合累计计数来做,复杂度能降到接近线性。参数上,窗口大小要结合业务节奏定:1 天、7 天、30 天各造一组,让模型自己选。窗口太短噪声大,太长则平滑掉了异常。注意计算窗口特征前必须按TransactionDT排序,否则「过去」就变成了「未来」,这是最隐蔽的泄漏。
4. 验证集切分与不平衡处理:别让 AUC 骗了你
4.1 按时间切分而不是随机切分
前面提过,随机切分会让模型偷看未来。正确做法是取时间轴上的一个点,之前做训练,之后做验证。我一般用前 80% 的时间做训练、后 20% 做验证,并且保证验证集里正样本数量足够,否则指标波动会很大。
from sklearn.model_selection import train_test_split # 按时间排序后切分 df = df.sort_values('TransactionDT').reset_index(drop=True) split_idx = int(len(df) * 0.8) train = df.iloc[:split_idx] valid = df.iloc[split_idx:] print(train['isFraud'].mean(), valid['isFraud'].mean())切完要检查两件事:训练集和验证集的正样本率是否接近,如果差太多说明时间点上欺诈模式发生了突变,需要调整切分点;验证集的正样本绝对数量是否够,太少的话 AUC 的置信区间会很宽,调参时容易被噪声带偏。
4.2 不平衡处理的三种路线与取舍
正样本不到 4%,直接训练模型会偏向多数类。常见三条路线:调scale_pos_weight、做欠采样、做 SMOTE 过采样。我的经验是优先调scale_pos_weight,它不改变数据分布,只是调整损失函数的权重,最稳。欠采样会丢信息,SMOTE 在匿名特征上生成的样本未必有业务意义,容易引入噪声。
import lightgbm as lgb # 用负正样本比例设置权重 neg = (train['isFraud'] == 0).sum() pos = (train['isFraud'] == 1).sum() spw = neg / pos params = { 'objective': 'binary', 'metric': 'auc', 'scale_pos_weight': spw, 'learning_rate': 0.05, 'num_leaves': 64, 'feature_fraction': 0.7, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'verbose': -1 }scale_pos_weight设成负正比是起点,不是终点。设得太大模型会过度关注正样本,召回上去了但精确率掉得厉害,实际业务里误杀正常交易的代价很高。我一般会在负正比的 0.5 到 1.5 倍之间试几档,看验证集上的 PR 曲线而不是只看 AUC。num_leaves别开太大,匿名特征多、噪声大,叶子数超过 128 很容易过拟合。
4.3 评估指标的选择
AUC 是不平衡数据的常用指标,但它有个毛病:对正样本的排序敏感,对实际阈值不敏感。业务上你最终要定一个阈值来决定拦不拦,所以 PR 曲线和不同阈值下的召回率更有参考价值。我一般同时看 AUC、PR-AUC 和 top 1% 高风险交易的召回率——后者最接近真实风控场景,因为人工审核资源有限,只能看最可疑的那一小批。
from sklearn.metrics import roc_auc_score, average_precision_score pred = model.predict(valid[features]) print('AUC:', roc_auc_score(valid['isFraud'], pred)) print('PR-AUC:', average_precision_score(valid['isFraud'], pred)) # top 1% 高风险交易的召回率 threshold = np.percentile(pred, 99) top_mask = pred >= threshold recall_top = valid.loc[top_mask, 'isFraud'].sum() / valid['isFraud'].sum() print('Top1% recall:', recall_top)这段代码里np.percentile(pred, 99)取的是预测分数的 99 分位,意味着只拦最可疑的 1%。如果这个召回率能到 30% 以上,说明模型在头部排序上是有用的;如果很低,说明模型只是把大多数样本排对了,但对真正的欺诈没抓住,这时候要回去检查特征,而不是继续调参。
5. 避坑与排查:五个让我返工的血泪记录
5.1 现象:验证集 AUC 0.95,上线后一塌糊涂
原因几乎总是数据泄漏。最常见的是在构造聚合特征时用了全量数据,验证集的信息渗进了训练特征;其次是随机切分让模型见到了未来时间段的模式。排查方法很简单:把特征逐个拿掉重训,看哪个特征拿掉后验证集指标大幅下降,那个特征大概率有问题。解决就是严格按时间切分,所有聚合统计只在训练时间段内计算。
5.2 现象:模型训练报错,提示类别特征类型不对
LightGBM 对 category 类型有要求,pandas 的 object 列直接喂进去会报错或者被当成数值处理。原因是C系列字段读进来是 object,没转 category。解决是在训练前统一转换,并且保证训练集和验证集的类别编码一致——用同一份 category 映射,别各自转各自的,否则同一类别在两边编码不同,模型直接学错。
for col in cat_cols: df[col] = df[col].astype('category') # 用训练集的类别映射约束验证集 for col in cat_cols: valid[col] = valid[col].cat.set_categories(train[col].cat.categories)5.3 现象:窗口特征算出来全是 0 或者异常大
原因是没排序就做 rolling,或者时间单位搞错了。TransactionDT是秒,窗口如果按天设,要乘 86400。另一个常见错误是 groupby 之后索引错位,导致特征对到了错误的行上。解决是计算前强制sort_values('TransactionDT'),算完用reset_index对齐,并且拿几行手工验算,确认「过去 1 天」确实只包含当前时刻之前的交易。
5.4 现象:加入身份特征后效果反而变差
身份表缺失率太高,直接填充(比如填 -1 或众数)会引入大量虚假模式,模型把「缺失」和「欺诈」错误关联。原因是填充方式粗暴,没有区分「真缺失」和「有值」。解决是把缺失本身做成一个指示特征(是否缺失 0/1),原始值保留 NaN 交给 LightGBM 处理,它对缺失有原生支持,比手工填充更稳。
5.5 现象:本地 CV 很稳,换一版数据就崩
原因是特征里混入了对时间敏感的统计量,比如用全量算的 UID 频次。数据一换,频次分布整体偏移,模型就失效。解决是把所有统计类特征改成「截至当前时刻」的累计量,或者至少在训练集内计算再映射。这类问题在离线评估里很难发现,只能靠严格的时序切分和特征审计来防。
6. 进阶技巧:用分组时序验证和特征重要性做减法
走到这一步,模型能跑、指标能看,但真正决定这套方案值不值得投入的,是你能不能判断「哪些特征在真正起作用」。我习惯在最终调参前做一次分组时序验证:把验证集按时间再切成几段,看模型在各段上的表现是否稳定。如果某一段突然掉点,说明模型对那个时间段的模式不适应,可能是特征漂移,也可能是那段数据本身有异常。
# 把验证集按时间分成 4 段,看各段 AUC valid = valid.sort_values('TransactionDT').reset_index(drop=True) valid['seg'] = pd.qcut(valid['TransactionDT'], 4, labels=False) for seg in sorted(valid['seg'].unique()): sub = valid[valid['seg'] == seg] if sub['isFraud'].nunique() > 1: auc = roc_auc_score(sub['isFraud'], model.predict(sub[features])) print(f'Seg {seg}: AUC={auc:.4f}, n={len(sub)}')各段 AUC 差距在 0.02 以内算稳定,超过 0.05 就要警惕。这时候别急着加特征,先做减法:用 LightGBM 的feature_importance看增益排名,把长期排在末尾、且业务上说不通的特征砍掉,重训看指标是否保持。匿名特征多的时候,砍掉一批低增益特征往往能让模型更稳,因为噪声少了。
另一个我常用的技巧是对高增益特征做单变量分析:取增益前 20 的特征,逐个看它在欺诈样本和正常样本上的分布差异。如果某个特征增益很高但分布几乎重合,大概率是模型在过拟合噪声,可以考虑去掉。这个动作花不了多少时间,但能帮你把特征集从几百个压到几十个,训练速度和解译性都会好很多。
最后说个习惯:每次改特征或调参,我都固定记录三件事——验证集 AUC、PR-AUC、top 1% 召回率,外加这次改动的具体内容。风控建模的迭代很容易陷入「感觉变好了」的错觉,只有把指标钉在表格里,才能看出哪次改动是真提升、哪次只是噪声波动。这套数据我前后复现过几轮,最大的教训就是别迷信单次 CV 的高分,时序切分和特征审计做到位,比多调几个超参数有用得多。希望帮到你。
本文还有配套的精品资源,点击获取