電信運(yùn)營商客戶數(shù)據(jù)集
背景描述
----關(guān)于用戶留存有這樣一個(gè)觀點(diǎn),如果將用戶流失率降低5%,公司利潤將提升25%-85%。如今高居不下的獲客成本讓電信運(yùn)營商遭遇“天花板”,甚至陷入獲客難的窘境。隨著市場飽和度上升,電信運(yùn)營商亟待解決增加用戶黏性,延長用戶生命周期的問題。因此,電信用戶流失分析與預(yù)測至關(guān)重要。
數(shù)據(jù)說明
----每一行代表一個(gè)客戶,每一列包含列元數(shù)據(jù)中描述的客戶屬性。原始數(shù)據(jù)包含7043行(客戶)和21列(特性)。
數(shù)據(jù)來源
https://www.kaggle.com/blastchar/telco-customer-churn
問題描述
----預(yù)測客戶是否流失?
本文索引
-->1.觀察理解數(shù)據(jù),提出問題;
-->2.數(shù)據(jù)清洗;
-->3.探索性數(shù)據(jù)分析;
-->4.數(shù)據(jù)預(yù)處理;
-->5.挖掘建模;
-->6.模型評估 ;
-->7.結(jié)論和建議
一、觀察理解數(shù)據(jù),提出問題
1.已知,數(shù)據(jù)每一行代表一個(gè)客戶,每一列包含列元數(shù)據(jù)中描述的客戶屬性。原始數(shù)據(jù)包含7043行(客戶)和21列(特性)
2.數(shù)據(jù)包含1個(gè)是否流失的y標(biāo)簽列"Churn",20個(gè)特征列x;其中"SeniorCitizen"、"tenure"、"MonthlyCharges"3個(gè)字段為數(shù)值特征,其他均為文本特征。
3.目標(biāo)為研究20個(gè)特征列與是否流失標(biāo)簽列y之間的關(guān)系模型,預(yù)測現(xiàn)有客戶的可能流失情況,針對預(yù)測結(jié)果制定相應(yīng)挽留措施,降低流失率。
二、數(shù)據(jù)清洗
----數(shù)據(jù)清洗的“完全合一”規(guī)則:
1.完整性:單條數(shù)據(jù)是否存在空值,統(tǒng)計(jì)的字段是否完善。
2.全面性:觀察某一列的全部數(shù)值,通過常識來判斷該列是否有問題,比如:數(shù)據(jù)定義、單位標(biāo)識、數(shù)據(jù)本身。
3.合法性:數(shù)據(jù)的類型、內(nèi)容、大小的合法性。比如數(shù)據(jù)中是否存在非ASCII字符,性別存在了未知,年齡超過了150等。
4.唯一性:數(shù)據(jù)是否存在重復(fù)記錄,因?yàn)閿?shù)據(jù)通常來自不同渠道的匯總,重復(fù)的情況是常見的。行數(shù)據(jù)、列數(shù)據(jù)都需要是唯一的。
#導(dǎo)包及數(shù)據(jù)集
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() #結(jié)果不存在空值
pd.set_option('display.max_columns',None) #設(shè)置pandas顯示所有列
df.head(5)
df.dtypes #'TotalCharges'總消費(fèi)額字段顯示為文本字符串類型,一般經(jīng)驗(yàn)看要轉(zhuǎn)化為浮點(diǎn)型
#df['TotalCharges'].astype(float)
#"ValueError: could not convert string to float: "顯示不能轉(zhuǎn)換為float
各字段大概意義
--customerID 用戶ID;--gender 性別;--SeniorCitizen 是否老年人;--Partner 是否有伴侶;--Dependents 是否家屬;
--tenure 入網(wǎng)時(shí)長;--PhoneService 電話服務(wù);--MultipleLines 多業(yè)務(wù);--InternetService 網(wǎng)絡(luò)服務(wù);--OnlineSecurity 在線安全;
--OnlineBackup 在線備份;--DeviceProtection 設(shè)備保護(hù);--TechSupport 技術(shù)支持;
--StreamingTV 電視業(yè)務(wù);--StreamingMovies 影視服務(wù);
--Contract 合同;--PaperlessBilling 無紙賬單;--PaymentMethod 付款方法;
--MonthlyCharges 每月收費(fèi);--TotalCharges 總花費(fèi);--Churn 是否流失;
df['TotalCharges'].value_counts() #存在空字符串

#使用強(qiáng)制轉(zhuǎn)化為數(shù)字,不可轉(zhuǎn)換的變?yōu)镹aN
df['TotalCharges'] = df['TotalCharges'].convert_objects(convert_numeric=True)


#空值的在網(wǎng)時(shí)長‘tenure’均是0,預(yù)估這部分用戶是在當(dāng)月入網(wǎng)的用戶,根據(jù)一般經(jīng)驗(yàn)看,此部分用戶肯定是需要繳費(fèi)的。
#此部分用戶包含重要特征,所以不選擇刪除此部分?jǐn)?shù)據(jù),把這部分用戶入網(wǎng)時(shí)長改為1,消費(fèi)總額按當(dāng)月的繳費(fèi)
df['tenure'].replace(0,1,inplace=True)
df['TotalCharges']=df['TotalCharges'].fillna(df['MonthlyCharges'])
三、探索性數(shù)據(jù)分析

#y標(biāo)簽分布
df['Churn'].value_counts().plot(kind='bar')
print('流失率:',df['Churn'].value_counts()[1]/len(df['Churn'])) #整體流失率達(dá)到26.5%,正負(fù)樣本并不均衡

#性別
plt.rcParams['font.sans-serif']=['SimHei'] #用來正常顯示中文標(biāo)簽
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

def feature_explor(feature):
#導(dǎo)包
%matplotlib inline
import seaborn as sns
import matplotlib.pyplot as plt
#流失率計(jì)算
a1 = df.groupby([feature])['Churn'].value_counts().to_frame()
a1.rename(columns={"Churn":'計(jì)數(shù)'},inplace=True)
a1.reset_index(inplace=True)
a2=df[feature].value_counts().to_frame()
a2.reset_index(inplace=True)
a2.columns=[feature,'計(jì)數(shù)1']
a3=pd.merge(a1,a2,how='left',on=feature)
a3['流失率']=a3['計(jì)數(shù)']/a3['計(jì)數(shù)1']
print(a3[a3["Churn"]=='Yes'])
#特征流失率可視化
# g1=sns.barplot(x=feature,y='流失率',hue='Churn',data=a3)
# plt.title('{0} 維度流失率'.format(feature)) #添加標(biāo)題
# #得到標(biāo)簽數(shù)據(jù)
# #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列、數(shù)值列特征以及標(biāo)簽'Churn';'SeniorCitizen'不是真正的數(shù)值特征,算是分類特征,保留在列表內(nèi)
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('*********************************************************')



'gender':男女流失率基本相同
'SeniorCitizen':老人流失率高于非老人群體
'Partner':沒有伴侶的流失率高于有伴侶的
'Dependents':沒有家屬的高于有家屬的
'PhoneService'、'MultipleLines Churn':對流失率影響差異不明顯
'InternetService'、'OnlineSecurity'、'OnlineBackup'、'DeviceProtection'、'TechSupport'、
'StreamingTV'、'StreamingMovies'、'Contract'、'PaperlessBilling '、'PaymentMethod':對流失率有明細(xì)影響
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")
#設(shè)置字體大小
plt.rcParams.update({'font.size': 20})
plt.legend(fontsize=10)
# plt.xlim([-10,10]) #入網(wǎng)3個(gè)月左右流失量達(dá)到峰值,入網(wǎng)時(shí)長越高流失越少

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")
#設(shè)置字體大小
plt.rcParams.update({'font.size': 20})
plt.legend(fontsize=10) #沒有付費(fèi)80-120區(qū)間流失量高

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")
#設(shè)置字體大小
plt.rcParams.update({'font.size': 20})
plt.legend(fontsize=10)
# plt.xlim([-2000,400]) #總花費(fèi)300-400時(shí)流失較多

五、數(shù)據(jù)預(yù)處理
del df['customerID'] #刪除ID列
#1、特征縮放
# 數(shù)值化
#使用pd.get_dummies(df),把數(shù)據(jù)OneHotEncoder編碼;把標(biāo)簽y轉(zhuǎn)化為正負(fù)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)
#特征縮放-數(shù)值特征標(biāo)準(zhǔn)化
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種方法的結(jié)果
print(skb.get_support())
print(rfe.get_support())
print(sfm.get_support())

#訓(xùn)練集、測試集拆分
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)
#構(gòu)造模型
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 #支持向量機(jī)
from sklearn.ensemble import RandomForestClassifier,AdaBoostClassifier #隨機(jī)森林(bagging,boost-Adaboost)
from sklearn.metrics import accuracy_score,recall_score,f1_score #模型評估,準(zhǔn)確率,召回率,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))
模型結(jié)果




根據(jù)以上分析,得到高流失率用戶的特征:
1、老年用戶,未婚用戶,無親屬用戶更容易流失;
2、在網(wǎng)時(shí)長小于半年,有電話服務(wù),光纖用戶/光纖用戶附加流媒體電視、電影服務(wù),無互聯(lián)網(wǎng)服務(wù);
3、簽訂的合同期較短,采用電子支票支付,是電子賬單,月租費(fèi)約80-120元的客戶容易流失;
其它屬性對用戶流失影響較小,以上特征保持獨(dú)立。
針對上述結(jié)論,從業(yè)務(wù)角度給出相應(yīng)建議:
根據(jù)預(yù)測模型,定期構(gòu)建一個(gè)高流失率的用戶列表,針對不同用戶制定相應(yīng)營銷舉措,挽留用戶流失。