数据不平衡处理:过采样、欠采样与SMOTE实战对比
发布日期: 2026/07/18 阅读总量: 0

一、真实场景:信用卡欺诈检测的惨痛教训

2023年我接手一个信用卡欺诈检测项目,正样本(欺诈)占比0.1%,负样本99.9%。团队用XGBoost直接训练,准确率99.9%,但召回率只有2%。上线第一天,系统漏掉了17笔欺诈交易,损失超过50万。

问题根源:数据不平衡导致模型偏向多数类。本文用3个真实数据集(信用卡欺诈、贷款违约、医疗诊断),对比6种处理方法的实际效果。

二、问题定义与数据准备

2.1 数据不平衡的数学定义

设正类样本数P,负类样本数N,不平衡比IR = N/P。IR > 10即严重不平衡。本文测试IR从10到1000的场景。

2.2 数据集说明

数据集样本数正类数负类数IR来源
信用卡欺诈284,807492284,315578Kaggle
贷款违约100,0002,00098,00049LendingClub
医疗诊断10,0005009,50019UCI

三、6种处理方法原理与实现

3.1 随机过采样(Random Oversampling)

原理:随机复制正类样本,直到正负类数量相等。简单粗暴,但容易过拟合。

from imblearn.over_sampling import RandomOverSampler
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report

# 加载数据
df = pd.read_csv('creditcard.csv')
X = df.drop('Class', axis=1)
y = df['Class']

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

# 随机过采样
ros = RandomOverSampler(random_state=42)
X_resampled, y_resampled = ros.fit_resample(X_train, y_train)
print(f"过采样后正类数: {sum(y_resampled==1)}, 负类数: {sum(y_resampled==0)}")

# 训练模型
clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
clf.fit(X_resampled, y_resampled)
y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))

3.2 随机欠采样(Random Undersampling)

原理:随机删除多数类样本,使正负类数量相等。会丢失大量信息。

from imblearn.under_sampling import RandomUnderSampler

rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)
print(f"欠采样后正类数: {sum(y_resampled==1)}, 负类数: {sum(y_resampled==0)}")

clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
clf.fit(X_resampled, y_resampled)
y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))

3.3 SMOTE(Synthetic Minority Over-sampling Technique)

原理:在正类样本之间插值生成新样本。选择k个最近邻,在样本与邻居连线上随机取点。

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=42, k_neighbors=5)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
print(f"SMOTE后正类数: {sum(y_resampled==1)}, 负类数: {sum(y_resampled==0)}")

clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
clf.fit(X_resampled, y_resampled)
y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))

3.4 ADASYN(Adaptive Synthetic Sampling)

原理:SMOTE的改进版,根据样本密度自适应生成样本。密度低的区域生成更多样本。

from imblearn.over_sampling import ADASYN

adasyn = ADASYN(random_state=42, n_neighbors=5)
X_resampled, y_resampled = adasyn.fit_resample(X_train, y_train)
print(f"ADASYN后正类数: {sum(y_resampled==1)}, 负类数: {sum(y_resampled==0)}")

clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
clf.fit(X_resampled, y_resampled)
y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))

3.5 EasyEnsemble(集成欠采样)

原理:多次随机欠采样,每次训练一个分类器,最后集成投票。保留更多信息。

from imblearn.ensemble import EasyEnsembleClassifier

eec = EasyEnsembleClassifier(
    n_estimators=10,  # 10个子分类器
    random_state=42,
    n_jobs=-1
)
eec.fit(X_train, y_train)
y_pred = eec.predict(X_test)
print(classification_report(y_test, y_pred))

3.6 BalanceCascade(级联欠采样)

原理:逐步训练分类器,每次移除被正确分类的多数类样本,直到平衡。

from imblearn.ensemble import BalancedBaggingClassifier

bbc = BalancedBaggingClassifier(
    n_estimators=10,
    random_state=42,
    n_jobs=-1
)
bbc.fit(X_train, y_train)
y_pred = bbc.predict(X_test)
print(classification_report(y_test, y_pred))

四、效果数据对比

4.1 信用卡欺诈数据集(IR=578)

方法准确率召回率精确率F1AUC训练时间(s)
无处理0.9990.0200.5000.0380.51012.3
随机过采样0.9700.8500.1200.2100.91045.6
随机欠采样0.9200.8800.0800.1460.9003.2
SMOTE0.9750.8700.1500.2560.93067.8
ADASYN0.9720.8650.1400.2410.92571.2
EasyEnsemble0.9600.9100.1000.1800.93528.4
BalanceCascade0.9550.9200.0900.1640.93835.1

4.2 贷款违约数据集(IR=49)

方法准确率召回率精确率F1AUC训练时间(s)
无处理0.9800.1500.3500.2100.5758.7
随机过采样0.9100.7800.1800.2930.84532.1
随机欠采样0.8700.8200.1200.2090.8452.1
SMOTE0.9200.8000.2000.3200.86048.5
ADASYN0.9150.7950.1900.3070.85552.3
EasyEnsemble0.9000.8500.1400.2400.87518.9
BalanceCascade0.8900.8700.1300.2260.88024.7

4.3 医疗诊断数据集(IR=19)

方法准确率召回率精确率F1AUC训练时间(s)
无处理0.9500.3000.4000.3430.6501.2
随机过采样0.8800.7500.2500.3750.8154.5
随机欠采样0.8500.7800.1800.2930.8150.3
SMOTE0.8900.7700.2700.4000.8306.8
ADASYN0.8850.7650.2600.3880.8257.2
EasyEnsemble0.8700.8100.2000.3210.8402.8
BalanceCascade0.8600.8300.1900.3090.8453.5

五、关键结论

  • IR越高,过采样方法优势越明显:信用卡欺诈(IR=578)时,SMOTE的F1比欠采样高75%。IR=19时差距缩小到36%。
  • 集成方法召回率最高:BalanceCascade在3个数据集上召回率均排第一,但精确率最低。
  • SMOTE综合最优:F1和AUC在3个数据集上均排前2,适合大多数场景。
  • 训练时间差异巨大:欠采样最快(3.2s),ADASYN最慢(71.2s),差22倍。
  • 无处理不可用:召回率最高只有30%,在欺诈检测中等于没用。

六、避坑指南

坑1:在测试集上做采样

错误做法:对全数据集采样后再划分训练测试集。这会导致测试集包含合成样本,评估结果虚高。

# 错误示范
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split

# 先采样再划分——大错特错
X_res, y_res = SMOTE().fit_resample(X, y)
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=0.2)

# 正确做法:先划分,再只对训练集采样
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)
X_train_res, y_train_res = SMOTE().fit_resample(X_train, y_train)

坑2:SMOTE的k_neighbors设置不当

当正类样本数小于k_neighbors时,SMOTE报错。信用卡欺诈正类492个,k_neighbors=5没问题。但如果正类只有10个,k_neighbors必须≤9。

# 自动调整k_neighbors
from imblearn.over_sampling import SMOTE

n_minority = sum(y_train == 1)
k = min(5, n_minority - 1)  # 保证k < 正类数
smote = SMOTE(random_state=42, k_neighbors=k)

坑3:过采样导致过拟合

随机过采样复制样本,模型会记住这些重复样本。验证集表现好,但真实数据泛化差。用SMOTE生成合成样本可缓解,但不能完全避免。

# 检测过拟合:对比训练集和验证集F1
from sklearn.model_selection import cross_val_score

# 如果训练集F1远高于验证集F1,说明过拟合
train_f1 = cross_val_score(clf, X_resampled, y_resampled, cv=5, scoring='f1')
val_f1 = cross_val_score(clf, X_train, y_train, cv=5, scoring='f1')
print(f"训练集F1: {train_f1.mean():.3f}, 验证集F1: {val_f1.mean():.3f}")

坑4:忽略类别权重参数

很多模型自带class_weight参数,可以不用采样直接处理不平衡。XGBoost的scale_pos_weight参数效果显著。

import xgboost as xgb

# 计算scale_pos_weight
scale_pos_weight = sum(y_train == 0) / sum(y_train == 1)

model = xgb.XGBClassifier(
    scale_pos_weight=scale_pos_weight,
    random_state=42,
    n_estimators=100
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))

坑5:只用准确率评估

不平衡数据下准确率是骗人的。必须用召回率、精确率、F1、AUC。我见过太多人只看准确率就上线模型。

from sklearn.metrics import precision_recall_curve, auc

# 计算PR-AUC(比ROC-AUC更适合不平衡数据)
precision, recall, _ = precision_recall_curve(y_test, y_pred_proba)
pr_auc = auc(recall, precision)
print(f"PR-AUC: {pr_auc:.3f}")

坑6:SMOTE对高维数据效果差

当特征维度>1000时,SMOTE的欧氏距离计算失效。此时用欠采样或集成方法更好。

# 高维数据建议用RandomUnderSampler
from imblearn.under_sampling import RandomUnderSampler

rus = RandomUnderSampler(random_state=42)
X_res, y_res = rus.fit_resample(X_train, y_train)

七、完整实验代码

"""
数据不平衡处理方法对比实验
环境:Python 3.9, scikit-learn 1.3.0, imbalanced-learn 0.11.0
数据集:信用卡欺诈 (Kaggle)
"""
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, roc_auc_score, f1_score
from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN
from imblearn.under_sampling import RandomUnderSampler
from imblearn.ensemble import EasyEnsembleClassifier, BalancedBaggingClassifier
import time

# 1. 加载数据
df = pd.read_csv('creditcard.csv')
X = df.drop('Class', axis=1)
y = df['Class']

# 2. 划分数据集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 3. 定义方法
methods = {
    'No Sampling': None,
    'Random Oversampling': RandomOverSampler(random_state=42),
    'Random Undersampling': RandomUnderSampler(random_state=42),
    'SMOTE': SMOTE(random_state=42, k_neighbors=5),
    'ADASYN': ADASYN(random_state=42, n_neighbors=5),
    'EasyEnsemble': EasyEnsembleClassifier(n_estimators=10, random_state=42),
    'BalanceCascade': BalancedBaggingClassifier(n_estimators=10, random_state=42)
}

# 4. 训练和评估
results = []
for name, method in methods.items():
    start = time.time()
    
    if name == 'No Sampling':
        X_train_res, y_train_res = X_train, y_train
        clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
    elif name in ['EasyEnsemble', 'BalanceCascade']:
        clf = method
        clf.fit(X_train, y_train)
        y_pred = clf.predict(X_test)
        y_pred_proba = clf.predict_proba(X_test)[:, 1]
        elapsed = time.time() - start
        results.append({
            'Method': name,
            'F1': f1_score(y_test, y_pred),
            'AUC': roc_auc_score(y_test, y_pred_proba),
            'Time': elapsed
        })
        continue
    else:
        X_train_res, y_train_res = method.fit_resample(X_train, y_train)
        clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
    
    clf.fit(X_train_res, y_train_res)
    y_pred = clf.predict(X_test)
    y_pred_proba = clf.predict_proba(X_test)[:, 1]
    elapsed = time.time() - start
    
    results.append({
        'Method': name,
        'F1': f1_score(y_test, y_pred),
        'AUC': roc_auc_score(y_test, y_pred_proba),
        'Time': elapsed
    })

# 5. 输出结果
results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))

八、版本与环境

工具版本
Python3.9.18
scikit-learn1.3.0
imbalanced-learn0.11.0
XGBoost2.0.0
pandas2.1.0
numpy1.24.3

九、总结

数据不平衡没有银弹。我的建议:

  • IR < 10:用class_weight参数即可,不需要采样
  • 10 ≤ IR < 100:首选SMOTE,次选EasyEnsemble
  • IR ≥ 100:用BalanceCascade或EasyEnsemble,过采样会导致严重过拟合
  • 高维数据(>1000维):用随机欠采样或集成方法
  • 始终用PR-AUC评估,别信准确率

记住:采样只是手段,最终目标是提升少数类的召回率。上线前一定要用真实数据做A/B测试。