竞赛背景
皇包车(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()
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())
用户性别信息
用户性别共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%%')
用户年龄信息
用户年龄共4742条信息,以60后、70后、80后、90后为主。
fig,axes = plt.subplots(figsize=(10,4))
user.age.value_counts().plot.bar()
plt.xticks(rotation=0)
虽然女性用户多余男性,但提供年龄信息的用户中,男性多于女性,看来即使是匿名,女性也不愿意暴露年龄啊。
fig,axes = plt.subplots(figsize=(10,4))
sns.countplot(x='age',data=user,hue='gender')
用户浏览行为
行为类型一共有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)次数表示。用户活跃的两个峰值分别在四五月和十月,小长假是人们出国游的首选时间。
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)')
fig,axes = plt.subplots(figsize=(10,5))
(action.drop_duplicates(['userid']).groupby('date').userid.count()/action[action['actionType']==1].groupby('date').userid.count()).plot()
小时访问分析
数据的点击量呈现一个非常奇怪的形状,在日间(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)')
不同类型用户访问量
#对访问类型分类
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)')
用户转化模型
首先是唤醒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()
用户评价
用户评分
评价表中共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()
用户评价标签
以四分为分界线划分好评与差评,分别制作词云图如下:
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')
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')
用户评论关键词
用户评论关键词同样以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')
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')
订单数据
该数据描述了用户的历史订单信息。数据共有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()
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()
对比发现精品服务的复购率与总复购率相似。
用户出游
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()
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()
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()
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()
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()
特征工程
特征工程包括一切对特征的处理,特征提取、特征组合、标准化、特征筛选等,对连续特征进行分箱,对分类特征进行虚拟变量化,这里只提取了一些特征,没有进行特征筛选。
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)
小结
由于电脑性能问题,部分优化未能完成,如:
- 特征选择:可以先使用lasso选取少量重要特征作为基础,然后逐一增加剩余特征,如果AUC增加,则保留,否则剔除。
- 模型调参:使用GridSearchCV可实现自动化调参,进一步优化模型。
在实际项目中使用sklearn建模时,需要注意:
- 将数据集划分为训练集、验证集、测试集,训练集用于训练模型,验证集用于验证模型,完成模型参数选择后,重新使用训练集+验证集训练模型,使用测试集最终评估模型。
- 涉及数据标准化时,一定要注意信息泄露问题,如使用MinMaxScalar,$$X_{new} = \frac {X_{old}-X_{min}}{X_{max}-X_{min}}$$将数据标准化为0-1之间的数,在标准化时,已经使用了全体数据,如果标准化后交叉验证或网格搜索必然会导致信息泄露到验证集中,正确的方法应当是先划分训练集再进行标准化处理,再训练模型,但是直接使用GridSearchCV或者KFold无法插入标准化这一步,这时可以选择使用管道(pipe)完成。