电信运营商用户流失分析与预测

闲言碎语

  决定转行做数据分析有3个月了,这段时间里学了SQL,Python,PowerBI,还掌握了电商内容平台的基本业务逻辑和简单机器学习算法,是时候该提升一下自己的实战经验了。
  之前做过三个项目,1.豆瓣电影数据分析,侧重点是数据爬取+matplotlib可视化+pandas操作2.CD网站用户行为分析,这个项目主要是照着秦路老师的思路做的,在练习的基础上,加了一些自己的理解,再加入了RFM模型,对用户进行分层,侧重业务理解+pandas操作3.Titanic生存者预测,主要工作是把Kaggle上的开源项目复现了一遍,掌握了数据挖掘的基本流程:分析、特征工程、建模、调参、模型融合、模型评估。

项目概述

  今天这个项目同样来自于Kaggle,内容是:电信运营商用户流失数据。
  分析主要围绕降低电信运营商用户流失率展开,根据用户的个人情况、服务属性、合同信息展开分析,找出影响用户流失的关键因素,并建立了用户流失的分类模型,针对潜在的流失用户制定预警与召回策略。

分析思路

第一部分:数据预处理

导入数据、类型转换、处理异常值

第二部分:从流失率角度进行分析

用户的个人情况、服务属性、合同信息对于流失率的影响

第三部分:从用户价值角度进行分析

用户缴费金额分布、用户累计缴费金额分布、用户终身价值(LTV)

第四部分:通过分类模型预测用户流失

特征工程、模型选择(单一模型、多模型融合)、模型评估

1、数据预处理

1.1 导入数据、库文件

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import sklearn as sk
import warnings
Filename='WA_Fn-UseC_-Telco-Customer-Churn.csv'
%matplotlib inline
Telco_Data_Origin=pd.read_csv(Filename)
Telco_Data=Telco_Data_Origin.copy()
Telco_Data.info()
图1.字段概况

查看字段,发现字段可以分为四类:

  • 用户流失情况:ChurnKey=['customerID','Churn']
  • 用户类型:CustomerAttributes=['gender','SeniorCitizen','Partner','Dependents','tenure']
  • 服务属性:ServiceAttributes=['PhoneService','MultipleLines','InternetService','OnlineSecurity','OnlineBackup', 'DeviceProtection','TechSupport','StreamingTV','StreamingMovies']
  • 合同信息:ContractAttributes=['Contract','PaperlessBilling','PaymentMethod','MonthlyCharges','TotalCharges']

1.2 类型转换

#字符型转换>>数值型
for i in ['Churn','Partner','Dependents','PhoneService','PaperlessBilling']:
    Telco_Data[i]=Telco_Data_Origin[i].apply(lambda x:1 if x=='Yes' else 0)
for i in ['MultipleLines','OnlineSecurity','OnlineBackup','DeviceProtection','TechSupport','StreamingTV','StreamingMovies']:
    Telco_Data[i]=Telco_Data_Origin[i].apply(lambda x:1 if x=='Yes' else (0 if x=='No' else np.nan))
Telco_Data['gender']=Telco_Data_Origin['gender'].apply(lambda x:1 if x=='Male' else 0)
#总缴费数值型转换
Telco_Data['TotalCharges']=Telco_Data_Origin['TotalCharges'].convert_objects(convert_numeric=True)
Telco_Data.loc[Telco_Data['TotalCharges'].isnull(),'TotalCharges']=Telco_Data[Telco_Data['TotalCharges'].isnull()].MonthlyCharges

进行数据类型转换,主要是将字符型数据转换成数值型,注意TotalCharges字段中有11个用户数据缺失,通过观察发现,这11人tenure=0,在数据采集时,这11人刚刚办理套餐,使用未满一个月,在此以MonthlyCharges进行填充。

2、流失率数据分析

2.1 描述分析

Telco_Data.describe().loc['mean']
各字段占比

在此仅展示均值结果,均值代表各字段阳性用户占比,对于tenure,MonthlyCharges,TotalCharges,则是人均使用期限、人均月缴费、人均总缴费

2.2 用户流失比例

#中文字体
plt.style.use('ggplot')
fontsize=18
plt.rcParams['font.sans-serif']=['SimHei'] 
plt.rcParams.update({'font.size': fontsize})
#画图
fig=plt.figure()
ax=fig.add_subplot(1,1,1)
Telco_Data[ChurnKey].Churn.value_counts().plot.bar(ax=ax)
#标注比例
Churn1=Telco_Data[ChurnKey].Churn.value_counts()[0]
Churn2=Telco_Data[ChurnKey].Churn.value_counts()[1]
Total=Telco_Data[ChurnKey].Churn.value_counts().sum()
ax.text(x=0-0.12,y=Churn1+50,s='%.2f %%' % (Churn1/Total*100),fontsize=fontsize-3)
ax.text(x=1-0.12,y=Churn2+50,s='%.2f %%' % (Churn2/Total*100),fontsize=fontsize-3)
#坐标轴
ax.set_ylim([0,6000])
ax.set_xticklabels(['未流失','流失'],rotation=0)
ax.set_title('流失情况')
plt.show()
用户流失比例
  • 作出流失率直方图,可以看到流失率占比26.54%,数据集为非平衡数据集

2.3 用户类型分析

下面分别绘制性别、年龄、伴侣、子女的分布直方图

#根据性别与是否流失进行透视
Sex_Churn=pd.pivot_table(data=Telco_Data,values='customerID',index='gender',columns='Churn',aggfunc='count')
#根据性别与是否流失进行透视
Senior_Churn=pd.pivot_table(data=Telco_Data,values='customerID',index='SeniorCitizen',columns='Churn',aggfunc='count')
#根据Partner与是否流失进行透视
Partner_Churn=pd.pivot_table(data=Telco_Data,values='customerID',index='Partner',columns='Churn',aggfunc='count')
#根据Dependents与是否流失进行透视
Dependents_Churn=pd.pivot_table(data=Telco_Data,values='customerID',index='Dependents',columns='Churn',aggfunc='count')
#性别年长与否
fig,ax=plt.subplots(1,2,figsize=(15, 6))

Pivot=[Sex_Churn,Senior_Churn]
Title=['流失与性别的关系','流失与年长与否的关系']
Label=[['女','男'],['年轻','年长']]
lim=[[0,3500],[0,5000]]
fontsize=15

for i in range(2):
    Pivot[i].plot.bar(title=Title[i],ax=ax[i])
    Total=Pivot[i].sum().sum()
    ax[i].text(x=0,y=Pivot[i].iloc[0,0]+30,s='%.2f %%' % (Pivot[i].iloc[0,0]/Pivot[i].iloc[0,:].sum()*100),fontsize=fontsize,horizontalalignment='right')
    ax[i].text(x=0,y=Pivot[i].iloc[0,1]+30,s='%.2f %%' % (Pivot[i].iloc[0,1]/Pivot[i].iloc[0,:].sum()*100),fontsize=fontsize,horizontalalignment='left')
    ax[i].text(x=1,y=Pivot[i].iloc[1,0]+30,s='%.2f %%' % (Pivot[i].iloc[1,0]/Pivot[i].iloc[1,:].sum()*100),fontsize=fontsize,horizontalalignment='right')
    ax[i].text(x=1,y=Pivot[i].iloc[1,1]+30,s='%.2f %%' % (Pivot[i].iloc[1,1]/Pivot[i].iloc[1,:].sum()*100),fontsize=fontsize,horizontalalignment='left')
    ax[i].set_xticklabels(Label[i],rotation=0)
    ax[i].set_ylim(lim[i])
    ax[i].legend(['未流失','流失'],fontsize=16,loc='upper right')(Sex_Churn.iloc[0,1]/Total*100))
性别、年龄分布
#伴侣,子女
fig,ax=plt.subplots(1,2,figsize=(15, 6))

Pivot=[Partner_Churn,Dependents_Churn]
Title=['流失与是否有伴侣的关系','流失与是否有子女的关系']
Label=[['无','有'],['无','有']]
lim=[[0,3500],[0,3800]]
fontsize=15

for i in range(2):
    Pivot[i].plot.bar(title=Title[i],ax=ax[i])
    Total=Pivot[i].sum().sum()
    ax[i].text(x=0,y=Pivot[i].iloc[0,0]+30,s='%.2f %%' % (Pivot[i].iloc[0,0]/Pivot[i].iloc[0,:].sum()*100),fontsize=fontsize,horizontalalignment='right')
    ax[i].text(x=0,y=Pivot[i].iloc[0,1]+30,s='%.2f %%' % (Pivot[i].iloc[0,1]/Pivot[i].iloc[0,:].sum()*100),fontsize=fontsize,horizontalalignment='left')
    ax[i].text(x=1,y=Pivot[i].iloc[1,0]+30,s='%.2f %%' % (Pivot[i].iloc[1,0]/Pivot[i].iloc[1,:].sum()*100),fontsize=fontsize,horizontalalignment='right')
    ax[i].text(x=1,y=Pivot[i].iloc[1,1]+30,s='%.2f %%' % (Pivot[i].iloc[1,1]/Pivot[i].iloc[1,:].sum()*100),fontsize=fontsize,horizontalalignment='left')

    ax[i].set_xticklabels(Label[i],rotation=0)
    ax[i].set_ylim(lim[i])
    ax[i].legend(['未流失','流失'],fontsize=16,loc='upper right')(Sex_Churn.iloc[0,1]/Total*100))
是否有伴侣、子女
#已使用月份
fig,ax=plt.subplots(1,2,figsize=(15, 6))
Telco_Data[Telco_Data.Churn==0].tenure.hist(bins=20,ax=ax[0],density=True,color='#054E9F',alpha=0.6,label='数量分布')
sns.kdeplot(Telco_Data[Telco_Data.Churn==0].tenure,shade=True,color='Red',label='kde',legend='kde',ax=ax[0])
ax[0].set_xlim([-5,75]);ax[0].set_title('用户使用月份分布(未流失)')

Telco_Data[Telco_Data.Churn==1].tenure.hist(bins=20,ax=ax[1],density=True,color='#054E9F',alpha=0.6,label='数量分布')
sns.kdeplot(Telco_Data[Telco_Data.Churn==1].tenure,shade=True,color='Red',label='kde',legend='kde',ax=ax[1])
ax[1].set_xlim([-5,75]);ax[1].set_title('用户使用月份分布(流失)')
用户使用月份分布

可以看到,在用户类型上:

    1. 性别对于用户流失没有显著差异,不同性别的流失率与整体流失率几乎没有区别
    1. 年轻用户占比高于年长用户,前者占总数比例84%,后者占比16%,年长用户更容易发生流失,约41.7%的年长用户流失。
    1. 有伴侣/子女的用户流失率比没有伴侣/子女的用户更低
    1. 使用服务时间上,未流失用户集中在两端,新开通服务的用户以及长期用户占比较高;流失用户比较集中,流失主要发生在开通服务后的半年内,随后流失比例趋于稳定。这是符合电信服务的特点的,人们一般不会频繁更换运营商。

2.4 服务属性分析

2.4.1 是否使用电话/网络服务

数据透视、可视化步骤与上面相近,这里不再赘述,可得

网络与电话服务
  • 绝大多数的用户开通了电话服务,只有10%的用户没有开通,开不开通电话服务流失率都在25%左右,对比整体流失率26.51%,可以说明是否使用电话服务对于流失没有显著影响
  • 未开通网络服务的用户约有2成,未开通网络服务的用户流失率比开通的要低得多,约为7.4%;开通网络服务的用户中,使用光纤上网的用户流失率较高,约为42%,这说明,可能是网络服务对用户流失造成较大的影响。

2.4.2 细分服务选项

细分服务影响

图中第1项为电话服务的细分服务,第2-7项为网络服务的细分服务,图上标注了每项服务内流失/留存用户占总用户比例,可以看到:

  • 对于电话服务,开通子服务MultipleLines的用户流失率为28.61%,开通电话服务的用户流失率为26.71%,并没有显著差异
  • 对于网络服务,保障类服务(OnlineSecurity,TechSupport,OnlineBackup,DeviceProtection)的流失率要低于娱乐类服务(StreamingTV,StreamingMovies)

下面进一步观察DSL与Fiber Optic两种上网方式下,各类网络服务对流失率的影响。


各类网络服务的影响
  • 各种细分服务下,光纤上网用户的流失率都要高于DSL上网,这说明是运营商提供的光纤上网服务引起了用户不满,并造成了大量流失,而非光纤上网下某一子服务造成的。
  • 另外,保障性服务确实能够降低用户流失率。

2.5 合同信息分析

2.5.1 支付方式分析

支付期限的影响
  • 选择月度套餐每月支付的用户流失率要高于购买一年/两年套餐的用户,说明鼓励用户订购长期套餐有助于维持用户留存。
    支付方式的影响
  • 支付方式对于用户流失有着较大影响,选用电子支票的用户流失率高达45.29%。
  • 账单是否纸质化也有着相对大的影响,采用纸质化账单的用户流失率比非纸质化账单的要低。
  • 这两张图说明,电子支付流失率要高于纸质支付方式,推测原因是用户自身而非支付方式的原因。

2.5.2 支付金额分析

#总缴费分布
fig,ax=plt.subplots(1,2,figsize=(15, 6))
sns.violinplot(x='Churn',y='TotalCharges',data=Telco_Data,showmeans=False,showmedians=True,ax=ax[0])
ax[0].set_title('总缴费分布')
ax[0].set_xticklabels(['未流失','流失'],rotation=0)
sns.violinplot(x='Churn',y='MonthlyCharges',data=Telco_Data,showmeans=False,showmedians=True,ax=ax[1])
ax[1].set_title('月度缴费分布')
ax[1].set_xticklabels(['未流失','流失'],rotation=0)
总缴费与月度缴费用户分布提琴图
fig,ax=plt.subplots(1,2,figsize=(15, 6))
qparts=20
#总缴费
Telco_Data_Cut=pd.cut(Telco_Data.TotalCharges,bins=qparts)
Telco_Data_Cut=pd.concat([Telco_Data_Cut,Telco_Data.Churn],axis=1)
(Telco_Data_Cut.groupby('TotalCharges').sum()/Telco_Data_Cut.groupby('TotalCharges').count()).plot.bar(ax=ax[0])
#月缴费
Telco_Data_Cut=pd.cut(Telco_Data.MonthlyCharges,bins=qparts)
Telco_Data_Cut=pd.concat([Telco_Data_Cut,Telco_Data.Churn],axis=1)
(Telco_Data_Cut.groupby('MonthlyCharges').sum()/Telco_Data_Cut.groupby('MonthlyCharges').count()).plot.bar(ax=ax[1])
不同缴费用户的流失率分布图
  • 对于月度缴费而言,流失用户集中在三个区域内,分别是a.20左右;b.45-55;c.75-100。流失率最高的两个区间为28-48,68-109
  • 对于总缴费,无论流失与否,用户数量都随着总缴费数量逐渐减少。流失率随着总缴费额减少而呈现减少趋势。

2.6 小结

这一部分主要分析了用户流失情况,总体流失率为26.25%,分别从1.用户类型、2.服务属性、3.合同信息(支付方式、支付金额)这三个角度分析,发现:

  • 用户类型上,年龄、是否有子女伴侣对流失率有较大影响,年长的、没有子女伴侣的用户是高流失群体。
  • 服务属性上,使用光纤上网网络服务的用户更容易流失,网络服务下的子服务也对流失有影响,保障类服务能降低流失率,而娱乐类服务会导致流失率增加,可能是与用户的预期不符造成。
  • 支付方式上,签订长期合同的用户不易流失,两年期>一年期>每月支付的合同形式;电子支付,采用无纸化账单的用户更容易流失
  • 支付金额上,从月度缴费上看,流失用户集中在三个区域内,分别是a.20左右;b.45-55;c.75-100。流失率最高的两个区间为28-48,68-109

3、 用户价值分析

3.1 用户缴费金额分布

fig,ax=plt.subplots(1,2,figsize=(15, 6))
ax[0].scatter(x=Telco_Data.tenure,y=Telco_Data.TotalCharges)
ax[0].set_xlabel('用户使用月份');ax[0].set_ylabel('用户总缴费')
ax[1].scatter(x=Telco_Data.MonthlyCharges,y=Telco_Data.TotalCharges/Telco_Data.tenure)
ax[1].set_xlabel('用户月度缴费');ax[1].set_ylabel('用户总缴费/用户使用月份')
用户缴费金额分布
  • 金额上,总缴费均摊到每月的费用与月度缴费基本落在一条直线上,说明每名用户的套餐金额随时间并没有发生太大的变化。

3.2 累计缴费金额分布

fig,ax=plt.subplots(1,2,figsize=(15, 6))
ax[0].set_xlabel('用户排名');ax[0].set_ylabel('用户月度缴费占比')
(Telco_Data.MonthlyCharges.sort_values(ascending=True).cumsum()/\
(Telco_Data.MonthlyCharges.sort_values(ascending=True).cumsum()).max()).reset_index(drop=True).\
plot(ax=ax[0])

ax[1].set_xlabel('用户排名');ax[1].set_ylabel('用户总缴费占比')
(Telco_Data.TotalCharges.sort_values(ascending=True).cumsum()/\
Telco_Data.TotalCharges.sort_values(ascending=True).cumsum()).max()).reset_index(drop=True)\
.plot(ax=ax[1])
用户累计消费图

对用户月度缴费、总缴费进行排序,并做出累计曲线,可以发现:

  • 总缴费曲线变化比月度缴费曲线变化更为明显,前期更为平坦,后期拉升更为剧烈。
  • 从月度付费来看,曲线变化并不明显,缴费金额前1000(14.3%)的用户对营业额贡献大约在23%左右,从总缴费来看,这一数值达到40%,这说明用户的价值往往依靠长期稳定的付费,这也符合电信行业的特点。
  • 这也说明降低流失率,比拉新,引导用户提升缴费金额(升级套餐) 更具有效益

3.3 用户终身价值(LTV)分析

按照已使用月份tenure作为分组依据(tenure=0的归入1进行计算),分别计算各组的流失率,月度缴费均值ARPU以及历史缴费总额。用户剩余价值=ARPU/(1-流失率)

Telco_Data_LTV=Telco_Data.copy()
IDXtelco=Telco_Data_LTV.loc[Telco_Data_LTV.tenure==0].index
Telco_Data_LTV.loc[IDXtelco,'tenure']=1
Telco_Data_LTV=Telco_Data_LTV.groupby(by='tenure').agg({'Churn':'mean','customerID':'count','MonthlyCharges':'mean','TotalCharges':'mean'})
Telco_Data_LTV.columns=['ChurnRate','CustomerNum','MonthlyChargesAvg','TotalChargesAvg']
Telco_Data_LTV['RemaingCharges']=Telco_Data_LTV['MonthlyChargesAvg']/Telco_Data_LTV['ChurnRate']
fig,ax=plt.subplots(2,2,figsize=(15,15))
Telco_Data_LTV.ChurnRate.plot(ax=ax[0,0],title='流失率ChurnRate')
Telco_Data_LTV.MonthlyChargesAvg.plot(ax=ax[0,1],title='人均月缴费MonthlyChargesAvg')
Telco_Data_LTV.TotalChargesAvg.plot(ax=ax[1,0],title='人均总缴费TotalChargesAvg')
Telco_Data_LTV.RemaingCharges.plot(ax=ax[1,1],title='人均剩余价值RemaingCharges ')
流失率、用户价值随已使用时间的变化
Telco_Data_LTV['LTV']=Telco_Data_LTV['TotalChargesAvg']+Telco_Data_LTV['RemaingCharges']
Telco_Data_LTV['LTV'].plot(title='用户生命周期总价值LTV',figsize=(7,5))
#流失率ChurnRate,CustomerNum,人均月缴费MonthlyChargesAvg,人均总缴费TotalChargesAvg,人均剩余价值RemaingCharges 
#LTV=TotalChargesAvg+RemaingCharges
用户生命周期总价值
  • 使用服务时长越长,用户生命周期总价值越高,这是符合我们认知的。需要特别注意的是,使用服务时长达到72个月的用户生命周期总价值特别高,这是由于他们的流失率极低,可以认为他们是该电信运营商的核心用户。
  • 这说明了,每一位长期用户,都能为运营商带来稳定持续的收益。电信行业更应当关注用户流失,培养长期用户。如果能够预测用户流失,针对流失用户制定针对性对策,将带来持久的收益提升。

4.留存率预测

4.1 特征工程

4.1.1 特征提取与编码

#对于离散特征,采用One-Hot编码
ConvertFeatures=['MultipleLines','OnlineSecurity','DeviceProtection','InternetService','OnlineBackup',
                 'TechSupport','StreamingTV','StreamingMovies','Contract','PaymentMethod']
Telco_DT=Telco_Data.copy()
for i in ConvertFeatures:
    Telco_DT[i]=pd.factorize(Telco_DT[i])[0]
    Dummy=pd.get_dummies(Telco_DT[i],prefix=i)
    Telco_DT=pd.concat([Telco_DT,Dummy],axis=1)
#对于连续特征,采用标准化方式处理
from sklearn import preprocessing 
ConvertNumericalFeatures=['tenure','MonthlyCharges','TotalCharges']
scaler = preprocessing.StandardScaler().fit(Telco_DT[ConvertNumericalFeatures])
Telco_DT[ConvertNumericalFeatures]=scaler.transform(Telco_DT[ConvertNumericalFeatures])

4.1.2 特征相关性分析

colormap = plt.cm.viridis
fontsize=11
plt.rcParams['font.sans-serif']=['SimHei'] 
plt.rcParams.update({'font.size': fontsize})
plt.figure(figsize=(14,12))
plt.title('Pearson Correaltion of Feature',y=1.05,size=15)
sns.heatmap(Telco_DT[Telco_Data.columns].drop('customerID',axis=1).astype(float).corr(),linewidths=0.1,vmax=1.0,square=True,cmap=colormap,linecolor='white',annot=True)
特征间的相关性矩阵

4.1.3 特征间的数据分布

Features=['gender','SeniorCitizen','Partner','Dependents','tenure','PhoneService','InternetService','Contract',
          'PaperlessBilling','PaymentMethod','MonthlyCharges','TotalCharges','Churn']
FeaturePlot = sns.pairplot(Telco_DT[Features],hue='Churn',
                                      palette = 'seismic',size=1.8,diag_kind ='kde',diag_kws=
                                      dict(shade=True),plot_kws=dict(s=10))
FeaturePlot.set(xticklabels=[])
特征间的数据分布

4.2 采用不同模型筛选特征

from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn import ensemble
from sklearn import model_selection
from imblearn.over_sampling import SMOTE

def get_top_n_features(train_data_X, train_data_Y, top_n_features):
    # Random Forest
    rf_est = RandomForestClassifier(random_state=0)
    rf_param_grid = {'n_estimators': [500], 'min_samples_split': [2, 3], 'max_depth': [20]}
    rf_grid = model_selection.GridSearchCV(rf_est, rf_param_grid, n_jobs=-1, cv=10, verbose=1)
    rf_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best RF Params:' + str(rf_grid.best_params_))
    print('Top N Features Best RF Score:' + str(rf_grid.best_score_))
    print('Top N Features RF Train Score:' + str(rf_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_rf = pd.DataFrame({'feature': list(train_data_X),
                                          'importance': rf_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_rf = feature_imp_sorted_rf.head(top_n_features)['feature']
    print('Sample 10 Features from RF Classifier')
    print(str(features_top_n_rf[:10]))

    # AdaBoost
    ada_est =AdaBoostClassifier(random_state=0)
    ada_param_grid = {'n_estimators': [500], 'learning_rate': [0.01, 0.1]}
    ada_grid = model_selection.GridSearchCV(ada_est, ada_param_grid, n_jobs=-1, cv=10, verbose=1)
    ada_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best Ada Params:' + str(ada_grid.best_params_))
    print('Top N Features Best Ada Score:' + str(ada_grid.best_score_))
    print('Top N Features Ada Train Score:' + str(ada_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_ada = pd.DataFrame({'feature': list(train_data_X),
                                           'importance': ada_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_ada = feature_imp_sorted_ada.head(top_n_features)['feature']
    print('Sample 10 Feature from Ada Classifier:')
    print(str(features_top_n_ada[:10]))

    # ExtraTree
    et_est = ExtraTreesClassifier(random_state=0)
    et_param_grid = {'n_estimators': [500], 'min_samples_split': [3, 4], 'max_depth': [20]}
    et_grid = model_selection.GridSearchCV(et_est, et_param_grid, n_jobs=-1, cv=10, verbose=1)
    et_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best ET Params:' + str(et_grid.best_params_))
    print('Top N Features Best ET Score:' + str(et_grid.best_score_))
    print('Top N Features ET Train Score:' + str(et_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_et = pd.DataFrame({'feature': list(train_data_X),
                                          'importance': et_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_et = feature_imp_sorted_et.head(top_n_features)['feature']
    print('Sample 10 Features from ET Classifier:')
    print(str(features_top_n_et[:10]))
    
    # GradientBoosting
    gb_est =GradientBoostingClassifier(random_state=0)
    gb_param_grid = {'n_estimators': [500], 'learning_rate': [0.01, 0.1], 'max_depth': [20]}
    gb_grid = model_selection.GridSearchCV(gb_est, gb_param_grid, n_jobs=-1, cv=10, verbose=1)
    gb_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best GB Params:' + str(gb_grid.best_params_))
    print('Top N Features Best GB Score:' + str(gb_grid.best_score_))
    print('Top N Features GB Train Score:' + str(gb_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_gb = pd.DataFrame({'feature': list(train_data_X),
                                           'importance': gb_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_gb = feature_imp_sorted_gb.head(top_n_features)['feature']
    print('Sample 10 Feature from GB Classifier:')
    print(str(features_top_n_gb[:10]))
    
    # DecisionTree
    dt_est = DecisionTreeClassifier(random_state=0)
    dt_param_grid = {'min_samples_split': [2, 4], 'max_depth': [20]}
    dt_grid = model_selection.GridSearchCV(dt_est, dt_param_grid, n_jobs=-1, cv=10, verbose=1)
    dt_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best DT Params:' + str(dt_grid.best_params_))
    print('Top N Features Best DT Score:' + str(dt_grid.best_score_))
    print('Top N Features DT Train Score:' + str(dt_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_dt = pd.DataFrame({'feature': list(train_data_X),
                                          'importance': dt_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_dt = feature_imp_sorted_dt.head(top_n_features)['feature']
    print('Sample 10 Features from DT Classifier:')
    print(str(features_top_n_dt[:10]))
    
    # merge five models
    features_top_n_5mods = pd.concat([features_top_n_rf, features_top_n_ada, features_top_n_et, features_top_n_gb, features_top_n_dt], 
                               ignore_index=True)
    
    features_importance_all = [feature_imp_sorted_rf, feature_imp_sorted_ada, feature_imp_sorted_et, 
                               feature_imp_sorted_gb, feature_imp_sorted_dt]
                                #pd.concat([feature_imp_sorted_rf, feature_imp_sorted_ada, feature_imp_sorted_et, 
                              #     feature_imp_sorted_gb, feature_imp_sorted_dt],ignore_index=True)
    
    return features_top_n_5mods , features_importance_all
#筛选特征
feature_to_pick = 40
TestTelcoX=Telco_DT.drop(ConvertFeatures+['customerID']+['Churn'],axis=1).iloc[:,:]
TestTelcoY=Telco_DT['Churn'].iloc[:]
features_top_n_5mods,features_importance_all = get_top_n_features(TestTelcoX,TestTelcoY,feature_to_pick)
  • 采用Random Forest、AdaBoost、Extra Tree、GBDT、Decision Tree五种模型,筛选出每个模型中重要性排名前40的特征。
#共5个模型,每个模型搜集前n=20条特征,去重得到feature_top_n;feature_importance为所有模型特征的重要程度,共5*cols条
#下面展示每种算法重要性排名前十的属性
N=feature_to_pick;NN=20
RF_imp_idx=features_top_n_5mods[0:NN];RF_imp=features_importance_all[0].set_index('feature',drop=True).loc[RF_imp_idx,:]
ADA_imp_idx=features_top_n_5mods[N:N+NN];ADA_imp=features_importance_all[1].set_index('feature',drop=True).loc[ADA_imp_idx,:]
ET_imp_idx=features_top_n_5mods[N*2:N*2+NN];ET_imp=features_importance_all[2].set_index('feature',drop=True).loc[ET_imp_idx,:]
GB_imp_idx=features_top_n_5mods[N*3:N*3+NN];GB_imp=features_importance_all[3].set_index('feature',drop=True).loc[GB_imp_idx,:]
DT_imp_idx=features_top_n_5mods[N*4:N*4+NN];DT_imp=features_importance_all[4].set_index('feature',drop=True).loc[DT_imp_idx,:]

warnings.filterwarnings("ignore")
fig,ax=plt.subplots(3,2,figsize=(18,15))
RF_imp.sort_values(by='importance').plot.barh(ax=ax[0,0],title='Random Forest')
ADA_imp.sort_values(by='importance').plot.barh(ax=ax[0,1],title='Adaboost')
GB_imp.sort_values(by='importance').plot.barh(ax=ax[1,0],title='GBDT')
ET_imp.sort_values(by='importance').plot.barh(ax=ax[1,1],title='Extra Tree')
DT_imp.sort_values(by='importance').plot.barh(ax=ax[2,0],title='Decision Tree')

#将每个模型的重要特征去重
features_matters=features_top_n_5mods.drop_duplicates()
imp_all=pd.concat(features_importance_all)
imp_all=imp_all.groupby('feature').mean().loc[features_matters,:]
imp_all.sort_values(by='importance').tail(20).plot.barh(ax=ax[2,1],title='All')
各个模型各特征重要性排名
  • 列举在各个模型中重要性排名前20的特征直方图,加权平均得到所有模型的特征排名。
  • 其中,用户总缴费、月度缴费以及已经使用时间这三项重要性最强,毕竟用户留存时间越久,付得钱越多,说明用户粘性越高。合同支付方式、使用哪类网络服务、账单是否纸质化等因素也占有较为重要的地位,基本符合前期的分析结果。

4.3 生成训练集、测试集

#选取前35项作为特征
features_matters=imp_all.sort_values(by='importance',ascending=False).index[0:35].tolist()
train_X,test_X,train_Y,test_Y=model_selection.train_test_split(Telco_DT[features_matters],Telco_DT['Churn'],test_size=0.2)
over_samples=SMOTE(random_state=1234)
train_X,train_Y=over_samples.fit_sample(train_X,train_Y)

#提取数值
x_train = train_X.values 
x_test = test_X.values
y_train = train_Y.values
y_test = test_Y.values
  • 由于数据集阳性与阴性样本非平衡,直接预测时,模型会对阴性样本产生偏好,虽然能保证较高的整体准确率,但召回率较低,在生成训练集与测试集时采用SMOTE过采样算法对数据集进行平衡。
  • 测试集与训练集比例为1:4。

4.4 采用单一模型进行测试

  • 下面4.4.1-4.4.6分别计算了Random Forest、Adaboost、Decision Tree、KNN、Extra Tree、GBDT这六种模型的训练与测试结果
  • 对每个模型,主要步骤包括:通过训练集进行10重验证调节参数、在测试集上对模型进行评估计算准确率(Precision)与召回率(Recall)、作出ROC曲线混淆矩阵

4.4.1 Random Forest

#RF模型调参
rf_est = RandomForestClassifier(warm_start=True,max_features='sqrt',
                            min_samples_split=3,min_samples_leaf=2,n_jobs=-1,verbose=0)
rf_param_grid = {'n_estimators': [700], 'max_depth': [8],'min_samples_split':[10],'min_samples_leaf':[20]}
#n_estimators:[500,600,700,800,900,1000]
#max_depth:[6,8,10,12,15,20]
#min_samples_split:range(10, 90, 20)
#min_samples_leaf:range(5, 65, 10),
rf_grid = model_selection.GridSearchCV(rf_est, rf_param_grid, n_jobs=-1, cv=10, verbose=1,scoring=None)
rf_grid.fit(x_train, y_train)
print('RandomForest 最佳参数',rf_grid.best_params_)
print('RandomForest 最佳得分',rf_grid.best_score_)
随机森林调参结果
#RF模型拟合
rf = RandomForestClassifier(max_depth=8, n_estimators=700,warm_start=False,max_features='auto',min_samples_leaf=20,
                            min_samples_split=10,n_jobs=-1,verbose=0)
rf.fit(x_train,y_train)
y_predict = rf.predict(x_test)
y_predict_proba = rf.predict_proba(x_test)[:,1]
RST=[];AST=[];RST.append(y_predict);AST.append(y_predict_proba)
#拟合结果在训练集上可视化
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(y_predict,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
预测精度
ROC曲线
混淆矩阵

4.4.2 AdaBoost

#Ada模型调参
ada_est=AdaBoostClassifier(n_estimators=100,learning_rate=0.5)
ada_param_grid = {'n_estimators': [100,200,300,400,500,600]}
#n_estimators:[500,600,700,800,900,1000]
ada_grid = model_selection.GridSearchCV(ada_est, ada_param_grid, n_jobs=-1, cv=10, verbose=1,scoring=None)
ada_grid.fit(x_train, y_train)
print('AdaBoost 最佳参数',ada_grid.best_params_)
print('AdaBoost 最佳得分',ada_grid.best_score_)
AdaBoost调参结果
#Ada模型拟合
ada=AdaBoostClassifier(n_estimators=300,learning_rate=0.1)
ada.fit(x_train,y_train)
y_predict = ada.predict(x_test)
y_predict_proba = ada.predict_proba(x_test)[:,1]
RST.append(y_predict);AST.append(y_predict_proba)
#拟合结果在训练集上可视化
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(y_predict,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
预测精度

ROC曲线

混淆矩阵

4.4.3 Decision Tree

#DT模型调参
dt_est = DecisionTreeClassifier()
dt_param_grid = {'max_depth': [5,8,16,20]}
dt_grid = model_selection.GridSearchCV(dt_est, dt_param_grid, n_jobs=-1, cv=10,verbose=1,scoring='recall')
dt_grid.fit(x_train, y_train)
print('DecisionTree 最佳参数',dt_grid.best_params_)
print('DecisionTree 最佳得分',dt_grid.best_score_)
决策树调参结果
#DT模型拟合
dt=DecisionTreeClassifier(max_depth=5)
dt.fit(x_train,y_train)
y_predict = dt.predict(x_test)
y_predict_proba = dt.predict_proba(x_test)[:,1]
RST.append(y_predict);AST.append(y_predict_proba)
#拟合结果在训练集上可视化
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)

预测精度

ROC曲线

混淆矩阵

4.4.4 KNN

KNN模型调参
knn_est = KNeighborsClassifier()
knn_param_grid = {'n_neighbors': [100,200,300]}
knn_grid = model_selection.GridSearchCV(knn_est, knn_param_grid, n_jobs=-1, cv=10,verbose=1,scoring=None)
knn_grid.fit(x_train, y_train)
print('knn 最佳参数',knn_grid.best_params_)
print('knn 最佳得分',knn_grid.best_score_)
KNN调参结果
#KNN模型拟合
knn=KNeighborsClassifier(n_neighbors=100)
knn.fit(x_train,y_train)
y_predict = knn.predict(x_test)
y_predict_proba = knn.predict_proba(x_test)[:,1]
RST.append(y_predict);AST.append(y_predict_proba)
#拟合结果在训练集上可视化
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(y_predict,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
预测精度

ROC曲线

混淆矩阵

4.4.5 Extra Tree

#ET模型调参
et_est=ExtraTreesClassifier()
et_param_grid = {'n_estimators': [600], 'max_depth': [8],'min_samples_leaf':[5],'min_samples_split':[10]}
#n_estimators:[500,600,700,800,900,1000]
#max_depth:[6,8,10,12,15,20]
#min_samples_split:range(10, 90, 20)
#min_samples_leaf:range(10, 60, 10),
et_grid = model_selection.GridSearchCV(et_est, et_param_grid, n_jobs=-1, cv=10,verbose=1,scoring=None)
et_grid.fit(x_train, y_train)
print('ExtraTree 最佳参数',et_grid.best_params_)
print('ExtraTree 最佳得分',et_grid.best_score_)
Extra Tree调参结果
#ET模型拟合
et=ExtraTreesClassifier(n_estimators=600,max_depth=8,min_samples_leaf=10,min_samples_split=20)
et.fit(x_train,y_train)
y_predict = et.predict(x_test)
y_predict_proba = et.predict_proba(x_test)[:,1]
RST.append(y_predict);AST.append(y_predict_proba)
#拟合结果在训练集上可视化
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(y_predict,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
预测精度

ROC曲线

混淆矩阵

4.4.6 GBDT

#GBDT模型调参
gb_est=ExtraTreesClassifier()
gb_param_grid = {'n_estimators': [100], 'max_depth': [5],'min_samples_leaf':[10],'min_samples_split':[20]}
#n_estimators:[500,600,700,800,900,1000]
#max_depth:[6,8,10,12,15,20]
#min_samples_split:range(10, 90, 20)
#min_samples_leaf:range(10, 60, 10),
gb_grid = model_selection.GridSearchCV(gb_est, gb_param_grid, n_jobs=-1, cv=10,verbose=1,scoring=None)
gb_grid.fit(x_train, y_train)
print('ExtraTree 最佳参数',gb_grid.best_params_)
print('ExtraTree 最佳得分',gb_grid.best_score_)
GBDT调参结果
#gb模型拟合
gb = GradientBoostingClassifier(n_estimators=100,learning_rate=0.008,min_samples_split=20,min_samples_leaf=10,max_depth=5,verbose=0)
gb.fit(x_train,y_train)
y_predict = gb.predict(x_test)
y_predict_proba = gb.predict_proba(x_test)[:,1]
RST.append(y_predict);AST.append(y_predict_proba)
#拟合结果在训练集上可视化
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(y_predict,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
模型精度

ROC曲线

混淆矩阵

4.5 模型融合测试

  • 采用两种模型融合策略,Voting与Stacking分别进行测试。

4.4.1 Voting

AVG=np.zeros((len(RST[0])))
AVG_Pred=np.zeros((len(AST[0])))
#对前面几种模型的结果进行加权平均
for i in range(len(RST)):
    for j in range(len(RST[i])):
        AVG[j]=AVG[j]+RST[i][j]/6
        AVG_Pred[j]=AVG_Pred[j]+AST[i][j]/6
for j in range(len(AVG)):
    if(AVG[j]>0.5):
        AVG[j]=1.0
    else:
        AVG[j]=0.0
#可视化
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,AVG))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,AVG))
fpr,tpr,threshold=metrics.roc_curve(y_test,AVG_Pred)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(AVG,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
Voting模型精度

ROC曲线

混淆矩阵

4.4.2 Stacking-LR

  • 第一层采用Random Forest,Adaboost,KNeighbors,Decision Tree,GBDT五种模型,对每个学习器进行K-fold交叉验证,将验证集的结果拼凑,作为下一层的输入。
  • 第二层使用LR模型,将第一层的预测结果作为特征进行学习。
#这里的方法借鉴了[http://blog.csdn.net/koala_tree](http://blog.csdn.net/koala_tree)
from sklearn.model_selection import KFold
#K重验证参数
ntrain = train_X.shape[0]
ntest = test_X.shape[0]
SEED = 0 #for reproducibility
NFOLDS = 7 # set folds for out-of-fold prediction
kf = KFold(n_splits = NFOLDS,random_state=SEED,shuffle=False)
 
def get_out_fold(clf,x_train,y_train,x_test):
    oof_train = np.zeros((ntrain,))
    oof_test = np.zeros((ntest,))
    oof_test_skf = np.empty((NFOLDS,ntest))
    
    for i, (train_index,test_index) in enumerate(kf.split(x_train)):
        x_tr = x_train[train_index]
        y_tr = y_train[train_index]
        x_te = x_train[test_index]
        
        clf.fit(x_tr,y_tr)
        
        oof_train[test_index] = clf.predict_proba(x_te)[:,1]
        oof_test_skf[i,:] = clf.predict_proba(x_test)[:,1]
        
    oof_test[:] = oof_test_skf.mean(axis=0)
    return oof_train.reshape(-1,1),oof_test.reshape(-1,1)
#第一层训练
#得出第一层的结果、第二层输入
ada_oof_train,ada_oof_test = get_out_fold(ada,x_train,y_train,x_test) #Ada
rf_oof_train,rf_oof_test = get_out_fold(rf,x_train,y_train,x_test)  # Random Forest
dt_oof_train,dt_oof_test = get_out_fold(dt,x_train,y_train,x_test)  # DT
knn_oof_train,knn_oof_test = get_out_fold(knn,x_train,y_train,x_test)  # KNeighbors
et_oof_train,et_oof_test = get_out_fold(et,x_train,y_train,x_test)  # ET
gb_oof_train,gb_oof_test = get_out_fold(gb,x_train,y_train,x_test)  # GB
#第二层训练
x_train_2 = np.concatenate((rf_oof_train,ada_oof_train,knn_oof_train,dt_oof_train,gb_oof_train),axis=1)
x_test_2 = np.concatenate((rf_oof_test,ada_oof_test,knn_oof_test,dt_oof_test,gb_oof_test),axis=1)

lr = LogisticRegression(tol=0.00001, C=0.1, random_state=1234, max_iter=20,solver='liblinear',class_weight=None,penalty='l1')
lr.fit(x_train_2, y_train)
y_predict = lr.predict(x_test_2)
y_predict_proba = lr.predict_proba(x_test_2)[:,1]
#可视化结果
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(y_predict,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
Stacking模型精度

ROC曲线

混淆矩阵

4.4.3 Stacking-Xgboost

  • 再尝试采用Xgboost模型作为第二层学习器
#第二层训练集
x_train_2 = np.concatenate((rf_oof_train,ada_oof_train,knn_oof_train,dt_oof_train,et_oof_train,gb_oof_train),axis=1)
x_test_2 = np.concatenate((rf_oof_test,ada_oof_test,knn_oof_test,dt_oof_test,et_oof_test,gb_oof_test),axis=1)
#x_train = np.concatenate((rf_oof_train,ada_oof_train,et_oof_train,gb_oof_train,dt_oof_train,knn_oof_train,svm_oof_train),axis=1)
#x_test =np.concatenate((rf_oof_test,ada_oof_test,et_oof_test,gb_oof_test,dt_oof_test,knn_oof_test,svm_oof_test),axis=1)
from xgboost import XGBClassifier,XGBRegressor

#xgboost调参
gbm_est = XGBClassifier(min_child_weight=3,gamma=0.9,subsample=0.8,
                    colsample_bytree=0.8,objective='binary:logistic',nthread=-1,scale_pos_weight=1)
gbm_param_grid = {'n_estimators': [50], 'max_depth': [6],'min_child_weight':[2]}
gbm_grid = model_selection.GridSearchCV(gbm_est, gbm_param_grid, n_jobs=-1, cv=5, verbose=1,scoring='recall')
gbm_grid.fit(x_train_2, y_train)
print('模型最佳得分:\n',gbm_grid.best_score_)
print('模型最佳参数:\n',gbm_grid.best_params_)
#xgboost训练
gbm = XGBClassifier(**gbm_grid.best_params_,gamma=0.0,subsample=1.0,
                    colsample_bytree=0.8,objective='binary:logistic',nthread=-1,scale_pos_weight=1).fit(x_train,y_train)
gbm.fit(x_train_2,y_train)
y_predict = gbm.predict(x_test_2)
y_predict_proba = gbm.predict_proba(x_test_2)[:,1]
#可视化结果
from sklearn import metrics
print('模型在测试集的预测准确率:\n',metrics.accuracy_score(y_test,y_predict))
print('模型在测试集的预测召回率:\n',metrics.recall_score(y_test,y_predict))
fpr,tpr,threshold=metrics.roc_curve(y_test,y_predict_proba)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,alpha=0.5,edgecolor='black',color='steelblue')
plt.plot(fpr,tpr,lw=1,color='black')
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.text(x=0.5,y=0.3, s="ROC curve (area=%0.2f)" % roc_auc)
#混淆矩阵
cm=pd.crosstab(y_predict,y_test)#=metrics.confusion_matrix(y_test,y_predict)
sns.heatmap(cm,annot=True,cmap='GnBu',fmt='d')
plt.xlabel('Real')
plt.ylabel('Predict')
plt.show()
模型精度

ROC曲线

混淆矩阵

4.6 预测部分总结

4.4,4.5节分别采用单模型与多模型融合的方式对用户流失进行分类,主要内容为

  • 特征选择:选择重要性排名前35位的特征进行建模。
  • 模型选择:分别采用RandomForest,AdaBoost,DecisionTree,KNN,ExtraTree,GBDT等模型进行预测。
  • 模型优化目标:考虑到问题重点关注流失率,应该尽可能找出所有流失的用户制定关怀政策,因此,在预测时应当提升模型的召回率。注:样本中流失用户仅占1/4,首先应当采用SMOTE算法对数据集进行平衡改进。
  • 单模型预报结果:对模型分别调参后,得到单模型AUC值分别为0.86,0.86,0.83,0.85,0.84,0.85,说明这些模型都能够较好地预测出客户流失,RandomForest,AdaBoost与KNN在测试集上召回率超过80%。
  • 模型融合:考虑到单个学习器未必能够获得稳定的预测结果,进一步采用了模型融合进行研究,分别采用voting,stacking这两种策略进行融合,stacking策略下分别采用LR回归与Xgboost作为二级学习器建模。其中,Xgboost-Stacking策略表现较差,AUC仅为0.81,voting与LR-stacking的AUC值均为0.85,召回率分别为0.8275,0.805。
  • 结合实际问题,推荐使用模型为RandomForest,AdaBoost或Voting模型

5 总结与建议

总结:

  • 通过分析发现,高流失用户表现出以下几个特征:无伴侣或子女,年长,使用光纤上网服务,附加娱乐性服务而非保障性服务,选择月度付费而非年度付费,采用线上支付方式,电子账单,使用时间不足半年的新用户。
  • 通过数据挖掘,得到了多个分类预测模型,AUC值达到0.85。受非平衡数据集的影响,模型在测试集上的准确率不高,但召回率在80%以上,能够覆盖绝大多数的流失用户。

建议:

  • 用户类型上,针对年长的、没有伴侣、子女的用户可以推出相应的优惠套餐或在一定期限内提供礼品等优惠活动;可以进一步对支付金额钻取,计算各类服务、各年龄段下各种支付金额用户的流失率,研究是否是不同类型的用户对不同服务的价格敏感性不同
  • 网络服务上,运营商应当进一步调研光纤服务,可以从两个方面入手:a.服务质量问题 b.用户对于光纤服务的价格满意度;提供网络服务时,可以免费提供一些保障性的增值服务,以此提升用户留存;针对娱乐性服务,同样需要进行a,b两方面调研。
  • 合同与支付方式上,应当鼓励用户签订长期合同,适当推出一年期、两年期的优惠套餐、附赠娱乐性或保障性增值服务,提升用户粘性;建议对采用电子支付,无纸质化账单的用户进行进一步挖掘。考虑到电子支付的发展趋势,这些用户的流失可能不是支付方式导致的,需要挖掘这些用户的共同特点,进行引导。
  • 通过预测模型,将流失可能性较高的用户单独管理,制定更有针对性的个性化套餐服务,将他们培养成具有较高粘性的长期用户。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345