【作者主页】Francek Chen
【专栏介绍】 ⌈ ⌈ ⌈机器学习与数据挖掘实战案例 ⌋ ⌋ ⌋ 机器学习是人工智能的一个分支,专注于让计算机系统通过数据学习和改进。它利用统计和计算方法,使模型能够从数据中自动提取特征并做出预测或决策。数据挖掘则是从大型数据集中发现模式、关联和异常的过程,旨在提取有价值的信息和知识。机器学习为数据挖掘提供了强大的分析工具,而数据挖掘则是机器学习应用的重要领域,两者相辅相成,共同推动数据科学的发展。本专栏介绍机器学习与数据挖掘的相关实战案例。
【GitCode】专栏资源保存在我的GitCode仓库:https://gitcode.com/Morse_Chen/ML-DM_cases。
文章目录
一、目标分析
(一)背景
在O2O消费模式运营局面下,优惠券的合理投放成为了现在商户经营店铺的一项考虑因素,某电商平台根据自身拥有的用户消费信息数据,查看用户领取优惠券后的使用情况。随着移动设备的完善和普及,移动互联网+各行各业的模式进入了高速发展阶段,其中以O2O消费模式最为吸引眼球,将线上消费和线下消费进行结合。O2O行业天然关联数亿消费者,各类APP每天记录了超过百亿条用户行为和位置记录,比如在美团点餐、滴滴打车、天猫购物或者浏览商品等行为都会被记录,因而O2O成为大数据科研和商业化运营的最佳结合点之一。
O2O消费对于用户而言,不仅可以使用户获得更为丰富、全面的商户及其服务信息内容,而且还可以使用户获得相对线下直接消费较低的价格;对于商户而言,可以获得更多、更好的宣传机会去吸引新用户到店消费,同时可以通过在线预约的方式合理安排经营节约成本。
在市场竞争十分激烈情况下,商户会想出各种各样的办法去吸引新用户,其中以优惠券盘活老用户或吸引新客户进店消费成为了O2O的一种重要营销方式,但如果投放优惠券的形式不恰当时,可能会造成一定的负面影响,例如,人们在生活中常常会收到各式各样关于优惠券或其他活动的短信或APP推送的信息,在大多数情况下,人们并不会去使用或在意这些优惠券,优惠券的随机推送并没有摸清用户的需要,此时会对多数用户造成无意义的干扰。同样的,对于商户而言,滥发的优惠券可能降低品牌声誉,同时还会增加营销成本。
个性化投放是提高优惠券核销率的重要技术,不仅可以让具有一定偏好的消费者得到真正的实惠,而且赋予商户更强的营销能力。
(二)数据说明
某平台拥有用户线下的真实消费行为和位置信息等数据,为保护用户隐私和数据安全,数据已经过随机采样和脱敏处理。数据样本包括训练样本和测试样本。其中,训练样本共有1444037条记录,是用户在2016年1月1日至2016年5月30日之间的真实线下消费行为信息。测试样本为用户在2016年6月1日至15日的领取商户优惠券信息。
总的数据属性包含用户ID、商品ID、优惠券ID、优惠券折扣力度、用户距门店距离、领取优惠券日期、消费日期7个属性。
名称 | 含义 |
---|---|
user_id | 用户ID |
merchant_id | 商户ID |
coupon_id | 优惠券ID。null表示无优惠券消费,此时discount_rate和date_received字段无意义 |
discount_rate | 优惠券折扣力度。其中x范围是[0,1]代表折扣率;x:y表示满x减y |
distance | 用户距门店距离(如果是连锁店,那么取最近的一家门店)。表示用户经常活动的地点离该商户的最近门店距离是x×500m,x的范围是(0,10)且x取整数;例如,当x=1时,用户活动地点离最近门店的距离为500m;x=2时,距离为1000m,以此类推。此外,null表示无此信息,0表示距离低于500m,10表示距离大于等于5km |
date_received | 领取优惠券日期 |
date | 消费日期。如果消费日期为null但优惠券ID不为null,该记录表示领取优惠券但没有使用;如果消费日期不为null但优惠券ID为null,则表示普通消费日期;如果消费日期和优惠券ID都不为null,则表示用优惠券消费日期 |
(三)分析目标
本案例的主要目标是预测用户在领取优惠券后15天以内的使用情况,为了将该问题转化为二分类问题,将领取优惠券后15天以内使用的样本标记为正样本,记为1;15天以内没有使用的样本标记为负样本,记为0;未领取优惠券进行消费的样本为普通样本,记为-1。确定是分类问题后,结合用户使用优惠券的情景和实际业务场景,需要构建用户、商户、优惠券、用户和商户交互的相关指标,并根据这些指标构建分类模型,预测用户在领取优惠券15天以内的使用情况。
根据上述的分析过程与思路,结合数据特点和分析目标,主要包括以下步骤。
- 读取用户真实线下消费行为历史数据。
- 对读取的数据进行数据探索性分析与预处理,包括数据缺失值与异常值的处理、数据清洗、指标构建等操作。
- 使用决策树分类模型、梯度提升分类模型和XGBoost分类模型进行分类预测,并对构建好的模型进行模型评价。
O2O优惠券使用预测总体流程如图所示。
二、数据探索
(一)描述性统计分析
原始数据集中包括用户ID、商户ID、优惠券ID、优惠券折扣力度、用户经常活动的地点与商户最近的门店距离等信息,需要对原始数据进行描述性统计分析、从多个维度进行探索性分析。对训练样本、测试样本进行描述性统计分析,分别得到训练样本和测试样本的属性观测值中的空值数、最大值和最小值。
import pandas as pd
# 读取训练样本和测试样本
data_train = pd.read_csv('../data/train.csv')
data_test = pd.read_csv('../data/test.csv')
# 对数据进行描述性统计分析
# 返回缺失值个数、最大值、最小值
# 训练样本的描述性统计分析
# 在describe函数中,percentiles参数表示指定计算的分位数表,如1/4分位数、中位数等
explore_train = data_train.describe(percentiles=[], include='all').T
explore_train['null'] = data_train.isnull().sum() # 计算缺失值
explore_train = explore_train[['null', 'max', 'min']]
explore_train.columns = ['空值数', '最大值', '最小值'] # 表头重命名
# 测试样本的描述性统计分析
explore_test = data_test.describe(percentiles=[], include='all').T
explore_test['null'] = data_test.isnull().sum() # 统计缺失值
explore_test = explore_test[['null', 'max', 'min']]
explore_test.columns = ['空值数', '最大值', '最小值'] # 表头重命名
# 写出结果
explore_train.to_csv('../tmp/explore_train.csv') # 训练样本的描述性统计分析
print('训练样本的描述性统计分析:\n', explore_train)
print()
explore_test.to_csv('../tmp/explore_test.csv') # 测试样本的描述性统计分析
print('测试样本的描述性统计分析:\n', explore_test)
# 合并训练数据和测试数据
data1 = pd.concat([data_train, data_test], axis=0)
# 处理data_received属性、date属性
data1['date_received'] = data1['date_received'].astype('str').apply(lambda x: x.split('.')[0])
data1['date_received'] = pd.to_datetime(data1['date_received'])
data1['date'] = data1[ 'date'].astype('str').apply(lambda x: x.split('.')[0])
data1['date'] = pd.to_datetime(data1['date'])
在上面两个表中,discount_rate
属性为字符型,所以不存在最大值和最小值。
训练样本的优惠券ID、优惠率、领取优惠券日期空值的记录数一致,可能是因为一部分用户没有领取优惠券而直接到门店消费;而date属性的空值数比优惠券ID的空值数多,即存在一部分用户的消费日期为空而优惠券ID不为空,这可能是因为这部分用户是领取优惠券但没有进行消费;测试样本的优惠券ID、优惠率、领取优惠券日期和消费日期均存在空值。
(二)分析优惠形式信息
由于原始训练数据中的discount_rate
字段部分是以小数形式(如0.8、0.9等)存在的,表示折扣率,部分是以比值形式(如30:5、100:10等)存在的,表示满额减免,分别分析这两种优惠形式的分布情况。得到的结果如图所示。
# 绘制图形分析满减优惠和形式和折扣率优惠形式
import matplotlib.pyplot as plt
import re
indexOne = data1['discount_rate'].astype(str).apply(lambda x: re.findall('\d+:\d+', x) != []) # 满减优惠形式的索引
indexTwo = data1['discount_rate'].astype(str).apply(lambda x: re.findall('\d+\.\d+', x) != []) # 折扣率优惠形式的索引
dfOne = data1.loc[indexOne, :] # 取出满减优惠形式的数据
dfTwo = data1.loc[indexTwo, :] # 取出折扣率优惠形式的数据
# 在满减优惠形式的数据中,15天内优惠券被使用的数目
numberOne = sum((dfOne['date'] - dfOne['date_received']).dt.days <= 15)
# 在满减优惠形式的数据中,15天内优惠券未被使用的数目
numberTwo = len(dfOne) - numberOne
# 在折扣率优惠形式的数据中,15天内优惠券被使用的数目
numberThree = sum((dfTwo['date'] - dfTwo['date_received']).dt.days <= 15)
# 在折扣率优惠形式的数据中,15天内优惠券未被使用的数目
numberFour = len(dfTwo) - numberThree
# 绘制图形
plt.figure(figsize=(6, 3))
plt.rcParams['font.sans-serif'] = 'Simhei'
plt.subplot(1, 2, 1)
plt.pie([numberOne, numberTwo], autopct='%.1f%%', pctdistance=1.4)
plt.legend(['优惠券15天内被使用', '优惠券15天内未被使用'], fontsize=7, loc=(0.15, 0.91)) # 添加图例
plt.title('满减优惠形式', fontsize=15, y=1.05) # 添加标题
plt.subplot(1, 2, 2)
plt.pie([numberThree, numberFour], autopct='%.1f%%', pctdistance=1.4)
plt.legend(['优惠券15天内被使用', '优惠券15天内未被使用'], fontsize=7, loc=(0.15, 0.91)) # 添加图例
plt.title('折扣率优惠形式', fontsize=15, y=1.05) # 添加标题
plt.savefig('../img/分析优惠形式信息图.jpg', dpi=2080)
plt.show()
由图可知,满减优惠和折扣率优惠形式的优惠券在15内未被使用的比例相对较大,分别为94.1%、89%,满减优惠形式的优惠券在15天内被使用的比例仅为5.9%,折扣率优惠形式的优惠券被使用的比例为11%,说明大多数用户没有使用优惠券到店进行消费。
(三)分析用户消费行为信息
1. 绘制折线图分析用户消费次数
选取领取优惠券日期、消费日期这2个属性计算用户消费次数、领劵数和领券消费数,分析用户的消费行为信息。统计各月份的用户消费次数,并绘制2016年各月份用户消费次数折线图。
# 提取月份
data_month = data1['date'].apply(lambda x: x.month)
# 对各月份用户消费次数进行统计
data_count = data_month.value_counts().sort_index(ascending=True)
# 绘制用户消费次数折线图
fig = plt.figure(figsize=(8, 5)) # 设置画布大小
plt.rcParams['font.sans-serif'] = 'SimHei' # 设置中文显示
plt.rcParams['axes.unicode_minus'] = False
plt.rc('font', size=12)
plt.plot(data_count.index, data_count, color='#0504aa', linewidth=3.0, linestyle='-.')
plt.xlabel('月份')
plt.ylabel('消费次数')
plt.title('2016年各月用户消费次数')
plt.show()
由图可知,5月份用户消费次数最多,有可能是五一节假日商户投放优惠券的优惠率较多吸引用户消费。2月份处于低谷,可能是春节长假店铺休息导致。
2. 绘制柱形图分析用户领券数与领券消费数
绘制2016年各月份用户领优惠券次数和领券消费次数柱形图,如图所示。
# 提取领券日期的月份
received_month = data1['date_received'].apply(lambda x: x.month)
month_count = received_month.value_counts().sort_index(ascending=True)
# 获取领券消费数据
cop_distance = data1.loc[data1['date'].notnull()&data1['coupon_id'].notnull(),['user_id', 'distance', 'date', 'discount_rate']]
# 统计领券消费次数
date_month = cop_distance['date'].apply(lambda x: x.month)
datemonth_count = date_month.value_counts().sort_index(ascending=True)
datemonth_countlist = list(datemonth_count) # 转为列表
# 绘制用户领券次数与领券消费次数的柱形图
import numpy as np
fig = plt.figure(figsize=(8, 5)) # 设置画布大小
name_list = [i for i in range(1, 7)]; x = [i for i in range(1, 7)]
width = 0.4 # width设置宽度大小
plt.bar(x, height=list(month_count), width=width, label='用户领券', alpha=1, color='#0504aa')
for i in range(len(x)):
x[i] = x[i] + width
plt.bar(x, height=np.array(datemonth_countlist), width=width, label='用户领券消费', alpha=0.4, color='red')
plt.legend() # 图例
plt.xlabel('月份')
plt.ylabel('次数')
plt.title('2016年各月用户领券次数与领券消费次数')
plt.show()
由图可知,1月份用户领取优惠券的次数达到最高峰,可能是用户领取优惠券为春节囤年货做准备,其次是5月份用户领取优惠券数量,可能是为母亲节给母亲送礼物做准备。从用户领券消费情况看,虽然商户投放优惠券很多,但相对于投放的优惠券数量,用户很少使用优惠券到商户进行消费,说明出现了商户滥发优惠券现象。
(四)分析商户投放优惠券信息
1. 绘制柱形图分析商户投放优惠券数量
统计商户投放优惠券数量、用户到门店消费的距离、用户持券与未持券到门店消费的距离等,用于分析商户投放优惠券信息。平台有众多家商户参与优惠券投放,绘制投放优惠券数量排名前10的商户ID柱形图。
# 提取商户投放优惠券数据
coupon_data = data1.loc[data1['coupon_id'].notnull(), ['merchant_id', 'coupon_id']]
merchant_count = coupon_data['merchant_id'].value_counts()
print('参与投放优惠券商户总数为:', merchant_count.shape[0])
print('商户最多投放优惠券{max_count}张\n商户最少投放优惠券{min_count}张'.format(max_count=merchant_count.max(), min_count=merchant_count.min()))
# 绘制柱形图分析商家投放数量
fig = plt.figure(figsize=(8, 5)) # 设置画布大小
plt.rc('font', size=12)
plt.bar(x=range(len(merchant_count[:10])), height=merchant_count[:10], width=0.5, alpha=0.8, color='#0504aa')
# 给柱形图添加数据标注
for x, y in enumerate(merchant_count[:10]):
plt.text(x-0.4, y+500, "%s" %y)
plt.xticks(range(len(merchant_count[:10])), merchant_count[:10].index)
plt.xlabel('商户ID')
plt.ylabel('投放优惠券数量')
plt.title('投放优惠券数量前10名的商户ID')
plt.show()
由图可知,平台有众多家商户参与优惠券投放,绘制投放优惠券数量排名前10的商户ID柱形图。ID为3381的商户投放数量高达117818张,其次是ID为760和450的商户,投放数量分别为70977张、70884张,其他商户投放优惠券数量都相对较低。
2. 绘制饼图分析用户到门店消费距离
绘制饼图分析用户到门店消费的距离。
# 提取用户消费次数数据
date_distance = data1.loc[data1['date'].notnull() & data1['distance'].notnull(), ['user_id', 'distance', 'date']]
print('数据形状:', date_distance.shape)
# 统计用户消费次数
dis_count = date_distance['distance'].value_counts()
# 绘制用户到门店消费的距离比例饼图
fig = plt.figure(figsize=(10, 10)) # 设置画布大小
plt.rcParams['font.sans-serif'] = 'SimHei' # 设置中文显示
plt.rcParams['axes.unicode_minus'] = False
plt.rc('font', size=15)
plt.pie(x=dis_count, labels=dis_count.index, labeldistance=1.2,pctdistance=1.4, autopct='%1.1f%%')
plt.title('用户到门店消费的距离比例', fontdict={'weight': 'normal','size': 25})
plt.show()
由图可知,大部分用户更偏向近距离消费,其中用户到门店消费距离不足500m的用户占到所有用户中的68.3%,但出现4.7%的用户却选择到距离大于等于5km的门店进行消费,可以看出这部分用户对该品牌门店的消费依赖性。
绘制饼图分别分析用户持券与没持券到门店消费的距离。
# 提取用户领券到店铺消费距离数据
cop_distance = data1.loc[data1['date'].notnull() & data1['distance'].notnull()&data1['coupon_id'].notnull(),
['user_id', 'distance', 'date', 'discount_rate']]
print('数据形状:', cop_distance.shape)
cop_count = cop_distance['distance'].value_counts()
# 提取用户未用券到店铺消费距离数据
nocop_distance = data1.loc[data1['date'].notnull() & data1['distance'].notnull()&data1['coupon_id'].isnull(),
['user_id', 'distance', 'date', 'discount_rate']]
print('数据形状:', nocop_distance.shape)
nocop_count = nocop_distance['distance'].value_counts()
# 绘制用户持券到门店消费的距离比例饼图比例饼图
plt.figure(figsize=(12, 6)) # 设置画布大小
plt.subplot(1, 2, 1) # 子图
plt.rcParams['font.sans-serif'] = 'SimHei' # 设置中文显示
plt.rcParams['axes.unicode_minus'] = False
plt.pie(x=cop_count, labels=cop_count.index, pctdistance=1.4, labeldistance=1.2, textprops=dict(fontsize=8), autopct='%1.1f%%')
plt.title('用户持券到门店消费的距离比例', fontdict={'weight': 'normal','size': 17})
# 绘制用户未持券直接到门店消费的距离比例饼图
plt.subplot(1, 2, 2)
plt.pie(x=nocop_count, labels=nocop_count.index, pctdistance=1.4, labeldistance=1.2,textprops=dict(fontsize=8), autopct='%1.1f%%')
plt.title('用户没用券直接到门店消费的距离比例', fontdict={'weight':'normal','size': 17})
plt.savefig('../img/分析用户持券与未持券到门店消费的距离比例饼图.jpg', dpi=720)
plt.show()
由图可知,两个饼图的分布情况类似,无论是否持券消费,大部分用户都偏向去近距离的门店消费。而只有少部分用户愿意选择去5km外的门店进行消费,说明这些用户对门店有一定的依赖性。
三、数据预处理
(一)数据清洗
对原始数据进行探索性分析时,发现数据存在缺失值、部分属性的数据类型不统一、数据的属性过少等问题,需要对数据进行清洗和变换。
通过对原始数据观察发现数据中存在3种数据缺失的情况:第一种是优惠券ID为null,优惠率也为null,但有消费日期,这类用户属于没有领优惠券进行消费的普通消费者;第二种是用户消费记录中同时存在优惠券ID、优惠券率、消费日期,这类用户属于领取优惠券的消费者;第三种是用户虽然领取了商户的优惠券,但没有消费日期,可能是商户与用户的距离比较远而没使用优惠券进行消费。
优惠券的优惠率存在两种形式:一种为折扣率形式的样本,如0.8;另一种是满减优惠形式的样本,如300:30。如果该属性没有进行统一处理,可能会导致结果不准确,因此使用统一样本形式,这里的处理方法是将满减优惠统一替换成折扣率。
数据清洗具体处理方法如下:
- 将
date_rececived
和date
属性的数据类型转为时间类型。 Discount_rate
属性中的满减优惠统一替换成折扣率,例如,满减优惠形式“300:30”或“300:30:00”改为折扣率形式“0.9”。
训练数据train.csv
处理如下:
import pandas as pd
import numpy as np
# 读取训练样本
data = pd.read_csv('../data/train.csv')
# 处理data_received属性并转为时间类型
data['date_received'] = data['date_received'].astype('str').apply(lambda x: x.split('.')[0])
data['date_received'] = pd.to_datetime(data['date_received'])
# 处理date属性并转为时间类型
data['date'] = data[ 'date'].astype('str').apply(lambda x: x.split('.')[0])
data['date'] = pd.to_datetime(data['date'])
# 自定义discount函数处理优惠率属性
data['discount_rate'] = data['discount_rate'].fillna('null')
def discount(x):
if ':' in x :
split = x.split(':')
discount_rate = (int(split[0]) - int(split[1])) / int(split[0])
return round(discount_rate, 2)
elif x == 'null':
return np.nan
else :
return float(x)
# 调用discount函数将满减优惠改写成折扣率形式
data['discount_rate'] = data['discount_rate'].map(discount)
测试数据test.csv
处理如下:
import pandas as pd
import numpy as np
# 读取测试样本
data = pd.read_csv('../data/test.csv')
# 处理data_received属性并转为时间类型
data['date_received'] = data['date_received'].astype('str').apply(lambda x:x.split('.')[0])
data['date_received'] = pd.to_datetime(data['date_received'])
# 处理date属性并转为时间类型
data['date'] = data[ 'date'].astype('str').apply(lambda x:x.split('.')[0])
data['date'] = pd.to_datetime(data['date'])
# 自定义discount函数处理优惠率属性
data['discount_rate'] = data['discount_rate'].fillna('null')
def discount(x):
if ':' in x :
split = x.split(':')
discount_rate = (int(split[0])-int(split[1]))/int(split[0])
return round(discount_rate, 2)
elif x == 'null':
return np.nan
else :
return float(x)
# 调用discount函数将满减优惠改写成折扣率形式
data['discount_rate'] = data['discount_rate'].map(discount)
(二)数据变换
1. 构建用户标签和剔除未领券进行消费的样本
经过对原始数据进行观察,发现数据中的属性个数较少,不足以精确地分析问题,需要从更多维度上构造出新的属性。本案例主要分析目标是预测用户在领取优惠券后15天以内的使用情况,以15天为阈值划分样本。
类别 | 标签 | 样本 |
---|---|---|
普通样本 | -1 | 没有领取优惠券进行消费的样本 |
正样本 | 1 | 领取优惠券并在15天内使用的样本 |
负样本 | 0 | 领取优惠券在15天后使用的样本或领取优惠券但并未使用的样本 |
2. 构建指标
由于原始数据中仅有6个属性,不足以精确地描述问题,需要进行数据变换,从而构造出新的、更加有效的属性。用户是否会使用优惠券可能会受优惠券的折扣力度、商户知名度、商户与用户的距离或用户自身消费习惯等因素的影响,一般优惠券的折扣力度越大,用户使用优惠券的可能性也越大;较高知名度商户投放的优惠券,用户使用优惠券的可能性也越大;同时,若用户对某商户较为熟悉,使用该商户投放的优惠券的可能性也越大。
所以,结合O2O消费模式的特点,可以从用户、商户、优惠券以及用户和商户交互4个维度进行深入分析,将指标扩展为与用户、商户、优惠券、交互关系相关的指标。
构建的指标名称及其说明如表所示。
类别 | 指标名称 | 说明 |
---|---|---|
用户 | 优惠券使用频数 | 用户使用优惠券消费次数 |
消费频数 | 用户总消费次数 | |
领取优惠券率 | 用户使用优惠券消费次数与总消费次数的比值 | |
领取优惠券未使用率 | 用户领取优惠券而未使用的数量 | |
领取、使用优惠券间隔 | 用户使用优惠券日期平均与领取日期相隔天数 | |
商户 | 优惠券核销频数 | 商户发放的优惠券被使用的数量 |
优惠券核销率 | 商户发放的优惠券被使用的占比 | |
投放优惠券频数 | 商户发放优惠券的数量 | |
优惠券未核销频数 | 商户发放优惠券而未被使用的数量 | |
投放、使用优惠券间隔 | 商户发放的优惠券与被使用平均相隔天数 | |
位置 | 距离 | distance字段 |
优惠券 | 折扣率 | coupon_discount字段 |
优惠券流行度 | 被使用优惠券与发放优惠券总数的比值 |
自定义特征包feature_name.py
代码如下:
import pandas as pd
def feature_name(train_quality=None):
# 用户特征
# 用户使用优惠券消费次数
data_user = pd.DataFrame()
data_user['user_use_coupon_times'] = ((train_quality.loc[:, 'date_received'].notnull())&(train_quality.loc[:, 'date'].notnull())).groupby(train_quality['user_id']).sum()
# 用户总消费次数
data_user['user_consume_times'] = (train_quality.loc[:,'date'].notnull()).groupby(train_quality['user_id']).sum()
# 用户使用优惠券消费次数与总消费次数的比值
data_user['user_use_coupon_rate'] = data_user['user_use_coupon_times']/data_user['user_consume_times']
data_user['user_use_coupon_rate'].fillna(0, inplace=True)
# 用户领取优惠券而未使用的数量
data_user['user_receive_coupon_unused_times'] = ((train_quality.loc[:, 'coupon_id'].notnull())&(train_quality.loc[:, 'date'].isnull())).groupby(train_quality['user_id']).sum()
# 用户使用优惠券的日期平均与领取日期相隔多少天
data_user['user_mean_use_coupon_interval'] = (train_quality['date']-train_quality['date_received']).dt.days.groupby(train_quality['user_id']).mean()
data_user['user_mean_use_coupon_interval'] = data_user['user_mean_use_coupon_interval'].fillna(data_user['user_mean_use_coupon_interval'].max() + 1)
# 用户领取的所有优惠券的数量
data_user['number_received_coupon']=train_quality.groupby('user_id').agg({'coupon_id': lambda x: sum(x.notnull())})
# 商户特征
# 商户投放的优惠券被使用的数量
data_merchant = pd.DataFrame()
data_merchant['merchant_launch_coupon_used_count'] = (train_quality[['date_received','date']].count(axis=1) == 2).groupby(train_quality["merchant_id"]).sum()
# 商户发放的优惠券被使用数与商户总消费次数的比值
merchant_consume_times = (train_quality.loc[:, 'date':'date'].count(axis=1)).groupby(train_quality['merchant_id']).sum()
data_merchant['merchant_launch_coupon_used_rate'] = data_merchant['merchant_launch_coupon_used_count']/merchant_consume_times
data_merchant['merchant_launch_coupon_used_rate'].fillna(0, inplace=True)
# 商户投放优惠券的数量
data_merchant['merchant_launch_coupon_count'] = (train_quality.loc[:, 'coupon_id':'coupon_id'].count(axis=1)).groupby(train_quality["merchant_id"]).sum()
# 商户投放优惠券而未被使用的数量
data_merchant['merchant_receive_coupon_unused_times'] = ((train_quality.loc[:, 'coupon_id'].notnull()) & (train_quality.loc[:, 'date'].isnull())).groupby(train_quality['merchant_id']).sum()
# 商户投放的优惠券平均相隔多少天会被使用
data_merchant['merchant_mean_launch_coupon_interval'] = (train_quality['date']-train_quality['date_received']).dt.days.groupby(train_quality['merchant_id']).mean()
data_merchant['merchant_mean_launch_coupon_interval'].fillna(data_merchant['merchant_mean_launch_coupon_interval'].max() + 1, inplace=True)
# 优惠券
# 优惠券流行度=被使用优惠券/发放优惠券总数
data_coupon = pd.DataFrame()
coupon_consume_times=(train_quality.loc[:, 'coupon_id':'coupon_id'].count(axis=1)).groupby(train_quality['merchant_id']).sum()
coupon_used_count = (train_quality.loc[:, 'date':'date'].count(axis=1)).groupby(train_quality['merchant_id']).sum()
data_coupon['coupon_used_rate'] = coupon_used_count/coupon_consume_times
data_coupon['coupon_used_rate'].fillna(0, inplace=True)
# 交互指标
# 用户在某商家的消费次数
user_merchant_cus = train_quality.groupby(['user_id', 'merchant_id']).agg({'date': lambda x: sum(x.notnull())})
user_merchant_cus.columns = ['user_merchant_cus']
# 用户领取某商户的优惠券数
user_merchant_received_coupon = train_quality.groupby(['user_id', 'merchant_id']).agg({'coupon_id': lambda x: sum(x.notnull())})
user_merchant_received_coupon.columns = ['user_merchant_received_coupon']
# 用户在领取的某商家优惠券中使用过的优惠券数
user_merchant_used_coupon = ((train_quality.loc[:, 'date_received'].notnull()) & (train_quality.loc[:, 'date'].notnull())).groupby([train_quality['user_id'],train_quality['merchant_id']]).sum()
user_merchant_used_coupon=pd.DataFrame(user_merchant_used_coupon)
user_merchant_used_coupon.columns = ['user_merchant_used_coupon']
offline_train = pd.merge(user_merchant_cus, user_merchant_received_coupon, left_on=['user_id', 'merchant_id'], right_index=True, how='left')
data_mutual = pd.merge(offline_train, user_merchant_used_coupon, left_on=['user_id', 'merchant_id'], right_index=True, how='left')
return data_user, data_merchant, data_coupon, data_mutual
训练数据train.csv
处理如下:
# 标记样本
# 建立训练样本分类标签
# 创建label列值为0,假设所有样本均为负样本(未使用优惠券或领取优惠券15天内未使用),记为0
data["label"] = 0
# 优惠券ID为空(即未领取优惠券进行消费)的样本为普通样本,记为-1
data.loc[data['coupon_id'].isnull(), 'label'] = -1
# 领取优惠券在15天内使用的样本为正样本,记为1
data.loc[(data['date'] - data['date_received']).dt.days <= 15, 'label'] = 1
# 构建指标
quality = data.copy()
# 导入自定义特征包feature_name构建用户、商户、优惠券、交互相关指标
from feature_name import feature_name
data_user, data_merchant, data_coupon, data_mutual = feature_name(train_quality=quality)
# 对构建后的用户、商户、优惠券、交互相关指标进行数据拼接
# 对样本与指标类型表进行拼接
merge = pd.merge(data_user, quality, on='user_id')
merge = pd.merge(merge, data_merchant, on="merchant_id")
merge = pd.merge(merge, data_coupon, on='merchant_id')
clean_train = pd.merge(merge, data_mutual, on=['user_id', 'merchant_id'])
clean_train.isnull().sum() # 统计缺失值
clean_train.fillna(0) # 缺失值填充
print('构建指标后训练样本的形状:', clean_train.shape[0])
# 写出数据
clean_train.to_csv('../tmp/clean_train.csv', index=False)
测试数据test.csv
处理如下:
# 标记样本
# 建立训练样本分类标签,-1代表普通样本,1代表正样本, 0代表负样本
data["label"] = 0 # 创建一个列值为0,
data.loc[data['coupon_id'].isnull(), 'label']=-1 #普通样本-1
data.loc[(data['date']-data['date_received']).dt.days<=15, 'label']=1 # 正样本1
# 构建指标
quality = data.copy()
# 导入自定义特征包feature_name构建用户、商户、优惠券、交互相关指标
from feature_name import feature_name
data_user, data_merchant, data_coupon, data_mutual = feature_name(train_quality=quality)
# 对构建后的用户、商户、优惠券、交互相关指标进行数据拼接
# 对样本与指标类型表进行拼接
merge = pd.merge(data_user, quality, on='user_id')
merge = pd.merge(merge, data_merchant, on="merchant_id")
merge = pd.merge(merge, data_coupon, on='merchant_id')
clean_test = pd.merge(merge, data_mutual, on=['user_id','merchant_id'])
clean_test.isnull().sum() # 统计缺失值
clean_test.fillna(0) # 缺失值填充
print('构建指标后测试样本的形状:', clean_test.shape[0])
# 写出数据
clean_test.to_csv('../tmp/clean_test.csv', index=False)
四、分析与建模
预测用户领券后的使用情况是一个分类问题,对于分类模型的建立和预测,可采用朴素贝叶斯、决策树、SVM、逻辑回归、神经网络、深度学习等分类算法。预测用户在2016年6月领取优惠券后15天以内的使用情况,本案例主要采用决策树分类算法、梯度提升分类算法和XGBoost分类算法3种分类算法。
import pandas as pd
import numpy as np
# 读取数据
train = pd.read_csv('../tmp/clean_train.csv')
test = pd.read_csv('../tmp/clean_test.csv')
# 抽取正负样本
train = train[train['label'] == 1].sample(sum(train['label'] == 1)).append(train[train['label'] == 0].sample(sum(train['label'] == 0)))
test = test[test['label'] == 1].sample(sum(test['label'] == 1)).append(test[test['label'] == 0].sample(sum(test['label'] == 0)))
# 删除列
x_train = train.drop(['user_id', 'merchant_id', 'coupon_id', 'date_received', 'date'], axis=1)
x_test = test.drop(['user_id', 'merchant_id', 'coupon_id', 'date_received', 'date', 'label'], axis=1)
# 处理无穷数据(无穷数据大或者无穷数据小)
x_train[np.isinf(x_train)] = 0
x_test[np.isinf(x_test)] = 0
(一)决策树分类模型
一般的决策树算法都是采用自顶向下递归的方式,从训练集和与训练集相关联的类标号开始构造决策树。随着树的构建,训练集递归地划分成较小的子集。算法的重点是确定分裂准则,分裂准则通过将训练集划分成个体类的“最好”方法,确定在节点上根据哪个属性的哪个分裂点来划分训练集。
采用scikit-learn
库的决策树分类器DecisionTreeClassifier
,该分类器基于CART决策树进行优化,选择基尼指数(Gini index)最小的作为节点特征,CART决策树是二叉树,即一个节点只分两支。
由于本案例是对用户领取优惠券的使用预测,未领取优惠券进行消费的样本不满足分析要求,所以抽取正、负样本进行模型构建与分析。对训练样本建立基于CART的决策树分类模型,并进行预测。
# 决策树分类模型
from sklearn.tree import DecisionTreeClassifier
model_dt1 = DecisionTreeClassifier(max_leaf_nodes=16, random_state=123).fit(x_train.drop(['label'], axis=1), x_train['label'])
# 模型预测
pre_dt = model_dt1.predict(x_test)
# dt_class存放决策树分类预测结果
dt_class = test[['user_id', 'merchant_id', 'coupon_id']]
dt_class['class'] = pre_dt
# 写出决策树分类预测结果
dt_class.to_csv('../tmp/dt_class.csv', index=False)
决策树分类模型得到测试样本的部分预测结果如表所示。
user_id | merchant_id | coupon_id | class |
---|---|---|---|
921158 | 760 | 2418 | 1 |
2386694 | 5152 | 9545 | 1 |
3371615 | 7903 | 8137 | 1 |
4126937 | 2714 | 13408 | 1 |
2013596 | 7903 | 8137 | 1 |
7181491 | 7422 | 4727 | 0 |
1094267 | 8022 | 8461 | 0 |
1000086 | 760 | 2418 | 1 |
2084031 | 760 | 2418 | 1 |
1423399 | 760 | 2418 | 1 |
6844106 | 96 | 1967 | 0 |
5209508 | 2645 | 4745 | 1 |
3258024 | 3403 | 11214 | 0 |
6897338 | 2436 | 12462 | 1 |
(二)梯度提升分类模型
# 梯度提升分类模型
from sklearn.ensemble import GradientBoostingClassifier
# 构建模型
model = GradientBoostingClassifier(n_estimators=100, max_depth=5)
# 模型训练
model.fit(x_train.drop(['label'], axis=1), x_train['label'])
# 模型预测
pre_gb = model.predict(x_test)
# gb_class存放梯度提升预测结果
gb_class = test[['user_id', 'merchant_id', 'coupon_id']]
gb_class['class'] = pre_gb
# 写出梯度提升预测结果
gb_class.to_csv('../tmp/gb_class.csv', index=False)
使用训练样本构建梯度提升分类模型并对测试样本进行预测,得到的部分预测结果如表所示。
user_id | merchant_id | coupon_id | class |
---|---|---|---|
921158 | 760 | 2418 | 1 |
2386694 | 5152 | 9545 | 1 |
3371615 | 7903 | 8137 | 1 |
4126937 | 2714 | 13408 | 1 |
2013596 | 7903 | 8137 | 1 |
7181491 | 7422 | 4727 | 0 |
1094267 | 8022 | 8461 | 0 |
1000086 | 760 | 2418 | 1 |
2084031 | 760 | 2418 | 1 |
1423399 | 760 | 2418 | 1 |
6844106 | 96 | 1967 | 0 |
5209508 | 2645 | 4745 | 1 |
3258024 | 3403 | 11214 | 1 |
6897338 | 2436 | 12462 | 1 |
(三)XGBoost分类模型
XGBoost算法是集成学习中的序列化方法,该算法的目标函数是正则项,误差函数为二阶泰勒展开。由于XGBoost算法的目标函数中加入了正则项,能用于控制模型的复杂度,因此XGBoost算法训练出的模型不容易过拟合。
使用xgboost
库下的分类子库(xgb.XGBClassifier
)实现XGBoost算法,使用训练样本构建XGBoost分类模型并预测测试样本的结果。
#!pip install xgboost
# XGBoost分类模型
import xgboost as xgb
model_test = xgb.XGBClassifier(max_depth=6, learning_rate=0.1, n_estimators=150, silent=True, objective='binary:logistic')
# max_depth是数的最大深度,默认值为6,避免过拟合
# learning_rate为学习率,n_estimators为总共迭代的次数,即决策树的个数,silent为中间过程
# binary:logistic 二分类的逻辑回归,返回预测的概率(不是类别)
# 模型训练
model_test.fit(x_train.drop(['label'], axis=1), x_train['label'])
# 模型预测
y_pred = model_test.predict(x_test)
# xgb_class存放xgboost预测结果
xgb_class = test[['user_id', 'merchant_id', 'coupon_id']]
xgb_class['class'] = y_pred
# 写出xgboost预测结果
xgb_class.to_csv('../tmp/xgb_class.csv', index=False)
使用训练样本构建梯度提升分类模型并对测试样本进行预测,得到模型预测的部分结果如表所示。
user_id | merchant_id | coupon_id | class |
---|---|---|---|
921158 | 760 | 2418 | 1 |
2386694 | 5152 | 9545 | 1 |
3371615 | 7903 | 8137 | 1 |
4126937 | 2714 | 13408 | 1 |
2013596 | 7903 | 8137 | 1 |
7181491 | 7422 | 4727 | 0 |
1094267 | 8022 | 8461 | 0 |
1000086 | 760 | 2418 | 1 |
2084031 | 760 | 2418 | 1 |
1423399 | 760 | 2418 | 1 |
6844106 | 96 | 1967 | 0 |
5209508 | 2645 | 4745 | 1 |
3258024 | 3403 | 11214 | 1 |
6897338 | 2436 | 12462 | 1 |
将决策树分类模型、梯度提升分类模型和XGBoost分类模型的分类预测结果进行对比,可看出部分测试样本的类别预测结果有所不同,需要对模型进一步评价。
五、模型评价
常用的评价分类模型的指标有准确率、精确率、召回率、F1值、AUC值、ROC曲线等,指标相互之间是有关系的,只是侧重点不同。本案例选用准确率、精确率、AUC值和ROC曲线这4个指标对各个模型进行评价。
针对决策树分类模型、梯度提升分类模型和XGBoost分类模型分别计算准确率、精确率、AUC值。
from sklearn.metrics import precision_score, roc_auc_score, roc_curve
from sklearn.metrics import accuracy_score
import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier
import xgboost as xgb
from sklearn.ensemble import GradientBoostingClassifier
import matplotlib.pyplot as plt
train_class = pd.read_csv('../tmp/clean_train.csv') # 已预处理和贴标签训练数据
test_class = pd.read_csv('../tmp/clean_test.csv') # 已预处理和贴标签测试数据
# 抽取正负样本
train_class = train_class[train_class['label'] == 1].sample(sum(
train_class['label'] == 1)).append(train_class[train_class['label'] == 0].sample(sum(train_class['label'] == 0)))
test_class = test_class[test_class['label'] == 1].sample(sum(
test_class['label'] == 1)).append(test_class[test_class['label'] == 0].sample(sum(test_class['label'] == 0)))
# 删除列
x_train = train_class.drop(['user_id', 'merchant_id', 'coupon_id', 'date_received', 'date'], axis=1)
x_test = test_class.drop(['user_id', 'merchant_id', 'coupon_id', 'date_received', 'date'], axis=1)
# 处理无穷数据(无穷数据大或者无穷数据小)
x_train[np.isinf(x_train)] = 0
x_test[np.isinf(x_test)] = 0
# 决策树分类模型
model_dt_evaluate = DecisionTreeClassifier(max_leaf_nodes=16, random_state=123).fit(x_train.drop(['label'], axis=1), x_train['label'])
model_dt_pre = model_dt_evaluate.predict(x_test.drop(['label'], axis=1)) # 模型预测
# 决策树分类模型评价指标值
pre = model_dt_evaluate.predict_proba(x_test.drop(['label'], axis=1)) # 输出预测概率
auc = roc_auc_score(x_test['label'], pre[:, 1]) # 计算AUC值
print('AUC值为%.2f%%:'% (auc * 100.0))
dt_evaluate_accuracy = accuracy_score(x_test['label'], model_dt_pre)
print('准确率为%.2f%%:'% (dt_evaluate_accuracy * 100.0))
dt_evaluate_p = precision_score(x_test['label'], model_dt_pre)
print('精确率为%.2f%%'% (dt_evaluate_p * 100.0))
# 绘制ROC曲线
tr_fpr, tr_tpr, tr_threasholds = roc_curve(x_test['label'], pre[:, 1])
plt.title("ROC %s(AUC=%.4f)"% ('曲线', auc))
plt.xlabel('假正率')
plt.ylabel('真正率')
plt.plot(tr_fpr, tr_tpr)
plt.show()
# 梯度提升分类
model = GradientBoostingClassifier(n_estimators=100, max_depth=5)
model.fit(x_train.drop(['label'], axis=1), x_train['label']) # 模型训练
# 梯度提升评价指标
pre = model.predict_proba(x_test.drop(['label'], axis=1)) # 输出预测概率
auc = roc_auc_score(x_test['label'], pre[:, 1]) # 计算AUC值
print('AUC值为%.2f%%:'% (auc * 100.0))
pre1 = model.predict(x_test.drop(['label'], axis=1))
gb_evaluate_accuracy = accuracy_score(x_test['label'], pre1)
print('准确率为%.2f%%:'% (gb_evaluate_accuracy * 100.0))
gb_evaluate_p = precision_score(x_test['label'], pre1)
print('精确率为%.2f%%'% (gb_evaluate_p * 100.0))
# 绘制ROC曲线
tr_fpr, tr_tpr, tr_threasholds = roc_curve(x_test['label'], pre[:, 1])
plt.title("ROC %s(AUC=%.4f)"% ('曲线', auc))
plt.xlabel('假正率')
plt.ylabel('真正率')
plt.plot(tr_fpr, tr_tpr)
plt.show()
# XGBoost分类模型
model_xgb_evaluate = xgb.XGBClassifier(max_depth=6, learning_rate=0.1,n_estimators=150, silent=True, objective='binary:logistic')
model_xgb_evaluate.fit(x_train.drop(['label'], axis=1), x_train['label'])
# 对验证样本进行预测
model_xgb_pre = model_xgb_evaluate.predict(x_test.drop(['label'], axis=1))
# XGBoost分类模型评价指标
pre = model_xgb_evaluate.predict_proba(x_test.drop(['label'], axis=1)) # 输出预测概率
auc = roc_auc_score(x_test['label'], pre[:, 1]) # 计算AUC值
print('AUC值为%.2f%%:'% (auc * 100.0))
xfb_evaluate_accuracy = accuracy_score(x_test['label'], model_xgb_pre)
print('准确率为:%.2f%%'% (xfb_evaluate_accuracy * 100.0))
xfb_evaluate_p = precision_score(x_test['label'], model_xgb_pre)
print('精确率为:%.2f%%'% (xfb_evaluate_p * 100.0))
# 绘制ROC曲线
tr_fpr, tr_tpr, tr_threasholds = roc_curve(x_test['label'], pre[:, 1])
plt.title("ROC %s(AUC=%.4f)"% ('曲线', auc))
plt.xlabel('假正率')
plt.ylabel('真正率')
plt.plot(tr_fpr, tr_tpr)
plt.show()
模型 | AUC值 | 准确率 | 精确率 |
---|---|---|---|
决策树分类模型 | 99.82% | 98.63% | 94.20% |
梯度提升分类模型 | 99.85% | 98.69% | 92.20% |
XGBoost分类模型 | 99.85% | 98.68% | 92.18% |
由表可知,决策树分类模型、梯度提升分类模型和XGBoost分类模型3个模型的AUC值和准确率均超过98%,且各指标之间的值相差不大,决策树分类模型的精确率比梯度提升分类模型和支持向量机分类模型的精确率高,可以大致说明决策树分类模型的预测效果优于梯度提升分类模型和支持向量机分类模型。
AUC值表示ROC曲线下的面积,面积越大,准确率越高。
决策树分类模型AUC值和准确率均超过98%,说明模型预测效果较好。
梯度提升分类模型的AUC值和准确率均超过98%,但是低于决策树分类模型的AUC值和准确率,说明模型预测效果比决策树分类模型较差。
XGBoost分类模型的AUC值和准确率均超过98%,但其值低于决策树分类模型,说明XGBoost分类模型预测效果比决策树分类模型预测效果较差。
综合上述分析,决策树分类模型的精确率比梯度提升分类模型和XGBoost分类模型的精确率高,可以大致说明决策树分类模型的预测效果优于梯度提升分类模型和XGBoost分类模型。
小结
本案例根据O2O平台中用户使用优惠券的历史记录,先对原始数据进行描述性统计和探索性分析,主要分析优惠形式信息、用户消费行为和商户投放优惠券信息。后对数据进行数据预处理,包括数据清洗和数据转换,以及结合用户、商户、优惠券、用户和商户交互特点构造新指标。最后分别建立决策树分类模型、梯度提升分类模型和XGBoost分类模型,预测用户在领取优惠券后15天以内的使用情况,并对各个模型进行模型评价。
标签:优惠券,date,05,test,用户,O2O,train,数据挖掘,data From: https://blog.csdn.net/Morse_Chen/article/details/144492626附:以上文中的数据集及相关资源下载地址:
链接:https://pan.quark.cn/s/4d7a305df20a
提取码:pLUU