酒店预订网客户流失分析案例

这次分析某酒店预订网在2016-05-15至2016-05-21这一周内的预定信息,对其客户流失概率进行建模分析,并对客户进行用户画像和RFM模型分析,针对不同类别客户进行个性化的服务,以减少客户流失。
导入包

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 解决坐标轴刻度负号乱码
plt.rcParams['axes.unicode_minus'] = False
# 解决中文乱码问题
plt.rcParams['font.sans-serif'] = ['Simhei']
#显示全部特征
pd.set_option('display.max_columns', None)

%matplotlib inline

一、读入数据

rawdata=pd.read_csv('userlostprob.txt',sep='\t')
rawdata.head()
原始数据

可以看到,原始数据是存在大量缺失值的。

# 查看数据维度
rawdata.shape

维度为(689945, 51)。总共689945条样本数据,除去标签列和id列,总共49个特征。

# 查看每列数据类型
rawdata.info()

可以看到,除了预定时间和入住时间2列为字符型之外,其余均为数值型,我们之后只需要将预定时间和入住时间处理为数值型。

# 查看数据缺失情况
rawdata.isnull().mean()

数据缺失值较多,特别是historyvisit_7ordernum缺失达到88%。

# 标签分布
rawdata['label'].value_counts()

数据存在偏斜,但不平衡程度不大。

二、数据探索

1 预定日期和入住日期

# 访问日期和入住日期
# 入住时间人数统计
arrival=rawdata[['arrival']]
arrival['counta']=1
arrival=arrival.groupby('arrival').sum().reset_index()
# 访问时间人数统计
d=rawdata[['d']]
d['countd']=1
d=d.groupby('d').sum().reset_index()
# 合并入住时间和访问时间人数
time_table=pd.merge(arrival,d,left_on='arrival',right_on='d',how='left')
time_table.fillna(0,inplace=True)
del time_table['d']

# 画出日期与人数的关系图
import matplotlib.pyplot as plt
plt.figure(figsize=(13, 5));
plt.style.use('bmh')

x=range(len(time_table));
y1=time_table['counta'].values;
y2=time_table['countd'].values;
z=time_table['arrival'].values;
plt.plot(x,y1,c="r",label='入住人数');
plt.bar(x,y2,align="center",label='访问人数');
plt.xlabel('日期');
plt.ylabel('人数');
plt.xticks(x,z,fontsize=11,rotation=45);
plt.title('访问和入住人数图',fontsize=20)
plt.legend(fontsize=20)
# plt.show()
arrival & d

520那天预定人数和入住人数都达到峰值,因为情侣会出门“过节”。521之后入住人数就一路走低。后面有两个小突起是周末。

2 访问时间段

# 访问时间段
plt.figure(figsize=(15, 6))
plt.style.use('seaborn-colorblind')

plt.hist(rawdata['h'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('访问时间'); 
plt.ylabel('人数'); 
plt.title('访问时间段');
h

5点是访问人数最少的时点,这个时候大家都在睡觉。5点过后访问人数开始上升,在晚间9、10点的时间段,访问人数是最多的。

3 客户价值

# 客户价值刻画
import matplotlib.pyplot as plt
plt.figure(figsize=(20, 7))
plt.style.use('seaborn-colorblind')

x1=rawdata['customer_value_profit'].dropna()
x2=rawdata['ctrip_profits'].dropna()

plt.subplot(121)
plt.plot(x1,linewidth=0.5)
plt.title('客户近1年价值')

plt.subplot(122)
plt.plot(x2,linewidth=0.5)
plt.title('客户价值')
customer_value_profit & ctrip_profits

可以看到,“客户近1年价值”和“客户价值”两个特征是非常相关的,都可以用来表示[客户的价值]这么一个特征。同时可以看到,大部分的客户价值都处在0-100这个范围,但是有些客户价值非常大,设置达到了600,这些客户都可以在以后的分析中重点观察,因为他们是非常有“价值”的。

4 消费能力指数

# 消费能力指数
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.style.use('seaborn-colorblind')

plt.hist(rawdata['consuming_capacity'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('消费能力指数'); 
plt.ylabel('人数'); 
plt.title('消费能力指数图');
consuming_capacity

可以看到,消费能力指数的值范围是0-100,相当于对酒店客户(及潜在客户)的一个消费能力进行打分。指数值基本呈现一个正态分布的形状,大部分人的消费能力在30附近。当然,我们同时可以看到,消费能力达到近100的人数也非常多,说明在我们酒店的访问和入住客户中,有不在少数的群体是消费水平非常高的,土豪还是多啊。

5 价格敏感指数

# 价格敏感指数
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))

plt.style.use('seaborn-colorblind')
plt.hist(rawdata['price_sensitive'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('价格敏感指数'); plt.ylabel('人数'); 
plt.title('价格敏感指数图');
price_sensitive

价格敏感指数,用来反映客户对价格的一个在意程度。可以看到,出去两头的极值现象,中间的分布是一个右偏(正偏态)的数据,大部分人的价格敏感指数比较低,也就是说,大部分客户(及潜在客户)是对价格不是很敏感的,并不会一味地去追求低价的酒店和房间,或许,酒店方面不需要在定价方面花费太多的脑筋。当然,我们也会发现,100处的人数也并不少,还是存在一部分的群体对价格极度敏感的,如果是针对这一部分客户,用一些打折优惠的方式会有意想不到的成效。

6 入住酒店平均价格

# 酒店价格偏好
plt.figure(figsize=(20, 7))
plt.style.use('seaborn-colorblind')

plt.subplot(121)
plt.hist(rawdata['avgprice'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('酒店价格'); 
plt.ylabel('偏好人数'); 
plt.title('酒店价格偏好');

plt.subplot(122)
plt.hist(rawdata[rawdata['avgprice']<2000]['avgprice'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('酒店价格'); 
plt.ylabel('偏好人数'); 
plt.title('2000元以内酒店偏好');
avgprice

我们的avgprice酒店平均价格指标的范围是1-6383元,这是当然了,还是有土豪会去住这种近万元的酒店的。但是,我们从左图可以看到,酒店价格在1000以上的,选择的人就非常少了,价格在2000元以上的酒店就更加是没有人去选择了,绘制在图中已经就是看不到了。为了排除“土豪”们的数据,更加具体地看一看我们“小屁民”的酒店选择倾向,我们选择了酒店价格在2000元以下的数据重新绘制一张图,如右图。右图中可以更加明显地看出,消费者对酒店价格的选择,基本是一个正偏态的分布,大部分人会选择的平均价格在300元左右(基本就是7天、如家这类吧)。

7 酒店星级偏好

# 酒店星级偏好
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['starprefer'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('星级偏好程度'); plt.ylabel('选择人数'); 
plt.title('酒店星级偏好');
starprefer

8 用户年订单数

# 用户年订单数
plt.figure(figsize=(20, 7))
plt.style.use('seaborn-colorblind')

plt.subplot(121)
plt.hist(rawdata['ordernum_oneyear'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('年订单数'); 
plt.ylabel('人数'); 
plt.title('客户年订单数分布');

plt.subplot(122)
plt.hist(rawdata[rawdata['ordernum_oneyear']<100]['ordernum_oneyear'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('年订单数'); 
plt.ylabel('人数'); 
plt.title('年订单数100单内的分布');
ordernum_oneyear

9 订单取消率

# 订单取消率
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['ordercanceledprecent'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('订单取消率'); plt.ylabel('人数'); 
plt.title('订单取消率');
ordercanceledprecent

10 距离上一次预定的时间

# 距离上一次预定的时间
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['lasthtlordergap'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('间隔时长'); plt.ylabel('人数'); 
plt.title('距离上次预定的时间');
lasthtlordergap

这里的间隔时长我们并不清楚是个什么衡量单位,可能是一个时间戳计算而来的数字。但是通过上图,我们还是可以观察出,预定间隔时间越长的人数是递减的,相当多数的人订酒店还是比较频繁的,对于酒店方而言,可能意味着酒店的品质还可以,“熟客”会经常性地选择预定本酒店。

11 s_id会话

# 会话描述
# 生成会话表,flag是新客和老客的总人数,rate是新客和老客中最终预定的比率
s_table=rawdata[['label','sid']]
s_table['sid']=np.where(s_table['sid']==1,1,0)
s_table['flag']=1
s=s_table.groupby('sid').sum().reset_index()
s['rate']=s['label']/s['flag']                       # flag求和刚好是sid为0和1的个数,label求和刚好是流失人数,相除则为流失率

# 绘制柱状图
plt.figure(figsize=(15, 7))
plt.style.use('seaborn-colorblind')
label=("老客","新访")

plt.subplot(121)
percent=[s['flag'][0]/s['flag'].sum(),s['flag'][1]/s['flag'].sum()]
colors=['steelblue','lightskyblue']
plt.pie(percent,autopct='%.2f%%',labels=label,colors=colors)
plt.title('新老客户占比')

plt.subplot(122)
plt.bar(s['sid'],s['rate'],align="center",tick_label=label,hatch="///",edgecolor = 'k')
plt.ylabel('流失率'); 
plt.title('新老客户中的客户流失率')
s_id

会话id即SessionID,sessionID用来判断是同一次会话,是服务器分配给访问者的一个id。sid值为1,我们就可以认为用户是第一次访问酒店界面,即将其标记为“新访”(新的访问者)。上图左图展示了“新访”和“老客”的分布情况,右图展示了“新访”和“老客”中最终会预定的一个比例。可以看到,老客的预定概率比新客的预定概率稍微高一点。

12 酒店转换率

plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['hotelcr'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('酒店cr值');  
plt.title('酒店转换率');
hotelcr

13 酒店独立访客

# 当前酒店历史uv
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['hoteluv'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('酒店uv值');  
plt.title('酒店历史独立访客量');
hoteluv

14 当前酒店点评数

# 当前酒店点评数
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['commentnums'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('点评数量');  
plt.title('酒店点评数');
commentnums

15 当前酒店评分人数

# 当前酒店评分人数
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['novoters'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('点评人数');  
plt.title('酒店评分人数');
novoters

16 当前酒店历史订单取消率

# 当前酒店历史取消率
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.hist(rawdata['cancelrate'].dropna(), bins = 50, edgecolor = 'k');
plt.xlabel('订单取消率');  
plt.title('酒店订单取消率');
cancelrate

17 当前酒店可订最低价

# 当前酒店可订最低价
plt.style.use('bmh')
plt.figure(figsize=(10, 6))

plt.plot(rawdata['lowestprice'].dropna())
plt.xlabel('酒店最低价');  
plt.title('酒店最低价');
lowestprice

酒店的信息特征基本都是呈现右偏的。

三、特征工程

# 为了避免在原数据集上进行修改操作,我们将rawdata复制一份
rawdf=rawdata.copy()

1 列值处理

字符串类型的特征都需要处理成数值型才能建模。本例只有两个日期是字符串的,将其相减得到“提前预定的天数”,作为新的特征。

## 增加列
# 将两个日期变量由字符串转换为日期型格式
rawdf['arrival']=pd.to_datetime(rawdf['arrival'])
rawdf['d']=pd.to_datetime(rawdf['d'])
# 生成提前预定时间列
rawdf['day_advanced']=(rawdf['arrival']-rawdf['d']).dt.days

## 删除列
rawdf=rawdf.drop(['sampleid','d','arrival'],axis=1)

2 异常值处理

我们在之前数据可视化的过程中,发现有一些特征值中是存在异常值的,比如用户偏好价格会出现绝对值非常大的负值。因此,我们需要对这些异常值进行一定的处理。

# 将customer_value_profit、ctrip_profits中的负值按0处理
# 将delta_price1、delta_price2、lowestprice中的负值按中位数处理(之后可以试一试众数的效果)
filter1=['customer_value_profit','ctrip_profits']
filter2=['delta_price1','delta_price2','lowestprice']

for f in filter1:
    rawdf.loc[rawdf[f]<0,f] = 0

for f in filter2:
    rawdf.loc[rawdf[f]<0,f] = rawdf[f].median()

3 缺失值处理

我们之前查看过缺失值情况,特征值中只有iforderpv_24h、sid、h、day_advanced这四个是不存在缺失的,其他的44个特征都是存在缺失值的,并且大部分的缺失值都挺多的,因此,我们接下来需要对缺失值进行处理。

# 定义删除空值行列的函数
def nan_drop(df, axi, rate=0.5):
    df.dropna(axis=axi,thresh=df.shape[1-axi]*rate,inplace=True)
    
# 删除缺失值比例大于80%的行和列
print('删除空值前数据维度是:{}'.format(rawdf.shape))

nan_drop(rawdf,axi=0,rate=0.2)
nan_drop(rawdf,axi=1,rate=0.2)

print('删除空值后数据维度是:{}'.format(rawdf.shape))

可以看到,空值删除操作后,样本数据减少了100条,不算多,影响不大;特征值少了一个,进一步查看可以得知,historyvisit_7ordernum这一列被删除了,因为这一列的缺失值比例高达88%,数据缺失过多,我们将其删除。
接下来进行缺失值的填充。趋于正态分布的字段,使用均值填充:businessrate_pre2、cancelrate_pre、businessrate_pre;右偏分布的字段,使用中位数填充。

# 缺失值填充
def nan_fill(df):
    filter_mean=['businessrate_pre2','cancelrate_pre','businessrate_pre']
    for col in df.columns:
        if col in filter_mean:
            df[col]=df[col].fillna(df[col].mean())
        else:
            df[col]=df[col].fillna(df[col].median())
    return df

rawdf=nan_fill(rawdf)

4 极值处理

有些特征明显有异常大和异常小的值,这里分别用1%和99%分位数替换超过上下限的值。

# 极值处理
for col in rawdf.columns:
    percent1=np.percentile(rawdf[col],1)       # 该列的1%分位数
    percent99=np.percentile(rawdf[col],99)       # 该列的99%分位数
    
    rawdf.loc[rawdf[col]<percent1,col]=percent1    # 小于1%分位数的,用1%分位数填充
    rawdf.loc[rawdf[col]>percent99,col]=percent99    # 大于99%分位数的,用99%分位数填充

5 相关性分析

分别生成用户行为特征的相关性矩阵和酒店信息特征的相关性矩阵。

# 用户特征的相关性分析
# 用户特征提取
user_features=['visitnum_oneyear','starprefer','sid','price_sensitive','ordernum_oneyear','ordercanncelednum','ordercanceledprecent','lastpvgap',
               'lasthtlordergap','landhalfhours','iforderpv_24h','historyvisit_totalordernum','historyvisit_avghotelnum','h',
               'delta_price2','delta_price1','decisionhabit_user','customer_value_profit','ctrip_profits','cr','consuming_capacity','avgprice']
# 生成用户特征的相关性矩阵
corr_mat=rawdf[user_features].corr()

# 绘制用户特征的相关性矩阵热度图
# plt.matshow(corr_mat1,cmap='plasma')        #matshow不能显示数字,只能用seaborn了
fig,ax = plt.subplots(figsize=(18, 12))
sns.heatmap(corr_mat, xticklabels=True, yticklabels=True, square=False, linewidths=.5, annot=True, cmap='Blues')
用户行为特征相关性矩阵

从上面的相关性矩阵热度图上,我们可以一目了然地看到用户行为数据中各个特征之间的相关性大小,颜色越深,则表示相关性程度越大。

我们可以很清楚地从上图看到,不少特征之间是存在着高相关性的,除去对角线上的相关性为1之外,我们可以看到,ordernum_oneyear和historyvisit_totalordernum的相关性高达0.93,因为它们都是表示用户1年内的订单数,我们选择其中名字更好识别的ordernum_oneyear作为用户年订单数的特征。

除此之外,decisionhabit_user和historyvisit_avghotelnum相关性达到了0.89,说明也是高度相关的,说明可能用户的决策习惯就是根据用户近3个月的日均访问数来设定的,我们可以通过PCA提取一个主成分用来表示用户近期的日均访问量。

再就是customer_value_profit和ctrip_profits这两个特征之间相关性达到了0.85,这两个特征我们在上面的数据可视化中就有提到,表示的是不同时间长度下衡量的客户价值,必然是高度相关的,我们可以用PCA的方法提取出一个主成分来代表客户价值这么一个信息。

avgprice和consuming_capacity之间的相关性达到了0.91,同时starprefer与consuming_capacity相关性0.71,starprefer与avgprice相关性0.66,都比较高。这三个特征我们在数据可视化的部分也有提过,它们都代表了消费者的一个消费水平,消费能力越大,愿意或者说是会去选择的酒店的平均价格就会越高,对酒店的星级要求也会越高。在这里,我们将相关性太大的avgprice和consuming_capacity抽象为用户的消费水平。

delta_price1和delta_price2的相关性高达0.91,可以抽象出一个指标叫做“用户偏好价格”。

# 酒店信息特征的相关性分析
hotel_features=['hotelcr','hoteluv','commentnums','novoters','cancelrate','lowestprice','cr_pre','uv_pre','uv_pre2','businessrate_pre',
                'businessrate_pre2','customereval_pre2','commentnums_pre','commentnums_pre2','cancelrate_pre','novoters_pre','novoters_pre2',
                'deltaprice_pre2_t1','lowestprice_pre','lowestprice_pre2','firstorder_bu','historyvisit_visit_detailpagenum']
# 生成用户特征的相关性矩阵
corr_mat1=rawdf[hotel_features].corr()

fig,ax = plt.subplots(figsize=(18, 12))
sns.heatmap(corr_mat1, xticklabels=True, yticklabels=True, square=False, linewidths=.5, annot=True, cmap='Blues')
酒店信息特征相关性矩阵

novoters和commentnums相关性高达0.99。前者是当前点评人数,后者是当前点评数,可以抽象出“酒店热度”指标;

novoters_pre和commentnums_pre相关性高达0.99,可以抽象出“24小时内浏览次数最多的酒店热度”指标;

novoters_pre2和commentnums_pre2相关性高达0.99,可以抽象出“24小时内浏览酒店平均热度”指标;

cancelrate和hoteluv相关性0.76,和commentnums相关性0.84,和novoters相关性0.85,酒店的“人气”高,说明访问的频繁,历史取消率可能也会高一点。

uv_pre和uv_pre2相关性高达0.9;businessrate_pre和businessrate_pre2相关性高达0.84;commentnums_pre和commentnums_pre2相关性高达0.82;novoters_pre和novoters_pre2相关性高达0.83。这些指标之间都是“浏览最多的酒店的数据”和“浏览酒店的平均数据”的关系,相关性高是正常的,暂时不用抽象出其他的指标。

6 降维

# 之前我们通过相关性分析得出的重复列,删除
delete_columns = ['historyvisit_totalordernum','cityuvs']
rawdf.drop(delete_columns,axis=1,inplace= True)

print('删除后数据维度是:{}'.format(rawdf.shape))
from sklearn.decomposition import PCA

# 定义降维函数,将我们抽象出的特征代替原本的几个特征
def PCA_transform(df,col,new_col,n=1):
    pca=PCA(n_components=n)
    pca.fit(df[col])
    df[new_col]=pca.transform(df[col])                                          # 添加新生成列
    rawdf.drop(col,axis=1,inplace= True)                              # 删除原来的特征列

c_value=['customer_value_profit','ctrip_profits']                   # 用户价值
consume_level=['avgprice','consuming_capacity']                     # 用户消费水平
price_prefer=['delta_price1','delta_price2']                        # 用户偏好价格
hotel_hot=['commentnums','novoters']                                # 酒店热度
hotel_hot_pre=['commentnums_pre','novoters_pre']                    # 24小时内浏览次数最多的酒店热度
hotel_hot_pre2=['commentnums_pre2','novoters_pre2']                 # 24小时内浏览酒店的平均热度

PCA_transform(rawdf,c_value,'c_value')
PCA_transform(rawdf,consume_level,'consume_level')
PCA_transform(rawdf,price_prefer,'price_prefer')
PCA_transform(rawdf,hotel_hot,'hotel_hot')
PCA_transform(rawdf,hotel_hot_pre,'hotel_hot_pre')
PCA_transform(rawdf,hotel_hot_pre2,'hotel_hot_pre2')

print('PCA降维后数据维度是:{}'.format(rawdf.shape))

PCA降维后数据维度是:(689845, 40)。

7 数据标准化

因为后面的建模中要用到LR、SVM这些需要计算距离的分类模型,所以我们需要先将数据进行标准化处理。

# 数据标准化
from sklearn.preprocessing import StandardScaler

y=rawdf['label']
x=rawdf.drop('label',axis=1)

scaler = StandardScaler()
scaler.fit(x)

X= scaler.transform(x)

四、建模

先拆分训练集和测试集

# 拆分训练集和测试集
from sklearn.model_selection import train_test_split, GridSearchCV

X_train,X_test,y_train,y_test = train_test_split(X,y,test_size= 0.2,random_state=13)

1 逻辑回归

from sklearn.linear_model import LogisticRegression
from sklearn import metrics

lr = LogisticRegression()                                        # 实例化一个LR模型
lr.fit(X_train,y_train)                                          # 训练模型
y_prob = lr.predict_proba(X_test)[:,1]                           # 预测1类的概率
y_pred = lr.predict(X_test)                                      # 模型对测试集的预测结果
fpr_lr,tpr_lr,threshold_lr = metrics.roc_curve(y_test,y_prob)    # 获取真阳率、伪阳率、阈值
auc_lr = metrics.auc(fpr_lr,tpr_lr)                              # AUC得分
score_lr = metrics.accuracy_score(y_test,y_pred)                 # 模型准确率
print([score_lr,auc_lr])

[0.7389196123766933, 0.6979154122462893]

2 朴素贝叶斯

from sklearn.naive_bayes import GaussianNB
from sklearn import metrics

gnb = GaussianNB()                                                  # 建立高斯模型
gnb.fit(X_train,y_train)                                            # 训练模型
y_prob = gnb.predict_proba(X_test)[:,1]                             # 预测1类的概率 
y_pred = gnb.predict(X_test)                                        # 模型对测试集的预测结果
fpr_gnb,tpr_gnb,threshold_gnb = metrics.roc_curve(y_test,y_prob)     # 获取真阳率、伪阳率、阈值
auc_gnb = metrics.auc(fpr_gnb,tpr_gnb)                              # 模型准确率
score_gnb = metrics.accuracy_score(y_test,y_pred)
print([score_gnb,auc_gnb])

[0.6323376990483369, 0.6653944945916186]

3 支持向量机

from sklearn.svm import SVC
from sklearn import metrics

svc = SVC(kernel='rbf',C=1.0).fit(X_train,y_train)
y_prob = svc.decision_function(X_test)                              # 决策边界距离
y_pred = svc.predict(X_test)                                        # 模型对测试集的预测结果
fpr_svc,tpr_svc,threshold_svc = metrics.roc_curve(y_test,y_prob)     # 获取真阳率、伪阳率、阈值
auc_svc = metrics.auc(fpr_svc,tpr_svc)                              # 模型准确率
score_svc = metrics.accuracy_score(y_test,y_pred)
print([score_svc,auc_svc])

[0.7704774260884691, 0.7776290866238482]

4 决策树

from sklearn import tree
from sklearn import metrics

dtc = tree.DecisionTreeClassifier()                              # 建立决策树模型
dtc.fit(X_train,y_train)                                         # 训练模型
y_prob = dtc.predict_proba(X_test)[:,1]                          # 预测1类的概率
y_pred = dtc.predict(X_test)                                     # 模型对测试集的预测结果 
fpr_dtc,tpr_dtc,threshod_dtc= metrics.roc_curve(y_test,y_prob)   # 获取真阳率、伪阳率、阈值
score_dtc = metrics.accuracy_score(y_test,y_pred)                
auc_dtc = metrics.auc(fpr_dtc,tpr_dtc) 
print([score_dtc,auc_dtc])

[0.8756749704643797, 0.8479299034443152]

5 随机森林

from sklearn.ensemble import RandomForestClassifier
from sklearn import metrics

rfc = RandomForestClassifier()                                     # 建立随机森林分类器
rfc.fit(X_train,y_train)                                           # 训练随机森林模型
y_prob = rfc.predict_proba(X_test)[:,1]                            # 预测1类的概率
y_pred=rfc.predict(X_test)                                         # 模型对测试集的预测结果
fpr_rfc,tpr_rfc,threshold_rfc = metrics.roc_curve(y_test,y_prob)   # 获取真阳率、伪阳率、阈值  
auc_rfc = metrics.auc(fpr_rfc,tpr_rfc)                             # AUC得分
score_rfc = metrics.accuracy_score(y_test,y_pred)                  # 模型准确率
print([score_rfc,auc_rfc])

[0.8934543266965768, 0.9377285634331807]

6 XGBoost

import xgboost as xgb
from sklearn import metrics

# 读入训练数据集和测试集
dtrain=xgb.DMatrix(X_train,y_train)
dtest=xgb.DMatrix(X_test)

# 设置xgboost建模参数
params={'booster':'gbtree',
    'objective': 'binary:logistic',
    'eval_metric': 'auc',
    'max_depth':8,
    'gamma':0,
    'lambda':2,
    'subsample':0.7,
    'colsample_bytree':0.8,
    'min_child_weight':3,
    'eta': 0.2,
    'nthread':8,
     'silent':1}

# 训练模型
watchlist = [(dtrain,'train')]
bst=xgb.train(params,dtrain,num_boost_round=500,evals=watchlist)

# 输入预测为正类的概率值
y_prob=bst.predict(dtest)
# 设置阈值为0.5,得到测试集的预测结果
y_pred = (y_prob >= 0.5)*1
# 获取真阳率、伪阳率、阈值
fpr_xgb,tpr_xgb,threshold_xgb = metrics.roc_curve(y_test,y_prob)   
auc_xgb = metrics.auc(fpr_xgb,tpr_xgb)                             # AUC得分
score_xgb = metrics.accuracy_score(y_test,y_pred)                  # 模型准确率
print([score_xgb,auc_xgb])

[0.8921134457740507, 0.9417563281093095]

7 模型比较

画出ROC曲线

plt.style.use('bmh')
plt.figure(figsize=(13,10))

plt.plot(fpr_lr,tpr_lr,label='lr: 69.79%')                             # 逻辑回归
plt.plot(fpr_gnb,tpr_gnb,label='gnp:66.54%')                          # 朴素贝叶斯模型
plt.plot(fpr_svc,tpr_svc,label='svc:77.76%')                          # 支持向量机模型
plt.plot(fpr_dtc,tpr_dtc,label='dtc:84.79%')                          # 决策树
plt.plot(fpr_rfc,tpr_rfc,label='rfc:93.77%')                          # 随机森林
plt.plot(fpr_xgb,tpr_xgb,label='xgb:94.18%')                          # XGBoost

plt.legend(loc='lower right',prop={'size':25})
plt.xlabel('伪阳率')
plt.ylabel('真阳率')
plt.title('ROC曲线')
plt.show()
ROC曲线

可以看到,贝叶斯表现最差,逻辑回归的表现也不是很好,说明数据不是线性可分的。随机森林和xgboost的表现差不多,都很好,二者的AUC值都在0.9以上,分类效果可以说相当不错了。
可以调用xgb画出重要特征图。

from xgboost import plot_importance

# plt.figure(figsize=(13,10))
fig,ax = plt.subplots(figsize=(15,15))
plot_importance(bst,height=0.5,ax=ax,max_num_features=40)
特征重要性

重要的特征:24小时内是否访问订单填写页(24小时内是否访问订单填写页)、近3个月用户历史日均访问酒店数(historyvisit_avghotelnum)、当前酒店转换率(hotelcr)、当前酒店历史订单取消率(ordercanceledprecent)、星级偏好(starprefer)、用户历史取消率(cancelrate)、 7天内访问酒店详情页数(historyvisit_visit_detailpagenum)、价格敏感指数(price_sensitive)、当前酒店访客量(hoteluv)、浏览最多的酒店商务属性(businessrate_pre)。

五、RFM分析和用户画像

1 RFM分析

RFM模型,即为:
R(Rencency):最近一次消费
F(Frequency):消费频率
M(Monetary):消费金额



在本案例中,我们选择lasthtlordergap(距离上次下单的时长)、ordernum_oneyear(用户年订单数)、consume_level(用户消费水平)分别作为R、F、M的值,对我们的用户群体进行聚类。

rfm = rawdf[['lasthtlordergap','ordernum_oneyear','consume_level']]
rfm.rename(columns={'lasthtlordergap':'recency','ordernum_oneyear':'frequency','consume_level':'monetary'},inplace=True)
rfm.head()

# 进行归一化
# 数据标准化
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
scaler.fit(rfm)
rfm = scaler.transform(rfm)

# 分箱
rfm['R']=pd.qcut(rfm["recency"], 2)
rfm['F']=pd.qcut(rfm["frequency"], 2)
rfm['M']=pd.qcut(rfm["monetary"], 2)

# 编码
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder().fit(rfm['R'])
rfm['R']=le.transform(rfm['R'])
le = LabelEncoder().fit(rfm['F'])
rfm['F']=le.transform(rfm['F'])
le = LabelEncoder().fit(rfm['M'])
rfm['M']=le.transform(rfm['M'])
def get_label(r,f,m):
    if (r==0)&(f==1)&(m==1):
        return '高价值客户'
    if (r==1)&(f==1)&(m==1):
        return '重点保持客户'
    if((r==0)&(f==0)&(m==1)):
        return '重点发展客户'
    if (r==1)&(f==0)&(m==1):
        return '重点挽留客户'
    if (r==0)&(f==1)&(m==0):
        return '一般价值客户'
    if (r==1)&(f==1)&(m==0):
        return '一般保持客户'
    if (r==0)&(f==0)&(m==0):
        return '一般发展客户'
    if (r==1)&(f==0)&(m==0):
        return '潜在客户'

def RFM_convert(df):
    df['Label of Customer']=df.apply(lambda x:get_label(x['R'],x['F'],x['M']),axis=1)
    
    df['R']=np.where(df['R']==0,'高','低')
    df['F']=np.where(df['F']==1,'高','低')
    df['M']=np.where(df['M']==1,'高','低')
    
    return df[['R','F','M','Label of Customer']]

rfm0=RFM_convert(rfm)
rfm0.head(10)

我们可以看一下按照RFM划分的8类客户占比。

import matplotlib.pyplot as plt

tmp = rfm0.groupby('Label of Customer').size()

fig, ax = plt.subplots(figsize=(10,10))
colors=['deepskyblue','steelblue','lightskyblue','aliceblue','skyblue','cadetblue','cornflowerblue','dodgerblue']
ax.pie(tmp.values, radius=1,autopct='%1.1f%%',pctdistance=0.75,colors=colors)
ax.pie([1], radius=0.6,colors='w')
ax.set(aspect="equal", title='客户细分情况')
plt.legend(tmp.index,bbox_to_anchor=(1, 1), loc='best', borderaxespad=0.)
plt.show()

2 用户画像

其实我们并不想将用户分的这么细,并且我们其实有挺多的用户行为特征数据,所以也并不想仅用RFM这3个指标进行分析。所以,我们接下来用K-Means聚类的方法将用户分为3类,观察不同类别客户的特征。

# 选取出几个刻画用户的重要指标
user_feature = ['decisionhabit_user','ordercanncelednum','ordercanceledprecent','consume_level','starprefer','lasthtlordergap','lastpvgap','h','sid',
                'c_value','landhalfhours','price_sensitive','price_prefer','day_advanced','historyvisit_avghotelnum','ordernum_oneyear']
user_attributes = rawdf[user_feature]
user_attributes.head()
# 数据标准化
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(user_attributes)

user_attributes = scaler.transform(user_attributes)

进行聚类

from sklearn.cluster import KMeans

Kmeans=KMeans(n_clusters=3,random_state=13)                                     # 建立KMean模型
Kmeans.fit(user_attributes)                                                     # 训练模型
k_char=Kmeans.cluster_centers_                                                  # 得到每个分类的质心
personas=pd.DataFrame(k_char.T,index=user_feature,columns=['0类','1类','2类'])  # 用户画像表
personas
fig,ax = plt.subplots(figsize=(4, 8))
sns.heatmap(personas, xticklabels=True, yticklabels=True, square=False, linewidths=.5, annot=True, cmap='Blues')

我们可以看到,聚出来的3类用户有各自非常明显的特征:
可以看到,2类中蓝色格子明显最多,它的R(lasthtlordergap)为-0.17非常小,F(ordernum_oneyear)为1.1比较高了,M(consume_level)为1.3也几乎是最高的。很明显,2类客户为我们的“高价值客户”;而1类中几乎都是白格子,无论是客户价值还是消费水平值都是最低的,很明显,这一类我们将其归为“低价值客户”;剩下的0类我们将其称为“中等群体”。
将3类用户可视化。

# 画饼图
# with plt.xkcd():
import matplotlib.pyplot as plt
plt.figure(figsize=(9,9))

class_k=list(Kmeans.labels_)                          # 每个类别的用户个数
percent=[class_k.count(0)/len(user_attributes),class_k.count(1)/len(user_attributes),class_k.count(2)/len(user_attributes)]   # 每个类别用户个数占比

fig, ax = plt.subplots(figsize=(10,10))
colors=['aliceblue','steelblue','lightskyblue']
types=['中等群体','低价值用户','高价值用户']
ax.pie(percent,radius=1,autopct='%.2f%%',pctdistance=0.75,colors=colors,labels=types)
ax.pie([1], radius=0.6,colors='w')
plt.show()

我们可以看到,在我们的客户群体中,“低价值客户”的占比非常之大,中等人群占比最小。我们的“高价值客户”占比17.34%。

3 用户分析

3.1 高价值用户分析

2类客户描述:
消费水平高,客户价值大,追求高品质,对酒店星级要求高,访问频率和预定频率都较高,提前预定的时间都较短,决策一般都较快(日均访问数少),订单取消率较高,可以分析出这类客户商务属性偏重,可能随时要出差,因此都不会提前预定,可能出差随时会取消,因此酒店取消率也会更高一点。sid的值较大,说明高价值客户群体多集中在老客户中。价格敏感度较高,说明可能比较要求性价比。h值非常小,可能访问和预定时间多在半夜或是清晨。
这部分客户对于我们而言是非常重要的,因此我们需要对其实施个性化的营销:
1、为客户提供更多差旅酒店信息。
2、多推荐口碑好、性价比高的商务酒店。
3、推荐时间集中在半夜或是清晨。

3.2 中等群体分析

0类客户描述:
消费水平和客户价值都偏低,对酒店品质也不太追求,访问和预定频率也都较高,提前预定的时间是三类中最长的,最值得注意的是,0类客户中有两个颜色非常深的蓝色格子,是用户决策和近3个月的日均访问数。可以看出,这类客户通常很喜欢逛酒店界面,在决定要订哪家酒店前通常会花费非常多的时间进行浏览才能做出选择,并且一般都会提前很久订好房。我们可以给这类客户打上“谨慎”的标签。我们可以合理推断,这一类客户,可能预定酒店的目的多为出门旅行。
针对这部分客户,我们需要:
1、尽可能多地进行推送,因为此类客户通常比较喜欢浏览。
2、推送当地旅游资讯,因为这类客户旅游出行的概率较大。
3、多推荐价格相对实惠的酒店。

3.3 低价值用户分析

1类客户描述:
消费水平和客户价值极低,对酒店品质不追求,偏好价格较低,决策时间很短,访问和预定频率很低,sid值很低,说明新客户居多。
针对这部分客户,我们需要:
1、不建议花费过多营销成本,但因为新用户居多,属于潜在客户,可以维持服务推送。
2、推送的内容应多为大减价、大酬宾、跳楼价之类的。
3、此类用户占比居多,可进一步进行下沉分析,开拓新的时长。

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 3,414评论 3 7
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 4,208评论 1 15
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 1,691评论 0 6
  • 昨晚第一次尝试给自己染发。 骚动的内心总是会让我对很多事情好奇,在自己勇气能承受的范围之内尽力去尝试。所以,这次给...
    雨文_yuwencc1009阅读 131评论 0 1
  • 你笑,全世界会陪你一起笑。你哭,只有你一个人自己哭。我们身边发生的每一件事都是我们吸引过来的。今天的课让我再次想到...
    牛欢Vincent阅读 16评论 0 0