【因果推斷】優惠券政策對不同店鋪的影響

這次依然是用之前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 Y?T=0)以及對于受到了干預的店鋪如果當初沒有受到干預的話會有多少銷售額(圖中的 Y ? ∣ T = 1 Y^*|T=1 Y?T=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.pswp.cn/web/41527.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/41527.shtml
英文地址,請注明出處:http://en.pswp.cn/web/41527.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SQL Server 2022 中的 Tempdb 性能改進非常顯著

無論是在我的會話中還是在我寫的博客中&#xff0c;Tempdb 始終是我的話題。然而&#xff0c;當談到 SQL Server 2022 中引入的重大性能變化時&#xff0c;我從未如此興奮過。他們解決了我們最大的性能瓶頸之一&#xff0c;即系統頁面閂鎖并發。 在 SQL Server 2019 中&#x…

三級_網絡技術_06_IP地址規劃技術

1.下列對IPv6地址表示中&#xff0c;錯誤的是()。 AE1A:0:0:0:0:A2:F3:FE08:5 E3E0::1A90:FE:0:4CA2:9C5C E2C1::0:0:81/48 :E140:1A5C:0:05D9 2.下列對IPv6地址表示中&#xff0c;錯誤的是()。 1F1A:0:0:0:0:A2:F3:FE08:3 E360::2A90:FE:0:4CA2:9C5A B2C1::0:0:81/48 …

Go語言如何入門,有哪些書推薦?

Go 語言之所以如此受歡迎&#xff0c;其編譯器功不可沒。Go 語言的發展也得益于其編譯速度夠快。 對開發者來說&#xff0c;更快的編譯速度意味著更短的反饋周期。大型的 Go 應用程序總是能在幾秒鐘之 內完成編譯。而當使用 go run編譯和執行小型的 Go 應用程序時&#xff0c;其…

如何利用Github Action實現自動Merge PR

我是螞蟻背大象(Apache EventMesh PMC&Committer)&#xff0c;文章對你有幫助給項目rocketmq-rust star,關注我GitHub:mxsm&#xff0c;文章有不正確的地方請您斧正,創建ISSUE提交PR~謝謝! Emal:mxsmapache.com 1. 引言 GitHub Actions 是 GitHub 提供的一種強大而靈活的自…

(九)Docker 的網絡通信

文章目錄 1、常用模式2、概念了解2.1、Docker 內容器的通信2.2、宿主機和 Docker容器的通信 1、常用模式 直接使用 端口映射&#xff0c;宿主機:宿主機端口->容器 ip:容器端口端口映射宿主機 hosts 映射&#xff0c;首先在宿主機 /etc/hosts文件中增加 宿主機 ip:容器 host…

Mojo: 輕量級Perl框架的魔力

在Perl的豐富生態系統中&#xff0c;Mojolicious&#xff08;簡稱Mojo&#xff09;是一個輕量級的實時Web框架&#xff0c;以其極簡的API和強大的功能而受到開發者的喜愛。Mojo不僅適用于構建高性能的Web應用&#xff0c;還可以用來編寫簡單的腳本和命令行工具。本文將帶你探索…

SSM中小學生信息管理系統 -計算機畢業設計源碼02677

摘要 隨著社會的發展和教育的進步&#xff0c;中小學生信息管理系統成為學校管理的重要工具。本論文旨在基于SSM框架&#xff0c;采用Java編程語言和MySQL數據庫&#xff0c;設計和開發一套高效、可靠的中小學生信息管理系統。中小學生信息管理系統以學生為中心&#xff0c;通過…

赤壁之戰的烽火臺 - 觀察者模式

“當烽火連三月&#xff0c;家書抵萬金&#xff1b;設計模式得其法&#xff0c;千軍如一心。” 在波瀾壯闊的三國歷史長河中&#xff0c;赤壁之戰無疑是一場改變乾坤的重要戰役。而在這場戰役中&#xff0c;一個看似簡單卻至關重要的系統發揮了巨大作用——烽火臺。這個古老的…

UNION、UNION ALL、INTERSECT、MINUS

UNION、UNION ALL、INTERSECT、MINUS&#xff1f; 說明 UNION&#xff1a;對兩個結果集進行并集操作&#xff0c;不包括重復行&#xff0c;同時進行默認規則的排序&#xff1b;IUNION ALL&#xff1a;對兩個結果集進行并集操作&#xff0c;包括重復行&#xff0c;不進行排序&am…

OpenAI的崛起:從夢想到現實

OpenAI的崛起不僅是人工智能領域的重大事件&#xff0c;也是科技史上一個引人注目的篇章。本文將深入探討OpenAI從創立到如今的演變過程&#xff0c;分析其成功的關鍵因素&#xff0c;以及未來的發展方向。 一、OpenAI的初創期&#xff1a;理想主義與混亂并存 OpenAI成立于20…

插入排序——C語言

假設我們現在有一個數組&#xff0c;對它進行排序&#xff0c;插入排序的算法如同它的名字一樣&#xff0c;就是將元素一個一個插入到合適的位置&#xff0c;那么&#xff0c;該如何做呢&#xff1f; 如果我們要從小到大進行排序的話&#xff0c;步驟如下&#xff1a; 1.對于…

區間最值問題-RQM(ST表,線段樹)

1.ST表求解 ST表的實質其實是動態規劃&#xff0c;下面是區間最小的遞歸公式&#xff0c;最大只需將min改成max即可 f[i][j] min(f[i][j - 1], f[i (1 << j - 1)][j - 1]); 二維數組的f[i][j]表示從i開始連續2*j個數的最小/大值。 例如&#xff1a;我們給出一個數組…

uniapp啟動安卓模擬器mumu

mumu模擬器下載 ADB&#xff1a; android debug bridge &#xff0c; 安卓調試橋&#xff0c;是一個多功能的命令行工具&#xff0c;他使你能夠與連接的安卓設備進行交互 # adb連接安卓模擬器 adb connect 127.0.0.1:port # 查看adb設備 adb deviceshubuilderx 有內置的adb&a…

MSPM0G3507——滴答定時器和普通定時

滴答定時器定時&#xff1a;&#xff08;放在主函數即可&#xff09; volatile unsigned int delay_times 0;//搭配滴答定時器實現的精確ms延時 void delay_ms(unsigned int ms) {delay_times ms;while( delay_times ! 0 ); } //滴答定時器中斷 void SysTick_Handler(…

Kubernets Apiserver IP 段變更后的故障處理

集群Service IP 段變更后&#xff08;從 10.96.0.0/16 變為 10.17.0.0/16&#xff09;&#xff0c;導致 kubernetes.default.svc 的ClusterIP IP &#xff08;10.96.0.1&#xff09;和段范圍不一樣&#xff0c;對于這個情況&#xff0c;需要重建該 svc。 重建方法很簡單&#…

Python28-7.4 獨立成分分析ICA分離混合音頻

獨立成分分析&#xff08;Independent Component Analysis&#xff0c;ICA&#xff09;是一種統計與計算技術&#xff0c;主要用于信號分離&#xff0c;即從多種混合信號中提取出獨立的信號源。ICA在處理盲源分離&#xff08;Blind Source Separation&#xff0c;BSS&#xff0…

運維---關于服務治理Nacos的快問快答

問題&#xff1a;在服務治理中&#xff0c;服務提供者、服務消費者和注冊中心分別承擔著怎樣的角色&#xff1f; 回答&#xff1a; 服務提供者主要負責暴露服務接口&#xff0c;以供其他服務進行調用。 服務消費者的職責是調用其他服務所提供的接口。 注冊中心則承擔著記錄…

【機器學習】(基礎篇一) —— 什么是機器學習

什么是機器學習 本系列博客為你從機器學習的介紹開始&#xff0c;使用大量的代碼實戰和驗證&#xff0c;最終幫助你完全掌握什么是機器學習 人工智能、機器學習和深度學習的關系 人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09;&#xff1a;是一門研…

Java多線程不會?一文解決——

方法一 新建類如MyThread繼承Thread類重寫run()方法再通過new MyThread類來新建線程通過start方法啟動新線程 案例&#xff1a; class MyThread extends Thread {public MyThread(String name) {super(name);}Overridepublic void run() {for(int i0;i<10;i){System.out.…