【Kaggle】Telco Customer Churn 电信用户流失预测案例

第二部分导读

  在上一部分中,我们已经完成了对数据集背景解读、数据预处理与探索性分析。在数据背景解读中,我们介绍了数据集来源、电信用户流失分析的基本业务背景,并详细解释了每个字段的基本含义;在数据预处理过程中,我们对数据集进行了缺失值和异常值分析,并且根据实际业务情况对缺失值进行了0值填补;而在探索性分析的过程中,我们对比分析了标签不同取值时特征取值的分布情况,并从中初步分析影响用户流失的关键因素。
  本节开始,我们将围绕此前已经处理好的数据来进一步来进行用户流失预测。当然,要进行尽可能精准的用户流失预测,就离不开特征工程、模型选择与训练、参数调优和模型融合这些环节。考虑到该数据集的建模目标有两个,其一是希望能够进行尽可能精准的预测,同时由于该案例也包含数据分析背景,要求模型结果也能够为业务人员在业务开展过程中提供具体指导意见,因此无论是在模型选型过程还是特征工程环节,我们都将同时纳入这两个因素进行综合考虑。
  当然,本节我们将优先考虑具备模型可解释性的逻辑回归和决策树,这两个算法也是大多数在要求对结果进行解释的场景下优先考虑的模型,此外在实际建模过程中需要注意的是,不同模型需要带入的数据编码类型也各不相同,因此在本节中,我们也将详细介绍各类数据编码方法及其使用过程中的注意事项。
  尽管本节将详细讨论各种数据编码的方法以及模型训练、调参的技巧,但需要知道的是,在大多数情况下、大多数技巧其实都是无法显著提升模型效果的,这也就是为什么很多时候模型训练需要反复尝试、不断探索的根本原因。但无论这些技巧能否在本次案例的数据集中发挥作用,它们都是当下通过长期实践总结出的最为有效的方法和策略,都值得重点学习。而在实际的模型训练过程中,我们也将广泛的尝试各种方法和各种技巧(而不是只介绍对当前数据集有用的方法),借此深化同学们对这些方法和技巧的理解。而在后续的案例中大家会逐渐发现,很多方法其实都有各自发挥用途的场景。

Part 2.数据编码与模型训练

  本节开始将正式进入到模型训练部分,首先我们根据上一小节的分析结果,再次执行数据预处理过程:

import numpy as np
import pandas as pdimport 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则明确要求不能对离散字段字段进行哑变量变换、否则会影响模型速度和效果。因此,本部分我们先介绍较为通用的离散字段的编码方法,然后再根据后续实际模型要求,选择不同编码方式对数据进行处理。

1.OrdinalEncoder自然数排序

  首先是自然数排序方法,该方法的过程较为简单,即先对离散字段的不同取值进行排序,然后对其进行自然数值取值转化,例如下述过程:

对于自然数排序过程,我们可以通过简单的pandas中的列取值调整来进行,例如就像此前对标签字段取值的调整过程,此外也可以直接考虑调用sklearn中的OrdinalEncoder()评估器(转化器)。

from sklearn import preprocessing
preprocessing.OrdinalEncoder()
#OrdinalEncoder()

和所有的sklearn中转化器使用过程类似,需要先训练、后使用:

X1 = np.array([['F'], ['M'], ['M'], ['F']])
X1
#array([['F'],
#       ['M'],
#       ['M'],
#       ['F']], dtype='<U1')
# 实例化转化器
enc = preprocessing.OrdinalEncoder()
# 在X1上训练
enc.fit(X1)
#OrdinalEncoder()
# 对X1数据集进行转化
enc.transform(X1)
#array([[0.],
#       [1.],
#       [1.],
#       [0.]])

当然,该评估器的训练过程就相当于记录了原分类变量不同取值和自然数之间的对应关系,我们可以调用转化器如下属性来查看映射关系:

enc.categories_
#[array(['F', 'M'], dtype='<U1')]

由于自然数是从0开始排序,因此上述映射关系为F转化为0、M转化为1。
  当我们训练好了一个转化器后,接下来我们就能使用该转化器进一步依照该规则对其他数据进行转化:

X2 = np.array([['M'], ['F']])
enc.transform(X2)
#array([[1.],
#       [0.]])

而这种转化器的使用方式,也为非常便于我们执行在训练集上进行训练、在测试集上进行测试这一过程。

2.OneHotEncoder独热编码

  • 一般过程

  当然,除了自然顺序编码外,常见的对离散变量的编码方式还有独热编码,独热编码的过程如下:
不难发现,独热编码过程其实和我们此前介绍的哑变量创建过程一致(至少在sklearn中并无差别)。对于独热编码的过程,我们可以通过pd.get_dummies函数实现,也可以通过sklearn中OneHotEncoder评估器(转化器)来实现。

preprocessing.OneHotEncoder()
#OneHotEncoder()

基本过程如下:

X1
#array([['F'],
#       ['M'],
#       ['M'],
#       ['F']], dtype='<U1')
enc = preprocessing.OneHotEncoder()
enc.fit_transform(X1).toarray()
#array([[1., 0.],
#       [0., 1.],
#       [0., 1.],
#       [1., 0.]])

同样,训练完成后的转化器会记录转化规则:

enc.categories_
#[array(['F', 'M'], dtype='<U1')]
# 该排序实际上也是字典顺序
'M' > 'F'
#True

并能够对新的数据依据原转化规则进行转化:

X2
#array([['M'],
#       ['F']], dtype='<U1')
enc.transform(X2).toarray()
#array([[0., 1.],
#       [1., 0.]])
  • drop='if_binary’过程

  对于独热编码的使用,有一点是额外需要注意的,那就是对于二分类离散变量来说,独热编码往往是没有实际作用的。例如对于上述极简数据集而言,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']})
X3

drop_enc = preprocessing.OneHotEncoder(drop='if_binary')
drop_enc.fit_transform(X3).toarray()
#array([[0., 1., 0., 0.],
#       [1., 0., 0., 1.],
#       [1., 1., 0., 0.],
#       [0., 0., 1., 0.]])
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 + '_' + fcate_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 + '_' + fcate_cols_new.append(feature_name)return(cate_cols_new)

测试函数效果:

cate_colName(drop_enc, cate_cols)
#['Gender', 'Income_High', 'Income_Low', 'Income_Medium']

  当然,我们也可以围绕tcc数据集进行drop='if_binary’的多分类转化:

enc = preprocessing.OneHotEncoder(drop='if_binary')
df_cate = tcc[category_cols]
enc.fit(df_cate)
#OneHotEncoder(drop='if_binary')
pd.DataFrame(enc.transform(df_cate).toarray(), columns=cate_colName(enc, category_cols))


至此完整介绍独热编码相关功能与数据集列名称提取函数的使用方法。

3.ColumnTransformer转化流水线

  在执行单独的转化器时,我们需要单独将要转化的列提取出来,然后对其转化,并且在转化完成后再和其他列拼接成新的数据集。尽管很多时候表格的拆分和拼接不可避免,但该过程显然不够“自动化”。在sklearn的0.20版本中,加入了ColumnTransformer转化流水线评估器,使得上述情况得以改善。该评估器和pipeline类似,能够集成多个评估器(转化器),并一次性对输入数据的不同列采用不同处理方法,并输出转化完成并且拼接完成的数据。

from sklearn.compose import ColumnTransformer

  ColumnTransformer的使用过程并不复杂,其基本说明如下:

ColumnTransformer?
#Init signature:
#ColumnTransformer(
#    transformers,
#    *,
#    remainder='drop',
#    sparse_threshold=0.3,
#    n_jobs=None,
#    transformer_weights=None,
#    verbose=False,
#)

其中,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’字符串表示直接让连续变量通过,不对其进行任何处理。

当然,如果需要对连续变量进行处理,如需要对其进行归一化或者分箱,则将’passthrough’cabs关于改为对应转化器。

  然后,我们就能使用ColumnTransformer对上述两个过程进行集成:

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


当然,我们还需要继续提取列名称,需要注意的是,转化后的数据仍然是离散字段排在连续字段前面,和两个转化器集成顺序相同。首先我们可以通过ColumnTransformer的.named_transformers_属性来查看具体的训练好的每个转化器(若未训练,则调用该属性会报错)

preprocess_col.named_transformers_
#{'cat': OneHotEncoder(drop='if_binary'),
# 'num': 'passthrough',
# 'remainder': 'drop'}
# 查看独热编码转化器
preprocess_col.named_transformers_['cat']
#OneHotEncoder(drop='if_binary')
# 转化后离散变量列名称
category_cols_new = cate_colName(preprocess_col.named_transformers_['cat'], category_cols)
# 所有字段名称
cols_new = category_cols_new + numeric_cols
# 输出最终dataframe
pd.DataFrame(preprocess_col.transform(tcc), columns=cols_new)

此外,在使用ColumnTransformer时我们还能自由设置系数矩阵的阈值,通过sparse_threshold参数来进行调整,默认是0.3,即超过30%的数据是0值时,ColumnTransformer输出的特征矩阵是稀疏矩阵。

二、连续字段的特征变换

1.数据标准化与归一化

  当然,除了离散变量的重编码外,有的时候我们也需要对连续变量进行转化,以提升模型表现或模型训练效率。在之前的内容中我们曾介绍了关于连续变量标准化和归一化的相关内容,对连续变量而言,标准化可以消除量纲影响并且加快梯度下降的迭代效率,而归一化则能够对每条数据进行进行范数单位化处理,我们可以通过下面的内容进行标准化和归一化相关内容回顾。


标准化与归一化

  从功能上划分,sklearn中的归一化其实是分为标准化(Standardization)和归一化(Normalization)两类。其中,此前所介绍的Z-Score标准化和0-1标准化,都属于Standardization的范畴,而在sklearn中,Normalization则特指针对单个样本(一行数据)利用其范数进行放缩的过程。不过二者都属于数据预处理范畴,都在sklearn中的Preprocessing data模块下。

需要注意的是,此前我们介绍数据归一化时有讨论过标准化和归一化名称上的区别,在大多数场景下其实我们并不会对其进行特意的区分,但sklearn中标准化和归一化则各指代一类数据处理方法,此处需要注意。

标准化 Standardization

  sklearn的标准化过程,即包括Z-Score标准化,也包括0-1标准化,并且即可以通过实用函数来进行标准化处理,同时也可以利用评估器来执行标准化过程。接下来我们分不同功能以的不同实现形式来进行讨论:

  • Z-Score标准化的评估器实现方法

  实用函数进行标准化处理,尽管从代码实现角度来看清晰易懂,但却不适用于许多实际的机器学习建模场景。其一是因为在进行数据集的训练集和测试集切分后,我们首先要在训练集进行标准化、然后统计训练集上统计均值和方差再对测试集进行标准化处理,因此其实还需要一个统计训练集相关统计量的过程;其二则是因为相比实用函数,sklearn中的评估器其实会有一个非常便捷的串联的功能,sklearn中提供了Pipeline工具能够对多个评估器进行串联进而组成一个机器学习流,从而简化模型在重复调用时候所需代码量,因此通过评估器的方法进行数据标准化,其实是一种更加通用的选择。
  既然是实用评估器进行数据标准化,那就需要遵照评估器的一般使用过程:
首先是评估器导入:

from sklearn.preprocessing import StandardScaler

然后是查阅评估器参数,然后进行评估器的实例化:

# 查阅参数
StandardScaler?scaler = StandardScaler()

然后导入数据,进行训练,此处也是使用fit函数进行训练:

X = np.arange(15).reshape(5, 3)
X
#array([[ 0,  1,  2],
#       [ 3,  4,  5],
#       [ 6,  7,  8],
#       [ 9, 10, 11],
#       [12, 13, 14]])
X_train, X_test = train_test_split(X)
X_train, X_test
#(array([[ 9, 10, 11],
#        [ 6,  7,  8],
#        [ 0,  1,  2]]),
# array([[12, 13, 14],
#        [ 3,  4,  5]]))
scaler.fit(X_train)
#StandardScaler()

  虽然同样是输入数据,但标准化的评估器和训练模型的评估器实际上是不同的计算过程。此前我们介绍的线性方程的评估器,输入数据进行训练的过程(fit过程)实际上是计算线性方程的参数,而此处标准化的评估器的训练结果实际上是对输入数据的相关统计量进行了汇总计算,也就是计算了输入数据的均值、标准差等统计量,后续将用这些统计量对各数据进行标准化计算。不过无论计算过程是否相同,评估器最终计算结果都可以通过相关属性进行调用和查看:

# 查看训练数据各列的标准差
scaler.scale_
#array([3.74165739, 3.74165739, 3.74165739])
# 查看训练数据各列的均值
scaler.mean_
#array([5., 6., 7.])
# 查看训练数据各列的方差
scaler.var_
#array([14., 14., 14.])
np.sqrt(scaler.var_)
#array([3.74165739, 3.74165739, 3.74165739])
# 总共有效的训练数据条数
scaler.n_samples_seen_
#3

当然,截止目前,我们只保留了训练数据的统计量,但尚未对任何数据进行修改,输入的训练数据也是如此

X_train
#array([[ 9, 10, 11],
#       [ 6,  7,  8],
#       [ 0,  1,  2]])

  接下来,我们可以通过评估器中的transform方法来进行数据标准化处理。注意,算法模型的评估器是利用predict方法进行数值预测,而标准化评估器则是利用transform方法进行数据的数值转化。

# 利用训练集的均值和方差对训练集进行标准化处理
scaler.transform(X_train)
#array([[ 1.06904497,  1.06904497,  1.06904497],
#       [ 0.26726124,  0.26726124,  0.26726124],
#       [-1.33630621, -1.33630621, -1.33630621]])
# 利用训练集的均值和方差对测试集进行标准化处理
scaler.transform(X_test)
#array([[ 1.87082869,  1.87082869,  1.87082869],
#       [-0.53452248, -0.53452248, -0.53452248]])
z_score(X_train)
#array([[ 1.06904497,  1.06904497,  1.06904497],
#       [ 0.26726124,  0.26726124,  0.26726124],
#       [-1.33630621, -1.33630621, -1.33630621]])

此外,我们还可以使用fit_transform对输入数据进行直接转化:

scaler = StandardScaler()
# 一步执行在X_train上fit和transfrom两个操作
scaler.fit_transform(X_train)
#array([[ 1.06904497,  1.06904497,  1.06904497],
#       [ 0.26726124,  0.26726124,  0.26726124],
#       [-1.33630621, -1.33630621, -1.33630621]])
X_train
#array([[ 9, 10, 11],
#       [ 6,  7,  8],
#       [ 0,  1,  2]])
scaler.transform(X_test)
#array([[ 1.87082869,  1.87082869,  1.87082869],
#       [-0.53452248, -0.53452248, -0.53452248]])

接下来,我们就能直接带入标准化后的数据进行建模了。

  • 0-1标准化的评估器实现方法

  类似的,我们可以调用评估器进行0-1标准化。

from sklearn.preprocessing import MinMaxScaler
MinMaxScaler?
scaler = MinMaxScaler()
scaler.fit_transform(X)
#array([[0.  , 0.  , 0.  ],
#       [0.25, 0.25, 0.25],
#       [0.5 , 0.5 , 0.5 ],
#       [0.75, 0.75, 0.75],
#       [1.  , 1.  , 1.  ]])
X
#array([[ 0,  1,  2],
#       [ 3,  4,  5],
#       [ 6,  7,  8],
#       [ 9, 10, 11],
#       [12, 13, 14]])
scaler.data_min_
#array([0., 1., 2.])
scaler.data_max_
#array([12., 13., 14.])

此外,sklearn中还有针对稀疏矩阵的标准化(MaxAbsScaler)、针对存在异常值点特征矩阵的标准化(RobustScaler)、以及非线性变化的标准化(Non-linear transformation)等方法,相关内容待后续进行介绍。

归一化 Normalization

  和标准化不同,sklearn中的归一化特指将单个样本(一行数据)放缩为单位范数(1范数或者2范数为单位范数)的过程,该操作常见于核方法或者衡量样本之间相似性的过程中。这些内容此前我们并未进行介绍,但出于为后续内容做铺垫的考虑,此处先介绍关于归一化的相关方法。同样,归一化也有函数实现和评估器实现两种方法。

  • 归一化的函数实现方法

  先查看函数相关说明文档:

preprocessing.normalize?

  此前我们曾解释到关于范数的基本概念,假设向量x=[x1,x2,...,xn]Tx = [x_1, x_2, ..., x_n]^Tx=[x1​,x2​,...,xn​]T,则向量x的1-范数的基本计算公式为:
∣∣x∣∣1=∣x1∣+∣x2∣+...+∣xn∣||x||_1 = |x_1|+|x_2|+...+|x_n| ∣∣x∣∣1​=∣x1​∣+∣x2​∣+...+∣xn​∣
即各分量的绝对值之和。而向量x的2-范数计算公式为:
∣∣x∣∣2=(∣x1∣2+∣x2∣2+...+∣xn∣2)||x||_2=\sqrt{(|x_1|^2+|x_2|^2+...+|x_n|^2)} ∣∣x∣∣2​=(∣x1​∣2+∣x2​∣2+...+∣xn​∣2)​
即各分量的平方和再开平方。
  而sklearn中的Normalization过程,实际上就是将每一行数据视作一个向量,然后用每一行数据去除以该行数据的1-范数或者2-范数。具体除以哪个范数,以preprocessing.normalize函数中输入的norm参数为准。

X
#array([[ 0,  1,  2],
#       [ 3,  4,  5],
#       [ 6,  7,  8],
#       [ 9, 10, 11],
#       [12, 13, 14]])
# 1-范数单位化过程
preprocessing.normalize(X, norm='l1')
#array([[0.        , 0.33333333, 0.66666667],
#       [0.25      , 0.33333333, 0.41666667],
#       [0.28571429, 0.33333333, 0.38095238],
#       [0.3       , 0.33333333, 0.36666667],
#       [0.30769231, 0.33333333, 0.35897436]])
np.linalg.norm(X, ord=1, axis=1)
#array([ 3., 12., 21., 30., 39.])
np.sum(X, axis=1)
#array([ 3, 12, 21, 30, 39])
X / np.linalg.norm(X, ord=1, axis=1).reshape(5, 1)
#array([[0.        , 0.33333333, 0.66666667],
#       [0.25      , 0.33333333, 0.41666667],
#       [0.28571429, 0.33333333, 0.38095238],
#       [0.3       , 0.33333333, 0.36666667],
#       [0.30769231, 0.33333333, 0.35897436]])
# 2-范数单位化过程
preprocessing.normalize(X, norm='l2')
#array([[0.        , 0.4472136 , 0.89442719],
#       [0.42426407, 0.56568542, 0.70710678],
#       [0.49153915, 0.57346234, 0.65538554],
#       [0.5178918 , 0.57543534, 0.63297887],
#       [0.53189065, 0.57621487, 0.62053909]])
np.linalg.norm(X, ord=2, axis=1)
#array([ 2.23606798,  7.07106781, 12.20655562, 17.3781472 , 22.56102835])
np.sqrt(np.sum(np.power(X, 2), axis=1))
#array([ 2.23606798,  7.07106781, 12.20655562, 17.3781472 , 22.56102835])
X / np.linalg.norm(X, ord=2, axis=1).reshape(5, 1)
#array([[0.        , 0.4472136 , 0.89442719],
#       [0.42426407, 0.56568542, 0.70710678],
#       [0.49153915, 0.57346234, 0.65538554],
#       [0.5178918 , 0.57543534, 0.63297887],
#       [0.53189065, 0.57621487, 0.62053909]])
# 范数单位化结果
np.linalg.norm(preprocessing.normalize(X, norm='l2'), ord=2, axis=1)
#array([1., 1., 1., 1., 1.])

  此外,我们也可以通过调用评估器来实现上述过程:

from sklearn.preprocessing import Normalizer
Normalizer?
normlize = Normalizer()
normlize.fit_transform(X)
#array([[0.        , 0.4472136 , 0.89442719],
#       [0.42426407, 0.56568542, 0.70710678],
#       [0.49153915, 0.57346234, 0.65538554],
#       [0.5178918 , 0.57543534, 0.63297887],
#       [0.53189065, 0.57621487, 0.62053909]])
normlize = Normalizer(norm='l1')
normlize.fit_transform(X)
#array([[0.        , 0.33333333, 0.66666667],
#       [0.25      , 0.33333333, 0.41666667],
#       [0.28571429, 0.33333333, 0.38095238],
#       [0.3       , 0.33333333, 0.36666667],
#       [0.30769231, 0.33333333, 0.35897436]])

2.连续变量分箱

  此外,在实际模型训练过程中,我们也经常需要对连续型字段进行离散化处理,也就是将连续性字段转化为离散型字段。

连续字段的离散过程如下所示:

  连续变量的离散过程也可以理解为连续变量取值的重新编码过程,在很多时候,连续变量的离散化也被称为连续变量分箱。需要注意的是,离散之后字段的含义将发生变化,原始字段Income代表用户真实收入状况,而离散之后的含义就变成了用户收入的等级划分,0表示低收入人群、1表示中等收入人群、2代表高收入人群。连续字段的离散化能够更加简洁清晰的呈现特征信息,并且能够极大程度减少异常值的影响(例如Income取值为180的用户),同时也能够消除特征量纲影响,当然,最重要的一点是,对于很多线性模型来说,连续变量的分箱实际上相当于在线性方程中引入了非线性的因素,从而提升模型表现。当然,连续变量的分箱过程会让连续变量损失一些信息,而对于其他很多模型来说(例如树模型),分箱损失的信息则大概率会影响最终模型效果。
  当然,分箱的过程并不复杂,唯一需要注意的就是需要确定划分几类、以及每一类代表什么含义。一般来说分箱的规则基本可以由业务指标来确定或者由某种计算流程来确定。

  • 根据业务指标确定

  在一些有明确业务背景的场景中,或许能够找到一些根据长期实践经验积累下来的业务指标来作为划分依据,例如很多金融行业会通过一些业务指标来对用户进行价值划分,例如会规定月收入10000以上属于高收入人群,此时10000就可以作为连续变量离散化的依据。

  • 根据计算流程确定

  当然,更常见的一种情况是并没有明确的业务指标作为划分依据,此时我们就需要通过某种计算流程来进行确定。常见方法有四种,分别是等宽分箱(等距分箱)、等频分箱(等深分箱)、聚类分箱和有监督分箱,接下来我们对这四种方法依次进行介绍。

2.1 等宽分箱

  所谓等宽分箱,需要先确定划分成几分,然后根据连续变量的取值范围划分对应数量的宽度相同的区间,并据此对连续变量进行分箱。例如上述Income字段取值在[0,180]之间,现对其进行等宽分箱分成三份,则每一份的取值范围分别是[0,60),[60,120),[120,180],连续字段将据此进行划分,分箱过程如下所示:
当然,我们也可以在sklearn的预处理模块中调用KBinsDiscretizer转化器实现该过程:

# 转化为列向量
income = np.array([0, 10, 180, 30, 55, 35, 25, 75, 80, 10]).reshape(-1, 1)

这里需要注意,一列特征必须以列向量呈现,才能够被KBinsDiscretizer正确识别

preprocessing.KBinsDiscretizer
#sklearn.preprocessing._discretization.KBinsDiscretizer

KBinsDiscretizer转化器的使用并不复杂,我们可以在n_bins参数位上输入需要分箱的个数,strategy参数位上输入等宽分箱、等频分箱还是聚类分箱,encode参数位上输入分箱后的离散字段是否需要进一步进行独热编码处理或者自然数编码。而上述分箱过程可通过如下过程实现:

preprocessing.KBinsDiscretizer?
#Init signature:
#preprocessing.KBinsDiscretizer(
#    n_bins=5,
#    *,
#    encode='onehot',
#    strategy='quantile',
#    dtype=None,
#)# 三分等宽分箱,strategy选择'uniform'
dis = preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform')
dis.fit_transform(income)
#array([[0.],
#       [0.],
#       [2.],
#       [0.],
#       [0.],
#       [0.],
#       [0.],
#       [1.],
#       [1.],
#       [0.]])
'''或者'''
from sklearn.preprocessing import KBinsDiscretizer
dis = KBinsDiscretizer(n_bins=3,encode="ordinal",strategy="uniform")
dis.fit_transform(income)
#array([[0.],
#       [0.],
#       [2.],
#       [0.],
#       [0.],
#       [0.],
#       [0.],
#       [1.],
#       [1.],
#       [0.]])

当然,在分箱结束后,可以通过.bin_edges_查看分箱依据(每个箱体的边界)。需要主要注意的是,这些分箱的边界也就是模型测试阶段对测试集进行分箱的依据,这也符合“在训练集上训练(找到分箱边界),在测试集上测试(利用分箱边界对测试集进行分箱)”这一基本要求。

dis.bin_edges_
#array([array([  0.,  60., 120., 180.])], dtype=object)

2.2 等频分箱

  在等频分箱的过程中,需要先确定划分成几分,然后选择能够让每一份包含样本数量相同的划分方式。例如对于上述数据集,若需要分成两份,则需要先对所有数据进行排序,然后选取一个中间值对其进行切分,对于income来说,“中间值”应该是23.5:

np.sort(income.flatten(), axis=0)
#array([  0,  10,  10,  25,  30,  35,  55,  75,  80, 180])

因此以32.5作为切分依据,对其进行分箱处理:

能够看出,最终分成了样本数量相同的两份。我们也可以在sklearn中实现该过程:

# 两分等频分箱,strategy选择'quantile'
dis = preprocessing.KBinsDiscretizer(n_bins=2, encode='ordinal', strategy='quantile')
dis.fit_transform(income)
#array([[0.],
#       [0.],
#       [1.],
#       [0.],
#       [1.],
#       [1.],
#       [0.],
#       [1.],
#       [1.],
#       [0.]])
'''或者'''
from sklearn.preprocessing import KBinsDiscretizer
dis = KBinsDiscretizer(n_bins=2,encode="ordinal",strategy="quantile")
dis.fit_transform(income)
#array([[0.],
#       [0.],
#       [1.],
#       [0.],
#       [1.],
#       [1.],
#       [0.],
#       [1.],
#       [1.],
#       [0.]])dis.bin_edges_
#array([array([  0. ,  32.5, 180. ])], dtype=object)

当然,如果样本数量无法整除等频分箱的箱数,则最后一个“箱子”将包含余数样本。例如对10条样本进行三分等频分箱,则会分为3/3/4的结果:

dis = preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='quantile')
dis.fit_transform(income)
#array([[0.],
#       [0.],
#       [2.],
#       [1.],
#       [2.],
#       [1.],
#       [1.],
#       [2.],
#       [2.],
#       [0.]])
dis.bin_edges_
#array([array([  0.,  25.,  55., 180.])], dtype=object)

  从上述等宽分箱和等频分箱的结果不难看出,等宽分箱会一定程度受到异常值的影响,而等频分箱又容易完全忽略异常值信息,从而一定程度上导致特征信息损失,而若要更好的兼顾变量原始数值分布,则可以考虑使用聚类分箱。

2.3 聚类分箱

  所谓聚类分箱,指的是先对某连续变量进行聚类(往往是KMeans聚类),然后用样本所属类别作为标记代替原始数值,从而完成分箱的过程。这里我们仍然可以通过income数据来模拟该过程,此处我们使用KMeans对其进行三类别聚类:

from sklearn import cluster
kmeans = cluster.KMeans(n_clusters=3)
kmeans.fit(income)
#KMeans(n_clusters=3)

在训练完成评估器后,通过.labels_查看每条样本所属簇的类别:

kmeans.labels_
#array([1, 1, 2, 1, 0, 1, 1, 0, 0, 1])

该值也就是离散化后每条样本的取值,该过程将第三条数据单独划分成了一类,这也满足了此前所说的一定程度上保留异常值信息这一要求,能够发现,聚类过程能够更加完整的保留原始数值分布信息。

当然,KBinsDiscretizer转化器中也集成了利用KMeans进行分箱的过程,只需要在strategy参数中选择’kmeans’即可:

# 两分等频分箱,strategy选择'kmeans'
dis = preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans')
dis.fit_transform(income)
#array([[0.],
#       [0.],
#       [2.],
#       [0.],
#       [1.],
#       [0.],
#       [0.],
#       [1.],
#       [1.],
#       [0.]])
dis.bin_edges_
#array([array([  0.        ,  44.16666667, 125.        , 180.        ])],
#      dtype=object)

而在实际建模过程中,如无其他特殊要求,建议优先考虑聚类分箱方法。(介于等宽分箱、等频分箱之间,不过于受到异常值影响,也不忽略异常值的影响)

2.4 有监督分箱

  当然,无论是等宽/等频分箱,还是聚类分箱,本质上都是进行无监督的分箱,即在不考虑标签的情况下进行的分箱。而在所有的分箱过程中,还有一类是有监督分箱,即根据标签取值对连续变量进行分箱。在这些方法中,最常用的分箱就是树模型分箱。
  而具体来看,树模型的分箱有两种,其一是利用决策树模型进行分箱,简单根据决策树的树桩(每一次划分数据集的切分点)来作为连续变量的切分依据,由于决策树的分叉过程总是会选择让整体不纯度降低最快的切分点,因此这些切分点就相当于是最大程度保留了有利于样本分类的信息,我们可以通过如下示例进行说明,例如假设y为数据集标签:

y = np.array([1, 1, 0, 1, 0, 0, 0, 1, 0, 0])

则可以以income为特征,y为标签训练决策树:

from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier().fit(income, y)

观察训练结果:

plt.figure(figsize=(6, 2), dpi=150)
tree.plot_tree(clf)


那么根据上述结果,如果需要对income进行三类分箱的话,则可以选择32.5和65作为切分点,对数据集进行切分:

  不难发现,这种有监督的分箱的结果其实会极大程度利于有监督模型的构建(例如如果按照上述规则进行分箱,则不会影响决策树前两层的生长)。但有监督的分箱过程其实也会面临诸如可能泄露数据集标签信息从而造成过拟合(目标编码容易造成模型过拟合)、决策树生长过程不稳定、树模型容易过拟合等问题影响。因此,一般来说有监督的分箱可能会在一些特殊场景下采用一些变种的方式来进行,例如在推荐系统中常用的GBDT+LR模型组合中,就会采用一种非常特殊的方式对连续变量进行分箱:假设我们采用训练集中所有连续变量及标签训练两颗决策树,第一棵树有3个叶节点,第二棵树有2个叶节点,假设某条样本在第一棵树的第二个叶节点中、出现在第二棵树的第二个叶结点中,则我们可以将该样本标记为01001,其中总共5位数表示总共5个叶节点,而0表示该样本未出现在该位置上、1表示出现在该位置上,并最终将01001代替该样本的所有连续变量的取值,在每条样本都采用了该方式重编码后,我们就能完整使用新生成的这5列替换原数据集中所有连续变量。

需要知道的是,这种编码方式并没有在sklearn中集成,很多过程需要手写算法或调用其他库,后续在遇到时再进行具体实现方面的介绍。

注意,这种根据标签对连续变量进行有监督的重编码的方式,有时也被称为目标编码,也是特征工程中特征衍生的一类重要方法,我们会在Part 3特征工程部分再进行讨论。

除了上述针对连续变量的特征变换方法外,还有一种能够对连续变量取对数的操作,能够让连续变量一定程度恢复正态分布的分布特性,进而提升模型效果。该方法实现过程较为简单,直接借助numpy中对数运算功能即可,例如np.log(income+1)对其进行转化即可(+1是为了防止0值出现),该方法有时会对部分线性模型起作用。

3.连续变量特征转化的ColumnTransformer集成

  上述所介绍的关于连续变量的标准化或分箱等过程,也是可以集成到ColumnTransformer中的。例如,如果同时执行离散字段的多分类独热编码和连续字段的标准化,则可以创建如下转化流:

ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', preprocessing.StandardScaler(), numeric_cols)
])
#ColumnTransformer(transformers=[('cat', OneHotEncoder(drop='if_binary'),
#                                 ['gender', 'SeniorCitizen', 'Partner',
#                                  'Dependents', 'PhoneService', 'MultipleLines',
#                                  'InternetService', 'OnlineSecurity',
#                                  'OnlineBackup', 'DeviceProtection',
#                                  'TechSupport', 'StreamingTV',
#                                  'StreamingMovies', 'Contract',
#                                  'PaperlessBilling', 'PaymentMethod']),
#                                ('num', StandardScaler(),
#                                 ['tenure', 'MonthlyCharges', 'TotalCharges'])])

类似的,如果需要同时对离散变量进行多分类独热编码、对连续字段进行基于kmeans的三分箱,则可以创建如下转化流:

ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans'), numeric_cols)
])
#ColumnTransformer(transformers=[('cat', OneHotEncoder(drop='if_binary'),
#                                 ['gender', 'SeniorCitizen', 'Partner',
#                                  'Dependents', 'PhoneService', 'MultipleLines',
#                                  'InternetService', 'OnlineSecurity',
#                                  'OnlineBackup', 'DeviceProtection',
#                                  'TechSupport', 'StreamingTV',
#                                  'StreamingMovies', 'Contract',
#                                  'PaperlessBilling', 'PaymentMethod']),
#                                ('num',
#                                 KBinsDiscretizer(encode='ordinal', n_bins=3,
#                                                  strategy='kmeans'),
#                                 ['tenure', 'MonthlyCharges', 'TotalCharges'])])

后续我们将基于这些高度自动化的转化流程,来进行离散变量和连续变量整体重编码。

电信用户流失预测案例(2)(特征工程)相关推荐

  1. 电信用户流失预测案例(1)

    [Kaggle]Telco Customer Churn 电信用户流失预测案例 前言:案例学习说明与案例建模流程   在学习了经典机器学习算法和Scikit-Learn的调参策略之后,接下来,我们将把 ...

  2. 电信用户流失预测案例(3)

    三.逻辑回归模型训练与结果解释   在完成数据重编码后,接下来即可进行模型训练了,此处我们首先考虑构建可解释性较强的逻辑回归与决策树模型,并围绕最终模型输出结果进行结果解读,而在下一节,我们将继续介绍 ...

  3. 基于python的电信用户流失预测

    题目: 电信用户流失预测 <大数据分析与应用> 一.介绍项目背景与分析目标 电信用户流失预测是一种针对电信运营商的数据挖掘应用,目的是通过分析历史的客户使用数据,预测未来的客户流失情况.其 ...

  4. python数据分析实战:生存分析与电信用户流失预测

    文章目录 1.背景 1.1 生存分析.KM曲线及Cox回归 1.2 案例背景 2.AIC向前逐步回归法进行特征选择 3.Cox模型搭建 3.1 特征重要性分析 3.2 模型校准 3.3 对个体进行预测 ...

  5. 电信行业用户流失预测——你的用户会流失吗?

    博客目的 随着通信技术的飞速发展,通信用户数量的急剧增加,通信市场趋于饱和,运营商之间的竞争愈演愈烈,使得运营商更加关注用户资源流失的问题.通过使用用户产生的数据预测潜在的流失用户,并对这些潜在的流失 ...

  6. 电信用户流失分析与预测

    电信用户流失分析与预测 一. 研究背景 二. 分析结论与建议 三. 任务与实现 四. 数据集解析 五. 数据分析套餐 1.准备工作 导入相关的库 导入数据集 2.数据预处理 类型转换 缺失值处理 重复 ...

  7. 【数据分析与挖掘实战】电信用户流失分析与预测

    背景 关于用户留存有这样一个观点,如果将用户流失率降低5%,公司利润将提升25%-85%.如今高居不下的获客成本让电信运营商遭遇"天花板",甚至陷入获客难的窘境.随着市场饱和度上升 ...

  8. Python数据分析高薪实战第十二天 网络服务用户流失预测分析和国产电视剧评分预测分析

    29 综合实战:网络服务用户流失预测与分析 绝大多数互联网公司都面临一个非常重要的问题:用户流失问题.随着互联网和移动互联网的充分发展,发展新用户(也就是一般所说的拉新)的成本越来越高,往往要几块或者 ...

  9. 通过客户流失预测案例感悟数据分析设计方法思考——数据驱动、AI驱动

    国际著名的咨询公司Gartner在2013年总结出了一套数据分析的框架,数据分析的四个层次:描述性分析.诊断性分析.预测性分析.处方性分析. Gartner于2020年中给出预测,到2024年底,75 ...

最新文章

  1. 机器学习PAL产品优势
  2. 混沌大学签约神策数据,加快颠覆式创新教学步伐
  3. 理解 | 理解a: float=10
  4. 网络运维装linux,网络安装linux系统
  5. vscode python第三方库检测_VSCode中使用Pylint检查python代码
  6. 标题显示字数限制 html css,【紧急】我想问一下HTML的TITLE标签,里面的内容能填写多少个?有限制吗_html/css_WEB-ITnose...
  7. Linux 命令(86)—— head 命令
  8. 俞昆20155335《网络对抗》MSF基础应用
  9. SQL注入(SQL注入(SQLi)攻击)攻击-脱库
  10. es做mysql二级索引_用Elasticsearch实现HBase二级索引
  11. ARM+DSP双核处理器应用程序攻略
  12. 使用Telerik控件搭建Doubanfm频道部分
  13. Vue组件选项props
  14. 综合布线中的配线架与理线架
  15. python学习(二十一)
  16. EndnoteX9下载及教程
  17. APIDOC- API文档生成工具——node
  18. 1104. Path In Zigzag Labelled Binary Tree**
  19. 关于数据存储的三道面试题,你会吗?
  20. matlab生成0-1之间的随机数(不同区间 权重不同)

热门文章

  1. java 全角_Java全角、半角字符的关系以及转换
  2. matlab产生时间数组以月为单位_Matlab中处理日期与时间的函数
  3. ospf hello时间和dead_深入理解OSPF协议----第二讲:OSPF报文类型
  4. backgroundworker 导致程序无法退出_macOS技巧—六种方法强制退出无响应的程序
  5. 文本编辑器实现打开帮助文件的功能
  6. C#中的数据类型转换
  7. python 设计 实践_Python程序设计实践教程
  8. C++成员对象和封闭类
  9. 143. Leetcode 78. 子集 (回溯算法-子集问题)
  10. python programming training(二): 排序算法