赛题回顾
本赛题提供用户在2016年1月1日至2016年6月30日之间真实线上线下消费行为,预测用户在2016年7月领取优惠券后15天以内是否核销。评测指标采用AUC,先对每个优惠券单独计算核销预测的AUC值,再对所有优惠券的AUC值求平均作为最终的评价标准。
大赛地址
解决方案概述
本赛题提供了用户线下消费和优惠券领取核销行为的纪录表,用户线上点击/消费和优惠券领取核销行为的纪录表,记录的时间区间是2016.01.01至2016.06.30,需要预测的是2016年7月份用户领取优惠劵后是否核销。根据这两份数据表,我们首先对数据集进行划分,然后提取了用户相关的特征、商家相关的特征,优惠劵相关的特征,用户与商家之间的交互特征,以及利用本赛题的leakage得到的其它特征(这部分特征在实际业务中是不可能获取到的)。最后训练了XGBoost模型进行预测。
原作者代码地址
本文对其解题思路进行解析,并解读代码(部分代码有调整)。
数据集划分
可以采用滑窗的方法得到多份训练数据集,特征区间越小,得到的训练数据集越多。以下是一种划分方式:

划取多份训练集,一方面可以增加训练样本,另一方面可以做交叉验证实验,方便调参。
为什么这样划分?可以从下面两个角度理解:
1.我们提取的不管是用户特征,还是优惠券或者商户特征,其实本质上来说就是反映其特点的数据,这些属性不是很容易改变的。所以我们可以用提前一些的数据来反映其本身的特性;
2.测试集给到的时间区间是到0630,但是6.15之后的数据,本身反映不出来客户在15天内会不会用,可能现在没用,但是后面在15天之内用了,测试集在这个区间内的标签没法提取,所以训练集踢出这部分数据。但是在特征提取的时候,一些特征还是可以利用这部分数据的。
特征工程
赛题提供了online和offline两份数据集,online数据集可以提取到与用户相关的特征,offline数据集可以提取到更加丰富的特征:用户相关的特征,商家相关的特征,优惠劵相关的特征,用户-商家交互特征。
另外需要指出的是,赛题提供的预测集中,包含了同一个用户在整个7月份里的优惠券领取情况,这实际上是一种leakage,比如存在这种情况:某一个用户在7月10日领取了某优惠券,然后在7月12日和7月15日又领取了相同的优惠券,那么7月10日领取的优惠券被核销的可能性就很大了。我们在做特征工程时也注意到了这一点,提取了一些相关的特征。加入这部分特征后,AUC提升了10个百分点,相信大多数队伍都利用了这一leakage,但这些特征在实际业务中是无法获取到的。
以下简要地说明各部分特征:
用户线下相关的特征
- 用户领取优惠券次数
- 用户获得优惠券但没有消费的次数
- 用户获得优惠券并核销次数
- 用户领取优惠券后进行核销率
- 用户满050/50200/200~500 减的优惠券核销率
- 用户核销满050/50200/200~500减的优惠券占所有核销优惠券的比重
- 用户核销优惠券的平均/最低/最高消费折率
- 用户核销过优惠券的不同商家数量,及其占所有不同商家的比重
- 用户核销过的不同优惠券数量,及其占所有不同优惠券的比重
- 用户平均核销每个商家多少张优惠券
- 用户核销优惠券中的平均/最大/最小用户-商家距离
用户线上相关的特征
- 用户线上操作次数
- 用户线上点击率
- 用户线上购买率
- 用户线上领取率
- 用户线上不消费次数
- 用户线上优惠券核销次数
- 用户线上优惠券核销率
- 用户线下不消费次数占线上线下总的不消费次数的比重
- 用户线下的优惠券核销次数占线上线下总的优惠券核销次数的比重
- 用户线下领取的记录数量占总的记录数量的比重
商家相关的特征
- 商家优惠券被领取次数
- 商家优惠券被领取后不核销次数
- 商家优惠券被领取后核销次数
- 商家优惠券被领取后核销率
- 商家优惠券核销的平均/最小/最大消费折率
- 核销商家优惠券的不同用户数量,及其占领取不同的用户比重
- 商家优惠券平均每个用户核销多少张
- 商家被核销过的不同优惠券数量
- 商家被核销过的不同优惠券数量占所有领取过的不同优惠券数量的比重
- 商家平均每种优惠券核销多少张
- 商家被核销优惠券的平均时间率
- 商家被核销优惠券中的平均/最小/最大用户-商家距离
用户-商家交互特征
- 用户领取商家的优惠券次数
- 用户领取商家的优惠券后不核销次数
- 用户领取商家的优惠券后核销次数
- 用户领取商家的优惠券后核销率
- 用户对每个商家的不核销次数占用户总的不核销次数的比重
- 用户对每个商家的优惠券核销次数占用户总的核销次数的比重
- 用户对每个商家的不核销次数占商家总的不核销次数的比重
- 用户对每个商家的优惠券核销次数占商家总的核销次数的比重
优惠券相关的特征
- 优惠券类型(直接优惠为0, 满减为1)
- 优惠券折率
- 满减优惠券的最低消费
- 历史出现次数
- 历史核销次数
- 历史核销率
- 历史核销时间率
- 领取优惠券是一周的第几天
- 领取优惠券是一月的第几天
- 历史上用户领取该优惠券次数
- 历史上用户消费该优惠券次数
- 历史上用户对该优惠券的核销率
其它特征
这部分特征利用了赛题leakage,都是在预测区间提取的。
- 用户领取的所有优惠券数目
- 用户领取的特定优惠券数目
- 用户此次之后/前领取的所有优惠券数目
- 用户此次之后/前领取的特定优惠券数目
- 用户上/下一次领取的时间间隔
- 用户领取特定商家的优惠券数目
- 用户领取的不同商家数目
- 用户当天领取的优惠券数目
- 用户当天领取的特定优惠券数目
- 用户领取的所有优惠券种类数目
- 商家被领取的优惠券数目
- 商家被领取的特定优惠券数目
- 商家被多少不同用户领取的数目
- 商家发行的所有优惠券种类数目
代码解读
导入数据
#导入相关库
import numpy as np
import pandas as pd
from datetime import date
#导入数据
#这里用了keep_default_na=False,加载后数据中的缺省值默认是null,大部分是数字字段的数据类型是object即可以看做是字符串,
#当不写这句话时默认缺省值NAN,即大部分是数字字段是float,这也直接导致了怎么判断缺省值的问题:
#当是null时很好说,比如判断date字段时是否是空省值就可用:off_train=='null'
#当时NAN时,需要用函数isnull或者notnull函数来判断
off_train=pd.read_csv(r'E:\code\o2o\data\ccf_offline_stage1_train\ccf_offline_stage1_train.csv',header=0,keep_default_na=False)
off_train.columns=['user_id','merchant_id','coupon_id','discount_rate','distance','date_received','date'] #标题小写统一
off_test=pd.read_csv(r'E:\code\o2o\data\ccf_offline_stage1_test_revised.csv',header=0,keep_default_na=False)
off_test.columns=['user_id','merchant_id','coupon_id','discount_rate','distance','date_received']
on_train=pd.read_csv(r'E:\code\o2o\data\ccf_online_stage1_train\ccf_online_stage1_train.csv',header=0,keep_default_na=False)
on_train.columns=['user_id','merchant_id','action','coupon_id','discount_rate','date_received','date']
划分数据集
#划分数据集
#测试集数据
dataset3=off_test
#用于测试集提取特征的数据区间
feature3=off_train[((off_train.date>='20160315')&(off_train.date<='20160630'))|
((off_train.date=='null')&(off_train.date_received>='20160315')&(off_train.date_received<='20160630'))]
#训练集二
dataset2=off_train[(off_train.date_received>='20160515')&(off_train.date_received<='20160615')]
#用于训练集二提取特征的数据区间
feature2=off_train[((off_train.date>='20160201')&(off_train.date<='20160514'))|
((off_train.date=='null')&(off_train.date_received>='20160201')&(off_train.date_received<='20160514'))]
#训练集一
dataset1=off_train[(off_train.date_received>='20160414')&(off_train.date_received<='20160514')]
#用于训练集一提取特征的数据区间
feature1=off_train[((off_train.date>='20160101')&(off_train.date<='20160413'))|
((off_train.date=='null')&(off_train.date_received>='20160101')&(off_train.date_received<='20160413'))]
#去除重复
#原始数据收集中,一般会存在重复项,对后面的特征提取会造成干扰。
#这一步原作者并没有做,我也测试了,在同一模型参数下,去除重复可以使最后的结果有所提升。
for i in [dataset3,feature3,dataset2,feature2,dataset1,feature1]:
i.drop_duplicates(inplace=True)
i.reset_index(drop=True,inplace=True)
提取特征
1.预测区间内提取相关特征
#5 other feature
#这部分特征是利用测试集数据提取出的特征,在实际业务中是获取不到的
def get_other_feature(dataset3,filename='other_feature3'):
#用户领取的所有优惠券数目
t=dataset3[['user_id']]
t['this_month_user_receive_all_coupon_count']=1
t=t.groupby('user_id').agg('sum').reset_index()
#用户领取的特定优惠券数目
t1=dataset3[['user_id','coupon_id']]
t1['this_month_user_receive_same_coupon_count']=1
t1=t1.groupby(['user_id','coupon_id']).agg('sum').reset_index()
#如果用户领取特定优惠券2次以上,那么提取出第一次和最后一次领取的时间
t2=dataset3[['user_id','coupon_id','date_received']]
t2.date_received=t2.date_received.astype('str')
t2=t2.groupby(['user_id','coupon_id'])['date_received'].agg(lambda x:':'.join(x)).reset_index()
t2['receive_number']=t2.date_received.apply(lambda s:len(s.split(':')))
t2=t2[t2.receive_number>1]
t2['max_date_received']=t2.date_received.apply(lambda s:max([int(d) for d in s.split(':')]))
t2['min_date_received']=t2.date_received.apply(lambda s:min([int(d) for d in s.split(':')]))
t2=t2[['user_id','coupon_id','max_date_received','min_date_received']]
#用户领取特定优惠券的时间,是不是最后一次&第一次
t3=dataset3[['user_id','coupon_id','date_received']]
t3=pd.merge(t3,t2,on=['user_id','coupon_id'],how='left')
t3['this_month_user_receive_same_coupon_lastone']=t3.max_date_received-t3.date_received.astype('int')
t3['this_month_user_receive_same_coupon_firstone']=t3.date_received.astype('int')-t3.min_date_received
def is_firstlastone(x):
if x==0:
return 1
elif x>0:
return 0
else:
return -1 #those only receive once
t3.this_month_user_receive_same_coupon_lastone=t3.this_month_user_receive_same_coupon_lastone.apply(is_firstlastone)
t3.this_month_user_receive_same_coupon_firstone=t3.this_month_user_receive_same_coupon_firstone.apply(is_firstlastone)
t3=t3[['user_id','coupon_id','date_received','this_month_user_receive_same_coupon_lastone','this_month_user_receive_same_coupon_firstone']]
#用户在领取优惠券的当天,共领取了多少张优惠券
t4=dataset3[['user_id','date_received']]
t4['this_day_user_receive_all_coupon_count']=1
t4=t4.groupby(['user_id','date_received']).agg('sum').reset_index()
#用户在领取特定优惠券的当天,共领取了多少张特定的优惠券
t5=dataset3[['user_id','coupon_id','date_received']]
t5['this_day_user_receive_same_coupon_count']=1
t5=t5.groupby(['user_id','coupon_id','date_received']).agg('sum').reset_index()
#对用户领取特定优惠券的日期进行组合
t6=dataset3[['user_id','coupon_id','date_received']]
t6.date_received=t6.date_received.astype('str')
t6=t6.groupby(['user_id','coupon_id'])['date_received'].agg(lambda x:':'.join(x)).reset_index()
t6.rename(columns={
'date_received':'dates'},inplace=True)
def get_day_gap_before(s):
date_received,dates=s.split('-')
dates=dates.split(':')
gaps=[]
for d in dates:
this_gap=(date(int(date_received[0:4]),int(date_received[4:6]),int(date_received[6:8]))-
date(int(d[0:4]),int(d[4:6]),int(d[6:8]))).days
if this_gap>0:
gaps.append(this_gap)
if len(gaps)==0:
return -1
else:
return min(gaps)
def get_day_gap_after(s):

本文介绍了一种基于用户消费和优惠券核销行为预测模型,通过详细的数据集划分、特征工程和模型训练,实现了对用户领取优惠券后是否核销的准确预测。
最低0.47元/天 解锁文章
1367





