本文是对携程用户流失预测案例的一个总结,主要任务是对客户流失率进行建模分析,并挖掘出影响用户流失的关键因素。
目录:
● 项目介绍
● 问题分析
● 数据探索
● 特征工程
● 建模分析
● 总结
一、项目介绍
携程作为中国领先的综合性旅行服务公司,每天向超过2.5亿会员提供全方位的旅行服务,在这海量的网站访问量中,我们可分析用户的行为数据来挖掘潜在的信息资源。其中,客户流失率是考量业务成绩的一个非常关键的指标。数据是由携程官方提供的经过脱敏处理的一周用户访问数据,不包含用户ID等信息,目的是为了深入了解用户画像及行为偏好,找到最优算法,挖掘出影响用户流失的关键因素。
二、 问题分析
这个项目要解决的问题是关于用户流失的,在官方提供的字段和解释中,有一个label字段,这个是目标变量,也就是我们需要进行预测的值。label=1代表客户流失,label=0代表客户未流失,很显然这是个分类的预测问题。
对于本项目而言,最终的评价标准是要求在精确度达到97%的情况下,最大化召回率。从业务角度这个评价标准很好理解,平台一定不允许有客户流失的情况发生,但是挽回可能流失的客户需要成本,所以要求召回率尽可能高。
如无特别说明,一般用的是PR曲线和ROC曲线。ROC曲线有一个突出优势,就是不受样本不均衡的影响。
三、 数据探索
1、数据总体情况
官方共提供2个数据集,分别为训练集userlostprob_train.txt和测试集userlostprob_test.txt。训练集为2016.05.15-2016.05.21期间一周的访问数据,测试集为2016.05.22-2016.05.28期间一周的访问数据。本文采用测试集数据,为保护客户隐私,不提供uid等信息。数据经过了脱敏,和实际商品的订单量、浏览量、转化率等有一些差距,但是不会影响这个问题的可解性。
数据共有51个字段,除了目标变量label,还有50个特征。
拿到一份数据,首先要做的是基于业务情况,理解每个字段背后的含义。在特征变量超过10个的情况,一般要先对其进行分类。通过观察和业务情况,我将该数据大概分成三类:客户行为指标、订单指标和酒店指标如下图。当然我也参考了网络上一些大神的分类,可能还是存在一些偏差。订单相关的指标:如入住日期、订单数、取消率等;客户行为相关的指标:如星级偏好、用户偏好价格等;酒店相关的指标,如酒店评分均值、酒店评分人数、平均价格等。
从字段的描述信息来看,可以得到以下异常信息:
某些特征列存在不同程度的缺失;
有的特征列存在极值的情况,后面在进行缺失值填充的时候需要注意分布形态,另外对极值也需要处理一下,减小异常点对预测的影响;
类似'delta_price'等代表价格的特征存在负值,这属于异常情况,后面需要对负值进行处理。
na_rate = (len(df)-df.count())/len(df) # 缺失率计算
a = na_rate.sort_values(ascending=False)
a1 = pd.DataFrame(a)
plt.style.use('bmh')
plt.figure(figsize = (10,12))
plt.barh(range(df.shape[1]),a1[0])
plt.xlabel('na_rate')
plt.xlim([0,1])
plt.yticks(range(df.shape[1]),a1.index.values.tolist())
for x,y in enumerate(a1[0]):
plt.text(y,x,'%.3f'%y, va='bottom')
2、各特征的分布
查看所有数值型特征的分布情况,一方面有利于特征工程中根据数据分布合理选用处理方法,包括异常值、缺失值处理,连续特征离散化处理;另一方面有助于深入了解用户行为。
for i in range(4,50):
plt.hist(df[df.columns[i]].dropna().get_values())
plt.xlabel(df.columns[i])
plt.show()
忽略两端极值的影响,可以把businessrate_pre、businessrate_pre2、cancelrate_pre这3个特征近似看作正态分布,使用平均值填充缺失值。
诸如starprefer、consuming_capacity、price_sensitive这些特征,近似看作偏态分布,可以使用中位数填充缺失值。
四、 特征工程
数据和特征决定了机器学习效果的上限,而模型和算法只是逼近这个上限。特征工程是建模前的关键步骤,特征处理得好,可以提升模型的性能。
整个特征工程的任务主要包括:格式转换、缺失值处理、衍生特征、聚类特征、独热编码、标准化等。
1、时间特征处理
(1)格式转换
时间特征不存在缺失值,可以先处理。访问日期d和入住日期arrival是字符串格式,需要进行格式转换。这里使用pandas中常用的时间函数to_datetime(),将字符串格式转换为日期格式。
df['d'] = pd.to_datetime(df['d'], format = '%Y-%m-%d')
df['arrival'] = pd.to_datetime(df['arrival'], format='%Y-%m-%d')
(2)衍生特征
衍生特征是根据现有特征衍生出来的一些特征,比如访问日期和实际入住日期之间的差值,入住日期是周几,入住日期否为周末。在机器学习中,是否为周末这个特征往往是非常重要的。
# 查看用户周几入住
df['week2day'] = df['arrival'].map(lambda x: x.weekday())
# 查看用户入住日期与访问(预订)日期的差值
df['booking_gap'] = (df['arrival'] -df['d']).map(lambda x: x.days).astype(int)
# 查看用户入住的日期是否为周末
def is_weekend(a):
if int(a) in [0,1,2,3,4]:
return 0 # 0代表是工作日
else:
return 1 # 1代表是周末
df['is_weekend'] = df['week2day'].apply(is_weekend)
使用完这两个时间特征之后,和sampleid一起删除,这些特征对预测用户流失没有实质的帮助。
df.drop(['d','arrival','sampleid'],axis=1,inplace=True) # 删除特征
2、异常值处理
用户偏好价格delta_price1、delta_price2,以及当前酒店可订最低价lowestprice存在大量的负值,理论上酒店的价格不可能为负。同时数据分布比较集中,因此采取中位数填充。而客户价值customer_value_profit、ctrip_profits也不应该为负值,这里将其填充为0。deltaprice_pre2_t1是酒店价格与对手价差均值,可以为负值,无需处理。
# 查看最小值为负值的特征
df_min=df.min().iloc[4:]
df_min[df_min<0]
neg1=['delta_price1','delta_price2','lowestprice']
neg2=['customer_value_profit','ctrip_profits']
for col in neg1:
df.loc[df[col]<0,col]=df[col].median() # 填充中位数
for col in neg2:
df.loc[df[col]<0,col]=0 # 填充0
3、缺失值处理
缺失值全部为数值型数据,结合各个特征的数据分布情况,这里用了平均值和中位数填充两种方式。对于近似正态分布的数据,采用平均值填充,而近似偏态分布的数据,以及其他数值型数据,全部采用中位数填充。缺失率在80%以上的,如historyvisit_7ordernum,已经没有太多分析价值,直接去掉。
df=df.drop('historyvisit_7ordernum',axis=1) # 删除特征
fillwithmean=['businessrate_pre','businessrate_pre2','cancelrate_pre']
for col in df.columns:
if col in fillwithmean:
fillvalue = df[col].mean()
df[col] = df[col].fillna(fillvalue) # 填充平均值
else:
fillvalue = df[col].median()
df[col] = df[col].fillna(fillvalue) # 填充中位数
4、聚类特征
在实际行为中用户ID对我们的分析流失起到重要的作用,但是由于数据敏感性问题,样本数据中并没有给出,但是我们可以通过将一些特征信息进行聚类,对这些样本进行用户分组,同理也可对酒店分组。把这两类主体进行一个聚类,并把类的标签作为一个新的特征。这里使用KMeans的方法做聚类处理,分别将用户和酒店分成3个类别。
# 标准化
from sklearn.preprocessing import StandardScaler #标准化
ss = StandardScaler()
# 用户聚类
from sklearn.cluster import KMeans
user_group = df[['historyvisit_totalordernum','ordercanncelednum','ordercanceledprecent','historyvisit_visit_detailpagenum','historyvisit_avghotelnum','lowestprice_pre']]
for i in range(len(user_group.columns)):
user_group[user_group.columns[i]] = ss.fit_transform(user_group[user_group.columns[i]].values.reshape(-1,1)) # reshape(-1,1)将array转换为一列
# 酒店聚类
hotel_group = df[['commentnums','novoters','cancelrate','hoteluv','hotelcr','lowestprice']]
for i in range(len(hotel_group.columns)):
hotel_group[hotel_group.columns[i]] = ss.fit_transform(hotel_group[hotel_group.columns[i]].values.reshape(-1,1))
df['user_type'] = KMeans(n_clusters=3, init='k-means++').fit_predict(user_group) # KMeans方法,分3类
df['hotel_type'] = KMeans(n_clusters=3, init='k-means++').fit_predict(hotel_group) # KMeans方法,分3类
5、连续特征离散化
在这个案例中,将某些数值型特征转换成类别呈现更有意义,比如用户决策习惯、星级偏好、平均价格、消费能力指数等,同一类别表现出相似的属性。同时可以使得算法减少噪声的干扰。而且在机器学习中,一般很少直接将连续值作为逻辑回归模型的特征输入。特征离散化以后,可以简化逻辑回归模型,降低了模型过拟合的风险。后面会用到逻辑回归模型,所以在这里还是先做离散化处理。
根据业务经验选择合适的连续型特征,在一定的数值范围内划分分区。
def deal_decisionhabit_user(x):
if x<10:
return 0
elif x<30:
return 1
else:
return 2
def deal_starprefer(x):
if x<50:
return 0
elif x<80:
return 1
else:
return 2
def deal_avgprice(x):
if x<400:
return 0
elif x<1000:
return 1
else:
return 2
def deal_consuming_capacity(x):
if x<40:
return 0
elif x<80:
return 1
else:
return 2
离散化之后的特征,以及酒店和用户这两个聚类特征,均为数值型,都需要转换为字符串型,以便接下来进行独热编码。
df['decisionhabit_user'] = df['decisionhabit_user'].map(lambda x:str(deal_decisionhabit_user(int(x))))
df["starprefer"] = df["starprefer"].map(lambda x:str(deal_starprefer(int(x))))
df["consuming_capacity"] = df["consuming_capacity"].map(lambda x: str(deal_consuming_capacity(int(x))))
df['avgprice'] = df['avgprice'].map(lambda x: str(deal_avgprice(int(x))))
df[["user_type","hotel_type"]]=df[["user_type","hotel_type"]].applymap(str)
6、分类变量one-hot-encode
对分类变量进行独热编码,可以解决分类器不好处理属性数据的问题,编码后的特征都可以看做是连续的特征,并且在一定程度上也起到了扩充特征的作用。这里使用get_dummies方法实现one-hot-encode。
df=pd.get_dummies(df)
7、用户分组特征
由于数据集没有提供用户uid,需要根据已有特征对用户进行分组,生成用户标签usertag。这里采取了一种近似的方法,如果用户的某些行为特征相同,则认为是同一个用户的行为。后面需要根据用户标签分割数据集,同一个用户的信息不能同时出现在训练集和测试集中,否则模型会过拟合。
这里用于判断是否为同一用户行为的特征有:用户一年内取消订单数、近3个月用户历史日均访问酒店数、用户年订单数、客户价值_近1年、客户价值、用户转化率、年访问次数,并且使用hash函数处理字符串。
df['usertag']= df.ordercanncelednum.map(str) \
+ df.historyvisit_avghotelnum.map(str) \
+ df.ordernum_oneyear.map(str) \
+ df.customer_value_profit.map(str) \
+ df.ctrip_profits.map(str) \
+ df.cr.map(str) \
+ df.visitnum_oneyear.map(str)
df.usertag = df.usertag.map(lambda x: hash(x)) # 生成哈希值
8、标准化
对于一些基于距离的模型,需要标准化处理,比如回归分析、神经网络、SVM。
而对于与距离计算无关的树模型,不需要标准化处理,比如决策树、随机森林等,因为树中节点的选择只关注当前特征在哪里切分对分类更好,即只在意特征内部的相对大小,而与特征间的相对大小无关。
这里还是标准化处理下,后面会用到不同的模型做对比。注意只用对部分数值型特征进行标准化,label列(0/1)、新生成的独热编码(0/1)、用户标签usertag无需参与标准化。所以先对数据进行拆分,处理之后再使用concat合并。
df1=df
df2=pd.DataFrame(df1['label']) # label
df3=df1.iloc[:,1:-24] # 需要标准化的特征
df4=df1.iloc[:,-24:] # 独热编码和用户标签
columns=df3.columns.tolist() # 提取df3的列名,存放在列表中
# 对拆分后的df3进行标准化
scaler = StandardScaler()
scaler.fit(df3)
df3=scaler.transform(df3)
df3=pd.DataFrame(df3,columns=columns) # 标准化处理后的数据是array,转换为DataFrame
df_concat1=pd.concat([df2,df3],axis=1) # 与df2合并
df_concat2=pd.concat([df_concat1,df4],axis=1) # 与df4合并
df_new=df_concat2 # 生成新的DataFrame
8、分割数据集
在使用数据集训练模型之前,我们需要先将整个数据集分为训练集、验证集、测试集。训练集是用来训练模型的,通过尝试不同的方法和思路使用训练集来训练不同的模型,再通过验证集使用交叉验证来挑选最优的模型,通过不断的迭代来改善模型在验证集上的性能,最后再通过测试集来评估模型的性能。
由于官方提供的数据已经划分好训练集和测试集,我们现在需要在原始训练集中划分出训练集和验证集,这里是70%划分为训练集,30%划分为验证集。
那究竟依据什么特性进行划分呢?划分数据集需注意时间性、地域性、层次性(stratifiedKFold)。前面已经提到过,在做本地数据集划分的时候需要基于用户进行划分,也就是要保证划分前后的数据是满足独立同分布的。另外,由于提供的是一周的数据,时间序列特性不是很明显,所以没有按时间线对数据进行划分。
splitnum=int(len(df_new.index)* 0.7) # 分割点:70%
df_new=df_new.sort_values(by="usertag") # 按照usertag排序
# 前70%行数据生成训练集
df_new.iloc[:splitnum,].to_csv(r'data/userlostprob_train.csv', sep='\t', index=False)
# 其余数据生成测试集
df_new.iloc[splitnum:, ].to_csv(r'data/userlostprob_test.csv', sep='\t', index=False)
五、建模分析
对于一个分类问题,一般经常使用的模型有逻辑回归、随机森林、xgboost。在正常的情况下,xgboost会比随机森林效果更好,但是如果数据的噪声比较大的话,也会出现随机森林的效果更好的情况。为了比较不同模型在这个分类问题中的性能表现,这里使用了三个模型分别训练和评估。
导入包,使用sklearn库完成建模分析。
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import accuracy_score
from sklearn import metrics
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
from sklearn.feature_selection import SelectFromModel
导入处理后的数据集,并删除usertag标签。
train=r'data/userlostprob_train.csv'
test=r'data/userlostprob_test.csv'
trainData = pd.read_csv(train,sep="\t").drop(["usertag"],axis=1)
testData = pd.read_csv(test,sep="\t").drop(["usertag"],axis=1)
从训练集和测试集中分别提取特征和目标变量label,训练模型后,用测试集评估模型的性能。
train_X = trainData.iloc[:,1:] # 特征从第1列开始选
train_Y = trainData.iloc[:,0] # 第0列是label
test_X = testData.iloc[:,1:]
test_Y = testData.iloc[:,0]
1、逻辑回归模型
(1)导入模型
from sklearn import linear_model
from sklearn.linear_model import LogisticRegression
(2)模型性能评估
输出准确率accuracy、AUC面积以及精确度precision≥0.97条件下的最大召回率recall。
lr = LogisticRegression()
lr.fit(train_X,train_Y) # 训练模型
test_pred_lr = lr.predict_proba(test_X)[:,1] # 预测为1的可能性
fpr_lr,tpr_lr,threshold = metrics.roc_curve(test_Y,test_pred_lr)
auc = metrics.auc(fpr_lr,tpr_lr)
score = metrics.accuracy_score(test_Y,lr.predict(test_X)) # 输入真实值和预测值
print([score,auc]) # 准确率、AUC面积
precision_lr, recall_lr, thresholds = precision_recall_curve(test_Y, test_pred_lr)
pr_lr = pd.DataFrame({"precision": precision_lr, "recall": recall_lr})
prc_lr = pr_lr[pr_lr.precision >= 0.97].recall.max()
print(prc_lr) # 精确度≥0.97条件下的最大召回率
逻辑回归模型过于简单,预测准确率比较低,在precision≥0.97的情况下,最大recall仅为0.0001。
2、随机森林模型
(1)导入分类器
from sklearn.ensemble import RandomForestClassifier
(2)模型性能评估
输出准确率accuracy、AUC面积以及精确度precision≥0.97条件下的最大召回率recall。
rfc = RandomForestClassifier(n_estimators=200)
rfc.fit(train_X,train_Y) # 训练模型
test_pred_rfc = rfc.predict_proba(test_X)[:,1] # 预测为1的可能性
fpr_rfc,tpr_rfc,thre_rfchold = metrics.roc_curve(test_Y,test_pred_rfc)
auc = metrics.auc(fpr_rfc,tpr_rfc)
score = metrics.accuracy_score(test_Y,rfc.predict(test_X)) # 输入真实值和预测值
print([score,auc]) # 准确率、AUC面积
precision_rfc, recall_rfc, thresholds = precision_recall_curve(test_Y, test_pred_rfc)
pr_rfc = pd.DataFrame({"precision": precision_rfc, "recall": recall_rfc})
prc_rfc = pr_rfc[pr_rfc.precision >= 0.97].recall.max()
print(prc_rfc) # 精确度≥0.97条件下的最大召回率
[0.90051888068642982, 0.95880960471941379]
0.636531495094
对于这个项目,随机森林模型表现较好,迭代200次以后模型准确率0.900,在precision≥0.97的情况下,最大recall已经可以达到0.636。
(3)特征重要性
使用feature_importance方法,可以得到特征的重要性排序。当然,还可以使用plot_importance方法,默认的importance_type=“weight”,将其设置为“gain”,可以得到和feature_importance方法相同的结果。
importance = rfc.feature_importances_
indices = np.argsort(importance)[::-1] # np.argsort()返回数值升序排列的索引,[::-1]表示倒序
features = train_X.columns
for f in range(train_X.shape[1]):
print("%2d) %3d %20s (%.4f)" %(f+1,indices[f],features[indices[f]], importance[indices[f]]))
# 作图
plt.figure(figsize=(15,8))
plt.title('Feature importance')
plt.bar(range(train_X.shape[1]),importance[indices],color='blue')
plt.xticks(range(train_X.shape[1]),indices)
plt.xlim([-1,train_X.shape[1]])
plt.show()
在前15个特征中,用户相关的指标有:年访问次数、访问时间点、一年内距上次访问时长、用户转化率、一年内距离上次下单时长、提前预定时间、用户价值。酒店相关的指标有:24小时内已访问酒店商务属性指数均值、24小时内已访问酒店可订最低价均值、24小时历史浏览次数最多酒店历史uv、24小时内已访问次数最多酒店可订最低价、24小时历史浏览酒店历史uv均值。城市相关的指标:昨日提交当前城市同入住日期的app订单数、昨日访问当前城市同入住日期的app uv数。
(4)筛选特征
根据特征重要性结果挑选特征,重新进行模型训练和评估。这里筛选的阈值设定为0.005。不过筛选特征后模型性能没有得到明显提升。
selection = SelectFromModel(rfc, threshold=0.005, prefit=True)
select_train_X = selection.transform(train_X)
select_test_X = selection.transform(test_X)
3、xgboost模型
(1)导入分类器
import xgboost as xgb
from xgboost.sklearn import XGBClassifier
(2)模型调参
使用GridSearchCV(网格搜索)的方法调节xgboost模型的参数,主要的影响参数有树的最大深度、最小叶子节点样本权重和、惩罚项系数gamma、使用数据占比、使用特征占比。这里分步调节,寻找最优参数。
param_test1 = {
'max_depth': range(3, 10, 2),
'min_child_weight': range(1, 6, 2)}
param_test2 = {
'gamma': [i / 10.0 for i in range(0, 5)]}
param_test3 = {
'subsample': [i / 10.0 for i in range(6, 10)],
'colsample_bytree': [i / 10.0 for i in range(6, 10)]}
gsearch = GridSearchCV(estimator=XGBClassifier(learning_rate =0.1, n_estimators=1000, max_depth=5,min_child_weight=1, gamma=0, subsample=0.8, colsample_bytree=0.8, objective= 'binary:logistic', nthread=1, scale_pos_weight=1, seed=27),param_grid =param_test1,scoring='roc_auc',n_jobs=1,iid=False, cv=5)
gsearch.fit(train_X ,train_Y )
means = gsearch.cv_results_['mean_test_score']
params = gsearch.cv_results_['params']
print(means, params)
# 模型最好的分数、模型最好的参数、模型最好的评估器
print(gsearch.best_score_ ,gsearch.best_params_,gsearch.best_estimator_)
(3)模型性能评估
使用上一步找到的最优参数组合,代入模型进行训练和评估。输出准确率accuracy、AUC面积以及精确度precision≥0.97条件下的最大召回率recall。
model = XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
colsample_bytree=0.8, gamma=0, learning_rate=0.1, max_delta_step=0,
max_depth=9, min_child_weight=1, missing=None, n_estimators=1000,
reg_alpha=0, reg_lambda=1, scale_pos_weight=1, seed=27, silent=True,
subsample=0.9)
model.fit(train_X ,train_Y) # 训练模型
test_pred_xgb = model.predict_proba(test_X)[:,1] # 预测为1的可能性
fpr_xgb,tpr_xgb,threshold = metrics.roc_curve(test_Y,test_pred_xgb)
auc = metrics.auc(fpr_xgb,tpr_xgb)
score = metrics.accuracy_score(test_Y,model.predict(test_X)) # 输入真实值和预测值
print([score,auc]) # 准确率、AUC面积
precision_xgb, recall_xgb, thresholds = precision_recall_curve(test_Y, test_pred_xgb)
pr_xgb = pd.DataFrame({"precision": precision_xgb, "recall": recall_xgb})
prc_xgb = pr_xgb[pr_xgb.precision >= 0.97].recall.max()
print(prc_xgb) # 精确度≥0.97条件下的最大召回率
[0.9064082247903219, 0.95434695969275529]
0.587697393873
迭代1000次之后,得到的模型准确率0.906,在precision≥0.97的情况下,最大recall可以达到0.587。
(4)特征重要性
从xgboost模型也可以得到影响用户流失的特征,按照重要性排序,有24小时内是否访问订单填写页、提前预订时间、用户转化率、用户决策、访问时间点、用户年订单数、当前酒店历史cr、用户类型、24小时内已访问酒店商务属性指数均值等。
其中酒店相关的指标有:当前酒店历史cr、24小时内已访问酒店商务属性指数均值。用户相关的指标有:24小时内是否访问订单填写页、提前预订时间、用户转化率、用户决策、访问时间点、用户年订单数、用户类型。
使用随机森林模型和xgboost模型得到的在top10特征差异较大,重合的特征只有4个,访问时间点、提前预订时间、用户转化率、24小时内已访问酒店商务属性指数均值。
这两个模型的ROC曲线和PR曲线差异不大,总体而言随机森林模型比xgboost模型表现好。从评定标准来看,随机森林的召回率比xgboost稍高一些。认为可能是因为数据缺失较多,造成了噪音比较大。
六、总结
1、特征工程
缺失值和异常值处理是关键,根据数据和模型选择是否需要独热编码和标准化,按照业务经验合理构造衍生特征和聚类特征。筛选特征的方法有很多种,比如方差、卡方值、相关系数等,这里用了树模型的特征重要性。特征工程决定了机器学习效果的上限,模型优化只能无限接近这个上限。
2、模型对比结果
使用逻辑回归、随机森林和xgboost三种模型做对比分析,按照评定标准,在精确度≥0.97的条件下,随机森林模型的性能最优,召回率可以达到0.636。该模型可以直接上线用于用户流失预测。
3、影响用户流失的关键因素
从模型表现上看,随机森林效果最优。根据特征重要性排序,提取影响用户流失的最关键因素。其中用户相关的指标有:年访问次数、访问时间点、一年内距上次访问时长、用户转化率、一年内距离上次下单时长。酒店相关的指标有:24小时内已访问酒店商务属性指数均值、24小时内已访问酒店可订最低价均值、24小时历史浏览次数最多酒店历史uv、24小时内已访问次数最多酒店可订最低价、24小时历史浏览酒店历史uv均值。城市相关的指标:昨日提交当前城市同入住日期的app订单数、昨日访问当前城市同入住日期的app uv数。