解决样本类别分布不均衡的问题

  • 1 哪些运营场景中容易出现样本不均衡
  • 1.1 异常检测场景
  • 1.2 客户流失场景
  • 1.3 罕见事件的分析
  • 1.4 发生频率低的事件
  • 2 通过过抽样和欠抽样解决样本不均衡
  • 2.1 过抽样
  • 2.2 欠抽样
  • 3 通过正负样本的惩罚权重解决样本不均衡
  • 4 通过组合/集成方法解决样本不均衡
  • 5 通过特征选择解决样本不均衡
  • 6 代码实操:Python处理样本不均衡



所谓的不均衡指的是不同类别的样本量差异非常大。样本类别分布不均衡主要出现在分类相关的建模问题上。样本类别分布不均衡从数据规模上可以分为

大数据分布不均衡

小数据分布不均衡两种。

大数据分布不均衡;这种情况下整体数据规模大,只是其中的小样本类的占比较少。但是从每个特征的分布来看,小样本也覆盖了大部分或全部的特征。例如拥有1000万条记录的数据集中,其中占比50万条的少数分类样本便于属于这种情况。

小数据分布不均衡;这种情况下整体数据规模小,并且占据少量样本比例的分类数量也少,这会导致特征分布的严重不平衡。例如拥有1000条数据样本的数据集中,其中占有10条样本的分类,其特征无论如何拟合也无法实现完整特征值的覆盖,此时属于严重的数据样本分布不均衡。

样本分布不均衡将导致样本量少的分类所包含的特征过少,并很难从中提取规律;即使得到分类模型,也容易产生过度依赖于有限的数据样本而导致过拟合的问题,当模型应用到新的数据上时,模型的准确性和健壮性将很差。

样本分布不均衡主要在于不同类别间的样本比例差异。以笔者的工作经验看,如果不同分类间的样本量差异达到超过10倍就需要引起警觉并考虑处理该问题,超过20倍就一定要解决了。

1 哪些运营场景中容易出现样本不均衡

在数据化运营过程中,以下场景会经常产生样本分布不均衡的问题:

1.1 异常检测场景

大多数企业中的异常个案都是少量的,比如恶意刷单、黄牛订单、信用卡欺诈、电力窃电、设备故障等,这些数据样本所占的比例通常是整体样本中很少的一部分,以信用卡欺诈为例,刷实体信用卡的欺诈比例一般都在0.1%以内。

1.2 客户流失场景

大型企业的流失客户相对于整体客户而言通常是少量的,尤其对于具有垄断地位的行业巨擘,例如电信、石油、网络运营商等更是如此。

1.3 罕见事件的分析

罕见事件与异常检测类似,都属于发生个案较少,不同点在于异常检测通常都有预先定义好的规则和逻辑,并且大多数异常事件都会对企业运营造成负面影响,因此针对异常事件的检测和预防非常重要;罕见事件则无法预判,并且也没有明显的积极或消极影响倾向。例如由于某网络大V无意中转发了企业的一条趣味广告导致用户流量明显提升便属于此类。

1.4 发生频率低的事件

这种事件是预期或计划性事件,但是发生频率非常低。例如每年1次的双11盛会一般都会产生较高的销售额,但放到全年来看这一天的销售额占比很可能只有1%不到,尤其对于很少参与活动的公司而言,这种情况更加明显。这就属于典型的低频事件。

2 通过过抽样和欠抽样解决样本不均衡

抽样是解决样本分布不均衡相对简单且常用的方法,包括过抽样欠抽样两种。

2.1 过抽样

又称上采样或(over-sampling),其通过增加分类中少数类样本的数量来实现样本均衡,最直接的方法是简单复制少数类样本形成多条记录。这种方法的缺点是,如果样本特征少则可能导致过拟合的问题。经过改进的过抽样方法通过在少数类中加入随机噪声、干扰数据或通过一定规则产生新的合成样本,例如SMOTE算法。

2.2 欠抽样

又称下采样(under-sampling),其通过减少分类中多数类样本的数量来实现样本均衡,最直接的方法是随机去掉一些多数类样本来减小多数类的规模,缺点是会丢失多数类样本中的一些重要信息。

总体上,过抽样和欠抽样更适合大数据分布不均衡的情况,尤其是过抽样方法,应用极为广泛。

3 通过正负样本的惩罚权重解决样本不均衡

在算法实现过程中,对于分类中不同样本数量的类别分别赋予不同的权重(一般思路分类中的小样本量类别权重高,大样本量类别权重低),然后进行计算和建模。

使用这种方法时需要对样本本身做额外处理,只需在算法模型的参数中进行相应设置即可。很多模型和算法中都有基于类别参数的调整设置,以scikit-learn中的SVM为例,通过在class_weight:{dict,‘balanced’}中针对不同类别来手动指定权重。如果使用其默认的方法balanced,那么SVM会将权重设置为与不同类别样本数量呈反比的权重来进行自动均衡处理,计算公式为:
n_samples / (n_classes * np.bincount(y))

4 通过组合/集成方法解决样本不均衡

组合/集成方法指的是在每次生成训练集时使用所有分类中的小样本量,同时从分类中的大样本量中随机抽取数据来与小样本量合并构成训练集,这样反复多次会得到很多训练集和训练模型。最后在应用时,使用组合方法(例如投票、加权投票等)产生分类预测结果。

例如,数据集中的正、负例的样本分别为100和10000条,比例为1:100。此时可以将负例样本(类别中的大量样本集)随机分为100份(当然也可以分更多),每份100条数据;然后每次形成训练集时使用所有的正样本(100条)和随机抽取的负样本(100条)形成新的数据集。如此反复可以得到100个训练集和对应的训练模型。

这种解决问题的思路类似于随机森林。在随机森林中,虽然每个小决策树的分类能力很弱,但是通过大量的“小树”组合形成的“森林”具有良好的模型预测能力。

如果计算资源充足,并且对于模型的时效性要求不高,这种方法比较合适。

5 通过特征选择解决样本不均衡

上述几种方法都是基于数据行的操作,通过多种途径可使不同类别的样本数据行记录均衡。除此以外,还可以考虑使用或辅助基于列的特征选择方法。

一般情况下,样本不均衡也会导致特征分布不均衡,但如果小类别样本量具有一定的规模,那么意味着其特征值的分布较为均衡,可通过选择具有显著型的特征配合参与解决样本不均衡问题,也能在一定程度上提高模型效果。

提示:上述几种方法的思路都是基于分类问题的。实际上,这种从大规模数据中寻找罕见数据的情况,也可以使用非监督式的学习方法,例如使用One-class SVM进行异常检测。分类是监督式方法,前期是基于带有标签(Label)的数据进行分类预测的;而采用非监督式方法,则是使用除了标签以外的其他特征进行模型拟合的,这样也能得到异常
数据记录。所以,要解决异常检测类的问题,先是考虑整体思路,再考虑方法模型。

6 代码实操:Python处理样本不均衡

专门用于不平衡数据处理的Python包imbalan-ced-learn,读者需要先在系统终端的命令行使用pip

install imbalanced-learn进行安装
本示例使用的数据源文件data2.txt位于“附件-chapter3”中(如需要数据文件测试,请去小白学(Python数据分析与数据运营)的日常杂记3)那篇文章中下载即可。

import pandas as pd
from imblearn.over_sampling import SMOTE # 过抽样处理库SMOTE
from imblearn.under_sampling import RandomUnderSampler  # 欠抽样处理库Random-UnderSampler
from sklearn.svm import SVC # SVM中的分类算法SVC
from imblearn.ensemble import EasyEnsemble  # 简单集成方法EasyEnsemble

# 导入数据文件
df = pd.read_table('D:/jupter_workspace/chapter3/data2.txt', sep='	', names=['col1', 'col2', 'col3', 'col4', 'col5', 'label']) # 读取数据文件

x = df.iloc[:, :-1] #切片,得到输入x
y = df.iloc[:, -1]  #切片,得到标签y
groupby_data_orginal = df.groupby('label').count() # 对label做分类汇总
print(groupby_data_orginal) # 打印输出原始数据集样本
---------------------------------->
输出结果:
       col1  col2  col3  col4  col5
label                              
0       475   475   475   475   475
1       525   525   525   525   525
# 使用SMOTE方法进行过抽样处理
model_smote = SMOTE() # 建立SMOTE模型对象
x_smote_resampled, y_smote_resampled = model_smote.fit_sample(x, y) # 输入数据并作过抽样处理
x_smote_resampled = pd.DataFrame(x_smote_resampled, columns=['col1', 'col2', 'col3', 'col4', 'col5'])  #将数据转换为数据框并命名列名
y_smote_resampled = pd.DataFrame(y_smote_resampled, columns=['label']) # 将数据转换为数据框并命名列名
smote_resampled = pd.concat([x_smote_resampled, y_smote_resampled], axis=1) #列合并数据框
groupby_data_smote = smote_resampled.groupby('label').count()  # 对label做分类汇总
print(groupby_data_smote) # 打印输出经过SMOTE处理后的数据集样本
------------------------->
结果
label                              
0       525   525   525   525   525
1       525   525   525   525   525
# 使用RandomUnderSampler方法进行欠抽样处理
model_RandomUnderSampler = RandomUnderSampler() # 建立RandomUnderSampler模型对象
x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled = model_RandomUnderSampler.fit_sample(x, y) ## 输入数据并作欠抽样处理
x_RandomUnderSampler_resampled = pd.DataFrame(x_RandomUnderSampler_resampled, columns=['col1','col2','col3','col4','col5']) # 将数据转换为数据框并命名列名
y_RandomUnderSampler_resampled = pd.DataFrame(y_RandomUnderSampler_resampled, columns=['label']) # 将数据转换为数据框并命名列名
RandomUnderSampler_resampled = pd.concat([x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled], axis=1) #列合并数据框
groupby_data_RandomUnderSampler = RandomUnderSampler_resampled.groupby('label').count() #对label做分类汇总
print(groupby_data_RandomUnderSampler) # 打印输出经过RandomUnderSampler处理后的数据集样本分类分布
----------------------------->
结果:
label                              
0       475   475   475   475   475
1       475   475   475   475   475
# 使用SVM的权重调节处理不均衡样本
model_svm = SVC(class_weight='balanced') # 创建SVC模型对象并指定类别权重
model_svm.fit(x, y) # 输入x和y并训练模型
x_columns = [[-2,2,3,-3,1]]  #小弟我自己新生成的一个测试数据,这个数据是从data2.txt中拿出来的,拿了很多数据做预测,得到的label都是和data2.txt上的对应label吻合
y_label = model_svm.predict(x_columns) #将x_columns带入predict预测 label是0,还是1
print(y_label)
# 使用集成方法EasyEnsemble处理不均衡样本
model_EasyEnsemble = EasyEnsemble() # 建立EasyEnsemble模型对象
x_EasyEnsemble_resampled, y_EasyEnsemble_resampled = model_EasyEnsemble.fit_sample(x, y) #输入数据并应用集成方法处理
print(x_EasyEnsemble_resampled.shape) # 打印输出集成方法处理后的x样本集概况
print(y_EasyEnsemble_resampled.shape) # 打印输出集成方法处理后的y标签集概况

# 抽取其中一份数据做审查
index_num = 1 # 设置抽样样本集索引
x_EasyEnsemble_resampled_t = pd.DataFrame(x_EasyEnsemble_resampled[index_num],columns=['col1','col2','col3','col4','col5']) # 将数据转换为数据框并命名列名
y_EasyEnsemble_resampled_t = pd.DataFrame(y_EasyEnsemble_resampled[index_num], columns=['label']) # 将数据转换为数据框并命名列名
EasyEnsemble_resampled = pd.concat([x_EasyEnsemble_resampled_t,y_EasyEnsemble_resampled_t], axis=1) #列合并数据框
groupby_data_EasyEnsemble = EasyEnsemble_resampled.groupby('label').count() # 对label做分类汇总
print(groupby_data_EasyEnsemble) # 打印输出经过EasyEnsemble处理后的数据集样本
--------------------------------------->
结果:
print(x_EasyEnsemble_resampled.shape) # 打印输出集成方法处理后的x样本集概况->
print(y_EasyEnsemble_resampled.shape) # 打印输出集成方法处理后的y标签集概况->
输出结果
------------------------------------------
(10, 950, 5)
(10, 950)
-------------------------------------------
print(groupby_data_EasyEnsemble) # 打印输出经过EasyEnsemble处理后的数据集样本->
输出结果
       col1  col2  col3  col4  col5
label                              
0       475   475   475   475   475
1       475   475   475   475   475
完整代码:
import pandas as pd
from imblearn.over_sampling import SMOTE # 过抽样处理库SMOTE
from imblearn.under_sampling import RandomUnderSampler  # 欠抽样处理库Random-UnderSampler
from sklearn.svm import SVC # SVM中的分类算法SVC
from imblearn.ensemble import EasyEnsemble  # 简单集成方法EasyEnsemble

# 导入数据文件
df = pd.read_table('D:/jupter_workspace/chapter3/data2.txt', sep='	', names=['col1', 'col2', 'col3', 'col4', 'col5', 'label']) # 读取数据文件

x = df.iloc[:, :-1] #切片,得到输入x
y = df.iloc[:, -1]  #切片,得到标签y
groupby_data_orginal = df.groupby('label').count() # 对label做分类汇总
print(groupby_data_orginal) # 打印输出原始数据集样本

# 使用SMOTE方法进行过抽样处理
model_smote = SMOTE() # 建立SMOTE模型对象
x_smote_resampled, y_smote_resampled = model_smote.fit_sample(x, y) # 输入数据并作过抽样处理
x_smote_resampled = pd.DataFrame(x_smote_resampled, columns=['col1', 'col2', 'col3', 'col4', 'col5'])  #将数据转换为数据框并命名列名
y_smote_resampled = pd.DataFrame(y_smote_resampled, columns=['label']) # 将数据转换为数据框并命名列名
smote_resampled = pd.concat([x_smote_resampled, y_smote_resampled], axis=1) #列合并数据框
groupby_data_smote = smote_resampled.groupby('label').count()  # 对label做分类汇总
print(groupby_data_smote) # 打印输出经过SMOTE处理后的数据集样本

# 使用RandomUnderSampler方法进行欠抽样处理
model_RandomUnderSampler = RandomUnderSampler() # 建立RandomUnderSampler模型对象
x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled = model_RandomUnderSampler.fit_sample(x, y) ## 输入数据并作欠抽样处理
x_RandomUnderSampler_resampled = pd.DataFrame(x_RandomUnderSampler_resampled, columns=['col1','col2','col3','col4','col5']) # 将数据转换为数据框并命名列名
y_RandomUnderSampler_resampled = pd.DataFrame(y_RandomUnderSampler_resampled, columns=['label']) # 将数据转换为数据框并命名列名
RandomUnderSampler_resampled = pd.concat([x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled], axis=1) #列合并数据框
groupby_data_RandomUnderSampler = RandomUnderSampler_resampled.groupby('label').count() #对label做分类汇总
print(groupby_data_RandomUnderSampler) # 打印输出经过RandomUnderSampler处理后的数据集样本分类分布

# 使用SVM的权重调节处理不均衡样本
model_svm = SVC(class_weight='balanced') # 创建SVC模型对象并指定类别权重
model_svm.fit(x, y) # 输入x和y并训练模型
x_columns = [[-2,2,3,-3,1]]
#y_label = model_svm.predict(x_columns)
#print(y_label)

# 使用集成方法EasyEnsemble处理不均衡样本
model_EasyEnsemble = EasyEnsemble() # 建立EasyEnsemble模型对象
x_EasyEnsemble_resampled, y_EasyEnsemble_resampled = model_EasyEnsemble.fit_sample(x, y) #输入数据并应用集成方法处理
print(x_EasyEnsemble_resampled.shape) # 打印输出集成方法处理后的x样本集概况
print(y_EasyEnsemble_resampled.shape) # 打印输出集成方法处理后的y标签集概况

# 抽取其中一份数据做审查
index_num = 1 # 设置抽样样本集索引
x_EasyEnsemble_resampled_t = pd.DataFrame(x_EasyEnsemble_resampled[index_num],columns=['col1','col2','col3','col4','col5']) # 将数据转换为数据框并命名列名
y_EasyEnsemble_resampled_t = pd.DataFrame(y_EasyEnsemble_resampled[index_num], columns=['label']) # 将数据转换为数据框并命名列名
EasyEnsemble_resampled = pd.concat([x_EasyEnsemble_resampled_t,y_EasyEnsemble_resampled_t], axis=1) #列合并数据框
groupby_data_EasyEnsemble = EasyEnsemble_resampled.groupby('label').count() # 对label做分类汇总
print(groupby_data_EasyEnsemble) # 打印输出经过EasyEnsemble处理后的数据集样本

提示: 第三方库imblearn提供了非常多的样本不均衡处理方法