天池 O2O 优惠券使用预测思路解析与代码实战

前阵子因为机器学习训练营的任务安排,需要打 AI 比赛。然后我了解到最近一场热度很高,非常适合新人入门的比赛:天池新人实战赛o2o优惠券使用预测。今天,红石将与大家分享本次比赛的一些初级理论分析和代码实践操作。本文将详细解释,目的是引导您再次参与比赛过程,实现机器学习理论分析到实际战斗的进步。不要说太多,让我们开始吧!

比赛介绍

首先附上这场比赛的链接:

天池天池新人实战赛o2o预测 使用优惠券| 赛制介绍

本赛题的比赛背景是随着移动设备的完善和普及,移动互联网 各行各业进入了高速发展阶段,这其中以 O2O(Online to Offline)消费最引人注目。本次比赛为参赛者提供 O2O 丰富的场景相关数据,希望参赛者能通过分析建模,准确预测用户是否会在规定的时间(15 天)内使用相应的优惠券。

从机器学习模型的角度来看,这是一个典型的分类问题,其过程是根据现有的培训集进行培训,然后测试和分类获得的模型。整个过程如下图所示:

评估方式

我们知道评估机器学习模型的方法有很多,比如准确性(Accuracy)、精确率(Precision)、召回率(Recall)。精确率和召回率一般结合 F1 score 可以更好地评估模型性能(尤其是在正负样本不平衡的情况下)。在本赛题中,官方评估方法是 AUC,即 ROC 曲线线和横坐标组成的区域。如下图所示:

关于 ROC 和 AUC 的概念在这里概念。至于为什么要用 ROC 和 AUC ?因为 ROC 曲线具有良好的特性:当测试集中的正负样本分布发生变化时,ROC曲线可以保持不变。也就是说,正负样本分布不均的场景可以更好地处理。

数据集导入

数据集永远是任何机器学习模型中最重要的。接下来,让我们来看看这个游戏的数据集。

首先,让我们来看看比赛提供给我们的数据集:

共有四份文件:

ccf_offline_stage1_test_revised.csvccf_offline_stage1_train.csvccf_online_stage1_train.csvsample_submission.csv

其中,第 2 是线下培训集,第 1 是线下测试集,第 3 是线上培训集(本文不用),第 4 是预测结果提交到官网的文件格式(只有按照这个格式提交才有效)。也就是说,我们使用第 2 文件来培训模型,预测第 1 文件,以获得用户在 15 天内使用优惠券的概率值。

接下来,列出 2、1、4 文件中的字段,如下图所示。

ccf_offline_stage1_train.csv:

ccf_offline_stage1_test_revised.csv:

sample_submission.csv:

记住两个字段:Date_received 是收到优惠券的日期,Date 是消费日期。以后我会详细介绍。

在介绍了几个数据文件和字段后,我们将编写程序,导入训练集和测试集,并导入需要使用的库。

# import libraries necessary for this projectimport os,sys,pickleimport numpy as npimport pandas as pdfrom datetime import datefrom sklearn.model_selection import KFold,train_test_split,StratifiedKFold,cross_val_score,GridSearchCVfrom sklearn.pipeline import Pipelinefrom sklearn.linear_model import SGDClassifier,LogisticRegressionfrom sklearn.preprocessing import StandardScalerfrom sklearn.metrics import log_loss,roc_auc_score,auc,roc_curvefrom sklearn.preprocessing import MinMaxScaler# display for this notebook% ** tplotlib inline%config InlineBackend.figure_for ** t = 'retina'

导入数据:

dfoff = pd.read_csv('data/ccf_offline_stage1_train.csv')dfon = pd.read_csv('data/ccf_online_stage1_train.csv')dftest = pd.read_csv('data/ccf_offline_stage1_test_revised.csv')dfoff.head(5)

训练前 5 行显示如下:

接下来,让我们做一个简单的统计,看看用户是否使用优惠券。

print('.有优惠卷,购买商品:%d' % dfoff[(dfoff['Date_received'] != 'null') & (dfoff['Date'] != 'null')].shape[0])print('.有优惠卷,未购商品:%d' % dfoff[(dfoff['Date_received'] != 'null') & (dfoff['Date'] == 'null')].shape[0])print(';无优惠卷,购买商品:%d' % dfoff[(dfoff['Date_received'] == 'null') & (dfoff['Date'] != 'null')].shape[0])print(&#三九、无优惠卷,未购商品:%d' % dfoff[(dfoff['Date_received'] == 'null') & (dfoff['Date'] == 'null')].shape[0])有优惠卷,购买商品:75382有优惠卷,未购商品:97900无优惠卷,购买商品:701602无优惠卷,未购商品:0

可以看出,很多人(701602)没有使用优惠券购买商品,也有很多人(977900)有优惠券但没有使用,很少有人真正使用优惠券购买商品(75382)!因此,这场比赛的意义是给那些真正可能购买商品的人。

特征提取

毫不夸张地说,特征工程可能比选择哪种算法更重要。接下来,让我们研究哪些特征可能对模型训练有用。

1.打折率(Discount_rate)

首先,首先要考虑的特点应该是优惠券的折扣率。因为很明显,一般来说,折扣越多,用户使用优惠券的可能性就越大。所以,让我们来看看训练集中折扣的类型。

print('Discount_rate 类型:n',dfoff['Discount_rate'].unique())Discount_rate 类型:[null 150:20 20:1 200:20 30:5 50:10 10:5>30:10 50:5 150:10 100:30 200:50 100:50 300:30 50:20.10:1 >‘30:1’ ‘0.100:5 5:1 100:20 .850:1 200:10 300:20>150:30 300:50 20:10.85’ ‘0.150:50 0.75’ ‘0.5 200:5.7’ > 30:20 300:10.50:30 200:100 150:5

根据打印结果,打折率分为 3 :

null 表示没有折扣[0,1] 表示折扣率x:y 表示满 x 减 y

那么我们的处理方法可以构建 4 函数,分别提取 4 特征,即:

打折类型:getDiscountType()折扣率:convertRate()满多少:getDiscountMan()减少多少:getDiscountJian()

函数代码如下:

# Convert Discount_rate and Distancedef getDiscountType(row): if row == 'null': return 'null' elif ':' in row: return 1 else: return 0def convertRate(row): """Convert discount to rate""" if row == 'null': return 1.0 elif ':' in row: rows = row.split(':'.) return 1.0 - float(rows[1])/float(rows else: return float(row) def getDiscountMan(row): if ':' in row: rows = row.split(':'.) return int(rows else: return 0def getDiscountJian(row): if ':' in row: rows = row.split(':'.) return int(rows else: return 0 def processData(df): # convert discount_rate df['discount_type'] = df['Discount_rate'].apply(getDiscountType) df['discount_rate'] = df['Discount_rate'].apply(convertRate) df['discount_ ** n'] = df['Discount_rate'].apply(getDiscountMan) df['discount_jian'] = df['Discount_rate'].apply(getDiscountJian) print(df['discount_rate'].unique() return df

然后分别对训练集和测试集进行 processData()函数处理:

dfoff = processData(dfoff)dftest = processData(dftest)

经过处理,我们可以看到训练集和测试集都有 4 的新特点:discount_type、discount_rate、discount_ ** n、discount_jian。

2.距离(Distance)

距离字段表示用户与商店的地理距离,显然,距离的远近也会影响到优惠券的使用与否。那么,我们就可以把距离也作为一个特征。首先看一下距离有哪些特征值:

print('Distance 类型:',dfoff['Distance'].unique())Distance 类型: [‘0’ ‘1’ ‘null’ ‘2’ ‘10’ ‘4’ ‘7’ ‘9’ ‘3’ ‘5’ ‘6’ ‘8’]

然后,定义提取距离特征的函数:

# convert distancedfoff['distance'] = dfoff['Distance'].replace('null', -1).astype(int)print(dfoff['distance'].unique())dftest['distance'] = dftest['Distance'].replace('null', -1).astype(int)print(dftest['distance'].unique())

处理之后,我们可以看到训练集和测试集都多出了 1 个新的特征:distance。

3.领劵日期(Date_received)

是还有一点很重要的是领券日期,因为一般而言,周末领取优惠券去消费的可能性更大一些。因此,我们可以构建关于领券日期的一些特征:

weekday : {null, 1, 2, 3, 4, 5, 6, 7}weekday_type : {1, 0}(周六和周日为1,其他为0)Weekda_1 : {1, 0, 0, 0, 0, 0, 0}Weekday_2 : {0, 1, 0, 0, 0, 0, 0}Weekday_3 : {0, 0, 1, 0, 0, 0, 0}Weekday_4 : {0, 0, 0, 1, 0, 0, 0}Weekday_5 : {0, 0, 0, 0, 1, 0, 0}Weekday_6 : {0, 0, 0, 0, 0, 1, 0}Weekday_7 : {0, 0, 0, 0, 0, 0, 1}

其中用到了独热编码,让特征更加丰富。相应的这 9 个特征的提取函数为:

def getWeekday(row): if row == 'null': return row else: return date(int(row[0:4]), int(row[4:6]), int(row[6:8])).weekday() + 1dfoff['weekday'] = dfoff['Date_received'].astype(str).apply(getWeekday)dftest['weekday'] = dftest['Date_received'].astype(str).apply(getWeekday)# weekday_type : 周六和周日为1,其他为0dfoff['weekday_type'] = dfoff['weekday'].apply(lambda x: 1 if x in [6,7] else 0)dftest['weekday_type'] = dftest['weekday'].apply(lambda x: 1 if x in [6,7] else 0)# change weekday to one-hot encoding weekdaycols = ['weekday_' + str(i) for i in range(1,8)]#print(weekdaycols)tmpdf = pd.get_dummies(dfoff['weekday'].replace('null', np.nan))tmpdf.columns = weekdaycolsdfoff[weekdaycols] = tmpdftmpdf = pd.get_dummies(dftest['weekday'].replace('null', np.nan))tmpdf.columns = weekdaycolsdftest[weekdaycols] = tmpdf

这样,我们就会在训练集和测试集上发现增加了 9 个关于领券日期的特征:

好了,经过以上简单的特征提取,我们总共得到了 14 个有用的特征:

discount_ratediscount_typediscount_ ** ndiscount_jiandistanceweekdayweekday_typeweekday_1weekday_2weekday_3weekday_4weekday_5weekday_6weekday_7

好了,我们的主要工作已经完成了大半!

标注标签 Label

有了特征之后,我们还需要对训练样本进行 label 标注,即确定哪些是正样本(y = 1),哪些是负样本(y = 0)。我们要预测的是用户在领取优惠券之后 15 之内的消费情况。所以,总共有三种情况:

Date_received == ‘null’:

表示没有领到优惠券,无需考虑,y = -1

2. (Date_received != ‘null’) & (Date != ‘null’) & (Date - Date_received <= 15):

表示领取优惠券且在15天内使用,即正样本,y = 1

3.(Date_received != ‘null’) & ((Date == ‘null’) | (Date - Date_received > 15)):

表示领取优惠券未在在15天内使用,即负样本,y = 0

好了,知道规则之后,我们就可以定义标签备注函数了。

def label(row): if row['Date_received'] == 'null': return -1 if row['Date'] != 'null': td = pd.to_datetime(row['Date'], for ** t='%Y%m%d') - pd.to_datetime(row['Date_received'], for ** t='%Y%m%d') if td <= pd.Timedelta(15, 'D'): return 1 return 0dfoff['label'] = dfoff.apply(label, axis=1)

我们可以使用这个函数对训练集进行标注,看一下正负样本究竟有多少:

print(dfoff['label'].value_counts())0 988887-1 7016021 ** 395Name: label, dtype: int **

很清晰地,正样本共有 ** 395 例,负样本共有 988887 例。显然,正负样本数量差别很大。这也是为什么会使用 AUC 作为模型性能评估标准的原因。

建立模型

接下来就是最主要的建立机器学习模型了。首先确定的是我们选择的特征是上面提取的 14 个特征,为了验证模型的性能,需要划分验证集进行模型验证,划分方式是按照领券日期,即训练集:20160101-20160515,验证集:20160516-20160615。我们采用的模型是简单的 SGDClassifier。

1.划分训练集和验证集

# data splitdf = dfoff[dfoff['label'] != -1].copy()train = df[(df['Date_received'] < '20160516')].copy()valid = df[(df['Date_received'] >= '20160516') & (df['Date_received'] <= '20160615')].copy()print('Train Set: n', train['label'].value_counts())print('Valid Set: n', valid['label'].value_counts())

2.构建模型

def check_model(data, predictors): classifier = lambda: SGDClassifier( loss='log', # loss function: logistic regression penalty='elasticnet', # L1 & L2 fit_intercept=True, # 是否存在截距,默认存在 ** x_iter=100, shuffle=True, # Whether or not the training data should be shuffled after each epoch n_jobs=1, # The number of processors to use class_weight=None) # Weights associated with classes. If not given, all classes are supposed to have weight one. # 管道机制使得参数集在新数据集(比如测试集)上的重复使用,管道机制实现了对全部步骤的流式化封装和管理。 model = Pipeline(steps=[ ('ss', StandardScaler()), # transformer ('en', classifier()) # esti ** tor ]) parameters = { 'en__alpha': [ 0.001, 0.01, 0.1], 'en__l1_ratio': [ 0.001, 0.01, 0.1] } # StratifiedKFold用法类似Kfold,但是他是分层采样,确保训练集,测试集中各类别样本的比例与原始数据集中相同。 folder = StratifiedKFold(n_splits=3, shuffle=True) # Exhaustive search over specified parameter values for an esti ** tor. grid_search = GridSearchCV( model, parameters, cv=folder, n_jobs=-1, # -1 means using all processors verbose=1) grid_search = grid_search.fit(data[predictors], data['label']) return grid_search

模型采用的是 SGDClassifier,使用了 Python 中的 Pipeline 管道机制,可以使参数集在新数据集(比如测试集)上的重复使用,管道机制实现了对全部步骤的流式化封装和管理。交叉验证采用 StratifiedKFold,其用法类似 Kfold,但是 StratifiedKFold 是分层采样,确保训练集,测试集中各类别样本的比例与原始数据集中相同。

3.训练

接下来就可以使用该模型对训练集进行训练了,整个训练过程大概 1-2 分钟的时间。

predictors = original_featuremodel = check_model(train, predictors)

4.验证

然后对验证集中每个优惠券预测的结果计算 AUC,再对所有优惠券的 AUC 求平均。计算 AUC 的时候,如果 label 只有一类,就直接跳过,因为 AUC 无法计算。

# valid predicty_valid_pred = model.predict_proba(valid[predictors])valid1 = valid.copy()valid1['pred_prob'] = y_valid_pred[:, 1]valid1.head(5)

注意这里得到的结果 pred_prob 是概率值(预测样本属于正类的概率)。

最后,就可以对验证集计算 AUC。直接调用 sklearn 库自带的计算 AUC 函数即可。

# avgAUC calculationvg = valid1.groupby(['Coupon_id'])aucs = []for i in vg: tmpdf = i[1] if len(tmpdf['label'].unique()) != 2: continue fpr, tpr, thresholds = roc_curve(tmpdf['label'], tmpdf['pred_prob'], pos_label=1) aucs.append(auc(fpr, tpr))print(np.average(aucs))0.532344469452

最终得到的 AUC 就等于 0.53。

测试

训练完模型之后,就是使用训练好的模型对测试集进行测试了。并且将测试得到的结果(概率值)按照规定的格式保存成一个 .csv 文件。

# test prediction for submissiony_test_pred = model.predict_proba(dftest[predictors])dftest1 = dftest[['User_id','Coupon_id','Date_received']].copy()dftest1['Probability'] = y_test_pred[:,1]dftest1.to_csv('submit.csv', index=False, header=False)dftest1.head(5)

值得注意的是,这里得到的结果是概率值,最终的 AUC 是提交到官网之后平台计算的。因为测试集真正的 label 我们肯定是不知道的。

提交结果

好了,最后一步就是在比赛官网上提交我们的预测结果,即这里的 submit.csv 文件。提交完之后,过几个小时就可以看到成绩了。整个比赛的流程就完成了。

优化模型

其实,本文所述的整个比赛思路和算法是比较简单的,得到的结果和成绩也只能算是合格,名次不会很高。我们还可以运用各种手段优化模型,简单来说分为以下三种:

特征工程机器学习模型融合

总结

本文的主要目的是带领大家走一遍整个比赛的流程,培养一些比赛中特征提取和算法应用方面的知识。这个天池比赛目前还是比较火热的,虽然没有奖金,但是参赛人数已经超过 1.1w 了。看完本文之后,希望大家有时间去参加感受一下机器学习比赛的氛围,将理论应用到实战中去。

本文完整的代码我已经放在了 GitHub 上,有需要的请自行领取:

RedstoneWill/MachineLearningInAction-Camp

同时,本比赛第一名的代码也开源了,一同放出,供大家学习:

wepe/O2O-Coupon-Usage-Forecast

扫码免费用

源码支持二开

申请免费使用

在线咨询