【机器学习实战】信用卡欺诈检测:从数据不平衡处理到模型优化全流程解析
1. 从零开始:理解信用卡欺诈检测的挑战
大家好,我是老张,在金融风控领域摸爬滚打了十来年,经手过的反欺诈模型项目少说也有几十个。今天想和大家聊聊一个非常经典,但也让无数新手头疼的实战项目——信用卡欺诈检测。如果你刚接触机器学习,想找一个能串联起数据清洗、特征工程、模型调优全流程的练手项目,那这个Kaggle上的信用卡欺诈数据集绝对是你的不二之选。
这个项目听起来高大上,但核心问题其实很直接:给你一堆信用卡交易记录,让你用算法判断哪一笔是盗刷。听起来像侦探破案,对吧?但难点在于,数据极度不平衡。想象一下,你检查了10万笔交易,其中只有172笔是欺诈,就像大海捞针。如果你让一个模型去学,它很可能“偷懒”,把所有交易都预测为“正常”,这样准确率高达99.8%以上,看起来漂亮极了,但一个欺诈都没抓到,模型完全没用。这就是我们面临的第一个,也是最核心的挑战:数据不平衡。不解决它,后续所有模型构建都是空中楼阁。
除了数据不平衡,这个数据集本身也很有特点。出于隐私保护,原始特征(比如交易商户、地点)都被PCA(主成分分析)处理过了,变成了V1到V28这28个神秘的数字特征。我们能直接用的只有“时间”和“交易金额”。这要求我们不能简单地套用模型,必须深入理解数据,做好特征工程,从有限的信息里挖出“金子”。整个流程,从数据导入、探索分析,到处理不平衡、训练模型、调参优化,再到最后根据业务选择阈值,是一套完整的机器学习实战链路。接下来,我就带你一步步走通它,分享我踩过的坑和验证过的好方法。
2. 数据预处理与探索性分析:像侦探一样审视数据
拿到数据后的第一步,绝不是急着跑模型。我习惯先花足够的时间“把玩”数据,理解它的每一寸肌肤。这就像侦探勘察现场,任何细节都可能成为破案关键。我们用的数据集来自Kaggle,包含了欧洲持卡人两天内的28万多笔交易,其中欺诈交易仅有492笔,占比0.172%。
2.1 数据初窥与质量检查
首先,我们得看看数据长什么样,有没有“硬伤”。用Pandas加载数据后,head()、tail()、info()、describe()这几个函数是我的标准动作。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# 加载数据
data = pd.read_csv('creditcard.csv')
print(f"数据形状: {data.shape}")
print(data.info())
print(data.describe())
运行后你会发现,数据有31列,从Time、V1...V28到Amount和Class。好消息是,info()显示所有字段都是非空的,没有缺失值,这省去了我们处理缺失值的大麻烦。describe()则能快速查看数值特征的统计分布,比如Amount(金额)的均值、标准差,你会发现它的范围波动很大,从0到几万,这提示我们后续可能需要做标准化。
2.2 深入洞察:可视化揭示数据秘密
数字是冰冷的,图表却能讲故事。对于不平衡数据,我们首先要看清“不平衡”到什么程度。
# 查看目标变量分布
fig, axs = plt.subplots(1, 2, figsize=(14, 5))
sns.countplot(x='Class', data=data, ax=axs[0])
axs[0].set_title('欺诈与非欺诈交易数量')
data['Class'].value_counts().plot.pie(autopct='%1.2f%%', ax=axs[1])
axs[1].set_title('类别占比')
plt.show()
图表会直观地告诉你,正常交易(Class=0)的柱子高耸入云,而欺诈交易(Class=1)的柱子几乎贴着地面。这再次强调了处理不平衡问题的紧迫性。
接下来,分析欺诈行为的特点。我通常会从时间和金额两个维度入手:
-
交易时间分析:原始‘Time’特征是以秒为单位的连续值,不利于分析。我通常会将其转换为小时‘Hour’,看看欺诈是否集中在特定时段。
data['Hour'] = data['Time'].apply(lambda x: int(x // 3600) % 24)然后绘制欺诈交易在不同小时的分布图。我常发现,欺诈交易在深夜至凌晨(比如0点到6点)的相对比例可能更高,因为这时真实用户活跃度低,不易被察觉。
-
交易金额分析:比较欺诈交易和正常交易的金额分布。
f, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(12,8)) ax1.hist(data[data['Class']==1]['Amount'], bins=50, color='red', alpha=0.7) ax1.set_title('欺诈交易金额分布') ax1.set_yscale('log') # 因为欺诈交易少,用对数坐标更清晰 ax2.hist(data[data['Class']==0]['Amount'], bins=100, color='blue', alpha=0.7) ax2.set_title('正常交易金额分布') ax2.set_yscale('log') plt.xlabel('Amount ($)') plt.show()你往往会发现,欺诈交易的金额大多较小且分散。盗刷者为了不触发风控警报,倾向于进行多次小额测试或消费。
-
特征间关系分析:虽然V1-V28是PCA结果,含义不明,但我们仍可以观察它们在欺诈和正常样本中的分布差异。使用
seaborn的distplot或kdeplot绘制同一特征在两个类别下的概率密度曲线。如果某个特征(如V14, V17)的分布曲线在两类间分离明显,那它很可能是一个强特征。反之,如果曲线几乎重合(如V22, V23),这个特征对区分欺诈的帮助可能就很小,可以考虑在后续剔除,以降低模型复杂度和过拟合风险。
3. 特征工程:为模型烹饪“美味佳肴”
数据探索之后,我们就要开始为模型准备“食材”了。特征工程做得好,模型效果差不了。这一步的目标是创造、选择和转换特征,让机器学习算法更容易学习到规律。
3.1 特征缩放:让所有特征站在同一起跑线
我们的特征中,‘Time’(或我们衍生的‘Hour’)和‘Amount’的数值范围与其他V1-V28特征相差巨大。V特征经过PCA后,通常服从标准正态分布(均值为0,标准差为1)。而‘Amount’可能从几块到几万块。如果不做处理,像逻辑回归、SVM、KNN这类基于距离或梯度的模型会被大数值特征主导。我常用的方法是标准化(StandardScaler),也就是Z-score归一化。
from sklearn.preprocessing import StandardScaler
# 假设我们已经创建了'Hour'特征,并决定对'Amount'和'Hour'进行缩放
cols_to_scale = ['Amount', 'Hour']
scaler = StandardScaler()
data[cols_to_scale] = scaler.fit_transform(data[cols_to_scale])
这步操作后,‘Amount’和‘Hour’也会变成均值为0,标准差为1的分布,和其他V特征“平等”了。
3.2 特征选择:去芜存菁,提升效率
不是所有特征都是有用的。有些特征可能是噪音,有些可能与其他特征高度相关(共线性)。特征选择能简化模型、加快训练速度、有时甚至能提升模型泛化能力。
对于这个数据集,我们可以用几种方法:
-
基于统计检验:如使用
SelectKBest配合卡方检验或F检验,选择与目标变量最相关的K个特征。 -
基于模型的特征重要性:训练一个树模型(如随机森林或XGBoost),查看特征重要性排序。
from sklearn.ensemble import RandomForestClassifier # 准备特征X和目标y(先不处理不平衡) X_temp = data.drop('Class', axis=1) y_temp = data['Class'] # 使用少量样本加快速度,注意保持类别比例(使用stratify) X_sample, _, y_sample, _ = train_test_split(X_temp, y_temp, test_size=0.7, stratify=y_temp, random_state=42) rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1) rf.fit(X_sample, y_sample) # 获取特征重要性 importances = rf.feature_importances_ indices = np.argsort(importances)[::-1] # 绘制重要性条形图 plt.figure(figsize=(12,6)) plt.title("随机森林特征重要性") plt.bar(range(X_temp.shape[1]), importances[indices]) plt.xticks(range(X_temp.shape[1]), X_temp.columns[indices], rotation=90) plt.show()从图中可以清晰看到V17、V14、V12、V10等特征重要性较高,而V22、V23、V24等重要性极低。我们可以考虑剔除重要性为0或极低的特征。
-
基于相关性的分析:分别计算欺诈样本和正常样本内部的特征相关性矩阵,并可视化。有时能发现,在欺诈样本中,某些特征间的相关性模式与正常样本不同,这些特征就值得保留。
在实际操作中,我通常会结合模型重要性排序和业务理解(比如时间、金额肯定重要)来最终确定特征子集。一个常见的做法是,先保留所有特征进行第一次建模,根据模型结果和特征重要性,再迭代进行特征筛选。
4. 应对数据不平衡:SMOTE过采样实战详解
终于来到这个项目的核心挑战环节。直接在不平衡数据上训练模型,模型会严重偏向多数类。我们之前提到的99.8%的准确率陷阱就是这么来的。解决不平衡问题主要有三大类方法:调整样本权重(Class Weight)、欠采样(Undersampling) 和过采样(Oversampling)。
- 调整样本权重:在模型(如逻辑回归、SVM、决策树)中设置
class_weight='balanced',让算法在计算损失时更“在意”少数类的错误。这种方法简单有效,是我的首选尝试方案。 - 欠采样:随机去掉一部分多数类样本,使两类样本数量接近。缺点是会丢失大量信息,可能影响模型性能。
- 过采样:增加少数类样本的数量。最简单的是随机复制少数类样本,但这容易导致过拟合。
这里我们重点介绍一个更高级的过采样技术——SMOTE(合成少数类过采样技术)。它不是在简单地复制样本,而是通过“创造”新样本来丰富少数类的分布。SMOTE的原理很巧妙:对于每一个少数类样本,它找到这个样本在特征空间中的K个最近邻(也是少数类样本),然后在这条连线上随机选择一个点,作为新合成的样本。
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split
# 首先,划分训练集和测试集!**非常重要**:必须在训练集上应用SMOTE,测试集必须保持原始分布以评估真实效果。
X = data.drop('Class', axis=1)
y = data['Class']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) # stratify保持分布
print("原始训练集类别分布:", y_train.value_counts())
# 应用SMOTE
smote = SMOTE(random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
print("SMOTE后训练集类别分布:", pd.Series(y_train_resampled).value_counts())
这里有一个至关重要的细节,也是新手常踩的坑:一定要先划分训练集和测试集,再对训练集进行过采样! 如果先过采样再划分,那么合成的新样本就会泄漏到测试集中,导致评估结果严重虚高,模型在实际应用中会表现很差。用我踩过坑的经验告诉你,这个顺序绝对不能错。
应用SMOTE后,你会发现训练集中欺诈和正常的样本一样多了。这时再用这个平衡的数据集去训练模型,模型就不会再忽视少数类了。不过,SMOTE也不是万能的,如果少数类样本本身非常少或者噪音很大,SMOTE生成的新样本可能并不合理。在实际项目中,我常会对比class_weight、SMOTE以及它们的组合效果,选择最适合当前数据的方法。
5. 模型构建、训练与初步评估
数据准备好了,我们就可以开始搭建模型了。信用卡欺诈检测是一个二分类问题,可选的算法很多。逻辑回归简单可解释,树模型(随机森林、XGBoost)强大且能捕捉非线性关系,深度学习模型更复杂但需要大量数据。作为实战入门,我们从经典的逻辑回归开始,它速度快,结果有概率意义,而且模型系数可以解释特征的重要性。
5.1 逻辑回归建模
我们用SMOTE处理后的平衡训练集来训练模型。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
# 初始化模型,可以设置class_weight='balanced'作为对比,但这里我们已经过采样了
model_lr = LogisticRegression(random_state=42, max_iter=1000, solver='liblinear')
# 使用过采样后的训练集
model_lr.fit(X_train_resampled, y_train_resampled)
# 在测试集上预测(注意:测试集是原始不平衡的!)
y_pred = model_lr.predict(X_test)
y_pred_proba = model_lr.predict_proba(X_test)[:, 1] # 获取预测为欺诈的概率
print("准确率 Accuracy:", accuracy_score(y_test, y_pred))
print("\n分类报告:\n", classification_report(y_test, y_pred))
print("\n混淆矩阵:\n", confusion_matrix(y_test, y_pred))
看一下输出,你会发现准确率可能比之前盲目预测全部为正常时要低一些,比如降到95%左右。但这恰恰是好事!因为混淆矩阵会显示,模型现在能抓到一部分欺诈交易了(True Positive)。在欺诈检测中,我们最关心的指标不是准确率,而是召回率(Recall)和精确率(Precision)。
- 召回率(Recall):所有真实的欺诈交易中,被模型正确找出来的比例。我们希望这个值越高越好,意味着漏报(False Negative)少。
- 精确率(Precision):所有被模型预测为欺诈的交易中,真正是欺诈的比例。我们也希望这个值高,意味着误报(False Positive)少。
但这两者往往相互矛盾:想抓住更多欺诈(提高召回率),就可能把更多正常交易误判为欺诈(降低精确率)。这个平衡点需要根据业务成本来决定。初步模型的结果给了我们一个基线,接下来就要通过调优来寻找更好的平衡。
5.2 评估指标可视化:ROC与PR曲线
除了看数字,绘制曲线能更直观地评估模型性能。
-
ROC曲线与AUC:ROC曲线描绘了在不同阈值下,真正例率(TPR,即召回率)和假正例率(FPR)的关系。AUC是曲线下的面积,越接近1模型越好。ROC曲线对不平衡数据不太敏感。
from sklearn.metrics import roc_curve, auc fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba) roc_auc = auc(fpr, tpr) plt.figure() plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})') plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--') plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver Operating Characteristic (ROC) Curve') plt.legend(loc="lower right") plt.show() -
精确率-召回率曲线(PR Curve):对于高度不平衡的数据,PR曲线比ROC曲线更具参考价值。它展示了不同阈值下精确率和召回率的权衡关系。
from sklearn.metrics import precision_recall_curve precision, recall, _ = precision_recall_curve(y_test, y_pred_proba) pr_auc = auc(recall, precision) plt.figure() plt.plot(recall, precision, color='blue', lw=2, label=f'PR curve (area = {pr_auc:.2f})') plt.xlabel('Recall') plt.ylabel('Precision') plt.title('Precision-Recall Curve') plt.legend(loc="upper right") plt.show()一个能有效识别欺诈的模型,其PR曲线的AUC应该明显高于少数类的比例(本例中0.172%)。
6. 模型优化:网格搜索与交叉验证调参
初步模型有了,但它的参数(如逻辑回归的正则化强度C和惩罚类型penalty)可能不是最优的。手动调参费时费力,我们使用网格搜索(Grid Search) 结合交叉验证(Cross Validation) 来自动化这个过程。
6.1 网格搜索交叉验证原理
交叉验证,比如10折交叉验证,会把训练集分成10份,轮流用其中9份训练,1份验证,循环10次,得到10个性能评估结果的平均值。这比单次划分训练/验证集更稳定,能更好地评估模型泛化能力。
网格搜索则是暴力美学:我们事先设定好一组想要尝试的参数组合(比如C: [0.01, 0.1, 1, 10, 100], penalty: ['l1', 'l2']),网格搜索会遍历所有组合,对每一组参数都用交叉验证来评估,最后选出在交叉验证集上平均得分最高的那组参数。
Scikit-learn提供了GridSearchCV这个强大的工具,一站式搞定。
from sklearn.model_selection import GridSearchCV
# 定义参数网格
param_grid = {
'C': [0.001, 0.01, 0.1, 1, 10, 100], # 正则化强度的倒数,越小正则化越强
'penalty': ['l1', 'l2'], # 正则化类型
'solver': ['liblinear'] # 支持l1和l2的求解器
}
# 创建网格搜索对象,使用5折交叉验证,以召回率(recall)作为评估指标
# 注意:这里我们使用过采样后的训练集 X_train_resampled, y_train_resampled
grid_search = GridSearchCV(LogisticRegression(random_state=42, max_iter=1000),
param_grid,
cv=5,
scoring='recall', # 重点关注召回率
n_jobs=-1, # 使用所有CPU核心并行计算
verbose=1) # 输出进度
grid_search.fit(X_train_resampled, y_train_resampled)
# 输出最佳参数和最佳得分
print("Best parameters found: ", grid_search.best_params_)
print("Best cross-validation recall score: {:.4f}".format(grid_search.best_score_))
这个过程可能需要一些时间,因为要训练模型 (6种C * 2种penalty * 5折) = 60 次。完成后,我们就得到了在交叉验证集上召回率最高的那组超参数。
6.2 用最优模型重新评估
得到最佳参数后,我们用这组参数在整个过采样的训练集上重新训练一个最终模型,并在从未参与过训练和参数搜索的原始测试集上进行最终评估。
# 获取最佳模型
best_lr_model = grid_search.best_estimator_
# 在测试集上做最终预测
y_test_pred = best_lr_model.predict(X_test)
y_test_pred_proba = best_lr_model.predict_proba(X_test)[:, 1]
print("优化后模型在测试集上的表现:")
print("准确率:", accuracy_score(y_test, y_test_pred))
print("\n分类报告:\n", classification_report(y_test, y_test_pred))
# 计算并绘制优化后的混淆矩阵
cnf_matrix = confusion_matrix(y_test, y_test_pred)
# 可以定义一个函数来绘制美观的混淆矩阵
def plot_confusion_matrix_custom(cm, classes):
plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
plt.title('Confusion Matrix')
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes)
plt.yticks(tick_marks, classes)
thresh = cm.max() / 2.
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
plt.text(j, i, format(cm[i, j], 'd'),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.tight_layout()
plot_confusion_matrix_custom(cnf_matrix, classes=['Normal', 'Fraud'])
plt.show()
对比优化前后的混淆矩阵和召回率,你应该能看到明显的提升。模型现在能捕捉到更多的欺诈交易,同时(希望)没有引入过多的误报。
7. 决策阈值调整:在业务与性能间寻找平衡
模型输出的是“欺诈概率”,比如0.87。我们通常默认以0.5为阈值,概率大于0.5的判为欺诈,反之正常。但这个0.5是任意的。调整决策阈值是模型上线前最关键的一步,它直接决定了模型在业务中的“松紧度”。
7.1 理解阈值的影响
提高阈值(比如从0.5升到0.9),模型会变得更“严格”,只有非常确信的交易才会被判定为欺诈。这会导致:
- 精确率(Precision)上升:被抓出来的欺诈里,真的欺诈比例更高。
- 召回率(Recall)下降:很多欺诈交易因为概率不够高而被漏掉。
降低阈值(比如从0.5降到0.1),模型会变得更“宽松”,更多交易被判定为欺诈。这会导致:
- 召回率(Recall)上升:能抓住更多的欺诈交易。
- 精确率(Precision)下降:被误判的正常交易增多,增加了运营团队的工作量和客户投诉风险。
7.2 如何选择最优阈值?
没有理论上的“最优”,只有业务上的“最合适”。这需要和业务方(风控、运营团队)紧密沟通。我们可以通过分析不同阈值下的性能指标,来辅助决策。
# 尝试一系列阈值
thresholds = np.arange(0.1, 1.0, 0.1)
recall_list = []
precision_list = []
f1_list = []
for thresh in thresholds:
# 根据阈值调整预测类别
y_pred_adjusted = (y_test_pred_proba >= thresh).astype(int)
cm = confusion_matrix(y_test, y_pred_adjusted)
tn, fp, fn, tp = cm.ravel()
recall = tp / (tp + fn) if (tp+fn) > 0 else 0
precision = tp / (tp + fp) if (tp+fp) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision+recall) > 0 else 0
recall_list.append(recall)
precision_list.append(precision)
f1_list.append(f1)
# 绘制指标随阈值变化曲线
plt.figure(figsize=(10,6))
plt.plot(thresholds, recall_list, 'b-', label='Recall')
plt.plot(thresholds, precision_list, 'g-', label='Precision')
plt.plot(thresholds, f1_list, 'r-', label='F1-Score')
plt.xlabel('Decision Threshold')
plt.ylabel('Score')
plt.title('Model Performance vs. Decision Threshold')
plt.legend()
plt.grid(True)
plt.show()
从图中,你可以看到召回率和精确率随阈值变化的“剪刀差”曲线。F1-Score是两者的调和平均数,其峰值对应的阈值可能是一个不错的折中点。但最终,你需要结合业务成本来定:调查一个误报需要多少人力成本?漏掉一个欺诈交易会带来多少资金损失?当边际收益和边际成本达到平衡时,对应的阈值就是业务上的最优解。
8. 项目总结与进阶思考
走完这一整套流程,你已经成功构建了一个从原始数据到优化模型的信用卡欺诈检测原型。我们经历了数据探索、特征工程、处理不平衡(SMOTE)、模型训练(逻辑回归)、超参数调优(网格搜索交叉验证)和阈值调整。这几乎是任何一个监督学习分类项目的标准流程。
回顾一下关键点:数据不平衡是核心挑战,SMOTE或类别权重是常用解法;特征工程是提升模型上限的关键,需要耐心探索和尝试;网格搜索交叉验证能系统性地寻找好参数,避免手动调参的盲目性;模型评估不能只看准确率,召回率、精确率、PR曲线、ROC曲线要综合看;决策阈值不是固定的0.5,必须根据业务实际情况调整。
这个项目还有很大的扩展空间。你可以尝试其他算法,比如随机森林或梯度提升树(如XGBoost, LightGBM),它们通常能取得比逻辑回归更好的性能。也可以尝试更复杂的特征工程,比如基于交易序列构造特征(本次数据的时间序列性不强)。还可以探索深度学习模型,如简单的全连接网络或更复杂的序列模型。另外,将模型部署成一个小型的实时API服务,模拟线上风控场景,也是非常棒的实战练习。
我在实际项目中还发现,单一模型有时会有瓶颈,可以考虑将逻辑回归、树模型等结果进行模型融合(Ensemble),比如投票法或堆叠法,往往能进一步提升稳定性和性能。机器学习实战就是一个不断迭代、优化和权衡的过程。希望这个详细的流程解析能帮你打下扎实的基础,少走一些我当年走过的弯路。最重要的是动手把代码跑起来,调整参数,观察结果变化,这才是成长最快的方式。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)