【因果推断】优惠券政策对不同店铺的影响

news2024/9/16 13:48:39

这次依然是用之前rossmann店铺竞赛的数据集。
之前的数据集探索处理在这里已经做过了,此处就不再赘述了CSDN链接
数据集地址:竞赛链接
这里探讨数据集中Promo2对于每家店铺销售额的影响。其中,Promo2是一个基于优惠券的邮寄活动,发送给参与商店的顾客。每封信里都有几张优惠券,大部分是所有产品的一般折扣,有效期为三个月。所以在这些优惠券到期之前,我们会给客户发新一轮的邮件。具体的变量解释可以看这里:Promo2释义
此处单纯是为了作因果推断的练习。由于笔者是因果推断的初学者,有些概念与处理可能会有疏漏错误,还请发现的读者不吝赐教。

一、数据可视化

首先,我们从单纯地数据可视化的角度来看总体均值以及店铺自身使用优惠券政策前后的比较。

import warnings
warnings.filterwarnings("ignore")

import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta
data = pd.read_csv("train.csv")
data = data[data["Open"]==1]
store_info = pd.read_csv("store.csv")

#假设一年的第一周是包含1月4日的那周
data = data[data["Sales"]>0]
data["Date"]=pd.to_datetime(data["Date"])
data["Year"]=[i.year for i in data["Date"]]
data["Month"]=[i.month for i in data["Date"]]
def get_first_day_of_week(year, week):
    if np.isnan(year):
        return np.nan
    year = int(year)
    week = int(week)
    first_thursday = datetime(year, 1, 4)  
    while first_thursday.weekday() != 3:  # weekday() returns 0 (Monday) through 6 (Sunday)  
        first_thursday += timedelta(days=1)  
  
    first_day_of_week = first_thursday - timedelta(days=first_thursday.weekday() - 1)  
  
    target_date = first_day_of_week + timedelta(weeks=week - 1)  
  
    return datetime.date(target_date)
res = []
for i in range(store_info.shape[0]):
    res.append(get_first_day_of_week(store_info.loc[i,"Promo2SinceYear"],store_info.loc[i,"Promo2SinceWeek"]))
store_info["Promo2StartTime"] = pd.to_datetime(res)
data = data.merge(store_info,on="Store")
data["Post"] = (data["Date"]>data["Promo2StartTime"]).astype("int8")
data["StateHoliday"] = (data["StateHoliday"]!="0").astype("int8")
## 先简单比较一下每天·店的均值
#均值比较
Promo2_date=data[data["Post"]==1].groupby(["Year","Month"])["Sales"].mean()
NoPromo2_date=data[data["Post"]==0].groupby(["Year","Month"])["Sales"].mean()
Promo2_date.index = ["-".join([str(j) for j in i]) for i in Promo2_date.index]
NoPromo2_date.index = ["-".join([str(j) for j in i]) for i in NoPromo2_date.index]
fig, ax = plt.subplots()
plt.plot(Promo2_date,label="has Promo2")
plt.plot(NoPromo2_date,label="does not have Promo2")
ax.xaxis.set_visible(False)
plt.legend()
plt.show()

在这里插入图片描述
图中蓝线代表实行了优惠券政策的店铺每个月的月均销量,而橙线则代表控制组的月均销量,可以看出控制组的销量明显高于优惠券政策的店铺。

## 去除那些没有“促销前”信息的店铺
tmp = data[~pd.isna(data["Promo2StartTime"])].groupby(["Store"])[["Promo2StartTime","Date"]].min()
drop_stores = list(tmp[tmp["Date"]>=tmp["Promo2StartTime"]].index)
for i in drop_stores:
    data = data[data["Store"]!=i]
data.index = range(data.shape[0])
data_promo2 = data[~pd.isna(data["Promo2StartTime"])].reset_index(drop=True)
isin_thirty_days=[abs(i).days<=30 for i in data_promo2["Date"]-data_promo2["Promo2StartTime"]]
data_promo2 = data_promo2[isin_thirty_days]
promo2_before=data_promo2[data_promo2["Date"]<data_promo2["Promo2StartTime"]].groupby(["Store"])["Sales"].mean()
promo2_after=data_promo2[data_promo2["Date"]>=data_promo2["Promo2StartTime"]].groupby(["Store"])["Sales"].mean()
fig, ax = plt.subplots(figsize=(10,10))
num_bars = 15
idx = range(num_bars)
plt.bar(idx,promo2_before[0:num_bars].values,width=0.35,label="before Promo2")
plt.bar([i+0.35 for i in idx],promo2_after[0:num_bars].values,width=0.35,label="after Promo2")
ax.xaxis.set_visible(False)
plt.legend()
plt.show()

在这里插入图片描述
选取部分店铺在发行优惠券前后的销售额均值对比,发现大多数情况下,销售额会随着降低,不过也有部分店铺在发行优惠券后销售额反而提升了。

## 作图平行趋势
promo2_bydate = data.groupby(["Promo2StartTime","Year","Month"])["Sales"].mean().reset_index()
nopromo2 = data[pd.isna(data["Promo2StartTime"])].groupby(["Year","Month"])["Sales"].mean().reset_index()
nopromo2.index = [str(i)+"-"+str(j) for i,j in zip(nopromo2["Year"],nopromo2["Month"])]
fig, ax = plt.subplots()
sns.lineplot(nopromo2.sort_values(["Year","Month"])["Sales"],label="no_promo")
for i in promo2_bydate["Promo2StartTime"].unique()[[0,10,20,15]]:
    tmp = promo2_bydate[promo2_bydate["Promo2StartTime"]==i].sort_values(["Year","Month"]).reset_index()
    tmp.index = [str(i)+"-"+str(j) for i,j in zip(tmp["Year"],tmp["Month"])]
    sns.lineplot(tmp["Sales"],label=str(i)[0:10])
    ax.vlines(x=str(i)[0:7].replace("-0","-"),ymin=50,ymax=9000,ls="dashed")
    
ax.xaxis.set_visible(False)

在这里插入图片描述
选取几个不同时间开始发行优惠券的店铺的历史销售作图,可以发现大多数发行优惠券店铺的整体历史销售和不发行优惠券的那店铺趋势大致相同。

二、分组双重差分

从计量经济学的角度来看,本身我们获得的数据是一份面板数据(Panel Data),所以可以使用双重差分(DID)的方法来判断优惠券政策是否显著影响了店铺的销售额。但现在有一个问题,我们每一家店铺的优惠券政策(干预)实际上并非同时发生的。这并不符合传统DID的假设,因为对于不同的店铺而言,优惠券政策的影响效果是不同的。因此我们需要使用一个更为灵活的模型考虑。我们期望对于不同的时间而言,每个店铺有不同的效应。为了保证不出现梯度爆炸,我将时间分组为每年每周,而由于这一数据集中,干预或许并非是随机发生的,因此需要将其他的混淆因子也加入到模型中,能够使得模型解释性更好。

import statsmodels.formula.api as smf
## 按照群组分组进行OLS以作DID

data["Promo2StartTime"] = data["Promo2StartTime"].fillna(pd.to_datetime("2100-01-01"))
data["Post"] = (data["Date"]>data["Promo2StartTime"]).astype("int8")
data["StateHoliday"] = (data["StateHoliday"]!="0").astype("int8")
data["WeekOfYear"] = [i.weekofyear for i in data["Date"]]
data_for_ols = data.groupby(["Store","Year","WeekOfYear"]).agg({"Sales":"sum",
                                                       "Promo":"sum",
                                                       "StateHoliday":"sum",
                                                       "SchoolHoliday":"sum",
                                                       "Promo2":"sum",
                                                       "Post":"sum"}).reset_index()
data_for_ols = data_for_ols.merge(store_info[["Store","StoreType","Assortment","CompetitionDistance","Promo2StartTime"]],on="Store")
boolize = ["Promo","StateHoliday","SchoolHoliday","Promo2","Post"]
for col in boolize:
    data_for_ols[col] = pd.Series([1 if i>0 else 0 for i in data_for_ols[col]]).astype("int8")
    
data_for_ols["Sales"] = np.log(data_for_ols["Sales"])
formular = "Sales~Promo2:Post:C(Promo2StartTime):C(Year):C(WeekOfYear)+Promo+StateHoliday+SchoolHoliday+StoreType+Assortment+CompetitionDistance"
twfe_model = smf.ols(formular,data=data_for_ols).fit()
df_predict = data_for_ols[data_for_ols["Post"]*data_for_ols["Promo2"]==1].reset_index()
df_predict["Promo2"] = 0
df_predict["predict"] = twfe_model.predict(df_predict)
print("参数个数:",len(twfe_model.params))
print("估计的ATT:",(df_predict["Sales"]-df_predict["predict"]).mean())

在这里插入图片描述
最终我们使用OLS最小二乘建模,获得了一个有3754个参数的回归模型。对于销售额而言,平均干预效果为负数。

三、干预异质性检验

那么对于不同的店铺而言,优惠券政策是否有不同影响?为了研究这一问题,笔者使用了元机器学习(Meta-Learner)的技巧,构建了一个X-Learner:
在这里插入图片描述
X-Learner分为两个阶段:第一个阶段训练1个倾向得分模型(也就是图中最左侧,根据协变量X来预测T的模型),同时训练2个模型回应(respond)模型(也就是图中左侧2个并列的模型),分别将受到干预和没收到干预的部分分开使用协变量预测因变量的模型。到了第二阶段,我们使用第一阶段的回应模型预测出两个反事实结果:对于没有受到干预的店铺,如果当初受到干预的话会有多少销售额(图中的 Y ∗ ∣ T = 0 Y^*|T=0 YT=0)以及对于受到了干预的店铺如果当初没有受到干预的话会有多少销售额(图中的 Y ∗ ∣ T = 1 Y^*|T=1 YT=1)。将 Y 1 Y1 Y1 Y 0 Y0 Y0相见,我们就获得了条件平均处理效果(CATE)。我们将CATE作为应变量,再使用协变量X训练2个模型(也就是图中带有绿色CATE的部分)。使用这2个模型按照倾向得分倒数的权重预测最终的CATE。(因为倾向得分越高表示越有可能受到干预,倒数反而代表对应样本更稀有,对于非随机实验而言参考价值更大)

## X-learner

## PS Model
Y_PS = "treatment"
train = pd.concat([train_T1,train_T0],axis=0)
train_ps = pd.get_dummies(train)
X_PS = [i for i in train_ps.columns if i != "Sales" and i != "treatment"]
ps_model = LogisticRegression(penalty='none')
ps_model.fit(train_ps[X_PS],train_ps[Y_PS])

## Predict_Model
X_lgbm = [i for i in data_t1.columns if i != "Sales" and i != "treatment"]
Y = "Sales"
model_t1 = LGBMRegressor()
model_t0 = LGBMRegressor()

model_t0.fit(train_T0[X_lgbm],train_T0[Y],sample_weight=1/ps_model.predict_proba(pd.get_dummies(train_T0)[X_PS])[:, 0])
model_t1.fit(train_T1[X_lgbm],train_T1[Y],sample_weight=1/ps_model.predict_proba(pd.get_dummies(train_T1)[X_PS])[:, 1])

## second_stage
tau_hat_0 = model_t1.predict(train_T0[X_lgbm])-train_T0[Y]
tau_hat_1 = train_T1[Y]-model_t0.predict(train_T1[X_lgbm])

model_tau_t1 = LGBMRegressor()
model_tau_t0 = LGBMRegressor()

model_tau_t0.fit(train_T0[X_lgbm],tau_hat_0)
model_tau_t1.fit(train_T1[X_lgbm],tau_hat_1)
# estimate the CATE
valid = pd.concat([valid_T1,valid_T0],axis=0)

ps_valid = ps_model.predict_proba(pd.get_dummies(valid)[X_PS])[:, 1]

cate_valid = valid.assign(
    cate=(ps_valid*model_tau_t0.predict(valid[X_lgbm]) +
          (1-ps_valid)*model_tau_t1.predict(valid[X_lgbm])
         )
)
sns.displot(cate_valid["cate"])

在这里插入图片描述
可以看出,使用X-Learner预测的结果中,在验证集大部分的店铺在受到了干预之后,销售量明显是负数;只有小部分的店铺销售额会受到正面影响。
接下来将验证集中各个CATE排序,求出累计的干预效果,以此做出QINI曲线。

sorted_cate = cate_valid["cate"].values[np.argsort(-cate_valid["cate"].values)]
cumulative_cate = np.cumsum(sorted_cate)/np.arange(1,len(sorted_cate)+1)
cumulative_fractions = np.arange(1, len(cumulative_cate) + 1) / len(cumulative_cate)
plt.plot(cumulative_fractions,cumulative_cate,"r",label="QINI Curve")
plt.plot([0,1],[cumulative_cate[0],cumulative_cate[-1]],"k--",label="Random Assignment")
plt.title("QINI Curve")
plt.legend()
plt.show()

在这里插入图片描述
可以看到,在一开始,的确有些店铺的销售额是受到了优惠券政策的正向影响;但是到了之后,优惠券政策依然是负向影响。

四、总结

本文使用了竞赛用的销售数据进行了销售额与优惠券政策的因果推断。实际上更多的是因果推断方法论的学习。对于优惠券政策而言,销售额很有可能并非是真正的干预目标,而且店铺是否要发现优惠券,也需要考量除了数据集以外的其他因素。而对于销售额的干预影响,很多公司都会做Uplift Model来衡量,笔者有空也会对此进行学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1904852.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ZYNQ-LINUX环境C语言利用Curl库实现HTTP通讯

前言 在Zynq-Linux环境中&#xff0c;需要使用C语言来编写APP时&#xff0c;访问HTTP一般可以使用Curl库来实现&#xff0c;但是在Zynq的SDK中&#xff0c;并没有集成该库&#xff0c;在寻找了很多资料后找到了一种使用很方便的额办法。这篇文章主要记录一下移植Curl的过程。 …

将iStoreOS部署到VMware ESXi变成路由器

正文共&#xff1a;888 字 19 图&#xff0c;预估阅读时间&#xff1a;1 分钟 前面把iStoreOS部署到了VMware workstation上&#xff08;将iStoreOS部署到VMware Workstation&#xff09;。如果想把iStoreOS直接部署到ESXi上&#xff0c;你会发现转换镜像不能直接生成OVF或者OV…

Redis源码整体结构

一 前言 Redis源码研究为什么先介绍整体结构呢?其实也很简单,作为程序员的,要想对一个项目有快速的认知,对项目整体目录结构有一个清晰认识,有助于我们更好的了解这个系统。 二 目录结构 Redis源码download到本地之后,对应结构如下: 从上面的截图可以看出,Redis源码一…

【2024_CUMCM】T检验、F检验、卡方检验

T检验 T检验主要用于比较两组数据的均值差异&#xff0c;适用于小样本数据分析。它可以分为单样本T检验、独立样本T检验和配对样本T检验。 单样本T检验用于比较一个样本与已知的总体均值差异&#xff0c;独立样本T检验用于比较两个独立样本的均值差异&#xff0c;配对样本T检…

【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【21】【购物车】

持续学习&持续更新中… 守破离 【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【21】【购物车】 购物车需求描述购物车数据结构数据Model抽取实现流程&#xff08;参照京东&#xff09;代码实现参考 购物车需求描述 用户可以在登录状态下将商品添加到购物车【用户购物…

从FasterTransformer源码解读开始了解大模型(2.1)代码通读03

从FasterTransformer源码解读开始了解大模型&#xff08;2.2&#xff09;代码解读03-forward函数 写在前面的话 本篇的内容继续解读forward函数&#xff0c;从650行开始进行解读 零、输出Context_embeddings和context_cum_log_probs的参数和逻辑 从653行开始&#xff0c;会…

Python实现ABC人工蜂群优化算法优化随机森林回归模型(RandomForestRegressor算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 人工蜂群算法(Artificial Bee Colony, ABC)是由Karaboga于2005年提出的一种新颖的基于群智能的全局优化…

LeetCode Hard|124.二叉树中的最大路径和

力扣题目链接 题目解读&#xff1a; 二叉树路径的定义即从1.任意节点出发&#xff0c;到达任意节点&#xff1b;2.该路径至少包含一个节点&#xff0c;且不一定经过跟节点&#xff1b;3.求所有可能路径和的最大值。 也就是说路径途径一个节点只能选择来去两个方向 考虑一个二叉…

微信公众平台测试账号本地微信功能测试说明

使用场景 在本地测试微信登录功能时&#xff0c;因为微信需要可以互联网访问的域名接口&#xff0c;所以本地使用花生壳做内网穿透&#xff0c;将前端服务的端口和后端服务端口进行绑定&#xff0c;获得花生壳提供的两个外网域名。 微信测试账号入口 绑定回调接口 回调接口的…

2024年06月CCF-GESP编程能力等级认证Python编程二级真题解析

本文收录于专栏《Python等级认证CCF-GESP真题解析》&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 一、单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 第 1 题 小杨父母带他到某培训机构给他报名参加CCF组织的GESP认证…

声明队列和交换机 + 消息转换器

目录 1、声明队列和交换机 方法一&#xff1a;基于Bean的方式声明 方法二&#xff1a;基于Spring注解的方式声明 2、消息转换器 1、声明队列和交换机 方法一&#xff1a;基于Bean的方式声明 注&#xff1a;队列和交换机的声明是放在消费者这边的&#xff0c;这位发送的人他…

力扣206

题目 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1]示例 3&#xff1a; 输…

【排序算法】—— 快速排序

快速排序的原理是交换排序&#xff0c;其中qsort函数用的排序原理就是快速排序&#xff0c;它是一种效率较高的不稳定函数&#xff0c;时间复杂度为O(N*longN)&#xff0c;接下来就来学习一下快速排序。 一、快速排序思路 1.整体思路 以升序排序为例&#xff1a; (1)、首先随…

PTA甲级1005:Spell It Right

错误代码&#xff1a; #include<iostream> #include<vector> #include<unordered_map> using namespace std;int main() {unordered_map<int, string> map {{0, "zero"}, {1, "one"}, {2, "two"}, {3, "three&qu…

有一个日期(Date)类的对象和一个时间(Time)类的对象,均已指定了内容,要求一次输出其中的日期和时间

可以使用友元成员函数。在本例中除了介绍有关友元成员函数的简单应用外&#xff0c;还将用到类的提前引用声明&#xff0c;请读者注意。编写程序&#xff1a; 运行结果&#xff1a; 程序分析&#xff1a; 在一般情况下&#xff0c;两个不同的类是互不相干的。display函…

实验六 图像的傅立叶变换

一&#xff0e;实验目的 1了解图像变换的意义和手段&#xff1b; 2熟悉傅立叶变换的基本性质&#xff1b; 3熟练掌握FFT变换方法及应用&#xff1b; 4通过实验了解二维频谱的分布特点&#xff1b; 5通过本实验掌握利用MATLAB编程实现数字图像的傅立叶变换。 6评价人眼对图…

股票Level-2行情是什么,应该怎么使用,从哪里获取数据

行情接入方法 level2行情websocket接入方法-CSDN博客 相比传统的股票行情&#xff0c;Level-2行情为投资者打开了更广阔的视野&#xff0c;不仅限于买一卖一的表面数据&#xff0c;而是深入到市场的核心&#xff0c;提供了十档乃至千档的行情信息&#xff08;沪市十档&#…

关于MCU-Cortex M7的存储结构(flash与SRAM)

MCU并没有DDR&#xff0c;所以他把代码存储在flash上&#xff0c;临时变量和栈运行在SRAM上。之所以这么做是因为MCU的CPU频率很低&#xff0c;一般低于500MHZ&#xff0c;flash的读取速度能够满足CPU的取指需求&#xff0c;但flash 的写入速度很慢&#xff0c;所以引入了SRAM …

【数据结构与算法】快速排序挖坑法

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《数据结构与算法》 期待您的关注 ​

go语言day11 错误 defer(),panic(),recover()

错误&#xff1a; 创建错误 1&#xff09;fmt包下提供的方法 fmt.Errorf(" 格式化字符串信息 " &#xff0c; 空接口类型对象 ) 2&#xff09;errors包下提供的方法 errors.New(" 字符串信息 ") 创建自定义错误 需要实现error接口&#xff0c;而error接口…