还对样本不平衡一筹莫展?来看看这个案例吧!
样本不平衡
数据集中各个类别的样本数量极不均衡,从数据规模上可分为:
大数据分布不均衡。整体数据规模大,小样本类的占比较少,但小样本也覆盖了大部分或全部特征。 小数据分布不均衡。整体数据规模小,少数样本比例的分类数量也少,导致特征分布严重不均衡。
样本不平衡处理方法
机器学习中样本不平衡,怎么办?中详细介绍了何谓样本不平衡,样本不平衡处理策略与常用方法。还包含分类模型评价指标。感兴趣或者需要的小伙伴们可以跳转查看。
分析数据集
导包
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)
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'
的变量,即为分类型变量。
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_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"
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 --