信用卡欺诈检测项目从零复现:完整代码讲解与结果分析

本文基于 Kaggle 信用卡欺诈数据集,从环境搭建开始,一步步带你复现一个完整的机器学习项目:EDA、数据预处理、SMOTE 不平衡处理、5 个模型对比、阈值调优、特征重要性分析。

文中所有截图都是本机运行的真实结果,图片位于 source/img/04Creditcard_Fraud/ 目录。如果你把这篇文档发到博客,请记得把图片目录一起上传,或者把图片换成你自己的图床链接。

目录

  1. 项目背景与目标
  2. 环境准备
  3. 项目结构与整体流程
  4. 分步代码讲解
  5. 复现结果汇总
  6. 常见问题
  7. 总结与改进方向

一、项目背景与目标

信用卡欺诈是金融风控中最典型的场景之一:每天有海量交易发生,其中欺诈交易占比极低,但一旦发生就会造成资金损失。机器学习要解决的核心问题是:

在几万笔交易中,准确找出那几笔“有问题”的交易,同时尽量不打扰正常用户。

这个任务有两个天然难点:

  1. 类别极度不平衡:本数据集中欺诈交易只占 0.1727%,如果模型无脑预测“正常”,准确率也能高达 99.8% 以上,但没有任何实际意义。
  2. 代价不对称:漏掉一笔欺诈(召回率低)和误伤一笔正常交易(精确率低)都会造成损失,所以不能只看准确率,需要综合评估。

本项目的目标就是:

  • 通过 EDA 了解数据分布;
  • 用 SMOTE 缓解类别不平衡;
  • 训练多个模型并对比评估;
  • 找到兼顾精确率与召回率的最佳模型;
  • 通过阈值调优进一步提升 F1 分数;
  • 分析模型最重要的特征。

二、环境准备

2.1 安装 Python 与依赖

建议使用 Python 3.9 及以上版本。你可以直接使用 Anaconda,也可以使用系统自带的 Python。

1
pip install pandas numpy matplotlib seaborn scikit-learn imbalanced-learn

需要说明的是,imbalanced-learn 是专门用来处理不平衡数据集的库,本项目主要使用其中的 SMOTE

2.2 下载数据集

数据集来自 Kaggle:Credit Card Fraud Detection

下载后把 creditcard.csv 放在项目根目录即可。

2.3 项目目录结构

1
2
3
4
5
Creditcard_Fraud_Project/
├── Creditcard_Fraud.py # 主脚本
├── creditcard.csv # 数据集
├── README.md # 项目说明
└── 运行图片/ # 运行结果截图

三、项目结构与整体流程

整个项目的代码都写在一个主脚本 Creditcard_Fraud.py 中,逻辑清晰,可以分为 7 个阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
读取数据

EDA 探索性数据分析

划分训练集 / 测试集 + 标准化

SMOTE 数据增强

5 个模型训练 + 5 折交叉验证

模型对比可视化

最佳模型阈值调优 + 特征重要性

下面我们按顺序讲解每一段代码。

四、分步代码讲解

4.1 导入库与中文显示设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (accuracy_score, confusion_matrix, classification_report,
f1_score, precision_recall_curve, precision_score,
recall_score, roc_auc_score, roc_curve)
from sklearn.tree import DecisionTreeClassifier
from imblearn.over_sampling import SMOTE
import time

plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文显示问题
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题

这里的重点有两个:

  • imblearn.over_sampling.SMOTE 用于后续的过采样;
  • plt.rcParams 设置中文字体,否则图表里的中文会变成方框。

4.2 读取数据

1
2
3
4
data = pd.read_csv('creditcard.csv')
X = data.copy()
X.drop('Class', axis=1, inplace=True)
y = data['Class']

X 是所有特征,y 是标签。Class = 1 表示欺诈交易,Class = 0 表示正常交易。

4.3 EDA:探索性数据分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print(X.info())
print(y.info())
print(X.isnull().sum())
print(y.isnull().sum())

# 目标变量分布
print(f'欺诈样本占比: {data["Class"].mean():.4%}')

# 金额分布
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
sns.histplot(data[data['Class'] == 0]['Amount'], bins=50, label='Normal')
plt.title('正常交易金额分布')
plt.subplot(1, 2, 2)
sns.histplot(data[data['Class'] == 1]['Amount'], bins=20, label='Fraud', color='red')
plt.title('欺诈交易金额分布(明显偏小)')
plt.show()

数据类型检查

运行 X.info() 可以看到:

1
2
3
4
RangeIndex: 284807 entries, 0 to 284806
Data columns (total 30 columns)
dtypes: float64(30)
memory usage: 65.2 MB

也就是说数据集一共有 284807 条交易记录,30 个特征全部是 float64 类型,没有缺失值。yint64 类型,也没有缺失值。

数据类型

缺失值检测以及欺诈样本占比

类别分布

1
欺诈样本占比: 0.1727%

对应到具体数量:

  • 正常交易:284315
  • 欺诈交易:492

欺诈样本只有 492 条,这是一个非常典型的高度不平衡数据集。

金额分布

正常与欺诈的数量对比

从图中可以看出:

  • 正常交易的金额分布较广,大部分金额集中在较小的区间;
  • 欺诈交易的金额明显偏小,大部分欺诈交易金额集中在 0 到几百美元之间。

这说明 Amount 特征对欺诈识别有一定区分度,因此后续需要单独做标准化。

4.4 划分数据集与标准化

1
2
3
4
5
6
7
8
9
10
11
12
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)

time_scaler = StandardScaler()
amount_scaler = StandardScaler()

X_train['Time'] = time_scaler.fit_transform(X_train['Time'].values.reshape(-1, 1))
X_test['Time'] = time_scaler.transform(X_test['Time'].values.reshape(-1, 1))

X_train['Amount'] = amount_scaler.fit_transform(X_train[['Amount']])
X_test['Amount'] = amount_scaler.transform(X_test[['Amount']])

这里有两个关键设计:

  1. 分层抽样 stratify=y:因为欺诈样本太少,如果不分层,随机划分很可能把欺诈样本全部切到训练集或测试集。分层抽样保证训练集和测试集中的正负样本比例与原始数据一致。本项目的划分结果是:

    • 训练集:227845
    • 测试集:56962 条(其中欺诈样本 98 条)
  2. 只对 TimeAmount 标准化V1 - V28 已经是 PCA 降维后的匿名特征,数值范围本身比较规整;而 Time(交易时间)和 Amount(金额)量纲差异大,不标准化会影响距离类模型(如 KNN、逻辑回归)的效果。

4.5 SMOTE 数据增强

1
2
3
smote = SMOTE(random_state=42, sampling_strategy='auto')
X_train_original, y_train_original = X_train, y_train
X_train, y_train = smote.fit_resample(X_train, y_train)

SMOTE(Synthetic Minority Over-sampling Technique)的核心思想是:

对少数类样本(欺诈样本),在其近邻样本之间通过插值合成新的少数类样本,而不是简单复制。

这样处理后,训练集中的正常样本和欺诈样本数量会变成一样多(各 227451 条,共 454902 条)。

需要注意两点:

  • 只在训练集上做 SMOTE:测试集必须保持真实分布,否则评估结果会失真;
  • 同时保留原始训练数据:代码用 X_train_original, y_train_original 保存了一份,用于后续对比“SMOTE 前后”的召回率变化。

4.6 模型训练循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
model_dict = {
'Logistic Regression': LogisticRegression(max_iter=10000, random_state=42),
'Decision Tree Classifier': DecisionTreeClassifier(random_state=42),
'Random Forest Classifier': RandomForestClassifier(random_state=42, n_estimators=100),
'Gradient Boosting Classifier': GradientBoostingClassifier(random_state=42),
'K Neighbors Classifier': KNeighborsClassifier(n_neighbors=5)
}

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for model_name, clf in model_dict.items():
# 1. 用原始数据训练,计算增强前的召回率
clf_original = clf
clf_original.fit(X_train_original, y_train_original)
y_pred_original = clf_original.predict(X_test)
recall_original = recall_score(y_test, y_pred_original)

# 2. 用 SMOTE 数据训练,并做 5 折交叉验证
start_time = time.time()
clf.fit(X_train, y_train)
cv_scores = cross_val_score(clf, X_train, y_train, cv=5, scoring='f1')
training_time = time.time() - start_time

# 3. 在测试集上预测并计算各项指标
y_pred = clf.predict(X_test)
y_proba = clf.predict_proba(X_test)[:, 1]

results['Accuracy'][model_name] = accuracy_score(y_test, y_pred)
results['Precision'][model_name] = precision_score(y_test, y_pred)
results['Recall'][model_name] = recall_score(y_test, y_pred)
results['Recall_Original'][model_name] = recall_original
results['F1-Score'][model_name] = f1_score(y_test, y_pred)
results['ROC-AUC'][model_name] = roc_auc_score(y_test, y_proba)

本项目对比了 5 个经典模型:

模型 说明
Logistic Regression 线性模型,训练快,适合做基线
Decision Tree 单棵决策树,容易过拟合
Random Forest 随机森林,100 棵树,集成学习
Gradient Boosting 梯度提升树,默认参数
KNN K 近邻,k=5

代码里还做了两件容易被忽略但很重要的事:

  • 每个模型都在原始数据上训练一次,得到“SMOTE 之前的召回率”,用于量化数据增强的效果;
  • 每个模型在 SMOTE 数据上做 5 折分层交叉验证,打印 CV F1,避免单次划分的偶然性。

原代码中还有 SVM 和单独的逻辑回归练习块,因为训练耗时太长,被注释掉了,不影响主流程。

4.7 评估指标

在欺诈检测场景中,最常用的指标是:

指标 含义 公式
Accuracy 所有样本中预测正确的比例 (TP + TN) / (TP + FP + FN + TN)
Precision 预测为欺诈的交易中,真的是欺诈的比例 TP / (TP + FP)
Recall 真实欺诈交易中被成功找出的比例 TP / (TP + FN)
F1-Score 精确率与召回率的调和平均 2PR / (P + R)
ROC-AUC 模型排序能力的综合指标,越接近 1 越好 由 TPR 与 FPR 曲线下面积计算

为什么不能只看 Accuracy?

因为欺诈占比只有 0.1727%,模型全部预测为“正常”也能得到 99.8% 的准确率,但一个欺诈都找不出来。所以在不平衡场景下,重点看 Recall(别漏掉欺诈)F1(精确率与召回率的平衡)

4.8 模型对比可视化

4.8.1 性能指标柱状图

Performance Metrics 对比

这张图把所有模型的 Accuracy、Precision、Recall、F1-Score、ROC-AUC 放在一起对比,可以直观看到:几乎所有模型 Accuracy 都接近 1,但 Precision 和 Recall 差异巨大,说明只看准确率没有意义。

4.8.2 最佳模型混淆矩阵

按 F1-Score 排序,本项目的前三名是:

  1. Random Forest:0.8410
  2. KNN:0.6078
  3. Decision Tree:0.4735

最佳模型混淆矩阵

随机森林的归一化混淆矩阵显示:正常交易中 99.97% 被正确识别,欺诈交易中 83.67% 被正确识别,只有 16.33% 的欺诈被漏掉。

4.8.3 ROC 曲线对比

ROC曲线对比

Top 3 模型的 ROC 曲线中,随机森林的 AUC 最高(0.9731),说明模型对“欺诈 vs 正常”的排序能力最好。

4.8.4 Precision-Recall 曲线对比

召回率对比

PR 曲线越靠近右上角越好。随机森林在召回率较高时仍然保持了不错的精确率,因此曲线位置最优。

4.8.5 Top 5 模型雷达图

表现前五模型性能雷达图

雷达图用 Precision、Recall、F1-Score、ROC-AUC 四个维度评估模型,随机森林在四个维度上都很均衡,面积最大。

4.8.6 SMOTE 前后召回率对比

进行数据增强前后的召回率对比

这是本项目最有意思的一张图。SMOTE 对每个模型的召回率提升效果如下:

模型 原始数据 Recall SMOTE 后 Recall 提升
Logistic Regression 0.6429 0.9184 +27.6 个百分点
Decision Tree 0.7449 0.7755 +3.1 个百分点
Random Forest 0.8163 0.8367 +2.0 个百分点
Gradient Boosting 0.1837 0.8980 +71.4 个百分点
KNN 0.8061 0.8776 +7.2 个百分点

可以看到:

  • 梯度提升提升最明显(+71.4 个百分点),说明原始不平衡数据让它“完全不敢预测欺诈”;
  • 随机森林本身在原始数据上召回率就比较高(0.8163),SMOTE 仍然带来了小幅提升。

4.9 最佳模型分析与阈值调优

4.9.1 随机森林的分类报告

1
2
best_model_name = max(results['F1-Score'].items(), key=lambda x: x[1])[0]
print(classification_report(y_test, y_preds[best_model_name], target_names=['正常', '欺诈']))

最佳模型调优前后对比

随机森林的分类报告:

1
2
3
4
5
6
7
8
              precision    recall  f1-score   support

Normal 0.9997 0.9997 0.9997 56864
Fraud 0.8454 0.8367 0.8410 98

accuracy 0.9995 56962
macro avg 0.9225 0.9182 0.9204 56962
weighted avg 0.9995 0.9995 0.9995 56962

4.9.2 为什么还要调阈值?

sklearn 默认把 predict_proba 输出概率 >= 0.5 判为欺诈。但这个 0.5 只是数学上的默认值,并不是业务上的最优值。

在欺诈检测中,我们可以调整阈值,在“多抓欺诈但多误报”和“少误报但漏欺诈”之间做选择。本项目通过 PR 曲线自动搜索 F1 最高的阈值:

1
2
3
4
precision, recall, thresholds = precision_recall_curve(y_test, y_proba_best)
f1_scores = 2 * (precision[:-1] * recall[:-1]) / (precision[:-1] + recall[:-1] + 1e-10)
best_idx = np.argmax(f1_scores)
best_threshold = thresholds[best_idx]

阈值调优前后F1分数对比

调优结果:

指标 默认阈值 0.5 最优阈值 0.77 变化
Precision 0.8454 0.9620 +11.7 个百分点
Recall 0.8367 0.7755 -6.1 个百分点
F1-Score 0.8410 0.8588 +1.8 个百分点

也就是说,把阈值提高到 0.77 后:

  • 精确率大幅提升(预测为欺诈的交易更可信了);
  • 召回率小幅下降(会漏掉略多一点的欺诈);
  • 整体 F1 从 0.8410 提升到 0.8588

实际业务中阈值怎么选,取决于“误报一笔正常交易”和“漏掉一笔欺诈”哪个成本更高。如果想提高召回率,就降低阈值;想提高精确率,就提高阈值。

4.10 特征重要性分析

随机森林是树模型,自带特征重要性。代码如下:

1
2
3
4
5
6
importance_df = pd.DataFrame({
'Feature': feature_names,
'Importance': feature_importance
}).sort_values('Importance', ascending=False)

print(importance_df.head(15))

随机森林Top15重要特征

随机森林模型特征重要性

Top 15 特征及重要性:

排名 特征 重要性
1 V14 0.1956
2 V10 0.1105
3 V4 0.1061
4 V12 0.0951
5 V17 0.0850
6 V3 0.0605
7 V11 0.0564
8 V16 0.0438
9 V2 0.0370
10 V9 0.0262
11 V7 0.0185
12 V21 0.0159
13 V8 0.0157
14 V18 0.0139
15 V27 0.0112

V14 的重要性最高(0.1956),V10V4V12 紧随其后。由于这些特征是 PCA 降维后的匿名特征,我们无法直接知道它们对应什么业务含义,但可以做特征选择实验:例如只用 Top 10 特征重新训练,往往也能保持接近的 F1。

4.11 为什么最终选择随机森林?

项目最后从三个角度论证模型选择:

各模型性能排名

精确率召回率权衡对比

各模型综合性能评分

  1. F1-Score 排名第一:随机森林 0.8410,明显高于第二名 KNN 的 0.6078
  2. PR 曲线位置最优:在相同召回率下,随机森林能保持更高的精确率。
  3. 综合评分最高:以 (F1 + ROC-AUC) / 2 作为综合评分,随机森林得到 0.9071,是所有模型中最高的。

五、复现结果汇总

如果你完整跑完一遍,应该能得到下面的结果表(本机实测):

模型 Accuracy Precision Recall F1-Score ROC-AUC
Logistic Regression 0.9742 0.0580 0.9184 0.1092 0.9699
Decision Tree 0.9970 0.3408 0.7755 0.4735 0.8865
Random Forest 0.9995 0.8454 0.8367 0.8410 0.9731
Gradient Boosting 0.9867 0.1055 0.8980 0.1888 0.9807
KNN 0.9981 0.4649 0.8776 0.6078 0.9536

结论:

  • 随机森林在精确率与召回率之间取得了最佳平衡,是最终选定的模型;
  • SMOTE 显著提升了所有模型的召回率,尤其是梯度提升(+71.4 个百分点);
  • 阈值调优让随机森林的 F1 从 0.8410 提升到 0.8588
  • V14V10V4 是最重要的三个特征。

六、常见问题

1. 运行很久怎么办?

项目里最耗时的是梯度提升、KNN 和随机森林的 5 折交叉验证。可以先把 model_dict 中的模型删到只剩 2 个,或者把 cross_val_scorecv 从 5 改成 3。也可以安装 scikit-learn-intelex 加速。

2. 中文显示成方框怎么办?

说明系统没有 SimHei 字体。可以把代码第一行改成:

1
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']

3. 为什么逻辑回归精确率那么低?

SMOTE 把训练集变得完全平衡后,逻辑回归会倾向于把更多交易预测为欺诈(在测试集上多误报了 1461 笔正常交易),所以召回率上去了、精确率掉下来了。这说明“过采样”不是免费的,需要在精确率和召回率之间做权衡。

4. SVM 为什么被注释掉了?

默认参数的 SVM 在 22 万条样本上训练非常慢,代码里已经注释掉,不影响复现。

5. 为什么测试集是 56962 条而不是整数?

train_test_split(test_size=0.2) 在分层抽样时按类别比例分配,正常类测试集为 56864 条,欺诈类测试集为 98 条,合计 56962 条。

七、总结与改进方向

这个项目完整走完了一个机器学习二分类任务的闭环:数据探索 → 不平衡处理 → 多模型对比 → 评估 → 调优 → 特征分析。从中可以学到的核心经验是:

  • 不平衡数据下不能只看 Accuracy,要看 Precision、Recall、F1 和 ROC-AUC;
  • SMOTE 能有效提升召回率,但要注意精确率的代价;
  • 默认阈值 0.5 不是最优的,结合业务用 PR 曲线调阈值非常实用;
  • 树模型(随机森林)在表格型数据上通常有很好的表现,而且自带特征重要性。

如果想继续深入,可以从这些方向改进:

  • 尝试 XGBoost、LightGBM、CatBoost;
  • 对比 SMOTE、ADASYN、Tomek Links、NearMiss 等采样策略;
  • 用 SHAP 做可解释性分析;
  • 引入业务成本函数,把“漏报/误报”的损失直接写进模型优化目标;
  • 把模型封装成 API,接入实时风控系统。

如果你按照本文成功复现了项目,欢迎在评论区分享你的模型结果,看看不同硬件和依赖版本下指标是否一致。