数据分析

用户流失预测模型-电信行业项目实战

2020-05-07  本文已影响0人  茶小美

本文以电信行业数据为基础,对其进行用户流失预警的建模,整理如下,欢迎拍砖~

一、流失知识点整理

1. 流失定义

不同产品存在不同的使用周期,因此在定义流失用户上,需要去进行用户调研,比如可以对时隔1周、1个月、3个月、半年未下单用户进行用户调研,去了解用户不再产生浏览和购买的行为原因,定义流失。

DAU/MAU

这里的DAU是一个月内日活均值,MAU是月活跃用户数去重。
DAU/MAU比值高,说明用户访问产品频率高且稳定,用户粘性高留存率高;
相反DAU/MAU比值低,说明用户访问产品频率低不稳定,用户粘性差留存率低;

以社交产品为例,DAU/MAU定义用户流失
一般来说0.03<DAU/MAU<1,DAU/MAU=0.03说明活跃用户只来一次,产品粘性太低;=1说明用户每天都来。微信的DAU/MAU处于0.8左右,一般产品0.3就比较好了。

2. 研究流失的目的

首要目的还是避免用户继续流失,其次才是挽回流失。
这里可以看用户的生命周期判断流失原因:
(1)获取期:新用户通过推广、宣传来到产品中,尝鲜型;
(2)提升期:用户有购买行为
(3)成熟期:用户存在复购和交叉购买行为
(4)衰退期:购买行为和频次开始衰退,是最需要预警的时期
(5)离开期:达到流失标准
根据以上五个生命周期为用户打上标签,判断用户是在哪个时期流失,相应的流失原因不同,采取不同措施对产品进行改进。

3. 流失指标
4. 确定首要挽回用户

对于老板来说,成本有限,挽回流失用户当然是挑最值得挽回那一波。因此这里用到了RMF模型,总的来说获取期用户的优先级一定是低于成熟期的。这里有我写过的RFM文章:RFM模型分析实战

5. 流失用户召回

使用push推送、短信、微信服务号等方式进行召回
这里也同样应用到了流失模型。
比如:根据购买频次和金额来细分。
1次也没购买过的用户可派发大额度优惠券、大促活动或超低价商品吸引回访,成为首单新客。
购买1—2次且客单价较低的用户,可精准推送优惠专场或在这个客单水平的好货。
购买3次及以上的用户,可推送用户偏好的品牌或品类,额外增加会员专属优惠券等形式。

总而言之,根据用户流失模型区分不同行为和属性的用户,以及他们流失的节点、原因,运营才可以做到有的放矢,强化用户召回的效果。

说在后面:用户的召回很难,更好的做法是避免用户流失。比如通过区分用户生命5个周期找到用户在各周期的流失原因进行产品或运营改进;比如通过分析之前已流失的用户特征属性,行为建立预警模型,针对现有用户有流失迹象提前预警提前进行挽回打消用户流失的念头,这才是关键!

二、电信行业流失预警模型

分析目的

哪些用户可能会流失?
流失概率高的用户有哪些共同特征?

数据探索EDA
2.1 导入数据
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
import os
os.chdir('/Users/xy/Desktop/专业知识/电信行业流失预警/')
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
2.2 数据概览
pd.set_option('display.max_columns', None)
df.head(5)
#各个字段取值个数
for i in range(21):
    print(df.iloc[:,i].value_counts())
df.shape

customerID:用户ID
gender:性别 male男性 female女性
SeniorCitizen:是否老年人 0否1是
Partner:是否有配偶 YES是NO否
Dependents:是否经济独立 YES是NO否
tenure:客户职位 73个
PhoneService:是否开通电话业务 YES是NO否
MultipleLines:是否开通多线业务 YES是NO否 No phoneservice
InternetService:是否开通互联网业务 No, DSL数字网络,fiber optic光纤网络
OnlineSecurity:是否开通网络安全服务YES是NO否 No phoneservice
OnlineBackup:是否开通在线备份业务YES是NO否 No phoneservice
DeviceProtection:是否开通了设备保护业务YES是NO否 No phoneservice
TechSupport:是否开通了技术支持服务YES是NO否 No phoneservice
StreamingTV:是否开通网络电视YES是NO否 No phoneservice
StreamingMovies:是否开通网络电影
Contract:签订合同方式 按月 一年 二年
PaperlessBilling:是否开通电子账单YES是NO否
PaymentMethod:付款方式(bank transfer,credit card,electronic check,mailed check)
MonthlyCharges:月费用
TotalCharges:总费用
Churn:是否流失 YES是NO否

(7043行, 21列)

其中:
gender:男女比例均衡
SeniorCitizen:非老年人居多
Partner:有无配偶比例均衡
Dependents:经济独立2k,非独立5k
PhoneService:开通电话业务居多
Contract:合同中按月的较多,按1年和2年的占比相似
Churn:数据集中有5174名用户没流失,有1869名客户流失,数据集不均衡

2.3 数据信息
df.info()
df.isnull().sum()
image.png

未发现缺失值

df['TotalCharges'].apply(pd.to_numeric, errors='coerce')#object转为float
df['TotalCharges'].isnull().sum()#再次查看缺失值
df.dropna(inplace=True)#删除缺失行

将TotalCharges的object转为float格式,网上给出的方法大多数都是dt_df = dt_df.convert_objects(convert_numeric=True),但是因为我现在的版本号低于1.0也不想升级,因此终于找到上面的方法改变类型啦!()
发现有11个缺失值,数量不多删除

df['Churn'].replace('Yes',1,inplace=True)
df['Churn'].replace('No',0,inplace=True)

将df['Churn']中值变为数值化,yes变为1,no变为1

2.4 数据可视化
2.41 流失用户占比
plt.rcParams['font.sans-serif']=['SimHei']  #正常显示中文
plt.rcParams['axes.unicode_minus'] = False  #正常显示负号

labels = ['未流失用户','流失用户']
Churn = df['Churn'].value_counts()
plt.pie(Churn,labels=labels,autopct='%.1f%%')
image.png

流失用户占比26.6%,未流失占比73.4%

2.42 性别、老年人、配偶、经济是否独立对流失用户的影响
plt.figure(1),plt.title('区分性别对流失的影响')
sns.countplot(x='gender',hue='Churn',data=df,palette='BuPu_r')
plt.figure(2),plt.title('区分老年人对流失的影响')
sns.countplot(x='SeniorCitizen',hue='Churn',data=df,palette='BuPu_r')
plt.figure(3),plt.title('区分配偶对流失的影响')
sns.countplot(x='Partner',hue='Churn',data=df,palette='BuPu_r')
plt.figure(4),plt.title('区分经济独立对流失的影响')
sns.countplot(x='Dependents',hue='Churn',data=df,palette='BuPu_r')
image.png
image.png
image.png
image.png

可以得出结论:男性与女性之间的流失没有差异;老年用户流失占比比非老年用户高;没有配偶的流失占比高于有配偶的流失占比;经济未独立的流失率远高于经济独立;

2.43 特征之间的关系

想知道特征之间的关系,需要将这些特征以数值形式展现。这里就用到LabelEncoder或pd.factorize,LabelEncoder能将文本或非连续性数字进行编号,缺点是该编码方法有顺序关系。pd.factorize与LabelEncoder的区别是:pd.factorize支持None默认不排序。pd.factorize()返回两个值前面值为编码后的值,后面为原来值。

from sklearn.preprocessing import LabelEncoder
labelencoder = LabelEncoder()
lst = []
columns=['gender', 'SeniorCitizen', 'Partner', 'Dependents',
       'tenure', 'PhoneService', 'MultipleLines', 'InternetService',
       'OnlineSecurity', 'OnlineBackup', 'DeviceProtection', 'TechSupport',
       'StreamingTV', 'StreamingMovies', 'Contract', 'PaperlessBilling',
       'PaymentMethod', 'MonthlyCharges', 'TotalCharges']
for i in range(1,20):
    corrdf = labelencoder.fit_transform(df.iloc[:,i])
    lst.append(corrdf)
df2 = pd.DataFrame(map(list,zip(*lst)),columns=columns)
df2.corr()

用corr相关矩阵难以看出这些列之间的关系,可以用热力图。

plt.figure(figsize=(16,8))
sns.heatmap(df2.corr(),cmap='YlGnBu',annot=True)
image.png
通常情况下通过以下取值范围判断变量的相关强度:
相关系数
0.8-1.0 极强相关
0.6-0.8 强相关
0.4-0.6 中等程度相关
0.2-0.4 弱相关
0.0-0.2 极弱相关或无相关

从热力图矩阵中能够明显看出配偶与经济独立有一定关系,职业与缴纳总费用以及合同期限有强相关关系

charge=df.iloc[:,1:20]#取要编码的列
for i in range(0,19):
    charge.iloc[:,i] = pd.factorize(charge.iloc[:,i])[0]
plt.figure(figsize=(16,8))
sns.heatmap(charge.corr(),cmap='YlGnBu',annot=True)
image.png

可以看出电话业务与多线业务之间存在强相关性,互联网服务、网络安全服务、在线备份业务、设备保护业务、技术支持服务、网络电视和网络电影之间存在较强的相关性,并且都呈正相关关系。

我稍微研究了下两者编码后的区别,都是按照从上到下出现顺序对离散非数值变量进行0,1,...的编码;对于离散数值变量如职业,labelencoder按照原数据大小从0向后依次编码,而pd.factorize仍旧按照出现顺序不分大小进行编码。这样也就解释了为什么在pd.factorize中有明显关系的几个列在labelencoder无明显关系了,因为被labelencoder中的有大小顺序掩盖了。(仅个人见解)

2.44 特征与churn之间的关系

首先介绍连续数据离散化相关知识点:
一般在回归、分类、聚类算法中,特征之间计算欧式距离来计算相似度十分重要。当特征中既包括离散变量也包括连续变量,或存在数量级不一致的情况下,就要进行归一化。
(1)连续变量处理方法

df_churn = pd.get_dummies(df.iloc[:,1:21])
plt.figure(figsize=(16,8)),plt.title('Churn与各特征之间相关性')
df_churn.corr()['Chaurn'].sort_values(ascending=False).plot(kind='bar')
image.png

可见变量gender 和 PhoneService处于中间,相关性非常小,可以舍弃。

columns=['OnlineSecurity', 'OnlineBackup', 'DeviceProtection', 'TechSupport',
       'StreamingTV', 'StreamingMovies']
fig,axes = plt.subplots(nrows=2,ncols=3,figsize=(16,10))
for i,item in enumerate(columns):#enumerate变量数据对象如列表,返回每个数据及其下标
    plt.subplot(2,3,(i+1))
    ax = sns.countplot(x=item,hue='Churn',data=df,order=['Yes','No','No internet service'],palette='YlGnBu_r')
    #频数柱形图,x为每个特征,hue为区分是否流失,y为频数,order可以规定x内显示顺序
    plt.xlabel(str(item))
    plt.title('Churn与'+str(item)+'的关系')
    i = i+1
plt.show()
image.png
上述可见,网络安全服务、在线备份业务、设备保护业务、技术支持服务、网络电视和网络电影这六个变量中没有开通网络服务的用户流失率相同的且较低,可能因为这六个变量只有在开通网络服务才会影响用户决策。因此这六个变量不会对未使用网络服务用户的流失产生推论效应。
在这六个变量中前四个变量“网络安全服务、在线备份业务、设备保护业务、技术支持服务”未开通该服务的用户流失占比远高于开通服务的流失占比,而“网络电视和网络电影”流失几乎不受是否开通该业务影响。猜测是因为前四个变量开通能提高用户体验质量减少流失,而“网络电视和网络电影”不够成熟导致用户体验变差反而造成流失。
2.45 合同与流失的关系
plt.title('合同类型与客户流失的关系')
sns.barplot(x='Contract',y='Churn',data=df,order=['Month-to-month','One year','Two year'])
plt.ylabel('Churn:流失占比')
image.png

签订合同方式对流失率的影响是:按月>1年>2年

2.46 付款方式与流失的关系
#付款方式与流失的关系
plt.title('合同类型与客户流失的关系')
sns.barplot(x='PaymentMethod',y='Churn',data=df,order=['Electronic check','Mailed check','Bank transfer (automatic)','Credit card (automatic)'])
plt.ylabel('Churn:流失占比')

image.png

从上图可知,电子账单的流失率最高,可见电子账单的流程或设计影响用户体验,需要改进。

3. 特征工程
3.1 特征筛选

根据上面的结论得知,变量gender 和 PhoneService对流失影响可忽略,删除这两列。customerID是随机字符对后续建模不影响,也删除。

df_new= df.iloc[:,2:20]
df_new.drop('PhoneService',inplace=True,axis=1)
df_id = df['customerID']#提取客户ID

对用户职位、总费用、月费用进行连续变量归一化,使方差为1均值为0,这样预测数值不会被这些过大的特征值主导

from sklearn.preprocessing import StandardScaler
standard = StandardScaler()
standard.fit(df_new[['tenure','MonthlyCharges','TotalCharges']]

df_new[['tenure','MonthlyCharges','TotalCharges']] = standard.transform(df_new[['tenure','MonthlyCharges','TotalCharges']])
sns.boxplot(data=df_new[['tenure','MonthlyCharges','TotalCharges']])
plt.title('职位、月费用和总费用箱型图可视化')
image.png

如果不进行归一化,得到的箱线图是这样的:


image.png

因此归一化十分重要。
箱线图中能够看到,三个特征没有异常数据。

3.2 处理对象类型数据
#查看对象类型都有哪些值
def uni(data):
    print(data,'--',df_new[data].unique())
dfobject = df_new.select_dtypes('object')
for i in range(0,len(dfobject.columns)):
    uni(dfobject.columns[i])
image.png

由上面分析可知,六个变量中没有开通网络业务对流失影响较小,因此可以将No internet service 和 No 是一样的效果,可以使用 No 替代 No internet service。

df_new.replace('No internet service','No',inplace=True)
df_new.replace('No phone service','No',inplace=True)
for i in range(0,len(dfobject.columns)):
    uni(dfobject.columns[i])
image.png
3.3 将数值进行编码

sklearn中的labelencoder进行编码

def labelend(data):
    df_new[data] = labelencoder.fit_transform(df_new[data])#labelencoder进行编码
for i in range(0,len(dfobject.columns)):
    labelend(dfobject.columns[i])
for i in range(0,len(dfobject.columns)):
    uni(dfobject.columns[i])
image.png
4. 构造模型
4.1 交叉验证

由于流失占比不均衡,因此采用分层交叉验证法

from sklearn.model_selection import StratifiedShuffleSplit
X = df_new#删除与流失无关的列,将对象类型进行编码,将数值较大列进行归一化后得到的特征集
y = df['Churn'].values#标签集
sss = StratifiedShuffleSplit(n_splits=5,test_size=0.2,random_state=0)
for train_index,test_index in sss.split(X,y):
    print('train',train_index,'test',test_index)#得到训练集和测试集的index
    X_train,X_test = X.iloc[train_index],X.iloc[test_index]#训练集特征,测试集特征
    y_train,y_test = y[train_index],y[test_index]#训练集标签,测试集标签
    
image.png
print('原始数据特征',X.shape)
print('训练数据特征',X_train.shape)
print('测试数据特征',X_test.shape)
image.png
print('原始数据标签',y.shape)
print('训练数据标签',y_train.shape)
print('测试数据标签',y_test.shape)
image.png
4.2 选择机器学习算法

由于这里是监督学习分类问题,可选算法有:SVM支持向量机,决策树,朴素贝叶斯,逻辑回归(当然其他还有很多,比如随机森林、KNN、xgboost、catboost等,但是暂时没有掌握)

#算法
from sklearn.svm import SVC #支持向量机
from sklearn.linear_model import LogisticRegression #逻辑回归
from sklearn.naive_bayes import GaussianNB#朴素贝叶斯
from sklearn.tree import DecisionTreeClassifier#决策树分类器

from sklearn.metrics import recall_score,f1_score,precision_score
Classifiers = [['SVM',SVC()],
              ['LogisticRegression',LogisticRegression()],
              ['GaussianNB',GaussianNB()],
              ['DecisionTreeClassifier',DecisionTreeClassifier()]]
Classify_results = []
names = []
prediction = []
for name ,classifier in Classifiers:
    classifier.fit(X_train,y_train)#训练这4个模型
    y_pred = classifier.predict(X_test)#预测这4个模型
    recall = recall_score(y_test,y_pred)#评估这四个模型的召回率
    precision = precision_score(y_test,y_pred)#评估这四个模型的精确率
    f1 = f1_score(y_test,y_pred)#评估这四个模型的f1分数
    class_eva = pd.DataFrame([recall,precision,f1])#将召回率、精确率和f1分数放在df中,方便接下来对比
    Classify_results.append(class_eva)
    name = pd.Series(name)
    names.append(name)
    y_pred = pd.DataFrame(y_pred)
    prediction.append(y_pred)

将得到的模型评估指标制作成表格

names = pd.DataFrame(names)
result = pd.concat(Classify_results,axis=1)
result.columns = names
result.index=[['recall','precision','f1']]
result

image.png

综上所述,朴素贝叶斯模型的f1分数最高,因此使用朴素贝叶斯效果最好。

5. 实施方案

由于没有给预测数据,这里选择最后10行数据作为预测

df_test = df_new.tail(10)#截取数据最后10行
cutid_test = df['customerID'].tail(10)#取出最后10行的ID
model = GaussianNB()
model.fit(X_train,y_train)#训练
pred_test_y = model.predict(df_test)#用最后10行的数据做预测
predf = pd.DataFrame({'customerID':cutid_test,'churn':pred_test_y})

image.png

对比了实际数据,返现10行中3行预测错误,其余正确,与f1score=63%相符。

6. 结论

通过上述分析

参考:
https://mp.weixin.qq.com/s/_20MN6V6aV1T3Ekd7C9neQ 李启方大佬
https://blog.csdn.net/u013385925/article/details/80142310onehot
https://blog.csdn.net/Li_yi_chao/article/details/80852701onehot
https://blog.csdn.net/ccblogger/article/details/80010974
https://blog.csdn.net/u010986753/article/details/98069124交叉验证
https://blog.csdn.net/wuzhongqiang/article/details/101560889分层交叉验证详解

上一篇下一篇

猜你喜欢

热点阅读