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

这次依然是用之前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.nanyear = 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以作DIDdata["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.rhkb.cn/news/371200.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Python API的机械臂UDP上报设置及读取

睿尔曼机械臂提供了1个可持续读取机械臂状态的接口&#xff0c;UDP通信状态反馈接口。 该接口提供了json协议、API的读取&#xff0c;设置通信开启之后无需再进行设置即可以固定频率读取。 Python程序源码可从以下网盘地址获取&#xff08;地址永久有效&#xff09;&#xff1…

基础权限存储

一丶要求 建立用户组shengcan&#xff0c;其id为 2000建立用户组 caiwu&#xff0c;其id 为2001建立用户组 jishu&#xff0c;其id 为 2002建立目录/sc,此目录是 shengchan 部门的存储目录&#xff0c;只能被 shengchan 组的成员操作4.其他用户没有任何权限建立目录/cw,此目录…

51单片机-第一节-LED和独立按键

一、点亮LED&#xff1a; 首先包含头文件 <REGX52.H> 随后令P2为0xFE。(此时二进制对应1111 1110&#xff0c;为0 的LED亮&#xff0c;故八个灯中的最后一个亮起)。 注&#xff1a;P2为控制LED的8位寄存器。 void main() {P2 0xFE;//1111 1110while(1){} } 二、L…

微信小程序简历Demo

微信小程序简历Demo 使用介绍最后获取源码 bilibili视频介绍 使用介绍 使用微信小程序实现的一个简历实现Demo 拖动马里奥&#xff0c;到指定Name下方 向上顶就可以显示对应的简历样式 点击头像可拨打电话 点击信息处可显示当前位置 最后 这是一个简单并且有趣的微信小程…

acwing 291.蒙德里安的梦想

解法&#xff1a; 核心&#xff1a;先放横着的&#xff0c;再放竖着的。 总方案数&#xff0c;等于只放横着的小方块的合法方案数。 如何判断当前方案是否合法&#xff1f;所有剩余位置&#xff0c;能否填充满竖着的小方块。 即按列来看&#xff0c;每一列内部所有连续的空着的…

Cyber Weekly #14:WAIC 2024

赛博新闻 1、WAIC2024开幕&#xff1a;一半机器人&#xff0c;一半大模型 7月4日&#xff0c;AI界春晚——2024世界人工智能大会&#xff08;WAIC 2024&#xff09;在上海开幕&#xff0c;大会展示了500家企业的1500项展品&#xff0c;突出了机器人和大模型技术。国产机器人和…

使用Keil将STM32部分程序放在RAM中运行

手动分配RAM区域,新建.sct文件,定义RAM_CODE区域,并指定其正确的起始地址和大小。 ; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************…

硬件开发笔记(二十四):贴片电容的类别、封装介绍,AD21导入贴片电容、原理图和封装库3D模型

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/140241817 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

【在Linux世界中追寻伟大的One Piece】HTTPS协议原理

目录 1 -> HTTPS是什么&#xff1f; 2 -> 相关概念 2.1 -> 什么是"加密" 2.2 -> 为什么要加密 2.3 -> 常见的加密方式 2.4 -> 数据摘要 && 数据指纹 2.5 -> 数字签名 3 -> HTTPS的工作过程 3.1 -> 只使用对称加密 3.2 …

13.BeanFactory后处理器

作用&#xff1a;为bean工厂补充一些bean的定义。 ConfigurationClassPostPorcessor Bean工厂后置处理器(ConfigurationClassPostProcessor)可以去解析&#xff1a; ComponentScan Bean Import ImportResource package com.xkj.org.a05;import com.alibaba.druid.pool.D…

【C语言】刷题笔记 Day1

多刷题 多思考 【题目1】 实现字母的大小写转换&#xff0c;实现多组输入输出 1. getchar 为输入函数&#xff0c;EOF&#xff08;end of file&#xff09;为文件结束标志&#xff0c;通常为文件结束的末尾。 2. 题目中要求实现多组输入输出&#xff0c;那我们用 while 循…

C# 编程中互斥锁的使用

C# 中的互斥锁 互斥锁是 C# 中使用的同步原语&#xff0c;用于控制多个线程或进程对共享资源的访问。其目的是确保在任何给定时间只有一个线程或进程可以获取互斥锁&#xff0c;从而提供互斥。 C# 中互斥锁的优点 可以使用互斥锁 (Mutex) 并享受其带来的好处。 1. 共享资源…

求职成功率的算法,与葫芦娃救爷爷的算法,有哪些相同与不同

1 本节概述 通过在B站百刷葫芦娃这部儿时剧&#xff0c;我觉得可以从中梳理出一些算法&#xff0c;甚至可以用于求职这个场景。所以&#xff0c;大家可以随便问我葫芦娃的一些剧情和感悟&#xff0c;我都可以做一些回答。 2 葫芦娃救爷爷有哪些算法可言&#xff1f; 我们知道…

【Linux】信号的处理

你很自由 充满了无限可能 这是很棒的事 我衷心祈祷你可以相信自己 无悔地燃烧自己的人生 -- 东野圭吾 《解忧杂货店》 信号的处理 1 信号的处理2 内核态 VS 用户态3 键盘输入数据的过程4 如何理解OS如何正常的运行5 如何进行信号捕捉信号处理的总结6 可重入函数volatile关…

1958.力扣每日一题7/7 Java(100%解)

博客主页&#xff1a;音符犹如代码系列专栏&#xff1a;算法练习关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 目录 思路 解题方法 时间复杂度 空间复杂度 Code 思路 首先将指定位…

用vue2+elementUI封装手机端选择器picker组件,支持单选、多选、远程搜索多选

单选注意点&#xff1a; touchmove.prevent: 在 touchmove 事件上添加 .prevent 修饰符&#xff0c;以阻止默认的滚动行为。 handleTouchStart: 记录触摸开始的 Y 坐标和当前的 translateY 值。 handleTouchMove: 计算触摸移动的距离&#xff0c;并更新 translateY 值。 han…

Android 开发中 C++ 和Java 日志调试

在 C 中添加堆栈日志 先在 Android.bp 中 添加 ‘libutilscallstack’ shared_libs:["liblog"," libutilscallstack"]在想要打印堆栈的代码中添加 #include <utils/CallStack.h> using android::CallStack;// 在函数中添加 int VisualizerLib_Crea…

基于支持向量机、孤立森林和LSTM自编码器的机械状态异常检测(MATLAB R2021B)

异常检测通常是根据已有的观测数据建立正常行为模型&#xff0c;从而将不同机制下产生的远离正常行为的数据划分为异常类&#xff0c;进而实现对异常状态的检测。常用的异常检测方法主要有&#xff1a;统计方法、信息度量方法、谱映射方法、聚类方法、近邻方法和分类方法等。 …

26_嵌入式系统网络接口

以太网接口基本原理 IEEE802标准 局域网标准协议工作在物理层和数据链路层&#xff0c;其将数据链路层又划分为两层&#xff0c;从下到上分别为介质访问控制子层(不同的MAC子层&#xff0c;与具体接入的传输介质相关),逻辑链路控制子层(统一的LLC子层&#xff0c;为上层提供统…

MySQL之备份与恢复(八)

备份与恢复 还原逻辑备份 如果还原的是逻辑备份而不是物理备份&#xff0c;则与使用操作系统简单地复制文件到适当位置的方式不同&#xff0c;需要使用MySQL服务器本身来加载数据到表中。在加载导出文件之前&#xff0c;应该先花一点时间考虑文件有多大&#xff0c;需要多久加…