信用卡欺诈检测机器学习实战:不平衡数据处理与模型评估

发布时间:2026/9/16 7:59:17
信用卡欺诈检测机器学习实战:不平衡数据处理与模型评估 1. 为什么盯上信用卡欺诈检测这个场景做风控和数据科学的朋友应该都有感触欺诈检测是机器学习在金融领域落地最扎实、边界最清晰的场景之一。它的业务回报很直白——每拦截一笔欺诈交易省下的就是真金白银它的问题定义也很聚焦——给定一笔交易的时序特征、金额特征、商户特征判断这笔交易是不是盗刷。比起那些“提升了用户活跃度”这种说不清道不明的指标欺诈检测的收益是可以直接对账的。我这次入手的是一个非常经典的信用卡欺诈检测数据集做了一版完整的机器学习案例从数据清洗、不平衡处理、特征工程、模型选型一直做到阈值调优和评估指标设计。整个流程跑下来你会发现这个项目特别适合作为入门结构化数据的机器学习项目来练手特征不是那种需要靠想象力去构造的文本或图像模型不是那种靠堆算力出效果的深度学习但它涵盖了机器学习项目中最容易踩坑的几个核心环节——类别不平衡怎么处理、评估指标怎么选、阈值怎么调、坏样本少的时候怎么防过拟合。如果你正在学Python和机器学习想找一个能写进简历、也能真正讲清楚业务逻辑的项目信用卡欺诈检测是很值得做的一个。它不像猫狗识别那样玩具感太强也不像推荐系统那样需要大量工程基建它是一个“单机就能跑、业务逻辑完整、难点清晰”的典型二分类问题。下面我会把这次项目从思路到落地的完整过程拆开讲包括我自己踩过的坑和最后沉淀下来的经验尽量让这份报告不仅能跑通还能让你在面试或者写技术总结的时候讲得出每一步为什么这么做。2. 项目整体设计思路先把业务问题翻译成机器学习能解的数学问题做任何机器学习项目动手写代码之前必须先做一件事把业务问题翻译成数据问题。这一步做不好后面模型再准都是自嗨。2.1 业务指标到模型指标的映射信用卡欺诈检测的业务诉求拆开看是两个第一别漏掉欺诈交易。漏掉一笔银行或持卡人就要承担一笔实际资金损失这对应机器学习里的召回率Recall也叫真正例率——在所有真实欺诈里我们到底抓出来了多少。第二别误伤正常交易。如果模型把大量正常消费拦截了用户刷卡体验会非常差客服投诉会爆炸这对应机器学习里的精确率Precision——在模型判为欺诈的交易里有多少是真的欺诈。这两个指标天然是矛盾的想多抓欺诈就会多拦正常交易想少误伤就会漏掉一部分真正的欺诈。这个矛盾在所有欺诈检测项目里都存在不存在一个绝对“最优”的模型只存在一个“符合业务风险偏好”的模型。所以在项目一开始我先确定了评估框架以召回率和精确率的平衡为核心以PR曲线Precision-Recall Curve下面积作为模型对比的主要指标而不是用准确率Accuracy。这里我要多说一句准确率在这个场景里是几乎没用的指标。因为欺诈交易在全部交易里的占比往往不到0.2%哪怕模型把所有交易都判为正常准确率也能到99.8%以上。是不是听起来很离谱但这就是不平衡数据的典型陷阱一个什么都不做的模型在准确率上也能“吊打”真正有用的模型。所以做欺诈检测一定要跨过“以准确率论英雄”这一关。2.2 技术选型为什么用Python 轻量级树模型组合这个项目我选的是Python技术栈模型主力是LightGBM和XGBoost同时跑了逻辑回归和随机森林做对比基线最后加了一个多层感知机MLP来看神经网络在这个强表格数据上的表现。为什么不一上来就上深度学习因为欺诈检测的数据本质上是强结构化表格数据特征维度不高这个数据集只有30列样本量也不算大约28万条在这个量级下经过良好调参的梯度提升树模型Gradient Boosting Decision TreeGBDT通常是性价比最高的选择。GBDT对特征尺度的鲁棒性很强、能自动捕捉特征之间的非线性关系、对缺失值也有内置处理策略不需要做非常复杂的特征工程就能拿到很好的效果。深度学习在图像、文本这些非结构化数据上是王者但在这种小规模表格数据上很容易陷入“调参一整天、不如树模型跑一遍”的尴尬。Python在这个场景下的优势不用多说pandas做数据处理、scikit-learn提供完整的评估和预处理管线、LightGBM和XGBoost训练效率高整个流程可以用一套语言贯通不需要跨语言切换。这也是Python在机器学习领域几乎成为事实标准的原因。2.3 数据不平衡问题的两条解决路线这个数据集的欺诈样本占比大概是0.172%也就是说每1000笔交易里只有不到2笔是欺诈。这种极端不平衡会让模型很容易“偷懒”——把所有样本都判为正常类损失函数照样很低。处理不平衡有两条路线我这次都做了尝试数据层面对训练集做SMOTE过采样、或者对多数类做下采样让正负样本比例不那么悬殊。算法层面在模型训练时给少数类更高的权重class_weight或者用代价敏感学习方法。最后的效果是在这个数据集上算法层面的处理比数据层面的SMOTE更稳定。原因我后面章节会细讲——SMOTE在特征维度低、样本重叠严重的场景下容易合成出“看起来像欺诈、实际是噪声”的样本反而干扰模型学习。这也是我踩过的一个比较深的坑。3. 数据探索与预处理把原始数据彻底摸清再动手建模这个环节是最容易让新手觉得“没技术含量”但实际最关键的步骤。数据没摸清后面所有建模都是空中楼阁。3.1 数据集结构与特征含义这次用的数据集来自欧洲信用卡持卡人的真实交易记录是学术和竞赛社区里最常用的欺诈检测基准数据集之一。特征一共30个其中Time交易发生时间距数据集第一笔交易的时间间隔秒V1到V28经过主成分分析PCA变换后的匿名化特征主要是为了保护原始数据隐私这些特征已经没有直接的业务含义只能当数值型特征用Amount交易金额Class标签0代表正常交易1代表欺诈交易这里要特别理解一件事V1到V28是PCA降维之后的结果这意味着我们无法从特征名去猜测“V3是不是代表商户类型”之类的业务含义。这一点给建模带来的影响是——无法做太细的基于业务逻辑的特征工程只能依赖算法自动学习特征关系。但反过来说也有好处省去了大量特征理解的时间可以把精力放在建模和评估上。3.2 数据探索性分析EDA的几个关键发现加载数据后我先做了快速探索。这段我直接说结论因为代码逻辑比较简单第一个发现类别分布极度不平衡。Class为1的样本只有492条占比0.172%。这是整个项目的核心难点所在后面所有技术选择都跟这个数字有关。第二个发现Time特征的分布很值得玩味。按小时聚合后能看到明显的周期性波动但是没有特别明显的季节性规律。这个特征在建模时我保留了原始秒数同时衍生出了“自数据开始以来经过的天数”这个特征给模型提供更宏观的时间尺度信息。第三个发现Amount特征高度右偏。绝大多数交易金额很小少数交易金额巨大存在明显的长尾。很多机器学习模型尤其是逻辑回归和神经网络对特征的尺度非常敏感所以这个特征肯定要做标准化处理不能直接用原始值喂给模型。第四个发现V1到V28这些PCA特征基本都在一个量纲范围内没有明显的缺失值也没有出现离谱的极端异常值。这让数据清洗的压力小了很多。3.3 数据集的划分策略为什么我不随机打乱这个是很多教程里容易被忽略但是极其重要的细节。我处理数据集的标准做法是按时间顺序切分训练集和测试集而不是用train_test_split默认的随机打乱。原因是这样的欺诈检测模型最终要部署到线上去预测的是“未来发生的交易”。如果训练集和测试集是随机打乱的那测试集里就会混入时间上早于训练集的样本相当于让模型“偷看”了一部分过去的信息。这种数据泄漏Data Leakage会让模型在离线评估时表现很好但上线之后效果大幅缩水。按时间切分能更真实地模拟“用过去的数据预测未来”的线上场景。具体切分我用的是80%时间线之前的样本做训练剩下20%最新样本做测试。划分完后确认了一件事欺诈样本在两个集合里都有分布不会出现训练集里有欺诈但测试集里有新欺诈模式导致评估失真。3.4 预处理细节标准化到底该怎么做预处理环节还有一个很容易犯的错误在全量数据上做标准化再划分训练集和测试集。正确做法是只在训练集上fit标准化器的参数均值和方差然后用训练集的参数去transform测试集。这样才能保证测试集的数据不会提前泄漏到训练过程中。代码上体现出来是这样from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 假设df是原始数据先按时间排序后再划分 df df.sort_values(Time).reset_index(dropTrue) split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx].copy() test_df df.iloc[split_idx:].copy() # 只对Amount和Time做标准化PCA特征本身已经在一个量级 scaler StandardScaler() scaler.fit(train_df[[Amount, Time]]) train_df[Amount_scaled] scaler.transform(train_df[[Amount, Time]])[:, 0] train_df[Time_scaled] scaler.transform(train_df[[Amount, Time]])[:, 1] test_df[Amount_scaled] scaler.transform(test_df[[Amount, Time]])[:, 0] test_df[Time_scaled] scaler.transform(test_df[[Amount, Time]])[:, 1]这里有个细节值得说一下为什么只标准Amount和Time、不动V1到V28因为PCA输出的特征本身就是经过标准化处理的数值范围已经统一了再动它们反而可能引入不必要的数值扰动。这个判断需要结合对数据生成方式的理解不是所有场景都适用。4. 模型训练与核心实现从基线到集成模型这部分是整个项目的核心实践环节。我的路线是先跑逻辑回归做基线baseline然后依次上随机森林、XGBoost、LightGBM最后验证MLP的效果用评估结果来对比选型。4.1 基线模型的搭建逻辑回归逻辑回归在这个场景下其实很有价值。它虽然简单但作为基线可以回答一个重要问题“再复杂的模型到底比简单模型好多少”如果逻辑回归通过过采样已经能达到不错的召回率那后面复杂模型就需要证明自己在这个指标上的增益是值得的。我用的是带class_weightbalanced的逻辑回归让算法自动根据类别比例调整正负样本的权重from sklearn.linear_model import LogisticRegression lr LogisticRegression( class_weightbalanced, max_iter500, random_state42 ) lr.fit(train_X, train_y)注意我在这里并没有对特征做额外的多项式扩展因为数据本身已经有了高维PCA特征线性模型也能捕捉到不少信息。测试集上的结果逻辑回归的PR-AUC大概在0.65到0.70之间召回率在较高阈值下能到0.6左右。这说明什么说明这个数据集的线性可分性其实还可以但还远不够好给后面的树模型留下了提升空间。4.2 梯度提升树的核心实现思路接下来是重头戏。XGBoost和LightGBM是当前表格数据竞赛里最常见的两个梯度提升框架它们的思想是一样的通过不断训练一棵棵新的决策树来拟合前一轮预测的残差最终把所有树的预测叠加起来得到一个强学习器。我在跑XGBoost时用的核心参数是这样一组基于对该数据集规模的判断和后续调参经验import xgboost as xgb xgb_model xgb.XGBClassifier( n_estimators300, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, scale_pos_weight580, # 负样本数 / 正样本数约等于 580:1 eval_metricaucpr, random_state42 ) xgb_model.fit(train_X, train_y)这个scale_pos_weight580是处理不平衡的核心参数它的大致计算逻辑是负样本数量除以正样本数量。设成这个值相当于告诉模型“你把正样本判错的代价是负样本判错的580倍”从而强制模型更重视少数类的学习。这个参数的设置我建议从一个相对激进的值开始然后根据验证集的PR曲线和召回率逐步微调最优值不一定是精确的类别比例通常需要一个范围搜索。eval_metricaucpr也是一个很关键的设定。默认的logloss在极度不平衡数据上很难直观反映“抓欺诈”的能力而PR-AUC能更直接地衡量模型在少数类上的排序能力更适合作为这个项目的早期评估参考。LightGBM的代码结构基本一致只是换了库和部分参数名核心差别在于它用了基于直方图的决策树学习算法训练速度比XGBoost快不少尤其在特征数较多时优势更明显。4.3 要不要用SMOTE我的实验结论这是这个项目里我反复试错的一个环节也是我认为最值得分享的经验之一。SMOTE的做法是在少数类样本之间做线性插值人为合成新的少数类样本。听起来很合理样本太少那就多造一些。但我实测下来在这个数据集上SMOTE反而让模型效果变差尤其是在树模型上。问题出在生成样本的质量。信用卡欺诈数据中不同欺诈交易的特征分布本来就比较分散许多欺诈样本之间在特征空间中的距离很远强行在它们之间插值生成的样本很容易落进“正常交易密度很高”的区域本质上是在制造类别重叠的噪声。模型学到这种噪声后在真实测试集上的表现反而下降。我的建议是如果你是新手优先尝试class_weight或scale_pos_weight而不是一上来就SMOTE。一方面它不需要额外的过采样逻辑、代码简单另一方面它在很多不平衡表格数据上效果并不输给SMOTE而且不会产生合成样本的风险。只有在样本量极其稀少比如正样本少于100条的时候采样法才会成为值得尝试的备选方案。4.4 模型对比与选型结论我用统一的评估流程把四个模型跑完后得出的对比结论是这样的模型PR-AUC召回率在精确率约0.5时训练耗时说明逻辑回归约0.68约0.62秒级线性基线结果中规中矩随机森林约0.75约0.72分钟级有明显提升但不如梯度提升树XGBoost约0.84约0.82分钟级梯度提升优势明显LightGBM约0.86约0.85分钟级综合表现最好最终我选LightGBM作为这个项目的主模型。原因很务实在PR-AUC这个核心指标上最高训练速度快内存占用小而且在后续阈值调优中表现出更丝滑的精确率-召回率平衡曲线给业务部署提供了更大的调节空间。排序上简单总结就是LightGBM ≈ XGBoost 随机森林 逻辑回归。5. 模型评估与业务部署从PR曲线到实际拦截决策模型训练完只是开始真正的难点在于怎么把模型的输出变成一个可以用的业务决策。5.1 为什么PR曲线是这个项目的核心评估工具前面反复提到PR曲线这里展开说说它的原理。精确率和召回率是一对矛盾指标而PR曲线就是把所有可能的分类阈值下的召回率精确率点连成一条曲线。曲线越靠近右上角说明模型在“尽可能抓全欺诈”和“尽可能别误伤”之间的平衡能力越强。ROC曲线受试者工作特征曲线是更常用的分类评估工具但在极度不平衡数据上它有一个严重问题负样本太多导致假正例率FPR的变化被稀释ROC曲线会显得过于乐观看不出模型之间真实的差距。而PR曲线直接关注正样本也就是欺诈样本的精确率和召回率更贴近业务关心的问题所以在这个项目里我全程围绕PR-AUC做评估。5.2 阈值的选择不要默认0.5很多机器学习框架默认用0.5作为分类阈值即模型输出概率大于0.5判为正类。但这是“默认值”不是“正确值”。在欺诈检测里模型输出的原始概率通常非常低因为欺诈本身就是小概率事件很可能模型输出0.2甚至0.05的概率就已经是极高的欺诈风险了。我在项目里画了不同阈值下的精确率和召回率变化曲线结果很直观阈值在0.5附近时召回率低得可怜因为几乎不会有欺诈样本能拿到这么高的概率而当阈值降到0.01附近时召回率能显著上升但误报率也随之增加。最终阈值定在多少取决于业务上“拦一笔欺诈的收益”和“误伤一笔正常交易的损失”之间的比值。我把这个权衡逻辑写成了一个函数业务人员可以直接输入他们对成本比值的估计函数自动返回推荐阈值import pandas as pd def find_best_threshold(y_true, y_prob, cost_fp1.0, cost_fn10.0): 根据误报代价和漏报代价自动寻找最优阈值 cost_fp: 误伤一笔正常交易的代价相对值 cost_fn: 漏掉一笔欺诈交易的代价相对值 thresholds np.arange(0.0, 1.0, 0.005) results [] for t in thresholds: y_pred (y_prob t).astype(int) tp ((y_pred 1) (y_true 1)).sum() fp ((y_pred 1) (y_true 0)).sum() fn ((y_pred 0) (y_true 1)).sum() cost fp * cost_fp fn * cost_fn results.append((t, tp, fp, fn, cost)) result_df pd.DataFrame(results, columns[threshold, tp, fp, fn, cost]) return result_df.loc[result_df[cost].idxmin()]这个方法的核心思想是不要把模型评估和业务决策割裂开阈值不是一个纯统计问题而是一个成本优化问题。5.3 特征重要性分析模型告诉我们什么LightGBM训练完后直接查看特征重要性能看到模型主要依赖哪些特征做判断。这个环节我做了一件事把所有特征按重要性分数排序画出前15个特征的重要性条形图。结果显示V14、V10、V12这几个PCA特征在模型中发挥的作用最大而Amount的重要性相对靠后。这个结果虽然不能直接对应到业务含义因为特征是PCA匿名化的但它仍然有实际价值如果未来数据源增加了新的特征我可以优先围绕这些高重要性特征的方向做扩展。同时也验证了一个通用经验——在信用卡欺诈中交易金额本身往往不是最有效的判别特征反而是交易之间的行为模式更关键。5.4 从离线模型到在线部署的思路严格来说这次案例做到离线评估和阈值确定就已经是一个完整项目了但如果你要上线还需要考虑一个在线预测的框架。我在项目中做了一版简单的推理封装模型训练完保存成文件LightGBM的txt格式或joblib序列化。线上系统把每条交易的实时特征组装成和训练时完全一致的特征向量。调用模型输出欺诈概率。概率超过阈值后触发人工审核或直接拦截。这里最容易出问题的是特征一致性训练时的特征是“Amount_scaled”线上提供的也是同一个字段但线上标准化时用的均值和方差必须和训练时完全一致否则模型输入分布变了输出概率就会失真。这是一个很多团队上线时才会发现的坑我在项目里把标准化器也一起序列化保存了就是为了避免这个问题。6. 高频踩坑与避坑指南这些坑我把标准答案写在这里最后这部分我整理了这次项目里遇到的最典型的几个问题每个都是自己试错过才有的体会希望对正在做类似项目的朋友有直接帮助。6.1 类别不平衡模型为什么“全判正常”最早跑基线模型时我遇到了一个很典型的现象模型预测结果里几乎没有正类样本全部是0。后来一查发现是因为当时用了默认参数没有设置class_weight或scale_pos_weight。模型发现把所有样本都判为正常损失函数已经很低了完全没有动力去学那些“稀少且奇怪”的欺诈样本。对策训练前先检查类别分布然后根据不平衡比例设置正样本权重。对于逻辑回归用class_weightbalanced对于XGBoost设置scale_pos_weight对于LightGBM用is_unbalance或scale_pos_weight这是第一个要做的事。6.2 数据泄漏代码跑得越好越要警觉我一开始也踩过这个坑在全量数据上做标准化再切分训练测试集结果测试集表现异常好PR-AUC直接飙到0.95以上。当时还很兴奋后来反应过来这是数据泄漏。虽然在这个数据集上泄漏的影响不算致命但它会让你的模型评估虚高对真实业务判断造成误导。怎么排查检查预处理流程里fit操作是不是只接触了训练集数据。标准化、缺失值填充、特征筛选这些逻辑都必须严格遵循“只fit训练集transform全部数据”的原则。6.3 过拟合正样本太少模型把噪声当规律因为只有几百条欺诈样本树模型很容易把训练集里的欺诈样本“背下来”导致在训练集上表现完美在测试集上崩掉。对策我用的主要手段是调低树深度max_depth设置在4到6之间、调低学习率learning_rate在0.01到0.1之间、增加subsample随机采样比例和colsample_bytree特征采样比例。同时用交叉验证而不是单次固定测试集来观察模型稳定性。这个方法可以多试试在PR-AUC相近的前提下优先选择结构更简单的模型——简单模型往往泛化能力更强。6.4 评估指标在欺诈场景里别盯着准确率看这个坑在前面已经反复提到但我还是想再强调一次在0.172%的正样本比例下准确率几乎不能提供任何有效信息。一个把全部交易判为正常的“废柴模型”准确率可以达到99.8%。如果你面试时还在用accuracy来评估欺诈检测模型面试官大概率会继续追问ROC、PR、混淆矩阵这些更细的内容。建议在这种不平衡场景下主要看PR-AUC其次看特定阈值下的精确率和召回率。如果业务对成本有明确要求可以直接用成本函数选阈值这也是我在阈值选择部分展示的做法。6.5 新欺诈模式模型再准也要设计人工兜底这是我特别想提醒的一点欺诈是动态对抗的欺诈团伙会根据风控策略不断调整手法。今天训练出的模型明天可能就会遇到全新的欺诈模式。因此只追求模型精度是不够的还需要设计一套包含规则引擎、人工审核、模型重训练的闭环系统。在项目里的体现是把那些模型预测为欺诈但经过人工确认为正常交易的样本和那些模型漏过的欺诈样本都保存下来作为下一轮训练的候选数据。这样模型才能持续进化不会越用越钝。7. 一点实践体会整个项目做完最深的体会是信用卡欺诈检测这个机器学习案例表面上看是一个“训练模型、调参、看分数”的标准流程但真正拉开差距的是那些代码之外的东西——怎么理解业务指标、怎么选择评估方式、怎么处理不平衡数据、怎么防止数据泄漏。这些能力不是跑通一个教程就能具备的需要你在自己的项目里反复试错、复盘才能慢慢形成肌肉记忆。如果你正在学习机器学习我建议你把信用卡欺诈检测当作第二个或第三个练手项目来做——第一个项目可以是泰坦尼克号生存预测那种入门级数据先把流程跑通然后立刻用这个项目来练习“处理不平衡数据”和“选择评估指标”这两个核心难点。这两个难点是所有真实场景中都会遇到的问题提前掌握会让你少走很多弯路。最后分享一个小技巧建模之前用pandas快速看一下正负样本的特征均值差异如果均值差异就很明显说明这是一个比较好分的任务如果均值差异不大那就要加倍小心可能需要做更精细的特征工程或者用更复杂的模型。我这次看到V14、V17这类特征在正常交易和欺诈交易之间均值差异很大心里基本就有底了——这个数据集是可做的剩下的就是如何在细节上把它做好。