💬在上一部分中,我们已经完成了对数据集背景解读、数据预处理与探索性分析。在数据背景解读中,我们介绍了数据集来源、电信用户流失分析的基本业务背景,并详细解释了每个字段的基本含义;在数据预处理过程中,我们对数据集进行了缺失值和异常值分析,并且根据实际业务情况对缺失值进行了0值填补;而在探索性分析的过程中,我们对比分析了标签不同取值时特征取值的分布情况,并从中初步分析影响用户流失的关键因素。
本节开始,我们将围绕此前已经处理好的数据来进一步来进行用户流失预测。当然,要进行尽可能精准的用户流失预测,就离不开特征工程、模型选择与训练、参数调优和模型融合这些环节。考虑到该数据集的建模目标有两个,其一是希望能够进行尽可能精准的预测,同时由于该案例也包含数据分析背景,要求模型结果也能够为业务人员在业务开展过程中提供具体指导意见,因此无论是在模型选型过程还是特征工程环节,我们都将同时纳入这两个因素进行综合考虑。
本节我们将优先考虑具备模型可解释性的逻辑回归和决策树,这两个算法也是大多数在要求对结果进行解释的场景下优先考虑的模型,此外在实际建模过程中需要注意的是,不同模型需要带入的数据编码类型也各不相同,因此在本节中,我们也将详细介绍各类数据编码方法及其使用过程中的注意事项。
💤根据上一小节的分析结果,再次执行数据预处理过程:
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 读取数据
tcc = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
# 标注连续/离散字段
# 离散字段
category_cols = ['gender', 'SeniorCitizen', 'Partner', 'Dependents',
'PhoneService', 'MultipleLines', 'InternetService', 'OnlineSecurity', 'OnlineBackup',
'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies', 'Contract', 'PaperlessBilling',
'PaymentMethod']
# 连续字段
numeric_cols = ['tenure', 'MonthlyCharges', 'TotalCharges']
# 标签
target = 'Churn'
# ID列
ID_col = 'customerID'
# 验证是否划分能完全
assert len(category_cols) + len(numeric_cols) + 2 == tcc.shape[1]
注意,此处我们暂时将tenure划为连续性字段,以防止后续One-Hot编码时候诞生过多特征。然后进行连续变量的缺失值填补:
tcc['TotalCharges']= tcc['TotalCharges'].apply(lambda x: x if x!= ' ' else np.nan).astype(float)
tcc['MonthlyCharges'] = tcc['MonthlyCharges'].astype(float)
tcc['TotalCharges'] = tcc['TotalCharges'].fillna(0)
tcc['Churn'].replace(to_replace='Yes', value=1, inplace=True)
tcc['Churn'].replace(to_replace='No', value=0, inplace=True)
当然,清洗完后的数据需要进行进一步重编码后才能带入进行建模,在考虑快速验证不同模型的建模效果时,需要考虑到不同模型对数据编码要求是不同的,因此我们需要先介绍关于机器学习中特征编码的相关内容。
离散字段的数据重编码
在上一小节中,我们对离散特征进行哑变量的变换过程其实就是一个数据重编码的过程。当然,需要注意的是,不同类型的字段由不同的编码方式,例如文本类型字段可能需要用到CountVector或TF-IDF处理、时序字段可能需要分段字典排序等,并且,不同模型对于数据编码类型要求也不一样,例如逻辑回归需要对多分类离散变量进行哑变量变换,而CatBoost则明确要求不能对离散字段字段进行哑变量变换、否则会影响模型速度和效果。因此,本部分我们先介绍较为通用的离散字段的编码方法,然后再根据后续实际模型要求,选择不同编码方式对数据进行处理。
OrdinalEncoder自然数排序
首先是自然数排序方法,该方法的过程较为简单,即先对离散字段的不同取值进行排序,然后对其进行自然数值取值转化;对于自然数排序过程,我们可以通过简单的pandas中的列取值调整来进行,例如就像此前对标签字段取值的调整过程,此外也可以直接考虑调用sklearn中的OrdinalEncoder()评估器(转化器)。
from sklearn import preprocessing
preprocessing.OrdinalEncoder()
X1 = np.array([['F'], ['M'], ['M'], ['F']])
enc = preprocessing.OrdinalEncoder()
enc.fit(X1)
enc.transform(X1) # 进行转换 类似模型的predict方法
array([[0.],
[1.],
[1.],
[0.]])
这种转化器的使用方式,也为非常便于我们执行在训练集上进行训练、在测试集上进行测试这一过程。
💥独热编码与哑变量区别:
- 哑变量:生成的特征数量为n-1(n为原始分类变量的类别数),因为需要选择一个基准类别。
- 独热编码:生成的特征数量为n,即每个类别都对应一个独立的特征。
OneHotEncoder独热编码
独热编码过程其实和我们此前介绍的哑变量创建过程一致(至少在sklearn中并无差别)。对于独热编码的过程,我们可以通过pd.get_dummies函数实现,也可以通过sklearn中OneHotEncoder评估器(转化器)来实现。
preprocessing.OneHotEncoder()
X1
array([['F'],
['M'],
['M'],
['F']], dtype='<U1')
enc = preprocessing.OneHotEncoder()
enc.fit_transform(X1).toarray()
💢同样,训练完成后的转化器会记录转化规则:
enc.categories_
[array(['F', 'M'], dtype='<U1')]
# 该排序实际上也是字典顺序
'M' > 'F'
💢并能够对新的数据依据原转化规则进行转化:
X2
# array([['M'],
['F']], dtype='<U1')
enc.transform(X2).toarray()
#
array([[0., 1.],
[1., 0.]])
对于独热编码的使用,有一点是额外需要注意的,那就是对于二分类离散变量来说,独热编码往往是没有实际作用的。例如对于上述极简数据集而言,Gender的取值是能是M或者F,独热编码转化后,某行Gender_F取值为1、则Gender_M取值必然为0,反之亦然。因此很多时候我们在进行独热编码转化的时候会考虑只对多分类离散变量进行转化,而保留二分类离散变量的原始取值。此时就需要将OneHotEncoder中drop参数调整为'if_binary',以表示跳过二分类离散变量列。
X3 = pd.DataFrame({'Gender': ['F', 'M', 'M', 'F'], 'Income': ['High', 'Medium', 'High', 'Low']})
drop_enc = preprocessing.OneHotEncoder(drop='if_binary') # 跳过二分类离散变量
drop_enc.fit_transform(X3).toarray()
drop_enc.categories_
[array(['F', 'M'], dtype=object),
array(['High', 'Low', 'Medium'], dtype=object)]
不过需要注意的是,对于sklearn的独热编码转化器来说,尽管其使用过程会更加方便,但却无法自动创建转化后的列名称,而在需要考察字段业务背景含义的场景中,必然需要知道每一列的实际名称(就类似于极简示例中每一列的名字,通过“原列名_字段取值”来进行命名),因此我们需要定义一个函数来批量创建独热编码后新数据集各字段名称的函数。
💢首先我们先尝试围绕上述极简数据集来提取(创建)独热编码后新数据集的字段名称:
# 提取原始列名称
cate_cols = X3.columns.tolist()
cate_cols
# ['Gender', 'Income']
# 新编码字段名称存储
cate_cols_new = []
# 提取独热编码后所有特征的名称
for i, j in enumerate(cate_cols):
if len(drop_enc.categories_[i]) == 2:
cate_cols_new.append(j)
else:
for f in drop_enc.categories_[i]:
feature_name = j + '_' + f
cate_cols_new.append(feature_name)
# 查看新字段名称提取结果
cate_cols_new
# ['Gender', 'Income_High', 'Income_Low', 'Income_Medium']
# 组合成新的DataFrame
pd.DataFrame(drop_enc.fit_transform(X3).toarray(), columns=cate_cols_new)
💫然后将上述过程封装为一个函数:
def cate_colName(Transformer, category_cols, drop='if_binary'):
"""
离散字段独热编码后字段名创建函数
:param Transformer: 独热编码转化器
:param category_cols: 输入转化器的离散变量
:param drop: 独热编码转化器的drop参数
"""
cate_cols_new = []
col_value = Transformer.categories_
for i, j in enumerate(category_cols):
if (drop == 'if_binary') & (len(col_value[i]) == 2):
cate_cols_new.append(j)
else:
for f in col_value[i]:
feature_name = j + '_' + f
cate_cols_new.append(feature_name)
return(cate_cols_new)
测试函数效果:
cate_colName(drop_enc, cate_cols)
# ['Gender', 'Income_High', 'Income_Low', 'Income_Medium']
💯至此完整介绍独热编码相关功能与数据集列名称提取函数的使用方法。
ColumnTransformer转化流水线
在执行单独的转化器时,我们需要单独将要转化的列提取出来,然后对其转化,并且在转化完成后再和其他列拼接成新的数据集。尽管很多时候表格的拆分和拼接不可避免,但该过程显然不够“自动化”。在sklearn的0.20版本中,加入了ColumnTransformer转化流水线评估器,使得上述情况得以改善。该评估器和pipeline类似,能够集成多个评估器(转化器),并一次性对输入数据的不同列采用不同处理方法,并输出转化完成并且拼接完成的数据。
from sklearn.compose import ColumnTransformer
ColumnTransformer的使用过程并不复杂,其基本说明如下:
ColumnTransformer?
其中,transformers表示集成的转化器,例如,如果我们需要对tcc数据集中的离散字段进行多分类独热编码,则需要输入这样一个转化器参数:
('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols)
当然,ColumnTransformer可以集成多个转化器,即可以在一个转化流水线中说明对所有字段的处理方法。例如上述转化器参数只说明了需要对数据集中所有category_cols字段进行OneHotEncoder(drop='if_binary')转化,而对于tcc数据集来说,还有numeric_cols,也就是连续性字段,当然我们其实目前并不需要对这些连续型字段进行处理,但仍然不妨输入一个处理连续型字段的转化器参数(原因稍后解释),该参数可以写成如下形式:
('num', 'passthrough', numeric_cols)
如果需要对连续变量进行处理,如需要对其进行归一化或者分箱,则将'passthrough'cabs关于改为对应转化器。
preprocess_col = ColumnTransformer([
('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols),
('num', 'passthrough', numeric_cols)
])
而此时preprocess_col则表示对数据集的离散变量进行多分类独热编码处理,对连续变量不处理。如果从效果上看,preprocess_col和我们单独使用多分类独热编码处理离散变量过程并无区别,但实际上我们更推荐使用preprocess_col来进行处理,原因主要有以下几点:其一,通过preprocess_col处理后的数据集无需再进行拼接工作,preprocess_col能够直接输出离散变量独热编码+连续变量保持不变的数据集;其二,preprocess_col过程还能够对未选择的字段进行删除或者保留、或者统一再使用某种转化器来进行转化(默认是删除其他所有列),通过remainder参数来进行说明。例如,我们现在可以借助preprocess_col直接对tcc数据集进行离散变量独热编码、连续变量保留、以及剔除ID列和标签列的操作:
# 训练转化器
preprocess_col.fit(tcc)
#
ColumnTransformer(transformers=[('cat', OneHotEncoder(drop='if_binary'),
['gender', 'SeniorCitizen', 'Partner',
'Dependents', 'PhoneService', 'MultipleLines',
'InternetService', 'OnlineSecurity',
'OnlineBackup', 'DeviceProtection',
'TechSupport', 'StreamingTV',
'StreamingMovies', 'Contract',
'PaperlessBilling', 'PaymentMethod']),
('num', 'passthrough',
['tenure', 'MonthlyCharges', 'TotalCharges'])])
# 输出转化结果
pd.DataFrame(preprocess_col.transform(tcc))