分类建模实战:交叉验证调参、下采样与 SMOTE 过采样解决样本失衡问题
前言
在做分类任务时,我们经常会遇到两个头疼的问题:一是模型参数怎么调才靠谱,二是数据集类别极度不平衡怎么办。今天就通过贷款资格检测这个经典数据集,把交叉验证调参、下采样和过采样三种方法从头到尾走一遍。在银行信贷风控场景中,最典型的问题就是样本不平衡:绝大多数申请人会正常还款,真正违约的用户只占极小比例。直接用原始数据训练模型,往往整体准确率能到 99%,但对违约样本的识别能力几乎为零,完全达不到风控预警的要求。本文基于某银行脱敏后的贷款申请数据集,从基础的交叉验证调参讲起,再到下采样、SMOTE 过采样两种不平衡处理方案,一步步落地实现。所有代码均附带详细注释,可直接复现运行。
一、交叉验证调参
我们平时把数据切成训练集和测试集,用训练集训模型、测试集看效果,这种方法有个问题:单次划分的结果偶然性很大,换个随机种子可能准确率就变了。
交叉验证的思路很简单:把训练集分成 K 份,轮流用 K-1 份训练、剩下 1 份验证,重复 K 次后取平均得分。这样得到的模型性能评估更稳定,调参也更可信。
1.2 完整代码与逐段讲解
① 导入工具包与混淆矩阵绘图函数
import pandas as pd import numpy as np from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt # 混淆矩阵可视化函数 def cm_plot(y, yp): cm = confusion_matrix(y, yp) plt.matshow(cm, cmap=plt.cm.Blues) plt.colorbar() for x in range(len(cm)): for y in range(len(cm)): plt.annotate(cm[x, y], xy=(y, x), horizontalalignment="center", verticalalignment="center") plt.ylabel('True label') plt.xlabel('Predicted label') return plt这里封装了一个混淆矩阵绘图函数,后面三个 demo 都会用到,作用是把预测结果直观地展示成热力图,每个格子里标上具体样本数。
② 数据读取与预处理
# 读取数据 data = pd.read_csv("creditcard.csv") # 对 Amount 字段做标准化 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() data['Amount'] = scaler.fit_transform(data[['Amount']]) # 删除 Time 列(对分类无帮助) data = data.drop(['Time'], axis=1) # 划分特征与标签 x = data.drop(['Class'], axis=1) y = data['Class'] # 划分训练集和测试集 7:3 from sklearn.model_selection import train_test_split x_train, x_test, y_train, y_test = train_test_split( x, y, test_size=0.3, random_state=0 )知识点:
StandardScaler把数据缩放到均值为 0、方差为 1,逻辑回归对特征尺度敏感,必须做标准化。Time列是交易发生的时间戳,对欺诈检测没有直接意义,直接删掉。random_state=0固定随机种子,保证每次运行划分结果一致,方便复现。
③ 交叉验证调参(核心部分)
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score scores = [] c_param_range = [0.01, 0.1, 1, 10, 100] for i in c_param_range: lr = LogisticRegression(C=i, solver='lbfgs', max_iter=1000) # 8折交叉验证,评估指标为准确率 score = cross_val_score(lr, x_train, y_train, cv=8, scoring='accuracy') score_mean = sum(score) / len(score) scores.append(score_mean) print(score_mean) # 选出最优的 C 值 best_c = c_param_range[np.argmax(scores)] print(f"最优惩罚因子为:{best_c}")关键知识点:
- C 参数:逻辑回归中的正则化系数。C 越小,正则化越强,模型越简单,越不容易过拟合;C 越大,正则化越弱,模型越复杂。
cv=8表示 8 折交叉验证,把训练集切成 8 份,跑 8 次取平均。scoring='accuracy'用准确率作为评估指标。- 最后用
np.argmax(scores)找到得分最高对应的 C 值
④ 模型训练与结果评估
# 用最优参数训练最终模型 lr = LogisticRegression(C=best_c, max_iter=1000) lr.fit(x_train, y_train) from sklearn import metrics # 训练集表现 train_predicted = lr.predict(x_train) print(metrics.classification_report(y_train, train_predicted)) cm_plot(y_train, train_predicted).show() # 测试集表现 test_predicted = lr.predict(x_test) print(metrics.classification_report(y_test, test_predicted, digits=6)) cm_plot(y_test, test_predicted).show()classification_report会输出精确率、召回率、F1 值等指标。跑完你会发现,整体准确率特别高(99%+),但少数类(老赖)的召回率其实很低 —— 这就是类别不平衡带来的问题,也是后面两个 demo 要解决的。
二、下采样处理类别不平衡
2.1 什么是下采样
银行贷款数据集中,正常贷款占 99% 以上,老赖贷款只有不到 1%,模型哪怕全预测为正常也能有 99% 的准确率,但毫无实际意义。
下采样(Undersampling)的做法是:从数量多的类别里随机抽取一部分,让正负样本数量相等。简单粗暴,但缺点是会丢失大量多数类信息。
2.2 核心代码
① 下采样实现
# 先把标签拼回训练集,方便采样 x_train['Class'] = y_train data_train = x_train # 分离正负样本 positive_eg = data_train[data_train['Class'] == 0] # 正常交易 negative_eg = data_train[data_train['Class'] == 1] # 欺诈交易 # 从多数类中随机抽取与少数类数量相同的样本 positive_eg = positive_eg.sample(len(negative_eg)) # 拼接得到平衡数据集 data_c = pd.concat([positive_eg, negative_eg]) x = data_c.drop(['Class'], axis=1) y = data_c['Class']核心就是sample(len(negative_eg))这一行,从正常交易里随机抽出和欺诈交易一样多的样本,实现 1:1 平衡。
② 交叉验证调参 + 训练评估
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score scores = [] c_param_range = [0.01, 0.1, 1, 10, 100] for i in c_param_range: lr = LogisticRegression(C=i, solver='lbfgs', max_iter=2000) score = cross_val_score(lr, x, y, cv=8) score_mean = sum(score) / len(score) scores.append(score_mean) print(score_mean) best_c = c_param_range[np.argmax(scores)] print(f"最优惩罚因子为:{best_c}") # 训练最终模型 lr = LogisticRegression(C=best_c, solver='lbfgs', max_iter=2000) lr.fit(x, y)注意:交叉验证是在下采样后的平衡数据集上做的,但最终评估一定要在原始测试集上跑,这样才能反映真实场景的效果。
from sklearn import metrics # 在原始训练集上评估 train_predicted = lr.predict(x_train.drop(['Class'], axis=1)) print(metrics.classification_report(y_train, train_predicted)) cm_plot(y_train, train_predicted).show() # 在原始测试集上评估 test_predicted = lr.predict(x_test) print(metrics.classification_report(y_test, test_predicted)) cm_plot(y_test, test_predicted).show()下采样后,你会发现整体准确率下降了,但老赖贷款的召回率明显提升了 —— 这才是我们真正关心的指标,毕竟漏掉一笔老赖贷款的代价远大于误判一笔正常交易。
三、SMOTE 过采样(demo03 核心讲解)
3.1 什么是过采样
下采样是减少多数类,过采样(Oversampling)则是增加少数类。最简单的过采样是直接复制少数类样本,但容易过拟合。
SMOTE(Synthetic Minority Oversampling Technique)是更智能的做法:在少数类样本之间插值,人工合成新的少数类样本,既平衡了数据集,又不容易过拟合。
3.2 核心代码
① SMOTE 过采样实现
from imblearn.over_sampling import SMOTE oversample = SMOTE(random_state=0) os_x_train, os_y_train = oversample.fit_resample(x_train, y_train)就这三行代码,imblearn库直接帮你完成 SMOTE 算法,输出的os_x_train和os_y_train就是平衡后的训练集,正负样本 1:1。
使用前需要先安装:
pip install imbalanced-learn
② 交叉验证调参 + 训练评估
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score scores = [] c_param_range = [0.01, 0.1, 1, 10, 100] for i in c_param_range: lr = LogisticRegression(C=i, solver='lbfgs', max_iter=1000) score = cross_val_score(lr, os_x_train, os_y_train, cv=5) score_mean = sum(score) / len(score) scores.append(score_mean) print(score_mean) best_c = c_param_range[np.argmax(scores)] print(f"最优惩罚因子为:{best_c}") lr = LogisticRegression(C=best_c, solver='lbfgs', max_iter=1000) lr.fit(os_x_train, os_y_train)同样,评估要在原始测试集上进行:
from sklearn import metrics train_predicted = lr.predict(x_train) print(metrics.classification_report(y_train, train_predicted)) cm_plot(y_train, train_predicted).show() test_predicted = lr.predict(x_test) print(metrics.classification_report(y_test, test_predicted)) cm_plot(y_test, test_predicted).show()对比下采样和过采样的结果,SMOTE 通常在保留数据信息上更有优势,实际工业界用得也更多。
四、小案例:寝室分配数据
把同样的流程套用到寝室分配数据集上,整体流程和前面完全一致:读取数据 → 标准化 → 划分训练测试集 → SMOTE 过采样 → 交叉验证调参 → 评估。
唯一区别是数据格式不同(用np.loadtxt读取 txt 文件),核心逻辑和前面一模一样。
五、方法对比与总结
最后把三种方法的核心要点整理一下:
⚠️ 提一下:采样操作只能在训练集上做,测试集必须保持原始分布不动,否则评估结果没有参考价值。