☰
IEEE-CIS欺诈检测实战:Jupyter Notebook全流程与避坑指南
2026/10/12 15:27:16 网站建设 项目流程

简介:这份资源面向数据挖掘与风控建模学习者,围绕IEEE-CIS欺诈检测赛题提供一套完整的探索性数据分析(EDA)实践材料,帮助读者理解二分类欺诈识别任务中从数据清洗到特征构建的完整流程。压缩包共6个文件,以3个Python工具脚本、1个Jupyter Notebook、1个Markdown说明及1个gitignore配置为主,整体约1.02MB,其中脚本分别承担数据清洗、可视化报告与特征工程等职责,Notebook则承载深入分析过程。目前已有598人学习下载。读者可借助其中的分析笔记与工具函数,掌握缺失值处理、类别特征编码、欺诈样本分布观察等关键环节,并参考作者在Kaggle内核中简化EDA流程的思路,快速搭建自己的赛题分析框架,适合具备一定Python与机器学习基础、希望系统入门风控建模的读者参考。

1. 拆开 IEEE-CIS 欺诈检测数据集:一份 Jupyter Notebook 实战资源能帮你省掉多少试错

如果你正在找一份能直接跑通、结构清晰的欺诈检测实战代码,IEEE-CIS Fraud Detection 这个项目大概率已经出现在你的搜索结果里了。它来自 Kaggle 上那场著名的交易欺诈识别竞赛,核心是一份用 Jupyter Notebook 组织的完整方案,覆盖了从数据加载、特征工程、模型训练到结果提交的全流程。数据集本身包含约 59 万条训练交易记录和 50 万条测试记录,特征维度超过 400 列,涵盖交易金额、产品编码、信用卡信息、设备类型、邮箱域名等。这份 Notebook 资源的价值不在于它拿了多高的名次,而在于它把欺诈检测里最耗时的几个环节——类别特征编码、时间序列切分、不平衡采样、特征交互构造——用可复现的方式串了起来。适合谁?刚接触风控建模、想找一个完整 pipeline 练手的人;已经做过二分类任务、但对欺诈场景下正负样本极度不平衡(正样本占比约 3.5%)不知道怎么处理的人;以及需要一份能改改就能套到自己业务数据上的模板的人。

2. 数据加载与内存优化:59 万行 400 列怎么不爆内存

2.1 先搞清楚数据长什么样再动手

拿到 IEEE-CIS 数据集的第一件事不是急着pd.read_csv,而是先看目录结构和文件大小。数据集通常包含train_transaction.csv、train_identity.csv、test_transaction.csv、test_identity.csv四个主文件,外加sample_submission.csv。transaction 表是主表,identity 表是辅助信息,两者通过TransactionID关联。训练集 transaction 约 59 万行、394 列,identity 约 14 万行、41 列。直接全量读取,pandas 默认的 int64 和 float64 会把内存吃到 2GB 以上,如果机器只有 8GB 内存,后续做特征工程时很容易触发 OOM。

常见做法是先用nrows参数读几百行看看每列的 dtype 和缺失情况,再决定用什么类型加载。我一般会写一个 dtype 映射字典,把低基数的类别列转成 category,把数值列根据取值范围降到 int8/int16/int32 或 float32。

import pandas as pd import numpy as np # 先采样看结构 sample = pd.read_csv('train_transaction.csv', nrows=5000) print(sample.dtypes.value_counts()) print(sample.isnull().mean().sort_values(ascending=False).head(20)) # 构建 dtype 映射:类别列用 category,数值列降精度 def build_dtype(df): dtypes = {} for col in df.columns: if df[col].dtype == 'object': dtypes[col] = 'category' elif df[col].dtype == 'int64': dtypes[col] = 'int32' elif df[col].dtype == 'float64': dtypes[col] = 'float32' return dtypes dtypes = build_dtype(sample) train_trans = pd.read_csv('train_transaction.csv', dtype=dtypes) train_id = pd.read_csv('train_identity.csv', dtype=dtypes)

这段代码的逻辑是:先读 5000 行样本推断每列类型,再生成 dtype 字典用于全量加载。category类型对ProductCD、card4、card6、M1~M9这类低基数特征能大幅压缩内存,float32相比float64直接省一半。参数上,nrows控制采样行数,5000 行足够看出分布;dtypes字典的键是列名、值是目标类型。注意TransactionID和isFraud不要降成 float,前者是标识、后者是标签,保持 int 类型。

2.2 合并 transaction 和 identity 的正确姿势

两张表用TransactionID做 left join,以 transaction 为主表。identity 表只有约 24% 的交易有记录,合并后大量字段会出现缺失,这是正常的,不要用 inner join 把没 identity 的交易丢掉——那会损失四分之三的样本。

train = train_trans.merge(train_id, on='TransactionID', how='left') test = test_trans.merge(test_id, on='TransactionID', how='left') print(f'train shape: {train.shape}') print(f'fraud rate: {train["isFraud"].mean():.4f}') print(f'identity coverage: {train["id_01"].notnull().mean():.4f}')

合并后训练集约 59 万行、约 430 列。how='left'保证所有交易保留,identity 缺失的字段填 NaN。isFraud均值约 0.035,意味着正负比约 1:27,后面建模必须处理这个不平衡。identity 覆盖率约 0.24,说明大部分交易没有设备指纹信息,特征工程时要对 identity 字段的缺失模式单独处理,不能简单用均值填充。

提示:如果内存实在紧张,可以先把 transaction 表里TransactionAmt做 log 变换、把TransactionDT转成时间特征后,只保留重要性高的列,再和 identity 合并。

3. 特征工程:从 400 列里挖出真正有用的信号

3.1 时间特征与交易金额变换

TransactionDT是一个从某个参考点开始的秒数偏移,不是真实时间戳。直接当数值用没有意义,需要转成小时、星期、是否周末等周期特征。TransactionAmt是右偏分布,取 log 后更接近正态,对树模型和线性模型都更友好。

# 时间特征 train['hour'] = (train['TransactionDT'] / 3600) % 24 train['day_of_week'] = (train['TransactionDT'] / (3600 * 24)) % 7 train['is_weekend'] = train['day_of_week'].isin([5, 6]).astype(int) # 金额变换 train['amt_log'] = np.log1p(train['TransactionAmt']) train['amt_cents'] = (train['TransactionAmt'] - train['TransactionAmt'].floor()).round(2) # 按卡片聚合的金额统计 train['card_amt_mean'] = train.groupby('card1')['TransactionAmt'].transform('mean') train['amt_to_card_mean'] = train['TransactionAmt'] / (train['card_amt_mean'] + 1)

TransactionDT / 3600 % 24得到一天中的小时数,/ (3600*24) % 7得到星期几。is_weekend标记周末交易,欺诈在周末的分布往往和工作日不同。amt_log用log1p避免金额为 0 时报错。amt_cents提取小数部分,因为很多欺诈交易金额是整数,小数部分为 0 本身就是一个信号。card_amt_mean按 card1 分组算均值,amt_to_card_mean衡量当前交易金额相对该卡片历史均值的偏离程度,偏离越大越可疑。

3.2 类别特征编码与特征交互

IEEE-CIS 里有大量高基数类别特征,比如card1有上万种取值,addr1有几百种。直接用 one-hot 会维度爆炸,用 label encoding 又会引入虚假的大小关系。常见做法是:低基数用 one-hot,高基数用频率编码或目标编码,同时构造交叉特征。

from sklearn.preprocessing import LabelEncoder # 频率编码:把类别替换成出现次数 for col in ['card1', 'card2', 'addr1', 'P_emaildomain']: freq = train[col].value_counts() train[f'{col}_freq'] = train[col].map(freq) # 目标编码:用均值编码高基数特征(注意要在 CV 框架内做,避免泄漏) def target_encode(train_df, val_df, col, target, smooth=10): agg = train_df.groupby(col)[target].agg(['mean', 'count']) prior = train_df[target].mean() smooth_mean = (agg['mean'] * agg['count'] + prior * smooth) / (agg['count'] + smooth) return val_df[col].map(smooth_mean) # 交叉特征 train['card1_addr1'] = train['card1'].astype(str) + '_' + train['addr1'].astype(str) train['card1_addr1'] = LabelEncoder().fit_transform(train['card1_addr1'])

频率编码把card1的每个取值替换成它在数据集中出现的次数,出现次数异常少或异常多的卡片都值得关注。目标编码用目标变量的均值来编码类别,但必须加平滑项smooth,否则样本量少的类别会严重过拟合。card1_addr1交叉特征把卡片和地址组合起来,欺诈者可能用同一张卡配不同地址,或者同一地址配不同卡,这种组合的频次和欺诈率往往比单特征更有区分度。

注意:目标编码是欺诈检测里最容易翻车的地方。如果在全量数据上算均值再喂给模型,等于把标签信息泄漏进了特征,线下 AUC 会虚高到 0.99,线上直接崩。正确做法是在 K 折的每一折内,用训练折算编码、验证折做映射。

4. 不平衡处理与模型训练:为什么 AUC 高不代表模型能用

4.1 正负样本 1:27 的三种应对策略

欺诈检测的评估指标不是准确率,因为把所有交易都预测为正常也有 96.5% 的准确率。真正要看的是 AUC、召回率和精确率之间的权衡。处理不平衡常见三种做法:过采样(SMOTE)、欠采样、调整类别权重。SMOTE 在 400 维空间里对少数类插值,容易生成不真实的样本;欠采样会丢掉大量正常交易信息;调整scale_pos_weight或class_weight是成本最低、最不容易出错的方式。

from sklearn.model_selection import StratifiedKFold from xgboost import XGBClassifier from sklearn.metrics import roc_auc_score, average_precision_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) oof_preds = np.zeros(len(train)) feature_cols = [c for c in train.columns if c not in ['isFraud', 'TransactionID']] for fold, (tr_idx, val_idx) in enumerate(skf.split(train, train['isFraud'])): X_tr, y_tr = train.iloc[tr_idx][feature_cols], train.iloc[tr_idx]['isFraud'] X_val, y_val = train.iloc[val_idx][feature_cols], train.iloc[val_idx]['isFraud'] model = XGBClassifier( n_estimators=1000, max_depth=6, learning_rate=0.05, scale_pos_weight=27, # 负正比约 27:1 subsample=0.8, colsample_bytree=0.8, eval_metric='auc', early_stopping_rounds=50 ) model.fit(X_tr, y_tr, eval_set=[(X_val, y_val)], verbose=False) oof_preds[val_idx] = model.predict_proba(X_val)[:, 1] print(f'Fold {fold} AUC: {roc_auc_score(y_val, oof_preds[val_idx]):.4f}') print(f'Overall AUC: {roc_auc_score(train["isFraud"], oof_preds):.4f}') print(f'Overall AP: {average_precision_score(train["isFraud"], oof_preds):.4f}')

StratifiedKFold保证每折里正负比例和全集一致。scale_pos_weight=27让模型对少数类样本的损失加权,相当于告诉模型“漏掉一个欺诈样本的代价是误判一个正常样本的 27 倍”。early_stopping_rounds=50在验证集 AUC 连续 50 轮不提升时停止训练,防止过拟合。输出除了 AUC,还要看 AP(Average Precision),因为 AUC 在不平衡数据上容易给人过于乐观的错觉——AUC 0.9 可能对应 AP 只有 0.5。

4.2 用 LightGBM 做特征重要性筛选

XGBoost 跑完之后,用 LightGBM 再跑一遍做特征重要性排序,把重要性为 0 或极低的特征剔掉,既能加速后续迭代,也能减少噪声。

import lightgbm as lgb lgb_train = lgb.Dataset(X_tr, y_tr) lgb_val = lgb.Dataset(X_val, y_val, reference=lgb_train) params = { 'objective': 'binary', 'metric': 'auc', 'boosting_type': 'gbdt', 'num_leaves': 64, 'learning_rate': 0.05, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'scale_pos_weight': 27, 'verbose': -1 } model_lgb = lgb.train(params, lgb_train, valid_sets=[lgb_val], num_boost_round=1000, callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)]) importance = pd.DataFrame({ 'feature': feature_cols, 'importance': model_lgb.feature_importance('gain') }).sort_values('importance', ascending=False) print(importance.head(30)) low_imp = importance[importance['importance'] < 10]['feature'].tolist() print(f'低重要性特征数: {len(low_imp)}')

feature_importance('gain')返回每个特征在分裂时带来的增益总和,比split次数更能反映真实贡献。num_leaves=64控制树的复杂度,太大容易过拟合。bagging_fraction和feature_fraction都是 0.8,分别对样本和特征做子采样。跑完之后看重要性排名,通常TransactionAmt、card1、addr1、C13、V310这些会排在前列。把增益低于 10 的特征列出来,下一轮训练时直接 drop。

5. 避坑与排查:那些让线下 AUC 虚高、线上翻车的操作

5.1 目标编码在全量数据上做导致标签泄漏

现象:线下交叉验证 AUC 跑到 0.98 以上,提交到测试集后分数掉到 0.85 以下。原因:目标编码时用了全量训练数据的标签均值,验证折的编码里包含了验证折自身的标签信息。解决:把目标编码放进 K 折循环内部,每一折只用训练折算编码表,验证折和测试集用同一张表映射。如果嫌麻烦,至少用KFold加噪声的方式做平滑。

5.2 时间特征用错导致未来信息泄漏

现象:模型在验证集上表现很好,但按时间顺序切分后性能大幅下降。原因:TransactionDT是时间偏移,如果用随机 K 折,训练集里可能包含比验证集更晚的交易,模型学到了未来模式。解决:欺诈检测应该用时间序列切分,比如前 80% 时间做训练、后 20% 做验证。如果非要用 K 折,至少把TransactionDT作为分组依据,确保同一时间段的交易不跨折。

5.3 把 identity 缺失当成随机缺失处理

现象:填充 identity 字段的缺失值后,模型 AUC 反而下降。原因:identity 是否有记录本身就是一个强信号——有设备指纹的交易和没有的交易,欺诈率差异很大。直接填充会抹掉这个信号。解决:为每个 identity 字段加一个is_missing指示列,再把原始字段用中位数或众数填充。让模型同时看到“缺失模式”和“填充后的值”。

5.4 用准确率选模型导致漏掉大量欺诈

现象:模型准确率 96%,但召回率只有 10%,大部分欺诈交易被放过。原因:正样本只占 3.5%,模型只要全预测为正常就能拿到 96.5% 的准确率。解决:评估指标换成 AUC + AP + 在固定召回率下的精确率。业务上更关心的是“在召回 80% 欺诈交易的前提下,误报率能不能接受”。用precision_recall_curve找阈值,而不是用 0.5 做默认切分。

5.5 特征重要性筛选时把 ID 类特征留下

现象:TransactionID或card1的原始值排在重要性前列,但模型在新数据上表现不稳定。原因:ID 类特征的高基数让树模型可以记住训练样本,相当于过拟合。解决:把TransactionID从特征里剔除,card1只用它的频率编码或目标编码结果,不要用原始值。任何取值数接近样本量的特征都要警惕。

6. 进阶技巧:用贝叶斯优化调参并把 Notebook 变成可复用 pipeline

6.1 用 Optuna 替代手工调参

手工调 XGBoost 的max_depth、learning_rate、subsample组合,跑一轮要十几分钟,试错成本太高。Optuna 用 TPE 算法在参数空间里采样,通常 30~50 轮就能找到比手工更好的组合。

import optuna from sklearn.metrics import roc_auc_score def objective(trial): params = { 'n_estimators': trial.suggest_int('n_estimators', 500, 2000), 'max_depth': trial.suggest_int('max_depth', 4, 10), 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1, log=True), 'subsample': trial.suggest_float('subsample', 0.6, 1.0), 'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0), 'scale_pos_weight': trial.suggest_int('scale_pos_weight', 10, 50), 'eval_metric': 'auc', 'early_stopping_rounds': 50 } model = XGBClassifier(**params) model.fit(X_tr, y_tr, eval_set=[(X_val, y_val)], verbose=False) preds = model.predict_proba(X_val)[:, 1] return roc_auc_score(y_val, preds) study = optuna.create_study(direction='maximize') study.optimize(objective, n_trials=30, show_progress_bar=True) print(f'Best AUC: {study.best_value:.4f}') print(f'Best params: {study.best_params}')

trial.suggest_int和suggest_float定义搜索空间,log=True让学习率在对数尺度上采样,因为 0.01 和 0.1 的差异比 0.09 和 0.1 的差异重要得多。n_trials=30是精度和耗时的折中,如果机器允许可以加到 100。跑完之后用study.best_params重新训练最终模型。

6.2 把 Notebook 拆成可复用的 pipeline

Notebook 适合探索,但不适合反复跑。把数据加载、特征工程、训练、预测拆成独立的 Python 模块,用argparse或配置文件控制参数,才能真正复用到新数据上。

模块职责关键函数
data_loader.py读取 CSV、合并表、类型转换load_and_merge()
feature_engineer.py时间特征、金额变换、编码、交叉build_features()
trainer.pyK 折训练、早停、保存模型train_model()
predictor.py加载模型、生成提交文件predict()

我一般会在feature_engineer.py里把目标编码单独抽成一个类,初始化时传入smooth和cv参数,fit时只接受训练折数据,transform时对验证折和测试集做映射。这样即使换一份新的交易数据,只要字段名一致,改一下配置文件就能跑通。

从那以后我每次做欺诈检测项目,都会先把时间切分和编码泄漏这两件事检查一遍,再开始调模型。参数可以慢慢试,但数据泄漏一旦发生,后面所有工作都是白费。希望帮到你。

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

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

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

立即咨询