数据驱动分析
实践四 客户留存分析
在本系列前三篇文章中,我们已经定义了我们的指标、进行了客户分段并且建立机器学习模型来预测客户的LTV。由于我们通过客户分段和LTV预测知道了谁是我们最好的客户,我们应该尽力去留住这些客户。在这种情况下,客户留存率就成为了一个非常重要的指标值得我们去深入研究和分析。
留存率(Retention Ratio)指示了产品的市场适合度(Product Market Fit, PMF)。如果PMF没有满足,客户会迅速流失。改善留存率的一个强大的工具就是客户留存预测分析(Churn Analysis)。通过这种分析方法,你可以很轻易地了解在一个给定期间内哪些客户比较容易流失。
本篇文章,我们使用Telco数据集来开发一个留存预测模型,主要步骤包括:
- 探索式数据分析(Exploratory Data Analysis, EDA)
- 特征工程
- 使用逻辑回归来调查各个特征是如何影响留存的
- 使用XGBoost来建立一个分类模型
探索式数据分析(Exploratory Data Analysis)
import pandas as pd
%matplotlib inline
from sklearn.metrics import classification_report,confusion_matrix
import matplotlib.pyplot as plt
import numpy as np
import xgboost as xgb
df_data = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
df_data.head()
df_data.info()
此数据集的特征可以分为两类:
- 分类特征:gender,streaming tv,payment method等
- 数值特征:tenure,monthly charges,total charges等
我们首先调查以下分类特征对客户流失的预测能力。
df_data['Churn'] = np.where(df_data['Churn']=='No',0,1)
性别 (gender)
import chart_studio.plotly as py
import plotly.offline as pyoff
import plotly.graph_objs as go
pyoff.init_notebook_mode()
def featureEDA(feature):
df_plot = df_data.groupby(feature).Churn.mean().reset_index()
plot_data = [
go.Bar(
x=df_plot[feature],
y=df_plot['Churn'],
width = [0.5, 0.5],
marker=dict(
color=['green', 'blue','orange','red'])
)
]
plot_layout = go.Layout(
xaxis={"type": "category"},
yaxis={"title": "Churn Rate"},
title=feature,
plot_bgcolor = 'rgb(243,243,243)',
paper_bgcolor = 'rgb(243,243,243)',
)
fig = go.Figure(data=plot_data, layout=plot_layout)
pyoff.iplot(fig)
featureEDA('gender')
df_data.groupby('gender').Churn.mean()
gender
Female 0.269209
Male 0.261603
Name: Churn, dtype: float64
相对于男性客户,女性客户更容易流失,但是这个差别非常小。
下面我们对所有的分类特征都做类似的调研:
Internet Service
featureEDA('InternetService')
上图显示拥有光线入网的互联网服务的客户的流失率更高。
Contract
featureEDA('Contract')
正如所料,合同时间较短的更容易流失。
Tech Support
featureEDA('TechSupport')
没有使用技术支持服务的客户更容易流失,差了25%。
支付方式
featureEDA('PaymentMethod')
支付自动化更容易留住客户。
其他分类特征
from plotly.subplots import make_subplots
fig = make_subplots(rows=2, cols=2, start_cell="bottom-left",
subplot_titles=("PhoneService", "DeviceProtection","StreamingTV","PaperlessBilling"))
df_plot1 = df_data.groupby('PhoneService').Churn.mean().reset_index()
df_plot2 = df_data.groupby('DeviceProtection').Churn.mean().reset_index()
df_plot3 = df_data.groupby('StreamingTV').Churn.mean().reset_index()
df_plot4 = df_data.groupby('PaperlessBilling').Churn.mean().reset_index()
fig.add_trace(
go.Bar(
x=df_plot1['PhoneService'],
y=df_plot1['Churn'],
width = [0.5, 0.5],
name="Phone Service",
marker=dict(
color=['green', 'blue','orange','red'])
),
row=1,col=1
)
fig.add_trace(
go.Bar(
x=df_plot2['DeviceProtection'],
y=df_plot2['Churn'],
width = [0.5, 0.5],
name="Device Protection",
marker=dict(
color=['green', 'blue','orange','red'])
),
row=1,col=2
)
fig.add_trace(
go.Bar(
x=df_plot3['StreamingTV'],
y=df_plot3['Churn'],
width = [0.5, 0.5],
name="Streaming TV",
marker=dict(
color=['green', 'blue','orange','red'])
),
row=2,col=1
)
fig.add_trace(
go.Bar(
x=df_plot4['PaperlessBilling'],
y=df_plot4['Churn'],
name="Paperless Billing",
width = [0.5, 0.5],
marker=dict(
color=['green', 'blue','orange','red'])
),
row=2,col=2
)
pyoff.iplot(fig)
到这里,我们已经完成了对分类特征的探索,下面我们要对数值特征进行调研。
保有期(Tenure)
df_plot = df_data.groupby('tenure').Churn.mean().reset_index()
plot_data = [
go.Scatter(
x=df_plot['tenure'],
y=df_plot['Churn'],
mode='markers',
name='Low',
marker= dict(size= 7,
line= dict(width=1),
color= 'blue',
opacity= 0.8
),
)
]
plot_layout = go.Layout(
yaxis= {'title': "Churn Rate"},
xaxis= {'title': "Tenure"},
title='Tenure based Churn rate',
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",
)
fig = go.Figure(data=plot_data, layout=plot_layout)
pyoff.iplot(fig)
非常明显,高保有期的流失率会更低。
下面我们采用同样方法看一下月费和总费用(Month and Total charge)
fig = make_subplots(rows=1, cols=2,
subplot_titles=("Monthly Charge", "Total Charge"))
df_plot1 = df_data.groupby('MonthlyCharges').Churn.mean().reset_index()
df_plot2 = df_data.groupby('TotalCharges').Churn.mean().reset_index()
fig.add_trace(
go.Scatter(
x=df_plot1['MonthlyCharges'],
y=df_plot1['Churn'],
mode='markers',
marker= dict(size= 7,
line= dict(width=1),
color= 'blue',
opacity= 0.8
),
),
row=1,col=1
)
fig.add_trace(
go.Scatter(
x=df_plot2['TotalCharges'],
y=df_plot2['Churn'],
mode='markers',
marker= dict(size= 7,
line= dict(width=1),
color= 'blue',
opacity= 0.8
),
),
row=1,col=2
)
pyoff.iplot(fig)
不幸的是,并没有发现费用与流失率之间的显著性关联。
特征工程
我们将转换数据中的原始特征以求来获取更多的信息,策略如下:
- 使用聚类技术将数值特征分组
- 对二分类特征应用标签编码
- 对多值分类数据特征使用哑元变量
数值特征
EDA部分使我们知道数据中有三个数值特征:
- Tenture
- Monthly Charges
- Total Charges
我们将使用如下步骤来创建分组:
- 应用Elbow Method来确定合适的聚类数目
- 使用Kmeans进行聚类
- 观察聚类的情况和效果
from sklearn.cluster import KMeans
from sklearn.model_selection import KFold, cross_val_score, train_test_split
def order_cluster(cluster_field_name, target_field_name,df,ascending):
new_cluster_field_name = 'new_' + cluster_field_name
df_new = df.groupby(cluster_field_name)[target_field_name].mean().reset_index()
df_new = df_new.sort_values(by=target_field_name,ascending=ascending).reset_index(drop=True)
df_new['index'] = df_new.index
df_final = pd.merge(df,df_new[[cluster_field_name,'index']], on=cluster_field_name)
df_final = df_final.drop([cluster_field_name],axis=1)
df_final = df_final.rename(columns={"index":cluster_field_name})
return df_final
sse={}
df_cluster = df_data[['tenure']]
for k in range(1, 10):
kmeans = KMeans(n_clusters=k, max_iter=1000).fit(df_cluster)
df_cluster["clusters"] = kmeans.labels_
sse[k] = kmeans.inertia_
plt.figure()
plt.plot(list(sse.keys()), list(sse.values()))
plt.xlabel("Number of cluster")
plt.show()
kmeans = KMeans(n_clusters=3)
kmeans.fit(df_data[['tenure']])
df_data['TenureCluster'] = kmeans.predict(df_data[['tenure']])
df_data = order_cluster('TenureCluster', 'tenure',df_data,True)
df_data.groupby('TenureCluster').tenure.describe()
df_data['TenureCluster'] = df_data["TenureCluster"].replace({0:'Low',1:'Mid',2:'High'})
df_data.groupby('TenureCluster').tenure.describe()
df_plot = df_data.groupby('TenureCluster').Churn.mean().reset_index()
plot_data = [
go.Bar(
x=df_plot['TenureCluster'],
y=df_plot['Churn'],
width = [0.5, 0.5, 0.5,0.5],
marker=dict(
color=['green', 'blue', 'orange','red'])
)
]
plot_layout = go.Layout(
xaxis={"type": "category","categoryarray":['Low','Mid','High']},
title='Tenure Cluster vs Churn Rate',
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",
)
fig = go.Figure(data=plot_data, layout=plot_layout)
pyoff.iplot(fig)
同样方法应用于Month Charges和Total Charges
sse={}
df_cluster = df_data[['MonthlyCharges']]
for k in range(1, 10):
kmeans = KMeans(n_clusters=k, max_iter=1000).fit(df_cluster)
df_cluster["clusters"] = kmeans.labels_
sse[k] = kmeans.inertia_
plt.figure()
plt.plot(list(sse.keys()), list(sse.values()))
plt.xlabel("Number of cluster")
plt.show()
kmeans = KMeans(n_clusters=3)
kmeans.fit(df_data[['MonthlyCharges']])
df_data['MonthlyChargeCluster'] = kmeans.predict(df_data[['MonthlyCharges']])
df_data = order_cluster('MonthlyChargeCluster', 'MonthlyCharges',df_data,True)
df_data.groupby('MonthlyChargeCluster').MonthlyCharges.describe()
df_data['MonthlyChargeCluster'] = df_data["MonthlyChargeCluster"].replace({0:'Low',1:'Mid',2:'High'})
df_plot = df_data.groupby('MonthlyChargeCluster').Churn.mean().reset_index()
plot_data = [
go.Bar(
x=df_plot['MonthlyChargeCluster'],
y=df_plot['Churn'],
width = [0.5, 0.5, 0.5],
marker=dict(
color=['green', 'blue', 'orange'])
)
]
plot_layout = go.Layout(
xaxis={"type": "category","categoryarray":['Low','Mid','High']},
title='Monthly Charge Cluster vs Churn Rate',
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",
)
fig = go.Figure(data=plot_data, layout=plot_layout)
pyoff.iplot(fig)
df_data[pd.to_numeric(df_data['TotalCharges'], errors='coerce').isnull()]
df_data.loc[pd.to_numeric(df_data['TotalCharges'], errors='coerce').isnull(),'TotalCharges'] = np.nan
df_data = df_data.dropna()
df_data['TotalCharges'] = pd.to_numeric(df_data['TotalCharges'], errors='coerce')
sse={}
df_cluster = df_data[['TotalCharges']]
for k in range(1, 10):
kmeans = KMeans(n_clusters=k, max_iter=1000).fit(df_cluster)
df_cluster["clusters"] = kmeans.labels_
sse[k] = kmeans.inertia_
plt.figure()
plt.plot(list(sse.keys()), list(sse.values()))
plt.xlabel("Number of cluster")
plt.show()
kmeans = KMeans(n_clusters=3)
kmeans.fit(df_data[['TotalCharges']])
df_data['TotalChargeCluster'] = kmeans.predict(df_data[['TotalCharges']])
df_data = order_cluster('TotalChargeCluster', 'TotalCharges',df_data,True)
df_data.groupby('TotalChargeCluster').TotalCharges.describe()
df_data['TotalChargeCluster'] = df_data["TotalChargeCluster"].replace({0:'Low',1:'Mid',2:'High'})
df_plot = df_data.groupby('TotalChargeCluster').Churn.mean().reset_index()
plot_data = [
go.Bar(
x=df_plot['TotalChargeCluster'],
y=df_plot['Churn'],
width = [0.5, 0.5, 0.5],
marker=dict(
color=['green', 'blue', 'orange'])
)
]
plot_layout = go.Layout(
xaxis={"type": "category","categoryarray":['Low','Mid','High']},
title='Total Charge Cluster vs Churn Rate',
plot_bgcolor = "rgb(243,243,243)",
paper_bgcolor = "rgb(243,243,243)",
)
fig = go.Figure(data=plot_data, layout=plot_layout)
pyoff.iplot(fig)
处理分类变量
#import Label Encoder
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
dummy_columns = []
for column in df_data.columns:
if df_data[column].dtype == object and column != 'customerID':
if df_data[column].nunique() == 2:
#apply Label Encoder for binary ones
df_data[column] = le.fit_transform(df_data[column])
else:
dummy_columns.append(column)#apply get dummies for selected columns
df_data = pd.get_dummies(data = df_data,columns = dummy_columns)
df_data[['gender','Partner','TenureCluster_High','TenureCluster_Low','TenureCluster_Mid']].head()
使用逻辑回归
预测流失实际上是一个二分类问题。客户在给定期间之内或者流失或者留存。
all_columns = []
for column in df_data.columns:
column = column.replace(" ", "_").replace("(", "_").replace(")", "_").replace("-", "_")
all_columns.append(column)
df_data.columns = all_columns
glm_columns = 'gender'
for column in df_data.columns:
if column not in ['Churn','customerID','gender']:
glm_columns = glm_columns + ' + ' + column
import statsmodels.api as sm
import statsmodels.formula.api as smf
glm_model = smf.glm(formula='Churn ~ {}'.format(glm_columns), data=df_data, family=sm.families.Binomial())
res = glm_model.fit()
print(res.summary())
从模型报告中我们可以读出两个非常重要的信息。在建立流失预测模型中,我们一定会面对的:
- 哪一个特征会影响客户流失或者留存?
- 哪些是最重要的?我们应该聚焦在哪些方面?
对于第一个问题,我们应该观察第四列(P>|z|)。如果这个值小于0.05,则说明其对应的特征对于影响客户流失具有统计显著性。例如
- Senior Citizen
- InternetService_DSL
- OnlineSecurity_NO
对于第二个问题,如果我们想降低流失率,我们应该从什么地方开始?我们不妨换一种更为科学的问法。
哪一个特征的改变会为我们带来更高的ROI?
这个问题可以从coef列中得到答案,指数coef告诉了我们当改变该特征的一个单位会为我们带来的流失率的期望改变。下面的数据是这些系数的转换版本。
np.exp(res.params)
举例来说,如果其他因素保持不变,当Monthly Charges改变一个单位将会带来约3.4%的流失率改善。从上面的表中,我们可以很快确定哪些因素是比较重要的。
建立XGBoost预测模型
现在,建立预测模型的条件都已经具备。为了应用XGBoost模型,我们需要准备特征集合X和标签y,并且拆分训练和测试集。
#create feature set and labels
X = df_data.drop(['Churn','customerID'],axis=1)
y = df_data.Churn#train and test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.05, random_state=56)#building the model & printing the score
xgb_model = xgb.XGBClassifier(max_depth=5, learning_rate=0.08, objective= 'binary:logistic',n_jobs=-1).fit(X_train, y_train)
print('Accuracy of XGB classifier on training set: {:.2f}'
.format(xgb_model.score(X_train, y_train)))
print('Accuracy of XGB classifier on test set: {:.2f}'
.format(xgb_model.score(X_test[X_train.columns], y_test)))
Accuracy of XGB classifier on training set: 0.84
Accuracy of XGB classifier on test set: 0.78
在测试集上我们取得了78%的正确率。在数据集中流失率为26.5%,这意味者模型应该显著高于73.5%才有意义。让我们来检查一下模型的情况。
y_pred = xgb_model.predict(X_test)
print(classification_report(y_test, y_pred))
从上面的报告中不难看出,流失的召回率(recall)是一个最主要的问题,可以考虑通过一下方法来改善模型:
- 增加更多的数据
- 增加更多的特征
- 更细致化的特征工程
- 尝试其他模型
- 超参数调优
进一步说,我们需要了解哪一个特征对模型的贡献最大
from xgboost import plot_importance
fig, ax = plt.subplots(figsize=(10,8))
plot_importance(xgb_model, ax=ax)
我们可以看到,TotalCharges和MonthlyCharges相对于其他特征是最重要的。
最后,可能使用这个模型最好的方式是为每一个客户分配一个流失概率,建立用户分段,然后在此基础上再构建策略。
df_data['proba'] = xgb_model.predict_proba(df_data[X_train.columns])[:,1]
df_data[['customerID','proba']].head()
现在我们可以了解到在客户分段上(结合第二篇和第三篇),哪些客户更可能流失,后续可以采用相应的策略来改善。下篇文章我们将聚焦在如何预测客户的下一个购买日。
未完待续,…