A/B 测试前后的合成控制样本
原文towardsdatascience.com/synthetic-control-sample-for-before-and-after-a-b-test-683bac36ffc1简介A/B 测试非常强大。我喜欢这种实验因为它让我们能够比较结果并确定某物是否比另一物表现更好。A/B 测试有一个特定类型它增加了时间成分即A/B 测试前后的对比。在这个测试中比较的是干预前后的特定主体的状况。让我们将上一句翻译成现实世界的例子。一家公司想知道广告是否会推动销售额增长因此他们可以向处理组展示该广告并将结果与未看到该广告的控制组进行比较。广告前后的差异将表明干预是否有效。现在有时在干预之前无法提前计划并划分控制和处理组。这就是合成控制样本将变得有用的时刻。通过一些统计和机器学习可以模拟如果干预没有发生样本会发生什么。这就是我们将在本文中学习的内容。您可以在下面的帖子中了解更多关于 A/B 测试的信息。我的预处理与后处理测试简易指南问题描述我们正在处理一家零售连锁店的数据集。为了生成它我使用了 Kaggle 上的Rossmann 店铺数据集作为我的基准以了解各店铺的销售分布情况使其更接近现实。此外我选择了与测试店铺相似的对照店铺。因此数据是完全重新创建和修改的具有不同的店铺编号和销售额。变量包括店铺编号从 1 到 10。测试店铺是**#10**。日期2013 年至 2015 年的每日日期。销售额该日期的销售额美元。干预一位竞争对手在 2014 年 3 月在我们店铺#10 旁边开设了一家新店。控制店铺1-9周围没有竞争对手。我们意图了解这位竞争对手对店铺#10 销售额造成的影响以便采取任何必要的行动并应对竞争如果适用的话。从控制和处理店铺的销售趋势图中我们得到了下一个图。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b4948144978953f513261cd49842828f.png控制店铺销售与处理店铺销售对比。图片由作者提供。注意控制存储仍然保持稳定的模式甚至略有增长。另一方面处理存储在竞争对手开设后大幅下降。这是否与竞争有关让我们继续挖掘。合成控制样本我们刚刚看到在处理事件2014 年 3 月开设竞争对手之后测试商店 #10 的销售额有相当大的下降。如果我们获取数据并计算干预日期之前的平均每周销售额为 68,199.28。干预后51,283.35。平均减少率约为 25%。现在我们可以获取控制存储并将它们的性能与测试存储进行比较就像一个常规的之前与之后 A/B 测试一样。毕竟我在创建这个数据框时预先选择了类似的控制和测试存储。然而我们的意图是将商店 #10 的当前表现与一个假设的商店 #10如果没有开设竞争的表现进行比较。为了实现这一点我们需要模拟一个没有竞争的表现因此我们使用控制存储的数据。让我们思考一分钟控制存储周围没有竞争所以假设商店 #10 在 2014 年 3 月之后没有竞争的情况下会有类似的行为。之前与之后 A/B 测试是对同一主题两个时间点的比较。组 A 之前与组 A 之后。组 B 之前与组 B 之后。我们有组 B 之前和之后这是有竞争的测试商店 #10正如其名。我们需要一个没有竞争开设的商店 #10 行为的模拟。那么这就是我们要做的事情使用线性回归为商店 #10 创建一个无竞争的合成销售系列。这个合成样本是在控制存储上拟合的——这也是为什么控制存储必须与我们愿意模拟的任何东西相似的原因。让我们这样做。代码本练习中使用的库和模块。# Data manipulationimportpandasaspdimportnumpyasnp# DataVizimportmatplotlib.pyplotaspltimportseabornassns sns.set()# Statsimportscipy.statsasscsimportpingouinaspg# Modelingfromsklearn.linear_modelimportLinearRegressionfromsklearn.metricsimportmean_absolute_percentage_error我们开始分离控制和测试存储。# Select the control groupcontroldf2.query(Store ! 10)# Treatment Grouptreatdf2.query(Store 10)接下来我们必须将我们的control数据从每日销售重新采样为每周销售每 7 天求和。在这里我们已经有测试存储的按日期销售系列——y_sales因为它是单个商店。# Create dataset of control stores sales resampled from daily to weekly (7 days) salesunit_controlcontrol.pivot(indexDate,columnsStore,valuesSales).resample(7D).sum()# Agregating sales for Store #10 by each 7 daysy_salestreat.set_index(Date)[Sales].resample(7D).sum()然后我们将对控制存储的日期进行筛选与测试存储可用的相同日期进行筛选使两个数据集对齐。如果任何值是NA我们将用中位数填充它然后取所有控制存储的平均值将数据转换为按日期索引的销售系列。# Filter the control sample with only the dates contained in the treatment datasetaligned_datesunit_control.index.intersection(y_sales.index)# Get the control data filtered with dates from the treatment seriesX_salesunit_control.loc[aligned_dates].fillna(unit_control.median())# Transform the control data into a single series aggregated as the mean of sales by weekX_sales_meanX_sales.mean(axis1)这是X_sales_mean数据的视图。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/11180ce1c8957344ba471cfcc1a13e26.png按日期索引的控制存储销售系列。图片由作者提供。让我们绘制两个系列y_sales和X_sales_mean。# Set Date of the interventionintervention_datepd.to_datetime(2014-03-01)# Plotplt.figure(figsize(12,6))plt.plot(y_sales.index,y_sales,labelStore 10,colorgreen,lw2)plt.plot(X_sales_mean.index,X_sales_mean,labelNo Comp Stores,colorgray,linestyle--)plt.axvline(xintervention_date,colorred,linestyle:,lw2,labelOpened Competitor for Store 10 )plt.xlabel(D A T E)plt.ylabel(S A L E S)plt.title(Sales GAP Store 10 vs Stores W/o Competition)plt.legend()plt.show()https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/145e260c18c0ccb5c5b94e33c0b314f6.png店铺#10 与控制店铺的平均值无竞争。图由作者提供。我们可以观察到在竞争开始之前店铺#10 的结果始终高于控制店铺的平均值。但是一旦竞争对手开业销售额就持续低于控制店铺的平均值。现在我们应该拟合回归模型来创建无竞争的合成店铺#10。X: 控制店铺的销售量将是预测变量y: 测试店铺的销售量将是目标。X_before和y_before是训练集。X_after和y_after是测试集。# X amp; y setsX_beforeX_sales[:intervention_date]X_afterX_sales[intervention_date:]y_beforey_sales[:intervention_date]y_aftery_sales[intervention_date:]# Linear Regression fitlmLinearRegression(fit_interceptFalse)lm.fit(X_before,y_before)# Predictions before and after interventionpreds_beforelm.predict(X_before)preds_afterlm.predict(X_after)# Synthetic Control Seriessynthetic_seriespd.Series(np.concatenate([preds_before,preds_after]),indexX_sales.index)如果我们再次绘制这两个系列我们是在比较有竞争的店铺#10 与无竞争的店铺#10合成样本。# Plot Synthetic (No competition) vs 10plt.figure(figsize(12,6))plt.plot(y_sales.index,y_sales,labelStore 10,colorgreen,lw2)plt.plot(synthetic_series.index,synthetic_series,label10 W/O Competition,colorgray,linestyle--)plt.axvline(xintervention_date,colorred,linestyle:,lw2,labelOpened Competitor for Store 10 )plt.xlabel(D A T E)plt.ylabel(S A L E S)plt.title(Comparison: Store 10 vs Synthetic 10 No Comp)plt.legend()plt.show()这是图形。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b56008487155f5a442c81fc498e12c09.png实际店铺#10 与无竞争的合成店铺#10 对比。图由作者提供。我们可以注意到在干预日期之前该系列的前一部分拟合度相当好然后线条之间出现了一个相当大的差距其中实际店铺#10 的表现低于合成对这让我们得出结论性能下降的原因实际上是由于竞争的开放。为了量化这个差距让我们计算预测值与实际数据之间的平均绝对百分比误差MAPE。# MAPE Store 10 versus Synthetic Store 10mape_aftermean_absolute_percentage_error(y_after,preds_after)print(fMAPE after intervention:{mape_after:.2f})---------OUT-----------MAPE after intervention:0.32计算结果表明在竞争对手开业后店铺#10 的销售额下降了 32%。让我们更进一步使用控制店铺和测试店铺的实际值进行前后 A/B 测试这样我们可以检查结果是否一致。前后 A/B 测试https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/be43ad345531d6efc473823e79f8a3bc.pngA/B 测试 | 由 AI 生成的图像。Meta Llama2024。meta.ai前后测试比较了控制店铺与干预前后的测试店铺的性能即 10 号店铺附近竞争对手的开放。让我们设定测试的干预日期。# Intervention dateintervention_datepd.to_datetime(2014-03-01)接下来我们必须将我们的数据从每日销售重新采样到按 7 天聚合的销售。df_sales7d(df2.pivot(indexDate,columnsStore,valuesSales)#pivot for resampling.resample(7D)#aggregate sales by 7 days.sum()#sum sales.reset_index()#reset to make Date as column again.melt(id_varsDate,var_nameStore,value_nameSales)# unpivot.assign(afterlambdax:np.select([x.Dateintervention_date,x.Dateintervention_date],[0,1]))# add after Yes or No.assign(grouplambdax:np.select([x.Store10,x.Store!10],[treatment,control]))# add group treatment or control)# Viewdf_sales7d.head(2)这里是数据的视图。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/237259c31a8e2184e780a4a0b6a69583.png销售数据聚合 7 天。图由作者提供。接下来我们计算样本的平均值和标准误差。# Calculating averages and standard errorsab_means(df_sales7d.groupby([group,after]).agg({Sales:[mean,std]}).round(2))https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/de3fcb16a51cb954d9ce27ee5b423ee1.png样本的平均值和标准误差。图由作者提供。向前推进我们必须定义几个函数std_error_two_samples: 此函数计算控制样本和测试样本的单个标准误差。这对于计算前后组之间的置信区间将非常重要。ab_test一个执行 A/B 测试并计算组间差异置信区间的函数。注意这两个函数都可以在 GitHub 仓库中找到见此处。print(Control Before vs After:)ab_test(datadf_sales7d.query(group control).rename(columns{group:grp,after:group}),group_colgroup,target_colSales)print(---------------------------------------------------------------------)print(Treatment Before vs After:)ab_test(datadf_sales7d.query(group treatment).rename(columns{group:grp,after:group}),group_colgroup,target_colSales)结果如下Control Before vs After:The calculated standard erroris1635.206108090877The differenceinmeans Group B-A:1118.971107665042P-Value(two tails):0.49378591381056647confidence intervalwith95%confidenceis[-2085.974323.92]---------------------------------------------------------------------Treatment Before vs After:The calculated standard erroris1695.1522766982898The differenceinmeans Group B-A:-16915.930695613657P-Value(two tails):0.0confidence intervalwith95%confidenceis[-20238.37-13593.49]对于控制样本在 5% 的显著性水平下我们不能拒绝均值相等的零假设。因此商店在两个时期内统计上表现相似。两组样本均值之间的差异将在每周 -2,085 美元到 4,323 美元之间波动。对于测试商店 #10p 值表明样本均值在统计上存在差异。因此该商店受到了竞争的影响每周销售额下降了约 17k-25%这个差异从 -20k-29%到 -13.5k-19%不等这两个数字都远低于零。但是嘿…我是一个视觉型的人。我喜欢通过图形看到差异。plt.figure(figsize(18,5))# Control Samples Before vs. After# Creating Normal Distribution of both Control samplesplt.subplot(1,2,1)plot_anp.random.normal(locab_means.iloc[0,0],scaleab_means.iloc[0,1],size10000)plot_bnp.random.normal(locab_means.iloc[1,0],scaleab_means.iloc[1,1],size10000)plotpd.DataFrame({group:[Control_Before]*10000[Control After]*10000,mu:np.concatenate([plot_a,plot_b])})# Intervention date lineplt.axvline(xab_means.iloc[0,0],colorroyalblue,linestyle--,lw1)plt.axvline(xab_means.iloc[1,0],colordarkorange,lw1)sns.kdeplot(plot,xmu,huegroup)plt.title(Control Before vs After);plt.subplot(1,2,2)# Treatment Samples Before vs. After# Creating Normal Distribution of both Treatment samplesplot_anp.random.normal(locab_means.iloc[2,0],scaleab_means.iloc[2,1],size10000)plot_bnp.random.normal(locab_means.iloc[3,0],scaleab_means.iloc[3,1],size10000)plotpd.DataFrame({group:[Treatment Before]*10000[Treatment After]*10000,mu:np.concatenate([plot_a,plot_b])})# Intervention date lineplt.axvline(xab_means.iloc[2,0],colorroyalblue,linestyle--,lw1)plt.axvline(xab_means.iloc[3,0],colordarkorange,lw1)sns.kdeplot(plot,xmu,huegroup)plt.title(Treatment Before vs After);现在就是这里。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3b48867680428aef224a258d594044bd.png比较前后 A/B 测试 | 控制组 vs 处理组样本。图片由作者提供。我认为我们已经消除了关于新竞争对手对销售影响的任何疑虑。在你离开之前因果推断最近一直在增长。关于这个主题的新内容越来越常见。毕竟正如人们所说“相关性不等于因果性”所以当我们被客户或工作中的领导询问某个效果的影响或原因时我们必须有工具来评估这一点。一些不错的 Python 包如DoWhy、Causal Impact可以帮助我们从结果中推断因果关系。在本文中我们进行的练习与包causalimpact所做的工作非常相似。如果我们对其运行一些代码现在的结果应该对你来说很熟悉了。# !pip install causalimpact --quietfromcausalimpactimportCausalImpact# Create dataset with mean of the control stores (x) and the test store (y)df4(pd.concat([X_sales_mean.round(2),y_sales],axis1).rename(columns{0:x,Sales:y}).reindex(columns[y,x]))# Run Causal ImpactimpactCausalImpact(datadf4,pre_period[pd.to_datetime(2013-01-01),pd.to_datetime(2014-03-04)],post_period[pd.to_datetime(2014-03-11),pd.to_datetime(2015-07-28)])impact.run()# Plot resultimpact.plot()# Print Summaryimpact.summary()# Prin Reportimpact.summary(outputreport)这是结果。与我们的先前比较非常相似。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/eb8eefc7ee3911dcb4bd12632197dabe.png因果影响库输出。图片由作者提供。我推荐你看看。但要注意与 Pandas 更新版本的一些冲突。那就结束了。如果你喜欢这个内容请关注我获取更多。关注我 | 博客 网站Gustavo R Santos – Medium古斯塔沃·R·桑托斯完整代码 GitHubBefore-and-After-Testing/Python/Causal Inference at main · gurezende/Before-and-After-Testing参考文献Controle Sintético: Como Aplicar Inferência Causal na Prática4 Python Packages to Learn Causal AnalysisNotebook on nbviewer