简介机器学习在金融风控与反欺诈场景中面临的核心挑战往往不是模型复杂度而是极端不平衡的样本分布——真实交易中欺诈占比常低于千分之一常规准确率评估容易产生“模型很准”的幻觉。解决这一问题的关键在于理解数据预处理、特征筛选、标准化与重采样技术的正确顺序其中SMOTE通过合成少数类样本有效缓解类别失衡而Stacking集成学习则能融合多种模型的判断视角提升欺诈样本召回率。本文从不平衡分类的基础原理出发结合实际工程流程剖析标准化泄漏、SMOTE切分顺序、PR-AUC评估与阈值移动等高频踩坑点帮助读者构建一套可复现、可解释的金融反欺诈检测模型。1. 金融反欺诈检测的 python 机器学习实战为什么这份源码值得照着重跑金融反欺诈检测用 python 机器学习来做进门第一脚踩到的往往不是模型调参而是样本极端不平衡真实交易里欺诈占比常低于千分之一模型全预测成正常准确率也能超过 99%。这份课程设计正是围绕这一点展开的从数据预处理、特征筛选、标准化、SMOTE 采样到 stacking 集成学习把整条反欺诈建模流程走完最后产出可跑的反欺诈模型.ipynb、HTML 展示页和数据报告。拆这份源码时我更在意几个关键顺序有没有踩坑SMOTE 到底应该在切分前还是后做、标准化能不能在测试集上再 fit 一次、评估该用准确率还是召回率。这些细节决定了你是真正会做不平衡分类还是只把代码跑通然后交差。期末作业、课程设计选手或者对 SMOTE 和 stacking 还停留在概念层的从业者跟着这条链路走一遍能把“理论 → 数据 → 模型 → 评估”整个闭环一次性打通。2. 数据预处理缺失值、高维特征与标准化的三板斧2.1 读入数据与字段体检先看形状和缺失别急着建模解压creditcard.rar后拿到的是一张交易明细表第一行是表头。我一般不会直接丢给模型而是先用一段探数代码把数据“摸”一遍确认形状、缺失和类别分布。import pandas as pd import numpy as np # 解压后的交易数据第一行是表头直接读入 df pd.read_csv(creditcard.csv) print(数据形状: , df.shape) print(字段类型统计:) print(df.dtypes.value_counts()) # 缺失值要逐列看而不是只看总数 missing df.isnull().sum() print(逐列缺失情况:) print(missing[missing 0]) # 类别分布先判断欺诈占比 print(类别分布:\n, df[Class].value_counts()) print(欺诈占比: {:.4f}%.format(df[Class].mean() * 100))df.shape第一眼确认行数和列数重点观察是不是典型的 30 万行量级、31 列左右dtypes.value_counts()看有没有字段被错误读成 object比如金额列带逗号时就会被读成字符串缺失值必须逐列输出缺三行和缺一半的处理策略完全不同Class.mean()在 0/1 编码下直接等于欺诈样本占比这个缩写写法在反欺诈代码里很常见。这里有一个很容易忽略的点反欺诈数据的缺失值通常不多但如果发现某一列缺失比例超过 30%就不要用均值填充硬补了直接删列更稳。项目源码里的处理顺序是“先看缺失 → 再做特征筛选”这个顺序不能反因为后面标准化会把缺失值问题放大。资源包里几个文件的定位也可以先对齐方便后面对照着看资源内文件在流程中的位置creditcard.rar原始交易数据集解压后读入反欺诈模型.ipynb从数据体检到 stacking 评估的主流程反欺诈模型.htmlnotebook 静态导出不装环境也能看结果README.md环境版本与运行说明项目报告.pptx答辩 / 汇报用数据报告2.2 特征筛选去掉 Time 与高相关特征让模型聚焦这种交易数据的特征大多是脱敏后的 PCA 结果V1~V28字段名不告诉你业务含义所以特征筛选只能靠统计手段不能靠拍脑袋。常见做法是双通道筛选先看每个特征与标签的相关性再看特征之间的相关性冗余最后用随机森林重要性做交叉确认。# 计算每个特征与标签的相关系数绝对值降序排列 corr df.corr()[Class].abs().sort_values(ascendingFalse) print(corr.head(15)) # 随机森林做二次筛选看重非线性关系 from sklearn.ensemble import RandomForestClassifier X_raw df.drop(columns[Class]) y_raw df[Class] rf_probe RandomForestClassifier(n_estimators80, random_state42, n_jobs-1) rf_probe.fit(X_raw, y_raw) importance pd.Series(rf_probe.feature_importances_, indexX_raw.columns) print(importance.sort_values(ascendingFalse).head(15))n_estimators80对临时探数已经够用random_state42固定种子保证每次筛出来的结果一致n_jobs-1让多核 CPU 并行训练。相关系数和随机森林重要性两个通道互补前者抓线性关系后者抓非线性与组合关系两者都垫底的特征才值得删除。关于 Time 字段反欺诈场景有争议有些业务上认为凌晨交易更可疑但大多数课程设计里 Time 会造成时间泄漏而且它和欺诈标签之间没有稳定的业务因果我一般直接 drop。注意“特征筛选”不是简单地取 top N 个特征还要看特征间相关性。如果两个特征相关系数超过 0.9建议只保留其中一个否则进入逻辑回归或 stacking 后权重解释会失真。2.3 标准化与切分先切数据、再 fit 训练集顺序错一个结果全变味V1~V28 是 PCA 后的标准正态分布特征但 Amount 的量纲可能是几十到几千Time 的量纲是秒级。逻辑回归这类带梯度或距离度量的模型遇到这种量纲差异大数值特征会直接主导训练过程所以标准化不能省。from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 先做训练/测试切分再做标准化 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 关键顺序只 fit 训练集测试集只用 transform scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)fit_transform在训练集上计算均值和方差并完成转换transform只把同一组参数套到测试集上不重新计算。如果对测试集也来一次fit_transform等于建模时偷看了测试集分布这叫数据泄漏后面所有评估数字都会虚高。stratifyy保证切分后训练集和测试集的欺诈比例保持一致否则随机切分可能把本来就只有几百条的欺诈样本全分到测试集里。标准化放切分之后做是因为测试集实际上是“未来数据”你不可能拿未来数据的均值和方差去修正历史。这一点在答辩时经常被问到源码里如果顺序写反了整套流程的可信度会大打折扣。3. 不平衡数据处理SMOTE 采样原理与参数设置3.1 为什么随机过采样不够SMOTE 的合成逻辑欺诈样本占比常常在 0.1%~1% 之间直接训练出来的模型几乎只会说“正常”。最原始的解法是随机过采样也就是把少数类样本复制若干份但这样模型看到的是同一批样本的复制品练着练着就过拟合了它记得住这些样本本身却学不到欺诈样本的分布规律。SMOTESynthetic Minority Over-sampling Technique的做法是在特征空间里合成新样本而不是复制旧样本。它的核心逻辑是对每一个少数类样本先找出它在特征空间里的 k 个近邻然后在当前样本与某个近邻的连线上随机取一个插值点作为新合成样本。新样本不是凭空捏造而是两个真实样本的“中间态”理论上能缓解过拟合。用一句话概括随机过采样复制的是“过去”SMOTE 生成的是“附近”。对反欺诈这种特征维度高、样本量大的场景SMOTE 明显更合适。当然它也有代价——合成样本里的业务含义不可解释所以最终评估时一定要用测试集上的真实数据不能用合成数据自欺欺人。3.2 SMOTE 实现k_neighbors、sampling_strategy 与 fit_resample 的用法项目里用的是 imbalanced-learn 库直接调用 SMOTE 类核心方法是fit_resample它一次性完成“拟合 重采样”返回新的特征矩阵和标签数组。from imblearn.over_sampling import SMOTE from collections import Counter smote SMOTE( sampling_strategyauto, k_neighbors5, random_state42 ) # 输入是上一章标准化后的训练数据 X_train_res, y_train_res smote.fit_resample(X_train_scaled, y_train) print(重采样后训练集分布:, Counter(y_train_res))sampling_strategyauto表示把少数类过采样到与多数类等量也可以写成0.5表示少数类达到多数类的一半这样能控制合成样本总量减少噪声放大。k_neighbors5是每个少数类样本插值时参考的近邻数量这个值需要结合实际样本量调整random_state42固定插值过程的随机种子保证每次跑出来的合成样本一致方便复现。注意这里传给 SMOTE 的是X_train_scaled也就是标准化之后的训练集。标准化能保证插值时特征的量纲一致不然金额这种大数值特征会主导“近邻”的计算。重采样后的训练集分布应该会看到少数类数量被拉平多数类数量不变这样后续模型就不会一边倒地偏向多数类了。提示如果你安装的 imbalanced-learn 和 sklearn 版本不匹配调用 SMOTE 时可能报TypeError或版本冲突错误常规处理是升级这两个库到兼容版本后重装。3.3 SMOTE 的边界问题噪声放大与重叠样本怎么办SMOTE 不是万能的它在两类样本边界重叠严重的场景下反而会放大噪声。比如少数类样本本来就夹杂在多数类中间SMOTE 在它与近邻之间插值等于把边界噪声也复制了一遍模型训练出来的边界会更混乱。我实际用下来有两个经验一是当数据里噪声点比较多时优先用 SMOTEENN 或 SMOTETomek它们在插值之后会额外清洗掉边界上的冲突样本二是如果合成后模型的训练分数高得离谱、但测试集分数明显低一截先怀疑是不是 k_neighbors 选大了导致合成样本越过真实边界。from imblearn.combine import SMOTEENN # SMOTE 之后再用 ENN 清洗边界样本减少噪声放大 smote_enn SMOTEENN( smoteSMOTE(k_neighbors5, random_state42), random_state42 ) X_train_res, y_train_res smote_enn.fit_resample(X_train_scaled, y_train) print(SMOTEENN 后训练集分布:, Counter(y_train_res))SMOTEENN 的用法和 SMOTE 完全一致smote参数用来传入你配好的 SMOTE 实例内部先合成再清洗。代价是清洗后少数类样本量会低于多数类不再是一比一但这反而更接近真实分布。如果你发现训练出来的模型在测试集上召回率一直上不去往这个方向调比盲目增加合成样本量更有效。4. 模型构造stacking 集成学习的结构与调参4.1 stacking 与 bagging、boosting 的核心区别前面几步做完训练集已经变成了“标准化的真实多数类 合成的少数类”。接下来要解决的问题是怎么让多个模型的判断互相弥补。这里项目选的是 stacking 集成学习而不是随机森林或 XGBoost 的单模型。三个集成方向的差异可以用一张表说清集成方式组合思路典型代表在反欺诈里的定位bagging并行训练多个模型投票或平均随机森林降方差抗噪声boosting串行拟合上一轮残差XGBoost降偏差榨特征关系stacking基学习器输出喂给元学习器StackingClassifier组合不同算法视角stacking 的工作方式分两层第一层用 K 折交叉验证让每个基学习器对训练集每一条样本生成一个“未见己”的预测第二层把这些预测概率作为新的特征输入训练一个元学习器来决定“这个模型的话该听多少”。它和 bagging 的最大区别不是投票而是让元学习器去学习基学习器之间的组合权重。在反欺诈场景里不同算法对欺诈的判断视角不同stacking 能把这种差异变成提升。4.2 搭一个三模型 stacking两个基学习器加一个逻辑回归收口项目里的模型构造用的是 sklearn mlxtend 的组合基学习器选随机森林和 XGBoost元学习器用逻辑回归。这个组合在表格型数据上很稳随机森林擅长抓特征交互XGBoost 擅长逐步拟合残差逻辑回归则负责把两边概率校准成最终预测。from mlxtend.classifier import StackingClassifier from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier # 基学习器 1随机森林 base_rf RandomForestClassifier( n_estimators200, max_depth8, random_state42, n_jobs-1, class_weightbalanced ) # 基学习器 2XGBoost base_xgb XGBClassifier( n_estimators200, learning_rate0.1, max_depth4, random_state42, eval_metriclogloss, use_label_encoderFalse ) # 元学习器逻辑回归把两个基模型的概率组合起来 meta_lr LogisticRegression(max_iter1000) stack_model StackingClassifier( classifiers[base_rf, base_xgb], meta_classifiermeta_lr, use_probasTrue, cv5 ) stack_model.fit(X_train_res, y_train_res)classifiers传入基学习器列表meta_classifier是元学习器use_probasTrue让元学习器接收概率而不是 0/1 硬预测少损失信息cv5表示每个基学习器在训练时做五折交叉预测避免基学习器用自己的训练标签直接当元特征减小过拟合。用逻辑回归做收口除了概率校准好之外还有一个现实原因反欺诈模型需要向业务方解释“为什么这笔交易被判欺诈”。逻辑回归的系数能直观反映哪些基模型输出更可信比直接用另一个黑箱模型做 meta 更容易讲清楚。4.3 关键参数说明概率输出、交叉验证与类别权重几个参数值得单独展开。class_weightbalanced加在随机森林上这是和 SMOTE 并存的双保险SMOTE 从样本层面拉平分布类别权重从损失函数层面拉平惩罚两者一起用即使合成样本有噪声模型也不会完全忽略少数类。use_label_encoderFalse是针对新版 XGBoost 的兼容参数老版本不传会报警告eval_metriclogloss指定评估指标避免版本更新后默认指标歧义。max_depth4比默认值浅因为反欺诈特征经过 PCA 后信息已经相对浓缩树太深反而拟合到合成样本的噪声里。如果不想引入 mlxtendsklearn 也自带了StackingClassifier写法稍有不同from sklearn.ensemble import StackingClassifier stack_model StackingClassifier( estimators[(rf, base_rf), (xgb, base_xgb)], final_estimatormeta_lr, cv5, stack_methodpredict_proba )estimators需要带名字的元组列表stack_methodpredict_proba等价于 mlxtend 的use_probasTrue。两种写法训练逻辑一致选哪种取决于你环境里哪个库装得顺手。参数上唯一要注意的是基学习器不要太强也别太弱。太强会让元学习器只信任一个模型失去“组合”的意义太弱则喂给元学习器的特征没有区分度。深浅和树数量要控制在训练时间可接受的范围内。5. 避坑手记数据泄漏、准确率幻觉与重采样顺序混乱5.1 坑一先 SMOTE 再切分数据验证结果一片大好上线就翻车现象很多人拿到数据后先把整份数据喂给 SMOTE再切训练集和测试集结果测试集上的准确率、召回率都高得惊人答辩现场很漂亮但部署到真实交易上立刻崩。原因SMOTE 在全量数据上合成样本时会参照少数类样本的近邻生成插值。切分之后一条少数类合成样本和它的“近邻源样本”很可能分别落进训练集和测试集等于测试集里混进了训练数据的影子。模型在测试集上看到的不是“未来数据”而是训练样本的亲戚评估结果当然虚高。这属于典型的数据泄漏。解决顺序必须是“先切分再 SMOTE”而且 SMOTE 只能作用在训练集上。# 正确顺序先切分再重采样 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) smote SMOTE(random_state42) X_train_res, y_train_res smote.fit_resample(X_train, y_train) # 测试集保持原始分布不做任何重采样从那以后我只要看到有人把fit_resample写在train_test_split前面就会多问一句“你的测试集是不是已经脏了”。这个坑在课程设计里出现频率极高也是最容易被答辩老师一眼看穿的。5.2 坑二只看 accuracy模型精准抓不到欺诈现象模型输出准确率 99.8%看起来神勇无比。但把混淆矩阵拉出来一看欺诈样本的召回率只有 0.1几乎一个都没拦住。整个模型其实是“全体预测为正常”的复读机。原因准确率对不平衡数据极度不敏感。欺诈占比 0.17% 时模型什么都不学全预测为正常类准确率也有 99.83%。准确率这个指标在极度不平衡的二分类里没有任何区分能力它只会给你制造“模型很准”的幻觉。解决放弃用 accuracy 做唯一标准改用混淆矩阵加 PR-AUC。反欺诈场景里真正该问的问题是“真实欺诈里拦住了多少”召回率和“拦下来的里面有多少误伤”精确率而 PR-AUC 同时看这两个指标随阈值变化的综合表现比 ROC-AUC 对不平衡数据更敏感。指标容易被它骗的地方accuracy多数类占比太高99% 不代表抓得住欺诈precision 单独看可能只拦了十笔全中但漏了剩下的recall 单独看可能拦下一堆但误伤正常用户PR-AUC对不平衡最敏感适合做横向对比5.3 坑三对测试集又跑了一次 StandardScaler评估虚高现象离线评估时模型分数很高但把同一套代码换个时间段的数据预测效果明显下滑训练集和测试集的分布似乎对不上。原因有人在预处理阶段对 X_train 和 X_test 分别调用了fit_transform导致测试集的均值和方差被重新计算。模型训练时看到的是“以测试集分布为基准”的特征测试时就等于提前偷看了答案。更隐蔽的是这种错误在评估阶段不报错、不警告只会让数字悄悄变好很难被发现。解决标准化只允许对训练集fit测试集只允许transform。如果整个特征处理流程比较复杂更保险的做法是用 sklearn 的Pipeline把标准化、SMOTE、模型串成一条流水线让切分和 fit_resample 的边界在结构上不可越界。from sklearn.pipeline import Pipeline from imblearn.pipeline import make_pipeline # 用 imblearn 的 pipeline确保 SMOTE 只作用于训练集 pipe make_pipeline( StandardScaler(), SMOTE(random_state42), stack_model ) pipe.fit(X_train, y_train)用 pipeline 之后fit和predict的边界由框架强制约束从根上杜绝“测试集再 fit 一次”的错误。6. 验证方法从混淆矩阵到 PR-AUC 给反欺诈模型验明正身6.1 先看混淆矩阵再看分类报告模型训练完成只是开始验证才是真正见真章的地方。我拿到任意一个反欺诈模型第一件事永远是拉混淆矩阵而不是看训练损失或准确率。from sklearn.metrics import confusion_matrix, classification_report, precision_recall_curve, auc # 用保持原始分布的测试集做预测 y_pred stack_model.predict(X_test_scaled) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred)) # 计算 PR-AUC作为不平衡场景的核心指标 y_prob stack_model.predict_proba(X_test_scaled)[:, 1] precision, recall, _ precision_recall_curve(y_test, y_prob) print(PR-AUC:, auc(recall, precision))混淆矩阵的四象限里右下角“预测为欺诈且确实是欺诈”的值是核心右上角“把正常用户误判为欺诈”决定了业务投诉量。分类报告里的召回率回答“抓到了多少”精确率回答“误伤了谁”。两者不可偏废。6.2 阈值移动默认 0.5 几乎永远不是最优解反欺诈模型最后输出的是概率默认阈值 0.5 通常太保守。在欺诈占比极低的数据上哪怕概率只有 0.2也可能值得人工复核。调阈值的方法是画出 PR 曲线看精确率和召回率的交叉位置或者根据业务成本设定目标宁可多误伤几笔换取高召回还是宁可漏掉一部分换取低打扰。# 找召回率不低于 0.8 时的最大精确率阈值 target_recall 0.8 for threshold in [0.3, 0.2, 0.15, 0.1, 0.05]: y_prob_tmp (y_prob threshold).astype(int) cm_tmp confusion_matrix(y_test, y_prob_tmp) recall_tmp cm_tmp[1, 1] / (cm_tmp[1, 0] cm_tmp[1, 1]) precision_tmp cm_tmp[1, 1] / (cm_tmp[0, 1] cm_tmp[1, 1]) print(fthreshold{threshold}, recall{recall_tmp:.3f}, precision{precision_tmp:.3f})这个循环把阈值从 0.5 一路降到 0.05能直观看到“更低的阈值换来更高召回但代价是误报增多”。资源包里如果只给了默认阈值的评估结果建议自己补上这一步答辩时这段比背概念有说服力得多。从那以后我每次拿到这类不平衡数据都会强制走三件事先切分再 SMOTE标准化只 fit 训练集评估只用混淆矩阵和 PR-AUC顺手再做一遍阈值扫描。这套流程帮我绕过了不止一次“模型很准但实际抓不到欺诈”的翻车希望帮到你。想完整复现就用资源包里的反欺诈模型.ipynb按 README 搭好环境把路径替换成你解压出的creditcard.csv从头跑到尾再自己改两版特征和阈值理解会完全不一样。本文还有配套的精品资源点击获取