Kaggle系列(3)- Telco Customer Churn

电信用户流失分析与预测,涉及到模型选择与参数调优。

0x01、简介

这是来自IBM的样本数据集,记录了用户订购的服务、账户信息及人口学特征。希望通过预测用户行为来留住用户,可以分析所有相关的用户数据,开发出有针对性的留存方法。

每一行代表一个用户,每一列包括了用户的属性。数据集包括以下信息:

  • Churn:表示在最后一个月流失的用户。
  • 每个用户注册的服务,包括:phone(电话服务), multiple lines(多条线路), internet(网络服务), online security(在线安全服务), online backup(在线备份服务), device protection(设备防护服务), tech support(技术支持服务), and streaming TV and movies(流TV及电影)。
  • 用户账户信息:在网时间,contract(付费周期), payment method(付费方式), paperless billing(无纸化账单), monthly charges(月消费), and total charges(总消费)。
  • 用户的人口学特征:gender(性别), age range(年龄范围), and if they have partners and dependents(是否有伴侣及子女)。

Kaggle上的解释不是很完整,在参考资料2中,给出了更多的用户信息及其解释,结合这份数据,每个字段的解释如下:

字段名称说明
customerID用户的唯一标识
gender性别
SeniorCitizen是否65岁以上老人
Partner是否有伴侣
Dependents是否有被抚养人(孩子、父母等)
tenure入网月数
PhoneService订购家庭电话服务
MultipleLines订购多条电话线路
InternetService订购网络服务
OnlineSecurity订购附加的在线安全服务
OnlineBackup订购附加的在线备份服务
DeviceProtection为公司提供的网络设备购买附加的设备保护服务
TechSupport订购附加的技术支持以缩短等待时间
StreamingTV使用第三方的流TV(不额外收费)
StreamingMovies使用第三方的流电影(不额外收费)
Contract当前合约类型
PaperlessBilling是否使用无纸化账单
PaymentMethod付款方式
MonthlyCharges当前的包含所有服务的月总费用
TotalCharges入网至今的总费用
Churn是否流失

0x02、数据探索

2.1 文件信息总览

读取文件,可以看到:

  • 共有7043行,21列数据;
  • 大部分字段都是 object 类型,应该是字符串格式的;
  • 内存占用7.8MB,对于这个数据量来说是挺大的,后面可能需要优化。
import pandas as pd
import numpy as np
import time 

df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
df.info(memory_usage='deep')  # deep参数可以显示准确的内存占用


########## 结果 ##########
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 7.8 MB

看一下具体的数据,形成总体印象:

df.head()


########## 结果 ##########
customerID	gender	SeniorCitizen	Partner	Dependents	tenure	PhoneService	MultipleLines	InternetService	OnlineSecurity	...	DeviceProtection	TechSupport	StreamingTV	StreamingMovies	Contract	PaperlessBilling	PaymentMethod	MonthlyCharges	TotalCharges	Churn
0	7590-VHVEG	Female	0	Yes	No	1	No	No phone service	DSL	No	...	No	No	No	No	Month-to-month	Yes	Electronic check	29.85	29.85	No
1	5575-GNVDE	Male	0	No	No	34	Yes	No	DSL	Yes	...	Yes	No	No	No	One year	No	Mailed check	56.95	1889.5	No
2	3668-QPYBK	Male	0	No	No	2	Yes	No	DSL	Yes	...	No	No	No	No	Month-to-month	Yes	Mailed check	53.85	108.15	Yes
3	7795-CFOCW	Male	0	No	No	45	No	No phone service	DSL	Yes	...	Yes	Yes	No	No	One year	No	Bank transfer (automatic)	42.30	1840.75	No
4	9237-HQITU	Female	0	No	No	2	Yes	No	Fiber optic	No	...	No	No	No	No	Month-to-month	Yes	Electronic check	70.70	151.65	Yes
5 rows × 21 columns

2.2 数据处理

2.2.1 数据分布

1、看一下各个字段唯一值的个数

可以看到,除了 [‘customerID’,‘tenure’,‘MonthlyCharges’,‘TotalCharges’] 这四个字段外的其他字段均只有2~3个唯一值,那么可以分别统计一下唯一值的分布。

# 看一下 有多少个不同的值
print(df.agg({pd.Series.nunique}))


########## 总结 ##########
         customerID  gender  SeniorCitizen  Partner  Dependents  tenure  \
nunique        7043       2              2        2           2      73   

         PhoneService  MultipleLines  InternetService  OnlineSecurity  ...  \
nunique             2              3                3               3  ...   

         DeviceProtection  TechSupport  StreamingTV  StreamingMovies  \
nunique                 3            3            3                3   

         Contract  PaperlessBilling  PaymentMethod  MonthlyCharges  \
nunique         3                 2              4            1585   

         TotalCharges  Churn  
nunique          6531      2  

[1 rows x 21 columns]

2、各值的分布

# 看一下数据分布
col_number = ['customerID','tenure','MonthlyCharges','TotalCharges']

for col in df.columns.values:
    if col not in col_number:
        print('列名: {}\n{}\n{}\n'.format(col, '-'*20, df[col].value_counts()))


########## 结果 ##########
列名: gender
--------------------
Male      3555
Female    3488
Name: gender, dtype: int64

列名: SeniorCitizen
--------------------
0    5901
1    1142
Name: SeniorCitizen, dtype: int64

列名: Partner
--------------------
No     3641
Yes    3402
Name: Partner, dtype: int64

列名: Dependents
--------------------
No     4933
Yes    2110
Name: Dependents, dtype: int64

列名: PhoneService
--------------------
Yes    6361
No      682
Name: PhoneService, dtype: int64

列名: MultipleLines
--------------------
No                  3390
Yes                 2971
No phone service     682
Name: MultipleLines, dtype: int64

列名: InternetService
--------------------
Fiber optic    3096
DSL            2421
No             1526
Name: InternetService, dtype: int64

列名: OnlineSecurity
--------------------
No                     3498
Yes                    2019
No internet service    1526
Name: OnlineSecurity, dtype: int64

列名: OnlineBackup
--------------------
No                     3088
Yes                    2429
No internet service    1526
Name: OnlineBackup, dtype: int64

列名: DeviceProtection
--------------------
No                     3095
Yes                    2422
No internet service    1526
Name: DeviceProtection, dtype: int64

列名: TechSupport
--------------------
No                     3473
Yes                    2044
No internet service    1526
Name: TechSupport, dtype: int64

列名: StreamingTV
--------------------
No                     2810
Yes                    2707
No internet service    1526
Name: StreamingTV, dtype: int64

列名: StreamingMovies
--------------------
No                     2785
Yes                    2732
No internet service    1526
Name: StreamingMovies, dtype: int64

列名: Contract
--------------------
Month-to-month    3875
Two year          1695
One year          1473
Name: Contract, dtype: int64

列名: PaperlessBilling
--------------------
Yes    4171
No     2872
Name: PaperlessBilling, dtype: int64

列名: PaymentMethod
--------------------
Electronic check             2365
Mailed check                 1612
Bank transfer (automatic)    1544
Credit card (automatic)      1522
Name: PaymentMethod, dtype: int64

列名: Churn
--------------------
No     5174
Yes    1869
Name: Churn, dtype: int64

也可以把统计数据都合并到一张表中:

tmp_unique = pd.DataFrame(columns=['sub_value', 'sub_num', 'column_name'])

for cc in df.columns.values:
    if cc not in col_number:
        tmp_df = df.groupby(cc, as_index=False).agg({'customerID':pd.Series.nunique})
        tmp_df['column_name'] = cc
        tmp_df.columns = ['sub_value','sub_num', 'column_name']
        tmp_unique = pd.concat([tmp_unique, tmp_df], axis=0)

tmp_unique = tmp_unique[['column_name','sub_value','sub_num']]
print(tmp_unique)


########## 结果 ##########
        column_name                  sub_value sub_num
0            gender                     Female    3488
1            gender                       Male    3555
0     SeniorCitizen                          0    5901
1     SeniorCitizen                          1    1142
0           Partner                         No    3641
1           Partner                        Yes    3402
0        Dependents                         No    4933
1        Dependents                        Yes    2110
0      PhoneService                         No     682
1      PhoneService                        Yes    6361
0     MultipleLines                         No    3390
1     MultipleLines           No phone service     682
2     MultipleLines                        Yes    2971
0   InternetService                        DSL    2421
1   InternetService                Fiber optic    3096
2   InternetService                         No    1526
0    OnlineSecurity                         No    3498
1    OnlineSecurity        No internet service    1526
2    OnlineSecurity                        Yes    2019
0      OnlineBackup                         No    3088
1      OnlineBackup        No internet service    1526
2      OnlineBackup                        Yes    2429
0  DeviceProtection                         No    3095
1  DeviceProtection        No internet service    1526
2  DeviceProtection                        Yes    2422
0       TechSupport                         No    3473
1       TechSupport        No internet service    1526
2       TechSupport                        Yes    2044
0       StreamingTV                         No    2810
1       StreamingTV        No internet service    1526
2       StreamingTV                        Yes    2707
0   StreamingMovies                         No    2785
1   StreamingMovies        No internet service    1526
2   StreamingMovies                        Yes    2732
0          Contract             Month-to-month    3875
1          Contract                   One year    1473
2          Contract                   Two year    1695
0  PaperlessBilling                         No    2872
1  PaperlessBilling                        Yes    4171
0     PaymentMethod  Bank transfer (automatic)    1544
1     PaymentMethod    Credit card (automatic)    1522
2     PaymentMethod           Electronic check    2365
3     PaymentMethod               Mailed check    1612
0             Churn                         No    5174
1             Churn                        Yes    1869

综合以上数据概览情况,可以知道如下的字段信息:

字段名称字段类型说明枚举值
customerIDobject用户的唯一标识
genderobject性别Male,Female
SeniorCitizenint64是否65岁以上老人1,0
Partnerobject是否有伴侣Yes,No
Dependentsobject是否有被抚养人(孩子、父母等)Yes,No
tenureint64入网月数
PhoneServiceobject订购家庭电话服务Yes,No
MultipleLinesobject订购多条电话线路Yes,No
InternetServiceobject订购网络服务Fiber optic,DSL,No
OnlineSecurityobject订购附加的在线安全服务Yes,No,No internet service
OnlineBackupobject订购附加的在线备份服务Yes,No,No internet service
DeviceProtectionobject为公司提供的网络设备购买附加的设备保护服务Yes,No,No internet service
TechSupportobject订购附加的技术支持以缩短等待时间Yes,No,No internet service
StreamingTVobject是否使用第三方的流TV(不额外收费)Yes,No,No internet service
StreamingMoviesobject是否使用第三方的流电影(不额外收费)Yes,No,No internet service
Contractobject当前合约类型Month-to-month,One Year,Two Year
PaperlessBillingobject是否使用无纸化账单Yes,No
PaymentMethodobject用户付款方式Electronic check,Bank transfer (automatic),Credit card (automatic),Mailed check
MonthlyChargesfloat64当前的包含所有服务的月总费用
TotalChargesobject入网至今的总费用
Churnobject是否流失Yes,No

2.2.2 数值型数据处理

TotalCharges 为总消费金额,应将其转换为数值型。但 object 类型转换成 float 类型不能使用 astype(),应该使用 pd.to_numeric() 方法。转换后发现有空值,于是查看一下空值的情况,发现空值都是当月刚入网的用户,应该是还没有产生费用,所以可以将空值置为0。

# 把 TotalCharges 转成数值型 (str类型不能用 astype 转成 float)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')

# 查看是否有空值
print(df['TotalCharges'].isnull().sum())

# 有11行的 TotalCharges 为空,猜测这是指新入网用户还没产生费用? tenure 指的是入网周期
df.loc[df['TotalCharges'].isnull(), ['customerID','tenure','MonthlyCharges','TotalCharges','Churn']]

# 将空值置为0
df['TotalCharges'].fillna(0, inplace=True)


########## 结果 ##########
11

customerID	tenure	MonthlyCharges	TotalCharges	Churn
488	4472-LVYGI	0	52.55	NaN	No
753	3115-CZMZD	0	20.25	NaN	No
936	5709-LVOEQ	0	80.85	NaN	No
1082	4367-NUYAO	0	25.75	NaN	No
1340	1371-DWPAZ	0	56.05	NaN	No
3331	7644-OMVMY	0	19.85	NaN	No
3826	3213-VVOLG	0	25.35	NaN	No
4380	2520-SGTTA	0	20.00	NaN	No
5218	2923-ARZLG	0	19.70	NaN	No
6670	4075-WKNIU	0	73.35	NaN	No
6754	2775-SEFEE	0	61.90	NaN	No

现在可以看一下数据的范围,以便使用较小的数据类型(节省内存)。tenure 可以设置为 int8,其他两个可以设置为 float32。

df[['tenure','MonthlyCharges','TotalCharges']].agg({np.max, np.min, np.mean, pd.Series.std})


########## 结果 ##########

        tenure	MonthlyCharges	TotalCharges
amin	0.000000	18.250000	0.000000
std	24.559481	30.090047	2266.794470
amax	72.000000	118.750000	8684.800000
mean	32.371149	64.761692	2279.734304

2.3 数据关系探索

可以将特征分为三类:服务类(service)、人口学特征(demographic)和 账户信息(account),可以分别从以上几个方面与流失的关系进行分析。

service = ['PhoneService','MultipleLines','InternetService',
           'OnlineSecurity','DeviceProtection','TechSupport',
           'StreamingTV','StreamingMovies',
           'Contract','PaperlessBilling','PaymentMethod']
demographic = ['gender','SeniorCitizen','Partner','Dependents']
account = ['customerID','tenure','MonthlyCharges','TotalCharges','Churn']

2.3.1 入网时间与留存的关系

tenure 有 73 个唯一值,比较少,因此可以把每个值对应的总用户数、流失用户数都列出来,观察趋势。由结果图可以看到,流失曲线基本正常,在 0~6 个月流失曲线很陡,流失率较大;20个月之后逐渐稳定下来。

# 查看入网情况
tmp_df = df.groupby('tenure', as_index=False).agg({'customerID':pd.Series.count})
tmp_df.columns = ['tenure','cnts']

tmp_df2 = df[df['Churn'] == 'Yes'].groupby('tenure', as_index=False).agg({'customerID':pd.Series.count})
tmp_df2.columns = ['tenure','churn_yes']

tmp_df3 = df[df['Churn'] == 'No'].groupby('tenure', as_index=False).agg({'customerID':pd.Series.count})
tmp_df3.columns = ['tenure','churn_no']

tmp_df = tmp_df.merge(tmp_df2, on='tenure', how='left').merge(tmp_df3, on='tenure', how='left')
tmp_df.fillna(0, inplace=True)

# 绘图
s_name = list(tmp_df['tenure'])
s_value1 = list(tmp_df['cnts'])
s_value2 = list(tmp_df['churn_yes'])
s_value3 = list(tmp_df['churn_no'])

from matplotlib import pyplot as plt
fig = plt.figure(figsize=(12,6), facecolor='w')
plt.bar(s_name, s_value1)
plt.plot(s_name, s_value2,'r-')
plt.show()

入网时间与流失的关系

2.3.2 消费金额与流失的关系

使用箱线图,分别观察流失用户的消费分布:

  • 流失用户的总体消费金额比留存用户的低很多,与整体用户相比也处于较低的水平;
  • 流失用户的月消费金额则相对较大,且金额相对比较集中。
## MonthlyCharges 和 TotalCharges 与流失的关系
account_info = ['customerID','tenure','MonthlyCharges','TotalCharges','Churn']

def box_out(col):
    s_value1 = list(df[col])
    s_value2 = list(df.loc[(df['Churn'] == 'Yes'), col])
    s_value3 = list(df.loc[(df['Churn'] == 'No'), col])
    labels = ['num_all','num_yes','num_no']
    
    from matplotlib import pyplot as plt
    fig = plt.figure(figsize=(12,6), facecolor='w')
    plt.boxplot([s_value1, s_value2, s_value3], labels=labels, vert=False, showmeans=True)
    plt.title(col)
    
    plt.savefig('figure\\{}_box.png'.format(col), bbox_inches = 'tight', pad_inches = 0.1)
    exe_text('{}_box.png out'.format(col))

box_out('TotalCharges')
box_out('MonthlyCharges')

总消费与流失的关系
月消费与流失的关系

2.3.3 服务及人口学特征与流失的关系

取总的用户数和流失用户数,并计算流失率。因为要画图的项目太多了,所以就把它们生成png图片,放到同一个文件夹下保存起来。

先列出流失率较大的项目:

  • 流失率差别不大的服务:PhoneSerivce(电话服务)、MultipleLines(多线路)、StreamingTV、StreamingMoives(流媒体服务,无论是否订购,流失率都差不多);
  • 服务类流失率较大的项目:
    • 光纤用户(InterneiService:Fbier optic,41.9%);
    • 未订购在线安全服务(OnlineSecurity:No,41.8%);
    • 未订购在线备份服务(OnlineBackup:No,39.9%);
    • 未订购设备防护服务(DeviceProtection:No,39.1%);
    • 未订购技术支持服务(TechSupport:No,41.6%);
  • 费用类流失率较大的项目:
    • 每月付费用户(Contract:Month-to-month,42.7%);
    • 无纸化账单用户(PaperlessBilling:Yes,33.6%);
    • 电子支票付费用户(PaymentMethod:Electronic check,45.3%);
  • 人口学特征中流失率较大的特征:
    • 老年用户(SeniorCitizen:1,41.7%);
    • 无子女用户(Partner:No,33.0%);
    • 无父母用户(Dependents:No,31.3%);

可以看出:光纤用户流失较大;没有订购附加服务(安全、备份、防护、技术支持)的用户流失率较大;每月付费和电子支付用户流失率高;老年用户流失率高。其中,月付费用户、没有订购附加服务的用户流失率高是比较正常的。而光纤用户、老年用户、电子支票支付用户流失率高就需要再详细分析一下。

# 服务及人口学特征与流失的关系
def fig_out(col):
    tmp_df = df.groupby(col, as_index=False)['customerID'].count()
    tmp_df.columns =[col,'num_all']

    tmp_df2 = df[df['Churn'] == 'Yes'].groupby(col, as_index=False)['customerID'].count()
    tmp_df2.columns = [col,'num_yes']

    tmp_df = tmp_df.merge(tmp_df2, on=col, how='left')
    tmp_df.loc[:,['num_all','num_yes']].fillna(0, inplace=True)
    tmp_df.loc[:,'churn_yes'] = tmp_df[['num_yes','num_all']].apply(lambda x: (x['num_yes'] / x['num_all']), axis=1)
    #print(tmp_df)

    s_name = list(tmp_df[col])
    s_name2 = np.arange(len(s_name))
    s_value1 = list(tmp_df['num_all'])
    s_value2 = list(tmp_df['num_yes'])
    s_value3 = list(tmp_df['churn_yes'])
    
    # 条形图中 条的宽度
    wids = len(s_name) / (len(s_name) * 3)
    
    # 绘图
    from matplotlib import pyplot as plt
    fig, ax1 = plt.subplots(figsize=(10,6), facecolor='w')
    ax2 = ax1.twinx()
    
    ax1.bar(s_name2 - (wids/2), s_value1, width=wids, label='num_all')
    ax1.bar(s_name2 + (wids/2), s_value2, width=wids, label='num_yes')
    ax2.plot(s_name2, s_value3, color='r', linestyle='--', label='churn_yes')
    
    # 数据标签
    for a,b,c,d in zip(s_name2, s_value1, s_value2, s_value3):
        ax1.text(a - (wids/2), b, '{:,}'.format(b), ha='center', va='bottom', fontsize=10)
        ax1.text(a + (wids/2), c, '{:,}'.format(c), ha='center', va='bottom', fontsize=10)
        ax2.text(a, d, '{:.1%}'.format(d), ha='center', va='bottom', fontsize=10)
    
    ax1.legend()
    plt.title(col)
    plt.xticks(s_name2, s_name)
    #plt.legend(loc='upper right')
    plt.savefig('figure\\{}.png'.format(col), bbox_inches = 'tight', pad_inches = 0.1)
    #plt.show()
    exe_text('{}.png: out'.format(col))


for cc in service:
    fig_out(cc)
    
for cc in demographic:
    fig_out(cc)

以上代码共生成了16张图片,一张一张贴出来太麻烦了,所以将它们合并为一张大图:

# 将之前的图片合并为一张图
from PIL import Image

features = service + demographic
print(features)

def figs_union():
    # 读取图片
    img_list = [Image.open('figure\\{}.png'.format(i)) for i in features]
    # 把图片调整成同一尺寸(防止图片尺寸有微小不同)
    imgs = []
    for i in img_list:
        new_img = i.resize((647,373), Image.BILINEAR)
        imgs.append(new_img)
    # 获取图片的宽度、高度
    width, height = imgs[0].size
    # 创建空白大图(4 x 4)
    result = Image.new(imgs[0].mode, (width * 4, height * 4))
    # 拼接图片
    for i, im in enumerate(imgs):
        result.paste(im, box=((i % 4) * width, (i // 4) * height))
    # 保存图片
    result.save('features.png')

figs_union()

服务的人口学特征与流失的关系

2.3.4 人口学特征对各服务用户流失的影响

  • 性别对于服务的影响并不明显。
  • 老年人流失率较高的项目:
    • 光纤用户,47.3%;
    • 没有订购在线安全、在线备份、设备保护、技术支持的,约50%;
    • 按月付费,54.6%;
    • 无纸化账单,45.4%;
    • 电子支票,53.4%;
  • 无伴侣用户流失率较高的项目:
    • 光纤用户,49.7%;
    • 没有订购在线安全、在线备份、设备保护、技术支持的,约45%;
    • 按月付费,44.7%;
    • 无纸化账单,40.9%;
    • 付费方式-电子支票,50.8%;
  • 无子女流失率较高的项目:
    • 光纤用户,45%;
    • 没有订购在线安全、在线备份、设备保护、技术支持的,约44%;
    • 按月付费,45.2%;
    • 无纸化账单,38.2%;
    • 付费方式-电子支票,48.6%;

综合上述分析,可以知道:光纤服务,没有订购在线安全、在线备份、设备保护、技术支持等服务,预付费用户,使用无纸化账单,电子支票付费的用户,流失率较高。

#### 维度间关系:性别、老人、伴侣、孩子 X 服务
def figure_mix(col1, col2):
    tmp_df1 = df.groupby([col1,col2], as_index=False).agg({'customerID':pd.Series.nunique})
    tmp_df2 = df.loc[df['Churn'] == 'Yes',[col1,col2,'customerID']]\
                .groupby([col1,col2], as_index=False).agg({'customerID':pd.Series.nunique})
    tmp_df1.columns = [col1,col2,'num_all']
    tmp_df2.columns = [col1,col2,'num_yes']
    # 整合数据
    tmp_df = tmp_df1.merge(tmp_df2, on=[col1,col2], how='left')
    tmp_df.loc[:,'churn_yes'] = tmp_df[['num_all','num_yes']].apply(lambda x: (x['num_yes'] / x['num_all']), axis=1)
    # 打印结果
    print('{} X {}:\n{}\n{}\n'.format(col1, col2, '-'*20, tmp_df))

service = ['PhoneService','MultipleLines','InternetService',
           'OnlineSecurity','OnlineBackup','DeviceProtection','TechSupport',
           'StreamingTV','StreamingMovies',
           'Contract','PaperlessBilling','PaymentMethod']
demographic = ['gender','SeniorCitizen','Partner','Dependents']

for dd in demographic:
    for ss in service:
        figure_mix(dd,ss)


########## 结果 ##########
# 结果太长就不贴了

2.4 数据探索的结论

  • 光纤服务本来应该网速较快,且比 DSL 方便,但流失率较高,应检查光纤服务是否存在问题。
  • 大部分老年人订购了电话服务、多线路服务、按月付费、无纸化账单和电子支票付费,而没有订购在线安全、在线备份、设备保护和技术支持。
  • 电子支票本应该提高效率,但流失率较高,所以也可以找找此项业务是否存才缺陷。
  • 没有订购在线安全、在线备份、设备保护和技术支持的用户流失率较高,而老年用户尤其高,可将这四种服务组合起来向老年用户推广。

0x03、预处理

3.1 数据类型转换

1、需要转换数据类型

  • TotalCharges:前面已经由 object 类型转换为了 float64 类型;
  • SinorCitizen:只有0,1两个值,可以转换为 category 类型;
  • customerID:无需转换类型;
  • 其他 ojbect 类型:可以转换为 category 类型,以节省内存空间;
# 将 一些数据类型转换为 category 
service = ['PhoneService','MultipleLines','InternetService',
           'OnlineSecurity','OnlineBackup','DeviceProtection','TechSupport',
           'StreamingTV','StreamingMovies',
           'Contract','PaperlessBilling','PaymentMethod']
demographic = ['gender','SeniorCitizen','Partner','Dependents']
account = ['customerID','tenure','MonthlyCharges','TotalCharges','Churn']

df[service] = df[service].astype('category')
df[demographic] = df[demographic].astype('category')
df['Churn'] = df['Churn'].astype('category')

df.info(memory_usage='deep')


########## 结果 ##########
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype   
---  ------            --------------  -----   
 0   customerID        7043 non-null   object  
 1   gender            7043 non-null   category
 2   SeniorCitizen     7043 non-null   category
 3   Partner           7043 non-null   category
 4   Dependents        7043 non-null   category
 5   tenure            7043 non-null   int64   
 6   PhoneService      7043 non-null   category
 7   MultipleLines     7043 non-null   category
 8   InternetService   7043 non-null   category
 9   OnlineSecurity    7043 non-null   category
 10  OnlineBackup      7043 non-null   category
 11  DeviceProtection  7043 non-null   category
 12  TechSupport       7043 non-null   category
 13  StreamingTV       7043 non-null   category
 14  StreamingMovies   7043 non-null   category
 15  Contract          7043 non-null   category
 16  PaperlessBilling  7043 non-null   category
 17  PaymentMethod     7043 non-null   category
 18  MonthlyCharges    7043 non-null   float64 
 19  TotalCharges      7043 non-null   float64 
 20  Churn             7043 non-null   category
dtypes: category(17), float64(2), int64(1), object(1)
memory usage: 747.1 KB

2、编码转换

  • category 类型可以转换为整数形式。
  • 使用 OrdinalEncoder 转换后为float64,可以再次转换为 int8。
# 预处理:将 类别 编码转换为0-1的形式
# OneHorEncoder: 将类别特征转码为 one-hot 数列。
# LabelEncoder: 将 标签y 转换为 (0 ~ 类别数-1 )的区间。
# OrdinalEncoder: 将类别特征转码为整数数列。
from sklearn.preprocessing import LabelEncoder, OrdinalEncoder

# 取 category 类型的字段
category_list = df_data.select_dtypes('category').columns.to_list()

# 转换后字段类型为 float64
df_data[category_list] = OrdinalEncoder().fit_transform(df[category_list])  

# 转换为int类型
df_data[category_list] = df_data[category_list].astype('int8')

3.2 数据集分离

# 分离训练集与测试集
from sklearn.model_selection import train_test_split

set_y = df_data['Churn']
set_X = df_data.drop(['customerID','Churn'], axis=1)

train_X, test_X, train_y, test_y = train_test_split(set_X, set_y, test_size=0.2)  # 注意四个数据集的顺序

print('shape:\ntrain_X: {}, test_X: {}'.format(train_X.shape, test_X.shape))


########## 结果 ##########
shape:
train_X: (5634, 19), test_X: (1409, 19)

0x04、模型训练

# 算法模型
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier

from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score


# 模型训练与比较
def models_train(train_X, train_y, test_X, test_y):
    model_name, train_score = [], []
    pred_accuracy, pred_recall, pred_precision, pred_f1 = [], [], [], []
    
    for name, model in models:
        # 训练集交叉验证分, 5折交叉验证取均值,用以观察哪个模型在训练集上的表现好
        s_train = cross_val_score(model, train_X, train_y, cv=5).mean()
        
        # 构建和预测
        model.fit(train_X, train_y)
        pred_y = model.predict(test_X)
        
        s_accuracy = accuracy_score(pred_y, test_y)
        s_recall = recall_score(pred_y, test_y)
        s_precision = precision_score(pred_y, test_y)
        s_f1 = f1_score(pred_y, test_y)
        
        # 结果存储
        model_name.append(name)
        train_score.append(s_train)
        pred_accuracy.append(s_accuracy)
        pred_recall.append(s_recall)
        pred_precision.append(s_precision)
        pred_f1.append(s_f1)
        print('[{}] 完成model: {}'.format(time.strftime('%y-%m-%d %H:%M:%S',time.localtime()), name))
    
    # 合并结果
    models_score = pd.DataFrame({'ModelName':model_name, 'TrainScore':train_score, 'Accuracy':pred_accuracy,\
                                'Recall':pred_recall, 'Precision':pred_precision, 'F1':pred_f1})
    return models_score


# 定义模型及其参数
models = [('LR', LogisticRegression()),
          ('CART', DecisionTreeClassifier()),
          ('RF', RandomForestClassifier()),
          ('GBDT', GradientBoostingClassifier())]

# 训练模型,显示结果
model_score = models_train(train_X, train_y, test_X, test_y)
print(model_score)


########## 结果 ##########
  ModelName  TrainScore  Accuracy    Recall  Precision        F1
0        LR    0.804046  0.778566  0.634831   0.553922  0.591623
1      CART    0.740327  0.735273  0.542579   0.546569  0.544567
2        RF    0.792689  0.785664  0.663580   0.526961  0.587432
3      GBDT    0.804402  0.782115  0.654434   0.524510  0.582313

0x05、模型调优

  • 使用贝叶斯调优方法,对GBDT模型调优
# 超参数 贝叶斯调优 (pip3 intall scikit-optimize)
# API Reference: https://scikit-optimize.github.io/stable/modules/classes.html
from skopt import BayesSearchCV
from skopt.space import Real, Categorical, Integer

# 对GBDT调优
gbdt_optm = BayesSearchCV(estimator=GradientBoostingClassifier(),
                          search_spaces={'learning_rate':(0.01,0.1),
                                         'min_samples_split': Integer(2, 30),
                                         'min_samples_leaf':Integer(1,30),
                                         'max_features': Integer(4, 19),
                                         'max_depth': Integer(5, 50),
                                         'subsample':(0.5,1),
                                         'n_estimators': Integer(10, 400)
                                        },
                          cv=5,
                          verbose=-1,
                          n_jobs=-1 )

gbdt_optm.fit(train_X, train_y)

pred_gbdt = gbdt_optm.best_estimator_.predict(test_X)
print(f1_score(pred_gbdt, test_y))
print('-'*20)
print('Best params:\n{}'.format(gbdt_optm.best_params_))


########## 结果 ##########
0.5978428351309707
--------------------
Best params:
OrderedDict([('learning_rate', 0.01), ('max_depth', 3), ('max_features', 19), ('min_samples_leaf', 30), ('min_samples_split', 30), ('n_estimators', 400), ('subsample', 0.5)])

0x06、总结

1、数据分析与服务改进

这是一个二分类问题,特征个数不多而且大多数都是二值的特征,比较利于分类问题。通过流失率的高低,可以判断一项服务是否对流失有显著影响,高流失率的服务可能是存在问题的,可以找出这类服务并结合更具体的数据进行分析。

特征重要性可以表示对流失的影响程度高低。因此可以通过计算重要性来决定优先改进哪一项服务。

2、超参数的调优

第一次接触到参数的调优,这个项目中没有去详细了解其算法细节,后面将详细学习贝叶斯优化的算法。模型选择和参数调优对我来说一直是一个难点,后面还需要继续努力攻克。

3、内存优化

可以看到,一开始 DF 的内存为 7.8MB,改为 category 格式存储 int8 格式存储都是 741KB,占用内存减少了90%。


参考资料

  1. Telco-Customer-Churn Dataset - Kaggle
  2. Telco customer churn - IBM
  3. Kaggle:Telco-Customer churn(电信公司用户流失预测)- 知乎
  4. Matplotlib - 箱线图、箱型图 boxplot () 所有用法详解 - CSDN
  5. 十个Kaggle项目带你入门数据分析 - 知乎
  6. scikit-optimize API Reference - Github
  7. 4种主流超参数调优技术 - 知乎
  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值