摘要:O2O优惠券使用新人赛数据发掘工程主要是介绍O2O优惠券使用新人赛数据是如何被挖掘的,本文对单一字段及组合字段从经验上做出实践及可视化分析,并在后面介绍一些突破瓶颈的一些方法和经验。
cctristan同学在<<O2O优惠券使用新人赛-初试>>notebook里作了很好的入门示范,本文会在它的基础上进行补充。新手在入门本章之前可以对cctristan同学的<<O2O优惠券使用新人赛-初试>>notebook进行了解。
2.1 引入工程所需要的库
import pandas as pd
import seaborn as sns
import numpy as np
from matplotlib import pyplot as plt
import matplotlib
from datetime import datetime
import warnings
warnings.filterwarnings("ignore")
%matplotlib inline
我们需要对引用的库需要做一个基本的了解,比如说我们在进行可视化分析的时候,要知道seaborn有哪些样式的风格可以使用
print(plt.style.available)
print('pandas version:',pd.__version__)
print('seaborn version:',sns.__version__)
print('matplotlib version:',matplotlib.__version__)
2.2 读取数据
path ='datalab/Coupon Usage Data for O2O/'
train = pd.read_csv(path+'ccf_offline_stage1_train.csv')
train.head()
train.info()
train.isnull().sum()/len(train)
当我们读取数据,需要了解数据有7个列,有两个int64字段,5个object类型字段,没有缺失值。由此可知官方给出的数据已经帮助我们在缺失值做了一部分的工作,更细分的我们还需要知道5个object类型字段具体的内容是怎样。比如说Coupon_id字段有数字,也有字符串null,这个是在数据挖掘的工程里需要了解。
2.3 单一字段分析
(1) User_id 首先是一个用户编号,在工程里用户特征是非常重要的,因为它具有个性化及统计意义。这个工程属于用户画像的一部分,比如用户的年龄划分,性别划分,教育程度划分都是基础的工作,当然在这个工程里并没有年龄,性别及教育程度的特征,可以不作讨论。在<<O2O优惠券使用新人赛-初试>>提到,训练集及测试集的用户几乎是完全覆盖的。这样的特征字段其实对于线上线下同分布是及其有利的。
len(set(train['User_id']))
train['sum'] = 1
user_id_count = train.groupby(['User_id'], as_index = False)['sum'].agg({'count':np.sum})
user_id_count.sort_values(['count'],ascending=0)[:20]
我们发现了两个统计量,第一个是训练集有接近54万用户,第二是数据统计用户数据前20的用户编号和次数,这个可以帮助我们看到哪些用户操作是比较频繁的,这可以让我们联想到"重度用户"。我们当然可以对这些用户的操作数据进行可视化分析。
def user_count(data):
if data > 10:
return 3
elif data > 5:
return 2
elif data > 1:
return 1
else:
return 0
user_id_count['user_range'] = user_id_count['count'].map(user_count)
sns.set(font_scale=1.2,style="white")
f,ax=plt.subplots(1,2,figsize=(17.5,8))
user_id_count['user_range'].value_counts().plot.pie(explode=[0,0.1,0.1,0.1],autopct='%1.1f%%',ax=ax[0])
plt.ylim([0, 300000])
ax[0].set_title('user_range_ratio')
ax[0].set_ylabel('')
sns.despine()
sns.countplot('user_range',data=user_id_count,ax=ax[1])
plt.show()
从这个可视化结果,我们可以得到只有1次记录的用户占训练集的44.8%,有2到5次记录的用户占41.2%,6到10次记录占8.9%,超过10次记录占5.1%,这确实符合常规现象。
(2) Merchant_id 同理 User_id
Mer_id_count = train.groupby(['Merchant_id'], as_index = False)['sum'].agg({'count':np.sum})
Mer_id_count.sort_values(['count'],ascending=0)[:5]
def Mer_count(data):
if data > 1000:
return 3
elif data > 100:
return 2
elif data > 20:
return 1
else:
return 0
Mer_id_count['mer_range'] = Mer_id_count['count'].map(Mer_count)
sns.set(font_scale=1.2,style="white")
f,ax=plt.subplots(1,2,figsize=(17.5,8))
Mer_id_count['mer_range'].value_counts().plot.pie(explode=[0,0.1,0.1,0.1],autopct='%1.1f%%',ax=ax[0])
plt.ylim([0, 6000])
ax[0].set_title('Mer_range_ratio')
ax[0].set_ylabel('')
sns.despine()
sns.countplot('mer_range',data=Mer_id_count,ax=ax[1])
plt.show()
从这个可视化结果,可以得到商家记录的数据小于等于20的占训练集的52.8%,大于20小于等于100次数占33.5%,大于100小于等于1000占12.0%,超过1000次记录占1.7%。统计次数最多的商家编号是3381,超过14万。
(3) Coupon_id 是一个非常重要的字段,所有问题的核心都会围绕这个字段去展开。这个字段与前两者不同的是它包含一些缺失值,先对优惠券的缺失值的数据进行统计。
print('有优惠券的数据为', train[(train['Coupon_id'] != 'null')].shape[0])
print('无优惠券的数据为', train[(train['Coupon_id'] == 'null')].shape[0])
train1 = train[(train['Coupon_id'] != 'null')]
Cou_id_count = train1.groupby(['Coupon_id'], as_index = False)['sum'].agg({'count':np.sum})
Cou_id_count.sort_values(['count'],ascending=0)[:5]
def Cou_count(data):
if data > 1000:
return 3
elif data > 100:
return 2
elif data > 10:
return 1
else:
return 0
Cou_id_count['Cou_range'] = Cou_id_count['count'].map(Cou_count)
sns.set(font_scale=1.2,style="white")
f,ax=plt.subplots(1,2,figsize=(17.5,8))
Cou_id_count['Cou_range'].value_counts().plot.pie(explode=[0,0.1,0.1,0.1],autopct='%1.1f%%',ax=ax[0])
plt.ylim([0, 7000])
ax[0].set_title('Cou_range_ratio')
ax[0].set_ylabel('')
sns.despine()
sns.countplot('Cou_range',data=Cou_id_count,ax=ax[1])
plt.show()
我们可以得到有优惠券的数据占到总数据的60%,在优惠券的数据里,小于等于10的占训练集的53.9%,大于10小于等于100次数占39.6%,大于100小于等于1000占5.2%,超过1000次记录占1.4%。统计次数最多的优惠券编号是7610,超过4.6万。
(4)Discount_rate字段包含一些非结构化的字符串数据,比如'150:20' '20:1' '200:20' '30:5' '50:10' '10:5' '100:10' '200:30',像这种数据其实需要一定的数据转化,按照经验可以用1-20/150为折扣率,按照上述的方法也可以得到折扣率的统计及分布。
def convertRate(row):
"""Convert discount to rate"""
if row == 'null':
return 1.0
elif ':' in row:
rows = row.split(':')
return np.round(1.0 - float(rows[1])/float(rows[0]),2)
else:
return float(row)
train['discount_rate'] = train['Discount_rate'].apply(convertRate)
print('Discount_rate 类型:',train['discount_rate'].unique())
Discount_rate字段跟其它字段不一样的是,这个字段有20种折扣率的类型,不想其它字段类型种类上万,其次20种类型不太适合饼图。
sns.set(style="white")
sns.factorplot(x="discount_rate", data=train, kind="count", size=6, aspect=2,color='#ad9ee8')
可以统计不打折的优惠券(就是没有优惠)占到40%,打折的优惠券的折扣率基本分布在0.5至0.97之间,9折优惠券最多,8.3折其次
(5)Distance字段分析同Discount_rate
sns.set(style="white")
sns.factorplot(x="Distance", data=train, kind="count", size=6, aspect=2)
可以统计Distance为0占总数的40%以上,在图中可以看到有一个不是数字的类型,对于这种缺失值可以转化为数字比如-1,也可以把数字类型转化为字符串类型。
(6)Date_received和Date 都属于时间类型字段,也是非常重要的特征。借鉴cctristan同学<<O2O优惠券使用新人赛-初试>>notebook方法,可以得到比较明显的可视化分析。
couponbydate = train[train['Date_received'] != 'null'][['Date_received', 'Date']].groupby(['Date_received'], as_index=False).count()
couponbydate.columns = ['Date_received','count']
buybydate = train[(train['Date'] != 'null') & (train['Date_received'] != 'null')][['Date_received', 'Date']].groupby(['Date_received'], as_index=False).count()
buybydate.columns = ['Date_received','count']
date_buy = train['Date'].unique()
date_buy = sorted(date_buy[date_buy != 'null'])
date_received = train['Date_received'].unique()
date_received = sorted(date_received[date_received != 'null'])
sns.set_style('ticks')
sns.set_context("notebook", font_scale= 1.4)
plt.figure(figsize = (12,8))
date_received_dt = pd.to_datetime(date_received, format='%Y%m%d')
plt.subplot(211)
plt.bar(date_received_dt, couponbydate['count'], label = 'number of coupon received',color='#a675a1')
plt.bar(date_received_dt, buybydate['count'], label = 'number of coupon used',color='#75a1a6')
plt.yscale('log')
plt.ylabel('Count')
plt.legend()
plt.subplot(212)
plt.bar(date_received_dt, buybydate['count']/couponbydate['count'],color='#62a5de')
plt.ylabel('Ratio(coupon used/coupon received)')
plt.tight_layout()
我们可以从可视化的结果发现在春节期间优惠券发放数目是最多,但消费的比例是最低的,因此可以得出春节是一个比较明显但特征,而5月1日但表现却属于中规中矩,节日特征的选择需要慎重。其次在3月20日左右,消费的比列是最高的,这种现象其实需要深究,比如说3月20日左右是否优惠券的折扣力度是否提高,那么就要观察折扣率力度大的优惠券折扣率在3月20日左右分布如何,是否比平时折扣率的力度要大等等。这些会在复合字段去分析,其实包活上述优惠券每天领取,消费,消费的比例都属于复合特征的一部分。
2.4 复合字段分析
复合特征(组合特征)是整个数据挖掘工程的重中之重,用户喜欢哪种类型的商家,用户喜欢哪种类型的优惠券并消费哪种优优惠券,哪种类型的优惠券的商家消费的最多,在什么时间优惠券消费的最多都是很重要的特征,即便上述讲到的每天优惠券消费的比列也是不错的特征。在这个工程,特征的组合的重要性几乎占到了90%以上的比列。
2.4.1 以用户特征为主体进行组合,所描述的就是用户画像。
(1) UM(User_id and Merchant_id)组合特征
um_count = train.groupby(['User_id','Merchant_id'], as_index = False)['sum'].agg({'count':np.sum})
um_count.head(5)
这个特征就可以告诉我们,每个用户所对应的商家的记录有多少条。按照这样的思路,我们还可以统计每个用户所领取商家的优惠券有多少条,没有领取该商家优惠券有多少条,消费该商家的优惠券有多少条,该商家优惠券消费的比例是多少等等这些特征都可以得到。
(2) UC(User_id and Coupon_id)组合特征
train1 = train[train['Coupon_id'] != 'null']
uc_count = train1.groupby(['User_id','Coupon_id'], as_index = False)['sum'].agg({'count':np.sum})
uc_count.head(5)
同样可以统计得到每个用户所对应优惠券的数目,再根据上述UM统计可以知道编号为4,35分别有2次及4次记录,而这些记录都是有优惠券。
(3)UR(User_id and Discount_rate)组合特征
train['discount_rate'] = train['Discount_rate'].apply(convertRate)
折扣率在转换后发现有20种不同的数字,因为20并不是一个很大的数字,所以我们可以进行下面的组合
ur_count = train.groupby(['User_id','discount_rate'], as_index = False)['sum'].agg({'count':np.sum})
ur_count.head(5)
同理可以统计用户优惠券折扣率的分布情况,也可以统计到每个用户平均折扣率
(4)UD(User_id and Distance)组合特征
ud_count = train.groupby(['User_id','Distance'], as_index = False)['sum'].agg({'count':np.sum})
ud_count.head(5)
同理可以统计用户在距离商家距离的分布情况。如果不太清楚其特征的统计意义,可以从业务上去理解,比如说用户为4在Distance为10领过两次,Distance为10字段有说明是距离商家超过5公里的地方,说明这个这个潜在的用户距离商家比较远。
(4)UT(User_id and Time)组合特征, 用户和时间的特征有两个,一个是用户领取优惠券的时间组合特征,二是用户消费优惠券的消费特征。
from datetime import date
def getWeekday(row):
if row == 'null':
return row
else:
return date(int(row[0:4]), int(row[4:6]), int(row[6:8])).weekday() + 1
train['received_weekday'] = train['Date_received'].astype(str).apply(getWeekday)
train['consume_weekday'] = train['Date'].astype(str).apply(getWeekday)
# weekday_type : 周六和周日为1,其他为0
train['received_weekday_type'] = train['received_weekday'].apply(lambda x : 1 if x in [6,7] else 0 )
train['consume_weekday_type'] = train['consume_weekday'].apply(lambda x : 1 if x in [6,7] else 0 )
u_week_count = train.groupby(['User_id','received_weekday'], as_index = False)['sum'].agg({'count':np.sum})
u_week_type_count = train.groupby(['User_id','received_weekday_type'], as_index = False)['sum'].agg({'count':np.sum})
u_week_count.head(5)
可以得到每个用户在什么日期领取到优惠券,每个用户在是否是周末领取优惠券的频率,比如说用户4,星期二和星期七各取一次,频率为1:1
train1 = train[train['Date'] != 'null']
u_con_week_count = train1.groupby(['User_id','consume_weekday'], as_index = False)['sum'].agg({'count':np.sum})
u_con_week_type_count = train1.groupby(['User_id','consume_weekday_type'], as_index = False)['sum'].agg({'count':np.sum})
u_con_week_count.head(10)
同理也可以得到用户在什么日期消费优惠券及频率,比如165在非周末消费的比例为100%。而我们之前提到的用户4并没有消费。
按照这样的思路在做细一点,比如用户与节日组合特征分布情况,用户与日期小时组合分布情况都是必要的。为什么要做,因为它是用户画像工程的作业,你可以清晰的看到每个用户的行为习惯。接着再去做商家的画像,比如说这个商家潜在用户分布在什么位置,方圆1公里之内有多少潜在的用户,有多少消费的用户,什么时间用户领取的消费券最多,什么时间用户消费的优惠券最多等等诸如此类。
通常我们在中后期都会有一个经常出现的现象,这种现象我把它称之为台阶现象,我们大部分人经常会卡在某一个阶段,遇到瓶颈,很难突破,实验的结果经常是平稳的,当我们突破这一阶段,实验的结果就会出现新的台阶又回在这个曲线上保持相对平稳。
我们都希望能够突破瓶颈,并想拥有随时突破瓶颈的能力。当然我们也许可以在梦里实现,现实是非常难做的到。突破瓶颈的方法比如说有暴力搜索,还有很笨的一个个特征试。只要做足够多的实验,总会比之前的结果要好。这种方法在技术圈不少拿奖的方案里看过,比如说小数据的比赛。但这种方法其实在工业界上不太值得提倡的,作为一些尝试性的技巧是OK的。
更好的优秀的方法是从业务数据本身出发,找到它背后的规律,可以来源于你的生活,你的工作经验等等。比如说本作业,优惠券有一个非常重要的规律就是领取到消费券随着时间它消费的比例会越来越低。如果当从本身数据来看,很难发现这个规律,能够快速认可这个规律一个很大的原因是这种规律在生活中非常的熟悉。这就是画龙点晴的特征。
1.本章从工程的角度进行数据发掘作业,多走一些正道。