还对样本不平衡一筹莫展?来看看这个案例吧!

2021-06-24 11:22:26 浏览数 (1)

样本不平衡

数据集中各个类别的样本数量极不均衡,从数据规模上可分为:

  • 大数据分布不均衡。整体数据规模大,小样本类的占比较少,但小样本也覆盖了大部分或全部特征。
  • 小数据分布不均衡。整体数据规模小,少数样本比例的分类数量也少,导致特征分布严重不均衡。

样本不平衡处理方法

机器学习中样本不平衡,怎么办?中详细介绍了何谓样本不平衡,样本不平衡处理策略与常用方法。还包含分类模型评价指标。感兴趣或者需要的小伙伴们可以跳转查看。

分析数据集

导包

代码语言:javascript复制
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

from imblearn.under_sampling import RandomUnderSampler
from imblearn.over_sampling import RandomOverSampler
from imblearn.under_sampling import TomekLinks
from imblearn.over_sampling import SMOTE
from imblearn.combine import SMOTETomek

from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

查看下数据的基本情况

代码语言:javascript复制
train = pd.read_csv('../input/hr-analytics-job-change-of-data-scientists/aug_train.csv')
train.head(5)

前面已经对数据集(人力资源分析--数据科学家更换工作情况数据集)进行了完整的前期探索性数据分析,详情见文末链接。因此本文重点介绍处理该数据集中样本不平衡问题。

初步查看下数据状况

代码语言:javascript复制
train.info()
代码语言:javascript复制
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 19158 entries, 0 to 19157
Data columns (total 14 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   enrollee_id             19158 non-null  int64  
 1   city                    19158 non-null  object 
 2   city_development_index  19158 non-null  float64
 3   gender                  14650 non-null  object 
 4   relevent_experience     19158 non-null  object 
 5   enrolled_university     18772 non-null  object 
 6   education_level         18698 non-null  object 
 7   major_discipline        16345 non-null  object 
 8   experience              19093 non-null  object 
 9   company_size            13220 non-null  object 
 10  company_type            13018 non-null  object 
 11  last_new_job            18735 non-null  object 
 12  training_hours          19158 non-null  int64  
 13  target                  19158 non-null  float64
dtypes: float64(2), int64(2), object(10)
memory usage: 2.0  MB

缺失值分析

代码语言:javascript复制
Tr_total = train.isnull().sum().sort_values(ascending = False)
Tr_percent = (train.isnull().sum()/train.isnull().count()).sort_values(ascending = False)

Tr_missing_data = pd.concat([Tr_total,Tr_percent], axis = 1, keys = ['Total', 'Percent'])
Tr_missing_data.head(9)

Total

Percent

company_type

6140

0.320493

company_size

5938

0.309949

gender

4508

0.235306

major_discipline

2813

0.146832

education_level

460

0.024011

last_new_job

423

0.022080

enrolled_university

386

0.020148

experience

65

0.003393

target

0

0.000000

本数据集共有14个变量,其中有8个变量具有缺失值,两个变量(company_type与company_size)高达30%以上。为更好地运用数据集进行后续分析处理,需要对缺失值进行分析处理。

因本数据集中包含分类型变量与连续型变量,其处理策略有所不同,因此需将其分开处理。

分类型变量处理

缺失值处理

筛选出detype='object'的变量,即为分类型变量。

代码语言:javascript复制
cat_columns = train.columns[train.dtypes=='object']
cat_columns
代码语言:javascript复制
Index(['city', 'gender', 'relevent_experience',     
       'enrolled_university','education_level',     
       'major_discipline', 'experience',     
       'company_size', 'company_type',   
       'last_new_job'],  
      dtype='object')

并对筛选出的分类型变量选择一个简单的处理方式--众数填充。当然,如果有其他处理需要,可根据业务场景进行特别处理。

除了上面使用布尔索引的方法筛选出分类型变量,还可以运用select_dtypes方法,通过数据类型筛选特征值。

代码语言:javascript复制
categoricals = train.select_dtypes(exclude = [np.number])
categoricals.columns
代码语言:javascript复制
Index(['city', 'city_development_index', 'gender', 'relevent_experience',
       'enrolled_university', 'education_level', 'major_discipline',
       'experience', 'company_size', 'company_type', 'last_new_job',
       'training_hours'],
      dtype='object')
代码语言:javascript复制
train_impute_mode = categoricals.copy()
代码语言:javascript复制
for columna in cat_columns:
    train_impute_mode[columna].fillna(train_impute_mode[columna].mode()[0],inplace=True)
变量可视化

经缺失值处理后,对每个变量值进行可视化分析,可以更加方便地看出每个特征变量分布状况。此处使用sns.barplot绘制柱状图。

代码语言:javascript复制
def number_categories(categoricals): 
    c_count = 0
    height = 6
    width = 2
    fig, axes = plt.subplots(height, width, sharex=True, figsize=(20,23))
    plt.suptitle('Number of categorical variables', size=16, y=(0.94))
    
    for row in range(height):
        for col in range(width):
            c_idx = categoricals.columns[c_count]
            c_data = categoricals[c_idx].value_counts()
            sns.barplot(x = c_data.values, y = c_data.index, palette='deep', ax=axes[row, col])
            axes[row,col].set_title(c_idx)
            c_count= c_count   1

number_categories(categoricals)

连续型变量处理

处理完分类型变量后,需要处理连续型变量,此处注意需要先将目标变量target剔除。因本次连续型变量无缺失值,因此无需对其进行处理。

代码语言:javascript复制
numeric_variables = train.select_dtypes(include= [np.number])
numeric_variables.columns
numeric_variables = numeric_variables.drop(columns = 'target' , axis = 1)
代码语言:javascript复制
numeric_variables.head()

enrollee_id

city_development_index

training_hours

0

8949

0.920

36

1

29725

0.776

47

2

11561

0.624

83

3

33241

0.789

52

4

666

0.767

8

变量可视化

同样,可以对连续型变量可视化分析,看看数据分布特征,一览其全貌。本次使用sns.displot绘图,distplot 可以让频次直方图与 KDE 结合起来。

"City_development"

代码语言:javascript复制
plt.figure(figsize=(10,6))
sns.distplot(numeric_variables["city_development_index"])
plt.title("City_development",fontsize=15)
plt.ylabel("Density",fontsize=15)
plt.xlabel("city_development_index",fontsize=15)

"Training Hours"

代码语言:javascript复制
plt.figure(figsize=(10,6))
plt.subplots(sharex = True , figsize= (10,5))
plt.suptitle('Training Hours', size=16, y=(0.94))
sns.distplot(numeric_variables['training_hours'], hist= True)
plt.show()
连续型变量离散化

离散化是把无限空间中有限个体映射到有限空间中。

离散化的必要性:

  • 节约资源、提高效率。
  • 算法模型的需要(尤其是分类模型)。
  • 增强模型的准确性和稳定性,尤其是减轻异常值的特征,对基于距离计算的模型效果明显。
  • 特定数据处理和分析的必要步骤,尤其是图像处理方面应用广泛,图像的二值化处理。
  • 模型结果应用和部署的需要,值域分布过多、琐碎或划分不符合业务逻辑,需要重新划分。
代码语言:javascript复制
train['training_hours'] = pd.cut(train['training_hours'] ,bins = 3 , labels = ['long' , 'medium_long', 'short'])

train['city_development_index'] = pd.cut(train['city_development_index'] ,bins = 3 , labels = ['nothing' , 'moreorless', 'a_lot'])

目标变量

本次数据集是0-1分类型,属于分类模型目标变量。对其计数并可视化分析,看看其分布特征。

代码语言:javascript复制
sns.countplot(train['target'])

从此图中可以看出,此数据集存在较为显著的样本不平衡现象,这将会影响分类模型预测的准确性。

在此通过过采样的方式来平衡样本量,以提供模型可靠性。

删除无关变量

这里可以明显看出,目标变量与城市id无关。

代码语言:javascript复制
train = train.drop(columns = ['city' , 'enrollee_id'] , axis = 1)

train['major_discipline'].replace(to_replace = 'Other', value = 'Other_1', inplace = True )
train['company_type'].replace(to_replace = 'Other', value = 'Other_2', inplace = True )

编码

数据集包含分类型变量,Python建模库如sklearn无法对它们建模。因此,很有必要对特征变量进行逐一对编码。编码方式有很多种,本次选用pandas.get_dummies哑变量编码方式。

代码语言:javascript复制
gd_city_development_index = pd.get_dummies(
        train[['city_development_index']], 
        drop_first=True, 
        prefix=[None])
gd_gender = pd.get_dummies(
        train[['gender']] , 
        drop_first=True, 
        prefix=[None])
gd_relevent_experience = pd.get_dummies(
        train[['relevent_experience']], 
        drop_first=True, 
        prefix=[None])
gd_enrolled_university = pd.get_dummies(
        train[['enrolled_university']], 
        drop_first=True, 
        prefix=[None])
gd_education_level = pd.get_dummies(
        train[['education_level']], 
        drop_first=True ,
        prefix=[None])
gd_major_discipline = pd.get_dummies(
        train[['major_discipline']], 
        drop_first=True, 
        prefix=[None])
gd_experience = pd.get_dummies(
        train[['experience']],  
        drop_first=True, 
        prefix=[None])
gd_company_size = pd.get_dummies(
        train[['company_size']], 
        drop_first=True, 
        prefix=[None])
gd_company_type = pd.get_dummies(
        train[['company_type']], 
        drop_first=True, 
        prefix=[None])
gd_last_new_job = pd.get_dummies(
        train[['last_new_job']], 
        drop_first=True, 
        prefix=[None])
gd_training_hours = pd.get_dummies(
        train[['training_hours']], 
        drop_first=True, 
        prefix=[None])

pandas.get_dummies( data, prefix=None, prefix_sep='_', dummy_na=False, columns=None, sparse=False, drop_first=False, dtype=None) data: array-like, Series, or DataFrame 要获编码的数据 prefix: str, list of str, or dict of str, default None 前缀, 用于追加DataFrame列名称的字符串。在DataFrame上调用get_dummies时,传递长度等于列数的列表。或者,前缀可以是将列名称映射到前缀的字典。 drop_first: bool, default False 是否通过删除第一个级别以从k个分类级别中获取k-1个哑变量。

删除原始变量,并合并哑变量,得到最终训练数据集。

代码语言:javascript复制
to_drop = ['city_development_index',   
           'gender', 'relevent_experience',
           'enrolled_university', 'education_level', 
           'major_discipline',
           'experience', 'company_size',   
           'company_type', 'last_new_job',
           'training_hours']

train = train.drop(columns = to_drop , axis = 1)

data = pd.concat([train, 
                  gd_city_development_index,
                  gd_gender, 
                  gd_relevent_experience,
                  gd_enrolled_university, 
                  gd_education_level, 
                  gd_major_discipline, 
                  gd_experience, 
                  gd_company_size, 
                  gd_company_type, 
                  gd_last_new_job, 
                  gd_training_hours], 
                 axis = 1)
                 
data.head(7)

建模

代码语言:javascript复制
X = data.drop(columns = 'target' , axis = 1)                           
Y = data['target']

定义采样策略

代码语言:javascript复制
RUS = RandomUnderSampler()
ROS = RandomOverSampler()
TL = TomekLinks()
SMT = SMOTE(ratio ='minority')
SMTL = SMOTETomek()

训练模型

代码语言:javascript复制
X_rus, Y_rus = RUS.fit_sample(X,Y)
X_ros, Y_ros = ROS.fit_sample(X,Y)
X_tl, Y_tl = TL.fit_sample(X,Y)
X_smt, Y_smt = SMT.fit_sample(X, Y)
X_smtl, Y_smtl = SMTL.fit_sample(X, Y)

划分训练集和测试集

代码语言:javascript复制
X_train_rus, X_test_rus, Y_train_rus, Y_test_rus = train_test_split(
    X_rus, Y_rus, test_size = 0.2 ,random_state = 2020)
    
X_train_ros, X_test_ros, Y_train_ros, Y_test_ros = train_test_split(
    X_ros, Y_ros, test_size = 0.2 ,random_state = 2020)
    
X_train_tl, X_test_tl, Y_train_tl, Y_test_tl = train_test_split(
    X_tl, Y_tl,test_size = 0.2 ,random_state = 2020)
    
X_train_smt, X_test_smt, Y_train_smt, Y_test_smt = train_test_split(
    X_smt, Y_smt, test_size = 0.2 ,random_state = 2020)
    
X_train_smtl, X_test_smtl, Y_train_smtl, Y_test_smtl = train_test_split(
    X_smtl, Y_smtl, test_size = 0.2 ,random_state = 2020)

选择决策树分类模型

决策树是一种树状结构,它的每一个叶子结点对应着一个分类,非叶子结点对应着在某个属性上的划分,根据样本在该属性上的不同取值将其划分成若干个子集。

有关决策树相关内容,可参见决策树模型-理论篇决策树模型实例篇

代码语言:javascript复制
DT = DecisionTreeClassifier(criterion = 'entropy')
代码语言:javascript复制
methods = ['Non', 'RUS' , 'ROS' , 'TomekLinks' , 'SMOTE' , 'SMOTE   Tomek']

scores = []
DT.fit(X,Y)

score_0 = DT.score(X_train_rus, Y_train_rus)
scores.append(score_0)


# Fit with RUS
DT.fit(X_train_rus,Y_train_rus)

score_1 = DT.score(X_train_rus, Y_train_rus)
scores.append(score_1)


# Fit with ROS
DT.fit(X_train_ros,Y_train_ros)

score_2 = DT.score(X_train_ros, Y_train_ros)
scores.append(score_2)


# Fit with TomekLinks
DT.fit(X_train_tl,Y_train_tl)

score_3 = DT.score(X_train_tl, Y_train_tl)
scores.append(score_3)


# Fit with SMOTE
DT.fit(X_train_smt,Y_train_smt)

score_4 = DT.score(X_train_smt, Y_train_smt)
scores.append(score_4)


# Fit with SMOTE   Tomek
DT.fit(X_train_smtl,Y_train_smtl)

score_5 = DT.score(X_train_smtl, Y_train_smtl)
scores.append(score_5)
代码语言:javascript复制
#concate the results
results = pd.DataFrame(methods, columns=['ReSampling'])
results['accuracy'] = scores

#print the results
print('nModelling results:')
print(results.sort_values(by = 'accuracy' , ascending = False))
代码语言:javascript复制
Modelling results:
      ReSampling  accuracy
5  SMOTE   Tomek  0.946801
4          SMOTE  0.946282
1            RUS  0.935104
2            ROS  0.930332
3     TomekLinks  0.929991
0            Non  0.870862

可视化结果

代码语言:javascript复制

sns.set_style("dark")
results.plot.bar(x = 'ReSampling' , y= 'accuracy' , rot=0 , legend = False)

由结果可知,没有样本平衡的数据的得到的模型得分最低,其他通过各种样本平衡策略后的数据模型得分均有提升。

0 人点赞