天池 O2O 优惠券使用预测思路解析与代码实战
個(gè)人網(wǎng)站:redstonewill.com
前陣子因?yàn)闄C(jī)器學(xué)習(xí)訓(xùn)練營(yíng)的任務(wù)安排,需要打一場(chǎng) AI 比賽。然后就了解到最近熱度很高且非常適合新人入門的一場(chǎng)比賽:天池新人實(shí)戰(zhàn)賽o2o優(yōu)惠券使用預(yù)測(cè)。今天,紅色石頭把這場(chǎng)比賽的一些初級(jí)理論分析和代碼實(shí)操分享給大家。本文會(huì)講解的很細(xì),目的是帶領(lǐng)大家走一遍比賽流程,實(shí)現(xiàn)機(jī)器學(xué)習(xí)理論分析到比賽實(shí)戰(zhàn)的進(jìn)階。話不多說,我們開始吧!
比賽介紹
首先附上這場(chǎng)比賽的鏈接:
https://tianchi.aliyun.com/getStart/introduction.htm?spm=5176.100066.0.0.518433afBqXIKM&raceId=231593
本賽題的比賽背景是隨著移動(dòng)設(shè)備的完善和普及,移動(dòng)互聯(lián)網(wǎng)+各行各業(yè)進(jìn)入了高速發(fā)展階段,這其中以 O2O(Online to Offline)消費(fèi)最為吸引眼球。本次大賽為參賽選手提供了 O2O 場(chǎng)景相關(guān)的豐富數(shù)據(jù),希望參賽選手通過分析建模,精準(zhǔn)預(yù)測(cè)用戶是否會(huì)在規(guī)定時(shí)間(15 天)內(nèi)使用相應(yīng)優(yōu)惠券。
從機(jī)器學(xué)習(xí)模型的角度來說,這是一個(gè)典型的分類問題,其過程就是根據(jù)已有訓(xùn)練集進(jìn)行訓(xùn)練,得到的模型再對(duì)測(cè)試進(jìn)行測(cè)試并分類。整個(gè)過程如下圖所示:
評(píng)估方式
我們知道評(píng)估一個(gè)機(jī)器學(xué)習(xí)模型有多種方式,最常見的例如準(zhǔn)確率(Accuracy)、精確率(Precision)、召回率(Recall)。一般使用精確率和召回率結(jié)合的方式 F1 score 能較好地評(píng)估模型性能(特別是在正負(fù)樣本不平衡的情況下)。而在本賽題,官方規(guī)定的評(píng)估方式是 AUC,即 ROC 曲線與橫坐標(biāo)圍成的面積。如下圖所示:
關(guān)于 ROC 和 AUC 的概念這里不加解釋,至于為什么要使用 ROC 和 AUC 呢?因?yàn)?ROC 曲線有個(gè)很好的特性:當(dāng)測(cè)試集中的正負(fù)樣本的分布變化的時(shí)候,ROC曲線能夠保持不變。也就是說能夠更好地處理正負(fù)樣本分布不均的場(chǎng)景。
數(shù)據(jù)集導(dǎo)入
對(duì)任何機(jī)器學(xué)習(xí)模型來說,數(shù)據(jù)集永遠(yuǎn)是最重要的。接下來,我們就來看看這個(gè)比賽的數(shù)據(jù)集是什么樣的。
首先來看一下大賽提供給我們的數(shù)據(jù)集:
總共有四個(gè)文件,分別是:
-
ccf_offline_stage1_test_revised.csv
-
ccf_offline_stage1_train.csv
-
ccf_online_stage1_train.csv
-
sample_submission.csv
其中,第 2 個(gè)是線下訓(xùn)練集,第 1 個(gè)是線下測(cè)試集,第 3 個(gè)是線上訓(xùn)練集(本文不會(huì)用到),第 4 個(gè)是預(yù)測(cè)結(jié)果提交到官網(wǎng)的文件格式(需按照此格式提交才有效)。也就是說我們使用第 2 個(gè)文件來訓(xùn)練模型,對(duì)第 1 個(gè)文件進(jìn)行預(yù)測(cè),得到用戶在 15 天內(nèi)使用優(yōu)惠券的概率值。
接下來,對(duì) 2、1、4 文件中字段進(jìn)行列舉,字段解釋如下圖所示。
ccf_offline_stage1_train.csv:
ccf_offline_stage1_test_revised.csv:
sample_submission.csv:
重點(diǎn)記住兩個(gè)字段:Date_received 是領(lǐng)取優(yōu)惠券日期,Date 是消費(fèi)日期。待會(huì)我將詳細(xì)介紹。
介紹完幾個(gè)數(shù)據(jù)文件和字段之后,我們就來編寫程序,導(dǎo)入訓(xùn)練集和測(cè)試集,同時(shí)導(dǎo)入需要用到的庫。
# import libraries necessary for this project import os, sys, pickleimport numpy as np import pandas as pdfrom datetime import datefrom sklearn.model_selection import KFold, train_test_split, StratifiedKFold, cross_val_score, GridSearchCV from sklearn.pipeline import Pipeline from sklearn.linear_model import SGDClassifier, LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import log_loss, roc_auc_score, auc, roc_curve from sklearn.preprocessing import MinMaxScaler# display for this notebook %matplotlib inline %config InlineBackend.figure_format = 'retina'導(dǎo)入數(shù)據(jù):
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)是訓(xùn)練集前 5 行顯示如下:
接下來,我們來做個(gè)簡(jiǎn)單統(tǒng)計(jì),看一看究竟用戶是否使用優(yōu)惠券消費(fèi)的情況。
print('有優(yōu)惠卷,購(gòu)買商品:%d' % dfoff[(dfoff['Date_received'] != 'null') & (dfoff['Date'] != 'null')].shape[0]) print('有優(yōu)惠卷,未購(gòu)商品:%d' % dfoff[(dfoff['Date_received'] != 'null') & (dfoff['Date'] == 'null')].shape[0]) print('無優(yōu)惠卷,購(gòu)買商品:%d' % dfoff[(dfoff['Date_received'] == 'null') & (dfoff['Date'] != 'null')].shape[0]) print('無優(yōu)惠卷,未購(gòu)商品:%d' % dfoff[(dfoff['Date_received'] == 'null') & (dfoff['Date'] == 'null')].shape[0])有優(yōu)惠卷,購(gòu)買商品:75382
有優(yōu)惠卷,未購(gòu)商品:977900
無優(yōu)惠卷,購(gòu)買商品:701602
無優(yōu)惠卷,未購(gòu)商品:0
可見,很多人(701602)購(gòu)買商品卻沒有使用優(yōu)惠券,也有很多人(977900)有優(yōu)惠券但卻沒有使用,真正使用優(yōu)惠券購(gòu)買商品的人(75382)很少!所以,這個(gè)比賽的意義就是把優(yōu)惠券送給真正可能會(huì)購(gòu)買商品的人。
特征提取
毫不夸張第說,構(gòu)建機(jī)器學(xué)習(xí)模型,特征工程可能比選擇哪種算法更加重要。接下來,我們就來研究一下哪些特征可能對(duì)模型訓(xùn)練有用。
1.打折率(Discount_rate)
首先,第一個(gè)想到的特征應(yīng)該是優(yōu)惠卷的打折率。因?yàn)楹茱@然,一般情況下優(yōu)惠得越多,用戶就越有可能使用優(yōu)惠券。那么,我們就來看一下訓(xùn)練集中優(yōu)惠卷有哪些類型。
print('Discount_rate 類型:\n',dfoff['Discount_rate'].unique())Discount_rate 類型:
[‘null’ ‘150:20’ ‘20:1’ ‘200:20’ ‘30:5’ ‘50:10’ ‘10:5’ ‘100:10’ ‘200:30’ ‘20:5’ >‘30:10’ ‘50:5’ ‘150:10’ ‘100:30’ ‘200:50’ ‘100:50’ ‘300:30’ ‘50:20’ ‘0.9’ ‘10:1’ >‘30:1’ ‘0.95’ ‘100:5’ ‘5:1’ ‘100:20’ ‘0.8’ ‘50:1’ ‘200:10’ ‘300:20’ ‘100:1’ >‘150:30’ ‘300:50’ ‘20:10’ ‘0.85’ ‘0.6’ ‘150:50’ ‘0.75’ ‘0.5’ ‘200:5’ ‘0.7’ >‘30:20’ ‘300:10’ ‘0.2’ ‘50:30’ ‘200:100’ ‘150:5’]
根據(jù)打印的結(jié)果來看,打折率分為 3 種情況:
-
‘null’ 表示沒有打折
-
[0,1] 表示折扣率
-
x:y 表示滿 x 減 y
那我們的處理方式可以構(gòu)建 4 個(gè)函數(shù),分別提取 4 種特征,分別是:
-
打折類型:getDiscountType()
-
折扣率:convertRate()
-
滿多少:getDiscountMan()
-
減多少:getDiscountJian()
函數(shù)代碼如下:
# Convert Discount_rate and Distance def getDiscountType(row):if row == 'null':return 'null'elif ':' in row:return 1else:return 0def convertRate(row):"""Convert discount to rate"""if row == 'null':return 1.0elif ':' in row:rows = row.split(':')return 1.0 - float(rows[1])/float(rows[0])else:return float(row)def getDiscountMan(row):if ':' in row:rows = row.split(':')return int(rows[0])else:return 0def getDiscountJian(row):if ':' in row:rows = row.split(':')return int(rows[1])else:return 0def processData(df):# convert discount_ratedf['discount_type'] = df['Discount_rate'].apply(getDiscountType)df['discount_rate'] = df['Discount_rate'].apply(convertRate)df['discount_man'] = df['Discount_rate'].apply(getDiscountMan)df['discount_jian'] = df['Discount_rate'].apply(getDiscountJian)print(df['discount_rate'].unique())return df然后,對(duì)訓(xùn)練集和測(cè)試集分別進(jìn)行進(jìn)行 processData()函數(shù)的處理:
dfoff = processData(dfoff) dftest = processData(dftest)處理之后,我們可以看到訓(xùn)練集和測(cè)試集都多出了 4 個(gè)新的特征:discount_type、discount_rate、discount_man、discount_jian。
2.距離(Distance)
距離字段表示用戶與商店的地理距離,顯然,距離的遠(yuǎn)近也會(huì)影響到優(yōu)惠券的使用與否。那么,我們就可以把距離也作為一個(gè)特征。首先看一下距離有哪些特征值:
print('Distance 類型:',dfoff['Distance'].unique())Distance 類型: [‘0’ ‘1’ ‘null’ ‘2’ ‘10’ ‘4’ ‘7’ ‘9’ ‘3’ ‘5’ ‘6’ ‘8’]
然后,定義提取距離特征的函數(shù):
# convert distance dfoff['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())處理之后,我們可以看到訓(xùn)練集和測(cè)試集都多出了 1 個(gè)新的特征:distance。
3.領(lǐng)劵日期(Date_received)
是還有一點(diǎn)很重要的是領(lǐng)券日期,因?yàn)橐话愣?#xff0c;周末領(lǐng)取優(yōu)惠券去消費(fèi)的可能性更大一些。因此,我們可以構(gòu)建關(guān)于領(lǐng)券日期的一些特征:
-
weekday : {null, 1, 2, 3, 4, 5, 6, 7}
-
weekday_type : {1, 0}(周六和周日為1,其他為0)
-
Weekday_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}
其中用到了獨(dú)熱編碼,讓特征更加豐富。相應(yīng)的這 9 個(gè)特征的提取函數(shù)為:
def getWeekday(row):if row == 'null':return rowelse: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,其他為0 dfoff['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 = weekdaycols dfoff[weekdaycols] = tmpdftmpdf = pd.get_dummies(dftest['weekday'].replace('null', np.nan)) tmpdf.columns = weekdaycols dftest[weekdaycols] = tmpdf這樣,我們就會(huì)在訓(xùn)練集和測(cè)試集上發(fā)現(xiàn)增加了 9 個(gè)關(guān)于領(lǐng)券日期的特征:
好了,經(jīng)過以上簡(jiǎn)單的特征提取,我們總共得到了 14 個(gè)有用的特征:
-
discount_rate
-
discount_type
-
discount_man
-
discount_jian
-
distance
-
weekday
-
weekday_type
-
weekday_1
-
weekday_2
-
weekday_3
-
weekday_4
-
weekday_5
-
weekday_6
-
weekday_7
好了,我們的主要工作已經(jīng)完成了大半!
標(biāo)注標(biāo)簽 Label
有了特征之后,我們還需要對(duì)訓(xùn)練樣本進(jìn)行 label 標(biāo)注,即確定哪些是正樣本(y = 1),哪些是負(fù)樣本(y = 0)。我們要預(yù)測(cè)的是用戶在領(lǐng)取優(yōu)惠券之后 15 之內(nèi)的消費(fèi)情況。所以,總共有三種情況:
1.Date_received == ‘null’:
表示沒有領(lǐng)到優(yōu)惠券,無需考慮,y = -1
2.(Date_received != ‘null’) & (Date != ‘null’) & (Date - Date_received <= 15):
表示領(lǐng)取優(yōu)惠券且在15天內(nèi)使用,即正樣本,y = 1
3.(Date_received != ‘null’) & ((Date == ‘null’) | (Date - Date_received > 15)):
表示領(lǐng)取優(yōu)惠券未在在15天內(nèi)使用,即負(fù)樣本,y = 0
好了,知道規(guī)則之后,我們就可以定義標(biāo)簽備注函數(shù)了。
def label(row):if row['Date_received'] == 'null':return -1if row['Date'] != 'null':td = pd.to_datetime(row['Date'], format='%Y%m%d') - pd.to_datetime(row['Date_received'], format='%Y%m%d')if td <= pd.Timedelta(15, 'D'):return 1return 0dfoff['label'] = dfoff.apply(label, axis=1)我們可以使用這個(gè)函數(shù)對(duì)訓(xùn)練集進(jìn)行標(biāo)注,看一下正負(fù)樣本究竟有多少:
print(dfoff['label'].value_counts())0 988887
-1 701602
1 64395
Name: label, dtype: int64
很清晰地,正樣本共有 64395 例,負(fù)樣本共有 988887 例。顯然,正負(fù)樣本數(shù)量差別很大。這也是為什么會(huì)使用 AUC 作為模型性能評(píng)估標(biāo)準(zhǔn)的原因。
建立模型
接下來就是最主要的建立機(jī)器學(xué)習(xí)模型了。首先確定的是我們選擇的特征是上面提取的 14 個(gè)特征,為了驗(yàn)證模型的性能,需要?jiǎng)澐烛?yàn)證集進(jìn)行模型驗(yàn)證,劃分方式是按照領(lǐng)券日期,即訓(xùn)練集:20160101-20160515,驗(yàn)證集:20160516-20160615。我們采用的模型是簡(jiǎn)單的 SGDClassifier。
1.劃分訓(xùn)練集和驗(yàn)證集
# data split df = 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.構(gòu)建模型
def check_model(data, predictors):classifier = lambda: SGDClassifier(loss='log', # loss function: logistic regressionpenalty='elasticnet', # L1 & L2fit_intercept=True, # 是否存在截距,默認(rèn)存在max_iter=100, shuffle=True, # Whether or not the training data should be shuffled after each epochn_jobs=1, # The number of processors to useclass_weight=None) # Weights associated with classes. If not given, all classes are supposed to have weight one.# 管道機(jī)制使得參數(shù)集在新數(shù)據(jù)集(比如測(cè)試集)上的重復(fù)使用,管道機(jī)制實(shí)現(xiàn)了對(duì)全部步驟的流式化封裝和管理。model = Pipeline(steps=[('ss', StandardScaler()), # transformer('en', classifier()) # estimator])parameters = {'en__alpha': [ 0.001, 0.01, 0.1],'en__l1_ratio': [ 0.001, 0.01, 0.1]}# StratifiedKFold用法類似Kfold,但是他是分層采樣,確保訓(xùn)練集,測(cè)試集中各類別樣本的比例與原始數(shù)據(jù)集中相同。folder = StratifiedKFold(n_splits=3, shuffle=True)# Exhaustive search over specified parameter values for an estimator.grid_search = GridSearchCV(model, parameters, cv=folder, n_jobs=-1, # -1 means using all processorsverbose=1)grid_search = grid_search.fit(data[predictors], data['label'])return grid_search模型采用的是 SGDClassifier,使用了 Python 中的 Pipeline 管道機(jī)制,可以使參數(shù)集在新數(shù)據(jù)集(比如測(cè)試集)上的重復(fù)使用,管道機(jī)制實(shí)現(xiàn)了對(duì)全部步驟的流式化封裝和管理。交叉驗(yàn)證采用 StratifiedKFold,其用法類似 Kfold,但是 StratifiedKFold 是分層采樣,確保訓(xùn)練集,測(cè)試集中各類別樣本的比例與原始數(shù)據(jù)集中相同。
3.訓(xùn)練
接下來就可以使用該模型對(duì)訓(xùn)練集進(jìn)行訓(xùn)練了,整個(gè)訓(xùn)練過程大概 1-2 分鐘的時(shí)間。
predictors = original_feature model = check_model(train, predictors)4.驗(yàn)證
然后對(duì)驗(yàn)證集中每個(gè)優(yōu)惠券預(yù)測(cè)的結(jié)果計(jì)算 AUC,再對(duì)所有優(yōu)惠券的 AUC 求平均。計(jì)算 AUC 的時(shí)候,如果 label 只有一類,就直接跳過,因?yàn)?AUC 無法計(jì)算。
# valid predict y_valid_pred = model.predict_proba(valid[predictors]) valid1 = valid.copy() valid1['pred_prob'] = y_valid_pred[:, 1] valid1.head(5)注意這里得到的結(jié)果 pred_prob 是概率值(預(yù)測(cè)樣本屬于正類的概率)。
最后,就可以對(duì)驗(yàn)證集計(jì)算 AUC。直接調(diào)用 sklearn 庫自帶的計(jì)算 AUC 函數(shù)即可。
# avgAUC calculation vg = valid1.groupby(['Coupon_id']) aucs = [] for i in vg:tmpdf = i[1] if len(tmpdf['label'].unique()) != 2:continuefpr, 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。
測(cè)試
訓(xùn)練完模型之后,就是使用訓(xùn)練好的模型對(duì)測(cè)試集進(jìn)行測(cè)試了。并且將測(cè)試得到的結(jié)果(概率值)按照規(guī)定的格式保存成一個(gè) .csv 文件。
# test prediction for submission y_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)值得注意的是,這里得到的結(jié)果是概率值,最終的 AUC 是提交到官網(wǎng)之后平臺(tái)計(jì)算的。因?yàn)闇y(cè)試集真正的 label 我們肯定是不知道的。
提交結(jié)果
好了,最后一步就是在比賽官網(wǎng)上提交我們的預(yù)測(cè)結(jié)果,即這里的 submit.csv 文件。提交完之后,過幾個(gè)小時(shí)就可以看到成績(jī)了。整個(gè)比賽的流程就完成了。
優(yōu)化模型
其實(shí),本文所述的整個(gè)比賽思路和算法是比較簡(jiǎn)單的,得到的結(jié)果和成績(jī)也只能算是合格,名次不會(huì)很高。我們還可以運(yùn)用各種手段優(yōu)化模型,簡(jiǎn)單來說分為以下三種:
-
特征工程
-
機(jī)器學(xué)習(xí)
-
模型融合
總結(jié)
本文的主要目的是帶領(lǐng)大家走一遍整個(gè)比賽的流程,培養(yǎng)一些比賽中特征提取和算法應(yīng)用方面的知識(shí)。這個(gè)天池比賽目前還是比較火熱的,雖然沒有獎(jiǎng)金,但是參賽人數(shù)已經(jīng)超過 1.1w 了。看完本文之后,希望大家有時(shí)間去參加感受一下機(jī)器學(xué)習(xí)比賽的氛圍,將理論應(yīng)用到實(shí)戰(zhàn)中去。
本文完整的代碼我已經(jīng)放在了 GitHub 上,有需要的請(qǐng)自行領(lǐng)取:
https://github.com/RedstoneWill/MachineLearningInAction-Camp/tree/master/Week4/o2o%20Code_Easy
同時(shí),本比賽第一名的代碼也開源了,一同放出,供大家學(xué)習(xí):
https://github.com/wepe/O2O-Coupon-Usage-Forecast
總結(jié)
以上是生活随笔為你收集整理的天池 O2O 优惠券使用预测思路解析与代码实战的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货 | 谈谈我是如何入门这场 AI 大
- 下一篇: 说两件事!