查看原文
其他

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

云朵君 数据STUDIO 2022-04-28

样本不平衡

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

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

样本不平衡处理方法

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

分析数据集

导包

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

查看下数据的基本情况

train = pd.read_csv('../input/hr-analytics-job-change-of-data-scientists/aug_train.csv')
train.head(5)

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

初步查看下数据状况

train.info()
<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

缺失值分析

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)

TotalPercent
company_type61400.320493
company_size59380.309949
gender45080.235306
major_discipline28130.146832
education_level4600.024011
last_new_job4230.022080
enrolled_university3860.020148
experience650.003393
target00.000000

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

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

分类型变量处理

缺失值处理

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

cat_columns = train.columns[train.dtypes=='object']
cat_columns
Index(['city', 'gender', 'relevent_experience',
'enrolled_university','education_level',
'major_discipline', 'experience',
'company_size', 'company_type',
'last_new_job'],
dtype='object')

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

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

categoricals = train.select_dtypes(exclude = [np.number])
categoricals.columns
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')
train_impute_mode = categoricals.copy()
for columna in cat_columns:
    train_impute_mode[columna].fillna(train_impute_mode[columna].mode()[0],inplace=True)

变量可视化

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

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剔除。因本次连续型变量无缺失值,因此无需对其进行处理。

numeric_variables = train.select_dtypes(include= [np.number])
numeric_variables.columns
numeric_variables = numeric_variables.drop(columns = 'target' , axis = 1)
numeric_variables.head()

enrollee_idcity_development_indextraining_hours
089490.92036
1297250.77647
2115610.62483
3332410.78952
46660.7678

变量可视化

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

"City_development"

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"

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()

连续型变量离散化

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

离散化的必要性:

  • 节约资源、提高效率。

  • 算法模型的需要(尤其是分类模型)。

  • 增强模型的准确性和稳定性,尤其是减轻异常值的特征,对基于距离计算的模型效果明显。

  • 特定数据处理和分析的必要步骤,尤其是图像处理方面应用广泛,图像的二值化处理。

  • 模型结果应用和部署的需要,值域分布过多、琐碎或划分不符合业务逻辑,需要重新划分。

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分类型,属于分类模型目标变量。对其计数并可视化分析,看看其分布特征。

sns.countplot(train['target'])

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

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

删除无关变量

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

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哑变量编码方式。

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个哑变量。

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

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)

建模

X = data.drop(columns = 'target' , axis = 1)                           
Y = data['target']

定义采样策略

RUS = RandomUnderSampler()
ROS = RandomOverSampler()
TL = TomekLinks()
SMT = SMOTE(ratio ='minority')
SMTL = SMOTETomek()

训练模型

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)

划分训练集和测试集

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)

选择决策树分类模型

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

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

DT = DecisionTreeClassifier(criterion = 'entropy')
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)
#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))
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

可视化结果


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

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

推荐阅读

-- 数据STUDIO --

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存