对于互联网公司而言,用户流失是个不可避免的问题,据统计,获取一个新用户的成本比召回一个新用户的成本要大得多。如果能降低用户的流失率,那么公司的成本会显著降低。
本文通过分析一个电信公司流失用户分析的案例,将机器学习应用到实际工作中。
一、导入数据
from __future__ import division # 精确除法,“/”操作执行的是截断除法
import pandas as pd
import numpy as np
churn_df = pd.read_csv('Desktop/用户流失预警/churn.csv')#导入数据
col_names = churn_df.columns.tolist() #所有的列名展示出来
print("Column names:")
print(col_names)
数据简介
- State:州名
- Account Length:账户长度
- Area Code:区号
- Phone:电话号码
- ‘Int'l Plan:国际漫游需求与否
- VMail Plan:参与活动
- VMail Message:语音邮箱
- Day Mins:白天通话分钟数
- Day Calls:白天打电话个数
- Day Charge:白天收费情况
- Eve Mins:晚间通话分钟数
- Eve Calls:晚间打电话个数
- Eve Charge:晚间收费情况
- Night Mins:夜间通话分钟数
- Night Calls:夜间打电话个数
- Night Charge:夜间收费情况
- Intl Mins:国际通话分钟数
- Intl Calls:国际打电话个数
- Intl Charge:国际收费
- CustServ Calls:客服电话数量
- Churn:流失与否
to_show = col_names[:6] + col_names[-6:] #前6列和后6列
print("\nSample data:")
churn_df[to_show].head(6)
image.png
当我们拿到数据时,需要先对根据数据列名进行分析,这个数据共有21列,分别是用户信息和特征,最后一列就是他们的分类:流失与否,也是我们本次判断的目标。我们需要根据这些特征,训练一个准确率较高的模型。从机器学习的分类来讲, 这是一个监督问题中的分类问题,具体来说, 是一个二分类问题。
二,数据清洗
当我们拿到数据,了解此次分析的目标之后,便可以对数据进行处理了。
churn_df.info() # 查看数据整体情况,看是否有缺失值。
image.png
在处理缺失值的过程中,我们一般有这样的思路:如果是连续值,那么可以使用均数、众数、中位数来代替。如果是str类型的数据,那么可以使用频次最高的数据代替。这份数据比较完整,因此也不需要处理缺失值。
churn_df.describe() #describe() 展示每一列数据的描述性统计信息
Account Length Area Code VMail Message Day Mins Day Calls Day Charge Eve Mins Eve Calls Eve Charge Night Mins Night Calls Night Charge Intl Mins Intl Calls Intl Charge CustServ Calls
count 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000 3333.000000
mean 101.064806 437.182418 8.099010 179.775098 100.435644 30.562307 200.980348 100.114311 17.083540 200.872037 100.107711 9.039325 10.237294 4.479448 2.764581 1.562856
std 39.822106 42.371290 13.688365 54.467389 20.069084 9.259435 50.713844 19.922625 4.310668 50.573847 19.568609 2.275873 2.791840 2.461214 0.753773 1.315491
min 1.000000 408.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 23.200000 33.000000 1.040000 0.000000 0.000000 0.000000 0.000000
25% 74.000000 408.000000 0.000000 143.700000 87.000000 24.430000 166.600000 87.000000 14.160000 167.000000 87.000000 7.520000 8.500000 3.000000 2.300000 1.000000
50% 101.000000 415.000000 0.000000 179.400000 101.000000 30.500000 201.400000 100.000000 17.120000 201.200000 100.000000 9.050000 10.300000 4.000000 2.780000 1.000000
75% 127.000000 510.000000 20.000000 216.400000 114.000000 36.790000 235.300000 114.000000 20.000000 235.300000 113.000000 10.590000 12.100000 6.000000 3.270000 2.000000
max 243.000000 510.000000 51.000000 350.800000 165.000000 59.640000 363.700000 170.000000 30.910000 395.000000 175.000000 17.770000 20.000000 20.000000 5.400000 9.000000
三,探索性数据分析
- Step1.特征自己的信息
#我们先来看一下流失比例, 以及关于打客户电话的个数分布
import matplotlib.pyplot as plt # 仿真
%matplotlib inline
fig = plt.figure()
fig.set(alpha=0.3) # 设定图表颜色alpha参数
#subplot2grid(shape , loc )
plt.subplot2grid((1,2),(0,0))# 图像几行几列,从第0行第0列,
# line bar barsh kde
churn_df['Churn?'].value_counts().plot(kind='bar') #把用户是否流失分组起来,流失的有多少人,没有流失的有多少人
plt.title(u"stat for churn") # 设置标题
plt.ylabel(u"number") #流失与否的数量,一共3333行,没有流失的约占2700 ,流失的占500左右
plt.subplot2grid((1,2),(0,1))
churn_df[u'CustServ Calls'].value_counts().plot(kind='bar') # 客服电话, 客户打电话投诉多那流失率可能会大
plt.title("stat for cusServCalls") # 标题
plt.ylabel(u"number") #客户打1个客服电话的有1400个左右,客户.....总计加起来有3333个
plt.show()
image.png
- 一共3333个样本,False代表没有流失2700个左右 , True代表流失的为400个左右
- 客户打1个客服电话的有1400个左右,客户打2个客服电话的有760个人个左右,客户.....总计加起来有3333个
import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
plt.subplot2grid((1,3),(0,0)) # 在一张大图里分列几个小图
churn_df['Day Mins'].plot(kind='kde') # 白天通话分钟数,图用的kde的图例
plt.xlabel(u"Mins")# 横轴是分钟数
plt.ylabel(u"density") # density:密度
plt.title(u"dis for day mins") #标题
plt.subplot2grid((1,3),(0,1))
churn_df['Day Calls'].plot(kind='kde')# 白天打电话个数
plt.xlabel(u"call")# 客户打电话个数
plt.ylabel(u"density") #密度
plt.title(u"dis for day calls") #标题
plt.subplot2grid((1,3),(0,2))
churn_df['Day Charge'].plot(kind='kde') # 白天收费情况
plt.xlabel(u"Charge")# 横轴是白天收费情况
plt.ylabel(u"density") #密度
plt.title(u"dis for day charge")
plt.show()
image.png
- Step.2 特征和分类的关联
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
#查看流失与国际漫游之间的关系
int_yes = churn_df['Churn?'][churn_df['Int\'l Plan'] == 'yes'].value_counts() # 分组,yes:参与了有国际漫游需求的统计出来
int_no = churn_df['Churn?'][churn_df['Int\'l Plan'] == 'no'].value_counts() #分组:no:没有参与国际漫游的统计出来
#用DataFrame做图例上的标签 ,在右上角
df_int=pd.DataFrame({u'int plan':int_yes, u'no int plan':int_no})
df_int.plot(kind='bar', stacked=True)
plt.title(u"statistic between int plan and churn")
plt.xlabel(u"int or not")
plt.ylabel(u"number")
plt.show()
image.png
参与国际漫游的的用户流失率较高。 猜测也许他们有更多的选择, 或者对服务有更多的要求。 需要特别对待。
#查看客户服务电话和结果的关联
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
cus_0 = churn_df['CustServ Calls'][churn_df['Churn?'] == 'False.'].value_counts()
cus_1 = churn_df['CustServ Calls'][churn_df['Churn?'] == 'True.'].value_counts()
df=pd.DataFrame({u'churn':cus_1, u'retain':cus_0})
df.plot(kind='bar', stacked=True)
plt.title(u"Static between customer service call and churn")
plt.xlabel(u"Call service")
plt.ylabel(u"Num")
plt.show()
image.png
四,特征筛选
- 根据对问题的分析, 我们做第一件事情, 去除三列无关列。 州名, 电话, 区号
- 转化成数值类型:对于有些特征, 本身不是数值类型的, 这些数据是不能被算法直接使用的, 所以我们来处理一下
# 对于标签数据需要整合
ds_result = churn_df['Churn?']
#shift+tab:condition是布尔类型的数组,每个条件都和x ,y 对应
#等于True为1 ,等于False为0
Y = np.where(ds_result == 'True.',1,0)
dummies_int = pd.get_dummies(churn_df['Int\'l Plan'], prefix='_int\'l Plan') #prefix:前缀
# VMail Plan:某个策划活动 prefix:前缀
dummies_voice = pd.get_dummies(churn_df['VMail Plan'], prefix='VMail')
#concat:用来合并2个或者2个以上的数组
ds_tmp=pd.concat([churn_df, dummies_int, dummies_voice], axis=1)
# 删除州名、地区编号、手机号、用户是否流失、各种策略活动
to_drop = ['State','Area Code','Phone','Churn?', 'Int\'l Plan', 'VMail Plan']
df = ds_tmp.drop(to_drop,axis=1)
print("after convert ")
df.head(5)
image.png
# 整理好的数据拿过来
churn_result = churn_df['Churn?']
y = np.where(churn_result == 'True.',1,0)
to_drop = ['State','Area Code','Phone','Churn?']
churn_feat_space = churn_df.drop(to_drop,axis=1)
yes_no_cols = ["Int'l Plan","VMail Plan"]
churn_feat_space[yes_no_cols] = churn_feat_space[yes_no_cols] == 'yes'
features = churn_feat_space.columns
X = churn_feat_space.as_matrix().astype(np.float)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)
print("Feature space holds %d observations and %d features" % X.shape)
print("---------------------------------")
print("Unique target labels:", np.unique(y))
print("---------------------------------")
print(X[0])#第1行
print("---------------------------------")
print(len(y[y == 0]))
image.png
五 建立多种基础模型,尝试多种算法
# 调入工具包
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score,KFold
from sklearn.neighbors import KNeighborsClassifier
import matplotlib.pyplot as plt
# 初始化模型
models = []
models.append(('KNN', KNeighborsClassifier()))
models.append(('LR', LogisticRegression()))
models.append(('SVM', SVC()))
# 初始化
results = []
names = []
scoring = 'accuracy' # 准确率
for name, model in models:
#random_state = 0
kfold = KFold(5,shuffle=True,random_state = 0) # 5折
cv_results = cross_val_score(model, X, Y, cv=kfold)#scoring=scoring 默认为None
results.append(cv_results)#交叉验证给的结果分
names.append(name)
#模型的标准差,体现模型的分值的波动,std越小越稳定
msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std())
print(msg)
print("------------------------------")
# boxplot algorithm comparison
fig = plt.figure()
fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
plt.boxplot(results)
ax.set_xticklabels(names)
plt.show()
# 总结:SVM的效果比较好
image.png
image.png
六 模型调参/提升模型
from sklearn.ensemble import RandomForestClassifier as RF
num_trees = 100
max_features = 3
kfold = KFold(n_splits=10, random_state=7)
model = RF(n_estimators=num_trees, max_features=max_features)
results = cross_val_score(model, X, Y, cv=kfold)
print(results.mean())
0.9522954091816367
from sklearn.ensemble import GradientBoostingClassifier
seed = 7
num_trees = 100
kfold = KFold(n_splits=10, random_state=seed)
model = GradientBoostingClassifier(n_estimators=num_trees, random_state=seed)
results = cross_val_score(model, X, Y, cv=kfold)
print(results.mean())
0.9525966085846325
七 评估测试/结论汇报
def run_prob_cv(X, y, clf_class, **kwargs):
kf = KFold(5,True)
y_prob = np.zeros((len(y),2))
for train_index, test_index in kf.split(X):
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
# Predict probabilities, not classes
y_prob[test_index] = clf.predict_proba(X_test) #返回的是概率值 ,属于0的概率多少,属于1的概率是多少
return y_prob
import warnings
warnings.filterwarnings('ignore')
# Use 10 estimators so predictions are all multiples of 0.1
pred_prob = run_prob_cv(X, y, RF, n_estimators=10)
#print pred_prob[0]
pred_churn = pred_prob[:,1]#只要属于1的概率是多少 ,因为咱们关注的是流失的
is_churn = y == 1
# Number of times a predicted probability is assigned to an observation
counts = pd.value_counts(pred_churn) # 属于1的概率多少进行分组统计 , 即:pred_prob count
#print counts
# calculate true probabilities
true_prob = {}
for prob in counts.index:
true_prob[prob] = np.mean(is_churn[pred_churn == prob])
true_prob = pd.Series(true_prob)
# pandas-fu
counts = pd.concat([counts,true_prob], axis=1).reset_index()
counts.columns = ['pred_prob', 'count', 'true_prob']
counts
image.png
网友评论