python O2O网站优惠券使用率数据分析

项目源码请见:https://github.com/dennis0818/O2O_offline

一 项目简介

随着移动设备的完善和普及,移动互联网+各行各业进入了高速发展阶段,这其中以O2O(Online to Offline)消费最为吸引眼球。据不完全统计,O2O行业估值上亿的创业公司至少有10家,也不乏百亿巨头的身影。O2O行业关联数亿消费者,各类APP每天记录了超过百亿条用户行为和位置记录,因而成为大数据科研和商业化运营的最佳结合点之一。 以优惠券盘活老用户或吸引新客户进店消费是O2O的一种重要营销方式。然而随机投放的优惠券对多数用户造成无意义的干扰。对商家而言,滥发的优惠券可能降低品牌声誉,同时难以估算营销成本。个性化投放是提高优惠券核销率的重要技术,它可以让具有一定偏好的消费者得到真正的实惠,同时赋予商家更强的营销能力。

二 分析目标

  1. 分析店面客流量是否火爆的影响因素
  2. 分析顾客的消费习惯
  3. 分析投放的优惠券的使用情况

三 数据来源

本数据来自“天池大数据”的“天池新人实战赛o2o优惠券使用预测”赛题

本赛题提供用户在2016年1月1日至2016年6月30日之间真实线上线下消费行为。

四 数据分析

本次分析仅使用线下交易数据

1 字段表:

线下消费情况表(ccf_offline_stage1_train.csv):

FieldDescription
User_id用户ID
Merchant_id商户ID
Coupon_id优惠券ID:null表示无优惠券消费,
此时Discount_rate和Date_received字段无意义
Discount_rate优惠率:x\in[0,1]代表折扣率;x:y表示满x减y。单位元
DistanceUser经常活动的地点离该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&Coupon_id != null,
则表示用优惠券消费日期,即正样本;
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import patsy
from datetime import datetime

%matplotlib inline
offline = pd.read_csv('d:/python/exercise/samples/o2o/ccf_offline_stage1_train.csv', parse_dates = ['Date_received', 'Date'])
len(offline) #共175万条数据
1754884
offline.head()
User_idMerchant_idCoupon_idDiscount_rateDistanceDate_receivedDate
014394082632NaNNaN0.0NaT2016-02-17
11439408466311002.0150:201.02016-05-28NaT
2143940826328591.020:10.02016-02-17NaT
3143940826321078.020:10.02016-03-19NaT
4143940826328591.020:10.02016-06-13NaT

2 数据规整

#空值判断
offline.isnull().sum()
User_id               0
Merchant_id           0
Coupon_id        701602
Discount_rate    701602
Distance         106003
Date_received    701602
Date             977900
dtype: int64

把"Discount_rate"列中的满减转换为折扣率

offline['Discount_rate'] = offline['Discount_rate'].fillna('null')
def calculate_discount(s):
    if ':' in s :
        split = s.split(':')
        discount_rate = (int(split[0]) - int(split[1])) / int(split[0])
        return round(discount_rate, 2)
    elif s == 'null':
        return np.nan
    else :
        return float(s)
offline['Discount_rate'] = offline['Discount_rate'].map(calculate_discount)
offline.head()
User_idMerchant_idCoupon_idDiscount_rateDistanceDate_receivedDate
014394082632NaNNaN0.0NaT2016-02-17
11439408466311002.00.871.02016-05-28NaT
2143940826328591.00.950.02016-02-17NaT
3143940826321078.00.950.02016-03-19NaT
4143940826328591.00.950.02016-06-13NaT

根据文档描述,“Coupon_id”字段:null表示无优惠券消费,此时Discount_rate和Date_received字段无意义
检查“Coupon_id”和“Date_received”字段空值与非空值是否对应

nan1 = offline['Coupon_id'].isnull()
nan2 = offline['Date_received'].isnull()
np.all(nan1 == nan2) #True表示“Coupon_id”和“Date_received”这两列的空值与非空值是一一对应的
True
nan3 = offline['Discount_rate'].isnull()
np.all(nan1 == nan3) #True表示“Coupon_id”和“Discount_rate”这两列的空值与非空值是一一对应的
True

根据消费日期的字段描述:如果Date=null & Coupon_id != null,该记录表示领取优惠券但没有使用,即负样本;如果Date!=null & Coupon_id = null,则表示普通消费日期;如果Date!=null & Coupon_id != null,则表示用优惠券消费日期,即正样本

分别整理出如上各个样本集合

  1. 普通消费(nor_consume):不用券消费
  2. 有券没消费(cpon_no_consume)
  3. 用券消费(cpon_consume)
  4. 无券也没消费(no_cpon_no_consume)
nor_consume = offline[(offline['Coupon_id'].isnull()) & (offline['Date'].notnull())]  #普通消费(没券也买)
cpon_no_consume = offline[(offline['Coupon_id'].notnull()) & (offline['Date'].isnull())]  #有券没消费
cpon_consume = offline[(offline['Coupon_id'].notnull()) & (offline['Date'].notnull())]  #有券消费
no_cpon_no_consume = offline[(offline['Coupon_id'].isnull()) & (offline['Date'].isnull())]  #没券没消费
len(nor_consume)
701602

3 数据分析

绘制各种消费方式情形的饼图

consume_status_dict = {'nor_consume':len(nor_consume),'cpon_consume':len(cpon_consume),'cpon_no_consume':len(cpon_no_consume)}
consume_status = pd.Series(consume_status_dict)
#消费方式构成的饼图
fig, ax = plt.subplots(1, 1, figsize = (8,10))
consume_status.plot.pie(ax = ax, 
                        autopct = '%1.1f%%',
                        startangle = -90, 
                        shadow = True, 
                        explode = [0.02,0.05,0.02],
                        colors = ['#46A1EB', '#FC3790', '#24E360'],
                        textprops = {'fontsize': 15, 'color': 'black'},
                        wedgeprops = {'linewidth': 1, 'edgecolor':'black'}, 
                        labels = ['nor_consume \n ({})'.format(len(nor_consume)), 
                                  'cpon_consume \n ({})'.format(len(cpon_consume)),
                                  'cpon_no_consume \n ({})'.format(len(cpon_no_consume))]
                                 )

ax.set_ylabel('')
ax.set_title('consume_status')
plt.legend(labels = ['nor_consume','cpon_consume','cpon_no_consume'], loc = 'upper left')
<matplotlib.legend.Legend at 0x23f992696a0>

在这里插入图片描述

在有券消费人群中,分析距离和优惠券折扣

#各商家对应的顾客到店平均距离
Merchant_distance = cpon_consume.groupby(['Merchant_id'])['Distance'].mean()
#各商家对应的顾客到店消费平均折扣力度
Merchant_discount = cpon_consume.groupby('Merchant_id')['Discount_rate'].mean()

持券到店消费顾客数最多的店家

#顾客购买的每一件商品都形成一条记录,因此用unique将其去重

popular_merchant = cpon_consume.groupby(['Merchant_id'])['User_id'].agg(lambda x : len(x.unique())).sort_values(ascending = False)

#找出到店消费顾客数超过500的店家

popular_merchant500 = popular_merchant[popular_merchant >500]
popular_merchant500.name = 'customer_count'

到店持券消费人数超过500人的店,连接顾客到店平均距离和平均折扣力度

merchant_pop_dis = pd.merge(popular_merchant500, Merchant_distance,left_index=True, right_index=True)
merchant_pop_dis_rate = pd.merge(merchant_pop_dis, Merchant_discount, left_index = True, right_index = True)
merchant_pop_dis_rate
customer_countDistanceDiscount_rate
Merchant_id
534128000.1685980.826036
76026270.3498660.799873
338122481.6524290.744150
648520290.3685670.770439
209914010.9680720.900000
293413101.1148330.830000
45010940.8921640.819274
35329680.2724980.852289
75559251.3299770.828381
1520870NaN0.925632
69018550.5578950.834165
36218510.4727990.747565
41428320.5558820.900000
13795870.7062500.830096
14695842.0928000.721870
14335591.0549620.825959

计算到店消费人数与平均距离和折扣力度的相关系数

merchant_pop_dis_rate.corrwith(merchant_pop_dis_rate.customer_count)
customer_count    1.000000
Distance         -0.306180
Discount_rate    -0.204102
dtype: float64
#用热力图看看
sns.heatmap(merchant_pop_dis_rate.corr(), annot = True, cmap='RdYlGn',linewidths=0.2)
<matplotlib.axes._subplots.AxesSubplot at 0x23f96a1eb38>

在这里插入图片描述
由计算结果可知:

  1. 到店消费人数多少与顾客到店距离呈负相关,相关系数0.3≤|r|≤0.5,说明线性相关程度为低度相关
  2. 到店消费人数与优惠券打折力度呈负相关,相关系数|r|≤0.3,说明线性相关程度极弱

因此,这些店家之所以火爆应该是物美价廉等品质因素带来的销量

接下来,从个人因素出发,分析哪些顾客对优惠券依赖程度较高
之前拆分的三个表中:cpon_consume和nor_consume表分别收集了有券消费的记录和不用券消费的普通购买记录等信息,采用横向对比的思路,在两个表中的这些顾客中,找出对于同一家店,有优惠券才消费,没有优惠券就不消费的一些顾客

  • 先来看看在cpon_consume表中却不在nor_consume表中的顾客人数
len(np.setdiff1d(cpon_consume.User_id.values, nor_consume.User_id.values))
10476
  • 这样单纯的从cpon_consume表中排除出现在nor_consume的顾客,感觉打击面有点过大

    比如:
Coupon_consume
merchantcustomerrecord
a1\
b1\
c\\
nor_consume
merchantcustomerrecord
a\\
b\\
c1\


例子中,1号顾客在a和b店都有过只有券才去的记录,但是在c店只进行过普通消费(也许是离家近买瓶水),按照上面的方法,1号顾客就会被排除在外,但他对于a店和b店是只有优惠券才去的。
所以以上方法这10476人中存在漏网之鱼

采用如下方法把只在有优惠券才去某家店消费的所有顾客归纳出来


no_cpon_shopping = nor_consume.groupby(['Merchant_id','User_id']).size()
no_cpon_shopping = no_cpon_shopping.reset_index('User_id').rename(columns={'User_id': 'user', 0:'times'})
cpon_shopping = cpon_consume.groupby(['Merchant_id', 'User_id']).size()
cpon_shopping = cpon_shopping.reset_index('User_id').rename(columns = {'User_id':'user', 0: 'times'})
shopping = pd.concat([cpon_shopping, no_cpon_shopping], keys = ['cpon', 'nor'])
shopping = shopping.reset_index(level=0).rename(columns = {'level_0': 'how'})
shopping.head()
howusertimes
Merchant_id
3cpon26514181
4cpon39310771
4cpon70834754
5cpon31814531
5cpon41676831
#把在cpon_consume而不在nor_consume中的顾客区分出来
def diff_mania(df):
    cpon = df[df['how'] == 'cpon']['user']
    nor = df[df['how'] == 'nor']['user']
    c = np.setdiff1d(cpon, nor)
    return pd.Series(c)
shopping_mania = shopping.groupby(level = 0).apply(diff_mania)
len(shopping_mania.unique())
14407

这1万4千多人就是“除非这个店有打折,否则不买”的“购物狂”人群

shopping_mania = shopping_mania.unique()

选出这些购物狂和对应的消费记录

shopping_mania_records = cpon_consume[cpon_consume['User_id'].isin(shopping_mania)]
shopping_mania_records.head()
User_idMerchant_idCoupon_idDiscount_rateDistanceDate_receivedDate
6911474769012366.00.830.02016-05-232016-06-05
751147475341111.00.830.02016-01-272016-02-21
761147475341111.00.830.02016-02-072016-02-18
7711474753417751.00.800.02016-01-272016-01-28
8411474724548088.00.750.02016-03-242016-03-31
len(shopping_mania_records)
21064
#做个对比表,分别对比距离、折扣和用掉优惠券时间
mania = [shopping_mania_records.Discount_rate.mean(),
         shopping_mania_records.Distance.mean(),
         (shopping_mania_records['Date'] - shopping_mania_records['Date_received']).mean().days]
all_cpon_consume = [cpon_consume.Discount_rate.mean(),
                    cpon_consume.Distance.mean(),
                    (cpon_consume['Date'] - cpon_consume['Date_received']).mean().days]

df = pd.DataFrame({'mania': mania, 'all': all_cpon_consume}, index = ['discount', 'distance', 'period'])
df
maniaall
discount0.8293480.832929
distance1.4060871.014263
period8.0000007.000000

由表可以看出,购物狂们花掉的优惠券折扣高于平均值,因此他们宁愿走更远的距离,但是看来并不急于花掉

df.plot.line(linestyle = '-', marker='o' )
<matplotlib.axes._subplots.AxesSubplot at 0x23f9693dc18>

在这里插入图片描述

下面再来分析一下每天中优惠券总体的发放与使用情况

offline['Date'] = offline['Date'].dt.date
offline['Date_received'] = offline['Date_received'].dt.date
offline['Date'].notnull().sum()
776984
offline['Date_received'].notnull().sum()
1053282
date_sort = offline[offline['Date'].notnull()]['Date'].sort_values().unique()
date_receive_sort = offline[offline['Date_received'].notnull()]['Date_received'].sort_values().unique()
print('consume -- start with: ', date_sort[0].strftime('%Y-%m-%d'),' end with: ', date_sort[-1].strftime('%Y-%m-%d'))
consume -- start with:  2016-01-01  end with:  2016-06-30
print('receive -- start with: ', date_receive_sort[0].strftime('%Y-%m-%d'),' end with: ', date_receive_sort[-1].strftime('%Y-%m-%d'))
receive -- start with:  2016-01-01  end with:  2016-06-15
#每天优惠券使用数量
consume_num_everyday = cpon_consume[['User_id','Date_received']]
consume_num_everyday = consume_num_everyday.groupby(['Date_received'], as_index=False).count()
consume_num_everyday = consume_num_everyday.rename(columns= {'User_id':'count'})
#每天发放的优惠券数量
coupon_issue_everyday = offline[offline['Date_received'].notnull()][['Date_received','User_id']]
coupon_issue_everyday = coupon_issue_everyday.groupby(['Date_received'], as_index=False).count()
coupon_issue_everyday = coupon_issue_everyday.rename(columns={'User_id':'count'})
consume_num_everyday.head()
Date_receivedcount
02016-01-0174
12016-01-0267
22016-01-0374
32016-01-0498
42016-01-05107
coupon_issue_everyday.head()
Date_receivedcount
02016-01-01554
12016-01-02542
22016-01-03536
32016-01-04577
42016-01-05691

每天发放的优惠券和每天被用掉的优惠券的数量对比和百分比柱状图

plt.figure(figsize=(18,6))
plt.bar(date_receive_sort, coupon_issue_everyday['count'], label = 'amount of coupon everday')
plt.bar(date_receive_sort, consume_num_everyday['count'], label = 'amount of consume everday')
plt.yscale('log')
plt.legend()
<matplotlib.legend.Legend at 0x23fa8e26a20>

在这里插入图片描述

plt.figure(figsize=(18,6))
plt.bar(date_receive_sort, consume_num_everyday['count']/coupon_issue_everyday['count'], label = 'percent', color = ['#07BF8B'])


<BarContainer object of 167 artists>

在这里插入图片描述

#总平均使用率
(consume_num_everyday['count'].sum())/(coupon_issue_everyday['count'].sum())
0.07156867771404049

五 结论

  1. 顾客光顾最多的比较火爆的店面,并不受距离和打折力度影响,应该是商品品质或消费体验等水平较高所致
  2. 各店家发放的优惠券,被使用总数和发放总数的比例不到一成,所以随机发放优惠券的效果并不理想,有很大的优化空间
  3. 个性化投放是提高优惠券核销率的重要技术,它可以让具有一定偏好的消费者得到真正的实惠,通过分析发现,属于’shopping_mania’这个集合的1万4千多名消费者对于优惠券比较依赖,可以成为个性化发放的重点对象
  4. 更进一步有针对性的个性化投放可以通过机器学习建模来拟合顾客的消费习惯,从而更精确的挖掘优惠券的适用对象

  • 5
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值