O2O 优惠券使用预测

背景:

随着移动设备的完善和普及,移动互联网+各行各业进入了高速发展的阶段,这其中以O2O(Online to Offline)消费最为吸引眼球。目前需要根据O2O场景的数据通过分析建模,精准预测用户是否会在规定时间(15天)内使用相应优惠券。
从机器学习模型的角度来说,这是一个典型的分类问题,其过程就是根据已有训练集进行训练,得到的模型再对其测试集进行测试并分类。整体流程为:
训练数据-->特征提取-->建立模型-->测试数据-->特征提取-->预测-->结果

评估方式

评估一个机器学习模型有多种方式,最常见的如准确率(Accuracy)、精确率(Precision)、召回率(Recall)。一般使用精确率和召回率结合的方式F1 score能较好地评估模型性能(特别是在正负样本不平衡的情况下)。
这里使用AUC进行评估,AUC是ROC曲线与横坐标围成的面积,使用ROC和AUC的原因在于当测试集中的正负样本的分布变化的时候,ROC曲线能够保持不变,即能够更好地处理正负样本不均的场景。

我们主要用到的是线下测试集(ccf_offline_stage1_test_revised.csv)和线下训练集(ccf_offline_stage1_train.csv);其中,线下训练集主要记录用户线下消费和优惠券领取行为,字段有:

Field Description
User_id 用户id
Merchant_id 商户id
Coupon_id 优惠券id:null表示无优惠券消费,此时Discount_rate和Date_received字段无意义
Discount_rate 优惠率:x\in [0,1]代表折扣率;x:y表示满x减y。单位是元
Distance user经常活动的地点距离该merchant的最近门店距离是x*500米(如果是连锁店,则取最近的一家门店),x\in[0,10];null表示无此信息,0表示低于500米,10表示大于5公里;
Date_received 领取优惠券日期
Date 消费日期:如果Date=null&Coupon_id!=null,该记录表示领取优惠券但没有使用,即负样本;如果Date!=null&Coupon_id=null,则表示普通消费日期;如果Date!=null&Coupin_id!=null,则表示优惠券消费日期,即正样本;

线下测试集记录的是用户O2O线下优惠券使用预测样本

Field Description
User_id 用户ID
Merchant_id 商户ID
Coupon_id 优惠券ID
Discount_rate 优惠率:x\in [0,1]代表折扣率;x:y表示满x减y。单位是元
Distance user经常活动的地点距离该merchant的最近门店距离是x*500米(如果是连锁店,则取最近的一家门店),x\in[0,10];null表示无此信息,0表示低于500米,10表示大于5公里;
Date_received 领取优惠券日期

最后我们需要根据线下测试集中的user_id,coupon_id以及date_received进行概率probability预测,即得到用户在15天内使用优惠券的概率值。

记住:Date_received是领取优惠券日期,Date是消费日期。

导入必要的库:

import os,sys,pickle
import numpy as np
import pandas as pd
from datetime import date

from 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

%matplotlib inline 
%config InlineBackend.figure_format='retina'

读取数据:

dfoff=pd.read_csv('/ccf_offline_stage1_train.csv')
dftest=pd.read_csv('/ccf_offline_stage1_test_revised.csv')
print('有优惠券,购买商品:%d'%dfoff[(~dfoff['Date_received'].isnull())&(~dfoff['Date'].isnull())].shape[0])

print('有优惠券,未购买商品:%d'%dfoff[(~dfoff['Date_received'].isnull())&(dfoff['Date'].isnull())].shape[0])
print('无优惠券,购买商品:%d'%dfoff[(dfoff['Date_received'].isnull())&(~dfoff['Date'].isnull())].shape[0])
print('无优惠券,未购买商品:%d'%dfoff[(dfoff['Date_received'].isnull())&(dfoff['Date'].isnull())].shape[0])

有优惠券,购买商品:75382
有优惠券,未购买商品:977900
无优惠券,购买商品:701602
无优惠券,未购买商品:0

可以看出,很多人(701602)购买商品却没有使用优惠券,也有很多人(977900)有优惠券但却没有使用,真正使用优惠券购买商品的人(75382)很少,所以我们的目的就是把优惠券送给真正可能会购买商品的人。

1. 特征提取

构建机器学习模型,特征工程可能比选择哪种算法更加重要

1.1. Discount_rate(打折率)

常识来讲,优惠的越多,用户就越有可能使用优惠券。

print('Discount_rate 类型:\n',dfoff['Discount_rate'].unique())

根据打印的结果来看,打折率分为三种情况,nan表示没有打折;[0,1]表示折扣率;x:y表示满x减y

我们的处理方式可以构建四个函数,分别提取4种特征,分别是:

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

首先定义满多少和减多少的函数

def getDiscountMan(row):
    if ':' in row:
        rows=row.split(':')
        return int(rows[0])
    else:
        return 0

def getDiscountJian(row):
    if ':' in row:
        rows=row.split(':')
        return int(rows[1])
    else:
        return 0

def processData(df):
    df['discount_man']=df['Discount_rate'].astype(str).apply(getDiscountMan)
    df['discount_jian']=df['Discount_rate'].astype(str).apply(getDiscountJian)
    return df

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

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

其次,定义打折类型和折扣率

先解决字段中的nan,将其转化为null

dfoff['Discount_rate']=dfoff['Discount_rate'].fillna('null')
dftest['Discount_rate']=dftest['Discount_rate'].fillna('null')
#Convert Discount_rate and Distance
def getDiscountType(row):
    if  ':' in row:
        return 1
    else:
        return 0

def convertRate(row):
    """Convert discount to rate"""
    if ':'  in row:
        if row!='null':
            rows=row.split(':')
            return 1.0-float(rows[1])/float(rows[0])
        else:
            return float(row)

def processData1(df):
    #df['Discount_rate']=df['Discount_rate'].fillna('null')
    df['discount_type']=df['Discount_rate'].apply(getDiscountType)
    df['discount_rate']=df['Discount_rate'].apply(convertRate)
    return df

dftest=processData1(dftest)
dfoff=processData1(dfoff)

1.2. 距离

距离字段表示用户与商店的地理距离,距离的远近也会营销优惠券的使用与否。

#首先看下距离的取值有哪几种
print('Distance 类型:', dfoff['Distance'].unique())

#convert distance
dfoff['distance']=dfoff['Distance'].fillna(-1).astype(int)
dftest['distance']=dftest['Distance'].fillna(-1).astype(int)

1.3. 领券日期(Date_received)

一般而言,用户在周末领取优惠券去消费的可能性更大一些,因此可以构建关于领券日期的一些特征。

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}

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

def getWeekday(row):
    if row!='null':
        return date(int(row[0:4]),int(row[4:6]),int(row[6:8])).weekday()+1
#WEEKDAYI函数的功能是返回某日期的星期数。在默认情况下,它的值为1(星期天)-7(星期六)之间的一个整数
"""
WEEKDAY(参数1,参数2)
含义:“参数1”代表要查找的那一天的日期;“参数2”为确定返回值类型的数字,当其值为2时返回的数字1代表星期一,7代表星期日。
"""
def process_Data2(df):
    df['Date_received']=df['Date_received'].fillna('null')
    df['weekday']=df['Date_received'].astype(str).apply(getWeekday)
    return df
dfoff=process_Data2(dfoff)
dftest=process_Data2(dftest)

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)]
tmpdf=pd.get_dummies(dfoff['weekday'])
#默认情况下,get_dummies()不处理缺失值

tmpdf.columns=weekdaycols
dfoff[weekdaycols]=tmpdf

#对测试集进行同样的操作
tmpdf=pd.get_dummies(dftest['weekday'])
tmpdf.columns=weekdaycols
dftest[weekdaycols]=tmpdf

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

至此,我们共新增14个特征

2. 标注标签Label

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

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

  • (Date_received!='null')&(Date!='null')&(Date-Date_received<=15) 表示领取优惠券且在15天内使用,即正样本,y=1

  • (Date_received!='null')&((Date=='null')|(Date-Date_received>15)) 表示领取优惠券未在15天内使用,即负样本,y=0

明确规则后,定义标签备注函数
如果用户领券时间为null,则标注为-1;
如果用户消费时间不为null,则判断用户消费时间减去领券时间的差值是否在15天之内,如果是,则标注为1,否则为0

def label(row):
    if row['Date_received']=='null':
        return -1
    if 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 1
        return 0

def processdf1(df):
    df['Date_received']=df['Date_received'].fillna('null')
    df['Date']=df['Date'].fillna('null')
    df['label']=df.apply(label,axis=1)
    return df

有时候会遇到定义函数使用失灵的情况,就单独拆解来处理;
分别对训练集和测试集进行处理;

dfoff['Date_received']=dfoff['Date_received'].fillna('null')
dfoff['Date']=dfoff['Date'].fillna('null')
dfoff['label']=dfoff.apply(label,axis=1)

dftest['Date_received']=dftest['Date_received'].fillna('null')
dftest['Date']=dftest['Date'].fillna('null')

dftest['label']=dftest.apply(label,axis=1) 

print(dfoff['label'].value_counts())

-1.0 701602
1.0 64395
0.0 10987
可以看到正样本共有64395例,负样本共有10987例,显然,正负样本差别很大,这也是为什么会使用AUC作为模型性能评估标准的原因。

3. 建立模型

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

3.1. 划分训练集和验证集
剔除没有领优惠券的样本后,划分训练集和测试集。

#data split
df=dfoff[dfoff['label']!=-1].copy()
#除去含有null值的字段
df=df[~np.isnan(dfoff['label'])]
df=df[~np.isnan(dfoff['Distance'])]
df=df[~np.isnan(dfoff['discount_rate'])]

train=df[(pd.to_datetime(df['Date_received'],format='%Y%m%d')<'20160516')].copy()
valid=df[(pd.to_datetime(df['Date_received'],format='%Y%m%d')>='20160516')&(pd.to_datetime(df['Date_received'],format='%Y%m%d')<='20160615')].copy()
print('Train Set:\n',train['label'].value_counts())
print('Valid Set:\n',valid['label'].value_counts())

Train Set:
1.0 41524
0.0 7852
Name: label, dtype: int64
Valid Set:
1.0 22871
0.0 3135
Name: label, dtype: int64

3.2. 构建模型

def check_model(data, predictors):
    classifier = lambda: SGDClassifier(
       loss='log',  # loss function: logistic regression
       penalty='elasticnet', # L1 & L2
       fit_intercept=True,  # 是否存在截距,默认存在
       max_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())  # estimator
   ])

    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 estimator.
    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

3.3. 训练

original_feature=['discount_rate','discount_type','discount_man','discount_jian','distance','weekday_type','weekday']+weekdaycols
predictors=original_feature
model=check_model(train,predictors)

4. 验证

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

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

注意:这里得到的结果pred_prob是概率值(预测概率属于正类的概率)
最后对验证集计算AUC。直接调用sklearn库自带的AUC函数即可。

#avgAUC calculation
vg=valid.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))
        

可以看到模型的精度一般,可以用多种手段优化模型

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

5. 测试

使用训练好的模型对测试集进行测试,得到测试的结果---概率值;;由于我们不知道真正的Label,所以无法计算AUC

#同样地,需要去掉null值字段记录
dftest2=dftest[~np.isnan(dftest['discount_rate'])]

#test prediction for submission
y_test_pred=model.predict_proba(dftest2[predictors])
dftest1=dftest2[['User_id','Coupon_id','Date_received']].copy()
dftest1['Porbability']=y_test_pred[:,1]
dftest1.head()

你可能感兴趣的:(O2O 优惠券使用预测)