1

竞赛背景

  皇包车(HI GUIDES)是一个为中国出境游用户提供全球中文包车游服务的平台。拥有境外10万名华人司机兼导游(司导),覆盖全球90多个国家,1600多个城市,300多个国际机场。截止2017年6月,已累计服务400万中国出境游用户。

  由于消费者消费能力逐渐增强、 旅游信息不透明程度的下降,游客的行为逐渐变得难以预测,传统旅行社的旅游路线模式已经不能满足游客需求。如何为用户提供更受欢迎、更合适的包车游路线,就需要借助大数据的力量。结合用户个人喜好、景点受欢迎度、天气交通等维度,制定多套旅游信息化解决方案。

赛题地址:https://www.dcjingsai.com/com...

任务

  黄包车提供五万余条客户浏览APP行为,其中有些客户在浏览后完成了订单,且享受了精品旅游服务,而有些用户则没有下单。
  参赛者需要分析用户的个人信息和浏览行为,从而预测用户是否会在短期内购买精品旅游服务

数据导入及预览

import pandas as pd
import numpy as np
from sklearn import preprocessing
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

import warnings
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = [u'SimHei']
plt.rcParams['axes.unicode_minus'] = False

user_train = pd.read_csv(r'Data\trainingset\userProfile_train.csv')
action_train = pd.read_csv(r'Data\trainingset\action_train.csv')
comment_train = pd.read_csv(r'Data\trainingset\userComment_train.csv')
orderFuture_train= pd.read_csv(r'Data\trainingset\orderFuture_train.csv')
orderHistory_train= pd.read_csv(r'Data\trainingset\orderHistory_train.csv')

user_test = pd.read_csv(r'Data\test\userProfile_test.csv')
action_test = pd.read_csv(r'Data\test\action_test.csv')
comment_test = pd.read_csv(r'Data\test\userComment_test.csv')
orderFuture_test = pd.read_csv(r'Data\test\orderFuture_test.csv')
orderHistory_test = pd.read_csv(r'Data\test\orderHistory_test.csv')

user = pd.concat([user_train,user_test])
action = pd.concat([action_train,action_test])
comment = pd.concat([comment_train,comment_test])
orderHistory = pd.concat([orderHistory_train,orderHistory_test])
orderFuture = pd.concat([orderFuture_train,orderFuture_test])

  理解数据是进行分析和建模的基础,数据共有五张表,分别是用户信息表(user)、用户评论表(comment)、用户行为表(action)、历史订单表(orderHistory)、未来订单表(orderFuturen),以下是各表预览。

user.head()
userid gender province age
0 100000000013 NaN 60后
1 100000000111 NaN 上海 NaN
2 100000000127 NaN 上海 NaN
3 100000000231 北京 70后
4 100000000379 北京 NaN
action.head()
userid actionType actionTime
0 100000000013 1 1474300753
1 100000000013 5 1474300763
2 100000000013 6 1474300874
3 100000000013 5 1474300911
4 100000000013 6 1474300936
orderHistory.head()
userid orderid orderTime orderType city country continent
0 100000000013 1000015 1481714516 0 柏林 德国 欧洲
1 100000000013 1000014 1501959643 0 旧金山 美国 北美洲
2 100000000393 1000033 1499440296 0 巴黎 法国 欧洲
3 100000000459 1000036 1480601668 0 纽约 美国 北美洲
4 100000000459 1000034 1479146723 0 巴厘岛 印度尼西亚 亚洲
orderFuture.head()
orderType userid
0 0.0 100000000013
1 0.0 100000000111
2 0.0 100000000127
3 0.0 100000000231
4 0.0 100000000379
comment.head()
userid orderid rating tags commentsKeyWords
0 100000000013 1000015 4.0 NaN ['很','简陋','太','随便']
1 100000000231 1000024 5.0 提前联系|耐心等候 ['很','细心']
2 100000000471 1000038 5.0 NaN NaN
3 100000000637 1000040 5.0 主动热情|提前联系|举牌迎接|主动搬运行李 NaN
4 100000000755 1000045 1.0 未举牌服务 NaN

EDA及可视化

用户信息

  用户信息表共40307条用户数据,userid是唯一标识,数据缺失较为严重。

user.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 50383 entries, 0 to 10075
Data columns (total 4 columns):
userid      50383 non-null int64
gender      19769 non-null object
province    45484 non-null object
age         5961 non-null object
dtypes: int64(1), object(3)
memory usage: 1.9+ MB

用户地区分布

(user.province.value_counts()/user.province.value_counts().sum()).head().sum()
0.7712162518687891


  用户以北京、上海、广东、江苏、浙江等发达地区为主,五地区占到总用户数的77%。

fig,axes = plt.subplots(figsize=(20,10))
sns.countplot(x='province',data=user,order=user.province.value_counts().index.tolist())

clipboard.png

用户性别信息

  用户性别共15760条数据,女性占54.7%,男性45.3%。

fig,axes = plt.subplots(1,2,figsize=(12,4))
user.gender.value_counts().plot.bar(ax=axes[0])
axes[0].set_xticklabels(['女','男'],rotation=0)
user.gender.value_counts().plot.pie(ax=axes[1],autopct='%.2f%%')

clipboard.png

用户年龄信息

  用户年龄共4742条信息,以60后、70后、80后、90后为主。

fig,axes = plt.subplots(figsize=(10,4))
user.age.value_counts().plot.bar()
plt.xticks(rotation=0)

clipboard.png

  虽然女性用户多余男性,但提供年龄信息的用户中,男性多于女性,看来即使是匿名,女性也不愿意暴露年龄啊。

fig,axes = plt.subplots(figsize=(10,4))
sns.countplot(x='age',data=user,hue='gender')

clipboard.png

用户浏览行为

  行为类型一共有9个,其中1是唤醒app;2-4是浏览产品,无先后关系;5-9则是有先后关系的,从填写表单到提交订单再到最后支付。

import time
def time_convert(timestamp):
    str_time =time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(timestamp))
    return str_time
action.actionTime = action.actionTime.map(lambda x: time_convert(x))
action['year']=action.actionTime.str[:4]
action['month']=action.actionTime.str[5:7]
action['day']=action.actionTime.str[8:10]
action['date']=action.actionTime.str[:10]
action['time']=action.actionTime.str[11:]
action['year_month']=action.actionTime.str[:7]
action['hour']=action.actionTime.str[11:13]

用户月访问量

  MAU用月内产生用户行为的独立ID数量表示,PV用唤醒APP(行为1)次数表示。用户活跃的两个峰值分别在四五月和十月,小长假是人们出国游的首选时间。
clipboard.png

fig,axes = plt.subplots(2,1,figsize=(10,10))
action[action['year_month'] !='2016-08'].drop_duplicates(['userid']).groupby('year_month').userid.count().plot(ax=axes[0])
axes[0].set_title('独立用户月访问量(MAU)')
action[action.actionType==1].groupby('year_month').userid.count().plot(ax=axes[1])
axes[1].set_title('用户月访问量(PV)')

日访问量

  DAU为日内产生用户行为的独立ID数,PV为日内行为为1的行为条数。DAU峰值出现在4月初,但同一时间段内的PV却相对PV峰值5月初较低,说明4月初平均每用户唤醒次数较低,可能是有拉新活动。对两项指标相除,可以验证以上猜想。同样的,16年12月之前用户的PV/DAU较大,之后较为平稳,APP进入健康平稳期。

fig,axes = plt.subplots(2,1,figsize=(10,10))
action.drop_duplicates(['userid']).groupby('date').userid.count().plot(ax=axes[0])
axes[0].set_title('独立用户日访问量(DAU)')
action[action['actionType']==1].groupby('date').userid.count().plot(ax=axes[1])
axes[1].set_title('用户日访问量(PV)')

clipboard.png

fig,axes = plt.subplots(figsize=(10,5))
(action.drop_duplicates(['userid']).groupby('date').userid.count()/action[action['actionType']==1].groupby('date').userid.count()).plot()

clipboard.png

小时访问分析

  数据的点击量呈现一个非常奇怪的形状,在日间(8点到16点)呈现较低的访问量,并在12点左右达到最低值,可能是数据缺失或时区错误。

fig,axes = plt.subplots(2,1,figsize=(10,10))
action.drop_duplicates(['userid']).groupby('hour').userid.count().plot(ax=axes[0])
axes[0].set_title('独立用户小时访问量(HAU)')
action[action['actionType']==1].groupby('hour').userid.count().plot(ax=axes[1])
axes[1].set_title('用户日小时访问量(PV)')

clipboard.png

不同类型用户访问量

#对访问类型分类
def vis_type(x):
    if x in [2,3,4]:
        return 2
    else:
        return x
action['visitor_type']=action['actionType'].map(lambda x: vis_type(x))
fig,axes = plt.subplots(figsize=(10,5))
diff_visitor = action.groupby(['hour','visitor_type']).userid.count().unstack()
plt.plot(diff_visitor)
plt.title('用户日小时访问量(PV)')

clipboard.png

用户转化模型

  首先是唤醒APP(1)到浏览页面的转化(2)数据结果正常,但填写表单(5)数量远大于操作1、2,即大量表单在没有使用APP的情况下填写,可能是通过其他渠道跳入填写页面,或数据缺失严重。同时,填写表单(7)数量小于(8),可能数据缺失缺失较为严重。

from example.commons import Faker
from pyecharts import options as opts
from pyecharts.charts import Funnel, Page
df = action.groupby('visitor_type',as_index=False).userid.count().values.tolist()

def funnel_base() -> Funnel:
    c = (
        Funnel()
        .add("访问量",df)
        .set_global_opts(title_opts=opts.TitleOpts(title="访问转化"))
    )
    return c
funnel_base().render_notebook()

clipboard.png

用户评价

用户评分

  评价表中共9863条数据,其中评分无缺失值,平均分为4.91,五星好评占绝大多数。

comment.rating.mean()

4.916672610845424

from pyecharts.charts import Bar

bar = Bar()
bar.add_xaxis(comment.rating.value_counts().index.tolist())
bar.add_yaxis("评分", comment.rating.value_counts().values.tolist())
bar.render_notebook()

clipboard.png

用户评价标签

  以四分为分界线划分好评与差评,分别制作词云图如下:

tags_count = comment[comment.rating>=4].tags.str.split("|").dropna().apply(pd.value_counts).sum()
path=r'C:\Windows\Fonts\simhei.ttf'
import wordcloud
w = wordcloud.WordCloud(font_path=path,width=1400, height=1400, margin=2)
w.fit_words(tags_count)
plt.figure(dpi=1000)
plt.imshow(w)
plt.axis('off')

clipboard.png

tags_count = comment[comment.rating<4].tags.str.split("|").dropna().apply(pd.value_counts).sum()
path=r'C:\Windows\Fonts\simhei.ttf'
import wordcloud
w = wordcloud.WordCloud(font_path=path,width=1400, height=1400, margin=2)
w.fit_words(tags_count)
plt.figure(dpi=500)
plt.imshow(w)
plt.axis('off')

clipboard.png

用户评论关键词

  用户评论关键词同样以4分为分界线,分别制作词云图。

Keyword_count=comment[comment['rating']>=4].commentsKeyWords.dropna().str[1:-1].str.split(',').apply(pd.value_counts).sum()
path=r'C:\Windows\Fonts\simhei.ttf'
import wordcloud
w = wordcloud.WordCloud(font_path=path,width=1400, height=1400, margin=2)
w.fit_words(Keyword_count)
plt.figure(dpi=1000)
plt.imshow(w)
plt.axis('off')

clipboard.png

Keyword_count=comment[comment['rating']<4].commentsKeyWords.dropna().str[1:-1].str.split(',').apply(pd.value_counts).sum()
path=r'C:\Windows\Fonts\simhei.ttf'
import wordcloud
w = wordcloud.WordCloud(font_path=path,width=1400, height=1400, margin=2)
w.fit_words(Keyword_count)
plt.figure(dpi=1000)
plt.imshow(w)
plt.axis('off')

clipboard.png

订单数据

  该数据描述了用户的历史订单信息。数据共有7列,分别是用户id,订单id,订单时间,订单类型,旅游城市,国家,大陆。其中1表示购买了精品旅游服务,0表示普通旅游服务。

用户复购

  订单数据共20653项,涵盖10637名用户,用户复购图如下:

order_number=orderHistory.groupby(['userid'],as_index=False).orderid.count().groupby('orderid',as_index=False).userid.count().rename(columns={'orderid':'order_quantity','userid':'count'})
order_number=pd.concat([order_number[:8],pd.DataFrame([{'order_quantity':'8次以上','count':order_number[8:].count().sum()}])])

from pyecharts.charts import Page, Pie
def pie_base() -> Pie:
    c = (
        Pie()
        .add('',order_number.values.tolist())
        .set_global_opts(title_opts=opts.TitleOpts(title="所有服务用户复购图"))
        .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}"))
    )
    return c
pie_base().render_notebook()

clipboard.png

order_number=orderHistory[orderHistory.orderType==1].groupby(['userid'],as_index=False).orderid.count().groupby('orderid',as_index=False).userid.count().rename(columns={'orderid':'order_quantity','userid':'count'})
order_number=pd.concat([order_number[:8],pd.DataFrame([{'order_quantity':'8次以上','count':order_number[8:].count().sum()}])])

from pyecharts.charts import Page, Pie
def pie_base() -> Pie:
    c = (
        Pie()
        .add('',order_number.values.tolist())
        .set_global_opts(title_opts=opts.TitleOpts(title="精品服务用户复购图"))
        .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}"))
    )
    return c
pie_base().render_notebook()

clipboard.png

  对比发现精品服务的复购率与总复购率相似。

用户出游

orderHistory.orderTime = orderHistory.orderTime.map(lambda x: time_convert(x))

orderHistory['year']=orderHistory.orderTime.str[:4]
orderHistory['month']=orderHistory.orderTime.str[5:7]
orderHistory['day']=orderHistory.orderTime.str[8:10]
orderHistory['date']=orderHistory.orderTime.str[:10]
orderHistory['time']=orderHistory.orderTime.str[11:]
orderHistory['year_month']=orderHistory.orderTime.str[:7]
orderHistory['hour']=orderHistory.orderTime.str[11:13]
from pyecharts.charts import Bar
from pyecharts import options as opts
jingpin_top10 = orderHistory[orderHistory.orderType==1].city.value_counts()[:10]
bar = Bar()
bar.add_xaxis(jingpin_top10.index.tolist())
bar.add_yaxis("精品游十大热门城市", jingpin_top10.values.tolist())
bar.render_notebook()

clipboard.png

from pyecharts.globals import ThemeType
putong_top10 = orderHistory[orderHistory.orderType==0].city.value_counts()[:10]
bar = Bar({"theme": ThemeType.ESSOS})
bar.add_xaxis(putong_top10.index.tolist())
bar.add_yaxis("普通游十大热门城市", putong_top10.values.tolist())
bar.render_notebook()

clipboard.png

continent_jingpin=orderHistory[orderHistory['orderType']==1].groupby(['continent'],as_index=False).orderid.count()
continent_putong = orderHistory[orderHistory['orderType']==0].groupby(['continent'],as_index=False).orderid.count()
continent_putong = pd.concat([continent_putong,pd.DataFrame([{'continent':'南美洲','userid':0}])]).sort_values('continent')
bar = Bar()
bar.add_xaxis(continent_jingpin.continent.tolist())
bar.add_yaxis("精品游大陆分布", continent_jingpin.orderid.values.tolist())
bar.add_yaxis("普通游大陆分布", continent_putong.orderid.values.tolist())
bar.render_notebook()

clipboard.png

country_boutique = orderHistory[orderHistory['orderType']==1].groupby('country').country.count().sort_values(ascending = False)[:10]
country_ordinary = orderHistory[orderHistory['orderType']==0].groupby('country').country.count().sort_values(ascending = False)[:10]
bar = Bar()
bar.add_xaxis(country_boutique.index.tolist())
bar.add_yaxis("精品游十大热门国家", country_boutique.values.tolist())
bar.render_notebook()

clipboard.png

bar = Bar()
bar.add_xaxis(country_ordinary.index.tolist())
bar.add_yaxis("普通游十大热门国家", country_ordinary.values.tolist())
bar.render_notebook()
def bar_base_dict_config() -> Bar:
    c = (
        Bar({"theme": ThemeType.MACARONS})
        .add_xaxis(country_ordinary.index.tolist())
        .add_yaxis("普通游十大热门国家", country_ordinary.values.tolist())
        )
    return c

bar_base_dict_config().render_notebook()

clipboard.png

特征工程

  特征工程包括一切对特征的处理,特征提取、特征组合、标准化、特征筛选等,对连续特征进行分箱,对分类特征进行虚拟变量化,这里只提取了一些特征,没有进行特征筛选。

import pandas as pd
import numpy as np
from sklearn import preprocessing

import warnings
warnings.filterwarnings('ignore')

user_train = pd.read_csv(r'Data\trainingset\userProfile_train.csv')
action_train = pd.read_csv(r'Data\trainingset\action_train.csv')
comment_train = pd.read_csv(r'Data\trainingset\userComment_train.csv')
orderFuture_train= pd.read_csv(r'Data\trainingset\orderFuture_train.csv')
orderHistory_train= pd.read_csv(r'Data\trainingset\orderHistory_train.csv')

user_test = pd.read_csv(r'Data\test\userProfile_test.csv')
action_test = pd.read_csv(r'Data\test\action_test.csv')
comment_test = pd.read_csv(r'Data\test\userComment_test.csv')
orderFuture_test = pd.read_csv(r'Data\test\orderFuture_test.csv')
orderHistory_test = pd.read_csv(r'Data\test\orderHistory_test.csv')

user = pd.concat([user_train,user_test])
action = pd.concat([action_train,action_test])
comment = pd.concat([comment_train,comment_test])
orderHistory = pd.concat([orderHistory_train,orderHistory_test])
orderFuture = pd.concat([orderFuture_train,orderFuture_test])

orderHistory = orderHistory.sort_values(by=['userid','orderTime'])
#历史订单数量,时间戳统计值
orderHistory_internal_table = orderHistory.groupby('userid').orderTime.agg(['count','max','min','std','mean']).reset_index().rename(columns = {'count':'order_count',
                                                                                                                'max':'ordertime_max',
                                                                                                                'min':'ordertime_min',
                                                                                                                'std':'orderTime_std',
                                                                                                                'mean':'ordertime_mean'}).fillna(0)
#历史订单普通、精品订单数
orderHistory_internal_table = orderHistory_internal_table.merge(orderHistory[orderHistory['orderType']==0].groupby('userid').orderid.count().reset_index().rename(columns={'orderid':'ordinary_count'}),how='left',on='userid')
orderHistory_internal_table = orderHistory_internal_table.merge(orderHistory[orderHistory['orderType']==1].groupby('userid').orderid.count().reset_index().rename(columns={'orderid':'unordinary_count'}),how='left',on='userid')
#去过的国家、大陆、城市有几次。
orderHistory_internal_table = orderHistory_internal_table.merge(pd.get_dummies(orderHistory[['userid','country','continent','city']]).groupby('userid',as_index=False).sum(),on='userid',how='left')
#最后一次行程信息
orderHistory_internal_table = orderHistory_internal_table.merge(pd.get_dummies(orderHistory.groupby('userid',as_index=False).apply(lambda x:x.iloc[-1])[['userid','orderType','city','country','continent']]),on='userid',how='left')

data = orderFuture.copy() #以orderFuture为基础

data = data.merge(user)  #连接user

data = data.merge(comment,how = 'left') #连接comment
data['tags'] = data.tags.apply(lambda x : 0 if pd.isnull(x) else 1) #将tag分为有无
data['commentsKeyWords'] = data.commentsKeyWords.apply(lambda x:0 if pd.isnull(x) else 1) #将评论分为有无
del data['orderid'] #删除orderid列


action = action.sort_values(by=['userid','actionTime']) #按照userid,actiontime排序
#生成中间表包含action信息,首先是每个id的action数量,最大最小时间,均值标准差
action_internal_table = action.groupby('userid').actionTime.agg(['count','max','min','std','mean']).reset_index().rename(columns = {'count':'action_count',
                                                                                                                                    'max':'time_last_action',
                                                                                                                                    'min':'time_first_action',
                                                                                                                                   'std':'actiontime_std',
                                                                                                                                   'mean':'actiontime_mean'})
#2-4与5-9的比例

#增加每个id的倒数第1-20个行为类别
for i in range(20):
    action_internal_table = action_internal_table.merge(action.groupby('userid').actionType.apply(lambda x:x.iloc[-i-1] if len(x)>i else np.nan).reset_index().rename(columns={'actionType':'last_but{}_action_type'.format(i)}).reset_index(),how='left')
del action_internal_table['index']
#每个行为类型所占的比例
count = action.groupby('userid').actionType.count()
for i in range(1,10):
    action_internal_table = action_internal_table.merge((action[action['actionType']==i].groupby('userid').actionType.count()/count).reset_index().rename(columns={'actionType':'rate_{}'.format(i)}).fillna(0),on='userid',how='left')

#倒数第1-20个时间戳
for i in range(20):
    action_internal_table = action_internal_table.merge(action.groupby('userid').actionTime.apply(lambda x:x.iloc[-i-1] if len(x)>i else np.nan).reset_index().rename(columns={'actionTime':'last_but{}_action_type'.format(i)}).reset_index(),how='left')
del action_internal_table['index']


data = data.merge(action_internal_table,on='userid',how='left')
data = data.merge(orderHistory_internal_table,on='userid',how='left')
data = data.fillna(-999)
data = pd.get_dummies(data) #剩余分类变量直接虚拟变量化

建模

  使用xgboost建模,无须数据标准化。

import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.metrics import roc_auc_score

#划分训练集和验证集
X_trainval = data[data['userid'].isin(orderFuture_train.userid.tolist())].iloc[:,2:]
y_trainval = data[data['userid'].isin(orderFuture_train.userid.tolist())].iloc[:,0]
X_train,X_val,y_train,y_val = train_test_split(X_trainval,y_trainval,random_state=88,stratify=y_trainval)

#构建xgb分类器对象并训练
xgb_cla = xgb.XGBClassifier(learning_rate=0.1,
        n_estimators=1000,
        max_depth=3,
        min_child_weight=5,
        gamma=0,
        subsample= 0.8,
        colsample_bytree=0.8,
        eta=0.05,
        silent=1,
        objective='binary:logistic',
        scale_pos_weight=1).fit(X_train,y_train)
#计算AUC
roc_auc_score(y_val,xgb_cla.predict_proba(X_val)[:,1])
#预测
X_test = data[data['userid'].isin(orderFuture_test.userid.tolist())].iloc[:,2:]
predict = xgb_cla.predict_proba(X_test)[:,1]
orderFuture_test['orderType']=predict
orderFuture_test.to_csv('submission.csv',encoding='utf-8',index=False)

最终AUC:
clipboard.png

小结

  由于电脑性能问题,部分优化未能完成,如:

  • 特征选择:可以先使用lasso选取少量重要特征作为基础,然后逐一增加剩余特征,如果AUC增加,则保留,否则剔除。
  • 模型调参:使用GridSearchCV可实现自动化调参,进一步优化模型。

  在实际项目中使用sklearn建模时,需要注意:

  • 将数据集划分为训练集、验证集、测试集,训练集用于训练模型,验证集用于验证模型,完成模型参数选择后,重新使用训练集+验证集训练模型,使用测试集最终评估模型。
  • 涉及数据标准化时,一定要注意信息泄露问题,如使用MinMaxScalar,$$X_{new} = \frac {X_{old}-X_{min}}{X_{max}-X_{min}}$$将数据标准化为0-1之间的数,在标准化时,已经使用了全体数据,如果标准化后交叉验证或网格搜索必然会导致信息泄露到验证集中,正确的方法应当是先划分训练集再进行标准化处理,再训练模型,但是直接使用GridSearchCV或者KFold无法插入标准化这一步,这时可以选择使用管道(pipe)完成。

HH丶丶
29 声望8 粉丝