ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

机器学习反欺诈实战:基于IEEE-CIS数据集的LightGBM二分类与特征工程全流程

机器学习反欺诈实战:基于IEEE-CIS数据集的LightGBM二分类与特征工程全流程 简介针对IEEE-CIS欺诈检测竞赛的探索性数据分析EDA与特征工程资源面向参加Kaggle比赛或研究金融风控反欺诈的算法工程师适合具备一定机器学习基础、希望快速上手金融风控场景的中高级学习者。任务本质是二分类判断用户行为是否属于点击欺诈资源围绕此目标给出完整分析流程。主文件Jupyter笔记包含数据分布、缺失情况、相关性等深度洞察配套数据清洗、可视化、特征工程三个Python脚本可高效处理大规模交易与身份信息表格。压缩包共6个文件涵盖Jupyter笔记、Python脚本与说明文档大小仅1.02MB。已有599人学习。笔记可引导读者从原始表格到洞察结论掌握单变量/双变量分析与聚合特征构造思路Python脚本则提供缺失值填补、类型转换、图表报告等可复用函数便于迁移至相似反欺诈项目也可直接作为数据科学课程教学演示。整体代码结构清晰能有效缩短EDA流程、提升建模效率。1. IEEE-CIS-Fraud-Detection 是什么一份能直接复现的欺诈检测全流程IEEE-CIS-Fraud-Detection 很适合用来练手表格类机器学习一份 Jupyter Notebook 把约 59 万条交易、394 个脱敏特征、约 3.5% 的欺诈正样本做成可训练、可验证、可扩展的二分类方案核心任务是预测每笔交易的 isFraud 是否为 1评估指标用 ROC AUC。它适合刚入门风控和异常检测的开发者想把特征工程、LightGBM 交叉验证整条链路跑通也适合已经在用业务规则做反欺诈的工程师想看看机器学习能不能替代写死的阈值。整个流程不依赖 GPU16GB 内存的机器就能跑完Notebook 里每个 cell 都保留了中间输出方便逐行对照。它不只是一个模型文件而是一套从原始 CSV 到提交结果的完整流程换数据集时改改列名就能复用。2. 先摸清工程结构Transaction 与 Identity 两张表、394 个特征和 isFraud 目标拿到这份资源的第一步不是急着跑代码而是先搞清楚数据从哪来、表怎么关联、每个字段属于哪一族。这一步没做透后面写特征工程时很容易把两套表混在一起处理等模型训完才发现 join 错了行数返工成本很高。2.1 目录结构Notebook、数据输入、训练输出三方分离整套工程在目录上按「原始数据 / Notebook / 输出」三个区域分开实际下载后大致是这个样子ieee-cis-fraud-detection/ ├── input/ # 原始 CSV只读不写 │ ├── train_transaction.csv # 主表约 59 万行394 列 │ ├── train_identity.csv # 身份设备表约 14 万行41 列 │ └── test_transaction.csv # 测试集无 isFraud 标签 ├── notebooks/ │ ├── 01_eda.ipynb # 数据概览、缺失率、目标分布 │ ├── 02_feature_engineering.ipynb # 特征工程全流程 │ └── 03_train_lgb.ipynb # 五折训练 OOF 评估 提交 ├── output/ │ └── submission.csv └── README.md这里有个值得学习的设计input 目录只放原始 CSV路径不硬编码在 Notebook 里output 统一收预测结果。这样换机器、换环境时不需要改动任何 cell训练产物也永远不会污染数据目录。三个 Notebook 按 01、02、03 编号本身就是在表达「先看数、再提特征、最后建模」的先后依赖关系你照着编号顺序执行就能复现完整流程。2.2 两张源表的关联关系按 TransactionID 做扩展Identity 只覆盖四分之一主表是 train_transaction.csv每一行是一笔交易包含交易本身的时间、金额、卡片、地址信息。train_identity.csv 则记录设备指纹和身份相关字段需要通过 TransactionID 关联回主表。注意 identity 表只覆盖了约四分之一的主表记录——不是每笔交易都有设备信息。这个覆盖率差异直接决定了特征工程的处理方式主表自身的字段可以全量参与特征构建而 identity 表 join 不上来的行只能按缺失值处理。有人会先把两张表左连接成一张大宽表再统一处理也有人分开构造特征矩阵后再合并我一般倾向后者因为 identity 字段缺失率本身就是一个很有价值的信息维度分开处理更清晰。2.3 特征族谱394 列到底在描述什么394 个特征看着吓人拆成族谱后其实就八类。竞赛方对原始字段做了脱敏和匿名化但字段命名规律仍然保留了分组信息特征族字段范围代表的业务含义交易基础TransactionDT、TransactionAmt、ProductCD时间偏移秒、交易金额、产品编码卡片信息card1 ~ card6发卡组织、卡种、卡号区间等脱敏字段地址与距离addr1、addr2、dist1、dist2交易地址编码、收货距离计数族C1 ~ C14与账户/卡片相关的计数型聚合时间差族D1 ~ D15与上次同类行为的时间间隔匹配族M1 ~ M9各类身份校验是否匹配匿名数值族V1 ~ V339脱敏后的连续变量缺失率普遍偏高身份设备族id_01 ~ id_38、DeviceType、DeviceInfo设备指纹、浏览器类型、操作系统V1 到 V339 是篇幅最大的一个族也是缺失率最严重的地方。这类字段通常是原始特征经过匿名变换后的产物语义已经不可解读只能当作纯数值特征喂给模型缺失率过高的列直接丢弃。2.4 目标变量与评估口径为什么是 ROC AUC 而不是准确率isFraud 的正样本比例约 3.5%属于典型的类别不平衡场景。如果拿准确率当指标模型全预测 0 也能拿到 96.5% 的准确率但这个模型毫无价值。ROC AUC 评估的是排序能力随机抽一个欺诈样本和一个正常样本模型给欺诈样本打出更高分数的概率。这个口径和反欺诈业务是对齐的——生产环境里你不会只卡一个阈值而是按分数从高到低排序依次审核。AUC 高意味着同样的审核人力下能捞到更多欺诈交易。所以 Notebook 里第一个 cell 通常是打印正负样本比例和 TransactionDT 的时间跨度先把基本盘看清。3. 特征工程拆解内存压缩、缺失填充、组合特征与聚合特征的四个关键动作这个项目的分数高低八成取决于特征工程而不是模型本身。LightGBM 是出了名的给好特征就出好结果反过来也一样——原始 394 列直接丢进模型AUC 通常只能做到 0.90 上下把特征工程做完整同样的模型能上到 0.95 左右。差距全在这几个动作里。3.1 内存优化float64 到 float32先让 394 列装得进内存59 万行乘以 394 列pandas 默认按 float64 读取时单表内存占用轻松超过 2GB再加上身份表、测试集和中间变量16GB 内存很快见底。这个项目场景下的标准做法是写一个降精度函数import numpy as np import pandas as pd def reduce_mem_usage(df: pd.DataFrame) - pd.DataFrame: 把数值列从 float64/int64 降到够用的精度内存直接减半 for col in df.columns: col_type df[col].dtype if col_type object: continue # 字符串列后面单独做编码 c_min df[col].min() c_max df[col].max() if str(col_type)[:3] int: if c_min np.iinfo(np.int8).min and c_max np.iinfo(np.int8).max: df[col] df[col].astype(np.int8) elif c_min np.iinfo(np.int16).min and c_max np.iinfo(np.int16).max: df[col] df[col].astype(np.int16) else: df[col] df[col].astype(np.int32) else: df[col] df[col].astype(np.float32) # 浮点一律压到 float32 return df train reduce_mem_usage(train)这个函数的逻辑是对整数列按取值范围选最小的整型int8/int16/int32对浮点列一律压到 float32。LightGBM 训练对数值精度不敏感float32 和 float64 的预测差异基本在 AUC 小数点后第四位但内存直接省一半。有个细节要注意边界判断用的是而不是如果某列的最小值恰好等于 -128 会被漏判自己复用时改成更稳妥。3.2 缺失值处理与 label encoding-1 占位是这类项目的默认动作V 列大量缺失、identity 表覆盖率只有四分之一意味着缺失值处理是整个特征工程里最绕不开的一步。常见的做法是先统计每列缺失率把缺失超过 80% 的列直接丢掉剩余列在训练里交给 LightGBM 的原生缺失值支持处理类别列则先转 codes用 -1 统一表示缺失/未知# 统计缺失率并丢弃高缺失列 missing_rate train.isna().mean() drop_cols missing_rate[missing_rate 0.8].index.tolist() train train.drop(columnsdrop_cols) # 类别列编码astype(category) 后缺失值自动映射为 -1 cat_cols [ProductCD, card1, card2, card3, card4, card5, card6, addr1, addr2, P_emaildomain, R_emaildomain] for col in cat_cols: train[col] train[col].astype(category).cat.codescat.codes会把每个类别编号成从 0 开始的整数缺失值自动编为 -1正好当作未知这一类别。要特别注意的是codes 编号只表示类别序号不代表大小关系——card5 的 7 并不比 3 大。所以这类特征喂给 LightGBM 时必须通过categorical_feature参数声明为类别特征否则模型会把编号当成有序数值来切分效果会变差。3.3 组合特征card1_card2 与 addr 的高基数黑匣子这个项目里最有名的一个技巧是组合特征。card1 到 card6 各自脱敏后仍然保留了一定的卡片辨识度把 card1 和 card2 拼成一个新类别等于构造出这张具体卡片的近似标识。模型就能学到这张卡过去是否有欺诈历史这种强规律train[card1_card2] (train[card1].astype(str) _ train[card2].astype(str)) train[card1_card2] train[card1_card2].astype(category).cat.codes这类组合特征基数很高很多组合在训练集里只出现一两次。我的习惯是构造完之后统计一下基数如果某个组合特征的唯一值数量接近样本量就说明它已经开始背样本了。此时要么做频次过滤出现次数小于阈值就归为 -1要么改用组合后的 count/mean 聚合特征保留信息的同时把基数降下来。这条经验在后面避坑章节还会再展开。3.4 时间与聚合特征从 TransactionDT 拆小时按 card1 算金额统计TransactionDT 是以秒为单位的相对时间戳直接当数值特征用只能让模型学会第几天信息密度太低。通过取模运算可以还原出小时和星期规律——欺诈交易在一天内的分布并不均匀这些周期性信号模型自己很难从原始秒数里学到train[hour] train[TransactionDT] // 3600 % 24 # 一天 24 小时 train[weekday] train[TransactionDT] // 86400 % 7 # 一周 7 天聚合特征方面最常用的是按 card1 分组统计金额的均值、标准差和最近一次金额train[amt_mean_card1] train.groupby(card1)[TransactionAmt].transform(mean) train[amt_std_card1] train.groupby(card1)[TransactionAmt].transform(std)transform会把聚合结果广播回每一行不需要再 merge也不会改变行数。这几个特征表达的是这张卡的历史平均金额和金额波动程度模型拿到它们之后就能识别出金额明显偏离卡片历史习惯的交易。groupby 聚合结果是按索引对齐的如果之前做过行筛选或排序务必先reset_index再聚合否则特征会错位——这个问题每年都有新人踩一次。4. 建模与训练五折交叉验证下 LightGBM 的完整链路特征工程做完接下来就是建模。整个训练链路设计成五折交叉验证 早停 OOF 汇总三段式这套结构几乎是表格类比赛的标准答案换任何二分类数据集都能复用。4.1 数据集划分StratifiedKFold 保持正负样本比例3.5% 的正样本率意味着如果用普通 KFold 随机切五折某些折里的欺诈样本数可能波动很大导致不同折的模型看到的数据分布不一致。StratifiedKFold 能保证每一折里 isFraud 的比例都和全量基本一致from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42)n_splits5是权衡后的选择折数太少验证集波动大折数太多则每折训练数据变少。shuffleTrue先打散再切分避免原始数据按时间排序后切出偏枯的折random_state42固定下来保证任何人复现时拿到完全一样的划分。这一点很重要——如果不固定随机种子每次跑出来的 AUC 都不一样你就永远说不清分数变化到底是改特征导致的还是随机波动导致的。4.2 LightGBM 参数与早停小学习率配大轮数模型主体用 LightGBM核心参数组合是小学习率 大树数 早停让模型在低风险下把容量吃满import lightgbm as lgb params { objective: binary, # 二分类 metric: auc, # 与竞赛评估口径一致 learning_rate: 0.01, # 小步长每轮只走一点 num_leaves: 64, # 单棵树叶子数控制复杂度 feature_fraction: 0.8, # 每棵树随机用 80% 的特征 bagging_fraction: 0.8, # 每棵树随机用 80% 的行 bagging_freq: 1, # 每轮都做行采样 min_child_samples: 20, # 叶子最少样本数防过拟合 verbosity: -1, # 别刷屏 } oof pd.Series(0, indextrain.index) for fold, (tr_idx, va_idx) in enumerate(skf.split(train, y)): tr lgb.Dataset(train.iloc[tr_idx], y.iloc[tr_idx], categorical_featurecat_cols) va lgb.Dataset(train.iloc[va_idx], y.iloc[va_idx], categorical_featurecat_cols) model lgb.train(params, tr, num_boost_round10000, valid_sets[va], callbacks[lgb.early_stopping(200)])learning_rate0.01配num_boost_round10000的意思是每棵树只学一点点靠轮数把精度堆上去这样比大学习率少树更稳定尤其适合高基数特征多的场景。early_stopping(200)指定验证集连续 200 轮不提升就停并把当前最好轮次记在best_iteration上。feature_fraction和bagging_fraction是列采样与行采样两个都开能显著降低特征之间的共线性影响——本项目里 V 列之间相关性很高这个设置价值尤其大。4.3 OOF 汇总与特征重要性不靠单折分数下结论五折训完把每一折验证集的预测拼回原位置得到 OOFout-of-fold预测。它等价于每个样本都被一个没见过它的模型预测了一次用这个输出去算 AUC才是真实泛化性能的估计from sklearn.metrics import roc_auc_score oof_score roc_auc_score(y, oof) print(fOOF AUC {oof_score:.5f})要注意 predict 时必须指定num_iterationmodel.best_iteration否则会用完整 10000 棵树预测而早停已经证明后面的树在验证集上是负贡献。特征重要性则用 gain 而非 split 来排序imp pd.DataFrame({ feature: train.columns, gain: model.feature_importance(gain), }).sort_values(gain, ascendingFalse) imp.head(20)gain 表示该特征在分裂中带来的信息增益总和比 split分裂次数更能反映实际贡献。通常跑完第一次就能看到 card1_card2、amt_mean_card1 这类特征排在最前面这时候你就能确信特征工程的方向没错。5. 常见坑与排查内存爆炸、验证集虚高和背题特征的翻车记录这套流程踩过的坑不少我挑四个最有代表性的写下来每条都是「现象 → 原因 → 解决」三段式你照着排查能省不少时间。5.1 读取后内存直接爆掉现象read_csv 加载 train_transaction.csv 后进程被杀或 Jupyter 内核崩溃查看任务管理器发现内存占用接近 20GB。 原因pandas 默认把数值列读成 float64 和 int64394 列堆下来非常占内存中间如果再复制一次 DataFrame内存直接翻倍。 解决读取时就指定 dtype 或用前面写的reduce_mem_usage函数第一时间压缩。另一个办法是先pd.read_csv(..., nrows1000)探测每列类型再带着精确的 dtype 字典全量读取两种方式都能把峰值内存压低到 4GB 以内。5.2 随机划分验证 AUC 虚高时序划分分数掉一截现象StratifiedKFold 随机划分时 OOF AUC 稳定在 0.95 以上换成按 TransactionDT 排序后切验证集AUC 立刻掉到 0.92 左右。 原因欺诈行为在时间上存在聚集性随机打散等于把未来的欺诈模式泄漏进了训练集。模型学到的是时间段规律而不是跨时间的泛化规律这在业务上不可用——线上永远只有过去的数据。 解决训练之前先做一致性检查跑两版实验一版随机划分、一版按时间顺序划分两个分数都记录下来。如果时间划分分数明显更低说明特征里时间相关信号过重优先剔除与 TransactionDT 强相关的列或者干脆以时间划分的分数作为主指标。5.3 加上 card1_card2 后训练 AUC 逼近 1验证集却波动很大现象引入组合特征后训练集 AUC 冲到 0.99但五折验证分数互相能差 0.02稳定不下来。 原因组合特征基数太高很多组合在训练集里只出现几次甚至一次模型直接把这些样本背下来了遇到没见过的组合就乱猜。 解决对组合特征做频次过滤把出现次数低于 10 的组合统一映射成 -1或者退一步用组合后的 count 特征每个组合出现的次数和金额聚合替代原始组合编码信息还在基数可控。记住一个判断标准特征唯一值数量超过样本量的 5%基本就是在背题。5.4 本地 AUC 正常提交分数对不上现象OOF AUC 0.95提交后分数远低于本地甚至接近随机。 原因测试集在特征工程阶段被 shuffle 或筛选过预测阶段输出 submission.csv 时行序没有对齐原始索引标签和特征错位。 解决从训练阶段就把流程固化下来——预测前test.reset_index(dropTrue)把索引重置干净后再做特征工程保存提交文件时按原始 TransactionID 排序并校验行数。这个坑属于典型的过程正确但结果全错光看本地分数根本发现不了。6. 二次开发思路把这套 Notebook 改造成自己的风控基线复现只是第一步这套资源真正的价值在于可以迁移到自己的业务数据上。我的改造路径分三步走。第一步是换数据源时的列名映射。拿自己系统的交易表过来先写一个字段映射字典把业务字段对齐到这套工程的主表字段上field_map { pay_time: TransactionDT, pay_amount: TransactionAmt, user_id: card1, merchant_id: card2, } train train.rename(columnsfield_map)映射完先跑 01_eda 看分布确认金额、时间两列的语义一致再往下走。第二步是把特征工程代码从 Notebook 里抽成独立模块训练和预测共用同一份feature_engineering.py避免两个 Notebook 各写一套特征逻辑。第三步才是换模型——补一个 XGBoost 或 CatBoost两个模型预测分数做加权平均用 OOF AUC 决定权重一般从 0.6/0.4 起步试。部署上线时我会把训练好的模型用booster.save_model(lgb.txt)导出成文本文件服务端加载后在线预测前先把原始请求字段塞进同一个特征工程函数保证训练和线上特征完全一致。这里最容易翻车的是线上字段缺失——请求里少一个字段函数要按缺失值逻辑兜底而不是抛异常返回 500。从那以后我每次拿到一个新数据集都会强制走一遍「列名映射 → 特征工程函数化 → 随机/时序双验证」这三步基本杜绝了训练和线上不一致的翻车。这个项目作为起点已经足够完整剩下的就是你自己的数据了希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表