信用卡欺诈检测

一、前言

在金融风控领域,信用卡欺诈检测是一个经典且极具挑战性的问题。随着电子支付的普及,每天都有数以亿计的交易发生,而其中潜藏的欺诈交易虽然占比极低,却可能给银行和用户带来巨大的资金损失。

本文将基于 Kaggle 上经典的信用卡欺诈数据集(creditcard.csv),从零开始完整走一遍欺诈检测的建模流程。你将学到:

  • 极度不平衡数据集下,为什么准确率是一个"骗人"的指标
  • 如何通过分层抽样、特征预处理为建模打好基础
  • 逻辑回归在不平衡数据上的表现与局限
  • class_weight='balanced' 与 SMOTE 过采样两种平衡策略的对比
  • 随机森林如何突破线性模型的瓶颈
  • 如何通过调整决策阈值,在召回率与精确率之间找到业务最优平衡点

数据集包含 284807 条交易记录,其中欺诈交易仅占 0.17%------这正是整个问题的核心难点所在。


二、数据探索:先搞清楚手里有什么

2.1 导入数据集与依赖库

首先导入所需的库,并读取数据集,快速查看数据的基本面貌。

python 复制代码
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 设置pandas显示优化
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 2000)
pd.set_option('display.max_colwidth', 35)

# 设置显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

df = pd.read_csv("creditcard.csv")
print(df.head())
print(f"\n数据形状:{df.shape}")
print(f"\n数据类型与缺失值情况:")
print(df.info())
txt 复制代码
        Time        V1        V2        V3        V4        V5        V6        V7        V8        V9       V10       V11       V12       V13       V14       V15       V16       V17       V18       V19       V20       V21       V22       V23       V24       V25       V26       V27       V28  Amount  Class
0   0.0 -1.359807 -0.072781  2.536347  1.378155 -0.338321  0.462388  0.239599  0.098698  0.363787  0.090794 -0.551600 -0.617801 -0.991390 -0.311169  1.468177 -0.470401  0.207971  0.025791  0.403993  0.251412 -0.018307  0.277838 -0.110474  0.066928  0.128539 -0.189115  0.133558 -0.021053  149.62      0
1   0.0  1.191857  0.266151  0.166480  0.448154  0.060018 -0.082361 -0.078803  0.085102 -0.255425 -0.166974  1.612727  1.065235  0.489095 -0.143772  0.635558  0.463917 -0.114805 -0.183361 -0.145783 -0.069083 -0.225775 -0.638672  0.101288 -0.339846  0.167170  0.125895 -0.008983  0.014724    2.69      0
2   1.0 -1.358354 -1.340163  1.773209  0.379780 -0.503198  1.800499  0.791461  0.247676 -1.514654  0.207643  0.624501  0.066084  0.717293 -0.165946  2.345865 -2.890083  1.109969 -0.121359 -2.261857  0.524980  0.247998  0.771679  0.909412 -0.689281 -0.327642 -0.139097 -0.055353 -0.059752  378.66      0
3   1.0 -0.966272 -0.185226  1.792993 -0.863291 -0.010309  1.247203  0.237609  0.377436 -1.387024 -0.054952 -0.226487  0.178228  0.507757 -0.287924 -0.631418 -1.059647 -0.684093  1.965775 -1.232622 -0.208038 -0.108300  0.005274 -0.190321 -1.175575  0.647376 -0.221929  0.062723  0.061458  123.50      0
4   2.0 -1.158233  0.877737  1.548718  0.403034 -0.407193  0.095921  0.592941 -0.270533  0.817739  0.753074 -0.822843  0.538196  1.345852 -1.119670  0.175121 -0.451449 -0.237033 -0.038195  0.803487  0.408542 -0.009431  0.798278 -0.137458  0.141267 -0.206010  0.502292  0.219422  0.215153   69.99      0

数据形状:(284807, 31)

数据类型与缺失值情况:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 284807 entries, 0 to 284806
Data columns (total 31 columns):
 #   Column  Non-Null Count   Dtype
---  ------  --------------   -----
 0   Time    284807 non-null  float64
 1   V1      284807 non-null  float64
 2   V2      284807 non-null  float64
 3   V3      284807 non-null  float64
 4   V4      284807 non-null  float64
 5   V5      284807 non-null  float64
 6   V6      284807 non-null  float64
 7   V7      284807 non-null  float64
 8   V8      284807 non-null  float64
 9   V9      284807 non-null  float64
 10  V10     284807 non-null  float64
 11  V11     284807 non-null  float64
 12  V12     284807 non-null  float64
 13  V13     284807 non-null  float64
 14  V14     284807 non-null  float64
 15  V15     284807 non-null  float64
 16  V16     284807 non-null  float64
 17  V17     284807 non-null  float64
 18  V18     284807 non-null  float64
 19  V19     284807 non-null  float64
 20  V20     284807 non-null  float64
 21  V21     284807 non-null  float64
 22  V22     284807 non-null  float64
 23  V23     284807 non-null  float64
 24  V24     284807 non-null  float64
 25  V25     284807 non-null  float64
 26  V26     284807 non-null  float64
 27  V27     284807 non-null  float64
 28  V28     284807 non-null  float64
 29  Amount  284807 non-null  float64
 30  Class   284807 non-null  int64
dtypes: float64(30), int64(1)
memory usage: 67.4 MB
None

2.2 数据集基本信息梳理

从上面的输出可以整理出以下关键信息:

项目 说明
样本总数 284807 条,31 列
缺失值 无(所有字段 Non-Null Count 全部填满)
Time 交易距离首次交易的秒数
V1~V28 经过 PCA 降维后的脱敏特征(原始交易信息出于隐私保护不公开)
Amount 交易金额
Class 标签,0 = 正常交易,1 = 欺诈交易

2.3 类别不平衡:这个数据集最大的坑

前面做泰坦尼克、鸢尾花这类数据集时,类别分布基本均衡。但信用卡欺诈场景完全不同------绝大多数交易是正常的,欺诈样本极少。

思考题:如果直接统计准确率,会存在什么巨大陷阱?

我们先查看标签分布,验证类别不平衡的程度。

python 复制代码
# 统计标签分布
label_cnt = df["Class"].value_counts()
print("类别分布:")
print(label_cnt)
print(f"正常交易(0)占比:{label_cnt[0] / len(df) * 100:.2f}%")
print(f"欺诈交易(1)占比:{label_cnt[1] / len(df) * 100:.2f}%")
txt 复制代码
类别分布:
Class
0    284315
1       492
Name: count, dtype: int64
正常交易(0)占比:99.83%
欺诈交易(1)占比:0.17%

结果分析:欺诈样本只占 0.17%。如果一个模型"无脑"全部预测为 0(正常交易),准确率依旧高达 99.83%,但完全识别不出任何欺诈!

核心结论:准确率在不平衡数据集上彻底失效,不能作为主评价指标!

此外,划分数据集时必须使用 stratify=y,保证训练集、测试集中欺诈样本占比和原始数据一致。如果不使用分层抽样,很有可能测试集里几乎没有欺诈样本,导致评估完全失真。

思考题:在欺诈检测业务里------

  • 真正例 TP:欺诈交易,成功识别
  • 假负例 FN:欺诈交易被判定为正常(漏检,损失资金,业务最不能接受)
  • 假正例 FP:正常交易被误判为欺诈(误报警,打扰用户)

我们更应该关注召回率(Recall)还是精确率(Precision)?除此之外,不平衡数据集公认的核心评价指标是什么?


三、数据集划分与特征预处理

在建模之前,先说明两个特征处理要点:

  1. Time 时间特征业务意义较弱,一般直接删除;
  2. Amount 交易金额数值范围大,需要标准化;而 V1~V28 已经是 PCA 标准化后的特征,不用重复处理。
python 复制代码
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1. 构造特征与标签
X = df.drop(["Time", "Class"], axis=1)
y = df["Class"]

# 2. 分层划分训练集、测试集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

print(f"训练集大小:{X_train.shape}")
print(f"测试集大小:{X_test.shape}")
print(f"\n训练集标签分布:{y_train.value_counts()}")
print(f"\n测试集标签分布:{y_test.value_counts()}")

# 3. 标准化 Amount
scaler = StandardScaler()
X_train["Amount"] = scaler.fit_transform(X_train[["Amount"]])
X_test["Amount"] = scaler.transform(X_test[["Amount"]])
txt 复制代码
训练集大小:(227845, 29)
测试集大小:(56962, 29)

训练集标签分布:Class
0    227451
1       394
Name: count, dtype: int64

测试集标签分布:Class
0    56864
1       98
Name: count, dtype: int64

可以看到,分层抽样后训练集和测试集的欺诈占比都保持在约 0.17%,与原始数据一致。

引导思考:现在如果直接在原始不平衡数据集上训练逻辑回归,模型会有什么行为?

样本极度不均衡时,模型为了降低总体损失,会偏向预测类别 0(正常交易)。测试集准确率看起来很高,但欺诈样本大量漏检。此时准确率完全不能用来衡量模型好坏

那我们用什么指标作为核心评判标准?------AUC、召回率 Recall、精确率 Precision、F1、PR 曲线。


四、逻辑回归基线模型

4.1 原始不平衡数据训练

先在不做任何采样处理的原始数据上训练逻辑回归,作为基线。

python 复制代码
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score, roc_auc_score
from sklearn.metrics import confusion_matrix, classification_report

# 训练
lr = LogisticRegression(max_iter=300, random_state=42)
lr.fit(X_train, y_train)

# 预测
y_pred = lr.predict(X_test)
y_pred_proba = lr.predict_proba(X_test)[:, 1]  # 获取类别1(欺诈)概率

# 指标计算
acc = accuracy_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_pred_proba)

print("===== 逻辑回归(原始不平衡数据集)=====")
print(f"准确率 Accuracy: {acc * 100:.2f}%")
print(f"召回率 Recall(欺诈检出率): {recall * 100:.2f}%")
print(f"精确率 Precision: {precision * 100:.2f}%")
print(f"F1-score: {f1 * 100:.2f}%")
print(f"AUC: {auc * 100:.2f}%")
print("\n混淆矩阵:")
print(confusion_matrix(y_test, y_pred))
print("\n分类报告:")
print(classification_report(y_test, y_pred))
txt 复制代码
===== 逻辑回归(原始不平衡数据集)=====
准确率 Accuracy: 99.92%
召回率 Recall(欺诈检出率): 64.29%
精确率 Precision: 82.89%
F1-score: 72.41%
AUC: 95.52%

混淆矩阵:
[[56851    13]
 [   35    63]]

分类报告:
              precision    recall  f1-score   support

           0       1.00      1.00      1.00     56864
           1       0.83      0.64      0.72        98

    accuracy                           1.00     56962
   macro avg       0.91      0.82      0.86     56962
weighted avg       1.00      1.00      1.00     56962

结果分析

txt 复制代码
[[56851    13]   # 正常交易:56851正确,13条误报
 [   35    63]]  # 欺诈交易:35条漏检,63条成功检出

测试集一共 98 条欺诈,成功抓到 63 条,漏掉 35 条,召回率仅 64.29%。但准确率却高达 99.92%,极具迷惑性------再次印证:

极度不平衡数据集,准确率毫无参考价值!

4.2 加入 class_weight='balanced'

想要改善漏检问题,第一种轻量化方案是给逻辑回归设置 class_weight='balanced'。模型的损失函数会自动给少数类(欺诈 1)施加更大的惩罚权重。

思考:加上这个参数后,预期召回率会上升还是下降?精确率大概率如何变化?

python 复制代码
lr_balanced = LogisticRegression(max_iter=300, random_state=42, class_weight="balanced")
lr_balanced.fit(X_train, y_train)

y_pred_balanced = lr_balanced.predict(X_test)
y_pred_proba_balanced = lr_balanced.predict_proba(X_test)[:, 1]

acc = accuracy_score(y_test, y_pred_balanced)
recall = recall_score(y_test, y_pred_balanced)
precision = precision_score(y_test, y_pred_balanced)
f1 = f1_score(y_test, y_pred_balanced)
auc = roc_auc_score(y_test, y_pred_proba_balanced)

print("===== 逻辑回归 class_weight='balanced' =====")
print(f"准确率 Accuracy: {acc * 100:.2f}%")
print(f"召回率 Recall(欺诈检出率): {recall * 100:.2f}%")
print(f"精确率 Precision: {precision * 100:.2f}%")
print(f"F1-score: {f1 * 100:.2f}%")
print(f"AUC: {auc * 100:.2f}%")
print("\n混淆矩阵:")
print(confusion_matrix(y_test, y_pred_balanced))
print("\n分类报告:")
print(classification_report(y_test, y_pred_balanced))
txt 复制代码
===== 逻辑回归 class_weight='balanced' =====
准确率 Accuracy: 97.46%
召回率 Recall(欺诈检出率): 91.84%
精确率 Precision: 5.89%
F1-score: 11.06%
AUC: 97.14%

混淆矩阵:
[[55425  1439]
 [    8    90]]

分类报告:
              precision    recall  f1-score   support

           0       1.00      0.97      0.99     56864
           1       0.06      0.92      0.11        98

    accuracy                           0.97     56962
   macro avg       0.53      0.95      0.55     56962
weighted avg       1.00      0.97      0.99     56962

结果分析

txt 复制代码
[[55425  1439]   # 正常交易:1439笔被误判为欺诈(大量误报警)
 [    8    90]]  # 欺诈交易:仅漏掉8笔,90笔成功识别

对比不加权重的模型:

  • 召回率 64.29% → 91.84%:漏检大幅减少,符合金融风控诉求
  • 精确率 82.89% → 5.89%:代价极其明显,误报爆炸式增长

简单理解:class_weight="balanced" 强制模型优先识别少数类,模型为了尽量不漏掉欺诈,宁可错杀大量正常交易。

业务场景思考:如果银行短信频繁误提醒用户"您这笔交易疑似欺诈",用户投诉量会暴增,无法直接上线。

核心知识点:精确率与召回率天然存在权衡(Trade-off)

4.3 ROC 曲线与 PR 曲线

我们目前使用 predict(),默认阈值 = 0.5。对于不平衡数据集,0.5 这个阈值往往不是最优选择。思路是不改动模型,只调整判定阈值:

  • 阈值降低 → 更容易判定为欺诈 → 召回上升、精确率下降
  • 阈值抬高 → 更谨慎判定欺诈 → 召回下降、精确率上升

在调阈值之前,先画出两条评估曲线。需要注意:ROC 曲线容易对不平衡数据产生乐观假象,而 PR 曲线(精确率-召回率曲线)更适合不平衡二分类

  1. ROC 曲线:横轴 FPR(正常样本被误判的比例)。样本极度不均衡时,FPR 看上去很低,容易误导。
  2. PR 曲线:只聚焦正样本(欺诈),是工业界不平衡分类的首选评估曲线。
python 复制代码
from sklearn.metrics import roc_curve, precision_recall_curve, auc

# ---------- ROC曲线 ----------
fpr, tpr, thresholds_roc = roc_curve(y_test, y_pred_proba_balanced)
roc_auc = auc(fpr, tpr)

plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(fpr, tpr, lw=2, label=f'ROC曲线 (AUC={roc_auc:.4f})')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlabel("假阳性率 FPR")
plt.ylabel("真阳性率 TPR(Recall)")
plt.title("ROC曲线")
plt.legend()
plt.grid(alpha=0.3)

# ---------- PR曲线 ----------
precision, recall, thresholds_pr = precision_recall_curve(y_test, y_pred_proba_balanced)
pr_auc = auc(recall, precision)

plt.subplot(1, 2, 2)
plt.plot(recall, precision, lw=2, label=f'PR曲线 (AUC={pr_auc:.4f})')
plt.xlabel("召回率 Recall")
plt.ylabel("精确率 Precision")
plt.title("PR曲线(不平衡数据集推荐)")
plt.legend()
plt.grid(alpha=0.3)

plt.tight_layout()
plt.show()

# 寻找折中阈值示例
print("PR曲线下面积 PR-AUC:", pr_auc)
txt 复制代码
PR曲线下面积 PR-AUC: 0.7653147444807694

下一条技术路线思考 :除了加权损失、调整阈值,还有一大类方法------重采样

两类方案:

  1. 欠采样:随机删掉大量正常交易 → 缺点:丢失大量有效信息
  2. 过采样
    • 简单复制欺诈样本:极易过拟合
    • SMOTE:插值合成全新欺诈样本(主流方案)

思考题:SMOTE 能不能作用在测试集?为什么?


五、SMOTE 过采样实验

思路:在训练集上人工插值生成更多欺诈样本,让训练集类别接近平衡;测试集维持原样不变。

python 复制代码
from imblearn.over_sampling import SMOTE

# 初始化SMOTE
# 寻找少数类(欺诈)近邻样本,线性插值创造新的合成欺诈样本
# 不是简单复制,缓解过拟合
smote = SMOTE(random_state=42)

# 仅重采样训练集!
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)

print("==== SMOTE采样后训练集分布 ====")
print(y_train_smote.value_counts())

# 使用采样后的数据集训练逻辑回归
lr_smote = LogisticRegression(max_iter=300, random_state=42)
lr_smote.fit(X_train_smote, y_train_smote)

y_pred_smote = lr_smote.predict(X_test)
y_pred_proba_smote = lr_smote.predict_proba(X_test)[:, 1]

acc = accuracy_score(y_test, y_pred_smote)
recall = recall_score(y_test, y_pred_smote)
precision = precision_score(y_test, y_pred_smote)
f1 = f1_score(y_test, y_pred_smote)
auc = roc_auc_score(y_test, y_pred_proba_smote)

print("\n===== 逻辑回归 + SMOTE过采样 =====")
print(f"准确率 Accuracy: {acc * 100:.2f}%")
print(f"召回率 Recall(欺诈检出率): {recall * 100:.2f}%")
print(f"精确率 Precision: {precision * 100:.2f}%")
print(f"F1-score: {f1 * 100:.2f}%")
print(f"AUC: {auc * 100:.2f}%")
print("\n混淆矩阵:")
print(confusion_matrix(y_test, y_pred_smote))
print("\n分类报告:")
print(classification_report(y_test, y_pred_smote))
txt 复制代码
==== SMOTE采样后训练集分布 ====
Class
0    227451
1    227451
Name: count, dtype: int64

===== 逻辑回归 + SMOTE过采样 =====
准确率 Accuracy: 97.31%
召回率 Recall(欺诈检出率): 91.84%
精确率 Precision: 5.58%
F1-score: 10.53%
AUC: 97.00%

混淆矩阵:
[[55342  1522]
 [    8    90]]

分类报告:
              precision    recall  f1-score   support

           0       1.00      0.97      0.99     56864
           1       0.06      0.92      0.11        98

    accuracy                           0.97     56962
   macro avg       0.53      0.95      0.55     56962
weighted avg       1.00      0.97      0.98     56962

5.1 三组逻辑回归方案对比

先汇总三组逻辑回归的关键指标,方便直观对比:

方案 Recall Precision F1 AUC
原始不平衡 LR 64.29% 82.89% 72.41% 95.52%
LR + class_weight="balanced" 91.84% 5.89% 11.06% 97.14%
LR + SMOTE 过采样 91.84% 5.58% 10.53% 97.00%

现象解读 :SMOTE 之后训练集正负样本 1:1 平衡,最终效果和 class_weight="balanced" 高度接近;召回同样拉高到 91.84%,只漏掉 8 条欺诈;但精确率依旧极低,产生大量误报(1522 条正常交易被判定欺诈)。

原因:逻辑回归本身是线性模型,决策边界表达能力有限,单纯平衡样本分布,很难同时兼顾精确率与召回率。

线性模型遇到瓶颈,我们更换非线性模型------随机森林。

思考两点

  1. 随机森林同样支持 class_weight="balanced",要不要使用?
  2. 树模型能捕捉非线性特征组合,相比逻辑回归,有没有希望改善精确率过低的问题?

六、随机森林:非线性模型的突破

python 复制代码
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(
    n_estimators=100,
    class_weight="balanced",  # 给欺诈样本更大损失权重
    random_state=42,
    n_jobs=-1  # 调用全部 CPU 核心加速训练(数据集很大,提速明显)
)

rf.fit(X_train, y_train)

y_pred_rf = rf.predict(X_test)
y_pred_proba_rf = rf.predict_proba(X_test)[:, 1]

acc = accuracy_score(y_test, y_pred_rf)
recall = recall_score(y_test, y_pred_rf)
precision = precision_score(y_test, y_pred_rf)
f1 = f1_score(y_test, y_pred_rf)
auc = roc_auc_score(y_test, y_pred_proba_rf)

print("===== 随机森林 class_weight='balanced' =====")
print(f"准确率 Accuracy: {acc * 100:.2f}%")
print(f"召回率 Recall(欺诈检出率): {recall * 100:.2f}%")
print(f"精确率 Precision: {precision * 100:.2f}%")
print(f"F1-score: {f1 * 100:.2f}%")
print(f"AUC: {auc * 100:.2f}%")
print("\n混淆矩阵:")
print(confusion_matrix(y_test, y_pred_rf))
print("\n分类报告:")
print(classification_report(y_test, y_pred_rf))
txt 复制代码
===== 随机森林 class_weight='balanced' =====
准确率 Accuracy: 99.96%
召回率 Recall(欺诈检出率): 81.63%
精确率 Precision: 91.95%
F1-score: 86.49%
AUC: 96.73%

混淆矩阵:
[[56857     7]
 [   18    80]]

分类报告:
              precision    recall  f1-score   support

           0       1.00      1.00      1.00     56864
           1       0.92      0.82      0.86        98

    accuracy                           1.00     56962
   macro avg       0.96      0.91      0.93     56962
weighted avg       1.00      1.00      1.00     56962

6.1 全方案对比

方案 Recall Precision F1 AUC
原始不平衡 LR 64.29% 82.89% 72.41% 95.52%
LR + class_weight="balanced" 91.84% 5.89% 11.06% 97.14%
LR + SMOTE 过采样 91.84% 5.58% 10.53% 97.00%
随机森林 + class_weight="balanced" 81.63% 91.95% 86.49% 96.73%

混淆矩阵:

txt 复制代码
[[56857     7]   # 正常交易仅7笔误判欺诈(误报极少)
 [   18    80]]  # 98条欺诈,80条检出,18条漏检

关键结论

  1. 随机森林大幅优于线性逻辑回归
    • 逻辑回归开启平衡权重后直接陷入"高召回、极低精确率",大量误报警无法商用;
    • 非线性随机森林可以学习特征交互,很好地平衡精确率与召回,F1 达到 86.49%,是目前最优模型。
  2. 权衡关系清晰体现
    • 相比于带权重 LR:召回略微下降,但精确率从 5% 暴涨至 91.95%;
    • 业务价值:既抓住绝大多数欺诈,又几乎不会骚扰正常用户。

七、阈值调优:寻找业务最优平衡点

当前依旧使用默认阈值 0.5 划分正负样本。而我们手上有模型输出的欺诈概率 y_pred_proba_rf,可以通过调整阈值进一步微调:

  • 想要更少漏检 → 降低阈值
  • 想要更少误报 → 抬高阈值

写一段代码遍历阈值,寻找业务最优平衡点。

python 复制代码
# 取出随机森林输出的欺诈概率(数组)
proba_1 = y_pred_proba_rf
y_true = y_test.values  # y_test 是 Series,.value 将其转为 numpy 一维数组

# 遍历一系列阈值
threshold_list = np.linspace(0.05, 0.95, 30)  # 生成 0.05~0.95 之间一系列候选阈值

best_f1, best_thresh = 0.0, 0.0
print("阈值\t召回率\t精确率\tF1")

for thresh in threshold_list:
    y_cut = (proba_1 >= thresh).astype(int)  # 自定义阈值下的预测标签数组
    recall = recall_score(y_true, y_cut)
    precision = precision_score(y_true, y_cut)
    f1 = f1_score(y_true, y_cut)
    print(f"{thresh:.2f}\t{recall*100:.2f}\t{precision*100:.2f}\t{f1*100:.2f}")
    if f1 > best_f1:
        best_f1 = f1
        best_thresh = thresh

print(f"\nF1最优阈值:{best_thresh:.2f},最优F1={best_f1*100:.2f}%")
txt 复制代码
阈值	召回率	精确率	F1
0.05	89.80	36.82	52.23
0.08	89.80	57.14	69.84
0.11	89.80	65.67	75.86
0.14	89.80	70.97	79.28
0.17	88.78	75.00	81.31
0.21	88.78	75.65	81.69
0.24	86.73	75.89	80.95
0.27	84.69	77.57	80.98
0.30	84.69	79.81	82.18
0.33	84.69	83.00	83.84
0.36	83.67	85.42	84.54
0.39	83.67	86.32	84.97
0.42	83.67	90.11	86.77
0.45	82.65	90.00	86.17
0.48	81.63	91.95	86.49
0.52	81.63	93.02	86.96
0.55	78.57	92.77	85.08
0.58	77.55	93.83	84.92
0.61	77.55	95.00	85.39
0.64	77.55	95.00	85.39
0.67	77.55	96.20	85.88
0.70	75.51	96.10	84.57
0.73	73.47	96.00	83.24
0.76	73.47	96.00	83.24
0.79	72.45	95.95	82.56
0.83	70.41	97.18	81.66
0.86	67.35	97.06	79.52
0.89	62.24	96.83	75.78
0.92	53.06	96.30	68.42
0.95	47.96	97.92	64.38

F1最优阈值:0.52,最优F1=86.96%

可以看到,将阈值从默认的 0.5 微调至 0.52 后,F1 从 86.49% 提升到 86.96%,精确率从 91.95% 提升到 93.02%,而召回率保持 81.63% 不变。在实际业务中,可以根据对漏检和误报的容忍度,在这张阈值表里选择最合适的工作点。


八、项目总结

  1. 数据集特征:极端不平衡二分类,欺诈样本仅 0.17%,准确率指标失效
  2. 预处理要点:分层划分stratify=y、标准化 Amount、剔除 Time
  3. 两大优化路线
    • 算法层面:class_weight,修改损失权重,不改动样本
    • 数据层面:SMOTE 过采样,仅训练集采样
  4. 模型对比:线性 LR 表达能力受限;随机森林非线性模型综合效果最优
  5. 重要工程结论:
    • 不平衡数据集优先使用 PR 曲线、PR-AUC,谨慎依赖 ROC-AUC
    • 不要固守 0.5 概率阈值,根据业务权衡精确率 / 召回,自定义分界值

九、项目说明

由于我做的时候使用的是 Jupyter Notebook ,所以代码都是一段一段的看起来可能不方便,还请见谅!此外如果聪明的你发现了代码和表述有错误或者有更好的提议,还请告知,谢谢!本文的前言和项目总结是让 AI 写的,做项目的过程中是让 AI 引导一步一步思考的,说明我对于所学知识的运用还是不熟练。如果你也想做的话,注意单元 1 中的文件地址要改改!文件下载:creditcard.csv.zip