拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python实战信用卡欺诈检测:从数据清洗到模型部署全流程解析

1. 项目概述从数据到决策的欺诈检测实战最近在整理一个老项目是关于用Python做信用卡欺诈检测的。这活儿听起来挺高大上什么数据科学、机器学习都沾边但说白了核心就一件事怎么从一堆看似正常的交易记录里把那些“坏家伙”给揪出来。我经手过不少类似的风控项目发现很多朋友一上来就想搞复杂的模型结果往往在数据上就栽了跟头。这个项目完整走了一遍从原始数据清洗、探索性分析到数学建模、模型预测和比较的闭环算是一个挺标准的工业级数据分析流水线。无论你是想入门金融风控还是想系统性地练手一个数据科学项目跟着这个流程走一遍收获会比单纯调个包大得多。信用卡欺诈检测本质上是一个典型的不平衡分类问题。正常交易占了绝大多数通常超过99.9%而欺诈交易凤毛麟角。这种极端不平衡让很多常规的机器学习方法直接失效也是这个项目最大的挑战和乐趣所在。整个流程就像破案数据是你的现场证据清洗和分析是梳理线索建模就是构建推理逻辑最后模型比较则是验证哪种推理方式最有效。我们会用到pandas、numpy做数据处理matplotlib、seaborn做可视化分析scikit-learn构建和比较模型可能还会涉及一些如imbalanced-learn这样的专门处理不平衡数据的库。2. 核心思路与方案设计为何要遵循这个流程很多人拿到数据尤其是像信用卡交易这种表格数据第一反应就是直接扔进模型里跑。我早期也这么干过结果模型指标看起来很美比如准确率99.9%但实际上它对欺诈样本完全没识别能力——因为它简单地把所有样本都预测为“正常”就能达到这个准确率。这就是掉进了不平衡数据的陷阱。所以我们这个项目的设计思路是问题导向、循序渐进的核心目标不是追求某个指标的极致而是构建一个在业务上真正可用的、能稳定识别欺诈的决策系统。整个方案可以拆解为四个环环相扣的阶段2.1 数据清洗与预处理打好地基这是最枯燥但最关键的一步。原始数据往往存在缺失值、异常值、量纲不统一、格式错误等问题。对于欺诈检测我们尤其要关注缺失值处理是直接删除还是用均值、中位数、众数填充或者用模型预测这需要结合字段的业务含义来判断。例如“交易金额”缺失可能直接删除该记录而“商户类别”缺失或许可以用一个“未知”类别填充。异常值检测与处理异常值不一定是错误它可能就是欺诈所以不能武断地删除。我们需要结合业务知识比如单笔交易金额是否有合理上限和统计方法如3σ原则、IQR来甄别并打上标签供后续分析。特征工程原始特征可能不够用。例如我们可以从“交易时间”衍生出“是否午夜交易”、“是否节假日交易”从“历史交易频率”衍生出“近期交易频率突变率”等。好的特征工程能极大提升模型性能。2.2 探索性数据分析用眼睛发现故事EDA不是画几个图就完事了它的目的是深入理解数据分布、发现潜在规律和问题为后续建模提供假设和方向。不平衡性可视化用饼图或条形图直观展示欺诈与非欺诈的比例让所有人对问题的难度有个共识。特征分布分析分别查看欺诈样本和正常样本在各个特征上的分布差异如金额的分布直方图、箱线图。欺诈交易的平均金额是否更高是否集中在某些特定的商户类型或时间段相关性分析计算特征之间的相关性矩阵并用热力图展示。高相关性的特征可能会带来多重共线性问题在后续建模时需要考虑是否剔除其中一个。2.3 数学建模选择合适的“武器”这是核心环节。针对不平衡分类我们不会只用一个模型而是会尝试和比较多种策略算法选择逻辑回归、决策树、随机森林、梯度提升树如XGBoost、LightGBM、甚至简单的神经网络都是候选。它们各有优劣比如逻辑回归可解释性强而树模型通常能捕捉更复杂的非线性关系。不平衡处理技术这是建模的重点。我们会尝试代价敏感学习告诉模型把欺诈误判为正常的代价成本远高于把正常误判为欺诈。重采样技术过采样如SMOTE算法在欺诈样本的“特征空间”附近人工合成一些新的欺诈样本增加其数量。欠采样随机减少一部分正常样本使两类样本数量接近。但这种方法会丢失信息需谨慎。集成方法如使用EasyEnsemble或BalanceCascade将欠采样与集成学习结合。2.4 模型预测与比较用对的尺子量结果模型训练好了怎么比不能用准确率这种“欺骗性”的指标。我们构建一个多维度的评估体系核心评估指标精确率、召回率、F1-Score、AUC-ROC曲线。对于欺诈检测我们通常更看重召回率尽可能抓住更多的欺诈同时也要控制精确率不要误伤太多正常用户影响客户体验F1-Score是两者的调和。混淆矩阵最直观的工具直接展示模型预测的True Positive, False Positive, True Negative, False Negative。模型比较与选择在验证集或测试集上综合比较多个模型使用不同算法或不同采样策略的上述指标。同时还要考虑模型的可解释性业务部门能否理解和线上推理速度。注意切忌将测试集用于任何训练或参数调整过程必须严格保持其“未知性”否则评估结果将过于乐观不具备泛化参考价值。3. 数据清洗实战从原始数据到干净数据集理论说再多不如动手干。假设我们有一份名为creditcard.csv的数据这是一个在机器学习领域常用的开源数据集已进行过匿名化处理。我们使用pandas来打头阵。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 设置绘图风格 sns.set_style(whitegrid) plt.rcParams[font.sans-serif] [SimHei] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus] False # 用来正常显示负号 # 1. 加载数据 df pd.read_csv(creditcard.csv) print(f数据集形状: {df.shape}) print(df.info()) print(df.head())首先看看数据概貌。这个数据集通常包含‘Time’距离首笔交易的秒数、‘V1’-‘V28’经过PCA处理的主成分特征已脱敏、‘Amount’交易金额和‘Class’标签0为正常1为欺诈。3.1 处理缺失值与重复值幸运的是这个开源数据集通常很干净但真实数据没这么美好。# 检查缺失值 print(df.isnull().sum().sum()) # 查看总缺失值数 print(df.isnull().sum()) # 查看每列的缺失值 # 如果有缺失值根据情况处理 # 例如对于数值型特征用中位数填充对于类别型特征用众数或‘Unknown’填充 # df[Amount].fillna(df[Amount].median(), inplaceTrue) # 检查重复行 duplicate_rows df.duplicated().sum() print(f重复行数量: {duplicate_rows}) if duplicate_rows 0: df.drop_duplicates(inplaceTrue) print(f删除重复行后形状: {df.shape})3.2 探索性数据清洗金额与时间的处理‘Amount’特征的值域可能很大直接输入模型可能会对基于距离的算法如逻辑回归、SVM产生不良影响。我们通常需要标准化或归一化。from sklearn.preprocessing import StandardScaler, RobustScaler # 对于‘Amount’特征由于可能存在极端值欺诈交易金额可能很大使用RobustScaler比StandardScaler更好 # RobustScaler使用中位数和四分位数进行缩放对异常值不敏感 scaler RobustScaler() df[Amount_Scaled] scaler.fit_transform(df[Amount].values.reshape(-1, 1)) # ‘Time’特征可能呈现周期性我们可以将其转换为一天中的小时或直接使用正弦余弦变换捕捉周期性 df[Hour] df[Time] // 3600 % 24 # 转换为小时 # 使用正弦余弦变换 df[Time_sin] np.sin(2 * np.pi * df[Time] / (24*3600)) df[Time_cos] np.cos(2 * np.pi * df[Time] / (24*3600)) # 删除原始的‘Time’和‘Amount’列使用处理后的版本 df.drop([Time, Amount], axis1, inplaceTrue)3.3 关键实操心得不要盲目删除“异常值”在欺诈检测中那些在‘Amount’上看起来极高的值很可能就是我们要找的欺诈交易。如果你用常规的箱线图法则如IQR的1.5倍把它们当异常值删了就等于亲手把目标扔掉了。正确的做法是将它们标记出来在EDA阶段重点分析这些“异常值”中欺诈的比例。对‘Amount’进行缩放时选择对异常值稳健的方法如RobustScaler。在建模时树模型如随机森林、XGBoost对特征的量纲和异常值不敏感是更好的选择。4. 探索性数据分析透视数据中的欺诈密码数据洗干净了现在我们戴上“侦探眼镜”看看欺诈交易和正常交易到底有什么不同。4.1 类别不平衡可视化# 查看类别分布 class_dist df[Class].value_counts() print(class_dist) print(f欺诈交易占比: {class_dist[1]/class_dist.sum()*100:.4f}%) # 绘制类别分布图 plt.figure(figsize(10, 6)) ax sns.countplot(xClass, datadf, palette[green, red]) plt.title(交易类别分布 (0: 正常, 1: 欺诈), fontsize15) plt.xlabel(类别, fontsize12) plt.ylabel(数量, fontsize12) # 在柱子上显示数量 for p in ax.patches: ax.annotate(f{p.get_height():,.0f}, (p.get_x() p.get_width() / 2., p.get_height()), hacenter, vacenter, fontsize11, colorblack, xytext(0, 5), textcoordsoffset points) plt.show()这个图会直观地告诉你问题的严峻性——欺诈样本可能只占万分之几。这再次强调了不能用准确率作为主要指标。4.2 欺诈与正常交易的特征对比我们选取处理后的‘Amount_Scaled’和‘Hour’来看差异。# 分离两类数据 fraud df[df[Class] 1] normal df[df[Class] 0] # 对比交易金额标准化后 plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) sns.histplot(fraud[Amount_Scaled], colorred, label欺诈, kdeTrue, statdensity, bins50) sns.histplot(normal[Amount_Scaled], colorgreen, label正常, kdeTrue, statdensity, bins50, alpha0.6) plt.legend() plt.title(标准化交易金额分布对比) plt.xlabel(标准化金额) plt.ylabel(密度) # 对比交易时间小时 plt.subplot(1, 2, 2) sns.histplot(fraud[Hour], colorred, label欺诈, kdeFalse, bins24) sns.histplot(normal[Hour], colorgreen, label正常, kdeFalse, bins24, alpha0.6) plt.legend() plt.title(交易小时分布对比) plt.xlabel(一天中的小时) plt.ylabel(交易数量) plt.tight_layout() plt.show()通过对比图你可能会发现欺诈交易在金额分布上更分散既有小额试探也有大额盗刷在时间上可能更集中于深夜或凌晨。这些洞察对特征工程和业务规则制定都极有价值。4.3 特征间相关性分析# 计算相关系数矩阵 corr_matrix df.corr() # 绘制热力图重点关注与‘Class’的相关性 plt.figure(figsize(16, 12)) # 筛选出与目标变量相关性绝对值较高的特征 top_corr_features corr_matrix.index[abs(corr_matrix[Class]) 0.1] # 阈值可调 top_corr_matrix df[top_corr_features].corr() sns.heatmap(top_corr_matrix, annotTrue, cmapcoolwarm, center0, fmt.2f, squareTrue) plt.title(特征相关性热力图 (筛选后), fontsize15) plt.tight_layout() plt.show()热力图能帮你发现哪些特征与欺诈强相关正相关或负相关也能发现特征之间是否存在高度共线性比如两个特征相关系数超过0.9在后续建模时可能需要剔除其中一个以避免模型不稳定。5. 数学建模构建不平衡分类的防线EDA给了我们方向现在开始构建模型。我们将创建一个模型流水线并尝试不同的重采样策略。5.1 数据准备与划分首先将特征和标签分开并划分训练集和测试集。务必先划分再在训练集上进行重采样否则会引入数据泄露。from sklearn.model_selection import train_test_split # 准备特征X和标签y X df.drop(Class, axis1) y df[Class] # 划分训练集和测试集 (80%训练20%测试)使用分层抽样以保持类别比例 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42, stratifyy) print(f训练集形状: {X_train.shape}, 测试集形状: {X_test.shape}) print(f训练集中欺诈比例: {y_train.sum()/len(y_train):.4%}) print(f测试集中欺诈比例: {y_test.sum()/len(y_test):.4%})5.2 尝试不同的重采样策略我们将在训练集上应用不同的采样方法然后用逻辑回归作为基础分类器来初步比较效果。from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score from imblearn.over_sampling import SMOTE from imblearn.under_sampling import RandomUnderSampler from imblearn.pipeline import Pipeline # 使用pipeline整合采样和模型 # 定义评估函数 def evaluate_model(model, X_test, y_test): y_pred model.predict(X_test) y_pred_proba model.predict_proba(X_test)[:, 1] print(混淆矩阵:) print(confusion_matrix(y_test, y_pred)) print(\n分类报告:) print(classification_report(y_test, y_pred)) print(fAUC-ROC分数: {roc_auc_score(y_test, y_pred_proba):.4f}) return y_pred, y_pred_proba # 策略1: 不进行任何采样基线模型 print( 基线模型 (无采样) ) lr_baseline LogisticRegression(max_iter1000, random_state42, class_weightbalanced) # 使用class_weight平衡代价 lr_baseline.fit(X_train, y_train) evaluate_model(lr_baseline, X_test, y_test) # 策略2: 使用SMOTE过采样 print(\n 使用SMOTE过采样 ) pipeline_smote Pipeline([ (sampler, SMOTE(random_state42)), (classifier, LogisticRegression(max_iter1000, random_state42)) ]) pipeline_smote.fit(X_train, y_train) evaluate_model(pipeline_smote, X_test, y_test) # 策略3: 随机欠采样 print(\n 使用随机欠采样 ) pipeline_rus Pipeline([ (sampler, RandomUnderSampler(random_state42)), (classifier, LogisticRegression(max_iter1000, random_state42)) ]) pipeline_rus.fit(X_train, y_train) evaluate_model(pipeline_rus, X_test, y_test)运行后你会发现基线模型即使设置了class_weight的召回率可能很低它倾向于将多数类判对。SMOTE和欠采样策略通常会显著提升召回率抓住更多欺诈但精确率可能会下降误报增多。5.3 引入更强大的模型随机森林与XGBoost逻辑回归是线性模型捕捉复杂模式能力有限。我们尝试树模型。from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier # 使用SMOTE 随机森林 print( SMOTE 随机森林 ) pipeline_rf Pipeline([ (sampler, SMOTE(random_state42)), (classifier, RandomForestClassifier(n_estimators100, random_state42, n_jobs-1)) ]) pipeline_rf.fit(X_train, y_train) evaluate_model(pipeline_rf, X_test, y_test) # 使用XGBoost其内部有处理不平衡的参数scale_pos_weight print(\n XGBoost (使用scale_pos_weight) ) # scale_pos_weight 可以近似设置为负样本数/正样本数 scale_pos_weight_value len(y_train[y_train0]) / len(y_train[y_train1]) print(fscale_pos_weight 设置为: {scale_pos_weight_value:.2f}) xgb_model XGBClassifier( n_estimators100, max_depth5, learning_rate0.1, scale_pos_weightscale_pos_weight_value, random_state42, use_label_encoderFalse, eval_metriclogloss ) xgb_model.fit(X_train, y_train) evaluate_model(xgb_model, X_test, y_test)树模型通常能取得比逻辑回归更好的性能尤其是AUC-ROC分数。XGBoost的scale_pos_weight参数提供了一种便捷的代价敏感学习方式。6. 模型预测、比较与调优实战我们训练了多个模型现在需要系统性地比较它们并选择最优的进行调优。6.1 模型性能综合比较表我们将关键指标汇总到一个表格中以便对比。from collections import defaultdict models { LR_Baseline: lr_baseline, LR_SMOTE: pipeline_smote, LR_RUS: pipeline_rus, RF_SMOTE: pipeline_rf, XGBoost: xgb_model } results defaultdict(list) for name, model in models.items(): y_pred model.predict(X_test) y_pred_proba model.predict_proba(X_test)[:, 1] if hasattr(model, predict_proba) else None report classification_report(y_test, y_pred, output_dictTrue) results[Model].append(name) results[Precision (Fraud)].append(round(report[1][precision], 4)) results[Recall (Fraud)].append(round(report[1][recall], 4)) results[F1-Score (Fraud)].append(round(report[1][f1-score], 4)) results[AUC-ROC].append(round(roc_auc_score(y_test, y_pred_proba), 4) if y_pred_proba is not None else N/A) results_df pd.DataFrame(results).set_index(Model) print(模型性能综合比较:) print(results_df)通过这个表格你可以一目了然地看到哪个模型在召回率我们的首要关注点和F1-Score上表现最好。通常RF_SMOTE或XGBoost会名列前茅。6.2 使用交叉验证与网格搜索调优假设我们选择XGBoost进行深入调优。我们使用网格搜索来寻找最优超参数。from sklearn.model_selection import GridSearchCV, StratifiedKFold # 定义参数网格 param_grid { max_depth: [3, 5, 7], learning_rate: [0.01, 0.1, 0.2], subsample: [0.8, 1.0], colsample_bytree: [0.8, 1.0], n_estimators: [100, 200] } # 使用分层K折交叉验证确保每折中类别比例一致 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) # 重新初始化一个基础XGBoost模型 xgb XGBClassifier( scale_pos_weightscale_pos_weight_value, random_state42, use_label_encoderFalse, eval_metriclogloss, n_jobs-1 ) # 网格搜索以AUC-ROC作为评分标准 grid_search GridSearchCV( estimatorxgb, param_gridparam_grid, scoringroc_auc, # 重点关注AUC cvcv, verbose1, n_jobs-1 ) print(开始网格搜索调优...) grid_search.fit(X_train, y_train) print(f\n最佳参数: {grid_search.best_params_}) print(f最佳交叉验证AUC-ROC分数: {grid_search.best_score_:.4f}) # 用最佳模型在测试集上最终评估 best_xgb grid_search.best_estimator_ print(\n 调优后的最佳XGBoost模型在测试集上的表现 y_pred_best, y_pred_proba_best evaluate_model(best_xgb, X_test, y_test)6.3 模型可解释性特征重要性分析对于树模型我们可以查看哪些特征对预测欺诈最重要。# 获取特征重要性 feature_importance best_xgb.feature_importances_ feature_names X_train.columns importance_df pd.DataFrame({feature: feature_names, importance: feature_importance}) importance_df importance_df.sort_values(importance, ascendingFalse).reset_index(dropTrue) # 绘制特征重要性条形图 plt.figure(figsize(12, 8)) sns.barplot(ximportance, yfeature, dataimportance_df.head(20), paletteviridis) plt.title(XGBoost 特征重要性 Top 20, fontsize16) plt.xlabel(重要性分数, fontsize12) plt.ylabel(特征, fontsize12) plt.tight_layout() plt.show() print(最重要的10个特征:) print(importance_df.head(10))这个分析极具业务价值。你可以告诉风控团队“看V14、V4、V10这几个衍生特征是判断欺诈的关键。”这有助于他们理解模型决策甚至优化现有的规则引擎。7. 常见问题、排查技巧与部署思考在实际操作中你肯定会遇到各种问题。这里记录几个典型的坑和解决办法。7.1 问题过采样后模型过拟合了在测试集上表现很差。排查与解决检查数据泄露确保重采样如SMOTE只应用于训练集的每个交叉验证折内或者像我们之前做的那样在Pipeline里集成。绝对不能在划分训练测试集之前对整个数据集进行重采样。调整SMOTE参数SMOTE的k_neighbors参数默认是5。如果你的少数类样本非常少可能找不到足够的“邻居”来合成样本。可以尝试减小这个值或者使用其变体如BorderlineSMOTE、SVMSMOTE。尝试不同的采样比例不一定非要1:1的平衡。可以尝试让正负样本比例为1:2或1:3有时效果更好。使用集成方法如EasyEnsemble多次欠采样并集成或BalancedRandomForest它们天生对过拟合更鲁棒。7.2 问题模型召回率很高但精确率太低产生了太多误报业务方无法接受。排查与解决调整决策阈值默认情况下模型以0.5为界预测0或1。你可以通过调整这个阈值来在精确率和召回率之间取得平衡。提高阈值模型会更“谨慎”只有非常确信时才判为欺诈这会提高精确率但降低召回率。from sklearn.metrics import precision_recall_curve # 获取预测概率 y_proba best_xgb.predict_proba(X_test)[:, 1] # 计算不同阈值下的精确率和召回率 precisions, recalls, thresholds precision_recall_curve(y_test, y_proba) # 绘制P-R曲线 plt.plot(thresholds, precisions[:-1], b--, label精确率) plt.plot(thresholds, recalls[:-1], g-, label召回率) plt.xlabel(阈值) plt.legend(loccenter left) plt.ylim([0, 1]) plt.grid(True) plt.show() # 根据业务需求选择一个阈值例如要求精确率不低于80% target_precision 0.8 idx np.argmax(precisions target_precision) optimal_threshold thresholds[idx] print(f为达到至少{target_precision:.0%}的精确率建议阈值为: {optimal_threshold:.3f}) # 使用新阈值进行预测 y_pred_adjusted (y_proba optimal_threshold).astype(int)加入业务规则进行后处理例如对于模型判为欺诈但交易金额极小、或发生在常用地的交易可以人工复审或直接放过。这需要与业务部门紧密合作。特征工程寻找更能区分“易混淆的正常交易”和“欺诈交易”的特征。例如增加“本次交易与上次交易的地理位置距离”、“当前设备是否常用设备”等。7.3 问题模型上线后效果随时间衰减。排查与解决概念漂移欺诈模式会变化。需要建立模型监控体系定期如每周计算模型在最新数据上的性能指标如精确率、召回率。一旦发现显著下降就要触发预警。数据漂移输入数据的分布发生了变化。监控特征如‘Amount’的分布、‘Hour’的分布的稳定性。实施在线学习或定期重训练如果数据量允许可以设计一个pipeline定期如每月用最近一段时间的新数据对模型进行增量训练或全量重训练。7.4 部署考量从Jupyter Notebook到生产系统在笔记本里跑通模型只是第一步。要真正用起来需要考虑模型持久化使用joblib或pickle将训练好的最佳模型best_xgb和特征缩放器scaler保存下来。import joblib joblib.dump(best_xgb, credit_card_fraud_model.pkl) joblib.dump(scaler, robust_scaler.pkl)构建预测API使用Flask、FastAPI等框架加载保存的模型和缩放器创建一个接收交易数据、返回欺诈概率和标签的HTTP API服务。性能与延迟评估模型预测单条记录所需的时间确保能满足实时风控的需求通常在毫秒级。XGBoost预测速度很快通常不是瓶颈。日志与监控记录每一次预测的请求、特征、结果和响应时间便于问题回溯和模型审计。这个项目走下来你会发现信用卡欺诈检测是一个典型的、充满挑战的数据科学应用。它没有一劳永逸的银弹需要你扎实地走好数据清洗、分析、建模、评估、迭代的每一步。最大的体会是理解业务和理解数据比选择最复杂的模型更重要。那些从数据中挖掘出的关于“欺诈交易常在何时何地发生”的洞察其价值有时不亚于模型本身。最后模型上线不是终点建立一个包含监控、预警和重训练的闭环系统才是让模型持续创造价值的关键。在实际部署时不妨先从召回率较高的模型开始哪怕误报多一点再通过调整阈值和增加规则来逐步优化这样业务风险更可控。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门