信用卡欺诈检测实战:随机森林如何处理不平衡分类

发布时间:2026/9/7 21:09:23
信用卡欺诈检测实战:随机森林如何处理不平衡分类 1. 项目概述与场景理解1.1 为什么信用卡欺诈检测是机器学习入门的“黄金场景”做风控建模这些年我见过太多人一上来就奔着深度学习、图神经网络去结果卡在数据预处理上大半个月连个能用的基线模型都跑不出来。信用卡欺诈检测这个场景之所以经典恰恰因为它足够“小而全”数据量适中、特征维度清晰、标签明确同时又包含了类别不平衡、评估指标选择、阈值调优这些真实业务里躲不开的硬骨头。这个项目的核心任务其实很朴素给定一笔交易的特征判断这笔交易是正常交易还是欺诈交易。数据层面常用的公开数据集是欧洲持卡人两天内的真实交易记录一共28万多条样本其中欺诈样本只有492条占比约0.172%。就是这么个极度失衡的数据让很多初学者第一次体会到“准确率99.8%但模型毫无用处”的反差。举一个我在实际项目中遇到的例子在某金融机构的线上支付风控中初版模型准确率做到了99.9%上线后发现欺诈召回率只有可怜的12%。这意味着100笔欺诈交易里有88笔漏掉了而风险团队每天要人工复核几万条被误判的交易。这就是典型的“只看准确率”的坑。而随机森林在这个场景下恰好能以相对简单的原理、较少的数据清洗成本提供一个非常扎实的基线再配合阈值调整和代价敏感策略能在真实业务里跑出不错的效果。这也是我写这篇实战笔记的原因如果你正在学习机器学习或者刚进入风控领域想找一个能真正反映业务问题、又不至于被理论劝退的入门项目信用卡欺诈检测配随机森林是最合适的组合。1.2 这个项目适合谁能解决什么问题我建议三类人可以认真跟一遍这个项目第一类是刚学完决策树和集成学习理论、想上手第一个完整项目的同学你会在这里看到数据标准化、SMOTE过采样、交叉验证调参等一堆概念到底是怎么串起来的第二类是已经在做数据分析和SQL取数、想转算法岗的从业者这个项目可以帮助你建立“从数据到上线”的完整链路认知第三类是做信贷风控、反欺诈相关业务的同学虽然生产环境会用更复杂的方案但随机森林作为效果对照基线几乎所有团队都会保留一个。这个项目能解决的问题也很清晰第一理解随机森林在类别不平衡数据上的表现边界知道它什么时候值得用、什么时候需要换方案第二掌握一套可复现的代码流程从数据加载到特征处理再到模型评估每一步都有据可循第三真正理解为什么风控场景要用召回率、精确率、AUC和PR曲线而不是只看准确率。这些东西在你面试时随便深挖一问都能聊半小时。2. 核心思路拆解为什么是随机森林2.1 随机森林的原理回顾与优势分析随机森林的本质是Bagging思想加随机特征选择。Bagging就是有放回地随机抽取样本构建多棵决策树最后投票出结果而随机特征选择则是在每棵树的每个分裂节点不是从全部特征里找最优切分点而是随机抽一个特征子集来找。这两个随机性叠加带来的直接好处是树与树之间的相关性降低集成后的模型方差减小泛化能力更强。用通俗的话讲随机森林就像是一个公司请了几百个顾问来给决策投票每个顾问只基于自己看到的部分资料给出判断而且每个顾问看的资料还不完全一样最后少数服从多数。因为每个顾问的“偏见”都不同集体的判断反而更稳、更准。单个决策树容易过拟合但几百棵树一平均过拟合就会被明显抑制。实际使用中随机森林有几个非常实用的特性。第一它能直接输出特征重要性这在风控场景里是刚需我需要知道哪些字段对判断欺诈贡献最大比如交易金额、交易时间段、商户类型等第二它对异常值和缺失值不太敏感信用卡交易数据里经常有异常大额交易或者个别字段缺失随机森林的处理成本远低于XGBoost和神经网络第三训练过程可以并行化在几百万样本上训练几百棵树几分钟就能跑完这在快速迭代验证想法时特别重要。2.2 为什么不用逻辑回归、XGBoost或深度学习很多初学者会问既然有这么多模型为什么单挑随机森林来讲我每次都会给出一个非常现实的回答在欺诈检测这个场景模型选型要看业务阶段和数据基础。逻辑回归确实是风控领域的老牌模型可解释性极强线上部署简单但它的核心劣势在于需要手动做大量的特征交叉和变换对非线性关系的拟合能力有限。欺诈模式往往是复杂的非线性组合比如“深夜大额新设备异地历史无交易”逻辑回归很难自动捕捉这种组合模式。XGBoost和LightGBM在精度上通常优于随机森林这是事实。但它们的调参空间更大对特征工程的要求也更高新手很容易陷入调参泥潭。而且在小样本不平衡场景下XGBoost如果不做充分的防过拟合处理很容易在训练集上表现完美、在测试集上崩掉。我见过不少人一上来就上XGBoost结果AUC还不如随机森林原因就是没有控制好树深度和学习率。深度学习模型比如自编码器做异常检测在不平衡场景下有它的独特优势但需要的数据量、算力和调参经验都比较高而且可解释性差在强监管的金融场景里落地阻力很大。相比之下随机森林提供了一个“性价比”极高的方案代码量少、容错率高、效果在基线以上、特征重要性可以直接用。先用它把完整流程跑通再根据业务需要去尝试更复杂的模型这条路最稳。3. 数据准备与特征工程细节决定成败3.1 数据集选取与初始观察项目里我用的是经典的Credit Card Fraud Detection数据集Kaggle可以直接下载。这份数据包含28万笔交易30个特征其中V1到V28是PCA变换后的匿名特征目的是保护用户隐私另外还有Time距首笔交易经过的秒数和Amount交易金额两个原始特征标签列Class中0代表正常、1代表欺诈。拿到数据第一步不是建模而是做基础的描述性统计。我建议你输出一下每列的均值、标准差、缺失值数量以及标签的分布。28万多条交易里只有492条欺诈这个比例是极其悬殊的。如果不做任何处理直接把数据丢给模型模型会倾向于把所有样本预测为正常类因为这样准确率就能到99.8%以上。还要特别留意数据顺序问题。这份数据是按时间排序的如果用默认的train_test_split做随机切分训练集和测试集都会包含不同时间段的样本这种“随机打乱”在某些情况下是合理的但在真实风控场景里我们习惯按时间切分。因为欺诈模式会随时间变化用过去的数据训练、未来的数据验证才能检验模型真正的泛化能力。项目演示阶段可以先用随机切分但你要知道有这个区别。3.2 特征处理的三个关键动作第一个关键动作是Amount标准化。交易金额的取值范围很大从几毛钱到几千上万如果不做缩放决策树虽然不受量纲影响但在一些依赖距离计算的辅助分析比如聚类、KNN里会有大问题。更重要的是在后续做SMOTE过采样的时候K近邻算法对特征尺度极其敏感原始金额不缩放SMOTE生成的样本质量会很差。我用StandardScaler对Amount做标准化使均值为0、方差为1。第二个关键动作是Time特征的处理。原始Time是秒数直接作为特征效果一般更好的做法是提取小时信息将秒数除以3600取整得到交易发生的小时0到23。我做过对比实验用小时特征比用原始秒数的AUC略有提升原因是欺诈行为在特定时间段比如凌晨2点到5点有明显聚集特征而绝对秒数无法反映这种周期性。第三个关键动作是注意类别不平衡。有两个主流方向一个是算法层面调整随机森林的class_weight参数让模型在训练时给欺诈样本更高的惩罚权重另一个是数据层面用SMOTE生成少数类的合成样本。我个人的经验是先尝试class_weightbalanced因为它的效果稳定、代码简单、不改变样本分布不容易过拟合。SMOTE在某些情况下能提升召回率但如果原始特征噪声大或者维度高合成样本的质量很难保证需要配合调参仔细验证。3.3 数据切分的正确姿势我用70%的数据做训练、15%做验证、15%做测试。这里有个容易踩的坑如果只切一次模型的结果受随机种子影响很大所以我在全流程里固定random_state42方便复现和对比。如果你在跑实验时发现两次结果差异很大先检查是不是没有固定随机种子。另外要强调的是SMOTE必须在切分之后、且只在训练集上执行。如果先对整个数据集做SMOTE再切分会导致合成样本同时出现在训练集和测试集中测试集不再干净评估结果会被严重高估。这一点很多教程都没讲清楚但在实际项目中属于大忌务必记住。4. 随机森林核心参数实战解读4.1 主要超参数怎么设才靠谱随机森林需要重点关注的参数其实就五个n_estimators树的数量、max_depth最大深度、min_samples_split内部节点再划分所需最小样本数、min_samples_leaf叶子节点最少样本数、max_features每次分裂考虑的最大特征数。其余参数如max_leaf_nodes、criterion等在大多数场景保持默认即可。树的数量我建议从100开始试观察模型在验证集上的表现。超过200棵之后多数情况下收益会大幅递减但训练时间变长。实际项目里我经常用早停的思路先跑一个100到500的网格搜索找到性能趋于平稳的区间再定一个折中值而不是一味求多。max_depth是控制过拟合最重要的参数之一。如果数据量大、特征多可以放宽到20到30但如果数据量一般、噪声大建议限制在10左右。信用卡欺诈数据是典型的噪声大场景欺诈模式本身就很隐蔽树太深容易学到个别的噪声样本。我在这个项目里通过网格搜索确定max_depth12比默认的“不限制”效果更好。min_samples_split和min_samples_leaf是一对配合使用的参数。简单说min_samples_leaf50的意思是每个叶子节点至少要有50个样本才允许产生这会强制模型学习“大多数样本的共性”而不是为两三个样本单独建规则。这在小样本不平衡场景下特别有用能有效防止模型记住少数异常点。max_features是随机森林实现“随机性”的关键。分类问题推荐使用sqrt(总特征数)对信用卡这个30维特征的数据集来说就是取5到6个。如果你觉得模型方差大、不稳定可以适当调高这个值让每棵树看到更多特征。4.2 类别不平衡处理的两种思路对随机森林处理不平衡我在实际踩坑中总结的思路是先用class_weightbalanced如果召回率不够再用SMOTE。class_weightbalanced是sklearn内置的自动加权方式权重与类别频率成反比。欺诈样本数量约0.17%正常样本99.83%那么欺诈类别的权重约为正常类别的580倍。这个粗暴但有效的方法能让模型在选择分裂点时更关注那些被错误分类的少数类样本。SMOTE的思路与加权不同它是在特征空间中合成新的少数类样本。实现上用的是imbalanced-learn库核心代码如下from imblearn.over_sampling import SMOTE smote SMOTE(random_state42, k_neighbors5) X_train_res, y_train_res smote.fit_resample(X_train, y_train)SMOTE之后训练集里欺诈样本会增加到和正常样本一样多但这不代表模型一定会更好。合成样本可能在特征空间中处于不真实的区域导致模型学到一些虚假的规律。我在项目中分别试了三种方案不加处理、class_weightbalanced、SMOTE结果见后面的评估对比。整体结论是class_weight的性价比最高SMOTE在特定阈值下能提召回率但代价是精确率下降较多需要结合业务场景权衡。4.3 阈值调整被忽视的“免费午餐”很多教程讲完模型训练就结束了但风控业务里模型输出的其实是概率最终判不判欺诈取决于你设定的阈值。默认阈值是0.5意思是预测概率大于0.5才判为欺诈。但0.5这个阈值通常不是最优解。在欺诈检测这种极度不平衡的场景里模型输出的欺诈概率普遍偏低很多真实欺诈样本的概率只有0.1到0.3如果坚持用0.5召回率会非常低。我在项目里做了一次阈值扫描从0.1到0.9每隔0.05计算一次对应的精确率和召回率发现把阈值降到0.25左右时召回率能从45%提升到75%而精确率只下降了不到5个百分点。当然阈值不是越低越好阈值越低误报越多业务方的人力成本就越高。实际落地时阈值的选择要与业务方一起讨论明确“漏掉一笔欺诈损失多少钱”和“误判一笔正常交易损失多少钱”的代价比。这里给出一个实用的参考阈值召回率精确率误报数/万笔0.50.430.910.80.30.680.833.50.250.750.785.20.10.860.6112.75. 代码落地从训练到评估的完整流程5.1 环境准备与依赖安装我用的环境是Python 3.9核心依赖有pandas、numpy、scikit-learn、imbalanced-learn和matplotlib。建议新建一个虚拟环境避免依赖冲突这是我在项目里反复吃过亏后养成的习惯。具体命令如下conda create -n fraud python3.9 conda activate fraud pip install pandas numpy scikit-learn imbalanced-learn matplotlib这里特别说明一下imbalanced-learn不是sklearn自带的库需要单独安装。很多人一开始没装导入SMOTE时报ModuleNotFoundError还以为是自己代码写错了。5.2 完整训练评估代码下面是一份可以直接跑通的完整代码框架我在实际项目中经过多次迭代把关键环节都写进去注释了。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.metrics import (classification_report, roc_auc_score, precision_recall_curve, auc, confusion_matrix) from imblearn.over_sampling import SMOTE import matplotlib.pyplot as plt # 1. 数据加载和基础观察 df pd.read_csv(creditcard.csv) print(df.shape) print(df[Class].value_counts()) # 2. 特征工程 df[Hour] df[Time] // 3600 features [col for col in df.columns if col not in [Class, Time]] X df[features].copy() y df[Class].copy() # Amount标准化 scaler StandardScaler() X[Amount_scaled] scaler.fit_transform(X[[Amount]]) X X.drop(columns[Amount]) # 3. 数据切分 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy) # 4. 随机森林模型使用class_weight平衡类别 rf RandomForestClassifier( n_estimators200, max_depth12, min_samples_split20, min_samples_leaf50, max_featuressqrt, class_weightbalanced, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) # 5. 预测与评估 y_pred rf.predict(X_test) y_proba rf.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, y_proba)) # 6. 精确率-召回率曲线 precision, recall, thresholds precision_recall_curve(y_test, y_proba) pr_auc auc(recall, precision) print(PR AUC:, pr_auc) plt.figure(figsize(8, 6)) plt.plot(recall, precision, marker.) plt.xlabel(Recall) plt.ylabel(Precision) plt.title(Precision-Recall Curve) plt.show() # 7. 特征重要性 importances pd.Series(rf.feature_importances_, indexX.columns) print(importances.sort_values(ascendingFalse).head(10))5.3 代码里的关键细节说明这段代码有几处特别值得注意。第一train_test_split里我用了stratifyy参数它的作用是保持切分前后正负样本比例一致。如果不加这个参数切分时欺诈样本可能随机地全落进训练集或者测试集结果完全不可信。第二划分测试集时只用了一次切分80%训练/20%测试实际做调参时需要再分一部分验证集。初学者容易犯的错误是用测试集反复调参最后测试集泄漏了评估结果虚高。严谨的做法是训练集内部再做3到5折交叉验证来调参测试集只使用一次用来给出最终的泛化性能评估。第三predict_proba一定要用而且要取[:, 1]这一列。[:, 0]是预测为正常类的概率[:, 1]才是欺诈概率。这个细节我见到新手写错太多次了。第四n_jobs-1表示用全部CPU核心并行训练200棵树在这个数据量下不到30秒就能训完。随机森林天生可并行这是它比XGBoost更适合快速迭代的另一个优势。5.4 交叉验证与参数调优实战为了更严谨地确定参数我用GridSearchCV做了一次小范围的搜索。网格搜索的代码如下from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], max_depth: [8, 10, 12, 15], min_samples_leaf: [20, 50, 100], max_features: [sqrt, log2] } rf_base RandomForestClassifier( class_weightbalanced, random_state42, n_jobs-1 ) grid GridSearchCV( rf_base, param_grid, scoringroc_auc, cv3, verbose1, n_jobs-1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)这里有个经验值得分享我用scoringroc_auc而不是默认的accuracy。在这类不平衡问题上AUC是比准确率稳健得多的调参指标因为它不受阈值选择影响能直接反映模型区分正负样本的能力。网格搜索的结果是max_depth12、min_samples_leaf50、n_estimators200、max_featuressqrt与我的手工设置基本一致。搜索范围不建议一开始就铺得太大我在实践中的做法是先固定n_estimators200对max_depth和min_samples_leaf做粗筛缩小范围后再微调剩余参数。如果一开始就把所有参数的候选组合都塞进去组合数量指数级增长训练时间会非常痛苦。6. 模型评估风控指标的深度解读6.1 为什么准确率在信用卡欺诈场景是“陷阱”我见过太多人在项目汇报里写“模型准确率99.9%”结果被业务方一句“那我随便把所有交易都判为正常也有99.8%的准确率”怼得哑口无言。准确率公式是(TPTN)/(TPTNFPFN)在不平衡数据里TN正常交易判对了占了绝大多数即使模型完全不识别欺诈准确率也极高。这就是为什么风控领域要看Precision精确率、Recall召回率/灵敏度和它们的调和平均F1-score。精确率回答的问题是“模型预测为欺诈的交易里有多少真的是欺诈”召回率回答的问题是“所有真实欺诈交易里模型抓出了多少”。这两个指标天然存在矛盾把阈值调低抓得多但误报也多精确率下降把阈值调高误报少但漏报增加召回率下降。在实际生产中还有一个业务上更直观的指标每万个样本的误报数。比如阈值调到0.25时精确率0.78意味着每100个被标记为欺诈的交易中有22个是误报如果系统每天处理10万笔交易那就要人工复核2200笔。风控团队会据此评估人力成本能否接受。6.2 AUC与PR曲线的使用边界AUCROC曲线下面积是衡量模型排序能力的经典指标值越大说明模型把正样本排在负样本前面的能力越强。在信用卡欺诈场景下AUC通常能到0.95以上看起来非常漂亮。但AUC有个问题ROC曲线对类别不平衡不敏感即便欺诈样本只占0.17%ROC的形状也基本不受影响它反映的是整体的排序质量而不是实际抓欺诈的能力。PR曲线精确率-召回率曲线则完全不同。PR曲线下面积对少数类样本的表现极其敏感它直接刻画了模型在正样本很少时对正类的识别能力。在不平衡数据里PR曲线比ROC曲线更有区分度也更接近业务关切。所以我在项目报告里会同时给出AUC和PR AUC两个值。一个直观的对比假设两个模型的AUC都是0.96但它们的PR AUC可能是0.72和0.81差异显著。AUC接近不代表实际抓欺诈的能力接近。在风控场景中PR AUC才是更值得关注的指标。6.3 混淆矩阵的实战解读评估模型不能只看汇总指标我习惯在项目里打印出混淆矩阵逐项分析TP、FP、FN、TN。举个例子在一次实验中测试集有56876笔交易其中欺诈样本98笔TP72模型正确抓出了72笔欺诈交易FN26有26笔欺诈交易漏掉了这是最危险的FP14有14笔正常交易被误判为欺诈会造成用户困扰TN56774正确识别了56774笔正常交易从这个矩阵里可以看出召回率72/(7226)73.5%精确率72/(7214)83.7%。如果风控团队人力充足我们可能想进一步提升召回率到85%以上这时可以把阈值从0.45调整到0.25代价是误报增加。这个“在人力成本与风险损失之间找平衡”的过程就是风控模型落地的本质。7. 常见问题与排查技巧实录7.1 五个高频报错及解决方案第一个问题SMOTE报ValueError: Expected n_neighbors n_samples。原因是欺诈样本数量太少492条而SMOTE默认需要每个样本找到5个同类近邻如果某个样本的邻居数不足就会报错。解决办法是调小k_neighbors参数比如k_neighbors3或者先对少数类做简单的重复采样。我在项目中把k_neighbors设为3解决了这个问题。第二个问题predict_proba警告UserWarning: class labels too big。这个问题通常在标签不是0/1而是其他值时出现比如把Class列读成了字符串。解决办法是确认y是整数类型用df[Class].astype(int)强制转换。第三个问题训练时间过长。树的数量多、深度大、数据量大时训练时间会线性上升。解决办法是开n_jobs-1并行训练同时检查max_depth是否设得过大。我在实际项目里把n_estimators从500降到200AUC几乎无变化训练时间从5分钟降到40秒。第四个问题GridSearchCV跑得太慢。如果参数组合太多交叉验证时间和训练时间相乘会非常可怕。解决办法是先小范围粗筛再用粗筛结果缩小搜索空间或者改用RandomizedSearchCV随机组合参数搜索。第五个问题过拟合导致测试集AUC远低于训练集AUC。训练集AUC 0.99、测试集AUC 0.82这是典型的过拟合。解决办法是降低max_depth、增大min_samples_leaf或者减少n_estimators。随机森林虽然抗过拟合但深度不限制时照样会记住噪声。7.2 调参实战中的三个坑第一个坑是只看AUC调参忽略了业务阈值。AUC高不代表在预设阈值下的精确率和召回率满足业务要求。我在一个项目中用AUC调出了看起来最优的参数但在0.3阈值下召回率反而比次优参数低原因就是AUC优化的是整体排序而业务更关心的是概率分布中某个区间的表现。第二个坑是没有固定随机种子就反复调参。sklearn的随机森林里有random_state参数不固定的话每次运行结果都不一样你以为是调参带来的提升实际上可能只是随机波动。我在项目里固定random_state42并且在多个随机种子上跑结果取平均避免掉进这个坑。第三个坑是忽略特征重要性结果的反哺作用。随机森林输出特征重要性后直接用来做特征筛选这个思路本身没错但要注意高度相关的特征会导致重要性分散。比如V17和V16相关性较高模型在两者之间随机选一个分裂导致重要性被低估。如果要用重要性做特征筛选先检查一下特征相关矩阵把相关性大于0.8的特征成组处理。7.3 特征重要性解读的注意事项我每次做风控项目都会打印特征重要性排名但用的时候非常小心。在信用卡数据中V14、V10、V17、V12通常排在前面这些特征对应原始交易数据中某些经过PCA变换的行为模式业务上无法直接解读成“商户类型”或“交易地点”。这是PCA特征的局限但在真实业务里如果使用原始特征特征重要性分析是很有业务价值的。比如在另一个信贷场景项目中特征重要性显示“近3个月查询次数”“额度使用率”“逾期天数”排在最前面这些可以直接指导规则引擎的优化——把重要性高的特征纳入人工审核规则里能显著提升审核效率。特征重要性的另一个用法是发现数据质量问题。如果某个特征的重要性异常高比如超过0.3要多留个心眼有可能这个特征包含了标签信息泄漏。我曾经在处理一个保险欺诈项目时发现“理赔金额”特征重要性异常高排查后发现是因为只有欺诈案件才会有特定类型的理赔金额记录这属于典型的特征泄漏。在信用卡数据集里也要注意比如“交易是否成功”这种特征如果欺诈交易都在失败后才被标记模型等于直接看答案了。8. 从基线到落地我还有几点经验之谈这个项目做到最后我最大的感受是随机森林在信用卡欺诈检测里最大的价值是让我们能用最少的时间成本看清“数据—模型—评估—业务”这条链路里每一个环节的问题。它不需要你调出一朵花来只要参数不离谱就能给出一个可用的基线而真正拉开差距的是数据理解、评估指标选择和阈值决策。最后再分享一个小技巧在完成所有建模和评估之后把模型用joblib或pickle保存下来然后用几条真实的交易样本做一次人工推理亲自验证模型的判断是否合理。这个动作看起来简单但能帮你发现很多指标上看不出来的问题比如模型对某些特征组合产生奇怪的偏好。等你拿着这份完整流程去应对真实的风控业务会发现之前踩过的每一个坑都变成了别人问到你时你敢于拍胸脯回答的底气。