第81课 - 实战:欺诈检测(不平衡数据处理)
所属模块:实战项目
难度:Advanced
标签:project, anomaly-detection
上一课:实战:时间序列预测
下一课:实战:客户分群分析
1. 学习目标
完成本课学习后,你将能够:
- 识别并理解现实场景中“不平衡数据”的核心特征及其对机器学习模型的巨大影响。
- 掌握至少三种处理不平衡数据的主流技术:过采样、欠采样和集成学习方法。
- 运用正确的评估指标(如精确率-召回率曲线、F1分数、AUC-PR)来评估在不平衡数据集上训练的模型。
- 独立完成一个完整的、使用真实数据的欺诈检测项目,涵盖数据探索、处理、建模与评估全流程。
2. 核心概念
什么是不平衡数据? 在分类问题中,如果不同类别的样本数量差异极其悬殊,我们称之为“不平衡数据”。以信用卡欺诈检测为例,99.9%的交易是“正常”的,只有0.1%是“欺诈”。数据严重向“多数类”倾斜。
为什么它很棘手? 传统的机器学习算法(如逻辑回归、决策树)在设计时通常默认各类别数据量相近。如果直接在不平衡数据上训练,模型会很快“发现”一个取巧的办法:只要把所有样本都预测为“多数类”(例如,全部预测为“正常交易”),就能获得极高的准确率(比如99.9%)。但这毫无意义,因为我们最关心的“欺诈”类被完全忽略了。
核心解决思路:我们需要让模型“看到”足够多的“少数类”样本,或者让算法更关注“少数类”。
1. 重采样技术
- 过采样:增加“少数类”样本的数量。最经典的方法是SMOTE。它不是简单复制,而是通过在现有的少数类样本之间进行插值,来合成新的、合理的样本。
- 欠采样:减少“多数类”样本的数量。优点是速度快,但缺点是可能丢失大量有用信息。
2. 集成学习方法
- 平衡随机森林:一种改进的随机森林算法。它在构建每棵决策树时,都会对多数类进行欠采样,从而在每棵树上实现类别平衡,最后再综合所有树的结果。
- EasyEnsemble:一种更先进的方法。它通过多次(例如,10次)随机地从多数类中抽取与少数类数量相当的子集,然后分别用这个子集与全部少数类数据训练一个AdaBoost分类器,最后集成所有分类器的结果。它尽可能多地利用了多数类的信息。
3. 评估指标的选择 对于不平衡数据,请抛弃准确率,转而关注:
- 精确率:预测为欺诈的样本中,真正是欺诈的比例。(查准率)
- 召回率:所有真正的欺诈样本中,被成功预测出来的比例。(查全率)
- F1分数:精确率和召回率的调和平均数,综合衡量模型表现。
- AUC-PR曲线下面积:比AUC-ROC更能反映模型在不平衡数据上的性能。
3. 代码示例
我们将使用经典的 Credit Card Fraud Detection 数据集的一个简化版本进行演示。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, BalancedRandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, average_precision_score, precision_recall_curve
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler
from imblearn.pipeline import Pipeline as imbPipeline # 注意:这里使用的是imblearn的Pipeline
from collections import Counter
# 1. 数据加载与初步探索
# 假设我们已经下载了数据集文件 `creditcard_sample.csv`
try:
df = pd.read_csv('creditcard_sample.csv')
print(f"数据加载成功!数据集形状:{df.shape}")
except FileNotFoundError:
print("请先下载数据集文件。")
# 模拟生成一个小型不平衡数据集用于演示
np.random.seed(42)
n_normal, n_fraud = 10000, 100
df = pd.DataFrame({
'Time': np.random.randn(n_normal + n_fraud),
'V1': np.random.randn(n_normal + n_fraud),
'Amount': np.random.exponential(scale=100, size=n_normal + n_fraud),
'Class': np.concatenate([np.zeros(n_normal), np.ones(n_fraud)]).astype(int)
})
print("已生成模拟数据集。")
print("\n类别分布:")
print(df['Class'].value_counts())
# 输出类似:0: 10000, 1: 100。比例约为100:1,非常不平衡。
# 2. 数据准备
X = df.drop('Class', axis=1)
y = df['Class']
# 标准化特征(对逻辑回归等模型很重要)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 划分训练集和测试集 - 关键:使用分层抽样(stratify)保持类别比例
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.2, random_state=42, stratify=y
)
print("\n处理后训练集类别分布:", Counter(y_train))
print("处理后测试集类别分布:", Counter(y_test))
# 3. 使用不同方法处理不平衡数据并建模
# 方法A:逻辑回归(基准,无处理)
lr = LogisticRegression(random_state=42, max_iter=1000)
lr.fit(X_train, y_train)
y_pred_lr = lr.predict(X_test)
# 方法B:使用SMOTE过采样
smote = SMOTE(random_state=42)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)
print("\nSMOTE后训练集类别分布:", Counter(y_train_smote))
lr_smote = LogisticRegression(random_state=42, max_iter=1000)
lr_smote.fit(X_train_smote, y_train_smote)
y_pred_lr_smote = lr_smote.predict(X_test)
# 方法C:使用平衡随机森林(集成方法)
brf = BalancedRandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
brf.fit(X_train, y_train)
y_pred_brf = brf.predict(X_test)
# 4. 评估比较
models = {
'Logistic Regression (原始)': y_pred_lr,
'Logistic Regression (SMOTE)': y_pred_lr_smote,
'Balanced Random Forest': y_pred_brf
}
for name, y_pred in models.items():
print(f"\n{'='*50}")
print(f"模型:{name}")
print(classification_report(y_test, y_pred))
# 特别关注少数类(欺诈,Class=1)的精确率、召回率和F1分数
# 5. 深入分析:平衡随机森林的特征重要性
feature_importances = brf.feature_importances_
feature_names = df.drop('Class', axis=1).columns
feat_imp_df = pd.DataFrame({'Feature': feature_names, 'Importance': feature_importances})
feat_imp_df = feat_imp_df.sort_values(by='Importance', ascending=False).head(10)
plt.figure(figsize=(10, 6))
sns.barplot(x='Importance', y='Feature', data=feat_imp_df)
plt.title('平衡随机森林 - 前10重要特征')
plt.tight_layout()
plt.show()
4. 实践练习
练习1:探究不同采样策略的影响 在上面的代码基础上,尝试以下组合,并比较它们在测试集上对“欺诈”类(Class=1)的F1分数:
RandomUnderSampler(欠采样) +LogisticRegressionSMOTE+RandomForestClassifier(标准随机森林)- 使用
imblearn.pipeline.Pipeline将SMOTE和LogisticRegression串联起来,实现更规范的流程。
练习2:优化模型与评估
- 尝试调整
BalancedRandomForestClassifier的n_estimators(树的数量)参数,观察模型性能(主要看AUC-PR)是否有提升。 - 计算并绘制平衡随机森林模型在测试集上的精确率-召回率曲线。提示:使用
precision_recall_curve和average_precision_score函数。
练习3:特征工程进阶(挑战)
除了V1和Amount,原始数据集通常还有V2, V3, ..., V28等匿名特征。尝试:
- 加载更完整的数据集(如果可用)。
- 分析
Amount特征的分布,是否需要进行对数变换等预处理? - 使用
SelectKBest或基于模型的特征选择方法,找出对识别欺诈最有帮助的几个特征,并重新训练模型,看性能是否变化。
5. 常见错误
- 错误1:在处理数据前进行划分。如果先在整个数据集上应用SMOTE等过采样,会导致合成样本污染测试集,使评估结果过于乐观。正确做法是先划分训练/测试集,再只对训练集进行重采样。
- 错误2:只关注准确率。在不平衡数据上,99%的准确率可能毫无价值。必须查看少数类的召回率和精确率。
- 错误3:过拟合风险。过采样(尤其是SMOTE)可能会创造出一些位于类别边界、不真实的样本,增加过拟合风险。可以通过交叉验证仔细评估。
- 错误4:忽略业务成本。在欺诈检测中,漏掉一个欺诈交易(假阴性)的成本通常远高于误报一个正常交易(假阳性)。调参时应更侧重于提高召回率,即使精确率会有所下降。
6. 小结
本节课我们深入实战了机器学习中一个经典且棘手的问题——不平衡数据的分类,以欺诈检测为应用场景。
- 核心挑战:不平衡数据导致模型偏向多数类,传统评估指标(准确率)失效。
- 处理技术:
- 重采样:过采样(SMOTE合成新样本)和欠采样(减少多数类)。
- 集成学习:平衡随机森林、EasyEnsemble等算法从算法层面解决不平衡问题。
- 评估关键:必须使用针对少数类的指标,如精确率、召回率、F1分数、AUC-PR。
- 实战流程:数据探索 -> 分层抽样划分 -> 仅对训练集应用处理技术 -> 使用合适指标评估 -> 根据业务需求调优(如侧重召回率)。
处理不平衡数据是数据科学家和机器学习工程师的必备技能。掌握了这些方法,你就能应对诸如欺诈检测、疾病诊断、稀有事件预测等一系列现实世界的关键问题。