运营商客户流失分析与预测

电信运营商客户数据集

背景描述

----关于用户留存有这样一个观点,如果将用户流失率降低5%,公司利润将提升25%-85%。如今高居不下的获客成本让电信运营商遭遇“天花板”,甚至陷入获客难的窘境。随着市场饱和度上升,电信运营商亟待解决增加用户黏性,延长用户生命周期的问题。因此,电信用户流失分析与预测至关重要。

数据说明

----每一行代表一个客户,每一列包含列元数据中描述的客户属性。原始数据包含7043行(客户)和21列(特性)。

数据来源

https://www.kaggle.com/blastchar/telco-customer-churn

问题描述

----预测客户是否流失?

本文索引

-->1.观察理解数据,提出问题;
-->2.数据清洗;
-->3.探索性数据分析;
-->4.数据预处理;
-->5.挖掘建模;
-->6.模型评估 ;
-->7.结论和建议

一、观察理解数据,提出问题

1.已知,数据每一行代表一个客户,每一列包含列元数据中描述的客户属性。原始数据包含7043行(客户)和21列(特性)

2.数据包含1个是否流失的y标签列"Churn",20个特征列x;其中"SeniorCitizen"、"tenure"、"MonthlyCharges"3个字段为数值特征,其他均为文本特征。

3.目标为研究20个特征列与是否流失标签列y之间的关系模型,预测现有客户的可能流失情况,针对预测结果制定相应挽留措施,降低流失率。

二、数据清洗

----数据清洗的“完全合一”规则:

1.完整性:单条数据是否存在空值,统计的字段是否完善。

2.全面性:观察某一列的全部数值,通过常识来判断该列是否有问题,比如:数据定义、单位标识、数据本身。

3.合法性:数据的类型、内容、大小的合法性。比如数据中是否存在非ASCII字符,性别存在了未知,年龄超过了150等。

4.唯一性:数据是否存在重复记录,因为数据通常来自不同渠道的汇总,重复的情况是常见的。行数据、列数据都需要是唯一的。

#导包及数据集
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

df = pd.read_csv('C:/Users/82122/Desktop/data/WA_Fn-UseC_-Telco-Customer-Churn.csv',encoding='utf8')

#空值检查
df.isnull().sum()  #结果不存在空值
pd.set_option('display.max_columns',None)  #设置pandas显示所有列
df.head(5)

df.dtypes  #'TotalCharges'总消费额字段显示为文本字符串类型,一般经验看要转化为浮点型
#df['TotalCharges'].astype(float)  
#"ValueError: could not convert string to float: "显示不能转换为float

各字段大概意义

--customerID 用户ID;--gender 性别;--SeniorCitizen 是否老年人;--Partner 是否有伴侣;--Dependents 是否家属;

--tenure 入网时长;--PhoneService 电话服务;--MultipleLines 多业务;--InternetService 网络服务;--OnlineSecurity 在线安全;

--OnlineBackup 在线备份;--DeviceProtection 设备保护;--TechSupport 技术支持;

--StreamingTV 电视业务;--StreamingMovies 影视服务;

--Contract 合同;--PaperlessBilling 无纸账单;--PaymentMethod 付款方法;

--MonthlyCharges 每月收费;--TotalCharges 总花费;--Churn 是否流失;

df['TotalCharges'].value_counts()   #存在空字符串
图片.png
#使用强制转化为数字,不可转换的变为NaN
df['TotalCharges'] = df['TotalCharges'].convert_objects(convert_numeric=True)
图片.png

图片.png
#空值的在网时长‘tenure’均是0,预估这部分用户是在当月入网的用户,根据一般经验看,此部分用户肯定是需要缴费的。
#此部分用户包含重要特征,所以不选择删除此部分数据,把这部分用户入网时长改为1,消费总额按当月的缴费
df['tenure'].replace(0,1,inplace=True)
df['TotalCharges']=df['TotalCharges'].fillna(df['MonthlyCharges'])

三、探索性数据分析

图片.png
#y标签分布
df['Churn'].value_counts().plot(kind='bar') 
print('流失率:',df['Churn'].value_counts()[1]/len(df['Churn']))  #整体流失率达到26.5%,正负样本并不均衡
图片.png
#性别
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
df['gender'].value_counts().plot(kind='bar')
plt.title('gender 柱状图')
x_len=range(len(df['gender'].unique()))
y_data=list(df['gender'].value_counts())
for x,y in zip(x_len,y_data):
    plt.text(x,y+1,'%.0f'%y, ha='center', va= 'bottom',fontsize=10)
print('性别比例:',df['gender'].value_counts()[0]/df['gender'].value_counts()[1])  #性别比例接近1:1
print(df[df['Churn']=="No"]['gender'].value_counts())
print(df[df['Churn']=="Yes"]['gender'].value_counts())

性别比例: 1.0192087155963303
Male 2625
Female 2549
Name: gender, dtype: int64
Female 939
Male 930
Name: gender, dtype: int64


图片.png
def feature_explor(feature):
    #导包
    %matplotlib inline
    import seaborn as sns
    import matplotlib.pyplot as plt
    #流失率计算
    a1 = df.groupby([feature])['Churn'].value_counts().to_frame()
    a1.rename(columns={"Churn":'计数'},inplace=True)
    a1.reset_index(inplace=True)
    a2=df[feature].value_counts().to_frame()
    a2.reset_index(inplace=True)
    a2.columns=[feature,'计数1']
    a3=pd.merge(a1,a2,how='left',on=feature)
    a3['流失率']=a3['计数']/a3['计数1']
    print(a3[a3["Churn"]=='Yes'])
    #特征流失率可视化
#     g1=sns.barplot(x=feature,y='流失率',hue='Churn',data=a3)
#     plt.title('{0} 维度流失率'.format(feature))  #添加标题
#     #得到标签数据
#     #w=g1.get_width()
#     x_1=len(a2)
#     x_len=range(len(a3))
#     y_data=a3['流失率'].tolist()
#     for x,y in zip(x_len,y_data):
#         plt.text(x/x_1-0.25,y+0.05,'%.2f'%y, ha='center', va= 'bottom',fontsize=10)
#得到特征列表,并去除id列、数值列特征以及标签'Churn';'SeniorCitizen'不是真正的数值特征,算是分类特征,保留在列表内
c_l=df.columns.tolist()
l1=['customerID','tenure','MonthlyCharges','TotalCharges','Churn']
for i in l1:
    c_l.remove(i)
c_l
#对得到的特征列表,特征流失率可视化
for i in c_l:
    print('*********************************************************')    
    feature_explor(i)
    print('*********************************************************')
图片.png

图片.png

图片.png

'gender':男女流失率基本相同

'SeniorCitizen':老人流失率高于非老人群体

'Partner':没有伴侣的流失率高于有伴侣的

'Dependents':没有家属的高于有家属的

'PhoneService'、'MultipleLines Churn':对流失率影响差异不明显

'InternetService'、'OnlineSecurity'、'OnlineBackup'、'DeviceProtection'、'TechSupport'、

'StreamingTV'、'StreamingMovies'、'Contract'、'PaperlessBilling '、'PaymentMethod':对流失率有明细影响

import seaborn as sns
plt.figure(figsize=(10,4))
d1=df[df['Churn']=='Yes']['tenure'].dropna()
d2=df[df['Churn']=='No']['tenure'].dropna()
sns.kdeplot(d1,color= 'navy', label= 'Churn: Yes',shade='True')
sns.kdeplot(d2,color= 'orange', label= 'Churn: No', shade='True')
plt.xlabel('tenure')
plt.title("KDE for tenure")
#设置字体大小
plt.rcParams.update({'font.size': 20})
plt.legend(fontsize=10)   
# plt.xlim([-10,10])   #入网3个月左右流失量达到峰值,入网时长越高流失越少
图片.png
import seaborn as sns
plt.figure(figsize=(10,4))
d1=df[df['Churn']=='Yes']['MonthlyCharges'].dropna()
d2=df[df['Churn']=='No']['MonthlyCharges'].dropna()
sns.kdeplot(d1,color= 'navy', label= 'Churn: Yes',shade='True')
sns.kdeplot(d2,color= 'orange', label= 'Churn: No', shade='True')
plt.xlabel('MonthlyCharges')
plt.title("KDE for MonthlyCharges")
#设置字体大小
plt.rcParams.update({'font.size': 20})
plt.legend(fontsize=10)   #没有付费80-120区间流失量高
图片.png
import seaborn as sns
plt.figure(figsize=(10,4))
d1=df[df['Churn']=='Yes']['TotalCharges'].dropna()
d2=df[df['Churn']=='No']['TotalCharges'].dropna()
sns.kdeplot(d1,color= 'navy', label= 'Churn: Yes',shade='True')
sns.kdeplot(d2,color= 'orange', label= 'Churn: No', shade='True')
plt.xlabel('TotalCharges')
plt.title("KDE for TotalCharges")
#设置字体大小
plt.rcParams.update({'font.size': 20})
plt.legend(fontsize=10)
# plt.xlim([-2000,400])    #总花费300-400时流失较多
图片.png

五、数据预处理

del df['customerID']  #删除ID列
#1、特征缩放
# 数值化
#使用pd.get_dummies(df),把数据OneHotEncoder编码;把标签y转化为正负1,0
df1 = pd.get_dummies(df.iloc[:,0:-1])
df2=df
df2['Churn']=df2['Churn'].replace('Yes',1)
df2['Churn']=df2['Churn'].replace('No',0)
df3=pd.concat([df1,df2['Churn']],axis=1)
#特征缩放-数值特征标准化
from sklearn.preprocessing import StandardScaler
encoder = StandardScaler()
df3[['tenure']] = pd.DataFrame(encoder.fit_transform(df3[['tenure']]))
df3[['MonthlyCharges']] = pd.DataFrame(encoder.fit_transform(df3[['MonthlyCharges']]))
df3[['TotalCharges']] = pd.DataFrame(encoder.fit_transform(df3[['TotalCharges']]))
#特征选择-3种方法,过滤:selectKbest,包裹:RFE,嵌入:selectfrommodel
from sklearn.feature_selection import SelectKBest,RFE,SelectFromModel
X = df3.iloc[:,0:-1]
y = df3['Churn']
#过滤
skb=SelectKBest(k=35)
skb.fit(X,y)
x_skb=pd.DataFrame(skb.transform(X),columns=X.columns[skb.get_support()])
#包裹
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVR
# rfe=RFE(LogisticRegression(),35)
rfe=RFE(SVR(kernel='linear'),35)
rfe.fit(X,y)
x_rfe=pd.DataFrame(rfe.transform(X),columns=X.columns[rfe.get_support()])
#嵌入
from sklearn.tree import DecisionTreeRegressor
sfm=SelectFromModel(DecisionTreeRegressor(),threshold=0.0011)
sfm.fit(X,y)
x_sfm=pd.DataFrame(sfm.transform(X),columns=X.columns[sfm.get_support()])
x_sfm.shape
#比较3种方法的结果
print(skb.get_support())
print(rfe.get_support())
print(sfm.get_support())
图片.png
#训练集、测试集拆分
from sklearn.model_selection import train_test_split
x_tt,x_validation,y_tt,y_validation=train_test_split(X,y,test_size=0.2,random_state=42)
x_train,x_test,y_train,y_test=train_test_split(x_tt,y_tt,test_size=0.25,random_state=42)
#构造模型
from sklearn.neighbors import NearestNeighbors,KNeighborsClassifier  #KNN,K近邻
from sklearn.naive_bayes import GaussianNB,BernoulliNB  #朴素贝叶斯(高斯贝叶斯,伯努利贝叶斯)
from sklearn.linear_model import LogisticRegression  #逻辑回归
from sklearn.tree import DecisionTreeClassifier  #决策树
from sklearn.svm import SVC,SVR  #支持向量机
from sklearn.ensemble import RandomForestClassifier,AdaBoostClassifier  #随机森林(bagging,boost-Adaboost)
from sklearn.metrics import accuracy_score,recall_score,f1_score  #模型评估,准确率,召回率,f1

models=[]
models.append(('KNN',KNeighborsClassifier(n_neighbors=45)))
models.append(('GaussianNB',GaussianNB()))
models.append(('BernoulliNB',BernoulliNB()))
models.append(('LogisticRegression',LogisticRegression(C=10,penalty='l2',class_weight='balanced',max_iter=1000)))
models.append(('DecisionTreeClassifier',DecisionTreeClassifier(max_depth=4,class_weight='balanced')))
models.append(('SVC',SVC(C=1000,class_weight='balanced')))
models.append(('RandomForestClassifier',RandomForestClassifier(n_estimators=100,max_depth=6,class_weight='balanced')))
models.append(('AdaBoostClassifier',AdaBoostClassifier(LogisticRegression(C=10,penalty='l2',class_weight='balanced',max_iter=1000)
                                                       ,n_estimators=80,learning_rate=0.4)))



for clf_name,clf in models:
    clf.fit(x_train,y_train)
    xy_list=[(x_train,y_train),(x_validation,y_validation),(x_test,y_test)]
    print('****************************************************************')
    for i in range(len(xy_list)):
        x_part=xy_list[i][0]
        y_part=xy_list[i][1]
        y_pred=clf.predict(x_part)
        print(i)
        print(clf_name,'-ACC',accuracy_score(y_part,y_pred))
        print(clf_name,'-REC',recall_score(y_part,y_pred))
        print(clf_name,'-F1',f1_score(y_part,y_pred))

模型结果

图片.png

图片.png

图片.png

图片.png

根据以上分析,得到高流失率用户的特征:
1、老年用户,未婚用户,无亲属用户更容易流失;
2、在网时长小于半年,有电话服务,光纤用户/光纤用户附加流媒体电视、电影服务,无互联网服务;
3、签订的合同期较短,采用电子支票支付,是电子账单,月租费约80-120元的客户容易流失;
其它属性对用户流失影响较小,以上特征保持独立。

针对上述结论,从业务角度给出相应建议:
根据预测模型,定期构建一个高流失率的用户列表,针对不同用户制定相应营销举措,挽留用户流失。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335