R-和-Python-行为数据分析-全-

R 和 Python 行为数据分析(全)

原文:zh.annas-archive.org/md5/350d206394c314ed5702cafaf048e5be

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

统计学是一个用途惊人但实际有效的从业者很少的学科。

Bradley Efron 和 R. J. Tibshirani,《自举法入门》(1993 年)

欢迎来到用 R 和 Python 进行行为数据分析!我们生活在数据时代已成陈词滥调。工程师现在常常利用机器和涡轮机上的传感器数据来预测它们的故障时间并进行预防性维护。同样地,营销人员利用大量数据,从你的人口统计信息到过去的购买记录,来确定何时向你展示哪些广告。正如一句俗语所言,“数据是新石油”,而算法是推动我们经济前进的新型燃烧引擎。

大多数关于分析、机器学习和数据科学的书籍都暗示工程师和营销人员试图解决的问题可以用相同的方法和工具处理。当然,变量有不同的名称,并且需要一些特定领域的知识,但是 k 均值聚类就是 k 均值聚类,无论是在涡轮机数据还是社交媒体帖子数据上。通过这种方式全盘采纳机器学习工具,企业通常能够准确预测行为,但却失去了对实际情况更深入和丰富的理解。这导致了数据科学模型被批评为“黑匣子”的现象。

本书不是追求准确但不透明的预测,而是努力回答这样一个问题:“是什么驱动了行为?”如果我们决定给潜在客户发送电子邮件,他们会因为电子邮件而订阅我们的服务吗?哪些客户群体应该收到电子邮件?年长客户是否因为年龄较大而倾向于购买不同的产品?客户体验对忠诚度和保留率的影响是什么?通过改变我们的视角,从预测行为转向解释行为并测量其原因,我们将能够打破“相关不等于因果”的诅咒,这一诅咒阻碍了几代分析师对他们模型结果的自信。

这种转变不会因引入新的分析工具而来:我们将只使用两种数据分析工具:传统的线性回归及其逻辑回归衍生物。这两种模型本质上更易读取,尽管通常以较低的预测准确度为代价(即在预测中产生更多且更大的误差),但对于我们在这里测量变量之间关系的目的来说并不重要。

相反,我们将花费大量时间学习如何理解数据。在我的数据科学面试官角色中,我见过许多候选人可以使用复杂的机器学习算法,但对数据的实际理解却不深:除了算法告诉他们的内容,他们对数据的运作几乎没有直观的感觉。

我相信你可以培养出这种直觉,并在这个过程中通过采用以下方法极大地增加你的分析项目的价值和成果:

  • 一种行为科学的思维方式,将数据视为了解人类心理和行为的一种视角,而非终点。

  • 一套因果分析工具包,让我们能够自信地说一件事情导致另一件事情,并确定这种关系有多强。

虽然这些方法各自都能带来巨大的收益,但我认为它们是天然的互补,最好一起使用。鉴于“使用因果分析工具包的行为科学思维方式”有点啰嗦,我将其称为因果行为方法或框架。这个框架有一个额外的好处:它同样适用于实验数据和历史数据,同时利用它们之间的差异。这与传统的分析方法形成对比,传统方法使用完全不同的工具处理它们(例如,实验数据使用 ANOVA 和 T 检验);数据科学则不会区别对待实验数据和历史数据。

本书适合人群

如果你在使用 R 或 Python 分析业务数据,那么这本书适合你。我用“业务”这个词比较宽泛,指的是任何以正确的洞察力和可操作的结论推动行动为重点的盈利、非盈利或政府组织。

在数学和统计背景方面,无论你是业务分析师制定月度预测,还是 UX 研究员研究点击行为,或者是数据科学家构建机器学习模型,都无关紧要。这本书有一个基本要求:你需要至少对线性回归和逻辑回归有些许了解。如果你理解回归,你就能跟上本书的论点,并从中获得巨大的收益。另一方面,我相信,即使是具有统计学或计算机科学博士学位的专家数据科学家,如果他们不是行为或因果分析的专家,也会发现这些材料是新的和有用的。

在编程背景方面,你需要能够阅读和编写 R 或 Python 代码,最好两者都会。我不会教你如何定义函数或如何操作数据结构,比如数据框或 pandas。已经有很多优秀的书在做这方面的工作,比我做得更好(例如,Python 数据分析 作者 Wes McKinney(O'Reilly)和 R 数据科学 作者 Garrett Grolemund 和 Hadley Wickham(O'Reilly))。如果你读过这些书,参加过入门课程,或者在工作中至少使用过其中一种语言,那么你就有能力学习这里的内容。同样地,我通常不会展示和讨论书中用于创建众多图表的代码,尽管它将出现在书的 GitHub 中。

本书不适合人群

如果你在学术界或需要遵循学术规范(例如,制药试验)的领域,这本书可能仍然对你有兴趣,但我描述的方法可能会让你与你的导师/编辑/经理产生冲突。

本书概述传统行为数据分析方法,比如 T 检验或方差分析(ANOVA)。我还没有遇到过回归比这些方法在回答业务问题上更有效的情况,这就是为什么我故意将本书限制在线性和逻辑回归上的原因。如果你想学习其他方法,你需要去别处寻找(例如,使用 Scikit-Learn、Keras 和 TensorFlow 进行机器学习实践(O'Reilly)由 Aurélien Géron 撰写的机器学习算法)。

在应用设置中理解和改变行为需要数据分析和定性技能两者。本书主要集中在前者,主要出于空间考虑。此外,已经有许多优秀的书籍涵盖了后者,比如理查德·塞勒斯坦(Richard Thaler)和卡斯·桑斯坦(Cass Sunstein)的《推动:改善有关健康、财富和幸福的决策》(Penguin)以及斯蒂芬·温德尔(Stephen Wendel)的为行为变革设计:应用心理学和行为经济学(O'Reilly)。尽管如此,我将介绍行为科学的概念,以便你即使对这个领域还不熟悉,也能应用本书中的工具。

最后,如果你完全是新手对 R 或 Python 的数据分析,这本书不适合你。我建议你从一些优秀的介绍书籍开始,比如本节中提到的一些书籍。

R 和 Python 代码

为什么要用 R Python?为什么不用两者中更优秀的那个?“R 与 Python”的辩论在互联网上仍然存在,并且如我所知,大多数时候这种争论是无关紧要的。事实是,你将不得不使用你的组织中使用的任何一种语言。我曾在一家医疗公司工作过,由于历史和法规的原因,SAS 是主导语言。我经常使用 R 和 Python 进行自己的分析,但由于无法避免处理遗留的 SAS 代码,我在那里的第一个月就学会了我需要的 SAS 的基础知识。除非你的整个职业生涯都在一家不使用 R 或 Python 的公司,否则你最终可能会掌握这两者的基础知识,所以你最好接受双语能力。我还没有遇到过任何人说“学习阅读[另一种语言]的代码是浪费时间”。

假设你很幸运地在一个同时使用这两种语言的组织中工作,你应该选择哪种语言呢?我认为这实际取决于你的环境和你需要做的任务。例如,我个人更喜欢用 R 进行探索性数据分析(EDA),但我发现 Python 在网页抓取方面更容易使用。我建议根据你工作的具体情况选择,并依赖最新的信息:两种语言都在不断改进,过去某个版本的 R 或 Python 可能不适用于当前版本。例如,Python 正在成为比以前更友好的 EDA 环境。你的精力最好花在学习这两种语言上,而不是在论坛上寻找哪种更好。

代码环境

在每章的开头,我将指出需要专门加载的 R 和 Python 包。此外,整本书我还会使用一些标准包;为避免重复,它们只在这里提及(它们已经包含在 GitHub 上所有脚本中)。你应该始终从它们开始编写你的代码,以及一些参数设置:

## R
library(tidyverse)
library(boot) #Required for Bootstrap simulations
library(rstudioapi) #To load data from local folder
library(ggpubr) #To generate multi-plots

# Setting the random seed will ensure reproducibility of random numbers
set.seed(1234)
# I personally find the default scientific number notation (i.e. with 
# exponents) less readable in results, so I cancel it 
options(scipen=10)

## Python
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
from statsmodels.formula.api import ols
import matplotlib.pyplot as plt # For graphics
import seaborn as sns # For graphics

代码约定

我在 RStudio 中使用 R。R 4.0 在我写这本书的时候发布了,我已经采用了它,以尽可能保持书籍的更新。

R 代码是用代码字体编写的,并带有指示使用的语言的注释,就像这样:

## R
> x <- 3
> x
[1] 3

我在 Anaconda 的 Spyder 中使用 Python。关于 “Python 2.0 vs. 3.0” 的讨论希望现在已经结束了(至少对于新代码来说;旧代码可能是另一回事),我将使用 Python 3.7。Python 代码的约定与 R 类似:

## Python
In [1]: x = 3
In [2]: x
Out[2]: 3

我们经常会查看回归的输出。这些输出可能非常冗长,并包含了很多本书论点无关的诊断信息。在现实生活中,你不应忽略它们,但这是其他书更好涵盖的问题。因此,我会像这样缩写输出:

## R
> model1 <- lm(icecream_sales ~ temps, data=stand_dat)
> summary(model1)

...
Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -4519.055    454.566  -9.941   <2e-16 ***
temps        1145.320      7.826 146.348   <2e-16 ***
...

## Python 
model1 = ols("icecream_sales ~ temps", data=stand_data_df)
print(model1.fit().summary())

...     
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept  -4519.0554    454.566     -9.941      0.000   -5410.439   -3627.672
temps       1145.3197      7.826    146.348      0.000    1129.973    1160.666
...

函数式编程入门

作为程序员从初学者到中级水平的其中一步,就是停止编写代码的长串指令脚本,而是将代码结构化为函数。在本书中,我们将跨章节编写和重复使用函数,例如以下内容来构建 Bootstrap 置信区间:

## R
boot_CI_fun <- function(dat, metric_fun, B=20, conf.level=0.9){

  boot_vec <- sapply(1:B, function(x){
    cat("bootstrap iteration ", x, "\n")
    metric_fun(slice_sample(dat, n = nrow(dat), replace = TRUE))})
  boot_vec <- sort(boot_vec, decreasing = FALSE)
  offset = round(B * (1 - conf.level) / 2)
  CI <- c(boot_vec[offset], boot_vec[B+1-offset])
  return(CI)
}

## Python 
def boot_CI_fun(dat_df, metric_fun, B = 20, conf_level = 9/10):

  coeff_boot = []

  # Calculate coeff of interest for each simulation
  for b in range(B):
      print("beginning iteration number " + str(b) + "\n")
      boot_df = dat_df.groupby("rep_ID").sample(n=1200, replace=True)
      coeff = metric_fun(boot_df)
      coeff_boot.append(coeff)

  # Extract confidence interval
  coeff_boot.sort()
  offset = round(B * (1 - conf_level) / 2)
  CI = [coeff_boot[offset], coeff_boot[-(offset+1)]]

  return CI

函数还有一个额外的优势,即限制理解上的溢出:即使你不理解前面的函数如何工作,你仍然可以认为它们返回置信区间并遵循其余推理的逻辑,推迟到以后深入了解它们的代码。

使用代码示例

附加材料(代码示例等)可在 https://oreil.ly/BehavioralDataAnalysis 下载。

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至 bookquestions@oreilly.com

本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在自己的程序和文档中使用它。除非您复制了代码的大部分,否则无需联系我们寻求许可。例如,编写一个使用本书多个代码片段的程序并不需要许可。销售或分发 O'Reilly 书籍中的示例需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书中大量的示例代码整合到您产品的文档中需要许可。

我们感谢但不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“R 和 Python 的行为数据分析,作者 Florent Buisson(O’Reilly)。版权 2021 Florent Buisson,978-1-492-06137-3。”

如果您觉得您使用的代码示例超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com联系我们。

导航本书

本书的核心思想是,有效的数据分析依赖于数据、推理和模型之间的不断交流。

  • 真实世界中的实际行为及相关的心理现象,如意图、思想和情绪

  • 因果分析,特别是因果图

  • 数据

本书分为五个部分:

第一部分,理解行为

本部分通过因果行为框架和行为、因果推理和数据之间的联系来铺设舞台。

第二部分,因果图与去混淆

这部分介绍了混淆概念,并解释了因果图如何帮助我们解决数据分析中的混淆问题。

第三部分,鲁棒数据分析

在这里,我们探讨了处理缺失数据的工具,并介绍了 Bootstrap 模拟,因为在本书的其余部分中我们将广泛依赖 Bootstrap 置信区间。小型、不完整或形状不规则的数据(例如具有多个高峰或异常值的数据)并非新问题,但在行为数据中可能尤为突出。

第四部分,设计和分析实验

在这一部分,我们将讨论如何设计和分析实验。

第五部分,行为数据分析的高级工具

最后,我们综合一切来探讨中介效应、调节效应和工具变量。

本书的各个部分在一定程度上相互依赖,因此我建议至少在第一遍阅读时按顺序阅读它们。

本书使用的惯例

本书中使用以下排版惯例:

斜体

表示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序清单,以及在段落中引用程序元素,如变量或函数名称,数据库,数据类型,环境变量,语句和关键字。

等宽粗体

显示用户应该按原样键入的命令或其他文本。

等宽斜体

显示应由用户提供值或由上下文确定值的文本。

提示

这个元素表示提示或建议。

注意

这个元素表示一般性说明。

警告

这个元素指示警告或注意事项。

奥莱利在线学习

注意

40 多年来,奥莱利媒体一直提供技术和商业培训,知识和见解,帮助公司取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。奥莱利的在线学习平台让您随需应变地访问直播培训课程、深度学习路径、交互式编码环境,以及奥莱利和其他 200 多家出版商的大量文本和视频内容。更多信息,请访问http://oreilly.com

如何联系我们

请将有关本书的评论和问题发送至出版商:

  • 奥莱利媒体,公司

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

您可以访问本书的网页,其中列出了勘误表,示例和附加信息,网址为https://oreil.ly/Behavioral_Data_Analysis_with_R_and_Python

发送电子邮件至bookquestions@oreilly.com以评论或询问关于本书的技术问题。

有关我们的书籍和课程的新闻和信息,请访问http://oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://youtube.com/oreillymedia

致谢

作者们经常感谢他们的配偶对他们的耐心,并特别感谢那些有深刻见解的审阅者。我很幸运,这两者都集中在同一个人身上。我想没有其他人敢于或能够如此多次地把我送回绘图板,而这本书因此而大为改进。因此,我首先感谢我的生活和思想伴侣。

我的几位同事和行为科学家朋友慷慨地抽出时间阅读和评论了早期草稿。这本书因此变得更加出色。谢谢(按字母顺序逆序排列)Jean Utke、Jessica Jakubowski、Chinmaya Gupta 和 Phaedra Daipha!

特别感谢 Bethany Winkel 在写作中的帮助。

现在,我对最初的草稿有多么粗糙和令人困惑感到后悔。我的开发编辑和技术审阅人员耐心地推动我一路走到现在这本书的成就,分享他们丰富的视角和专业知识。感谢 Gary O’Brien,感谢 Xuan Yin,Shannon White,Jason Stanley,Matt LeMay 和 Andreas Kaltenbrunner。

第一部分:理解行为

本书的第一部分将解释为什么行为数据分析需要新的方法。

第一章将描述一种新的方法,即因果行为数据分析框架。我们将通过一个具体示例展示,即使是最简单的数据分析也可能因混杂变量的存在而偏离轨道。解决这个问题在传统方法中可能极为复杂,甚至不可能,但新框架提供了一个简单的解决方案。

第二章将进一步探讨行为数据的具体特性,同时为行为科学提供一个初步介绍,并确保我们的数据能够充分反映相应的真实行为。

第一章:数据分析的因果行为框架

正如我们在前言中讨论的那样,理解驱动行为以便改变它们是应用分析的关键目标之一,无论是在商业、非营利组织还是公共组织中。我们想弄清楚为什么有人买了某样东西,为什么另一个人没有买。我们想知道为什么有人续订了他们的订阅,为什么有人选择联系客服而不是在线支付,为什么有人注册成为器官捐赠者,或者为什么有人给予非营利组织捐款。拥有这些知识使我们能够预测人们在不同情境下的行为,并帮助我们确定我们的组织可以采取什么措施来鼓励他们再次这样做(或者不这样做)。我认为通过将数据分析与行为科学思维和因果分析工具包相结合,可以最好地实现这一目标,从而创建一个被我称为“因果行为框架”的集成方法。在这个框架中,行为位于顶部,因为理解它们是我们的最终目标。通过使用因果图数据来实现这种理解,它们形成了三角形的两个支柱(图 1-1)。

数据分析的因果行为框架

图 1-1. 数据分析的因果行为框架

在本书的过程中,我们将探索三角形的每个支柱,并看到它们如何相互联系。在最后一章中,我们将通过一行代码实现我们所有工作的结合,而传统方法则会面临艰巨的任务:衡量客户满意度提高未来客户支出的程度。除了执行这些非凡的任务外,这种新框架还将使您能够更有效地执行诸如确定电子邮件营销活动或产品特性对购买行为的影响等常见分析。

在深入讨论之前,熟悉预测分析的读者可能会想知道为什么我推崇因果分析而不是预测分析。答案是,尽管预测分析在业务环境中已经(并将继续)非常成功,但在涉及人类行为的分析中,它们可能存在不足。特别是,采用因果方法可以帮助我们识别和解决“混杂”的问题,这在行为数据中非常常见。我将在本书的其余部分详细阐述这些观点。

为什么我们需要因果分析来解释人类行为

理解因果分析在分析领域中的位置将帮助我们更好地识别为何它在业务环境中是必需的。正如我们将看到的那样,这种需求源于人类行为的复杂性。

不同类型的分析

有三种不同类型的分析:描述性、预测性和因果性。描述性分析提供了数据的描述。简单来说,我把它看作是“是什么”或“我们已经测量到了什么”的分析。业务报告属于这一范畴。上个月有多少客户取消了他们的订阅?去年我们赚了多少利润?每当我们在计算平均数或其他简单指标时,我们都在隐含地使用描述性分析。描述性分析是最简单的分析形式,但也往往被人低估。许多组织实际上很难获得对其业务的清晰和统一的视图。要了解组织中这个问题的程度,只需向财务部门和运营部门提出同样的问题,然后衡量答案的差异有多大。

预测性分析提供了一个预测。我把它看作是“如果当前条件持续下去会怎样”或“我们还没有测量到的内容”的分析。大多数机器学习方法(例如,神经网络和梯度提升模型)属于这种类型的分析,并帮助我们回答诸如“下个月有多少客户会取消订阅?”和“那个订单是欺诈的吗?”等问题。在过去的几十年里,预测性分析已经改变了世界;企业雇佣的大量数据科学家就是其成功的证明。

最后,因果分析提供了数据的原因。我把它看作是“如果……会怎样?”或“在不同条件下将会怎样”的分析。它回答了诸如“下个月有多少客户会取消订阅,除非我们给他们发优惠券?”这样的问题。因果分析最著名的工具是 A/B 测试,又称随机实验或随机对照试验(RCT)。这是因为回答上述问题的最简单且最有效的方法是向随机选定的一组客户发送优惠券,然后查看与对照组相比有多少客户取消了订阅。

我们将在书的第四部分中讨论实验,但在此之前,在第二部分中,我们将看一看那个工具箱中的另一个工具,即因果图,它甚至可以在我们无法进行实验时使用。事实上,我一个目标就是让您更广泛地思考因果分析,而不仅仅是把它等同于实验。

注意

虽然这些标签可能给人以整洁分类的印象,但实际上,在这三个类别之间存在更多的渐变,问题和方法在它们之间变得模糊。您还可能遇到其他术语,如规范性分析,它进一步模糊了界限,并添加了其他细微差别,但并没有显著改变整体情况。

人类是复杂的

如果预测性分析如此成功,而因果性分析使用的是与回归分析相同的数据分析工具,为什么不坚持使用预测性分析呢?简而言之,因为人类比风力发电机更复杂。人类行为:

具有多重原因

风力发电机的行为不受其个性,风力发电机社区的社会规范或其成长环境的影响,而任何单一变量对人类行为的预测能力几乎总是令人失望的,因为存在这些因素。

具有上下文相关性

对环境进行微小或表面的改变,比如改变选择的默认选项,可能会对行为产生很大的影响。从行为设计的角度来看,这是一种福祉,因为它使我们能够推动行为的变化,但从行为分析的角度来看,这是一种诅咒,因为这意味着每种情况在难以预测的方面都是独特的。

具有变量性(科学家会说是非确定性的)

同一个人在看似完全相同的情况下反复出现可能会表现出截然不同的行为,即使在控制了表面因素后也是如此。这可能是由于暂时的效应,比如情绪,或者长期效应,比如厌倦每天吃相同早餐。这两者都可以显著改变行为,但很难捕捉到。

具有创新性

当环境条件发生变化时,一个人可以转向一个他们以前从未表现过的行为,而且即使在最平凡的情况下也会发生这种情况。例如,你平时通勤的路上发生了一起车祸,所以你在最后一刻决定右转。

具有战略性

人类推断并对他人的行为和意图做出反应。在某些情况下,这可能意味着通过外部环境改变而被打乱的合作“修复”,使其更加牢固可预测。但在其他情况下,它可能涉及自愿模糊自己的行为,使其在像下棋这样的竞争性游戏中变得不可预测(或者欺诈!)。

人类行为的所有这些方面使其比物理物体的行为更不可预测。为了找到更可靠的分析规律,我们必须深入了解和衡量行为的原因。某人星期一早餐吃燕麦粥并采取了某条路线并不意味着他们星期二会做同样的事情,但您可以更有信心地说他们会有一些早餐,并会采取某条路线去上班。

混淆!让回归分析排除隐患的隐藏危险

我在前一节中提到,因果分析通常使用与预测分析相同的工具。然而,由于它们有不同的目标,这些工具的使用方式也不同。由于回归分析是这两种类型分析的主要工具之一,它可以很好地说明预测分析和因果分析之间的差异。对于预测分析适用的回归分析往往会对因果分析目的来说效果不佳,反之亦然。

预测分析的回归分析用于估计未知值(通常但不总是在未来)。它通过获取已知信息并使用各种因素来三角测量给定变量的最佳猜测值。重要的是预测值及其准确性,而不是预测是如何或为什么进行的。

因果分析也使用回归分析,但重点不在于估计目标变量的值。相反,重点在于该值的原因。从回归分析的角度来看,我们的兴趣不再是依赖变量本身,而是它与给定独立变量的关系。通过正确构建的回归分析,相关系数可以成为研究独立变量对依赖变量因果效应的便携式度量。

但是,对于此目的正确构建的回归分析意味着什么呢?为什么我们不能只是采用我们已经用于预测分析的回归分析,并将提供的系数视为因果关系的度量?我们不能这样做,因为回归分析中的每个变量都有可能修改其他变量的系数。因此,我们的变量组合必须经过精心设计,以不是创建最准确的预测,而是创建最准确的系数。两组变量通常是不同的,因为一个变量可以与我们的目标变量高度相关(因此具有高预测能力),而实际上不影响该变量。

在本节中,我们将探讨为什么这种视角上的差异很重要,以及为什么变量选择在行为分析中至关重要。我们将通过来自美国各地门店的虚构超市连锁企业 C-Mart 的一个具体例子来说明这一点。本书中使用的两家虚构公司之一,C-Mart 将帮助我们理解数字时代实体店公司数据分析的机遇和挑战。

数据

此章节的 GitHub 文件夹 包含两个 CSV 文件,chap1-stand_data.csvchap1-survey_data.csv,分别是本章两个例子的数据集。

Table 1-1 展示了关于 C-Mart 摊位上冰淇淋和冰咖啡销售日常水平的 chap1-stand_data.csv 文件中的信息。

表 1-1. chap1-stand_data.csv 中的销售信息

变量名称 变量描述
冰淇淋销售额 C-Mart 摊位的每日冰淇淋销售额
冰咖啡销售额 C-Mart 摊位的日销量
夏季月份 表示日期是否在夏季月份的二进制变量
温度 当天和该摊位的平均温度

表 1-2 显示了来自 chap1-survey_data.csv 的路人通过 C-Mart 摊位调查的信息。

表 1-2. 第一章调查数据在 chap1-survey_data.csv 中

变量名称 变量描述
香草口味 受访者对香草的口味偏好,0-25
巧克力口味 受访者对巧克力的口味偏好,0-25
购物 表示受访者是否曾在当地 C-Mart 摊位购物的二进制变量

为什么相关性不是因果关系:混杂因素的作用

C-Mart 在每家店铺都设有冰淇淋摊位。公司认为天气影响每日销量——或者用因果关系的术语来说,天气是销量的原因之一。换句话说,在其他条件不变的情况下,我们假设人们更可能在炎热的天气购买冰淇淋,这在直觉上是有道理的。这种信念得到了历史数据中温度和销量之间强相关性的支持,如图 1-3 所示(相应的数据和代码在本书的 GitHub 上)。

根据观测温度销售冰淇淋的函数图

图 1-3. 根据观测温度销售冰淇淋的函数图

如前言所述,我们将使用回归作为主要的数据分析工具。运行冰淇淋销售额对温度的线性回归只需一行代码:

## Python (output not shown)
print(ols("icecream_sales ~ temps", data=stand_data_df).fit().summary())
## R
> summary(lm(icecream_sales ~ temps, data=stand_dat))
...
Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -4519.055    454.566  -9.941   <2e-16 ***
temps        1145.320      7.826 146.348   <2e-16 ***
...

对于本书的目的,在输出中最相关的部分是系数部分,它告诉我们估计的截距——理论上的零温度下的平均冰淇淋销售额为−4,519,这显然是一个不合理的外推。它还告诉我们温度的估计系数为 1,145,这意味着每增加一度温度,冰淇淋销售额预计会增加 1,145 美元。

现在,让我们假设我们处于十月一个特别温暖的周末,根据模型的预测,公司提前增加了冰淇淋摊位的库存。然而,尽管这个十月的这个星期销量比平时高,但却远低于模型预测的数量。糟糕!发生了什么?数据分析师应该被解雇吗?

发生的事情是,模型没有考虑到一个关键的事实:大部分冰淇淋的销售发生在夏季,孩子们放暑假的时候。回归模型根据可用的数据做出了最佳预测,但由于增加的冰淇淋销售(学生暑假)部分归因于温度,因为夏季月份与温度呈正相关。由于十月份的温度增加并没有突然导致暑假(对不起,孩子们!),所以我们在那个温度下看到的销售量比夏季日子要少。

从技术角度来看,年月份是我们在温度和销售额之间关系的混杂因素。混杂因素是引入回归中偏差的变量;当混杂因素存在于你分析的情况中时,这意味着将回归系数解释为因果关系将会导致不正确的结论。

让我们想象一下像芝加哥这样的地方,那里有大陆性气候:冬天非常寒冷,夏天非常炎热。当比较一个随机炎热的日子和一个随机寒冷的日子的销售额时,如果没有考虑到它们各自的年月,你很可能在比较放暑假的炎热夏季日子的销售额和放学孩子们的寒冷冬季日子的销售额;这会夸大温度与销售额之间的表面关系。

在这个例子中,我们也许会预期在寒冷天气下销售量一直被低估。事实上,在夏季月份存在一种范式转变,当必须完全通过温度来管理这种转变时,在线性回归中,温度的回归系数对于较高温度来说会过高,而对于较低温度来说则会过低。

太多的变量可能会破坏原来的关系

解决混杂因素问题的一个潜在方法是将尽可能多的变量添加到回归模型中。这种“什么都包括在内”的思维方式在统计学家中仍有支持者。在《因果之书》中,朱迪亚·珀尔和达纳·麦肯齐提到,“一位领先的统计学家最近甚至写道,‘避免对一些观察到的协变量进行条件化...是非科学的特别策略’”(珀尔和麦肯齐,2018 年,第 160 页)^(2)。这在数据科学家中也是非常普遍的。公平地说,如果你的目标只是预测一个变量,并且你有一个经过精心设计以在测试数据之外进行足够泛化的模型,并且你不关心为什么预测变量取特定值,那么这是一个完全有效的立场。但是,如果你的目标是理解因果关系以便采取行动,那么这种方法就不适用了。因此,仅仅是向模型添加尽可能多的变量不仅效率低下,而且可能完全适得其反并且误导性极大。

让我们通过我们的例子来演示这一点,通过添加一个我们可能倾向于包括但会使我们的回归产生偏见的变量。我创建了变量 IcedCoffeeSales,它与 Temperature 相关,但与 SummerMonth 不相关。让我们看看如果我们在 TemperatureSummerMonth(一个二进制变量,指示月份是否为 7 月或 8 月(1),或其他任何月份(0))之外添加这个变量会发生什么:

## R (output not shown)
> summary(lm(icecream_sales ~ iced_coffee_sales + temps + summer_months))

## Python 
print(ols("icecream_sales ~ temps + summer_months + iced_coffee_sales", 
             data=stand_data_df).fit().summary())
...
                    coef    std err     t      P>|t|    [0.025     0.975]
----------------------------------------------------------------------------
Intercept         24.5560   308.872   0.080    0.937   -581.127    630.239
temps          -1651.3728  1994.826  -0.828    0.408  -5563.136   2260.391
summer_months   1.976e+04   351.717  56.179    0.000   1.91e+04   2.04e+04
iced_coffee_sales  2.6500     1.995   1.328    0.184     -1.262      6.562
...

我们看到 Temperature 的系数从之前的例子中显著偏移,现在变为负值。TemperatureIcedCoffeeSales 的高 p 值通常被视为有问题的迹象,但由于 Temperature 的 p 值“更差”,分析师可能会得出应将其从回归中移除的结论。这怎么可能呢?

数据的真相(已知,因为我制造了这些关系并在这些关系周围随机化了数据)是,当天气炎热时,人们更有可能购买冰咖啡。在炎热的日子里,人们也更有可能购买更多冰淇淋。但单单购买冰咖啡本身并不会使顾客更有可能购买冰淇淋。夏季月份与购买冰咖啡也没有相关性,因为学童对冰咖啡需求不是一个重要因素(详见边栏中的数学细节)。

图 1-4 显示了冰咖啡销量与冰淇淋销量之间的正相关性,因为在天气变暖时两者都会增加,但在夏季增加的冰咖啡销量可以通过与温度变量的共同相关性来解释。当回归算法试图使用手头的三个变量解释冰淇淋销量时,温度变量对冰咖啡销量的解释能力被添加到温度变量中,而冰咖啡则被迫弥补温度的压倒性影响。尽管冰咖啡销量在统计上并不显著且系数相对较小,但销售额远高于温度的度数,因此最终冰咖啡销量抵消了温度系数的膨胀。

冰咖啡销量与冰淇淋销量的绘图

图 1-4. 冰咖啡销量与冰淇淋销量的绘图

在前面的例子中,将变量 IcedCoffeeSales 添加到回归中使得温度与冰淇淋销量之间的关系变得混乱。不幸的是,反过来也是可能的:在回归中包含错误的变量可能会产生虚假的关系。

继续我们在 C-Mart 的冰淇淋示例,假设类别经理有兴趣了解顾客口味,所以他们让一个员工站在店外,调查路过的人,问他们对香草冰淇淋和巧克力冰淇淋的喜爱程度(都在 0 到 25 的范围内),以及是否曾经购买过摊位上的冰淇淋。为了简化,我们假设该摊位只销售巧克力和香草口味的冰淇淋。

为了例子简单,让我们假设对香草口味和巧克力口味的喜好完全没有相关性。有些人喜欢其中一种但不喜欢另一种,有些人两种都喜欢,有些人一种比另一种更喜欢,等等。但所有这些偏好都会影响某人是否购买冰淇淋,这是一个二元(是/否)变量。

因为变量购物是二元的,所以如果我们想要衡量Taste变量之一对购物行为的影响,我们将使用 logistic 回归。由于这两个Taste变量是不相关的,如果我们将它们相互绘制,我们将看到一个没有明显相关性的常规云;然而,它们各自影响购物在冰淇淋摊的概率(图 1-5)

左图:整体人群中对香草和巧克力的口味没有相关性;中图:在冰淇淋摊购物的人对香草的口味比不购物的人高;右图:对巧克力的口味结果相同

图 1-5. 左图:整体人群中对香草和巧克力的口味没有相关性;中图:在冰淇淋摊购物的人对香草的口味比不购物的人高;右图:对巧克力的口味结果相同

在第一个图中,我添加了一条最佳拟合线,几乎是平的,反映了变量之间缺乏相关性(相关系数等于 0.004,反映了抽样误差)。在第二和第三个图中,我们可以看到整体上购物者(购物 = 1)对香草和巧克力的口味平均较高,这是合理的。

到目前为止,一切都很好。假设你收到调查数据后,你的商业伙伴告诉你,他们正在考虑为冰淇淋摊引入一个优惠券激励措施:购买冰淇淋时,您可以获得未来访问的优惠券。这种忠诚度激励措施不会影响从未在摊位购物过的受访者,因此相关人群是那些曾在商店购物过的人。商业伙伴正在考虑在优惠券上使用口味限制来平衡库存,但不知道口味选择会受多大影响。如果购买了香草冰淇淋的顾客得到了一张 50%折扣的巧克力冰淇淋优惠券,除了增加废纸回收箱中的纸张外,还能起到什么作用吗?喜欢香草冰淇淋的人又如何看待巧克力冰淇淋呢?

你重新绘制了同样的图表,这次限制数据为那些对购物问题回答“是”的人(图 1-6)。

购物者对香草和巧克力的口味偏好

图 1-6. 购物者对香草和巧克力的口味偏好

现在这两个变量之间有强烈的负相关(相关系数为-0.39)。发生了什么?来到你的摊位的香草爱好者会变成巧克力的讨厌者,反之亦然吗?当然不是。这种相关性是在你限制到顾客时人为创造的。

让我们回到我们真正的因果关系:某人对香草的喜好越强,他们越有可能在你的摊位上购物,巧克力也是如此。这意味着这两个变量有累积效应。如果某人对香草和巧克力冰淇淋的喜好都较弱,他们很少会在你的摊位上购物;换句话说,你的顾客中大多数对香草口味不强的人对巧克力口味非常强烈。另一方面,如果某人对香草口味很强,即使对巧克力口味不是很强,他们也可能会来你的摊位购物。你可以在之前的图表中看到这一点:对于香草的高值(比如超过 15),数据点中巧克力的值较低(低于 15),而对于香草的低值(低于 5),图表中的数据点只有巧克力的值较高(超过 17)。没有人的偏好发生了变化,但是对香草和巧克力口味都不强的人被排除在你的数据集之外。

这种现象的技术术语是伯克森悖论,但朱迪亚·珀尔(Judea Pearl)和丹娜·麦肯齐(Dana Mackenzie)用更直观的名称称之为“解释消除效应”。如果你的某个顾客非常喜欢香草味,这完全解释了他们为何在你的摊位购物,他们不“需要”对巧克力有很强的喜好。另一方面,如果你的某个顾客对香草味不够喜欢,这不能解释他们为何在你的摊位购物,他们必须对巧克力的喜爱高于平均水平。

伯克森悖论一开始是违反直觉且难以理解的。这可能导致数据偏差,这取决于数据的收集方式,甚至在开始任何分析之前就已经存在。一个经典的例子是,在看待医院患者群体与普通人群时,某些疾病表现出更高的相关性。实际上,发生的情况是,只有当两者同时存在时,某种疾病才足以使某人去医院;某人的健康状况只有在两种疾病同时存在时才会变得严重到需要住院。^(3)

结论

预测分析在过去几十年来取得了极大的成功,并且未来也将如此。另一方面,当试图理解和——更重要的是——改变人类行为时,因果分析提供了一个引人注目的替代方案。

然而,与预测分析不同,因果分析要求采用不同的方法。希望本章的例子已经让你相信,不能简单地将一堆变量放入线性回归或逻辑回归中,然后期待最好的结果(我们可能认为这是“把所有变量都包括进去,上帝会认出自己的”方法)。你可能仍然会好奇,其他类型的模型和算法是否对混淆因素、多重共线性和虚假相关性免疫?不幸的是,答案是否定的。如果有什么不同的话,这些模型的“黑匣子”性质意味着混淆因素可能更难以捕捉。

在下一章中,我们将探讨如何思考行为数据本身。

^(1) 公平地说,在许多情况下,它们应该不同,因为数据用于不同的目的并遵循不同的约定。但即使是你期望只有一个真正答案的问题(例如,“我们现在有多少名员工?”),通常也会显示出差异。

^(2) 你可能会想知道,前面提到的统计学家是唐纳德·鲁宾。

^(3) 从技术角度讲,这种情况略有不同,因为存在阈值效应而不是两个线性(或逻辑)关系,但包含错误变量可能导致人为相关性的基本原则仍然适用。

第二章:理解行为数据

在第一章中,我们讨论了本书的核心目标是利用数据分析来了解驱动行为的原因。这需要理解数据和行为之间的关系,这在第一章中通过因果行为框架中的箭头表示(图 2-1)。

在本章中突出显示箭头的因果行为框架

图 2-1. 在本章中突出显示箭头的因果行为框架

请原谅这个流行文化的参考,但如果你看过《黑客帝国》,你会想起主角可以看着他周围的世界并看到其背后的数字。嗯,在本章中,你将学会看待你的数据并看到其背后的行为。

第一部分主要面向具有商业或数据分析背景的读者,并提供基本的“行为科学 101”介绍核心行为科学概念。如果你是经过训练的行为科学家,可能在这里找不到什么新东西,但你可能想浏览一下,以了解我使用的具体术语。

基于这种共识,第二部分将向你展示如何通过行为镜头来审视你的数据,并确定与每个变量相关的行为概念。在许多情况下,不幸的是,一开始一个变量与相应的行为只是松散相关的,因此我们还将学习如何“行为化”这样的偏离变量。

人类行为的基本模型

“行为”是一个我们非常熟悉的词,因为反复暴露,但很少,如果有的话,被正确定义。我曾经问过一个商业伙伴她试图鼓励什么行为,她的答案以“我们希望他们知道…”开始。在那一刻,我意识到两件事:(1)通过帮助澄清手头的目标,我可以为该项目增加比我最初预期的更多的价值,以及(2)我之前向她介绍的行为科学简介真的很糟糕,如果她仍然认为知道某事就是一种行为。希望这次我能做得更好,你能够从这一部分中学到增加组织价值的快乐。

实际上,我坚信行为思维的一个关键好处之一是让人们更加准确地思考他们试图做的事情。改变某人的想法并不等同于影响他们的行为,反之亦然。为此,我将提出一个简化但有希望可行的人类行为模型,首先以一个个人护理公司如何利用顾客的中年危机为例进行说明(图 2-2)。

我们为中年危机制定的人类行为模型

图 2-2. 我们关于中年危机的人类行为模型

在这个例子中,个人特征(进入 40 岁)导致情绪和思想(想要重新感受年轻),进而导致意图(决定购买一辆红色 Corvette)。这种意图可能会转化为相应的行为,也可能导致其他行动(比如染红头发),这取决于商业行为(播放电视广告)。

在某些情况下,我们可能试图影响的不是我们客户的行为,而是我们的员工、供应商等的行为。我们需要相应地调整这个模型,但直觉仍然是相同的:一方面,有一个我们试图影响行为的人类,另一方面,有我们的业务控制的所有过程、规则和决策(参见图 2-3)。

我们关于人类行为的一般模型

图 2-3. 我们关于人类行为的一般模型

在这个模型中,个人特征影响认知和情绪,进而影响意图,从而影响行为。作为我们控制的过程、规则和决策的商业行为则影响这三类别。

在这里,有许多不同的模型,这其中没有一个是最终或神奇的。但是对于本书讨论的数据分析阶段而言,我觉得这五个组成部分最能对应你将会遇到的数据类型。让我们逐一来看看这些组成部分,回顾每个组成部分的定义,并探讨我们如何能够以道德的方式收集和处理关于每个组成部分的数据。

个人特征

收集和使用诸如年龄、性别和家庭状况等人口统计变量是应用分析的基础,而且理由充分:这些变量通常能很好地预测一个人会做什么。然而,作为行为数据分析师,我们需要更广泛和更精确地思考个人特征。对于我们的目的而言,我们将个人特征定义为所有我们对一个人拥有的信息,这些信息在我们分析的相关时间框架内变化很少或者变化很慢。

回到 C-Mart 冰淇淋摊的例子,这意味着包括诸如个性特征和生活方式习惯之类的因素。假设你的一个顾客具有很高的经验开放性,并且总是愿意尝试一些新的口味组合,比如蓝莓和奶酪;你可以合理地将这种特质视为你分析中的一个稳定个人特征,尽管心理学告诉我们,个性在某人一生中会以可预测的方式发生变化。另一方面,一个展望未来几十年的城市规划师可能不仅要考虑生活方式习惯逐渐变化,还要考虑这些变化方向本身是否在改变。在这个意义上,我把个人特征看作是“主要原因”,定义为避免无限回归:我们追踪它们的变化,但忽略它们自身的原因。

虽然人口统计变量在相对稳定性和首要性方面符合上述定义,但将它们视为认知、情感、意图和行为的原因可能并不立即清晰。有些读者甚至可能不愿意说年龄或性别导致某人做某事,因为这听起来可能像是社会决定论。

我认为我们可以通过在概率意义上将原因定义为“贡献因素”来解决这些问题。到了 40 岁并不是发生中年危机的必要条件,也不是充分理由;而发生中年危机也不是购买红色雪佛兰的必要条件,也不是充分理由。事实上,这两种因果关系与其他贡献因素紧密相连:年龄对存在主义困扰的贡献依赖于职业和家庭轨迹等社会模式(例如,某人进入劳动市场的年龄或者生第一个孩子的年龄,如果他们做了其中的任何一个),而通过消费来解决这些困扰则仅在有足够的可支配收入时才可能。这也取决于一个人受广告影响的程度。

如行为科学的格言所述,“行为是个体和环境的函数”,社会因素往往比人口统计变量更具有影响力。从因果建模和数据分析的角度来看,社会现象与个人特征之间的这种相互作用可以通过调节和中介的方式来捕捉(我们将在第十一章和第十二章进行探讨)。

在这个意义上,人口统计变量在行为数据分析中通常非常有用,因为它们的预测能力暗示了其他更心理和可操作的个人特征(它们是一个贡献因素)。例如,在美国和许多其他国家,执法和护理仍然是明显性别化的领域。注意到这种实证规律不太可能为你赢得任何赞扬。更有趣的问题是这种效应是如何发生的。例如,一个假设是这种效应源于关于权威和关怀的社会表征和规范。或者,也可以假设这种现象是因为缺乏其他角色模型而自我延续的。确定哪种假设是真实的(或者如果两种都是真实的,哪种更有说服力)可能会导致对高中生进行更有效的职业咨询。

收集数据与伦理考虑

人口统计信息在业务分析中广泛可得且被使用(甚至可以说被过度使用)。主要挑战在于识别和衡量其他个人特征,以便正确地分配它们对行为的影响,而不是不适当地归因于人口统计变量。市场和用户研究在这里非常有用。

从伦理角度来看,错误归因的影响也很重要:在许多情况下,歧视可以被视为有意或无意地将行为(当前或预测的)错误归因于人口统计变量。即使你的分析是正确的,你也需要确保它不会无意中加强优势和劣势的模式。

认知与情绪

认知与情绪 是我用来涵盖情绪、思维、心理模型和信念等心理状态的一个术语。你可以把它看作是客户大脑中除了意图和更持久的个人特征之外的所有事物。例如,潜在客户是否知晓您的公司及其产品或服务?如果知晓,他们对此有何看法?他们对银行运作的心理模型是什么?每当他们看到路上一辆光鲜亮丽的特斯拉时,他们是否会感到羡慕?

认知与情绪涵盖了所有这些,以及更模糊的商业术语和短语,如客户满意度(CSAT)和客户体验(CX)。CX 已成为业务的口号:许多公司都设有 CX 团队,并举办与此主题相关的会议、咨询和书籍。但它究竟是什么?你能够衡量它的原因和影响吗?可以,但这需要知识谦逊和愿意花时间进行侦查工作。

收集数据与伦理考虑

认知和情绪在两个方面类似于心理学的个人特征:它们通常不能直接观察,相关数据是通过调查或在用户体验(UX)观察期间收集的。重要的是要注意,除非你使用生理测量,否则你将依赖于 陈述观察到 的测量。

这就带来了用户体验(UX)或人本设计与行为科学之间最大的区别之一:UX 的假设是人类知道他们想要什么,他们对某事感觉如何,以及为什么,而行为科学则从我们对自己头脑中发生的许多事情毫无察觉的假设开始。用一个法律比喻来说,行为科学家经常将某人说的话视为不可信,直到证明是可信的,而 UX 研究人员会将其视为诚实,直到证明是误导的。

但我不想夸大这种区别,实践中这种区别经常变得模糊不清,这取决于手头的情况。如果一位行为科学家被客户告知一个网站使用起来令人困惑和沮丧,他们将借鉴 UX 的做法,并相信客户确实体验到了负面情绪。相反,当进行基础产品研究时,一位技术娴熟的 UX 研究员通常会超越表达的意图,试图识别客户更深层次的需求。

从道德的角度来看,试图改变某人的认知和情绪显然是一个灰色地带。在这方面,我推荐的试金石是“NYT”测试:你的意图和方法是否善良且透明,以至于你的老板的老板看到它们出现在 纽约时报 的头版时不会介意?广告努力影响人们的想法和情绪,但我不指望会看到一个标题写着“公司花费数十亿让人们想要他们的东西”;无论如何都会引起大家的打哈欠。另一方面,“滑坡”——诸如“现在有三个人正在看这个房产!”之类的操纵性行为科学把戏——和老式的谎言是过不了这个测试的。这仍然留下了许多诚实的机会来应用行为科学:例如,以更符合客户心理模型的术语解释产品的好处,或者使购买体验更轻松或更愉快。因为本书的主题是数据分析而不是行为设计,所以我们只会看如何分析这种干预措施,而不是如何开发它们。

意图

当有人说“我要做 X 事情”,不管 X 是为一周的杂货购物还是预订假期,他们表达了一种 意图。因此,意图将我们带向行动的一步。

从技术上讲,意图是一种心理状态,可以被包括在先前的认知和情感类别中,但由于其在应用行为数据分析中的重要性,我认为它应该有自己的类别。除非你计划强迫客户走特定的路径(例如,从网站上删除电话号码以防他们给你打电话),否则你通常需要将他们的意图作为行为变化的导管。

此外,人们经常无法完成他们想做的事情,这个概念在行为科学中被称为意图行动差距(例如,想想新年决心经常被打破的情况)。因此,驱动客户行为的关键是确定潜在的反应是因为客户不想采取行动,还是因为意图和行动之间发生了某些事情。

数据收集和道德考虑

在业务的一般过程中,我们通常没有关于意图的直接数据。因此,大多数数据将来自客户的声明,通常通过两个来源:

调查

这些可以是“当时发生的”(例如,在访问网站或商店结束时)或异步的(例如,通过邮件、电子邮件或电话进行)。

用户体验观察

在这些用户体验观察中,UX 研究人员要求受试者经历某种体验,并收集关于他们意图的信息(通过要求他们在过程中大声思考或事后提问,比如,“那时你想做什么?”)。

或者,我们经常试图从观察到的行为中推断意图,这在分析中被称为“意图建模”。

从道德的角度来看,影响某人的意图是广告、营销和说服的黑暗艺术的目标。这是可行的,我真诚地相信在许多情况下甚至可以在道德上做到,正如 UX 和行为研究人员所努力的那样。但简单的事实是,这通常很难做到,而且可能适得其反。识别那些可以帮助客户弥合意图行动差距的地方则简单得多且更安全。换句话说,在我们的模型中,一个“痛点”通常反映了实现意图的障碍。

行动

行动行为的基本单位,我经常交替使用这两个词。我经常告诉人们的经验法则是,如果你当时在房间里,应该能够观察到一个行动或行为,而不必问这个人。“在亚马逊上购买东西”是一个行动。在亚马逊上“阅读产品评论”也是行动。但“知道某事”或“决定在亚马逊购买某物”不是。除非你问他们或看到他们行动(这是一个结果但不是同一件事),否则你无法知道某人已经做出了决定。

行动或行为通常可以在不同的粒度级别上定义。“去健身房”是可观察的,在许多情况下这是一个可以接受的细节水平。如果你给人们寄送免费访问的优惠券并且他们到场,你已经达到了你的目标,不需要更深入地思考。然而,如果你正在开发一个健康应用程序,并且希望确保人们按照他们的计划保持进度,你需要比这更详细。我曾参加过行为科学家史蒂夫·温德尔的演讲,他在演示中详细讲解了“去健身房”的每一个步骤(穿上健身服装,去到车上,决定一旦到达健身房要做什么锻炼等)。如果你试图识别和解决行为障碍,这通常是你需要采用的详细程度。同样的情况也适用于网站或应用程序中的客户旅程。什么是“注册”意味着什么?他们需要检索和输入哪些信息?你要求他们做出哪些选择?

收集数据及道德考量

行动或行为数据通常代表可用客户数据中最大的类别,并且通常归类为“交易数据”。然而,在许多情况下,这些数据需要额外的处理才能真正反映出行为。我们将在本章的第二部分更详细地探讨这一点。

如果你的目标是员工行为(例如在人员分析或减少流失方面),数据可能会更加稀缺和难以获取,因为通常需要从业务流程软件中提取,并且可能并非常规记录。

让我们明言不讳:行为分析的最终目标是修改行为。在某些情况下,这可能非常简单:如果你从网站上删除客服电话号码,就会减少打电话给你的人数。这是否是一个好主意则是另一回事。显然,行为修改有时可能非常困难,正如行为改变的大量和增长的文献所示。^(1)

无论如何,修改行为的目标都伴随着道德责任。在这里,纽约时报的问题是我推荐的试金石。

业务行为

我们需要考虑的最后一个数据类别是业务行为:组织或其员工对客户(或员工行为是你的焦点的情况下)产生影响的事物。这包括:

  • 通信,如电子邮件和信件

  • 网站语言的更改或呼叫中心代表的对话路径的更改

  • 业务规则,例如客户奖励的标准(例如,“如果客户在过去六个月内花费了 X 美元,请给他们寄送优惠券”)或招聘决策

  • 个体员工的决策,例如将客户账户标记为潜在欺诈或提升另一名员工

收集数据

商业行为对行为数据分析师来说既是一种祝福又是一种诅咒。一方面,从干预设计的角度来看,它们是我们驱动个人行为的主要杠杆之一:例如,我们可以修改一封信的内容或电子邮件的频率,以说服客户按时付款。因此,商业行为是一种极其宝贵的工具,没有它,我们的工作将变得非常困难,而且当我们自己对商业行为进行更改时,通常很容易收集相应的数据。商业行为的另一个优势是,在数据收集方面没有太多道德考虑:组织通常可以在正常业务过程中自由收集关于其自身运营和员工行为的数据(尽管当然也有一些情况被认为是侵犯隐私)。

另一方面,从数据收集和分析的角度来看,商业行为可能是分析师的噩梦:就像水对鱼一样,它们可能对组织来说是看不见的,它们对个人行为的影响就变成了难以解决的噪音。这发生的原因有两个。

首先,许多组织,如果他们根本就追踪商业行为,往往不会像追踪客户行为那样以相同的详细程度进行追踪。假设 C-Mart 在 2018 年夏季试验性地缩短了营业时间,导致销售暂时下降。光凭数据就难以找出原因!许多业务规则,即使它们在软件中实现了,也根本没有以机器可读格式记录在任何地方。如果相应的数据确实已记录,通常仅存储在部门数据库(或更糟的是,Excel 文件)中,而不是企业数据湖中。

其次,商业行为可能会影响对客户行为变量的解释。最明显的例子就是 诱惑,即有意引入的摩擦和误导性沟通,以混淆客户。想象一下,在网站上有一个表格,当你输入你的电子邮件地址时,自动勾选了你在表格开始时取消选中的“我想接收营销邮件”的框。那个被勾选的框是否真的表示客户想要接收营销邮件?除了这些明显的例子之外,商业行为可能隐藏在许多客户行为背后,尤其是在销售领域。许多购买倾向模型应该注明“在我们的销售团队决定联系的人群中”。悖论的是,尽管销售代表的薪酬结构通常是商业领导者最着迷的杠杆之一,但它很少被纳入客户购买行为模型中。

最终,获取有关业务行为的可靠数据,尤其是随着时间推移,对于行为数据分析师来说可能是一项艰巨的挑战——但这也意味着这是他们在进行任何分析之前为组织创造价值的一种方式。

如何连接行为和数据

个人特征、认知和情感、意图、行为和商业行为:这些是我们可以用来代表和理解我们的世界的概念,作为行为数据分析师。然而,连接行为和数据并不简单地将手头的变量分配到这些桶中的一个。仅仅“涉及行为”并不足以使变量有资格成为“行为数据”;正如我们在前一节中看到的,例如,一个据称是关于客户行为的变量可能实际上只反映了一个业务规则。在本节中,我将为您提供一些提示,以使您的数据具有行为化,并确保它尽可能与其应该代表的定性现实密切匹配。

为了更具体化,我们将看看我们的第二个虚构公司,Air Coach and Couch(AirCnC),一个在线旅行和住宿预订公司。 AirCnC 的领导层已要求他们的分析师衡量客户满意度对未来购买行为的影响,即在预订后的六个月内的支出量(M6Spend),这是他们的关键绩效指标之一。我们将在本书结尾,在第十二章回答这个常见但棘手的业务问题。现在,我们只是看看如何开始构思这个问题。

培养行为完整性心态

由于行为科学在企业中尚属新兴,你通常会成为第一个将这种视角带入组织数据的人,而该数据很可能包含数十个,甚至数百个或上千个变量。这是一项艰巨的任务,但采取正确的心态将有助于你理清头绪并开始工作。

想象一下,你是一名新被指派负责维护一座桥梁的结构工程师。你可以从一端开始,逐英寸(或逐厘米)评估其完整性,直到到达另一端,然后制定一个十年计划来实现完美的结构完整性。与此同时,坑洼变得越来越严重,危及驾驶者的安全,损坏他们的车辆。更合理的方法是快速浏览一下,优先解决可以迅速修复的主要问题,并确定临时解决方案的地点,直到你有时间和预算来进行更多的结构性变更为止。

同样的思维方式也适用于你的数据。除非你碰巧是初创企业的第一批员工之一,否则你将处理现有数据和遗留流程。不要惊慌,也不要按字母顺序浏览你的表格列表。从一个具体的业务问题开始,并按照它们对业务问题重要性递减的顺序识别最可能不准确的变量:

  1. 兴趣的原因和效果

  2. 中介变量和调节变量,如果相关

  3. 任何潜在的混杂因素

  4. 其他非混杂独立变量(也称为协变量)

在过程中,你将不得不做出判断:例如,你应该在分析中包含某个变量吗?还是它定义得如此不清晰,以至于最好不要包含它?不幸的是,并没有清晰的标准来正确地做出这些决定;你将不得不依靠你的商业感觉和专业知识。然而,有一种明确错误的做法:假装它们不存在。一个变量将被包括或不包括在你的分析中,这是无法回避的事实。如果你的直觉倾向于包含它,那么记录为什么,描述潜在的错误源,并指出如果该变量被省略,结果会有何不同。正如一位用户体验研究员在与我友好交谈时所说,作为商业研究人员意味着不断地弄清楚“你能够做什么”。

不信任和验证

不幸的是,在许多情况下,数据记录的方式受业务和财务规则的驱动,以事务为中心,而不是以客户为中心。这意味着在证明无罪之前,你应该将变量视为可疑:换句话说,不要自动假设变量 CustomerDidX 表示客户做了 X。它可能意味着完全不同的事情。例如:

  • 客户在不阅读提到他们同意 X 的详细信息的小字条款的情况下勾选了一个框。

  • 客户没有说任何话,因此我们默认他们选择了 X。

  • 客户声明他们做了 X,但我们无法验证。

  • 我们从供应商那里购买了数据,表明客户在他们生活的某个时刻经常做 X。

即使客户实际上做了 X,我们也不能假设他们的意图。他们可能做了这个:

  • 因为我们发送了他们一封提醒邮件

  • 四次因为页面没有刷新

  • 他们误以为自己真正想做 Y

  • 一周前,但由于监管约束,我们今天才记录它。

换句话说,用《公主新娘》中的一句流行台词来说:“你一直在使用那个变量。我觉得它并不是你认为的意思。”

确定类别

正如 AirCnC 的领导所述,他们要求分析师了解 CSAT 对购买行为的影响,这是一个艰巨的任务。我们的第一步是弄清楚我们在谈论什么。

在我大学的第一年,我的哲学教授会给我们一些如“进步是什么?”和“人与机器”的论文题目。除此之外,如果我们卡壳了,他还提供了一些极好的建议:用他的话说,我们应该弄清楚,“这是哪本书的一章。” 矛盾的是,当一个问题看起来令人望而却步且难以解决时,询问它属于哪个更广泛的类别通常会有所帮助。

幸运的是,作为行为数据分析师,我们不需要在国会图书馆里漫游以寻找灵感;我们在本章节早期定制的分类就能发挥作用:

  • 个人特征

  • 认知和情感

  • 意图

  • 行为(又称行为)

  • 商业行为和流程

让我们通过排除法进行。客户满意度并非固定不变,因此不属于个人特征的一部分。它既不是人们做的事情,也不是人们打算去做的事情,因此也不是行为或意图。最后,它也不仅仅是在业务层面上发生的事情,因此也不是商业行为或流程的一部分。因此,客户满意度是认知和情感的一部分,与它相似的客户体验也是如此。我们业务问题的第二个变量,“购买行为”,要分类起来容易得多:显然它是客户行为。

根据我的经验,许多商业分析项目失败或成果低迷,因为分析师没有澄清项目的核心。组织总是有一个主导的目标指标——公司的利润,非营利组织的客户结果等等。在更低层次上,部门通常有他们自己的目标指标,比如客户体验团队的净推荐值,IT 的停机百分比等等。如果业务伙伴要求您衡量或改善一个看似与这些目标指标无关的变量,通常意味着他们心中有一个隐含的可能有问题的行为理论将两者联系起来。

“客户参与度”,这是行为科学家经常被要求提高的另一个流行概念,是一个很好的例子。这并不是一个明确的概念,因为它可能真正指的是两种不同的事物:

  • 一种行为,即与业务的广泛互动模式:如果客户 A 更频繁地登录网站并花费更多时间浏览,他被认为比客户 B 更参与其中。

  • 一个认知或情感,比如当观众因为沉浸在电影或课程的流程中并渴望知道接下来会发生什么时,“参与”。

的确,我坚信对这两者之间的混淆解释了初创企业和更广泛的数字世界对参与度指标的吸引力,尽管它们可能具有误导性。例如,在第一种意义上,当我的洗衣机停止工作时,我与它的关系更为密切;但这并不转化为第二种意义上的愉悦和渴望。试图将参与作为行为来增加的组织通常对结果感到失望。当作为行为的参与不能转化为作为情感的参与时,它不会带来诸如更高忠诚度和留存率等期望的结果。

作为一个个人例子,一个商业伙伴曾经请我帮忙让员工进行某种培训。经过一些讨论,很快明确了她真正想要的是让员工遵守一个业务规则;她认为员工之所以不遵守是因为他们对规则了解不足。我们把项目重心转向了理解为什么员工不遵守规则以及如何鼓励他们这样做。简而言之:要警惕那些自我诊断的病人!

现在我们可以在我们的人类行为模型中重塑我们的业务问题:AirCnC 的领导们想知道认知/情感是否影响客户的行为,如果是的话,影响有多大。实际上,在绝大多数业务分析问题中,涉及的变量至少有一个是行为,要么是客户的行为,要么是业务的行为。如果我们给客户发送跟进电子邮件,他们会更满意吗?满意的客户更有可能购买吗?

如果在确定了相关变量的类别之后,其中没有一个属于客户或业务行为类别,这应该引起警觉。这表明一个没有明确“所以呢?”的业务问题。比如说,年长客户更满意。所以呢?我们要用这些信息做什么?业务结果是由行为驱动的,不管是我们自己的行为还是我们客户的行为。

一旦你确定了相关的行为,就该深入挖掘相应的变量了。

精炼行为变量

正如我之前提到的,一个变量“关于行为”并不等同于一个行为变量。通常你需要对它们进行转换,使它们真正成为行为变量。

让我们专注于客户行为,因为它们更直观,但逻辑类似适用于业务行为。一个良好的行为变量将具备以下特征:

可观察

正如在第一章中提到的,行为在原则上是可观察的。如果你在客户身边,你能看见他们这样做吗?在预订过程中放弃预订是可观察的;改变主意则不是。一个很好的线索是,如果它没有时间戳,那么它可能不够具体和细化。

个体化

企业经常依赖于汇总指标,如比率或比例(例如,取消账户的客户比例)。汇总指标可以提供有用的快照以供报告之用,但它们可能会受到偏见和混淆因素的影响,例如人口组成的变化(即客户组合),特别是当它们基于时间间隔计算时。

例如,假设一次成功的营销活动将大量新用户带到了 AirCnC 的网站上。同时假设在这个业务线上,相当大比例的新客户在第一个月取消了他们的账户。因此,在活动后的一个月里,AirCnC 的每日取消率可能会急剧上升,即使没有任何问题发生。一个好的经验法则是,健全的汇总变量建立在健全的个体变量基础之上。如果一个变量只在汇总时有意义,并且在个体水平上没有有意义的解释,那就是一个警示信号。在我们的例子中,取消率的有意义的个体对应物将是取消概率。在控制个体特征和与公司的任期时,这个指标将保持稳定,尽管有大量新客户的涌入。

原子

同样,企业经常会汇总一系列不同行为,这些行为有着共同的意图。例如,AirCnC 客户修改他们的账单地址可能有三种不同的方式:通过账户设置、在预订时编辑信息,以及联系客服中心。这些方式在观察客户时看起来可能不同,但在数据库中可能会以类似的方式记录。再次强调,我并不想暗示汇总的行为变量本质上是不好的。当然,有些分析需要使用二元变量ChangedBillingInformation。但至少,你应该了解可以采用的具体方式,并且尽可能检查你得出的总体结论是否适用于每一种方式。

在许多情况下,确定或创建令人满意的行为变量涉及到“亲手动手”。用于分析或研究目的的数据库通常提供了“精简过”的真实版本,如交易数据库中所示,仅列出最新的、经过审查的信息。这在大多数情况下是有道理的:如果客户预订了并取消了,并且已退款,我们不希望该金额计入AmountSpent变量。毕竟,从业务角度来看,AirCnC 并没有保留那笔钱。然而,从行为学角度来看,该客户与同一时间段内没有进行任何预订的客户是不同的,因此在某些分析中考虑这一点是相关的。不要去学习像 COBOL 这样的古老编程语言来访问最底层的数据库,但值得稍微深入你平常美丽表格之外的地方稍作探索。

理解背景

重申一下,“行为是人与环境的函数”是行为科学中的基本原理。尽管个人变量当然很重要,但我觉得分析师们往往过于依赖它们,因为它们易于获取,结果可能没有足够地考虑到背景变量。

通常理解人们行为背景的最佳方式是通过定性研究,如访谈和调查,这些见解可以用来生成新的变量。在这里,我将重点介绍如何从现有数据中提取背景信息:

这是时候了。

正如前面提到的,行为是可观察的。在理论上,甚至在实践中,都可以确定它发生的时刻,这要归功于时间戳。时间戳对于行为分析师来说是金子般珍贵的,因为它们提供直观且通常可操作的见解。毫不奇怪,没有现成的算法可以提取这些,只能依赖商业直觉和问题的特定性。我将给出寻找的最常见线索:

频率

在行为和更广泛地说,事件数据中,自然的倾向是查看频率,即单位时间内的事件/行为次数。不幸的是,频率数据有时会表现不佳,并展示出不反映行为变化的人为不连续性。例如,假设一个 AirCnC 的客户每年夏天和冬天都度假,这意味着每年预订两次。然而,有一年他们将冬季度假从 12 月改到了 1 月。我们将在前一年计算一次假期,而在后一年计算三次,即使行为实际上并没有改变。这种赶上现象——较长时间跟随较短时间,最好通过直接跟踪持续时间来理解。对于其他行为,过去的事就让它过去吧,频率将是更稳健的度量标准。

持续时间

持续时间也提供了一种衡量衰减效应的自然方法。很久以前发生的事情或您所做的事情往往比最近发生的事情效果小。这通常使持续时间成为一个良好的预测变量。如果一个顾客在五年前经历了糟糕的 AirCnC 体验后仍未离开,那么这可能不再对他们的决策产生太大影响,最好是根据它们发生的时间长短加权过去旅行的客户满意度,而不仅仅使用平均值。

连续性

同样,彼此非常接近的行为通常并非巧合,并且可以提供正在发生的事情的线索。一个顾客在尝试在线更改后打电话给 AirCnC 的呼叫中心以更改其账单信息,展示了与直接致电的顾客不同的行为。孤立的数据使全渠道分析项目变得艰巨,但成功将来自不同渠道的数据整合在一起将带来巨大回报。汇总行为数据的最佳方式之一是为“在做 X 后做 Z”创建变量。

社交日程

人们更可能在工作时间内,或在周一到周五的白天^(3)在路上,要么正在前往工作地点,要么正在从工作地点回家。一个周六上午悠闲地浏览度假目的地的顾客可能在孩子的体育练习场上。现代生活有其常识和时间表。由于其细粒度,通常最好从“每周小时”变量开始,而不是单独拥有“每天小时”和“每周天”的变量(当然是以当地时间为准)。根据您的业务线,您可以进一步将事物聚合成像“工作日晚上”等变量。

信息和“已知未知”

如果一棵树在森林中倒下并且在您的数据中记录下来,但没有顾客听到它,它会发出声音吗?您的组织和客户通常在不同时间点了解到事物。与行为相关的变量应始终反映进行该行为的人在执行该行为时可获得的信息。这可以简单地将“发送日期”替换为“预期收到日期”,适用于您发送给客户的邮件,反之亦然。人们不知道他们没有打开的电子邮件的内容。正如常识和行为逻辑的一部分,但它将有助于确保您的变量紧密契合相应的行为。

沉默的狗

有时人们会做一件事而不是另一件事,有时他们没有选择(或者他们没有看到)。人们做的事情通常与他们所做的事情一样有趣。识别替代行为的一种方式是寻找分叉:如果行为 A 之后经常发生行为 B,那么还有哪些行为 C 在 A 之后也经常发生?反之亦然,如果行为 B 之后经常发生行为 D,是什么其他行为导致了行为 D?这可以帮助您识别“幸福路径”或者在完成任务的过程中迷失的客户。

更好地理解行为背景是行为视角为像 AirCnC 这样的业务分析项目带来价值的方式之一。AirCnC 的领导是否想要衡量客户对他们的在线预订体验、住宿或 AirCnC 的服务代表(还有许多其他可能性)的满意度?你可以在客户与 AirCnC 的第一次互动之后的任何时候询问客户他们的满意度,他们会给出一个答案,但这并不能保证这些答案意味着相同的事情,甚至是任何事情。

在我们的情况下,AirCnC 的领导真正想要了解的是在服务呼叫之后客户满意度的重要性。这将帮助他们确定是否应该更多投资于雇佣和培训高质量的代表,或者是否可以将服务外包给低工资的国家(提示:他们可能不应该这样做)。

结论

应用行为科学的乐趣和挑战之一是它涉及定性和定量分析之间的持续来回。本章的意图是为您提供一个关于人类行为的基本模型和可操作的技巧,以便您可以开始灵活运用这些技能并将行为与数据联系起来。这意味着即使在进行任何数据分析之前,您也可以通过改进其数据的行为完整性和澄清解决业务问题的方法为组织增加价值。

在本书的下一部分,我们将介绍因果行为框架的第三支柱,即允许我们建立行为之间关系的因果图。

^(1) 塞勒和桑斯坦的Nudge(2009)和艾尔的Hooked: How to Build Habit-Forming Products(2014)是关于这个主题的两个良好的参考书目之一。

^(2) 在此,我预先向结构工程师道歉,如果这个比喻严重误代表了他们的工作,这很可能会发生。诗意的许可和其他一切。

^(3) 当然,除非恰好发生全球大流行。

第二部分:因果图与消除混杂

在第一部分中,我们看到混杂可能会危害甚至最简单的数据分析。在第二部分中,我们将学习构建因果图(CD)来表示、理解和消除变量之间的关系。

首先,第三章介绍了因果图及其构建基础。

在第四章中,我们将看到如何从头开始为新分析构建因果图。我们在冰淇淋示例中看到的因果图设计非常简单。但在现实生活中,确定应在我们的因果图中包含哪些变量,超出了我们感兴趣的因果关系,以及如何确定它们之间的关系,通常会变得复杂。

同样地,从我们的冰淇淋示例中消除混杂很简单:我们只需要在回归中包含我们感兴趣变量的联合原因。在更复杂的因果图中,确定应包含在回归中的变量可能变得困难。在第五章中,我们将看到可以应用于即使是最复杂因果图的规则。

第三章:因果图简介

实际上,除了少数例外,相关确实意味着因果关系。如果我们观察到两个变量之间存在系统性关系,并且我们已经排除了这只是随机巧合的可能性,那么一定存在某种因果关系。当马来影子戏剧的观众在屏幕上看到一个实心的圆形影子时,他们知道某个三维物体产生了这个影子,尽管他们可能不知道这个物体是球体还是侧面的饭碗。对于统计学入门而言,一个更准确的口号应该是简单的相关性暗示着一个未解决的因果结构。

比尔·希普利,《生物学中的因果与相关性》(2016)

因果图(CDs)很可能是大多数人从未听说过的分析工具中最强大的之一。因此,它们是因果行为框架的三个极点(顶点)之一(图 3-1)。它们提供了一种语言来表达和分析因果关系,特别适用于处理行为数据分析。

图 3-1. 数据分析的因果行为框架

在本章的第一部分中,我将展示 CD 如何从概念角度融入框架中,即它们如何与行为和数据连接。在第二部分中,我将描述 CD 中的三种基本结构:链式结构、分叉结构和碰撞结构。最后,在第三部分中,我们将看到可以应用于 CD 的一些常见转换。

因果图和因果行为框架

首先,让我们定义什么是 CD。CD 是变量的视觉表示,显示为方框,并显示它们之间的关系,表示为从一个框到另一个框的箭头。

在我们在第一章中的 C-Mart 示例中,变量 IcedCoffeeSales 受到一个单一原因 Temperature 的影响。图 3-2 展示了相应的因果图。

我们的第一个因果图

图 3-2. 我们的第一个因果图

每个矩形代表我们可以观察到的变量(我们在数据集中拥有的一个),它们之间的箭头表示因果关系的存在和方向。这里,TemperatureIcedCoffeeSales 之间的箭头表明前者是后者的原因。

然而,有时会有一个我们无法观察到的额外变量。如果我们仍然希望在因果图中显示它,我们可以用一个阴影矩形来表示^(1)(图 3-3)。

带有未观察变量的因果图

图 3-3. 带有未观察变量的因果图

在 图 3-3 中,CustomerSweetToothIcedCoffeeSales 的一个原因,这意味着甜食爱好者购买更多冰咖啡。然而,我们无法观察到顾客的甜食偏好程度。稍后我们将讨论因果分析中未观察到的混杂因素和更普遍地未观察到的变量的重要性。目前,我们只需要注意即使我们无法观察到特定变量,也可以通过将其表示为椭圆形包含在因果图中。

因果图表现行为

对因果图的第一种看法是将它们视为行为之间因果关系的表现,以及影响行为的现实世界中的其他现象(图 3-4)。从这个角度看,因果图的元素代表着存在并相互影响的真实“事物”。物理科学中的类比可以是磁铁、铁条和磁铁周围的磁场。你看不到磁场,但它确实存在,并影响铁条。也许你对磁场没有任何数据,也许你从未见过描述它的方程,但当你移动铁条时,你能感觉到它,并且你能对它的作用产生直觉。

CDs are connected to behaviors in our framework

图 3-4. CDs are connected to behaviors in our framework

当我们想要理解驱动行为的因素时,同样的观点适用。我们直观地理解人类有习惯、偏好和情感,尽管我们通常没有关于这些方面的数字数据。当我们说“乔买了花生是因为他饿了”时,我们依赖于我们对人类和乔的知识、经验和信念。我们把饥饿看作是一种真实存在的东西,即使我们没有测量乔的血糖或脑部活动。

在这里,我们对现实做出了因果性陈述:我们说如果乔不饿的话,他就不会买花生。因果性对我们对现实的直观理解非常基本,以至于即使是年幼的孩子在接触科学方法或数据分析之前也能做出正确的因果推论(通过使用“因为”这个词表明)。当然,直觉受到各种行为科学家所熟知的偏见的影响,即使它采取更为教育的形式,如常识或专业知识。但通常情况下,即使在没有数量数据的情况下,直觉也能在日常生活中很好地指导我们。

你可能担心使用 CDs 来表示对世界的直觉和信念引入了主观性,这当然是事实。但是因为 CDs 是思考和分析的工具,它们不必是“真实”的。你和我对 Joe 为什么买花生有不同的想法,这意味着我们可能会绘制不同的 CDs。即使我们完全同意什么导致什么,我们也不能在一个图表中表示所有东西及其关系;确定包含或排除哪些变量和关系涉及判断。在某些情况下,当数据可用时,它将有所帮助:我们将能够拒绝一个与手头数据不兼容的 CD。但在其他情况下,非常不同的 CDs 将与数据同样兼容,我们将无法在它们之间做出选择,特别是如果我们没有实验数据。

这种主观性可能看起来像是 CDs 的一个(可能是致命的)缺陷,但实际上却是一个特性,而不是错误。CDs 不会产生不确定性;它们只是反映了已经存在于我们世界中的不确定性。如果对当前情况有几种可能的解释看似同样有效,你应该明确地说明。另一种选择是允许那些在他们的头脑中有不同心理模型的人每个人都相信自己知道真相,并且其他人同意他们的看法,而实际上情况并非如此。至少公开不确定性将允许进行原则性讨论并指导您的分析。

因果图代表数据

尽管构建和解释 CDs 有一门艺术,但也有一门科学,我们可以利用 CDs 来表示数据中变量之间的关系(见图 3-5)。当这些关系完全是线性的或近似线性时,CDs 在线性代数中有明确的等价物。这意味着我们可以使用线性代数的规则和工具来验证我们如何操作和转换 CDs 的“合法性”,从而确保我们得出正确的结论。

CDs 也与数据相关联

图 3-5. CDs 也与数据相关联

线性要求可能看起来非常严格。然而,当某些关系不是线性的,但仍属于称为广义线性模型(GLM)的模型的广泛类别时,线性代数的一些规则和工具仍然适用。例如,逻辑回归模型就是一个 GLM。这意味着我们可以用 CDs 来表示和处理一个因果关系,其中影响变量是二进制的。如侧边栏所示,在这种情况下,数学变得更加复杂,但我们关于 CDs 的大多数直觉仍然成立。

从这个角度来看,从图 3-3 中连接TemperatureIcedCoffeeSales的因果图意味着:

IcedCoffeeSales = β * Temperature + ε

这个线性回归意味着,如果温度增加一度,其他条件保持不变,那么冰咖啡的销售将增加β美元。因果图中的每个方框代表一个数据列,就像表格 3-1 中模拟数据一样。

表格 3-1. 模拟数据,展示了我们因果图中的关系

日期 Temperature IcedCoffeeSales β * Temperature ε = IcedCoffeeSalesβ * Temperature
6/1/2019 71 $70,945 | $71,000 $55
6/2/2019 57 $56,969 | $57,000 $31
6/3/2019 79 $78,651 | $79,000 -$349

对于熟悉线性代数符号的人,我们可以将前述方程重写为:

(70,94556,96978,651)=1000*(715779)+(5531349)

从这个角度来看,因果图关注的是数据——变量及其之间的关系。这立即推广到多个原因。让我们绘制一个因果图,显示TemperatureSummerMonth都会导致IceCreamSales(图 3-8)。

一个带有多个原因的因果图

图 3-8. 一个带有多个原因的因果图

将这个因果图转化为数学术语,得到以下方程式:

IceCreamSales = β[T].Temperature + β[S].SummerMonth + ε

显然,这个方程是标准的多元线性回归,但其基于因果图的解释方式有所不同。在因果框架之外,我们只能得出一个结论:“温度增加一度与冰淇淋销售额增加β[T]美元相关。”因为相关性并非因果关系,因此推断任何进一步的事实都是不合理的。然而,在回归模型基于因果图的情况下,如本例,我们可以做出显著更强的陈述——即,“除非这个因果图是错误的,否则温度增加一度将导致冰淇淋销售额增加β[T]美元”,这正是业务关心的。

如果你有数据科学等量化背景,你可能会倾向于关注因果图与数据之间的连接,而牺牲了与行为之间的联系。这当然是一条可行的道路,并且它已经产生了一整套称为概率图模型的统计模型类别。例如,已经开发了算法来在数据中识别因果关系,而不依赖于人类的专业知识或判断。然而,这个领域仍处于起步阶段,当应用到现实数据时,这些算法通常无法在几种可能导致极不同业务影响的因果图之间进行选择。商业常识通常可以更好地选择最合理的一种。因此,我坚信你最好使用本书框架中展示的混合方法,并接受你需要运用自己的判断的观点。因果图在你的直觉和数据之间来回推理——在许多情况下,这正是成功之道。

因果图的基本结构

因果图可以呈现出多种各样的形状。幸运的是,研究人员已经花了一段时间在因果性上,并为此带来了一些秩序:

  • 因果图只有三种基本结构——链式结构、分叉结构和碰撞器结构——所有的因果图都可以表示为它们的组合。

  • 将因果图看作家谱,我们可以轻松描述在图中相距较远的变量之间的关系,例如称其中一个为另一个的“后代”或“子节点”。

实际上,就是这样!我们现在将更详细地了解这些基本结构,并且一旦你熟悉了它们以及如何命名变量之间的关系,你将能够完全描述你所使用的任何因果图。

链式结构

链式结构是一个有三个框表示的因果图,代表三个变量,并且两个箭头直线连接这些框。为了向你展示一个例子,我将介绍我们 C-Mart 例子中的一个新的待处理物——那就是强大的甜甜圈。为了简单起见,让我们假设我们已经看到的变量只有一个影响到了甜甜圈的销售:冰咖啡销售量。那么 温度冰咖啡销售量甜甜圈销售量 是因果相关的 (图 3-9)。

链式因果图

图 3-9. 链式因果图

这张因果图之所以被称为链式结构,是因为两个箭头“同向”,即第一个箭头从一个框指向另一个框,第二个箭头从第二个框指向最后一个框。这张因果图扩展了 图 3-3 中的图示。它表示了温度导致冰咖啡销售,进而导致甜甜圈销售的事实。

让我们定义一些术语,以便描述变量之间的关系。在这个图表中,Temperature 被称为 IcedCoffeeSales父亲,而 IcedCoffeeSalesTemperature子孙。但是 IcedCoffeeSales 也是 DonutSales 的父亲,后者是其子孙。当一个变量与另一个变量有父/子关系时,我们称之为直接关系。当它们之间有中介变量时,我们称之为间接关系。变量数目的确切数量并不重要,因此您不必计算箱子的数量来描述它们之间关系的基本结构。

另外,我们说一个变量是另一个变量的祖先,如果第一个变量是另一个的父亲,后者可能是另一个的父亲,依此类推,最终我们的第二个变量是第一个变量的子孙。在我们的例子中,TemperatureDonutSales 的祖先,因为它是 IcedCoffeeSales 的父亲,后者又是 DonutSales 的父亲。非常合乎逻辑地,这使得 DonutSales 成为 Temperature后代(Figure 3-10)。

链条中变量之间的关系

图 3-10. 链条中变量之间的关系

在这种情况下,IcedCoffeeSales 也是 TemperatureDonutSales 之间关系的中介者。我们将在第十二章中更深入地探讨中介作用。现在,让我们注意到,如果一个中介值不变,则链条中前面的变量不会影响链条中后面的变量,除非它们以其他方式相连。在我们的例子中,如果 C-Mart 遭遇冰咖啡短缺,我们预计在此期间,温度的变化不会对甜甜圈的销售产生任何影响。

折叠链条

上述因果图转化为以下回归方程:

IcedCoffeeSales = β[T].Temperature

DonutSales = β[I].IcedCoffeeSales

我们可以用第二个方程中的表达式替换 IcedCoffeeSales

DonutSales = β[I].(β[T]Temperature) = (β[I]β[T])Temperature*

但是 β[I]β[T] 只是两个常数系数的乘积,所以我们可以将其视为一个新系数: DonutSales=β˜T.Temperature。我们已成功将 DonutSales 表示为 Temperature 的线性函数,这可以转化为因果图(Figure 3-11)。

将一个 CD 折叠成另一个 CD

图 3-11. 将一个 CD 折叠成另一个 CD

在这里,我们折叠了一个链条,也就是说,我们去除了中间的变量,并用从第一个变量到最后一个变量的箭头替换它。通过这样做,我们有效地简化了我们原始的因果图,专注于我们感兴趣的关系。当链条中的最后一个变量是我们感兴趣的业务指标,而第一个变量是可操作的时,这是有用的。在某些情况下,例如,我们可能对TemperatureIceCoffeeSales之间以及IceCoffeeSalesDonutSales之间的中间关系感兴趣,以管理定价或促销。在其他情况下,我们可能只对TemperatureDonutSales之间的关系感兴趣,例如,为了进行库存规划。

线性代数的传递性财产在这里也适用:如果DonutSales导致另一个变量,那么这个链条也可以围绕DonutSales折叠,依此类推。

扩展链条

显然,折叠操作可以反转:我们可以通过在中间添加IceCoffeeSales变量,从我们的最后一个 CD 回到前一个 CD。更一般地说,我们称之为扩展链条,每当我们在当前由箭头连接的两个变量之间注入中间变量时。例如,假设我们从TemperatureDonutSales的关系开始(图 3-11)。这种因果关系转化为方程DonutSales = β[T]Temperature。假设Temperature仅通过IceCoffeeSales影响DonutSales。我们可以在我们的 CD 中添加这个变量,这使我们回到了我们从图 3-8 开始的原始 CD(图 3-12)。

将一个 CD 展开成另一个 CD

图 3-12. 将一个 CD 展开成另一个 CD

扩展链条可以帮助更好地理解特定情况中发生的事情。例如,假设温度增加了,但甜甜圈的销售量没有增加。这可能有两个潜在的原因:

  • 首先,温度的增加并没有增加冰咖啡的销量,也许是因为店长更加积极地使用了空调。换句话说,在图 3-11 中,第一个箭头消失或减弱。

  • 或者,温度的增加确实增加了冰咖啡的销售量,但冰咖啡的销售增长并没有增加甜甜圈的销售量,例如,因为人们正在购买新推出的饼干。换句话说,在图 3-11 中,第一个箭头保持不变,但第二个箭头消失或减弱。

根据实际情况,你可能会采取非常不同的纠正措施——要么关闭空调,要么改变饼干的价格。在许多情况下,查看链条中间的变量,即中介者,将帮助你做出更好的决策。

注意

由于链条可以随意折叠或展开,通常我们不会明确指示何时进行折叠。通常假设任何箭头都可能被展开,以突出沿途的中介变量。

这也意味着之前提到的“直接”和“间接”关系的定义与 CD 的特定表示相关:当你折叠一个链条时,两个原本间接关系的变量现在有了直接关系。

分叉

当一个变量导致两个或更多效应时,这种关系形成了一个分叉温度同时导致冰咖啡销量冰淇淋销量,所以这种分叉的表示如图 3-13 所示。

三个变量之间的分叉

图 3-13. 三个变量之间的分叉

这个 CD 显示,温度影响了冰淇淋销量冰咖啡销量,但它们之间并没有因果关系。如果天气炎热,冰淇淋和冰咖啡的需求都会增加,但购买其中一种并不会导致你想购买另一种,也不会减少你购买另一种的可能性。

当两个变量有一个共同原因的情况非常频繁,但也可能存在潜在问题,因为这会在这两个变量之间创建一个相关性。当天气炎热时,我们会看到冰淇淋和冰咖啡的销量增加,而天冷时,很少有人会同时想要这两种产品。从预测角度来看,从冰咖啡销量预测冰淇淋销量的线性回归会有相当的预测性,但这里相关性并不等于因果关系,由于我们知道因果影响为 0,模型提供的系数将不准确。

另一种看待这种关系的方式是,如果 C-Mart 的冰咖啡短缺,我们不会预期看到冰淇淋销量的变化。更广义地说,说分叉是数据分析世界中的一大罪恶根源并不为过。每当我们观察到两个变量之间的相关性,而这种相关性不反映它们之间的直接因果关系(即彼此不是对方的原因),往往是因为它们共享一个共同原因。从这个角度来看,使用 CD 的主要好处之一是,它们可以非常清楚和直观地显示这些情况以及如何进行纠正。

分叉也是我们观察人口统计变量时的典型情况:年龄、性别和居住地都可能导致各种其他可能相互作用或者不会相互作用的变量。你可以把人口统计变量比如年龄想象成一个有很多分支的分叉。

当 CD 中间有叉路时,有时会出现一个问题,即你是否仍然可以将链条围绕它折叠。例如,假设我们有兴趣使用图 3-14 中的 CD 分析 SummerMonthIcedCoffeeSales 之间的关系。

带有叉路和链条的 CD

图 3-14. 带有叉路和链条的 CD

在这个 CD 中,SummerMonth 一侧有一个叉路,另一侧有 IceCreamSalesTemperature,但也有一个链条 SummerMonthTemperatureIcedCoffeeSales。我们可以折叠链条吗?

在这种情况下,是的。我们将在第五章中看到如何确定一个变量是否是关系的混杂因素;在这里,我们只需说 IceCreamSales 不是 SummerMonthIcedCoffeeSales 之间关系的混杂因素,这是我们感兴趣的关系。因此,我们可以简化我们的 CD(图 3-15)。

前一个 CD 的折叠版本

图 3-15. 前一个 CD 的折叠版本

同样,如果我们对 图 3-14 中 SummerMonthIceCreamSales 之间的关系感兴趣,我们可以忽略 IcedCoffeeSales,但不能忽略 Temperature

因为叉路对因果分析非常重要,有时我们会想要表示它们,即使我们不知道联合原因是什么。在这种情况下,我们将用一个双头箭头表示未知的叉路(图 3-16)。

具有未知联合原因的叉路

图 3-16. 具有未知联合原因的叉路

双头箭头也看起来像两个变量互相导致。这是有意设计的,当我们观察到两个变量之间的相关性,但我们不知道哪个导致哪个时,我们也会使用双头箭头。因此,双头箭头包括两个变量 A 和 B 会呈现相关的三个可能原因:A 导致 B,B 导致 A,和/或 A 和 B 共享一个原因。有时我们会使用双头箭头作为一个占位符,直到我们澄清真正的原因;如果我们不关心原因,我们可能会在最终的 CD 中保留双头箭头。

碰撞器

世界上很少有只有一个原因的事物。当两个或更多变量导致相同的结果时,关系就形成了一个碰撞器。由于 C-Mart 的小吃店只销售两种口味的冰淇淋,巧克力和香草,代表口味和购买行为的因果图会显示对任一口味的食欲都会导致在该店购买冰淇淋的过去经历(图 3-17)。

碰撞器的碰撞直径(CD)

图 3-17. 碰撞器的碰撞直径(CD)

碰撞器是常见的现象,它们也可能是数据分析中的问题。碰撞器在某种意义上是分叉的对立面,与它们相关的问题也是对称的:如果我们不控制共同原因,那么分叉就会成为问题,而如果我们控制共同影响,碰撞器就会成为问题。我们将在第五章中进一步探讨这些问题。

总结本节,链、分叉和碰撞器代表了因果图中三个变量之间可能的三种关系方式。然而,它们并非彼此互斥,事实上,在同一个因果图中同时展现这三种结构是相当常见的,正如我们在第一个例子中所看到的(图 3-18)。

同时包含链、分叉和碰撞器的三变量因果图

图 3-18. 同时包含链、分叉和碰撞器的三变量因果图

在这里,SummerMonth 影响 IceCreamSalesTemperature,而 Temperature 本身又影响 IceCreamSales。这里的因果关系相对简单易懂,但是这个图表也包含了所有三种基本关系类型:

  • 一个链:SummerMonthTemperatureIceCreamSales

  • 一个分叉,其中 SummerMonth 导致 TemperatureIceCreamSales

  • 一个碰撞器,IceCreamSales 同时受 TemperatureSummerMonth 的影响

在这种情况下需要注意的另一件事是,变量之间可能存在多于一种关系。例如,SummerMonthIceCreamSales 的父变量,因为直接从前者到后者存在箭头(直接关系);但与此同时,SummerMonth 也通过链 SummerMonth → Temperature → IceCreamSales(间接关系)间接是 IceCreamSales 的祖先。所以你可以看到这些关系并不是排他的!

尽管一个因果图(CD)总是由这三种结构组成,但它并非静态的。通过修改变量本身及其关系,一个因果图可以被转换,我们现在就来看一下。

因果图的常见转换

链、分叉和碰撞器假定因果图中的变量是给定的。但是,就像链可以收缩或扩展一样,变量本身也可以被切片或聚合以“缩放”到特定行为和类别中。我们也可以决定修改箭头,例如在面对其他无法处理的循环时。

切片/解构变量

当你切片或解构一个变量以揭示其组成部分时,分叉和碰撞器经常会被创建。在先前的例子中,我们看过 TemperatureDonutSales 之间的关系,其中 IcedCoffeeSales 是中介者(图 3-19)。

我们将切片的链

图 3-19. 我们将切片的链

但也许我们想要根据类型分割IcedCoffeeSales,以更好地理解需求动态。这就是我所说的“切片”变量。这是允许的线性代数规则,因为我们可以将总冰咖啡销量表达为按类型销量的总和,比如美式和拿铁:

IcedCoffeeSales = IcedAmericanoSales + IcedLatteSales

现在我们的 CD 将成为图 3-20,左侧有一个分叉,右侧有一个碰撞器。

一个链条,其中中介被切分了

图 3-20. 一个链条,其中中介被切分了

现在,该变量的每个切片将有自己的方程式:

IcedAmericanoSales = β[TA].Temperature

IcedLatteSales = β[TL].Temperature

由于温度的影响完全被我们的IcedCoffeeSales切片所中介,我们可以创建如下的统一多元回归用于DonutSales

DonutSales = β[IA].IcedAmericanoSales + β[IL].IcedLatteSales

这将使您能够更精细地了解发生了什么——当温度升高时,您是否应该计划这两种类型的销售增长?它们对DonutSales有相同的影响吗,还是您应该更青睐其中一种?

聚合变量

正如你可能猜到的那样,切分变量是可以反转的,而且更一般地,我们可以聚合那些具有相同因果关系的变量。这可以用于按产品、地区、业务线等进行聚合和分解数据分析。但它也可以更宽泛地用于表示那些没有精确定义的重要因果因素。例如,假设年龄性别都影响香草味以及在 C-Mart 便利店购买冰淇淋的倾向,PurchasedIceCream(图 3-21)。

年龄和性别分开显示

图 3-21. 年龄和性别分开显示

因为年龄性别具有相同的因果关系,它们可以被聚合成一个人口特征变量(图 3-22)。

CD,其中年龄和性别被聚合成单一变量

图 3-22. CD,其中年龄和性别被聚合成单一变量

在这种情况下,我们显然没有一个称为“人口特征”或“人口统计学”的单一列数据;我们只是在我们的 CD 中使用该变量作为一种简化,用于表示我们可能会或可能不会在以后深入探讨的各种变量。假设我们想要运行一项 A/B 测试并理解当前的因果关系。正如我们将在后面看到的那样,随机化可以使我们控制人口统计因素,这样我们就不必在分析中包含它们,但我们可能希望在没有随机化的情况下将它们包含在我们的 CD 中。如果有必要,我们总是可以扩展我们的图表以准确表示涉及的人口统计变量。然而,请记住,任何变量都可以分割,但只有具有相同直接和间接关系的变量才可以聚合

循环如何处理?

在我们看到的三种基本结构中,两个给定框之间只有一条箭头。更一般地说,不可能通过按箭头方向两次到达相同的变量(例如,A → B → C → A)。一个变量可能是另一个变量的效果和另一个变量的原因,但不能同时是一个变量的原因和效果。

然而,在现实生活中,我们经常看到相互因果影响的变量。这种类型的 CD 被称为循环。循环可能由于多种原因而产生;在行为数据分析中最常见的两种是替代效应和反馈循环。幸运的是,当你遇到循环时,有一些解决方法可以帮助你应对。

理解循环:替代效应和反馈循环

替代效应是经济学理论的基石:顾客可能会根据产品的可用性和价格以及顾客对多样性的欲望来替代一种产品为另一种产品。例如,前来 C-Mart 便利店的顾客可能会根据不仅仅是温度,还包括特别促销以及本周喝咖啡的频率来选择冰咖啡或热咖啡。因此,从购买冰咖啡到购买热咖啡存在因果关系,反之亦然(图 3-23)。

生成循环的替代效应 CD

图 3-23. 生成循环的替代效应 CD
注意

需要注意的一点是箭头的方向显示了因果关系的方向(什么是原因,什么是结果),而不是效果的符号。在我们之前看过的所有因果图中,变量之间存在正相关关系,其中一个增加导致另一个增加。在这种情况下,关系是负的,其中一个变量的增加会导致另一个变量的减少。对于因果图来说,效果的符号并不重要,只要你正确地识别相关的因果关系,回归分析就能正确地排序系数的符号。

另一个常见的循环是反馈环路,其中一个人根据环境变化调整他们的行为。例如,C-Mart 的店长可能会关注等待队列的长度,如果现有的队列太长,会开设新的收银台,这样顾客就不会放弃并离开(图 3-24)。

反馈环路生成循环的示例

图 3-24. 反馈环路生成循环的示例

管理循环

循环反映了通常复杂的研究和管理情况,因此专门涉及这一领域的研究,称为系统思维,已经为此目的而兴起。(2)为了准确处理循环,已经开发了复杂的数学方法,如结构方程建模,但是它们的分析超出了本书的范围。然而,如果我不给出任何解决方案,那就不尽职责,因此我将提到两个经验法则,这应该能避免你被循环困扰。

第一点是要密切关注时间。在几乎所有情况下,一个变量影响另一个变量需要一定的时间,这意味着你可以通过更细致的时间粒度来观察数据,从而“打破循环”,将其转变为“非循环”的因果图,即没有循环的因果图(然后可以用本书介绍的工具进行分析)。例如,假设店长在等待时间增加后需要 15 分钟来开新的收银台,而顾客调整他们的等待时间感知也需要 15 分钟。在这种情况下,通过澄清事物的时间顺序,我们可以在我们的因果图中分割等待时间变量(图 3-25)。

将反馈环路分解为时间增量

图 3-25. 将反馈环路分解为时间增量

我将一点一点地解释这个因果图。在左侧,我们有一个从平均等待时间指向等待顾客数量的箭头:

NbCustomersWaiting(t + 15mn) = β[1].AvgWaitingTime(t)

这意味着,例如,上午 9:15 等待的顾客数量将以上午 9:00 的平均等待时间作为函数表达。然后,上午 9:30 等待的顾客数量将与上午 9:15 的平均等待时间有相同的关系,依此类推。

同样,在右侧,我们有一个从平均等待时间到开放的排队线的箭头:

NbLinesOpen(t + 15mn) = β[2].AvgWaitingTime(t)

这意味着,上午 9:15 开放的排队线数量将以上午 9:00 的平均等待时间作为函数表达。然后,上午 9:30 开放的排队线将与上午 9:15 的平均等待时间有相同的关系,依此类推。

然后在中间,我们有从等待顾客数量和从开放的收银台数量到平均等待时间的因果箭头。为了简单起见,在这里假设线性关系,这将转化为以下方程:

AvgWaitingTime(t) = β[3].NbCustomersWaiting(t) + β[4].NbLinesOpen(t)

实际上,在这种情况下假设线性关系是非常不可能成立的。对于队列或时间到事件变量(例如生存分析),已经开发了特定的模型。这些模型属于更广泛的广义线性模型类别,因此,我们的经验法则是它们在我们的目的下表现得像逻辑回归。

这意味着,顾客在上午 9:15 到达收银台排队时的平均等待时间取决于 9:15 时已经在场的顾客数量以及 9:15 时开放的收银台数量。然后,顾客在上午 9:30 到达收银台排队时的平均等待时间取决于 9:30 时已经在场的顾客数量以及 9:30 时开放的收银台数量,依此类推。

通过将变量分解为时间增量,我们已经能够创建一个没有严格意义上的循环的 CD。我们可以在不引入任何循环逻辑的情况下估计前述三个线性回归方程。

处理周期的第二条经验法则是简化你的 CD,并只保留你最感兴趣的因果路径上的箭头。反馈效应(其中一个变量影响刚刚影响它的变量)通常较小,往往比第一效应小得多,可以作为第一近似忽略。

在我们冰咖啡和热咖啡的例子中,当天气炎热时冰咖啡的销售增加可能会减少热咖啡的销售,这是一个你应该调查的合理担忧。然而,不太可能热咖啡销售的减少会进一步触发冰咖啡销售的增加,因此你可以在你的 CD 中忽略这种反馈效应(图 3-26)。

简化 CD 图表,忽略某些关系

图 3-26. 简化 CD 图表,忽略某些关系

在图 3-26 中,我们删除了从热咖啡购买到冰咖啡购买的箭头,并将该关系视为一个合理的近似。

再次强调,这只是一个经验法则,并绝对不是对忽略循环和反馈效应的一揽子邀请。这些应该在您的完整因果图中完全表示,以指导未来的分析。

路径

在看到各种变量如何相互作用之后,我们现在可以介绍最后一个涵盖所有内容的概念:路径。我们说两个变量之间存在路径如果它们之间有箭头连接,无论箭头的方向如何,并且路径中没有变量重复出现。让我们看看我们之前见过的因果图(图 3-27)中的路径是什么样子的。

因果图中的路径

图 3-27. 因果图中的路径

在前面的因果图中,从SummerMonthIcedCoffeeSales有两条路径:

  • 一条沿着链的路径SummerMonthTemperatureIcedCoffeeSales

  • 第二条路径通过IceCreamSalesSummerMonthIceCreamSalesTemperatureIcedCoffeeSales

这意味着一个链是一条路径,但分叉或碰撞器也是如此!还要注意,两个不同的变量之间可能会有多条路径共享一些箭头,只要它们之间至少有一个差异,就像这里的情况一样:从TemperatureIcedCoffeeSales的箭头出现在这两条路径中。

然而,以下路径不是TemperatureIcedCoffeeSales之间的有效路径,因为Temperature出现了两次:

  • TemperatureSummerMonthIceCreamSalesTemperatureIcedCoffeeSales

这些定义的一个结果是,如果你在一个因果图中选择两个不同的变量,它们之间总会至少有一条路径。路径的定义可能看起来如此宽泛以至于没有用处,但正如我们将在第五章中看到的那样,路径实际上将在识别因果图中的混杂变量中发挥关键作用。

结论

相关性并非因果关系,因为混杂变量可能会在我们的分析中引入偏倚。不幸的是,正如我们通过例子所看到的那样,简单地将所有可用变量和乱七八糟的变量放入回归中并不足以解决混杂问题。更糟糕的是,控制错误的变量可能会引入虚假相关性并产生新的偏倚。

作为朝向无偏回归的第一步,我介绍了一种称为因果图的工具。因果图可能是你从未听说过的最佳分析工具。它们可以用来表示我们对真实世界中因果关系的直觉,以及我们数据中变量之间的因果关系;但最强大的是作为将我们的直觉和专家知识与观察数据相连接的桥梁,反之亦然。

CDs 可以变得错综复杂,但它们基于三个简单的构建模块:链、分叉和碰撞器。根据与线性代数一致的简单规则,它们还可以折叠或扩展、切片或聚合。如果你想了解更多关于 CDs 的内容,Pearl 和 Mackenzie(2018)是一本非常易读且令人愉快的书面介绍。

CDs 的全部威力将在 第五章 中显现,我们将看到它们允许我们在回归中最优地处理混杂因素,即使是非实验数据。但 CDs 在更广泛的范围内也很有帮助,帮助我们更好地思考数据。在接下来的章节中,当我们开始清理和准备数据进行分析时,它们将允许我们在分析之前减少数据中的偏差。这将为你提供在简单环境中更加熟悉 CDs 的机会。

^(1) 在 CDs 中,表示未观察变量最常见的方法是使用椭圆形而不是矩形。

^(2) 对感兴趣的读者推荐阅读 Meadows 和 Wright(2008)的《系统思维入门》,以及 Senge(2010)的《第五项修炼:学习型组织的艺术与实践》。

第四章:从头开始建立因果图

此时,您可能会想知道[因果图]的来源。这是一个很好的问题。这可能是一个问题。 [CD]应该是您研究的现象的关于其最先进知识的理论表示。这是一个专家认为是事物本身的东西,这种专业知识来自于多种来源。例如,经济理论,其他科学模型,与专家的交流,您自己的观察和经验,文献综述,以及您自己的直觉和假设。

Scott Cunningham,《因果推断:混音带》(2021)

本书的目标始终是衡量一个变量对另一个变量的影响,我们可以将其表示为“起始”CD(图 4-1)。

最简单的 CD

图 4-1。最简单的 CD

一旦你画出那种关系,接下来会发生什么?你如何知道你应该包括哪些其他变量或不包括哪些变量?许多作者说你应该依靠专家知识,如果你在像经济学或流行病学这样的已建立领域工作的话,这是可以的。但在本书中,我的观点是,你很可能是你组织中的“行为科学家第一人”,因此你需要能够从一张空白的画布开始。

在本章中,我将概述一种配方,帮助您从图 4-1 的基本 CD 到一个可行的 CD。在我们进行这个过程的同时,请记住我们的最终目标是理解是什么推动行为,以便我们可以为我们的业务得出相关和可操作的结论。我们的目标不是建立对整个世界的完整和精确的知识。捷径和近似是公平的游戏,一切都应根据一个标准评估:这是否有助于我实现我的业务目标?

此外,我将概述的配方不是一个机械算法,你可以盲目地遵循到正确的 CD。相反,商业常识,常识和数据见解将是至关重要的。我们将在我们对手头因果关系的定性理解和数据中的定量关系之间来回反复,相互核对,直到我们觉得我们有一个令人满意的结果。在这里,“令人满意”是一个重要的词:在应用环境中,通常你不能告诉你的经理,你将在三年内给他们正确的答案。你需要在短期内给他们最不坏的答案,同时规划数据收集工作,以改进多年来的答案。

在接下来的部分中,我将为本章的业务问题及相关的兴趣变量提供介绍。然后,我们将按照以下步骤逐步构建相应的 CD,并逐步进行各个部分的描述:

  • 确定可能/应该包含在 CD 中的变量。

  • 确定是否应该包含变量。

  • 根据需要迭代这个过程。

  • 简化图表。

让我们开始吧!

业务问题和数据设置

在本节中,我们将使用一个位于同一城市内的两家酒店的实际预订数据集。数据和我们将使用的包将在下一小节中描述,并且我们将深入研究理解感兴趣的关系。

数据和包

本章的GitHub 文件夹包含名为chap4-hotel_booking_case_study.csv的 CSV 文件,其中列出了表 4-1 中的变量。

表 4-1. 数据文件中的变量

变量名 变量描述
NRDeposit (NRD) 二进制 0/1,预订是否有不可退还押金
IsCanceled 二进制 0/1,预订是否取消
DistributionChannel 分类变量,取值为“直接”,“公司”,“TA/TO”(旅行代理/旅行组织),“其他”
CustomerType 分类变量,取值为“瞬时”,“瞬时-团体”,“合同”,“团体”
MarketSegment 分类变量,取值为“直接”,“公司”,“在线 TA”,“离线 TA/TO”,“团体”,“其他”
Children 整数,预订中的儿童人数
ADR 数值,平均每日费率,总预订金额/天数
PreviousCancellation 二进制 0/1,客户之前是否取消过预订
IsRepeatedGuest 二进制 0/1,客户是否之前曾在酒店预订过
Country 分类,客户的国家/地区
Quarter 分类,预订季度
Year 整数,预订年份

在本章中,除了前言中提到的标准包装外,我们还将使用以下包装:

## R
library(rcompanion) # For Cramer V correlation coefficient function
library(car) # For VIF diagnostic function

## Python
from math import sqrt # For Cramer V calculation
from scipy.stats import chi2_contingency # For Cramer V calculation

理解感兴趣的关系

我们将尝试回答这个问题:“押金类型是否影响预订的取消率?”正如图 4-2 所述。

感兴趣的因果关系

图 4-2. 感兴趣的因果关系

让我们从按押金类型分类的基础取消率开始(我喜欢同时查看绝对数和百分比,以防某些类别的数值非常小):

## R (output not shown)
with(dat, table(NRDeposit, IsCanceled))
with(dat, prop.table(table(NRDeposit, IsCanceled), 1))

## Python
table_cnt = dat_df.groupby(['NRDeposit', 'IsCanceled']).\
agg(cnt = ('Country', lambda x: len(x)))
print(table_cnt)

table_pct = table_cnt.groupby(level=0).apply(lambda x: 100 * x/float(x.sum()))
print(table_pct)
                        cnt
NRDeposit IsCanceled       
0         0           63316
          1           23042
1         0              55
          1             982
                            cnt
NRDeposit IsCanceled           
0         0           73.318048
          1           26.681952
1         0            5.303761
          1           94.696239

我们可以看到,绝大多数预订没有押金,取消率约为 27%。另一方面,有不可退还押金(NRDeposit)的预订取消率非常高。乍看之下,这种相关性令人惊讶。如果我们将政策改为对所有人“无需押金”,能减少取消率吗?行为常识告诉我们,更有可能的是,酒店为“高风险”预订请求不可退还押金,并且存在混杂变量,如图 4-3 中所示。

因果关系可能会受到干扰

图 4-3。因果关系可能会受到干扰

我们很快就从 Figure 4-2 转到 Figure 4-3,但这是一个重要的步骤:Figure 4-2 中的 CD 代表了一个基本的商业分析问题,“押金类型与取消率之间的因果关系是什么?”另一方面,Figure 4-3 中的 CD 代表了一个更加明智的行为假设:“不可退还的押金似乎会增加取消率,但这种关系可能会受到我们需要确定的因素的干扰。”

使用 CD 进行行为数据分析的一个好处是它们是一个很好的协作工具。你组织中任何对 CD 有最基本了解的人都可以看到 Figure 4-3,并说,“嗯,是的,我们要求假期预订支付不可退还的押金,这些押金经常因为天气原因而被取消,”或者其他任何行为知识的片段,否则你无法得到。

在这一点上,最好的下一步是进行随机实验:将可退还或不可退还的押金分配给一组随机样本的客户,您将能够确认或否定您的行为假设。然而,您可能无法这样做,或者还没有准备好。与此同时,我们将尝试通过确定要包括的相关变量来解除关系的混淆。

确定要包括的候选变量

在尝试确定要包括的潜在变量时,自然的倾向是从您可以获得的数据开始。这种倾向是误导性的,类似于醉酒的人不是在他们丢失的地方找钥匙,而是在路灯下找,因为那里更亮。这样做可能会忽略最重要的变量,因为它们不在你的视线范围内。你也更可能只是看到数据中的变量,并不质疑它们是否是现实世界中发生情况的最佳表示。

例如,数据中的分类变量很可能代表的是业务中心的观点,而不是客户中心的观点,因此将一些类别聚合在一起或甚至将不同变量合并为新变量可能更合适。在我们的案例中,有一个变量MarketSegment和一个用于预订中儿童数量的变量。通过查看数据,我们可以确认很少有公司客户会带着孩子来。因此,我们可以考虑创建一个新的分类变量,其中包括“没有孩子的公司”,“没有孩子的非公司”和“有孩子的非公司”,将带有孩子的公司客户视为值得进行单独调查的异常值(也许是量身定制服务的基础?)。

我们将从行为类别开始,而不是陷入“你看到的就是所有”的偏见^2,从动作向后开始,详细了解第二章中概述的行为类别。

  • 行动

  • 意图

  • 认知与情绪

  • 个人特征

  • 业务行为

最后,每个类别中的变量可能受到时间趋势的影响,如线性趋势或季节性,因此我们将在本节末尾默认添加这些内容。为了加强对定性直觉的关注,我们不会在下一节关于验证关系的内容之前查看任何数据。通过这些类别替换我们的先验取消风险(以及其他潜在混杂因素),我们的 CD 现在看起来像是 图 4-4,其中添加了一堆未观察到的变量到我们的两个感兴趣的变量中。

包含潜在变量类别的更新 CD

图 4-4. 包含潜在变量类别的更新 CD

对于每个类别,我们现在将寻找可能成为我们两个感兴趣的变量之一的原因的变量。

行动

在寻找要包含在操作类别中的变量时,我们通常试图识别客户过去的行为,这些行为可能会影响酒店是否需要非退款押金(NRD)。

在这种情况下,一个明显的候选者是客户是否以前取消过。也许酒店更有可能要求那些过去曾经取消的客户支付非退款押金。同样可以想象,导致他们过去取消的原因也更可能导致他们将来取消。

更普遍地说,当我们感兴趣的变量之一本身就是一个行为时,过去的行为通常是一个很好的预测变量,即使只是作为未观察到的个人特征的代理。我们的数据中有两个与过去行为相关的变量:PreviousCancellationIsRepeatedGuest。图 4-5 展示了我们更新的 CD,未更改的部分为灰色。

在操作步骤结束时更新的 CD

图 4-5. 在操作步骤结束时更新的 CD

这并不意味着这些是唯一相关的过去行为;它们只是我想到的并且我们有数据的唯一一些。希望你能想到其他的!

意图

意图在数据分析中很容易被忽视,因为它们通常在我们现有的数据中丢失。然而,它们是行为的最重要驱动因素之一,而且通常可以通过对客户和员工进行访谈来揭示。因此,它们代表了不仅仅查看现有可用数据,而是采用“首先关注行为”的方法的好处的最佳例证。

在这种情况下,我能想到两个意图:旅行的原因和取消的原因(图 4-6)。

将意图添加到我们的 CD

图 4-6. 将意图添加到我们的 CD

请注意,我将TripReason表示为潜在混杂因素,即,它与我们感兴趣的两个变量都有箭头相连,而CancellationReason只影响IsCanceled。此时,这只是一个行为直觉,即取消原因不会影响押金类型。我的理由是,在存款时并不知道取消原因。

图 4-6 还展示了 CD 在行为分析中的多功能性:即使我们不知道任一情况下的具体原因列表,我们也可以在 CD 中记录这两个潜在变量,稍后我们将通过访谈确定它们。暂时来看,我们可以注意到我们数据中的三个变量似乎受到旅行原因的影响,并可以作为这样包括进去:CustomerTypeMarketSegmentDistributionChannel (图 4-7)。我们也将在个人特征的小节中重新审视这些变量。

意图步骤结束时更新的 CD

图 4-7. 意图步骤结束时更新的 CD

认知与情感

在尝试为分析确定相关社会、心理或认知现象时,我喜欢放大特定的决策点。在这里,客户进行预订和取消预订时就是这样的决策点。

在第一个决策点,顾客可能不明白他们的押金是不可退的,或者他们可能会忘记。在第二个决策点,他们可能将押金视为沉没成本,不会努力保留他们的预订 (图 4-8)。

认知与情感步骤结束时更新的 CD

图 4-8. 认知与情感步骤结束时更新的 CD

个人特征

如 第二章 中所述,人口统计变量通常并非因其本身而有价值,而是作为其他个人特征(如人格特质)的代理。因此,在这一步骤中的挑战是抵制数据中存在的任何人口统计变量的影响,并坚持我们的因果行为思维模式。在查看人口统计变量之前,首先考虑特质是一个很好的方法。

特质

基于我们对人格心理学的了解,导致取消行为的良好候选特质是责任心和神经质:似乎不太有组织和更为放松的人更有可能取消预订 (图 4-9)。

具有个性特征更新的 CD

图 4-9. 具有个性特征更新的 CD

人口统计变量

我们早前注意到,我们的酒店有企业客户和非企业客户预订。除了旅行和取消的原因外,这还影响一些其他个人特征,例如价格弹性和收入,这两者会影响我们感兴趣的两个变量。让我们将这些归为“财务特征”类别。它们可能在我们的数据中有所体现,例如CustomerTypeMarketSegmentDistributionChannel,以及其他几个变量,如ChildrenADR(即每晚的平均价格)和Country(图 4-10)。

使用人口统计变量更新的 CD

图 4-10. 使用人口统计变量更新的 CD

商业行为

商业行为在我们调查的关系中往往起着重要作用,但很容易被忽视和难以整合。

在这个例子中,业务规则显然起着重要作用,因为它们决定了哪些客户需要提供 NRD。从这个意义上说,它们影响CD中进入NRDeposit所有箭头。我们可以根据它采取多种方式来考虑这种影响的形式。

一个业务规则可以明确地连接两个可观察变量(可能包括我们感兴趣的变量)。例如,在这里,我们可以想象一个业务规则,规定所有之前取消预订的客户现在必须提供 NRD。通过列出所有这些规则,我们可以确认或否认从一个观察变量直接进入NRDeposit的所有箭头。这也可能揭示涉及业务规则但尚未包含在我们数据中的变量:例如,我们可以想象,如果客户在预订时未提供身份证明,则必须支付 NRD。我说“尚未包含在我们数据中”,因为根据定义,任何作为业务规则一部分的标准都是可观察的,即使它没有被数据库捕获。^(3)

或者,一个业务规则可能最好表示为一个额外的中介变量。例如,如果在圣诞节期间的所有预订必须由 NRD 支持,我们可以创建一个ChristmasHolidays二元变量,该变量与NRDeposit相互作用。那个变量将会调节其他变量(如CustomerTypeChildren)对NRDeposit的影响。

我们不知道我们示例中的两家酒店应用了什么业务规则,因此我们必须将该子部分留作我们希望通过后续访谈探索的内容。

时间趋势

最后,我们的数据中可能存在一些全局时间趋势,例如需求 NRD 的预订数量逐步增加,以及与此同时的取消率逐步增加,但二者无关。此外,酒店行业的季节性非常强烈,可能存在一些周期性方面,我们希望能够捕捉到(图 4-11)。

时间趋势步骤结束时的更新 CD

图 4-11. 时间趋势步骤结束时的更新 CD
注意

在这种情况下,年份季度 变量仅捕捉趋势和周期。有时,也可以考虑包括二元变量以解释使某年突出或标志性变化的特定事件。一个明显的例子是 COVID-19,在灰尘落定后,它将被证明在某些行业是暂时的起伏,但在其他行业则是根本性的变革的开始。

加上这最后一个变量之后,图 4-11 现在有一系列候选变量,一些是可观察的,一些不是。在下一节中,我们将看到如何确认保留哪些可观察变量。

根据数据验证要包含的可观察变量

让我们看看我们在识别阶段结束时作为候选的可观察变量(图 4-12)。

我们 CD 中的可观察变量,按类别(左)和数字(右)分割

图 4-12. 我们 CD 中的可观察变量,按类别(左)和数字(右)分割

在这个具体的例子中,所有这些可观察变量暂时都与我们感兴趣的两个变量有关。这是默认情况,但在某些情况下,您可能有非常强的先验理由,只将预测变量与您感兴趣的一个变量连接起来(例如,这是我们一些未观察到的变量的情况)。如果有疑问,我会谨慎一些,并包括这两种连接。

在 图 4-12 中,可观察变量被分为分类(CD 左侧)和数字(CD 右侧)。这两种数据类型需要不同的定量工具,因此我们将依次查看它们。

数字变量之间的关系

我们的第一步将是查看数据中所有数字变量的相关矩阵。一个有用但不那么干净的技巧是将二元变量转换为 0/1(如果它们还没有以这种格式存在),这样你可以将它们视为数字变量。这使你能够了解变量之间的相关性,但不要告诉你的统计学朋友!

查看你感兴趣的两个变量的行,可以让你看到它们与数据集中所有数值变量的相关性如何。一眼就能看到它还会显示出这些其他变量之间的任何大相关性。与感兴趣的因果相关性的强度可以帮助我们决定如何处理特定变量。

“强有多强?”这取决于情况。记住我们的目标是正确测量我们感兴趣的原因对我们感兴趣的效果的因果效应;作为一个经验法则,你可以认为与你感兴趣的原因和效果之间具有相同数量级(即逗号后第一个非零数字之间的零个数)的任何相关性都是“强”的。

正如你在 图 4-13 中所看到的,我们感兴趣的两个变量之间的相关系数为 0.16。第一列指示了与 NRDeposit 的相关性,第二列指示了与 IsCanceled 的相关性。PreviousCancellation 与我们感兴趣的变量的相关系数相同数量级(分别为 0.15 和 0.13)。类似地,ADRIsCanceled 的相关系数在该标准下是显著的(0.13)。

“数量级”阈值的包含并不科学,可以根据手头变量的数量来调节松紧程度。如果少数变量通过了阈值而其他变量接近该阈值,那么包含它们是完全可以接受的。

你可能会反对说,一个变量与我们感兴趣的变量之一的相关性可能很低,但仍然是需要考虑的混杂因素。这是正确的,根据强有力的理论基础,即使与我们感兴趣的变量的相关性很弱,你也可以包含一个变量。然而,为了实际目的,通常应重点关注与我们感兴趣的变量至少具有中等相关水平的变量。

数字和二进制变量的相关性矩阵

图 4-13. 数字和二进制变量的相关性矩阵

如果我们在 图 4-13 中包括所有绝对值为 0.1 或以上的相关性,并排除其他相关性,那么我们的 CD 现在就如同 图 4-14 中所示。

更新后的数字和二进制可观察变量的 CD

图 4-14. CD,更新了数字和二进制可观察变量的箭头

虽然相关矩阵只给出了对称系数,可以代表任何方向的箭头,但我运用了一些常识和业务知识来假设箭头的方向。酒店公司对时间没有掌控,所以我们可以假设Years是与它相关的变量的原因而不是结果,尽管这种影响可能通过中间变量(如随时间的社会趋势)传递。IsRepeatedGuestPreviousCancellation的先决条件;因为它涉及过去事件,它也必须是ADR的原因或它们共享一个共同的原因。

注意

不要忘记这只是一个暂时的CD:

  • 这些相关性中有一些可能是假阳性(系数看起来比实际强,纯粹是因为随机性),反之亦然,一些较小的相关性可能是假阴性。

  • 在这个阶段,我们暂时将相关性视为因果关系的证据。我们在图 4-14 中画的一些箭头本身可能反映了混杂关系。在充分衡量NRDepositIsCanceled之间的关系之后,我们可能希望或需要对其他关系(例如IsRepeatedGuestADR之间的关系)做同样的事情。

分类变量之间的关系

对于分类变量,同样的逻辑适用,唯一的复杂之处在于我们不能使用皮尔逊相关系数。然而,已经为分类变量开发了一个变体,克莱默 V。在 R 中,它是在rcompanion包中实现的:

## R
> with(dat, rcompanion::cramerV(NRDeposit, IsCanceled))
Cramer V 
   0.165

您可以看到,在二元变量的情况下,它产生的结果与直接应用皮尔逊相关系数的结果非常接近。不幸的是,它在 Python 中没有实现,但我提供了一个计算它的函数:

## Python
def  CramerV(var1, var2):
    ...
    return V

V = CramerV(dat_df['NRDeposit'], dat_df['IsCanceled'])   
print(V)
0.16483946381640308

图 4-15 显示了相应的相关矩阵。

这种相关性产生了多种见解。查看底部行,我们可以看到Quarter与其他任何东西都没有明显相关性。这表明季节性对我们的分析不是一个相关因素。相反,可能一个季度作为时间单位太粗略,我们需要放大到特定的时间段,如圣诞节假期。我们可以从我们的 CD 中删除Quarter,并用一个未观察到的变量Seasonality替换它,作为未来研究的线索。

我们的三个客户段变量 CustomerTypeMarketSegmentDistributionChannel 显示出混合模式,它们之间有些非常强的和一些弱的相关性。同样,它们与其他变量的相关性也参差不齐:例如,这三个变量中的每一个与 Country 的相关性都在 0.1X 数字范围内,但其中两个与 RepeatedGuest 的相关性较高(0.35 和 0.4),而第三个的相关性仅为 0.11。这表明这些变量不仅可以互换,而且它们捕捉了相同行为的某些方面。这需要进一步调查,并很可能创建新的变量。

用于分类和二进制变量的相关矩阵

图 4-15. 用于分类和二进制变量的相关矩阵

应用这些见解和仅包含大于 0.1 的相关性的相同标准后,我们的 CD 现在看起来像是 Figure 4-16。

用于分类和二进制可观察变量的更新箭头的 CD

图 4-16. 用于分类和二进制可观察变量的更新箭头的 CD

我们的 CD 开始变得适度复杂,但大部分可以通过几个行为参数来总结:

  • 左侧的四个变量反映个人特征,它们彼此之间显著相关。我选择用双向箭头来反映这些相关性,因为试图确定箭头的方向是毫无意义的:CustomerType 不会比 MarketSegment 更多地导致另一种情况发生。实际上,在进行必要的访谈后,我们应该创建捕捉更深层次个人特征的新变量。

  • 个人特征似乎会影响我们感兴趣的变量,可能导致一些混杂。

  • 个人特征似乎影响了过去的行为 IsRepeatedGuestPreviousCancellation。(同样,我基于业务知识对效果的方向做出假设。乍看之下,取消先前预订是否会导致某人改变国家或市场细分似乎不太可能。)一旦我们澄清了发挥作用的更深层个人特征的性质,我们可能决定将这些过去的行为纳入某些个人特征变量之下,隐式地创建行为人物(例如,“经常出差的商旅者(Y/N)”)。

数值和分类变量之间的关系

测量数值和分类变量之间的相关性比在同质类别内测量相关性更加繁琐。

说数值变量与分类变量之间存在相关性等同于说数值变量在分类变量的各类别间平均值不同。我们可以通过比较数值变量在分类变量的各类别间的平均值来检查是否如此。例如,我们预计客户的财务特征可能会影响预订的平均每日费率。最好在构建更好的客户分割变量之后再探索这种关系,但为了论证的目的,我们可以使用CustomerType

## R (output not shown)
> dat %>% group_by(CustTyp) %>% summarize(ADR = mean(ADR))
## Python
dat_df.groupby('CustTyp').agg(ADR = ('ADR', np.mean))
Out[10]: 
                        ADR
CustTyp                    
Contract          92.753036
Group             84.361949
Transient        110.062373
Transient-Party   87.675056

我们可以看到,平均每日费率在不同客户类型之间有显著变化。

注意

如果你不确定这些变化是否真正显著,或者它们只是反映了随机抽样误差,你可以使用 Bootstrap 为它们建立置信区间,如后文在第七章中解释的那样。

在我们的例子中,有两个数值变量,我们可能想要检查它们与分类变量的相关性:ADRYear。我们发现ADR在不同客户类型之间有显著变化,但这些变化在时间上相对稳定,这就导致了我们的最终可观测变量的 CD(图 4-17)。

在这一点上,我想重申并扩展我早些时候的警告:在验证可观测变量时,我已经默认假设相关性等于因果关系。但也许这些关系本身是混杂的:个人特征变量与PreviousCancellation之间的相关性可能完全是由个人特征变量与IsRepeatedGuest之间的关系造成的。

可观测变量的最终 CD

图 4-17. 可观测变量的最终 CD

举例来说,假设商务客户更有可能是重复客户。因此,他们看起来也可能比休闲客户有更高的取消率,即使在重复客户中,商务客户和休闲客户的取消率完全相同。

你可以把这些因果假设看作是善意的谎言:它们并非真实,但没关系,因为我们并不试图构建真实完整的 CD,我们只是试图解开NRD与取消率之间的关系。从这个角度来看,把箭头的方向搞对比有关联变量的不相关关系*重要得多。如果你仍然持怀疑态度,下一章的一个练习将进一步探讨这个问题。

逐步扩展因果图

在确认或否定基于数据的可观测变量之间的关系之后,我们得到了一个初步完整的 CD(图 4-18)。

暂时完成的 CD,包括可观察和不可观察的变量,在可读性上将个人特征变量分组到一个标题下

图 4-18. 暂时完成的 CD,包括可观察和不可观察的变量,在可读性上将个人特征变量分组到一个标题下

从这里开始,我们将通过识别未观察到的变量的代理,以及进一步识别当前变量的原因,来迭代地扩展我们的 CD。

识别未观察到的变量的代理

未观察到的变量代表一种挑战,因为即使通过访谈或 UX 研究确认了它们,也不能直接在回归分析中考虑它们。

我们仍然可以通过访谈和研究来尝试在一定程度上减轻它们。例如,我们可能会发现,责任心确实与取消率较低相关,但同时也与请求确认电子邮件相关(图 4-19)。

识别未观察到的变量的代理

图 4-19. 识别未观察到的变量的代理

当然,请求确认电子邮件并不仅仅是由责任心引起的——它也可能反映出意图的严肃性,对数字渠道的不熟悉等等。反过来,它可能通过提供有关预订的易于获取的信息而单独降低取消率。无论如何,如果我们发现这种行为与取消率呈负相关,我们可以利用这一洞察,例如向没有选择接收确认电子邮件的客户发送短信提醒。

通过头脑风暴和通过研究验证潜在未观察到的变量的代理,我们为可观察变量之间提供了有意义的连接。知道RequestedConfirmation通过ConscientiousnessIsCanceled相连接,为否则将是原始统计规律的行为基础提供了理由。

识别进一步的原因

我们还将通过识别“外部”变量的原因来扩展我们的 CD,即当前在我们的 CD 中没有任何父变量的变量。特别是,当我们有一个影响我们感兴趣的原因(可能间接影响)但不影响我们感兴趣的效果的变量A,以及另一个相反地影响我们感兴趣的效果但不影响我们感兴趣的原因的变量B时,AB的任何联合原因会在我们的 CD 中引入混淆,因为该联合原因也是我们两个感兴趣变量的联合原因。

在我们的例子中,唯一没有任何父变量(可观察或不可观察)的可观察变量是Year,显然它不能有一个(除了可能是物理定律?),所以这一步不适用。

迭代

当您引入新变量时,您将为代理和进一步的原因创造新的机会。例如,我们新引入的RequestedConfirmation可能会受到ConscientiousnessTripReason的影响。这意味着您应继续扩展您的 CD,直到看起来能够涵盖您能想到的所有相关变量及其相互关系。

然而,这一过程存在显著的递减回报:随着您将 CD“向外扩展”,新添加的变量往往与您感兴趣的变量之间的相关性越来越小,因为沿途的噪声太多。这意味着考虑它们将逐渐解除您感兴趣的关系。

简化因果图

一旦决定停止迭代扩展 CD,最终步骤就是对其进行简化。事实上,您现在有一个希望准确而完整的图表,用于实际目的,但可能结构不一定最有助于满足业务需求。因此,我建议进行以下简化步骤:

  • 当中间变量不重要或未被观察时,请折叠链条。

  • 当您需要查找观察变量或想要跟踪另一个变量与图表的关系方式时,请扩展链条。

  • 当您认为单个变量会包含有趣信息时(例如,与您感兴趣的变量的相关性主要由特定切片驱动),请切片变量。

  • 结合变量以增强图表的可读性或当不同类型之间的变化并不重要时。

  • 当您发现循环时,请通过引入中间步骤或确定重要关系的方面来打破它们。

在我们的示例中,我们可能会决定IsRepeatedGuestChildrenYear并未提供比PreviousCancellationADR更多的价值。事实上,我们可以放弃这三个变量,因为它们不会混淆我们感兴趣的关系(图 4-20)。

简化后的最终 CD

图 4-20. 简化后的最终 CD

您应该得到一个干净且(在某种程度上!)可读的图表,尽管它可能比我们迄今见过的图表要大一些。

如果这一过程看起来很长而有些乏味,那是因为它确实如此。深入了解您的业务非常重要,这是能够在无法进行实验时进行客户(或员工)行为因果推断的代价。

幸运的是,这个过程是非常累积和可转移的。一旦您为某项分析完成了这个过程,您对于对您业务重要的因果关系的认识就可以被重用于另一个分析。即使您第一次进行这个过程时不深入,您也可以只专注于一类混杂因素和原因;下一次您运行这个分析或类似的分析时,您可以从离开的地方继续,并对另一类进行更深入的挖掘,也许是关于客户体验的不同方面进行访谈。同样,一旦有人经历了这个过程,新的团队成员或员工可以非常容易和快速地获得相应的知识,并通过查看结果 CD 甚至只是记住要牢记的相关变量列表继续工作。

结论

用陈词滥调的话来说,构建 CD 是一门艺术和一门科学。我已尽力提供尽可能清晰的配方来实现这一点:

  1. 从您试图衡量的关系开始。

  2. 确定要包括的候选变量。也就是说,利用您的行为科学知识和业务专业知识来识别可能会影响您感兴趣的任一变量的变量。

  3. 根据数据中的相关性确认要包括哪些可观察变量。

  4. 通过不断添加可能的未观察变量的代理以及进一步增加到目前为止已包含的变量的进一步原因,逐步扩展您的 CD。

  5. 最后,通过删除无关的关系和变量来简化您的 CD。

在这样做的过程中,始终牢记您的最终目标:衡量您感兴趣的原因对您感兴趣的效果的因果影响。我们将在下一章中看到如何使用 CD 来消除分析中的混杂因素,并获得对该影响的无偏估计。因此,最好的 CD 是那种能让您充分利用当前可用数据并推动有益的进一步研究的 CD。

^(1) Nuno Antonio, Ana de Almeida, 和 Luis Nunes, “Hotel booking demand data sets,” Data in Brief, 2019. https://doi.org/10.1016/j.dib.2018.11.126

^(2) 这个色彩缤纷的标签是行为科学家丹尼尔·卡内曼推广的。

^(3) 想一想:否则规则会如何实施?

第五章:使用因果图解除数据分析中的混淆

因果关系对我们理解世界是如此基础性,以至于就连幼儿园的孩子也能直观地把握。然而,这种直觉和我们的数据分析可能会被混淆所误导,正如我们在第一章中看到的那样。如果我们不考虑我们感兴趣的两个变量的联合原因,那么我们会错误地解释正在发生的事情,而我们感兴趣的原因的回归系数也会出现偏差。然而,我们也看到了考虑错误变量的风险。这使得确定包括还是不包括哪些变量成为解除数据分析中最关键的问题之一,更广泛地说,是因果思维中最关键的问题之一。

可惜这是一个复杂的问题,各位作者提出了各种更多或更少扩展性的规则。在扩展性较高的一端,有一些规则偏向于谨慎和简单——你可以将它们看作是合理的“包罗万象”方法。在另一端,有些规则试图准确找出所需的变量,但这样做会增加复杂性和概念上的要求。

有趣的是,回答这个问题并不需要任何数据。也就是说,你可能想要或需要数据来建立正确的因果图,但一旦你拥有了正确的因果图,就无需查看任何数据来识别混淆。这使我们直接处于我们框架的因果图到行为边缘(图 5-1),因此在本章中我们不会使用任何数据。

相反,我将向您展示两个解除混淆的规则,它们各有利弊,“析取性因果准则”和“后门准则”,这样您可以根据您的情况选择使用哪一个。在下一节中,我将设立我们的业务问题,然后依次看看如何应用这两个准则。

本章讨论行为与因果图之间的关系

图 5-1。本章讨论行为与因果图之间的关系

业务问题:冰淇淋和瓶装水销售

我们本章的起点是 C-Mart 市场部发布的内部报告,题为“健康顾客”,追踪了向健康产品长期趋势的变化。基于该报告,C-Mart 推出了名为“您需要配有天然泉水的冰淇淋吗?”的快餐和冰淇淋特许经营的营销活动。我们的分析目标是获得冰淇淋销售对瓶装水销售影响的无偏估计。

通过利用现有数据和专用调查,市场分析团队建立了以下因果图,我们感兴趣的关系用粗体标出(图 5-2)。

我们业务情况的因果图

图 5-2. 我们业务情境的 CD

这个 CD 相对复杂,不明显的混杂因素可能潜藏其中,因此让我们将其分解成更易处理的部分(Figure 5-3)。

我们的 CD 分解为概念块

图 5-3. 将我们的 CD 分解为概念块

这两个概念块仅是理解的教学工具:它们既不是排他的(我们感兴趣的关系同时包含在内),也不是穷尽的(有些箭头两者都没有涵盖)。

我们的第一个块位于 CD 的左上角,显示了冰淇淋销售和汉堡包和薯条销售之间通过客户数量的连接。在客流量更大和更繁忙的日子里,总体销售往往更高,这使得各种变量同时变动。此外,店员已被指示在冰淇淋和薯条销售中提供“您要不要来杯矿泉水?”的提示,以及在汉堡包销售中提供“您要不要薯条?”的提示。

我们的第二个块位于 CD 的右下角,显示了两个在调查中已确定但在个别销售水平上不可用的因素的影响:顾客的平均年龄(年龄较小的顾客和有孩子的顾客更有可能购买含糖产品)和顾客的健康意识(有健康意识的顾客更倾向于购买水,而不倾向于购买苏打水,在其他一切条件相等的情况下)。

注意

在现实生活中的设置中,可以根据分析的需要自由地将大型或复杂的 CD 分解为块。只要在最后进行一些整理并检查从你感兴趣的原因到你感兴趣的效果的路径不包含在任何块中:你必须确保它们不会生成混杂,因为它们本身可能不是混杂,或者在分析概念块时已经处理了它们的混杂。

在这种情况下,目前尚不清楚我们感兴趣的冰淇淋销售与瓶装水销售之间是否存在混杂,以及如何解决。从技术上讲,在我们的 CD 中,我们没有任何同时导致这两者的共同原因。让我们转向我们的决策规则。

分离因果标准

分离因果标准是我们解除混杂的第一决策规则。像一个过于保护的父母一样,它超出了严格需要去除混杂的范围,使得它更简单易懂且易于应用。

定义

分离因果标准(DCC)指出:

在我们感兴趣的关系中,添加所有直接导致我们感兴趣的变量的回归变量,除了它们之间的中介变量,可以消除任何混杂。

第一个块

让我们从我们的冰淇淋例子的第一个区块开始分解这个定义:

1. 所有直接导致我们感兴趣的变量的变量

这意味着我们应该包括任何只是直接导致冰淇淋销售的变量,例如顾客数。我们还应该包括任何只导致瓶装水销售的变量,例如薯条销售。最后,我们应该包括任何两者都导致的原因,但在这种情况下我们没有这样的变量。

2. 除了它们之间的中介者

中介者是“传输”我们感兴趣的原因对我们感兴趣的效果的影响的变量。也就是说,它们是我们感兴趣的原因的子代和我们感兴趣的效果的父代。我们将在第十二章更详细地讨论中介者,所以现在我只想指出,我们需要排除它们在我们的控制列表之外,因为包括它们将取消我们试图捕捉的一些因果关系。在冰淇淋销售瓶装水销售之间我们没有中介者(即,一个变量既是前者的子代又是后者的父代),所以在这一点上我们做得很好。

3. 消除我们感兴趣的关系的任何混杂

如果我们包括第 1 点描述的变量但不包括第 2 点描述的变量,那么我们对冰淇淋销售瓶装水销售效应的回归系数将不受我们第一个区块中的变量的混杂影响。

需要注意的是,DCC 是一个充分但不是必要的规则:应用它足以消除混杂,但我们并不一定需要。例如,如果我们有一个变量只是我们感兴趣的一个变量的原因,并且我们确信它与任何其他变量绝对没有任何联系,那么它不能是混杂因素,我们也不需要将其包括以消除混杂。

但当你没有这种确定性时,DCC 会让你免于苦苦思索哪个变量导致了哪个,以及什么是混杂因素。你可能会忽略一些变量之间的关系,或者认为有关系而实际上没有;你可能会认为一个变量是混杂因素,而实际上不是,反之亦然。只要你正确确定一个变量是否与你感兴趣的两个变量之一有直接因果关系,你就能正确决定是否将其包含在内。

例如,让我们看看从顾客数瓶装水销售量的链路,途径汉堡销售薯条销售。我们在第二章看到,链路是一个因果图,通过直线箭头连接变量(图 5-4)。

我们第一个区块中的扩展链

图 5-4. 我们第一个区块中的扩展链

当然,我们可以用箭头表示这条链条向上、向下或从右到左;关键是它们都是朝着同一个方向前进的,这使我们能够折叠这条链条,并将NumberOfCustomers视为BottledWaterSales的直接原因。但是NumberOfCustomers确实是IceCreamSalesBottledWaterSales的联合直接原因,也是它们关系的混杂因素(图 5-5)。

折叠上链使 NumberOfCustomers 成为 BottledWaterSales 的直接原因

图 5-5. 折叠上链使 NumberOfCustomers 成为 BottledWaterSales 的直接原因

根据 DCC 的定义,将其应用于这个第一个区块意味着在我们的回归中包括NumberOfCustomersFrenchFrySales作为控制变量。从图 5-5 中可以看出,这样做有效地中和了上链的混杂效应。更一般地说,因为链条可以随意扩展或折叠,所以最终是我们感兴趣的两个变量的原因(因此也是混杂因素)的变量可能被隐藏在 CD 中的一系列中间变量之后。

DCC 的美妙之处在于,即使市场团队忽略了从NumberOfCustomersBottledWaterSales的上链,并且它没有被包含在 CD 中,包括NumberOfCustomersFrenchFrySales的要求也会处理混杂问题。另一方面,根据图 5-5,我们可以看到仅包括NumberOfCustomers就足够了,而包括FrenchFrySales也是多余的。这是我在章节介绍中提到的权衡之一:DCC 是一个广泛的规则,即使在 CD 中存在错误,也会消除混杂,但代价是冗余和需要更多数据。现在让我们转向 CD 中的第二区块。

第二区块

第二区块中的变量之间存在更复杂的关系(图 5-6)。

第二区块

图 5-6. 第二区块

在这里,我们除了关注的变量外,唯一拥有数据的变量是SodaSales。它既不是IceCreamSales也不是BottledWaterSales的原因,因此 DCC 不会要求将其包括在回归中。然而,它会要求包括AverageCustomerAgeCustomerHealthMindset,而我们没有这些数据。这并不一定意味着混杂正在发生,但我们无法确定它没有发生。这是 DCC 的最大局限性:如果你没有关于你感兴趣的变量的一些原因的数据,它就无法帮助你。现在让我们转向背门准则。

背门准则

背门准则(BC)构成了控制混杂因素的另一种规则。与分离因果准则相比,它提供了非常不同的权衡:理解起来更复杂,并且需要具有完全准确的 CD,但它聚焦于实际的混杂因素,并且不需要在我们的回归中包含任何多余的变量。从形式上讲,控制这一规则确定的变量就足以消除混杂因素。

定义

背门准则表明:

如果两个变量之间存在至少一个未阻塞的非因果路径,从我们感兴趣的原因开始,它们之间的因果关系就会受到干扰。

相反,要消除所有混杂因素,我们需要阻断所有非因果路径,从我们感兴趣的原因开始。

要理解这个定义,我们需要介绍或回顾一系列次要定义,在我们的示例中,与我们将在此重复的 CD 上下文相关(图 5-7)。

图 5-7. 我们业务情况下的 CD

首先,让我们回顾一下“路径”的定义:如果它们之间有箭头,不管箭头的方向如何,并且在这条路上没有变量出现两次,我们称之为存在路径。链是沿着三个或更多变量的路径,分叉和碰撞也是如此。从这个意义上说,CD 中的任意两个变量都至少通过一条路径相连,通常是几条。

例如,在我们的 CD 中,NumberOfCustomersBottledWaterSales 之间有七条不同的路径:

  • NumberOfCustomersIceCreamSalesBottledWaterSales

  • NumberOfCustomersBurgerSalesFrenchFrySalesBottledWaterSales

  • NumberOfCustomersBurgerSalesFrenchFrySalesCustomerHealthMindsetBottledWaterSales

  • NumberOfCustomersBurgerSalesFrenchFrySalesCustomerHealthMindsetSodaSalesAverageCustomerAgeIceCreamSalesBottledWaterSales

  • NumberOfCustomersSodaSalesCustomerHealthMindsetBottledWaterSales

  • NumberOfCustomersSodaSalesCustomerHealthMindsetFrenchFrySalesBottledWaterSales

  • NumberOfCustomersSodaSalesAverageCustomerAgeIceCreamSalesBottledWaterSales

请注意,NumberOfCustomersBurgerSalesFrenchFrySalesCustomerHealthMindsetSodaSalesNumberOfCustomersIceCreamSalesBottledWaterSales 不是一条路径,因为变量 NumberOfCustomers 在其中出现了两次,这是不允许的。

如果它是一个链,即其中所有的箭头都是朝着同一个方向的,那么路径就是“因果”的。标签“因果”指的是路径之间的因果关系,如果其中一个变量通过该路径导致另一个变量,则该路径是因果的。

前述列表中的路径 1 和 2 是因果关系的:它们是链条,代表NumberOfCustomers影响BottledWaterSales的通道。其他路径是非因果关系,因为它们每个都至少包含一个对撞机或叉路。请记住,对撞机是当两个变量导致同一个变量时的情况,而叉路是当两个变量被同一个变量导致时的情况。例如,路径 3 和 4 都在FrenchFrySales周围有一个对撞机,路径 4 还在SodaSales周围有一个对撞机,以及CustomerHealthMindsetAverageCustomerAge周围有两个叉路。

最后,我们将说,在我们的 CD 中,两个变量之间的路径如果是阻塞的,那么它要么是:

  • 沿着那条路径的中介变量之一包括在我们的回归中,并且它不是一个对撞机,或者

  • 在这条路径中,有一个对撞机,其核心变量未包含在我们的回归中。

否则,那条路径是未阻塞的。

阻塞或未阻塞的概念很难理解,因为它实际上包含了两件不同的事情:路径本身是否混杂以及它是否在我们的回归中被控制。你可以将未阻塞视为{混杂 未控制},阻塞为{非混杂 已控制}。

混杂的最终根本原因始终是联合原因(图 5-8,左面板)。但是,由于我们可以随意折叠或扩展链条,这个混杂因素可能“隐藏”在许多中介变量背后(图 5-8,中间面板)。然而,我们无法在链条中间折叠对撞机,因为它会破坏箭头的方向(图 5-8,右面板)。因此,对撞机阻止了混杂,除非我们在回归中包括它,这样就中和了它。

混杂因素是联合原因(左面板),但它可以隐藏在中介变量背后(中间面板),而对撞机阻止了我们折叠链条,因此消除了混杂(右面板)

图 5-8。混杂因素是联合原因(左面板),但它可以隐藏在中介变量背后(中间面板),而对撞机阻止了我们折叠链条,因此消除了混杂(右面板)

第一个区块

现在我们已经看到了 BC 的定义,让我们看看它如何适用于因果图的第一个区块中的变量。记住,DCC 要求我们在回归中包括NumbersOfCustomersFrenchFrySales作为控制变量。

我们可以从条件“从指向我们感兴趣的原因的箭头开始”开始应用 BC,这意味着我们感兴趣的原因的所有原因,这种情况下是IceCreamSales。在第一个区块中,只有一个,即NumberOfCustomers

对于通过 NumberOfCustomers 的每条路径,让我们应用 BC 的其他条件。第一块内的 IceCreamSalesBottledWaterSales 的路径通过 NumberOfCustomersIceCreamSalesNumberOfCustomersBurgerSalesFrenchFrySalesBottledWaterSales。这是 DCC 捕获并通过在我们的回归中包括 NumberOfCustomersFrenchFrySales 来控制的路径。让我们检查一下条件:

  • 那条路径是非因果的吗?是的,因为围绕 NumberOfCustomers 的分叉。

  • 默认情况下那条路径被阻断了吗?不是的,因为在那条路径中没有碰撞器,并且我们尚未包括任何变量作为控制。

因此,这条路径混淆了我们感兴趣的关系,我们需要通过在我们的回归中包括该路径的任意一个非碰撞器变量来控制它。也就是说,BC 告诉我们,包括任何一个 (NumberOfCustomers, BurgerSales, FrenchFrySales) 就足以控制这条路径。然而,我个人建议选择哪个变量:每当你能包括那条路径的第一个变量,即你感兴趣的原因的原因,你应该这样做。在我们的例子中,这将是 NumberOfCustomers。选择这个原因的原因是它也会自动控制起始于该变量的任何其他混淆路径,这意味着我们甚至不必检查起始于该变量的任何其他路径。

正如你所看到的,BC 比 DCC 更经济,通过利用我们对该块中变量的完整和正确因果图的假设:而 DCC 要求我们包括 NumberOfCustomersFrenchFrySales,BC 只需要包括 NumberOfCustomers,我们可以不检查任何其他路径就将第一块留下。

第二个阻止

请记住,DCC 对第二块中的变量保持沉默:我们不能包括变量 AverageCustomerAgeCustomerHealthMindset,因为我们没有相应的数据,因此我们不确定那里是否存在未受控制的混淆。BC 将使我们能够更加确定和精确。

AverageCustomerAge 是我们感兴趣的因果关系 IceCreamSales 的一个原因,因此让我们来检查路径 IceCreamSalesAverageCustomerAgeSodaSalesCustomerHealthMindsetBottledWaterSales

  • 那条路径是非因果的(即不是一个链):它在 AverageCustomerAge 周围有一个分叉,在 SodaSales 周围有一个碰撞器,然后在 CustomerHealthMindset 周围有另一个分叉。

  • 默认情况下它被阻塞了吗?是的,因为围绕 SodaSales 的碰撞器。

换句话说,这条路径并不是混杂因素,我们在回归中不需要控制它。更甚的是,将 SodaSales 包含在我们的回归中实际上会创建混杂,通过解除这条路径!

这个在对撞机周围有两个叉子的配置非常特殊,以至于有一个名字:M 型模式,我们可以通过重新排列我们的 CD(图 5-9)看到。不可否认,这个例子可能看起来有些刻意。但是,如果你觉得它过于人为和不真实,请注意,它是根据《为什么之书》中关于 2006 年实际烟草诉讼的一个例子改编的,那里包括了一个控制座椅安全带使用对估计吸烟对肺癌影响的偏差。

在我们的 CD 中可视化 M 型模式

图 5-9. 在我们的 CD 中可视化 M 型模式

此外,因为所有从IceCreamSalesBottledWaterSales的路径都通过AverageCustomerAge也经过SodaSales,只要我们在回归中不包括SodaSales,它们都会被阻断。

在 CD 中找到混杂因素是一门科学:应用规则,你就会知道。但它也有它的捷径:已经确定了通过两个IceCreamSales的原因存在混杂的可能性,并确保任何混杂会被阻断,我们就不必检查每一条通过这两个原因到IceCreamSales的路径。当你建立和操作更多的 CD 时,你会学会发展一种直觉。如果你有疑问,你随时可以检查每条可能的路径的规则,确保你是正确的。

后门准则比排除性因果准则更精确,后门准则对我们的 CD 中的错误不那么鲁棒。为了论证起见,假设营销团队在构建 CD 时犯了一个错误,并错误地得出结论,即SodaSales导致CustomerHealthMindset而不是反过来(在这种特定情况下,从行为角度来看这并不太合理,但请忍耐),导致了图 5-10 中所代表的关系。

第二块如果有错误会是什么样子

图 5-10. 第二个块如果有错误会是什么样子

在这种情况下,BC 会让我们错误地认为有混杂因素在起作用,并在我们的回归中包括SodaSales,引入了在此之前没有的混杂因素。

总结一下,BC 识别出通过两个直接导致IceCreamSales的原因有两条潜在的混杂途径。通过在我们的回归中包括NumberOfCustomers,我们处理了所有可能通过它产生的混杂路径。另一方面,通过包括SodaSales,我们放过了一个碰撞器,通过AverageCustomerAge来处理任何通过IceCreamSales的混杂因素。

结论

揭示因果关系的混淆是行为数据分析的核心问题之一,也是解决“相关不等于因果”的泥潭的办法。在本章中,我们看到了两条混淆规则,分离因素准则背门准则。第一条准则采取的立场是包括我们感兴趣变量的所有直接原因(除了中介因素)。第二条准则在应用中更为精准,直接针对混淆的机制,但在处理中错误容忍度较低。

第三部分:稳健的数据分析

理想的数据是大型、完整的,并且具有规则的形状(例如,数值变量的情况下符合正态分布)。这是你在入门统计课程中看到的数据。现实生活中的数据通常没有那么方便,特别是在处理行为数据时。

在第六章中,我们将看到如何处理缺失数据。虽然在数据分析中缺失数据是常见的,但行为数据增加了一层复杂性:缺失的数值通常与个体特征和行为相关,这在我们的分析中引入了偏差。幸运的是,使用 CDs 将允许我们尽可能地识别和解决这种情况。

在第七章中,我们将讨论一种名为 Bootstrap 的计算机模拟类型。这是一种非常多才多艺的工具,特别适用于行为数据分析:当处理小型或形状奇怪的数据时,它允许我们适当地衡量估计值周围的不确定性。此外,在设计和分析实验时,它提供了一种替代 p 值的方法,这将使我们的生活更加简单。

第六章:处理缺失数据

在数据分析中,缺失数据是常见的。在大数据时代,许多作者甚至更多的从业者将其视为一个次要的困扰,几乎没有太多思考:只需过滤掉缺失数据的行——如果你从 1200 万行减少到 1100 万行,那有什么大不了的呢?这仍然为您提供了足够的数据来运行您的分析。

不幸的是,过滤掉含有缺失数据的行可能会在你的分析中引入显著的偏差。比如说,老年客户更有可能存在缺失数据,因为他们较少设置自动付款;如果过滤这些客户,你的分析就会偏向于年轻客户,在过滤数据中会过度代表他们。其他处理缺失数据的常见方法,比如用该变量的平均值替换,也会引入它们自己的偏差。

统计学家和方法学家已经开发出了方法,这些方法的偏差要小得多,甚至没有偏差。然而,这些方法尚未被广泛采纳,但希望本章可以帮助您走在前列!

缺失值理论根植于统计学,可能会变得非常数学化。为了使本章节的学习更具体化,我们将通过一个模拟数据集来探讨 AirCnC。在业务背景下,市场部门为了更好地了解客户特征和动机,向三个州的 2,000 名客户发送了一封调查问卷,并收集了以下信息:

  • 人口统计特征

    • 年龄

    • 性别

    • 州(只选择三个州的客户,为了方便起见我们将它们称为 A、B 和 C)

  • 个性特质

    • 开放性

    • 外向性

    • 神经质

  • 预订金额

为了简化问题,我们假设人口统计变量都是预订金额的原因,并且彼此无关(见图 6-1)。

人口统计变量导致预订金额

图 6-1。人口统计变量导致预订金额
注意

正如我们在第二章中讨论的,当我说人口统计变量如性别外向性预订金额的原因时,我指的是两件事情:首先,它们是外生变量(即在我们研究中是主要原因),其次,它们由于社会现象的因果效应而成为预订金额的预测因素。

例如,性别的影响可能通过个人的收入、职业和家庭状况等多种因素进行中介。从这个意义上讲,更准确地说性别预订金额的原因的原因。然而,重要的是要注意,这种效应并没有混淆,因此它是真正的因果关系。

本章的流程将遵循您在面对新数据集时所采取的步骤:首先,我们将可视化缺失数据,以了解大致情况。然后,我们将学习如何诊断缺失数据,并看到由统计学家唐纳德·鲁宾开发的分类,这是参考资料。最后三节将展示如何处理该分类中的每一类。

对于 Python 用户来说,不幸的是,我们将要使用的出色 R 包没有直接的 Python 对应包。我将尽力向您展示 Python 中的替代方法和解决方法,但代码将会显著更长,不那么优雅。抱歉!

数据和包

使用模拟数据的一个好处是我们知道缺失数据的真实值。本章的 GitHub 文件夹 包含三个数据集(表 6-1):

  • 我们四个变量的完整数据

  • “可用”数据中某些变量的一些值缺失

  • 辅助变量的辅助数据集,我们将使用它来补充我们的分析

表 6-1. 我们数据中的变量

变量描述 chap6-complete_data.csv chap6-available_data.csv chap6-available_data_supp.csv
年龄 顾客年龄 完整 完整
开放度 开放度心理特征,0-10 完整 完整
额外 外向性心理特征,0-10 完整 部分
神经 神经质心理特征,0-10 完整 部分
性别 顾客性别的分类变量,F/M 完整 完整
顾客居住州的分类变量,A/B/C 完整 部分
预订金额 顾客预订金额 完整 部分
保险 顾客购买的旅行保险金额 完整
活跃度 顾客预订活跃程度的数值测量 完整

在本章中,除了常用的包外,我们还将使用以下包:

## R
library(mice) # For multiple imputation
library(reshape) #For function melt()
library(psych) #For function logistic()

## Python
from statsmodels.imputation import mice # For multiple imputation
import statsmodels.api as sm # For OLS call in Mice

可视化缺失数据

根据定义,缺失数据很难可视化。单变量方法(即一次处理一个变量)只能带我们走那么远,所以我们大多数时候会依赖双变量方法,将两个变量相互绘制以挖掘一些见解。与因果图结合使用,双变量图将使我们能够可视化关系,否则这些关系将非常复杂。

我们的第一步是了解“数据缺失”的情况。R 中的 mice 包有一个非常方便的函数md.pattern()来可视化缺失数据:

## R
> md.pattern(available_data)
    age open gender bkg_amt state extra neuro     
368   1    1      1       1     1     1     1    0
358   1    1      1       1     1     1     0    1
249   1    1      1       1     1     0     1    1
228   1    1      1       1     1     0     0    2
163   1    1      1       1     0     1     1    1
214   1    1      1       1     0     1     0    2
125   1    1      1       1     0     0     1    2
120   1    1      1       1     0     0     0    3
33    1    1      1       0     1     1     1    1
23    1    1      1       0     1     1     0    2
15    1    1      1       0     1     0     1    2
15    1    1      1       0     1     0     0    3
24    1    1      1       0     0     1     1    2
24    1    1      1       0     0     1     0    3
23    1    1      1       0     0     0     1    3
18    1    1      1       0     0     0     0    4
      0    0      0     175   711   793  1000 2679

md.pattern() 函数返回一张表,其中每一行代表数据可用性的模式。第一行每个变量都有“1”,因此表示完整的记录。表的左侧数字表示具有该模式的行数,右侧数字表示该模式中缺失的字段数。我们的数据中有 368 行完整记录。第二行只有神经质变量为“0”,因此表示只有神经质缺失的记录;我们的数据中有 358 行这样的记录。表的底部数字表示对应变量的缺失值数量,并且变量按缺失值数量递增排序。神经质变量位于表的最右侧,这意味着它有最高数量的缺失值,为 1,000。此函数还方便地返回表的可视化表示(图 6-2)。

缺失数据的模式

图 6-2. 缺失数据的模式

正如我们在图 6-2 中看到的,变量年龄开放性性别没有任何缺失数据,但其他所有变量都有。我们可以用我写的一个特定函数在 Python 中获得相同的结果,尽管格式不够易读:

## Python 
def md_pattern_fun(dat_df):
    # Getting all column names
    all_cols = dat_df.columns.tolist()
    # Getting the names of columns with some missing values
    miss_cols = [col for col in all_cols if dat_df[col].isnull().sum()]
    if miss_cols == all_cols: dat_df['index'] = dat_df.index
    # Removing columns with no missing values
    dat_df = dat_df.loc[:,miss_cols]
    #Showing total number of missing values per variable
    print(dat_df.isnull().sum()) 
    # Adding count value
    dat_df['count'] = 1
    # Showing count for missingness combinations
    print(dat_df.isnull().groupby(miss_cols).count())
md_pattern_fun(available_data_df)

extra       793
neuro      1000
state       711
bkg_amt     175
dtype: int64
                           count
extra neuro state bkg_amt       
False False False False      368
                  True        33
            True  False      163
                  True        24
      True  False False      358
                  True        23
            True  False      214
                  True        24
True  False False False      249
                  True        15
            True  False      125
                  True        23
      True  False False      228
                  True        15
            True  False      120
                  True        18

输出由两张表组成:

  • 第一张表显示了我们数据中每个变量的缺失值总数,如图 6-2 的底部所示。外向性有 793 个缺失值,依此类推。

  • 第二张表显示每种缺失数据模式的详细信息。左侧的逻辑值上方的变量(即外向性、神经质、状态、预订金额)是数据中有一些缺失值的变量。表的每一行指示具有特定缺失数据模式的行数。第一行由四个False组成,即没有任何变量缺失数据的模式,我们的数据中有 368 行,正如您在图 6-2 的第一行中看到的那样。第二行只将最后一个False改为True,为了易读性省略了前三个False(即任何空白逻辑值应往上读取)。这种模式 False``/``False``/``False``/``True 出现在仅预订金额有缺失值的情况下,发生在我们的数据中的 33 行,依此类推。

即使是这样一个小数据集,这种可视化也非常丰富,很难知道要寻找什么。我们将探讨两个方面:

缺失数据量

我们的数据有多少是缺失的?哪些变量有最高百分比的缺失数据?我们可以简单地丢弃有缺失数据的行吗?

缺失相关性

缺失数据是在个体级别还是变量级别?

缺失数据量

首要任务是确定我们数据的缺失情况以及哪些变量的缺失比例最高。我们可以在图 6-2 的底部找到所需的值,其中包括每个变量的缺失值数量,按照缺失程度递增排序,或在 Python 输出的底部找到。如果缺失数据量非常有限,例如您有一个 1000 万行数据集,其中没有变量有超过 10 个缺失值,那么通过多重插补来妥善处理它们将是过度的,我们稍后将看到。只需删除所有具有缺失数据的行,问题就解决了。这里的理由是,即使缺失值极其偏倚,它们的数量太少,不会以任何方式实质性地影响您分析的结果。

在我们的示例中,缺失值最多的变量是神经质,有 1,000 个缺失值。这多吗?限制在哪里?是 10、100、1,000 行还是更多?这取决于上下文。您可以使用一个快速且简单的策略:

  1. 选择缺失值最多的变量,并创建两个新数据集:一个将该变量的所有缺失值替换为该变量的最小值,另一个将其替换为该变量的最大值。

  2. 对您现在拥有的三个数据集中的每一个,运行关于该变量与您感兴趣效果的最重要关系的回归。例如,如果该变量是您感兴趣效果的预测变量,则运行该回归。

  3. 如果在三次回归中,回归系数没有实质性差异,即基于不同数值得出相同的业务影响或采取相同行动,那么您的数据缺失率在可接受范围内,可以放弃缺失数据。简而言之:这些数字对您的业务伙伴来说意味着相同的事情吗?如果是这样,您可以放弃缺失数据。

注意

这一经验法则很容易适用于数值变量,但是对于二元或分类变量呢?

对于二元变量,最小值将为 0,最大值将为 1,这是可以接受的。您创建的两个数据集将转化为最佳情况和最坏情况。

对于分类变量,最小和最大规则必须稍作调整:将所有缺失值替换为最少出现或最常出现的类别。

让我们以神经质为例,做到这一点。神经质是我们感兴趣效果预订金额的一个预测变量,所以我们将使用前面指出的关系:

## R (output not shown)
min_data <- available_data %>%
  mutate(neuro = ifelse(!is.na(neuro), neuro, min(neuro, na.rm = TRUE)))
max_data <- available_data %>%
  mutate(neuro = ifelse(!is.na(neuro), neuro, max(neuro, na.rm = TRUE)))
summary(lm(bkg_amt~neuro, data=available_data))
summary(lm(bkg_amt~neuro, data=min_data))
summary(lm(bkg_amt~neuro, data=max_data))

## Python (output not shown) 
min_data_df = available_data_df.copy()
min_data_df.neuro = np.where(min_data_df.neuro.isna(), min_data_df.neuro.min(), 
                             min_data_df.neuro)

max_data_df = available_data_df.copy()
max_data_df.neuro = np.where(max_data_df.neuro.isna(), max_data_df.neuro.max(), 
                             max_data_df.neuro)

print(ols("bkg_amt~neuro", data=available_data_df).fit().summary())
print(ols("bkg_amt~neuro", data=min_data_df).fit().summary())
print(ols("bkg_amt~neuro", data=max_data_df).fit().summary())

结果如下:

  • 基于可用数据的系数为−5.9。

  • 基于用神经质的最小值替换缺失值的系数为−8.0。

  • 基于用神经质的最大值替换缺失值的系数为 2.7。

这些值彼此非常不同,甚至具有不同的符号,因此我们绝对超过了材料显著性的阈值。我们不能简单地删除那些对神经质有缺失数据的行。对其他变量采用同样的方法也会显示出我们不能忽视它们的缺失值,需要适当处理。

缺失的相关性

一旦确定了需要处理的变量,我们就想知道它们的缺失程度有多相关。如果有变量的缺失高度相关,这表明一个变量的缺失导致了其他变量的缺失(例如,如果某人在调查中途停止回答问题,则之后的所有答案都将缺失)。或者,它们的缺失可能有共同的原因(例如,某些受试者更不愿意透露有关自己的信息)。在这两种情况下,识别缺失的相关性将帮助您建立更精确的 CD,节省时间并使分析更有效。

让我们通过一个简单的例子来看一下:想象一下我们有两个办公室的面试数据:Tampa 和 Tacoma。在两个办公室中,候选人必须通过相同的三个强制性面试部分,但在 Tampa,第一位面试官负责记录候选人的所有分数,而在 Tacoma,每位面试官都要记录其部分的分数。面试官是人类,有时会忘记将数据交给 HR。在 Tampa,如果面试官忘记提交数据,我们对于候选人除了系统中的 ID 外就没有任何数据(Figure 6-3 只显示了 Tampa 的数据)。

Tampa 数据中高度相关的缺失

图 6-3。Tampa 数据中高度相关的缺失。

高缺失相关性的标志是一行具有大量浅色方块(这里是 3),代表了高数量的案例(这里是总行数 2,000 中的 400)。此外,图中没有只有一个或两个浅色方块的行。

在这种情况下,逐个变量分析我们的缺失数据是毫无意义的。如果我们发现当 Murphy 是第一位面试官时,第一部分的数据非常可能丢失,那么对于其他部分也是如此。(Murphy,你只有一个任务!)

相反,在 Tacoma,不同部分的缺失完全不相关(Figure 6-4)。

这种模式与 Tampa 相反:

  • 我们有大量只有少数缺失变量的行(请看图右边的所有 1 和 2)。

  • 这些行代表了我们数据的大部分(左边的数据表明只有 17 行有 3 个缺失变量)。

  • 图中底部有大量浅色方块的线代表非常少的案例(相同的 17 个个体),因为它们是独立随机性的结果。

Tacoma 数据中的不相关缺失

图 6-4. Tacoma 数据中的不相关缺失

最后一个要点的论证可以通过更广泛地观察我们可以称之为“俄罗斯套娃”序列来扩展,这些序列呈递增的缺失,其中每个模式都在上一个模式上添加一个缺失变量,例如(I3)→(I3,I2)→(I3,I2,I1)。相应的案例数量是 262 → 55 → 17. 这些数字形成了一个递减序列,这是合乎逻辑的,因为如果变量的缺失完全不相关,我们有:

ProbI3 缺失 & I2 缺失)= ProbI3 缺失)* ProbI2 缺失

ProbI3 缺失 & I2 缺失 & I1 缺失)= ProbI3 缺失)* ProbI2 缺失)** ProbI*1 缺失

在样本较小和/或缺失程度非常高的情况下,这些方程可能在我们的数据中并不完全成立,但是如果任何变量的缺失量不到 50%,我们通常应该有:

ProbI3 缺失 & I2 缺失 & I1 缺失)< ProbI3 缺失 & I2 缺失)< ProbI3 缺失

在实际情况下,自己测试所有这些不等式可能会相当麻烦,尽管您可以编写一个函数以进行大规模测试。相反,我建议查看任何重要的异常值的可视化(即,一些相同变量的值远大于相同变量的某些值)。

更广泛地说,这种可视化在只有几个变量时很容易使用。一旦您有大量变量,您将不得不构建和可视化缺失的相关矩阵:

## R (output not shown)
# Building the correlation matrices
tampa_miss <- tampa %>%
  select(-ID) %>%
  mutate(across(everything(),is.na))
tampa_cor <- cor(tampa_miss) %>%
  melt()

tacoma_miss <- tacoma %>%
  select(-ID) %>%
  mutate(across(everything(),is.na))
tacoma_cor <- cor(tacoma_miss) %>%
  melt()

## Python (output not shown)
# Building the correlation matrices
tampa_miss_df = tampa_df.copy().drop(['ID'], axis=1).isna()
tacoma_miss_df = tacoma_df.copy().drop(['ID'], axis=1).isna()

tampa_cor = tampa_miss_df.corr()
tacoma_cor = tacoma_miss_df.corr()

Figure 6-5 显示了生成的相关矩阵。在左侧的矩阵中,对于 Tampa,所有值都等于 1:如果一个变量缺失,那么其他两个变量也是如此。在右侧的相关矩阵中,对于 Tacoma,主对角线上的值等于 1,但在其他地方都等于 0:知道一个变量缺失并不能告诉您其他变量的缺失情况。

完全相关缺失的相关矩阵(左)和完全不相关缺失的相关矩阵(右)

图 6-5. 完全相关缺失(左)和完全不相关缺失(右)的相关矩阵

让我们回到我们的 AirCnC 数据集,并看看它在我们的理论访谈示例中概述的两个极端之间的位置。Figure 6-6 重复了 Figure 6-2 以便更易于访问。

图 6-6 位于中间位置:所有可能的缺失模式都相当代表,这表明我们没有强烈聚集的缺失源。

缺失数据的模式(重复)

图 6-6. 缺失数据的模式(重复 图 6-2)

图 6-7 显示了我们的 AirCnC 数据缺失的相关性矩阵。正如你所看到的,我们变量的缺失几乎完全不相关,完全在随机波动范围内。如果你想更加熟悉缺失中的相关性模式,本章的一个GitHub 上的练习要求你识别其中一些。作为提醒,查看相关性模式本身并不是必要的,但通常可以启发思考并节省时间。

我们 AirCnC 数据中缺失的相关性矩阵

图 6-7. 我们 AirCnC 数据中缺失的相关性矩阵

诊断缺失数据

现在我们已经可视化了我们的缺失数据,是时候了解是什么导致了它。这就是因果图的作用所在,因为我们将使用它们来表示缺失数据的因果机制。

让我们从第 1 章中的一个非常简单的例子开始。在介绍因果图时,我提到未观察到的变量,比如顾客对香草冰淇淋的喜好,用一个较深的阴影矩形来表示(图 6-8)。

未观察到的变量以椭圆形表示

图 6-8. 未观察到的变量以较深的阴影矩形表示

未观察到的变量,在某些学科中有时被称为“潜变量”,指的是我们实际上没有的信息,尽管理论上可能是可以访问的或不可访问的。在目前的情况下,假设我们强迫客户在购买前透露他们对香草的口味。这将在我们的系统中创建相应的数据,然后我们将用它们进行数据分析(图 6-9)。

收集先前未观察到的信息

图 6-9. 收集先前未观察到的信息

但是,试图强迫客户透露他们不想透露的信息通常是不良的商业行为,并且通常是可选的。更一般地说,对一些客户收集了大量数据,而对其他客户则没有。我们将通过在 CD 中用虚线绘制相应的框来表示该情况(图 6-10)。

用虚线框表示部分观察到的变量

图 6-10. 用虚线框表示部分观察到的变量

例如,对于三名顾客,我们可能有以下数据,其中一名顾客拒绝透露他们对香草冰淇淋的口味(表 6-2)。

表 6-2. 我们 CD 的基础数据

顾客姓名 对香草的喜好 声明的口味 在摊位购买冰淇淋(Y/N)
N
鲍勃 Y
卡罗琳 N/A Y

在本章中,我们感兴趣的是理解一个变量的缺失原因,而不仅仅是理解一个变量的值的原因。因此,我们将创建一个变量来跟踪声明口味变量何时缺失 (表 6-3)。

表 6-3. 添加一个缺失变量

顾客姓名 对香草的喜好 声明的香草口味 声明的口味缺失(Y/N) 在摊位购买冰淇淋
N N
鲍勃 N Y
卡罗琳 N/A Y Y

让我们在我们的 CD 中添加该变量 (图 6-11)。

在我们的因果图中添加缺失

图 6-11. 在我们的因果图中添加缺失

我们通常将缺失作为相应部分观察变量的原因。直觉是信息完全存在于未观察变量中,并且部分观察变量等于未观察变量,除非信息被缺失变量“隐藏”。这种约定将使我们的生活更加轻松,因为它允许我们在 CD 中表达和讨论缺失的原因,这些原因代表了我们感兴趣的关系,而不必单独考虑缺失。

现在缺失已成为我们 CD 的一部分,下一个自然的步骤是问自己:“是什么原因导致了它?”

缺失的原因:鲁宾的分类

对于引起变量缺失的基本且互斥的三种可能性,统计学家唐纳德·鲁宾已进行了分类。

首先,如果一个变量的缺失仅取决于我们数据之外的变量,例如纯随机因素,那么该变量被称为完全随机缺失(MCAR) (图 6-12)。

声明的口味是完全随机缺失的

图 6-12. 声明的口味是完全随机缺失的

接着,如果我们的数据中的任何一个变量影响其缺失状态,那么一个变量从 MCAR(完全随机缺失)变为随机缺失(MAR)。数据之外的变量和随机因素也可能起作用,但变量的值可能不会影响其自身的缺失状态。例如,如果购买导致了声明的香草口味的缺失,例如因为我们只采访购买的顾客而不是路人 (图 6-13)。

声明的口味是随机缺失的

图 6-13. 所述口味是随机缺失

最后,任何值影响其自身缺失的变量都被认为是 缺失不是随机的(MNAR),即使数据内外的其他变量也会影响缺失。我们的数据内外可能还有其他变量起作用,但变量一旦影响其自身缺失,就从 MCAR 或 MAR 变为 MNAR。在我们的例子中,这意味着 香草口味 导致 陈述香草口味 的缺失(图 6-14)。

所述口味缺失不是随机

图 6-14. 所述口味缺失不是随机发生的
注意

我们通过从未观察到的变量而不是部分观察到的变量引入箭头来表示变量的值影响其缺失的想法。这样,我们可以做出有意义的陈述,如“实际上低于某个阈值的所有值在我们的数据中都是缺失的”。如果箭头来自部分可观察到的变量,我们将被困在无信息的陈述中,例如“缺失的值导致它们自己缺失”。

在理想的世界里,本节其余部分将包括识别每个缺失类别的方法。不幸的是,缺失数据分析仍然是一个尚未完全探索的开放领域。特别是,缺失和因果关系如何相互作用尚不清楚。因此,处理缺失数据仍然更像是一门艺术而不是科学。试图创建系统化的方法将需要处理大量的异常情况,以及引入循环论证,如“模式 X 表明变量 1 是 MAR,除非变量 2 是 MNAR;模式 Y 表明变量 2 是 MNAR,除非变量 1 是 MAR”。我已尽力在有限的数据集中涵盖尽可能多的情况,但在现实世界中,您可能会遇到“有点这个,有点那个”的情况,您需要判断如何继续。

然而,有些好消息是,除了我会指出的几个例外外,谨慎行事会花费更多时间但不会引入偏见。当您不确定一个变量是 MCAR、MAR 还是 MNAR 时,只需假设可能的最糟情形,您的分析将尽可能是无偏的。

在这种情况下,请回顾我们的 AirCnC 数据,看看如何在一个实际数据集中诊断缺失情况。作为一个快速的提醒,我们的数据集包含以下变量:

  • 人口统计特征

    • 年龄

    • 性别

    • 状态(A、B 和 C)

  • 个性特征

    • 开放性

    • 外向性

    • 神经质

  • 预订金额

诊断 MCAR 变量

MCAR 变量是最简单的情况。传感器故障了,一个 bug 阻止了数据从客户的移动应用程序传输,或者客户只是错过了输入他们对香草冰淇淋口味的字段。总之,缺失以直觉上的“随机”方式发生。我们默认诊断 MCAR 变量:如果变量似乎不是 MAR,则将其视为 MCAR。换句话说,在缺乏证据的情况下,你可以将 MCAR 视为我们的零假设。

我们将用来诊断缺失性的主要工具是逻辑回归,即一个变量是否缺失,取决于我们数据集中的所有其他变量。让我们以 外向性 变量为例:

## Python (output not shown)
available_data_df['md_extra'] = available_data_df['extra'].isnull().astype(float)
md_extra_mod =smf.logit('md_extra~age+open+neuro+gender+state+bkg_amt',
                      data=available_data_df)
md_extra_mod.fit().summary()

## R
> md_extra_mod <- glm(is.na(extra)~.,
                        family = binomial(link = "logit"), 
                        data=available_data)
> summary(md_extra_mod)

...
Coefficients:
              Estimate Std. Error z value Pr(>|z|)
(Intercept) -0.7234738  0.7048598  -1.026    0.305
age         -0.0016082  0.0090084  -0.179    0.858
open         0.0557508  0.0425013   1.312    0.190
neuro        0.0501370  0.0705626   0.711    0.477
genderF     -0.0236904  0.1659661  -0.143    0.886
stateB      -0.0780339  0.2000428  -0.390    0.696
stateC      -0.0556228  0.2048822  -0.271    0.786
bkg_amt     -0.0007701  0.0011301  -0.681    0.496
...

没有任何变量具有大且强烈统计显著的系数。在没有任何其他证据的情况下,这表明 外向性 的缺失性纯粹是随机的,我们将把我们的 外向性 变量视为 MCAR。

你可以将 MCAR 数据视为掷骰子或抛硬币。从我们的角度来看,这两种行为都是“随机”的,但它们仍然遵守物理法则。理论上,如果我们有足够的信息和计算能力,结果将是完全可预测的。这里也可能发生同样的情况。当我们说 外向性 是 MCAR 时,我们并不是在说“外向性 的缺失性基本上是随机且不可预测的”,我们只是说“我们目前分析中包含的变量没有一个与 外向性 的缺失性相关联。”但可能——甚至很可能——其他变量(责任感?信任?对技术的熟悉度?)会相关联。我们的目标不是对 外向性 发表哲学性声明,而是确定其缺失是否可能会使我们的分析产生偏见,考虑到当前可用的数据。

诊断 MAR 变量

MAR 变量是指其缺失性取决于数据集中其他变量的值。如果数据集中的其他变量能够预测某个变量的缺失性,则 MAR 将成为我们该变量的默认假设,除非我们有足够强的证据表明其为 MNAR。让我们看看 状态 变量的情况:

## R (output not shown)
md_state_mod <- glm(is.na(state)~.,
                    family = binomial(link = "logit"), 
                    data=available_data)
summary(md_state_mod)

## Python
available_data_df['md_state'] = available_data_df['state'].isnull()\
    .astype(float)
md_state_mod =smf.logit('md_state~age+open+extra+neuro+gender+bkg_amt',
                      data=available_data_df)
md_state_mod.fit(disp=0).summary()
...
              coef   std err       z         P>|z|   [0.025  0.975]
Intercept   -0.2410   0.809     -0.298       0.766   -1.826  1.344
gender[T.F] -0.1742   0.192     -0.907       0.364   -0.551  0.202
age          0.0206   0.010      2.035       0.042    0.001  0.040
open         0.0362   0.050      0.727       0.467   -0.061  0.134
extra        0.0078   0.048      0.162       0.871   -0.087  0.102
neuro       -0.1462   0.087     -1.687       0.092   -0.316  0.024
bkg_amt     -0.0019   0.001     -1.445       0.149   -0.005  0.001
...

年龄 略有显著性,具有正系数。换句话说,年长客户似乎更不可能提供他们的州。相应的因果图表示在 图 6-15 中。

性别随机缺失

图 6-15. 性别随机缺失

我们可以通过绘制观察到的 年龄状态 缺失情况的密度图(图 6-16)来确认这种相关性。相对于年轻客户,状态 在年长客户中的观察值更多,或者反过来,对于年长客户,状态 的缺失值更多。

缺失和观察到的年龄密度状态数据

图 6-16. 缺失和观察到的状态数据密度,按观察到的年龄分

这个密度图的一个局限性是它并不显示 X 变量(这里是Age)也缺失的行。当该变量也有缺失值时,这可能会产生问题或误导。一个可能的技巧是将 X 变量的缺失值替换为一个不合理的值,比如−10。Age没有任何缺失值,所以我们将使用Xtraversion作为我们的 X 变量,该变量有缺失值。让我们按Extraversion的值绘制观察到的和缺失的State数据的密度(图 6-17)。

按 Extraversion 水平绘制的缺失和观察到的 State 数据密度,包括缺失的 Extraversion

图 6-17. 按 Extraversion 水平绘制的缺失和观察到的 State 数据密度,包括缺失的 Extraversion

图 6-17 显示,在没有Extraversion数据的个体中,我们观察到的State要比State缺失的个体更多。总体上,我们看到了强有力的证据表明State不是 MCAR,而是 MAR,因为其缺失似乎与我们数据集中其他可用变量相关。

注意

你可能注意到我在早些时候谈论Age(或Extraversion)与State缺失之间关系时使用了“相关性”这个词。事实上,到目前为止,我们只展示了相关性,完全有可能Age并不导致State的缺失,而是它们两者都由第三个未观察到的变量导致。幸运的是,在谈论缺失时,相关性的因果性质(或其缺乏)不会影响我们的分析。松散地将两者等同起来不会引入偏差,因为我们实际上永远不会处理那种关系的系数。

诊断 MNAR 变量

MNAR 变量是其缺失性取决于其自身值的变量:较高的值缺失的可能性比较低的值更高,反之亦然。这种情况对数据分析是最具问题的,并且诊断起来最为棘手。它很难诊断,因为根据定义,我们不知道缺失的值。因此,我们需要做更多的调查工作。

让我们来看一下Neuroticism变量,并且像之前一样,首先对数据中其缺失情况进行回归分析:

## Python (output not shown)
available_data_df['md_neuro'] = available_data_df['neuro'].isnull()\
    .astype(float)
md_neuro_mod =smf.logit('md_neuro~age+open+extra+state+gender+bkg_amt',
                      data=available_data_df)
md_neuro_mod.fit(disp=0).summary()

## R
md_neuro_mod <- glm(is.na(neuro)~.,
                    family = binomial(link = "logit"), 
                    data=available_data)
summary(md_neuro_mod)

...
Coefficients:
             Estimate Std. Error z value Pr(>|z|)   
(Intercept) -0.162896   0.457919  -0.356  0.72204   
age         -0.012610   0.008126  -1.552  0.12071   
open         0.052419   0.038502   1.361  0.17337   
extra       -0.084991   0.040617  -2.092  0.03639 * 
genderF     -0.093537   0.151376  -0.618  0.53663   
stateB       0.047106   0.181932   0.259  0.79570   
stateC      -0.128346   0.187978  -0.683  0.49475   
bkg_amt      0.003216   0.001065   3.020  0.00253 **
...

我们可以看到BookingAmount具有显著的系数。表面上看,这表明神经质BookingAmount的 MAR。然而,这是一个关键线索,BookingAmount在我们的 CD 中是神经质的子节点。从行为角度来看,神经质BookingAmount的 MNAR 更为可能(即,缺失是由个性特征而非客户消费金额驱动的)。

确认我们怀疑的一种方法是识别另一个具有缺失数据的变量的子类,理想情况下与其尽可能相关,与第一个子类的相关性尽可能小。在我们的次要数据集中,我们有关于客户终生在公司购买的旅行保险总金额的数据。每次旅行的费用取决于只与预订金额略微相关的行程特征,因此在这方面我们做得很好。在我们的数据集中添加保险后,我们发现它强烈预测了神经质的缺失,并且观察到和缺失的神经质之间的保险金额分布彼此截然不同(图 6-18)。

观察到的保险金额下缺失和观察到的神经质数据密度

图 6-18. 观察到的保险金额下缺失和观察到的神经质数据密度

我们找到与神经质的缺失相关的更多子变量,我们的案例就越强烈,表明这个变量是 MNAR。正如我们稍后将看到的,处理 MNAR 变量的方法是在我们的填充模型中添加辅助变量,我们的子变量是理想的候选人,因此找到它们不是浪费时间而是下一步的先机。

从技术上讲,我们永远无法完全证明一个变量是 MNAR 而不仅仅是 MAR 其子类中的一个,但这不是问题:辅助变量不会使缺失数据修补产生偏差,如果原始变量确实是 MAR 而不是 MNAR。

缺失作为一个谱系

鲁宾的分类依赖于二元测试。例如,只要一个变量更可能对较高值缺失而不是对较低值缺失(反之亦然),它就是 MNAR,不考虑任何其他因素。然而,该关系在值和缺失之间的形状对实际目的很重要:如果一个变量的所有值在某个阈值以上或以下都缺失,我们将需要与默认方法不同的方式处理这个变量。这种情况也可能发生在 MAR 变量中,因此值得退后一步,更广泛地考虑缺失形状。

我们可以将变量的缺失看作是在完全概率性和完全确定性之间的一个谱系。在谱系的“概率性”端,变量是 MCAR,所有值都可能缺失。在谱系的“确定性”端,存在一个阈值:所有在阈值一侧的个体的值都是缺失的,而在阈值另一侧的个体的值是可用的。这通常是由于业务规则的应用。例如,在招聘背景下,如果只有 GPA 超过 3.0 的候选人才能接受面试,那么对于低于该阈值的候选人将没有任何面试分数。这会使面试分数GPA上成为 MAR(Figure 6-19)。

Interview score being MAR on GPA

图 6-19. 面试分数GPA上成为 MAR
注意

精研的 MCAR/MAR/MNAR 分类仅基于缺失原因,而不考虑该因果关系是否显示出随机性。在这里,令人感到反直觉的是,InterviewScore的缺失基于GPA的确定性关系,使得InterviewScoreGPA上成为 MAR,尽管其中并没有涉及随机性。

这也可能发生在 MNAR 类型的变量上,仅记录超过或低于某一阈值的值。例如,只有在文件中保存了正常范围之外的值,或者只有超过或低于某一阈值的人才会注册(例如,出于税务目的)。

在完全随机和完全确定之间的这两个极端情况(无论是 MAR 还是 MNAR 类型),存在着根据缺失原因的值连续增加或减少缺失概率的情况。

Figure 6-20 显示了两个变量 X 和 Y 的最简单情况下的情况,其中 X 具有缺失值。为了便于阅读,可用值显示为实心方块,而“缺失”值显示为叉号。Figure 6-20 的第一行显示了 Y 相对于 X 的散点图,第二行显示了 X 和缺失概率之间关系的线图:

  • 最左列显示 X 是 MCAR。缺失的概率在 0.5 处恒定且与 X 无关。方块和叉号在图中分布类似。

  • 中间列显示 X 是具有逐渐增强概率 MNAR 的概率性 MNAR。方块在图的左侧更为普遍,而叉号在右侧更为普遍,但左侧仍然存在叉号,右侧仍然存在方块。

  • 最右侧的列显示 X 是确定性 MNAR。所有低于 5 的 X 值是可用的(方块),而所有高于 5 的值都是“缺失”的(叉号)。

从 MCAR(最左侧)到确定性 MNAR(最右侧),穿过概率性 MNAR(中心)的缺失程度谱

图 6-20. 缺失程度谱,从 MCAR(最左侧)到确定性 MNAR(最右侧),穿过概率性 MNAR(中心)

这种缺失程度的谱很少在缺失数据的统计处理中讨论,因为仅凭数学方法很难确认。但这是一本关于行为分析的书籍,因此我们可以和应该使用常识和业务知识。在 GPA 的例子中,数据的阈值是由你应该了解的业务规则应用而来的。在大多数情况下,您期望一个变量落在某些值的特定范围内,并且您应该知道可能出现某个可能值不在您的数据中的概率有多高。

在我们的 AirCnC 调查数据中,我们有三个人格特质:开放性外倾性神经质。在现实生活中,这些变量将是对几个问题回答的汇总,并且在已知区间内有钟形分布(参见 Funder (2016) 了解人格心理学的良好介绍)。让我们假设在我们的数据中,相关区间是从 0 到 10,并且让我们来看看我们变量的分布(图 6-21)。

我们数据中的人格特征直方图

图 6-21. 我们数据中的人格特征直方图

显然,神经质有些问题。基于人格特征的构建方式,我们预期三个变量的曲线类型相同,而不会出现大量数值为 5 的客户,也不会一个数值为 4 的客户都没有。这极大地暗示了一个确定性的缺失非随机变量,我们必须相应地处理。

现在,您应该能够合理判断数据集中缺失情况的模式。有多少缺失值?它们的缺失是否与变量本身的值(MNAR)、另一个变量(MAR)或者都不相关(MCAR)有关?这些缺失关系是概率性的还是确定性的?

图 6-22 展示了一个决策树,总结了我们诊断缺失数据的逻辑。

诊断缺失数据的决策树

图 6-22. 诊断缺失数据的决策树

在接下来的部分,我们将看到如何在每种情况下处理缺失数据。

处理缺失数据

当我们进入本章的操作部分时,首先要记住的是我们并不是为了处理缺失数据而处理缺失数据:我们的目标是在我们的数据中获得无偏差和准确的因果关系估计。只有当缺失数据干扰了这一目标时,缺失数据才会成为问题。

这一点需要强调,因为你的第一反应可能是,成功处理缺失数据的结果是一个没有缺失数据的数据集,但事实并非如此。我们将使用的方法,多重插补(MI),会为您的数据创建多个副本,每个副本都有其自己的插补值。通俗地说,我们永远不会说“Bob 缺失的年龄的正确替代值是 42 岁”,而是“Bob 可能是 42 岁、38 岁、44 岁、42 岁或者 38 岁”。没有单一的最佳猜测,而是一系列可能性。另一个最佳实践方法,最大似然估计,甚至不会对个别值进行任何猜测,只处理高阶系数,例如均值和协方差。

在下一小节中,我将为您提供 MI 方法的高级概述。之后,我们将深入研究模型的更详细算法规格:

  1. 首先,预测均值匹配算法

  2. 接下来,正常算法

  3. 最后,如何将辅助变量添加到算法中

不幸的是,鲁宾分类中缺失类型与适当的算法规格之间并不存在一对一的关系,因为可用的信息量也很重要(参见 表 6-4)。

表 6-4. 根据缺失类型和可用信息的最佳 MI 参数

缺失类型 无信息 变量分布为正态 缺失分布为确定性
MCAR 均值匹配 正态分布 (不可能)
MAR 均值匹配 正态分布 正态分布 + 辅助变量
MNAR 均值匹配 + 辅助变量 正态分布 + 辅助变量 正态分布 + 辅助变量

多重插补(MI)介绍

要理解多重插补的工作原理,有助于将其与传统的处理缺失数据的方法进行对比。除了简单地删除所有具有缺失值的行之外,传统方法都依赖于用特定值替换缺失值。替换值可以是变量的整体平均值,或者基于其他可用于该客户的变量预测的值。无论用于替换值的规则如何,这些方法都存在根本性缺陷,因为它们忽略了由缺失数据引入的额外不确定性,并且可能会在分析中引入偏差。

解决这个问题的 MI 解决方案,正如其名称所示,是构建多个数据集,其中缺失值被不同的值替换,然后用每个数据集运行我们感兴趣的分析,并最终聚合生成的系数。

在 R 和 Python 中,整个过程都是在后台管理的,如果您想保持简单,只需指定要运行的数据和分析即可。

首先让我们看看 R 代码:

## R
> MI_data <- mice(available_data, print = FALSE)
> MI_summ <- MI_data  %>%
    with(lm(bkg_amt~age+open+extra+neuro+gender+state)) %>%
    pool() %>%
    summary()
> print(MI_summ)
         term   estimate  std.error statistic        df      p.value
1 (Intercept) 240.990671 15.9971117 15.064636  22.51173 3.033129e-13
2         age  -1.051678  0.2267569 -4.637912  11.61047 6.238993e-04
3        open   3.131074  0.8811587  3.553360 140.26375 5.186727e-04
4       extra  11.621288  1.2787856  9.087753  10.58035 2.531137e-06
5       neuro  -6.799830  1.9339658 -3.516003  15.73106 2.929145e-03
6     genderF -11.409747  4.2044368 -2.713740  20.73345 1.310002e-02
7      stateB  -9.063281  4.0018260 -2.264786 432.54286 2.401986e-02
8      stateC  -5.334055  4.7478347 -1.123471  42.72826 2.675102e-01

mice包(多重插补通过链式方程)具有mice()函数,用于生成多个数据集。然后,我们使用with()关键字将感兴趣的回归应用于每一个数据集。最后,从mice中使用pool()函数以我们可以用传统的summary()函数读取的格式汇总结果。

Python 代码几乎相同,因为它实现了相同的方法:

## Python 
MI_data_df = mice.MICEData(available_data_df)                                 
fit = mice.MICE(model_formula='bkg_amt~age+open+extra+neuro+gender+state', 
                model_class=sm.OLS, data=MI_data_df)                
MI_summ = fit.fit().summary()                                                       
print(MI_summ)

                          Results: MICE
===================================================================
Method:                  MICE         Sample size:          2000   
Model:                   OLS          Scale                 5017.30
Dependent variable:      bkg_amt      Num. imputations      20     
-------------------------------------------------------------------
           Coef.   Std.Err.    t    P>|t|   [0.025   0.975]   FMI  
-------------------------------------------------------------------
Intercept 120.3570   8.8662 13.5748 0.0000 102.9795 137.7344 0.4712
age        -1.1318   0.1726 -6.5555 0.0000  -1.4702  -0.7934 0.2689
open        3.1316   0.8923  3.5098 0.0004   1.3828   4.8804 0.1723
extra      11.1265   1.0238 10.8680 0.0000   9.1200  13.1331 0.3855
neuro      -4.5894   1.7968 -2.5542 0.0106  -8.1111  -1.0677 0.4219
gender_M   65.9603   4.8191 13.6873 0.0000  56.5151  75.4055 0.4397
gender_F   54.3966   4.6824 11.6171 0.0000  45.2192  63.5741 0.4154
state_A    40.9352   3.9080 10.4748 0.0000  33.2757  48.5946 0.3921
state_B    37.3490   4.0727  9.1706 0.0000  29.3666  45.3313 0.2904
state_C    42.0728   3.8643 10.8875 0.0000  34.4989  49.6468 0.2298
===================================================================

在这里,mice算法从statsmodels.imputation包中导入。MICEData()函数生成多个数据集。然后,通过MICE()函数指定模型公式、回归类型(这里是statsmodels.OLS)和要使用的数据。我们使用.fit().summary()方法拟合我们的模型,然后打印结果。

注意

Python 实现的mice方法的一个复杂之处在于它不支持分类变量作为预测因子。如果您确实想使用 Python,您首先需要对分类变量进行独热编码。以下代码展示了如何处理性别变量:

## Python
gender_dummies = pd.get_dummies(available_data_df.\
                                gender, 
                                prefix='gender')
available_data_df =  pd.concat([available_data_df, 
                                gender_dummies], 
                               axis=1)
available_data_df.gender_F = \
np.where(available_data_df.gender.isna(), 
         float('NaN'), available_data_df.gender_F)
available_data_df.gender_M = \
np.where(available_data_df.gender.isna(), 
         float('NaN'), available_data_df.gender_M)
available_data_df =  available_data_df.\
drop(['gender'], axis=1)

首先,我们使用 pandas 的get_dummies()函数创建变量gender_Fgender_M。在将这些列添加到我们的数据帧之后,我们指示缺失值的位置(默认情况下,独热编码函数在分类变量值缺失时将所有二进制变量设置为 0)。最后,我们从数据中删除原始分类变量,并使用包含新变量的模型进行拟合。

然而,独热编码通过删除变量之间的逻辑连接来破坏数据的内部结构,因此效果可能因人而异(例如,您可以看到因为结构不同,R 和 Python 中的分类变量系数不同),如果分类变量在您的数据中起重要作用,我建议您使用 R 而不是 Python。

就是这样!如果您现在停止阅读本章,您将得到一个处理缺失数据的解决方案,这比传统方法要好得多。然而,通过花时间揭示内部机制并更好地理解插补算法,我们可以做得更好。

默认插补方法:预测均值匹配

在前一小节中,我们未指定插补方法,依赖于mice的默认设置。在 Python 中,唯一可用的插补方法是预测均值匹配,因此在这里没有其他操作。让我们来看看 R 中默认的插补方法,通过请求插补过程的摘要:

## R
> summary(MI_data)
Class: mids
Number of multiple imputations:  5 
Imputation methods:
     age     open    extra    neuro   gender    state  bkg_amt 
      ""       ""    "pmm"    "pmm"       "" "logreg"    "pmm" 
PredictorMatrix:
       age open extra neuro gender state bkg_amt
age      0    1     1     1      1     1       1
open     1    0     1     1      1     1       1
extra    1    1     0     1      1     1       1
neuro    1    1     1     0      1     1       1
gender   1    1     1     1      0     1       1
state    1    1     1     1      1     0       1

这是很多信息。现在,让我们只看看Imputation methods一行。没有缺失数据的变量有一个空字段"",这是有道理的,因为它们不会被填充。分类变量具有方法logreg,即逻辑回归。最后,数值变量具有方法pmm,即预测均值匹配(PMM)。pmm方法的工作原理是选择具有缺失值的个体的最近邻居,并用一个邻居的值替换缺失值。例如,假设数据集只有两个变量:AgeZipCode。如果您有一个来自邮编 60612 且年龄缺失的客户,算法将随机选择同一邮编中另一个客户的年龄,或者尽可能接近的客户的年龄。

由于过程中存在一些随机性,每个填充的数据集最终会得到略有不同的值,我们可以通过mice包中方便的densityplot()函数可视化来查看:

## R 
> densityplot(MI_data, thicker = 3, lty = c(1,rep(2,5)))

图 6-23 显示了原始可用数据(粗线)和填充数据集(细虚线)中数值变量的分布。如您所见,分布与原始数据非常接近;唯一的例外是BookingAmount,在填充数据集中,它的分布总体上更集中在均值周围(即“更高的峰值”)。

我们数据中数值变量的填充值分布

图 6-23. 我们数据中数值变量的填充值分布

PMM 具有一些重要的特性,这些特性可能是需要的,也可能不需要,这取决于上下文。最重要的特性是它基本上是一种插值方法。因此,您可以将 PMM 想象为创建介于现有值之间的值。通过这样做,它最小化了创建荒谬情况的风险,比如怀孕的父亲或负数金额。这种方法在变量具有奇怪分布时也能很好地工作,比如我们数据中的Age,它有两个峰值,因为它对整体分布的形状没有假设;它只是选择一个邻居。

然而,PMM 有几个缺点:它速度较慢,并且在大数据集上不易扩展,因为它必须不断重新计算个体之间的距离。此外,当您有许多变量或许多缺失值时,最近的邻居可能“很远”,填充的质量会降低。这就是为什么当我们拥有分布信息时,PMM 不会是我们首选的选项,正如我们将在下一小节中看到的那样。

从 PMM 到正态填充(仅限 R)

虽然 PMM 是一个不错的起点,但我们经常有关于数值变量分布的信息,我们可以利用这些信息来加快和改进 R 中的插补模型。特别是,行为和自然科学通常假设数值变量服从正态分布,因为这是非常常见的。在这种情况下,我们可以对变量拟合正态分布,然后从该分布中提取插补值,而不是使用 PMM。这通过创建一个插补方法的向量来完成,对于我们将假设正态性的变量,向量中的值设为"norm.nob",然后将该向量传递给mice()函数的parameter方法。

## R
> imp_meth_dist <- c("pmm", rep("norm.nob",3), "", "logreg", "norm.nob")
> MI_data_dist <- mice(available_data, print = FALSE, method = imp_meth_dist)

正如您所见,语法非常简单。唯一的问题是确定我们要为哪些数值变量使用正常插补。让我们来看看我们可用数据中的数值变量(图 6-24)。

我们数据中的数值变量分布

图 6-24. 我们数据中的数值变量分布

年龄显然不是正态的,因为它有两个峰,但所有其他变量只有一个峰。开放性外向性预订金额看起来也相对对称(在技术上来说,它们不倾斜)。统计模拟表明,只要一个变量是单峰的,并且在一个方向上没有“粗尾”,假设正态性不会引入偏差。因此,我们可以假设开放性外向性预订金额是正态分布的。

正如我们在前一节中看到的,神经质呈现出不寻常的非对称模式:尽管我们使用的心理学尺度从 0 到 10,但其值被限制在[5,10]之间,这表明神经质可能是“确定性地”MNAR,即神经质的所有值低于某个阈值都会缺失。在这种情况下使用 PMM 进行插补是有问题的:在大部分值范围内插补时几乎没有或根本没有邻居可用。极端情况下,X 的所有缺失值将被插补为 5,即阈值的值。这是一种普通方法更能恢复真实缺失值的情况。我们可以通过比较两种方法插补的值来看出这一点。图 6-25 显示了神经质的可用值(方块)以及用 PMM 方法插补的值(十字,顶部面板)和用普通方法插补的值(十字,底部面板)。

PMM 插补(顶部)和普通插补(底部)对确定性 MNAR 变量的影响

图 6-25. PMM 插补(顶部)和普通插补(底部)对确定性 MNAR 变量的影响

正如您所见,PMM 方法不对神经质低于 5 的任何值进行插补,而普通方法则会这样做。此外,PMM 方法对接近 10 的值进行了过多的插补,而普通方法更好地捕捉了分布的整体形状。然而,普通方法远未恢复真实分布(该分布一直延伸到零)。这是确定性地为 MAR 或 MNAR 变量的常见问题。我们可以通过使用辅助变量进一步改进普通插补,正如我们将在下一小节中看到的那样。

添加辅助变量

很多时候,我们会有与我们的某个具有缺失数据的变量相关的变量(例如,该变量的原因或影响),但不属于我们的回归模型。这是 mice 算法特别出色的情况,因为我们可以将这些变量添加到我们的插补模型中,以提高其准确性。然后它们被称为我们插补模型的“辅助变量”。

对于我们的 AirCnC 示例,可用的补充数据集包含两个变量,保险活跃。前者指示客户购买的旅行保险金额,与神经质强相关;而后者则测量客户选择活跃度假(例如攀岩)的程度,并与外向性强相关。我们将使用它们来帮助插补这两个人格变量。

向插补模型添加辅助变量非常简单:我们只需在插补阶段之前将它们添加到我们的数据集中即可。

## R
augmented_data <- cbind(available_data, available_data_supp)
MI_data_aux <- mice(augmented_data, print = FALSE)

## Python
augmented_data_df = pd.concat([available_data_df, available_data_supp_df], 
                              axis=1)
MI_data_aux_df = mice.MICEData(augmented_data_df)

我们可以像以前一样运行所有分析。在添加辅助变量时,通常有意义使用普通方法来处理与我们的辅助变量相关的变量(这里是神经质外向性),特别是当这些变量被截断或 MNAR 时。

除了计算约束外,我们可以包含的辅助变量数量没有限制。然而,潜在风险是我们的一些辅助变量可能仅仅因为纯粹的随机性而似乎与原始数据集中的某个变量相关,例如,保险看似与外向性相关,尽管实际上并非如此。这样的“假阳性”相关性将会被插补模型错误地加强。

解决这个潜在问题的方法是仅限制辅助变量用于某些变量的插补。不幸的是,这种解决方案只在 R 中可用。这就是 mice() 函数的预测矩阵发挥作用的地方。该矩阵出现在打印插补阶段摘要时,并且还可以直接从我们的 MIDS 对象中提取:

## R
> pred_mat <- MI_data_aux$predictorMatrix
> pred_mat
          age open extra neuro gender state bkg_amt insurance active
age         0    1     1     1      1     1       1         1      1
open        1    0     1     1      1     1       1         1      1
extra       1    1     0     1      1     1       1         1      1
neuro       1    1     1     0      1     1       1         1      1
gender      1    1     1     1      0     1       1         1      1
state       1    1     1     1      1     0       1         1      1
bkg_amt     1    1     1     1      1     1       0         1      1
insurance   1    1     1     1      1     1       1         0      1
active      1    1     1     1      1     1       1         1      0

该矩阵指示用于填充哪些变量。默认情况下,除了自身外,所有变量都用于填充所有变量。矩阵中的“1”表示“列”变量用于填充“行”变量。因此,我们需要修改最后两列,用于仅填充保险活跃度神经质外向性

## R
> pred_mat[,"insurance"] <- 0
> pred_mat[,"active"] <- 0
> pred_mat["neuro","insurance"] <- 1
> pred_mat["extra","active"] <- 1
> pred_mat
          age open extra neuro gender state bkg_amt insurance active
age         0    1     1     1      1     1       1         0      0
open        1    0     1     1      1     1       1         0      0
extra       1    1     0     1      1     1       1         0      1
neuro       1    1     1     0      1     1       1         1      0
gender      1    1     1     1      0     1       1         0      0
state       1    1     1     1      1     0       1         0      0
bkg_amt     1    1     1     1      1     1       0         0      0
insurance   1    1     1     1      1     1       1         0      0
active      1    1     1     1      1     1       1         0      0

通过这种修改,我们将减少意外地将偶然相关性纳入我们的填充模型的风险。

扩展缺失数据集数量

mice算法在 R 中的默认生成的填充数据集数量为 5,在 Python 中为 10。这些对于探索性分析来说是很好的默认值。

在最终运行时,如果你只关心回归系数的估计值,应使用 20(通过将m=20作为mice()函数的参数设置传递)。如果你需要更精确的信息,例如置信区间或变量之间的交互作用,可能需要目标设定为 50 到 100。主要的限制因素是计算机的速度和内存——如果你的数据集大小为 100 Mb 甚至 1 Gb,你是否有足够的 RAM 来创建 100 份拷贝?——以及你的耐心。

更改填充数据集数量的语法很简单。在 R 中,它作为参数传递给mice()函数,而在 Python 中,它作为MICE对象的.fit()方法的参数传递:

## R
MI_data <- mice(available_data, print = FALSE, m=20)

## Python
fit = mice.MICE(model_formula='bkg_amt~age+open+extra+neuro+gender+state', 
                model_class=sm.OLS, data=MI_data_df) 
MI_summ = fit.fit(n_imputations=20).summary()

结论

缺失数据在行为数据分析中可能会带来真正的问题,但这并非必然。至少,使用 R 或 Python 中的mice包及其默认参数将优于删除所有具有缺失值的行。通过基于 Rubin 的分类正确诊断缺失性,并利用所有可用信息,通常可以取得更好的效果。总结决策规则,图 6-26 展示了用于诊断缺失数据的决策树,表 6-5 展示了基于缺失类型和可用信息的最佳 MI 参数。

诊断缺失数据的决策树

图 6-26. 诊断缺失数据的决策树

表 6-5. 根据缺失类型和可用信息的最佳 MI 参数

缺失类型 无信息 变量分布正常 缺失分布确定性
MCAR PMM norm.nob
MAR PMM norm.nob norm.nob + aux. var.
MNAR PMM + aux. var. norm.nob + aux. var. norm.nob + aux. var.

第七章:用自助法测量不确定性

有了理想的数据,您现在能够从行为数据中得出坚固的结论,并衡量业务/环境变化对人类行为的因果影响。但如果您有次优数据,该如何继续呢?在学术研究中,面对不确定的数据,人们总是可以回到零假设,并拒绝做出判断。但在应用研究中,没有零假设,只有可以选择的替代行动方案。

小样本量、形状奇特的变量或需要高级分析工具的情况(例如我们将在本书后面看到的层次建模)都可能导致不稳定的结论。当然,线性回归算法几乎在所有但极端情况下都会输出系数,但您应该信任它吗?您能否自信地建议您的老板将数百万美元押在它上面?

在本章中,我将向您介绍一种非常强大且通用的模拟工具——自助法(Bootstrap),它将使我们能够从任何数据中得出坚固的结论,无论数据多么少或奇特。它的工作原理是基于随机数创建和分析您数据的稍微不同的版本。自助法的一个伟大特性是,通过应用它,您绝对不会出错:在传统统计方法最理想的情况下(例如对大型和表现良好的数据集运行基本线性回归),自助法可能速度较慢且不够准确,但仍然在接近范围内。但一旦您远离这种最理想的情况,自助法往往会迅速超越传统统计方法,常常优势明显。^(1) 因此,在本书的其余部分中,我们将广泛依赖它。特别是在设计和分析第四部分中的实验时,我们将使用它来构建比传统统计方法更直观的模拟 p 值等效物。

在第一节中,我们将集中进行探索性/描述性数据分析,并且我们将看到自助法在这个阶段已经可以派上用场。在第二节中,我们将在回归的背景下使用自助法。然后,我们将扩展我们的视角,讨论何时使用自助法以及您可以使用哪些工具来简化生活。

自助法简介:“自我拉抬”

虽然我们的最终目标是将自助法用于回归分析,但我们可以从更简单的描述统计的例子开始:获取样本数据集的均值。

在本章中,除了常见的包外,我们还将使用以下包:

## Python
import statsmodels.api as sm # For QQ-plot
import statsmodels.stats.outliers_influence as st_inf # For Cook distance

商业问题:带有异常值的小数据

C-Mart 的管理层有兴趣了解其烘焙师准备定制蛋糕的时间,以便可能修订其定价结构。为此,他们要求 C-Mart 的工业工程师进行时间研究。正如其名称所示,时间研究(又称时间与动作研究)是直接观察生产过程以测量涉及任务的持续时间。考虑到这个过程耗时(故意的双关语),工程师选择了十家不同的店铺,这些店铺在某种程度上代表了 C-Mart 的业务。在每家店铺,他们观察一位烘焙师准备一块蛋糕。他们还记录了每位烘焙师的工作经验,以月计算。

总之,工程师有 10 个观察结果,并不是一个非常大的样本量。即使所有数据都非常一致地符合明确的关系,仅样本量就建议使用 Bootstrap。然而,在探索他们的数据时,工程师观察到存在一个异常值(图 7-1)。

我们在左上角有一个极端点,对应于一位新员工为企业撤退复杂蛋糕花了大部分时间的情况。工程师该如何报告他们研究的数据呢?他们可能会倾向于将最大的观测值视为异常值,这是委婉地说“丢弃它并假装它没有发生”的方式。但是,虽然这个观测结果不寻常,但并不是本质上的偏差。没有测量误差,并且这种情况可能偶尔发生。另一个选择是仅报告整体平均持续时间,为 56 分钟,但这样也会误导,因为它无法传达数据的变异性和不确定性。在这种情况下的传统建议是使用均值周围的置信区间。让我们通过回归来计算正常的 95% 置信区间。(在这种情况下使用回归是杀鸡用牛刀——有更简单的方法来计算平均值——但它将作为本章后续过程的温和介绍。)

烘焙师的经验和准备时间

图 7-1. 烘焙师经验与准备时间

我们首先运行回归 times~1,即仅带截距。然后我们提取该系数的估计结果,如果你对这个计算不熟悉,它等于我们因变量的平均值。我们还提取该系数的标准误差。正如你在任何统计课上学到的那样,正常 95% 置信区间的下限等于平均值减去 1.96 倍标准误差,上限等于平均值加上 1.96 倍标准误差:

## R (output not shown)
lin_mod_summ <- summary(lm(times~1, data=dat))
est <- lin_mod_summ$coefficients[1,1]
se <- lin_mod_summ$coefficients[1,2]
LL <- est-1.96*se
UL <- est+1.96*se

## Python
lin_mod = ols("times~1", data=data_df).fit()
est = lin_mod.params['Intercept']
se = lin_mod.bse['Intercept']
LL = est-1.96*se #Lower limit
UL = est+1.96*se #Upper limit
print("LL = ", LL)
print("UL = ",UL)

LL =  -23.040199740431333
UL =  134.64019974043134

不幸的是,本例中的 95%置信区间是[−23; 135],这显然是荒谬的,因为持续时间不能是负数。这是因为传统的置信区间假设手头的变量围绕其均值呈正态分布,而在这种情况下是不正确的。我们可以想象,工程师的观众对负持续时间可能并不感兴趣,但这是 Bootstrap 可以解决的问题之一。

样本均值的 Bootstrap 置信区间

Bootstrap 允许我们充分利用我们可用的数据,并在样本大小或数据形状挑战的情况下得出合理的结论。它通过基于我们可用的数据创建多个虚构数据集来实现这一目的。比较这些数据集可以帮助我们去除噪音,并更准确地表示离群值的重要性。它还可以提供更紧密的置信区间,因为它消除了噪音造成的一些不确定性。

这与从一开始就选择较窄范围(例如选择 80%置信区间而不是 95%置信区间)不同,因为 Bootstrap 生成的数据集反映了给定可用数据的真实概率分布。不会有生成的数据集显示负持续时间,因为数据不反映这种可能性,但原始数据确实包含非常长的持续时间。因此,使用 Bootstrap 生成的置信区间预计将从范围的负侧去除更多内容,但可能不会从正侧去除或者甚至可能增加。

建立 Bootstrap 置信区间的过程在概念上很简单:

  1. 我们通过从我们观察到的样本中有放回抽样来模拟相同大小的新样本。

  2. 接着,对于每一个模拟样本,我们计算我们感兴趣的统计量(这里是均值,这是我们的工业工程师想要测量的量)。

  3. 最后,我们通过查看第 2 步得到的数值的百分位数来构建我们的置信区间。

有放回抽样意味着每个值每次被抽中的概率都是相同的,无论它之前是否已经被抽中。

例如,从(A,B,C)有放回抽样等可能地产生(B,C,C)或(A,C,B)或(B,B,B),等等。因为每个位置有三种可能性,所以每个位置有 3 x 3 x 3 = 27 种可能的模拟样本。如果我们不进行放回抽样,这意味着一个值不能被抽取超过一次,唯一可能的组合将是原始样本的排列组合,如(A,C,B)或(B,A,C)。这将简单地意味着将值重新洗牌,这将是毫无意义的,因为均值(或任何其他感兴趣的统计量)将保持完全相同。

在 R 和 Python 中,有放回抽样非常简单:

## R
boot_dat <- slice_sample(dat, n=nrow(dat), replace = TRUE)

## Python
boot_df = data_df.sample(len(data_df), replace = True)

通过仅从我们观察到的样本中抽取新样本的美妙之处在于,它避免了对我们观察到的样本之外的数据做出任何分布假设。为了看到这意味着什么,让我们模拟 B = 2,000 个 Bootstrap 样本(为了避免混淆,我将始终使用 B 表示 Bootstrap 样本数量,N 表示样本大小),并计算每个样本的均值。我们的代码如下(R 和 Python 之间的调用号码是共享的):

## R
mean_lst <- list() ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)
B <- 2000
N <- nrow(dat)
for(i in 1:B){  ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)
  boot_dat <- slice_sample(dat, n=N, replace = TRUE)
  M <- mean(boot_dat$times)
  mean_lst[[i]] <- M}
mean_summ <- tibble(means = unlist(mean_lst)) ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)

## Python 
res_boot_sim = [] 
B = 2000
N = len(data_df)
for i in range(B): 
    boot_df = data_df.sample(N, replace = True)
    M = np.mean(boot_df.times)
    res_boot_sim.append(M)

1

首先,我初始化了一个空列表用于结果,以及 B 和 N。

2

然后我使用 for 循环从原始数据中有放回地抽取 Bootstrap 样本,每次计算均值并将其添加到结果列表中。

3

最后,在 R 中,我将列表重新格式化为 tibble,以便与 ggplot2 更轻松地使用。

图 7-2 显示了均值的分布情况。

2,000 个样本均值的分布情况

图 7-2. 2,000 个样本均值的分布情况

正如您所见,直方图非常不规则:靠近原始数据集均值的大峰以及与某些模式对应的较小峰。鉴于我们的异常值有多极端,每个七个峰对应于它在 Bootstrap 样本中的重复次数,从零到六。换句话说,在第一个(最左边)峰的样本中没有出现它,在第二个峰的样本中出现一次,依此类推。值得注意的是,即使我们增加 Bootstrap 样本的数量,直方图的不规则性也不会消失(即,“峰谷”之间的低谷不会填满),因为它反映了我们数据的粗糙性,而不是我们随机过程的限制。在我们的数据内部的值范围如此极端,以至于当排除异常值时可能的最高均值仍然很少高到足以满足包含异常值时可能的最低均值。如果异常值的值减半,因此接近人群的其余部分,则直方图将显着平滑化,因为异常值计数峰的边缘将彼此重叠。

Bootstrap 样本数量仍然很重要,但原因不同:数量越多,您将能够看到非常不太可能的样本,因此极端值更多。例如,如果我们抽取了异常值 10 次,那么样本均值的绝对最大可能值将为 413,其概率为 (0.1)¹⁰(十分之一的十次方),这意味着大约每 100 亿个样本中会发生一次。通过我们仅有的 2,000 个样本,我们几乎看不到大约 200 左右的值。但是我们的样本的整体均值或中位数仍将保持相同,加上或减去可忽略的抽样变化。

这里有一些关于样本数量的一般指导原则:

  • 100 到 200 个样本才能得到准确的中心估计(例如回归中的系数;它被称为“中心”,因为它大致处于置信区间的中心,与置信区间的边界或限制相反)

  • 1,000 到 2,000 个样本才能获得准确的 90% 置信区间边界

  • 5,000 个样本才能获得准确的 99% 置信区间边界

一般来说,从低处开始,如果有疑问增加数量然后再试一次。这与例如在数据上运行多次分析直到得到你喜欢的数字(也称为“ p 值破解”或“ p 破解”)根本不同;这更像是在查看图形时更改屏幕的分辨率。这对你的分析没有风险;它只是花费更多或更少的时间,这取决于你的数据量和计算机的计算能力。

根据我们拥有的数据,我们增加直方图的平滑度的唯一方法就是增加样本大小。然而,我们必须增加真实世界原始样本的大小,而不是自举样本的大小。为什么我们不能增加自举样本的大小(例如,从我们的 10 个值的样本中有放回地抽取 100 个值)?因为我们的目标不是创建新的样本,而是确定我们对均值的估计可能有多大偏差,当我们假设总体与我们的原始样本相似时。为了做到这一点,我们需要使用原始样本中的所有信息——不多不少。从我们的 10 个原始值中创建更大的样本将“假装”我们拥有比实际更多的信息。

工程师准备使用自举确定蛋糕制作持续时间的置信区间边界。这些边界是根据先前均值的经验分布确定的。这意味着他们不试图拟合统计分布(例如正态分布),而是可以简单地按大小顺序排列值,然后查看 2.5% 分位数和 97.5% 分位数,以找到双尾 95% 置信区间。使用 2,000 个样本,2.5% 分位数等于第 50 个最小均值的值(因为 2,000 * 0.025 = 50),而 97.5% 分位数等于较小到较大的第 1950 个均值的值,或第 50 个最大均值的值(因为两个尾部具有相同数量的值)。幸运的是,我们不必手动计算这些:

## R (output not shown)
LL_b <- as.numeric(quantile(mean_summ$means, c(0.025)))
UL_b <- as.numeric(quantile(mean_summ$means, c(0.975)))

## Python 
LL_b = np.quantile(mean_lst, 0.025)  
UL_b = np.quantile(mean_lst, 0.975)
print("LL_b = ", LL_b)
print("UL_b = ",UL_b)

LL_b =  7.4975000000000005
UL_b =  140.80249999999998

自举 95% 置信区间是 [7.50; 140.80](加减一些抽样差异),这更加现实。图 7-3 显示了与 图 7-2 相同的直方图,但添加了均值的均值、正常置信区间边界和自举置信区间边界。

2,000 个样本均值的分布,均值的均值(粗线),正常 95% 置信区间边界(虚线),和自举置信区间边界(虚线)的分布

图 7-3. 2,000 个样本均值分布,均值均值(粗线)、正常 95%置信区间界限(虚线)和 Bootstrap 置信区间界限(虚线)

除了 Bootstrap 下限高于零之外,我们还可以注意到 Bootstrap 上限略高于正常上限,更好地反映了分布向右的不对称性。

自适应统计量的 Bootstrap 置信区间

当传统统计方法失效时,使用 Bootstrap 使我们能够建立合理的置信区间。我们还可以在没有其他方法的情况下使用它来建立置信区间。例如,让我们假设 C-Mart 的管理层正在考虑实施时间承诺——“三小时内完成蛋糕或打五折”,并想知道目前有多少蛋糕需要超过三小时烘焙。我们的估计将是样本百分比:在观察到的 10 个案例中有 1 个,即 10%。但我们不能就此打住,因为这个估计存在显著不确定性,我们需要传达这一点。10 次观察中的 10%比起 100 次或 1000 次观察中的 10%更不确定。

那么我们如何围绕那个 10%的值建立置信区间?当然是使用 Bootstrap。这个过程与之前完全相同,只是我们不再取每个模拟样本的平均值,而是测量样本中超过 180 分钟值的百分比:

## R
promise_lst <- list()
N <- nrow(dat)
B <- 2000
for(i in 1:B){
  boot_dat <- slice_sample(dat, n=N, replace = TRUE)
  above180 <- sum(boot_dat$times >= 180)/N
  promise_lst[[i]] <- above180}
promise_summ <- tibble(above180 = unlist(promise_lst))
LL_b <- as.numeric(quantile(promise_summ$above180, c(0.025)))
UL_b <- as.numeric(quantile(promise_summ$above180, c(0.975)))
## Python
promise_lst = []
B = 2000
N = len(data_df)
for i in range(B):
    boot_df = data_df.sample(N, replace = True)
    above180 =  len(boot_df[boot_df.times >= 180]) / N
    promise_lst.append(above180)
LL_b = np.quantile(promise_lst, 0.025)  
UL_b = np.quantile(promise_lst, 0.975)

结果的直方图显示在图 7-4 中。由于我们只有 10 个数据点,所以条形之间有“白色空间”,因此百分比是 10%的倍数。如果数据点更多,情况将不同;通常情况下,百分比将是 1/N 的倍数,其中 N 为样本大小(例如,如果有 20 个点,百分比将是 5%的倍数)。

有超过 180 分钟准备时间的样本计数直方图

图 7-4. 有超过 180 分钟准备时间的样本计数直方图

在 2000 个模拟样本中的约 700 个样本中,没有蛋糕的准备时间超过 180 分钟。约 750 个样本中,恰好有一个这样的蛋糕,依此类推。相应的 95%置信区间是[0; 0.3]:第 50 个最低值为 0,第 50 个最高值为 0.3。

换句话说,即使数据受限,我们也可以相当自信地说,超过三小时准备的蛋糕比例超过 30%的可能性非常低(尽管不是不可能)。对于仅有 10 次观察和如此独特的统计数据来说,这仍然是一个相当大的置信区间!

注意

如果这个概念难以理解,您可以通过计算具有 10 次观察中 1 次成功的二项分布的置信区间来重新构思前述问题。在这种情况下,R 和 Python 提供了近似方法来计算置信区间。这些方法通常比我们的自举置信区间更保守(即更广),但差距不会很大。

使用自举法,工程师可以加强他们通常希望通过数据执行的分析。他们能够用有限的数据回答多种问题,并且具有相对可接受的确定性(和相应的可容忍不确定性)。

回归分析的自举法

尽管围绕均值建立置信区间可能很有用,但回归才是本书的重点,所以让我们看看如何利用自举法来实现这一目的。我们在 C-Mart 的工业工程师想要使用有关蛋糕准备的同一数据来确定经验对烘焙时间的影响。相应的因果图非常简单(图 7-5)。

我们感兴趣关系的因果图

图 7-5. 我们感兴趣关系的因果图

在没有显示任何混杂变量的因果图的情况下,对我们的数据进行回归是直接的。然而,得到的系数并不显著:

## Python (output not shown)
print(ols("times~experience", data=data_df).fit().summary())
## R 
mod <- lm(times~experience, data=dat)
mod_summ <- summary(mod)
mod_summ
...
Coefficients:
            Estimate Std. Error t value Pr(>|t|)  
(Intercept)  132.389     61.750   2.144   0.0644
experience    -9.819      6.302  -1.558   0.1578 
...

我们的估计系数为 −9.8,这意味着每增加一个月的经验,准备时间预计会减少 9.8 分钟。然而,基于回归标准误差的传统置信区间为 [–22.2; 2.5]。从传统的角度来看,这将是游戏结束:置信区间包含零,这意味着经验月份可能对烘焙时间有正面、负面或零影响,因此我们不会得出任何实质性结论。让我们看看自举法告诉我们的是什么。过程与之前完全相同:我们通过从原始样本中有放回地抽取大量次数的样本来模拟 10 个数据点的样本,然后保存回归系数。上次我们使用了 B = 2,000 个样本。这次让我们使用 B = 4,000,因为这使得相应的直方图看起来更加平滑(图 7-6):

## R (output not shown)
reg_fun <- function(dat, B){
  N <- nrow(dat)
  reg_lst <- list()
  for(i in 1:B){
    boot_dat <- slice_sample(dat, n=N, replace = TRUE)
    summ <- summary(lm(times~experience, data=boot_dat))
    coeff <- summ$coefficients['experience','Estimate']
    reg_lst[[i]] <- coeff}
  reg_summ <- tibble(coeff = unlist(reg_lst))
  return(reg_summ)}
reg_summ <- reg_fun(dat, B=4000)
## Python (output not shown)
reg_lst = []
B = 4000
N = len(data_df)
for i in range(B):
    boot_df = data_df.sample(N, replace = True)
    lin_mod = ols("times~experience", data=boot_df).fit()
    coeff = lin_mod.params['experience']
    reg_lst.append(coeff)
LL_b = np.quantile(reg_lst, 0.025)  
UL_b = np.quantile(reg_lst, 0.975)

准备时间对经验的回归系数分布,显示其均值(粗线)、自举置信区间边界(粗虚线)和正常置信区间边界(细点线)(B=4,000 自举样本)

图 7-6. 准备时间对经验的回归系数分布,显示其均值(粗线)、自举置信区间边界(粗虚线)和正常置信区间边界(细点线)(B = 4,000 自举样本)

Bootstrap 的置信区间是[–28; –0.2]。正如您在图 7-6 中看到的那样,与对称正态边界相比,它再次是不对称的,左侧有一个长尾。分布的高度不规则形状反映了存在两个竞争假设:

  • 靠近零的高而窄的峰是由不包含异常值的样本组成,因此它对应于异常值是不会重复的意外事件的观点。这是如果丢弃异常值时得到的置信区间。

  • 左侧宽阔的平台是由包含异常值一次或多次的样本组成。它反映了异常值真正代表我们数据的假设,并且其真实频率可能甚至高于我们的小样本。

您可以将其视为数据驱动的场景分析。如果不存在这种模式会怎样?如果它主导了我们的数据会怎样?与其在丢弃异常值或让其驱动我们的结果之间做出选择,Bootstrap 允许我们一次考虑所有可能性。

除了构建置信区间外,我们还可以使用 Bootstrap 来确定等效的 p 值。如果您查看本节开头回归的输出,您将看到经验列中 p 值为 0.16(即具有标签 Pr(>|t|)的列)。您可能已经被告知,如果系数的 p 值小于 0.05(或在更严格的情况下为 0.01),则系数在统计上显著(即与零显著不同)。从数学上讲,p 值是这样的,即(1 减去 p 值)-置信区间的上界为零。在正常回归的情况下,零是 84%置信区间的上界。因为 84%小于 95%或 99%,经验不会被认为在统计上显著。相同的逻辑也可以用于 Bootstrap;我们只需计算 Bootstrap 样本中系数高于零的分数,并乘以 2,因为这是一个双侧检验:^(2)

## Python (output not shown)
pval = 2 * sum(1 for x in reg_lst if x > 0) / B

## R
reg_summ %>% summarise(pval = 2 * sum(coeff > 0)/n())
# A tibble: 1 x 1
    pval
  <dbl>
1 0.04

这意味着我们的经验性 Bootstrap p 值^(3)约为 0.04,而不是基于统计假设的传统 p 值 0.16。这是有帮助的,因为人们通常熟悉统计 p 值,而 Bootstrap p 值可以替代使用。从商业角度来看,我们现在可以确信回归系数在零和强负之间。此外,我们可以轻松计算任何其他阈值的等效 p 值(例如,如果我们想要使用−1 而不是零作为阈值),或者任何区间,例如[−1; +1]。

何时使用 Bootstrap

希望到目前为止,你已经被 Bootstrap 在小型和奇异数据集上的优点所说服。但是对于大型或均匀形状的数据集,应该总是使用 Bootstrap 吗?简短的答案是,使用它永远不会错,但可能不切实际或过度复杂。对于实验数据,我们将大量依赖于 Bootstrap,正如我们将在本书的第四部分 Part IV 中看到的那样。对于本章节的观察数据分析,情况更为复杂。Figure 7-7 展示了我们将使用的决策树。它可能看起来有点吓人,但在概念上可以分解为三个块:

  • 如果你只需要一个中心估计(例如回归系数),并且传统估计的条件已经满足,你可以使用它。

  • 如果你需要一个置信区间(CI),并且传统 CI 的条件已经满足,你可以使用它。

  • 在任何其他情况或疑问时,请使用 Bootstrap CI。

让我们依次回顾这些块。

使用 Bootstrap 的决策树

图 7-7. 使用 Bootstrap 的决策树

传统中心估计条件的充分性

首先要记住的是,Bootstrap 产生的中心估计或系数非常接近传统方法得到的估计(即如果你不知道 Bootstrap,你会做的那种)。因此,当传统估计只需一行代码时,直接从 Bootstrap 开始从来都没有意义。

然而,如果你的数据很小(通常少于 100 行)或在任何方面都很奇怪(例如有多个峰值或不对称),那么中心估计可能会误导。在这种情况下,你应该真正使用 Bootstrap 来计算置信区间,最好将其结果显示为直方图,就像我们在 Figure 7-6 中所做的那样。

同样地,如果系数接近边界或阈值,因此经济上不明确,你将需要使用一个 CI,中心估计将不足以满足需求。

即使事情如此清晰和明确,你可能仍然想要一个 CI,例如因为你的老板或商业伙伴要求它。

传统 CI 的充分性条件

如果你想要一个 CI,但你的数据并非如此小或奇怪,以至于需要 Bootstrap CI,那么问题就变成了传统 CI 是否可靠并且足够满足你的目的。在这种情况下,你需要运行两个测试:

  • 检查是否存在影响力点。

  • 检查回归残差的正态性(仅当回归是线性的时候)。

只有当你的数据没有影响力点,并且残差没有问题时,你才能使用传统 CI。

有影响力的点是指删除它将大幅改变回归结果的点,而Cook's distance这一统计量精确地测量了这一点。对于我们这里的目的,知道数据点的 Cook's distance 超过一就被认为有影响力就足够了。R 和 Python 都有一行代码来计算关于回归模型的 Cook's distance:

## Python (output not shown)
CD = st_inf.OLSInfluence(lin_mod).summary_frame()['cooks_d']
CD[CD > 1]
## R
> CD <- cooks.distance(mod)
> CD[CD > 1]
     10 
1.45656

按定义,影响力点不遵循其他点的相同模式(否则删除它不会显著改变回归结果)。这意味着影响力点总是一个异常值,但异常值并不总是一个影响力点:异常值远离其他点形成的云团,但仍可能接近没有它计算的回归线,并且具有较小的 Cook's distance。在我们的烘焙示例中,异常值点也是一个影响力点。

如果您的数据中有任何影响力点,表明标准的分布假设未被满足,使用 Bootstrap 方法可能更明智。

如果你的数据中没有影响力点,那么在线性回归情况下还需要进行第二项检查:确保回归残差近似正态分布。这不适用于逻辑回归,因为它的残差遵循伯努利分布而不是正态分布。这一检查同时回答了“非正态有多非正态?”和“大有多大?”这两个问题,因为它们是相关的:更大的数据可以抑制对正态性的轻微偏差,所以对于一百个数据点来说可能会有问题的非正态度,在十万个数据点的情况下可能就可以接受了。

提取回归残差并直观评估其正态性。在 R 中,我们通过应用函数resid()到我们的线性回归模型来获得残差:

## R
res_dat <- tibble(res = resid(mod))
p1 <- ggplot(res_dat, aes(res)) + geom_density() + xlab("regression residuals")
p2 <- ggplot(res_dat, aes(sample=res)) + geom_qq() + geom_qq_line() + 
  coord_flip()
ggarrange(p1, p2, ncol=2, nrow=1)

Python 中的语法同样简单:首先从模型中获取残差,然后使用 Seaborn 包绘制密度图,再使用 statsmodels 包绘制 QQ 图:

## Python 
res_df = lin_mod.resid
sns.kdeplot(res_df)
fig = sm.qqplot(res_df, line='s')
plt.show()

图 7-8 展示了我们在 R 中创建的两个图,一个是密度图,一个是 QQ 图。

回归残差的密度图(左)和 QQ 图(右)

图 7-8. 回归残差的密度图(左)和 QQ 图(右)

让我们首先看左边的密度图。对于正态密度,我们期望看到一个以零为中心的单峰曲线,左右对称的光滑递减的尾部。由于存在一个具有较大残差的异常值,显然这里并非如此,因此我们得出结论残差不是正态分布的。

右侧的图是一个 QQ 图(QQ 代表分位数-分位数),用geom_qq()qqplot()绘制,显示了我们残差的值在 x 轴上和理论正态分布在 y 轴上。对于正态密度,我们期望所有点都在直线上或非常靠近直线,但由于异常值的存在,这里并非如此。

每当线性回归的残差不服从正态分布时,Bootstrap 将比传统方法提供更好的置信区间和 p 值结果。

总结一下,构建 Bootstrap 置信区间从来不会错,而且你总能依靠它们。但是当你只需要中心估计并且可以安全依赖它时,或者可以安全地依赖传统的置信区间时,就不必盲目地转向 Bootstrap 了。

最后,让我们稍微详细地看看如何确定要使用的 Bootstrap 样本数量。

确定 Bootstrap 样本数量

一旦决定使用 Bootstrap,你需要确定模拟中使用的样本数量。如果你只是想大致了解估计值的变异性,那么根据 Bootstrap 的“发明者”埃弗隆,B = 25 到 200 可以给出对主要估计值相当健壮的结果。把它看作是一个 75%的置信区间。你不会把它套上赌场,但它告诉你的不仅仅是一个平均值。

另一方面,假设你想要一个精确的 p 值或 95%的置信区间,因为不确定是否有关键阈值(通常为零)在其中或不在其中。那么你需要更大的 B,因为我们通常在 Bootstrap 分布的 2.5%最小值或最大值上看。使用 B = 200 时,双尾 95%置信区间的下限等于 200 * 2.5%,或第五小的值,同样地,上限等于第五大的值。五是一个相当小的数字。你很容易运气不好,得到五个比预期更小或更大的数字,从而打乱了你的置信区间边界。让我们通过仅使用 200 个样本重复上一节的 Bootstrap 回归来可视化这一点。如你在图 7-9 中看到的,分布的形状整体上与图 7-5 相似,但现在我们的置信区间的上限是大于零。

准备时间对经验的回归系数分布,包括其均值(粗线)、Bootstrap 置信区间边界(粗虚线)和正态置信区间边界(细虚线)(B=200 Bootstrap 样本)

图 7-9. 准备时间对经验的回归系数分布,包括其均值(粗线)、Bootstrap 置信区间边界(粗虚线)和正态置信区间边界(细虚线)(B = 200 Bootstrap 样本)

因此,如果业务决策依赖于该边界与零点的关系,您需要通过增加 B 来确保准确估计。在这种情况下,通常接受使用 1,000 或甚至 2,000 个样本的指导方针。在 B = 2,000 时,2.5%分位数等于第 50 个值,因此成功的机会大得多。此外,在本章中使用的非常小的数据集中,即使模拟 4,000 个样本也不超过几秒钟,这就是我为什么使用了这么大的 B。

让我们总结何时使用 Bootstrap 处理观测数据,结合测试条件和样本数量:

  • 始终从传统的回归模型开始获取主要估计值。

  • 如果数据中少于 100 个数据点,则始终使用 B 在 25 到 200 之间的 Bootstrap 来评估该估计的不确定性。

  • 对于 N > 100 的情况,请检查数据是否存在异常点(使用 Cook 距离)或非正态性(使用残差的密度图和 QQ 图)。如果有任何可疑情况,请再次使用 Bootstrap,主要估计使用 25 到 200 之间的 B。

  • 无论 N 如何,如果需要精确的置信区间或达到的显著性水平(又称 p 值),请使用 1,000 到 2,000 之间的 B 进行另一次 Bootstrap 模拟。

  • 一旦您对在数据上运行 Bootstrap 模拟所需的时间有了大致了解,并且对应的直方图或置信区间看起来如何,随时随地都可以增加 B 值。可以在夜间使用 B = 10,000 运行模拟,以获得一个精确的图表和精确的置信区间边界。

在 R 和 Python 中优化 Bootstrap

我已向您展示了如何“手动”应用 Bootstrap 算法,以便您了解其作用,但有些包可以用更少的代码行数运行得更快。它们还允许您使用改进版本的 Bootstrap,手动编码将变得不切实际。

R:Bootstrap 包

boot包及其boot()函数为 Bootstrap 分析提供了一站式服务。尽管其简单性,它生成 Bootstrap 样本的方式并不直观,因此最好先单独查看该特性。

请记住,在早期关于回归分析 Bootstrap 的章节中,我在运行我们感兴趣的回归之前使用slice_sample()函数生成 Bootstrap 样本:

## R
(...)
for(i in 1:B){
  boot_dat <- slice_sample(dat, n=N, replace = TRUE)
  summ <- summary(lm(times~experience, data=boot_dat))
(...)

生成 Bootstrap 样本的另一种方法是获取索引列表,使用替换抽样,然后根据该列表对数据进行子集操作:

## R
> I <- c(1:10)
> I
 [1]  1  2  3  4  5  6  7  8  9 10
> J <- sample(I, 10, replace = TRUE)
> J
 [1] 10  3  1  1  6  1  9  3  4  3
> boot_dat <- dat[J,]

这是boot()函数中使用的方法。我们必须创建一个函数,该函数以我们的原始数据和索引列表 J 作为参数,并返回我们感兴趣的变量(这里是经验的回归估计)。boot()函数将负责为每次迭代生成该列表;我们只需在我们的函数中使用它来对数据进行子集操作:

## R
boot_fun <- function(dat, J){
  Boot_dat <- dat[J,]
  summ <- summary(lm(times~experience, data=boot_dat))
  coeff <- summ$coefficients['experience','Estimate']
  return(coeff)
}

创建该函数后,我们将其作为参数 statistic 传递给 boot() 函数,以及我们的原始数据作为 data,以及自举样本的数量作为 R(用于复制)。boot() 函数返回一个对象,然后我们将该对象传递给 boot.ci() 函数以获取我们的置信区间:

## R
> boot.out <- boot(data = dat, statistic = boot_fun, R = 2000)
> boot.ci(boot.out, conf = 0.95, type = c('norm', 'perc', 'bca'))
BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS
Based on 2000 bootstrap replicates

CALL : 
boot.ci(boot.out = boot.out, conf = 0.95, type = c("norm", "perc", 
    "bca"))

Intervals : 
Level      Normal             Percentile            BCa          
95%   (-25.740,   6.567 )   (-28.784,  -0.168 )   (-38.144,  -0.383 )  
Calculations and Intervals on Original Scale

boot.ci() 函数可以根据参数 type 返回各种置信区间。“norm”是基于正态分布的传统置信区间。“perc”是我们之前手工计算的百分位或分位数自举法。“bca”是偏差校正和加速的百分位自举法(BC[a])。BC[a] 自举法通过利用部分统计特性改进了百分位自举法;这些超出了我们的范围。您可以在任何列出的参考资料中了解更多信息;总之,BC[a] 自举法被认为是使用自举模拟时的最佳实践。然而,从计算的角度来看,它可能相当苛刻,因此我建议先使用百分位自举法,一旦代码基本完成,再尝试切换到 BC[a] 自举法。

在目前的情况下,正态和百分位数置信区间与我们之前手工计算的结果非常接近,这是预期的。BC[a] 置信区间向左偏移,进一步加强了我们最初的结论,即系数很可能是强负数。

现在您已经理解了使用 boot 包背后的直觉,让我们创建一个可重复使用的函数:

## R 
boot_CI_fun <- function(dat, metric_fun){
  #Setting the number of bootstrap samples
  B <- 100

  boot_metric_fun <- function(dat, J){
    boot_dat <- dat[J,]
    return(metric_fun(boot_dat))
  }
  boot.out <- boot(data=dat, statistic=boot_metric_fun, R=B)
  confint <- boot.ci(boot.out, conf = 0.90, type = c('perc'))
  CI <- confint$percent[c(4,5)]

  return(CI)
}

boot_CI_fun() 函数接受数据集和度量函数作为参数,并基于 100 个自举样本和百分位方法返回该数据集上该度量函数的 90% 置信区间。

Python 优化

与 R 相比,Python 对分析师提供了非常不同的权衡:一方面,它具有较少的统计包,没有直接实现自举算法的 R boot 包的等价物。另一方面,从性能的角度来看,我发现它对初学者更加宽容。特别是对于自举法来说,因为初学者经常大量使用的 for 循环成本相对较低。因此,我预计 Python 用户能更充分地利用我们起步时的朴素实现。

但是,如果您需要在 Python 中为自举实现增加计算性能,可以全面采用“NumPy”:

## Python
# Creating unique numpy array for sampling
data_ar = data_df.to_numpy() ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)                                        
rng = np.random.default_rng() ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)           

np_lst = []
for i in range(B): 
    # Extracting the relevant columns from array
    boot_ar = rng.choice(data_ar, size=N, replace=True) ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)          
    X = boot_ar[:,1] ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)
    X = np.c_[X, np.ones(N)]
    Y = boot_ar[:,0] ![5](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/5.png)                                           

    ### LSTQ implementation
    np_lst.append(np.linalg.lstsq(X, Y, rcond=-1)[0][0]) ![6](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/6.png)

1

我们将原始的 pandas 数据框转换为 NumPy 数组。

2

我们仅在循环外部初始化 NumPy 随机数生成器一次。

3

我们通过使用 NumPy 随机数生成器来创建我们的自举数据集,这比 pandas 的 .sample() 方法快得多。

4

我们从数组中提取预测列,并在接下来的一行中手动添加一个常数列作为截距(而statsmodel在此之前在后台为我们处理了这个)。

5

我们从数组中提取因变量列。

6

从右向左读取函数调用:我们使用np.linalg.lstsq()函数对预测变量和因变量数据进行线性回归模型拟合。参数rcond=-1消除了一个不重要的警告。对于这个特定模型,我们想要的值在[0][0]单元格中;您可以通过运行一次np.linalg.lstsq(X, Y, rcond=-1)并检查其输出来找到您需要的特定单元格。最后,我们将值附加到我们的结果列表中。

完全采用 NumPy 可以显著提高性能,对于较大的数据集可能提高约 50 倍。然而,我们的原始代码对于小数据集表现良好,并且更易读且更少出错。此外,如果您超越了简单的线性或逻辑回归,您将不得不在互联网上搜索您想要的算法的 NumPy 实现。但是,如果您需要在 Python 中改进您的 Bootstrap 代码的性能,现在您知道该如何做了。

结论

行为数据分析通常需要处理较小或较奇怪的数据。幸运的是,随着计算机模拟的出现,我们在 Bootstrap 中有了一个很好的工具来处理这种情况。Bootstrap 置信区间使我们能够正确评估估计值的不确定性,而无需依赖于关于数据分布的可能存在问题的统计假设。对于观测数据,当我们的数据表现出有影响力点或非正态性的迹象时,Bootstrap 是最有用的;否则,它通常是多余的。然而,对于实验数据,由于过度依赖于 p 值来做出决策,我们将会广泛使用它,正如我们将在本书的下一部分中看到的。

^(1) 请参阅 Wilcox(2010),其中显示了默认假设正态性的危险性。

^(2) 要了解原因,请注意,如果您有一个 90%的置信区间,那么每一侧将有 5%的值保留在外面,因为(1 − 0.9)/ 2 = 0.05。相反,如果您看到在一个置信区间上有 5%的值在一侧,则这是 90%的置信区间,因为(1 − 2 * 0.05)= 0.9。

^(3) 要完全准确,我们的 Bootstrap p 值最好称为 Bootstrap 实现的显著水平(ASL)。

第四部分:设计与分析实验

在行为科学家和商业因果数据科学家中,运行实验是他们的基础工作。确实,随机分配实验组与控制组的受试者可以使我们消除任何潜在的混杂因素,而无需甚至识别它。

关于 A/B 测试的书籍随处可见。这本书的演示有何不同之处?我认为其方法的几个方面使其既更简单又更强大。

首先,将实验重新构建在因果行为框架内,将帮助您创建更好和更有效的实验,并更好地理解从观察到实验数据分析的光谱,而不是将它们视为相互分离的。

其次,大多数关于 A/B 测试的书籍依赖于统计测试,如均值的 T 检验或比例的检验。相反,我将依赖于我们已知的工具,线性和逻辑回归,这将使我们的实验更简单更强大。

最后,传统的实验方法根据其 p 值决定是否实施测试干预,这并不能带来最佳的业务决策。相反,我将依赖于Bootstrap及其置信区间,逐步确立其作为最佳实践。

因此,第八章将展示在使用回归和 Bootstrap 时,“简单”的 A/B 测试是什么样子。也就是说,对于每个顾客,我们抛掷一个比喻性的硬币。正面他们看到版本 A,反面他们看到版本 B。这通常是网站 A/B 测试的唯一解决方案。

然而,如果您事先知道将从中抽取实验对象的人群,您可以通过分层法创建更平衡的实验组。这可以显著增加实验的能力,正如我将在第九章中展示的那样。

最后,经常发生这样的情况,您无法按所需水平随机分组。例如,您对变更对客户的影响感兴趣,但必须在呼叫中心代表的级别上随机分组。这需要集群随机化和层次建模,我们将在第十章中看到。

第八章:实验设计:基础知识

让我们从一个非常简单的实验开始探索实验:受领先在线商店的影响,AirCnC 的管理层决定增加一个“一键预订”按钮来提高 AirCnC 的预订率。正如我之前所讨论的,我们将客户分配到我们的实验组中,当他们连接到网站时,我们将一个一个地进行。这是最简单的实验类型,并且许多公司提供了界面,允许您在几分钟内创建并开始运行像这样的 A/B 测试。

这个简单的实验将为我们提供一个机会,通过这个过程而不陷入技术细节的困扰:

  1. 第一步是计划实验。这是因果行为视角的重要性所在,它有助于确保您对成功的明确定义标准,并且您了解您正在测试什么以及您期望它如何影响您的目标指标。

  2. 然后,在回顾了我们将在本章余下部分使用的数据和软件包之后,我将向您展示如何进行随机分配,并确定您实验的样本大小。

  3. 最后,我们将分析实验的结果,在这种简单情况下将非常快速。

注意

实验设计的词汇很大程度上归功于其统计学和科学根源。我会谈论“控制”和“处理”组,以及“干预”,这些可能听起来很可怕或者有点过火,实际上我们只是在讨论网站上按钮的位置或者折扣的数量。总体而言,当谈论实验时,我无法使用更简单的词汇;但是当你和你的商业伙伴谈论一个具体的实验时,我鼓励你使用与该实验相关的具体术语(例如“旧和新创意”,“折扣较低组和折扣较高组”等)。

轶事:有一次当我提出一个“干预”时,一位商业伙伴以为我是在暗示他们做得不好,我需要干预。这并不是一个开启富有成效和信任关系的最佳方式。与人相处时要以他们的方式为出发点,并努力用他们能理解的语言与他们沟通,而不是期望他们了解你的语言。

实验计划:变革理论

计划是实验设计的关键步骤。实验可能因为各种各样的原因而失败,其中许多你无法控制,比如实施过程出现问题;但糟糕的规划既是失败的频繁原因,又是你可以控制的原因。任何专门从事实验的人都有关于实验的恐怖故事,这些实验可能在技术上完美无缺,也可能完全毫无意义,因为人们对被测试的内容没有明确的认识。

最终,如果你只是在形式上行事而没有运用你的商业头脑和常识,那么任何流程都无法拯救你,但希望我即将概述的公式能够帮助你确保你已经覆盖了所有基础。我们将借鉴非营利组织和政府规划的概念,即变革理论(ToC)。简言之,您的 ToC 应通过行为变革将您正在做的事与您的最终业务目标和目标指标联系起来:

实施 [干预措施] 将帮助我们通过 [业务目标] 以 [目标指标] 为衡量标准,通过 [行为逻辑] 实现。

我将依次详细阐述这四个组成部分,但为了让你了解我们将要达到的目标,这是我们最终的变革理论的样子:

实施 [一个一键预订按钮] 将帮助我们通过 [预订概率] 减少 [预订过程时间] 以实现 [更高的收入]。

可以以 CD 格式表示,如 图 8-1 所示。

我们的实验变革理论

图 8-1. 我们的实验变革理论

让我们首先审视我们的业务目标和目标指标,然后再审视我们的干预措施,最后审视我们的行为逻辑。

业务目标和目标指标

你可能会感到惊讶,我先从业务目标和目标指标开始,而不是定义干预措施。毕竟,我们应该首先了解我们要测试什么吧?不幸的是,一个常见的失败原因是在没有清晰的目标意识的情况下决定测试某些东西(通常是最新的管理潮流或你老板的老板读到的东西)。

业务目标

第一步是为实验确定业务目标。公司通常试图增加利润,但仅仅把“更高利润”作为你的业务目标并不会很有帮助。相反,我建议再深入一层,使用诸如收入、成本、客户保留率等更具体但显然对公司有益的变量。这可能看起来是一个微不足道的步骤,但实际上可以凸显实验目标的分歧(例如,是减少成本还是增加收入?)。在这里,一键预订按钮实验的业务目标是更高的收入(图 8-2)。

我们的业务目标是收入

图 8-2. 我们的业务目标是收入

目标指标

第二步是决定实验结束时如何衡量成功,即您的目标指标。这里存在一种权衡:一方面,您希望使用尽可能接近利润的指标,例如额外收入的美元或降低的成本;另一方面,您希望选择尽可能接近您干预的指标,以减少外部干扰。

在这一点上,你经常需要做出的妥协是使用“领先指标”——基本上是你最终感兴趣的变量的原因。例如,你最终可能关心客户的生命周期价值(LTV,即他们将与你的公司花费的总金额),但不得不接受三个月的预订金额。同样,注册可以用作使用的领先指标,使用可以用作购买金额的领先指标,等等。这将使你能够比使用长期业务指标更早地报告结果,同时仍然与你的业务目标有明确的联系。

但是,如果你的目标指标是操作指标而不是财务指标,情况可能会变得复杂。如果按钮缩短了某人预订旅行所需的时间,但在任何其他方面都没有增加预订量,这算成功吗?预订体验的满意度和净推荐分数又如何?这些可能无法直接转化为货币数字,但同时假设改善它们对你的业务有积极影响也并非不合理。有时,人们会非正式地选择操作指标作为实验的目标,并用一些模糊的手段假设这些指标最终会对公司的底线产生利益。当然,有了这本书,我们可以做得更好。我们可以通过观察性研究或专门的实验来验证和衡量短期操作指标与长期业务结果之间的因果关系,稍后我们会看到。

不良目标指标的陷阱

这里的目标再次是做出良好的商业决策。我不想成为货币数字的狂热者,因为这会过于限制,而且会排除大范围的业务改进。同时,你希望确保你有一个可追踪的可测量目标指标。在这里有几个潜在的陷阱是你应该避免的。

第一个是选择你无法可靠测量的东西。说“一键预订按钮使预订体验更容易”意味着什么?你会如何衡量它?事后问产品经理或产品所有者是否有改进并不是一种测量方法。“我们的客户评价网站导航更容易,通过访问结束时的两个问题调查来衡量”可能是一个不完美的代理指标,但至少可以测量。这就是为什么将业务目标和目标指标分开表达是有意义的:前者表达了你真正的意图,即使它不可测量,而后者清楚地显示了你打算测量什么。这样可以避免误解和目标变化。

第二个陷阱是列出一长串的指标,比如“成功将是预订率、预订金额、客户满意度或净推荐分数的改善”,甚至更糟糕的是,在看到结果后才确定成功的指标——例如,最初你认为实验会改善客户体验,但当结果出来时,客户体验没有改善而平均网站会话时长却有所改善。这种方法的问题在于它增加了误报的风险(在结果是随机巧合时称其为成功)。^(1) 但是,最多可以有两个或三个在实验之前明确定义的目标指标是可以的,只要你在分析结果时考虑到这一点(稍后会详细介绍)。有些人主张使用多个指标的综合(例如,加权平均),这被称为总体评价标准(OEC),但我个人觉得它往往会比帮助更多地使事情变得模糊。我更愿意鼓励你清楚地阐明你的变革理论以及各种指标之间的关系——例如,你是否期望 1 点击按钮能改善预订率客户体验,或者是通过客户体验来改善预订率?

总之,在 1 点击按钮实验的情况下,我们可以直接使用预订收入作为我们的目标指标,但我们并不期望我们的干预会改变每次预订的平均金额,所以使用客户完成预订的概率更有意义(图 8-3)。

添加目标指标,完成预订的概率

图 8-3. 添加目标指标,完成预订的概率

干预

一旦我们有了我们的业务目标和目标指标,我们就可以开始定义我们的干预措施。这里“1 点击按钮”的干预措施的想法来自公司管理层(图 8-4),但它也可以来自用户体验或行为研究:识别公司流程、产品和服务中的问题和改进机会确实是商业研究人员的主要任务之一,但这超出了本书的范围。

添加干预,1 点击按钮

图 8-4. 添加干预,1 点击按钮

乍一看,“1 点击预订”按钮有什么比这更简单的呢?我们大多数人在在线零售商的网站上或其他地方都看到过它的实现,这个想法似乎非常简单明了。但是,一个商业想法,大家都觉得自己立即知道它是什么,与一个具体的实现之间可能存在很大的差距。如果你考虑一下如何实施细节,实际上有很多问题需要回答,每个问题都有多种可能的答案:

  • 在流程的哪个环节,按钮变得可用?

  • 按钮位于哪里?

  • 按钮的外观是什么样的?它与页面上其他按钮的颜色相同,还是以鲜艳丰富的颜色突出?

  • 按钮上写了什么?

  • 我们需要哪些关于客户的信息才能使一键预订功能可用,以及如何确保我们已经获得了这些信息?

  • 客户点击按钮后会发生什么?他们会被带到哪一页,如果有的话,他们还需要采取什么行动?

  • 等等。

比如说,假设网站的颜色主题是淡绿色和蓝色,暗示自然和旅行,而新按钮是闪亮的红色。如果该按钮增加了预订量,可能是因为一键预订的吸引力,但也可能是因为客户难以看到普通预订按钮而放弃。在这种情况下,增加预订量的原因实际上是“更可见的预订按钮”,而不是“一键预订按钮”,但你不能区分这两者,因为一键预订按钮也更显眼。不幸的是,你永远不能只测试一个想法,因为你总是在测试它的实施的许多方面。

这里的教训是,A/B 测试是一个强大但局限的工具。你需要小心,不要做出或者让别人做出关于特定实验结果的夸大陈述。这绝对是说起来容易做起来难,因为业务伙伴通常希望得到广泛、清晰且没有任何小字条款的答案。话虽如此,甚至在你的报告中简单声明你正在测试特定实施方案而非一般性想法也是有用的,例如,“这个实验将测试在某些条件下添加一键预订按钮的影响,其结果不应被解读为适用于更广泛的预订按钮。”

更一般地说,我建议你测试尽可能小的干预。在这种情况下,你可以尝试在实施更大的一键预订按钮之前改变预订按钮的颜色或位置。你可能会遇到业务伙伴的阻力,他们经常希望一次运行多个改变的“综合”测试;在这种情况下,请明确表示你只是在测试这些改变不会破坏体验,而不是实际测量影响。一个更好的选择是在实验中测试相同概念的不同实施方式。如果四种稍有不同的一键预订按钮方案有相同的影响,你就可以更自信地得出关于一键预订的总体影响的结论;另一方面,如果它们有非常不同的影响,那么这表明实施方式非常重要,你需要非常小心地得出关于特定实施将如何泛化的结论。

行为逻辑

一旦我们知道我们的业务目标和目标指标,并且我们已经定义了我们的干预措施,最后一步是通过您的变革理论的行为逻辑将两者连接起来:我们的干预措施如何影响我们的目标指标?

这是实验失败的另一个令人惊讶的常见原因:已经确定了一个问题,并且有人决定实施他们最近考虑过的最新管理潮流,尽管不清楚它为什么会帮助解决这个具体的问题。或者有人决定更具吸引力和简单的用户界面(UI)将增加购买金额。为了对我们的实验有信心,您需要能够阐明一个合理的行为故事。[²] 在 1 点击预订的情况下,您可能假设客户识别了一个吸引人的预订,但在完成预订之前放弃,因为预订过程繁琐;1 点击按钮将通过缩短和简化预订过程影响预订的概率。这通常是您的 ToC 在 CD 中汇聚在一起的地方,就像我在本章开头展示给您的那个(图 8-5)。

我们变革理论的完整 CD

图 8-5. 我们变革理论的完整 CD

总的来说,阐明您的行为逻辑有两个好处。首先,它本身通常是可测试的。大量客户是否确实在开始预订和完成预订之间流失?如果是这样,从行为数据的角度来看,假设的逻辑是有意义的。但是,例如,大多数客户是否离开而没有开始特定产品的预订过程,例如,因为他们找不到自己喜欢的东西或者因为选项太多而感到不知所措,那么 AirCnC 试图解决的可能是错误的问题;提供 1 点击预订不太可能改善他们的数字。

提问自己:什么会证实或推翻我们的逻辑?如果是真的或假的,数据会是什么样子?如果你手头没有必要的数据,可能值得进行初步测试,例如,邀请 10 人进行用户体验测试,例如,观察他们在大声思考的同时尝试使用您的网站。这可能不能完全证实或推翻您的逻辑,但可能会给您一些指示,而且成本仅为考虑解决方案的开发成本的一小部分。正如阿尔伯特·爱因斯坦经常引用的一句话:“如果我有一个小时来解决一个问题,我会花 55 分钟思考问题,5 分钟思考解决方案。”

表达你的干预行为逻辑的第二个好处是,它通常会让你对潜在收益有所感知。如果处理中的问题得到解决,最佳情况下数字会是什么样子?假设所有在预订过程中放弃的客户都能通过 1 次点击预订完成,预订率会增加多少?这是实验角度的最佳情况,因为我们假设完全解决问题,但在现实中这是不太可能的。如果在这种情况下预订率的增加无法弥补实施 1 次点击预订的成本,那就别浪费时间测试了。

一旦确认你的最佳情况下的方案是有利可图的,你可以开始思考最可能的情况。我们期望 1 次点击预订能提高预订率多少?毫无疑问,这涉及到很多主观性和不确定性,但是在表达了起作用的行为机制之后,你通常可以做出合理的猜测。你真的期望 75%的人因为预订过程太长而放弃吗?此外,通过明确人们的假设和直觉,这也是一个有价值的练习。如果产品经理和用户体验研究员对于因为过程太长而放弃的客户的百分比有极大的分歧,你需要首先弥合这个差距。其中一方知道的是另一方不知道的吗?运用你的商业感和对流程的理解。如果大多数客户在过程中的确切步骤,例如支付时放弃,那么很可能是该特定步骤出了问题——人们不会同时失去耐心。然后你可以将预期的好处与实施解决方案的成本进行比较。这样还值得吗?

你也可以从另一个角度来考虑这个问题:首先确定解决方案的盈亏平衡点,即提高目标指标使得解决方案变得有利可图,然后考虑从行为角度来看这种改善是否现实。从心理学角度来看,从期望结果开始比从盈亏平衡点开始更好:如果从盈亏平衡点开始,你更有可能被其锚定,并找到理由证明其是可以实现的。然而,在许多情况下,你会首先知道盈亏平衡点,例如在初步成本效益分析中计算出来;你的公司或业务伙伴可能也会要求这样做,并拒绝首先考虑期望结果。不要太担心这个问题。无论你是根据期望结果还是最佳情况的结果工作,我们都需要它来确定我们实验的最小可检测效果。

重要的是,您的行为逻辑要与您的目标指标建立联系。不要把它留给“这将改善客户体验”。您如何知道这一点?如果您的逻辑是扎实的,您应该能够用因果图的术语表达它,其中至少一些效果是可观察的。

一个有用的经验法则可以帮助您表达您的逻辑是将您的业务指标分解成组成部分。例如,收入(或其大部分变体)可以分解为客户数量、购买的概率/频率、购买的数量和支付的价格。确定哪些组成部分可能会受到影响可以帮助您更好地阐述业务案例。如果您的业务合作伙伴担心客户数量的减少,并且建议的干预措施可能只会增加购买数量,您需要与他们澄清他们是否仍然会认为这是一种成功。这种方法还可以减少实验中的噪声;如果建议的干预措施可能只会增加购买数量,则您可以集中精力在这个指标上,而忽略价格支付中的某些随机波动。

数据和包

本章的 GitHub 文件夹包含两个 CSV 文件,其中列出了表 8-1 中的变量。在表中,勾号(✓)表示该文件中存在的变量,而叉号(☓)表示缺失的变量。

表 8-1. 我们数据中的变量

变量描述 chap8-historical_data.csv chap8-experimental_data.csv
性别 分类,“男”/“女”
时期 月份索引,在历史数据中为 1-32,在实验数据中为 33
季节性 年度季节性,介于 0 和 1 之间
月份 年份中的月份,1-12
已预订 二进制 0/1,目标变量
一键式 二进制 0/1,实验性处理

除了序言中列出的标准包之外,在本章中,我们还将使用以下包:

## R
library(pwr) # For traditional power analysis
## Python
import statsmodels.stats.proportion as ssprop # For the standardized effect size
import statsmodels.stats.power as ssp # For traditional power analysis

确定随机分配和样本大小/功效

一旦您建立并验证了实验的变革理论,下一步是确定您将如何进行随机分配以及需要多大的样本量。

根据我的经验,在特定环境中首次运行实验通常是一个重要的步骤。认真查看您的历史数据通常会带来令人惊讶的见解,这些见解可以重塑实验。此外,根据数据中的噪声和预期的影响大小(如果您正确地定义了实验的狭窄范围,则影响大小可能较小),发现您需要多大的样本量可能会让人感到谦卑。我仍然记得第一次得到数字后被告知我们需要进行一年的实验。

随机分配

随机分配的理论不可能更简单:每当客户到达相关页面时,他们应该以一定的概率看到当前版本的页面(在实验术语中称为“对照组”),并且以相反的概率看到带有一键预订按钮的版本(“处理组”)。

最直接的选择是 50%-50%的分配,直到达到目标样本大小,但如果您的交易量非常大,您可能希望使用不同的分割。比如,假设您管理一个每天有 1 亿次访问量的网站,并且确定您需要的样本大小为 200 万。您可以简单地选择 50%-50%,并在大约 30 分钟内完成实验。但是,如果您的处理出现了任何问题(例如,一个 bug 导致网站崩溃,虽然这是个极端案例),您将在意识到之前就有 100 万不满意的客户。此外,也许在这 30 分钟内的客户并不能代表您的全部客户群体(例如,中国此时大部分用户正在睡觉,而您主要得到的是美国访问者或者反过来)。在这种情况下,最好在一个更具代表性的时间段内获得您想要的 100 万次访问,比如一周或一个月(您不必担心对照组大于 100 万)。对于每天 1 亿次访问的网站来说,这将分别对应 99.86%-0.14%(因为 1 / (7 * 100) = 0.14%)和 99.97%-0.03%(因为 1 / (30 * 100) = 0.03%)。为了简单起见,我将在本章的其余部分假设 50%-50%的分割。

代码实现

从编码的角度来看,假设您没有使用能够为您处理此事的软件,这可以很容易地在 R 或 Python 中实现:

  1. 每当新客户到达相关页面时,我们生成一个介于 0 和 1 之间的随机数。

  2. 然后我们根据该随机数为客户分配一个组:如果 K 是我们想要的组数(包括对照组),那么所有随机数小于 1/K 的个体被分配到第一组;所有随机数介于 1/K 和 2/K 之间的个体被分配到第二组,依此类推。

在这里,K 等于 2,这转化为一个非常简单的公式:

## R
> K <- 2
> assgnt = runif(1,0,1)
> group = ifelse(assgnt <= 1/K, “control”, “treatment”)
## Python
K = 2
assgnt = np.random.uniform(0,1,1)
group = "control" if assgnt <= 1/K else "treatment"

随机分配的缺陷

然而,对于初学者来说,有一些微妙之处可能会导致问题。在本章中,我将涵盖两个问题:时间和分配的层次。

随机分配时间

第一个问题是确定在过程中随机分配的合适时机。假设每当客户进入网站的第一页时,您会将他们分配到控制组或处理组中的一个。这些客户中许多人将永远不会达到预订的阶段,因此不会看到您的预订界面。这将极大地降低实验的有效性,因为您实际上只对样本的一部分进行实验。

在确定哪些客户应该参与实验以及何时将他们分配到实验组时,您应该考虑如果实验成功,如何实施治疗。您的实验设计应该包括与业务通常情况下实施时会看到治疗的相同人群,并且仅包括他们。例如,离开网站而未预订的访客仍不会看到按钮,而任何未来的促销或预订页面的更改将被构建为附加的“在按钮上方”,该按钮将始终存在。因此,非预订访客应该被排除在外,但促销客户应该被包括在内。

随机分配级别

第二个挑战是确保随机分配发生在“正确”的行为级别上。我将通过一个例子来解释这意味着什么。假设一位访客进入 AirCnC 网站并开始预订,但然后因为某种原因离开网站(他们断开连接,是吃饭时间等),并稍后回到网站。他们应该看到相同的预订页面吗?如果他们第一次提供了 1 点击预订,他们第二次还应该被提供吗?

这里的问题在于,实际上可能有多个级别是有意义的。您可以在单个网站访问、预订、无论需要多少次访问,或客户帐户的级别上分配控制或处理(如果一个家庭中的几个人使用同一个帐户,则可能是同一个人)。不幸的是,在这里没有硬性规定,正确的方法必须根据情况逐案确定,考虑您想要得出的结论以及永久实施的情况。

在许多情况下,最好在最接近人类的级别上进行分配:如果无法区分家庭中的人员,则在客户账户上进行分配,或者如果每个人都有一个子账户,则在个别客户上进行分配,例如 Netflix。人类有持久的记忆,并且同一人的交替选项可能会令人困惑。在这里,这意味着我们的 AirCnC 客户应该在整个实验期间看到 1-click 按钮,而不管他们在此期间进行了多少次访问和预订。不幸的是,这意味着您不能仅仅在每次有人开始在网站上预订时象征性地掷骰子来确定他们的分配;您需要跟踪他们以前是否被分配过,并且如何分配。对于网站实验,可以通过 cookie 来实现这一点(假设客户允许使用 cookie!)。

注意

您进行随机分配的级别应该与您计算样本量的级别相同。如果您在客户级别进行分配,并且客户平均每月访问三次,那么您将需要一个比如果在访问级别进行分配长三倍的实验时间。但您选择进行随机分配的级别应该决定您的样本量,而不是反过来!

无论您选择哪个级别,您都必须跟踪分配任务,以便稍后将其与业务结果联系起来。这就是为什么最佳实践是使用集中系统记录所有任务,并将其连接到数据库中的客户 ID,以便随时为客户提供一致的体验。

更广泛地说,这两个挑战所指向的是,业务实验的实施几乎总是一项复杂的技术工作。现在有各种供应商提供有点即插即用的解决方案,可以在幕后隐藏复杂性,尤其是对于网站实验。无论您依赖它们还是依赖您的内部技术人员,您都需要了解他们如何进行随机分配,以确保您得到您想要的实验。

检查系统是否正常工作的好方法是从 A/A 测试开始,其中存在随机分配,但两组看到的页面版本相同。这将允许您检查两组中确实有相同数量的人,并且它们在任何重要方面都没有差异。

样本量和功效分析

一旦我们知道我们要测试什么以及如何测试,我们就需要确定我们的样本量。在某些情况下,例如我们的 1-click 预订实验,我们可以选择我们的样本量:我们可以决定实验要运行多长时间。在其他情况下,我们的样本量可能已经为我们定义好了,或者至少有其最大值。如果我们要在整个客户或员工人群中进行测试,我们不能仅仅为了实验而增加该人群!

无论我们处于什么样的情况,我们都将把样本大小与其他实验变量(如统计显著性)联系起来,而不是孤立地看待。了解这些变量如何相关至关重要,以确保我们的实验结果可用,并且我们从中得出正确的结论。不幸的是,这些是非常复杂和微妙的统计概念,并且它们并非为商业决策而开发。

符合本书精神,我将尽力在商业决策的背景下解释这些统计概念和惯例。然后,我将分享我对传统惯例的保留意见,并提出我对如何在传统框架内进行调整的建议。最后,我将描述一种曾慢慢获得动力的替代方法,我认为这种方法更为优越,即使用计算机模拟。

一点点不带数学的统计理论

当进行诸如“一键预订”按钮之类的实验时,我们的目标是做出正确的决策:我们应该实施它还是不实施?不幸的是,即使进行了实验(甚至进行了一百次),我们也无法百分之百确定我们是否做出了正确的决定,因为我们只有部分信息。当然,如果我们连续数年进行实验,可能会到达一个只有一百万分之一错误的机会的时候,但从来不会完全为零。此外,通常我们不希望连续数年进行实验,而是希望能进行其他实验!因此,实验的样本大小和我们的确定度之间存在权衡。

因为事后我们无法确定某个特定决策是正确的还是错误的,所以我们的方法是在实验之前尽量选择一个好的样本大小和一个好的决策规则。这里的“好”是什么意思呢?嗯,最理想的样本大小和规则应该是能够在长期内最大化我们的预期利润。相应的计算是可行的,但需要高级的方法,超出了本书的范围。^(3) 因此,我们将依赖以下措施:

  • 假设我们的一键预订按钮确实提高了我们的预订率,那么我们正确实施该按钮的概率是多少?这被称为“真正阳性”概率。另一方面,如果有积极效果,但我们错误地得出没有效果的结论,则称为“假阴性”。

  • 假设我们的一键预订按钮对我们的预订率没有明显影响(或者更糟糕的是,有负面影响!),那么我们错误地实施该按钮的概率是多少?这个概率称为“假正性”概率。另一方面,如果没有效果,并且我们正确地得出没有效果的结论,则称为“真负性”。

这些各种配置在 表 8-2 中总结。

表 8-2。在正确的情况下做出正确的决策

我们是否实施了 1-点击预订按钮?
1-点击预订按钮是否增加了预订率? 真正阳性
假阳性

我们希望我们的真正阳性和真负性率尽可能高,并且我们的假阳性和假阴性率尽可能低。然而,这个表格的简单性是具有误导性的,它实际上包含了无限多种情况:当我们说按钮增加了预订率时,这可能意味着增加了 1%,2%等等。另一方面,当我们说按钮不会增加预订率时,这可能意味着它完全没有效果,或者它以 1%,2%等速度降低预订率。所有这些效应大小都必须被考虑在内,以计算整体的真正阳性和真负性率,这将会变得太复杂。因此,我们将依赖于两个阈值。

第一个是所有科目的影响完全为零,也称为“尖锐零假设”(非尖锐零假设将是平均零效应跨科目)。对于这个值的假阳率称为我们实验的统计显著性。因为负面影响比零效应更容易捕捉,任何负值的假阳率至少将与统计显著性相同,并且较大的负效应将有更高的假阳率。学术研究中最常见的惯例是将统计显著性设置为 5%,尽管在某些领域如粒子物理学,有时可以低至 0.00005%。

第二个阈值设置在我们有兴趣测量的某些正效应上。例如,我们可能会说我们想选择一个样本大小,以便“合理确信”我们将捕捉到 1%的预订率增加,但是我们可以接受错过比这更小的效果。这个值通常称为“备择假设”,对于这个值的真正阳性率称为我们实验的统计功效。因为较大的效应更容易捕捉,任何更大值的真正阳性率至少会和统计功效一样大,并且更大的正效应将有更高的真正阳性率。“合理确信”传统上被认为是 80%。再次强调,这并不意味着你的实验“具有 80%的功效”,这个短语本身实际上是毫无意义的:对于某个更大的效应大小,实验也具有 90%的功效,对于某个更小的效应大小,实验具有 70%的功效,等等。

因此,根据传统惯例更新的我们的表格看起来像表 8-3。

表 8-3. 传统统计方法中使用的阈值

我们是否实施点击预订按钮?
点击预订按钮使预订率增加超过 1%。 > 80%(较大的效果大小)
点击预订按钮使预订率准确增加 1% 80%(统计功效)
点击预订按钮使预订率增加少于 1%。 < 80%(较大的效果大小)
点击预订按钮对预订率完全没有影响。 5%(统计显著性)
点击预订按钮严格降低预订率。 < 5%(较大的负面效果大小)

我并不喜欢仅仅因为它是传统的而使用任意数字,你应该随心所欲地调整“80%功效”的约定以适应你的需求。对于相关阈值效果大小使用 80%的功效意味着,如果干预确实具有那种效果大小,平均而言,你有 20%的机会不实施干预,因为你错误地得到了负面结果。对于难以测试的大型和昂贵的干预措施,我的观点是 80%的功效太低了,我个人会设定目标为 90%的功效。另一方面,你想要的功效越高,你需要的样本量就越大。如果在半年时间内,你的竞争对手已经完全两次改进了他们的网站并且在吞食你的午餐,那么你可能不想花费半年时间来确保 1 点击按钮的价值。

在我个人的经验中,对于实际世界中的功效分析和样本大小确定,一个关键但常被忽视的考虑因素是组织测试速度:你一年内可以进行多少实验?在许多公司中,这个数字受到某人的时间限制(分析师或业务伙伴的时间)、公司的计划周期、预算限制等的限制,而不是受到可用客户数量的限制。如果你真的希望每年只能计划、测试和实施一次干预措施,那么你真的想要进行为期三个月的实验,然后在其余的一年里什么都不做吗?另一方面,如果你可以每周进行一次实验,那么你真的想要花费三个月的时间确保一个正面但普通的影响,而不是有 12 次机会追求一个大的影响吗?因此,在进行数学计算之后,你应该始终对你的实验持续时间进行一次健全性检查,并相应地进行调整。

关于统计显著性,传统方法引入了控制组和处理组之间的不对称性,统计显著性阈值为 95%。处理组必须通过的证据标准要比控制组高得多,默认情况下是实施的。假设你正在设置一个新的营销电子邮件活动,有两个选项可以测试。为什么应该让一个版本比另一个版本受益?另一方面,如果你的活动已经运行了几年,并且你已经进行了数百次测试,当前版本可能非常出色,而 5%的错误放弃的机会可能太高;在这种情况下,正确的阈值可能是 99%,而不是 95%。更广泛地说,依赖于对所有实验都相同的传统值,对我来说感觉像是错失了反思假阳性和假阴性的成本。对于易于逆转且实施成本最小的一键按钮,我也会将统计显著性阈值定为 90%。

总结来说,从统计学的角度来看,我们的实验可以用四个值来概括:

  • 统计显著性,通常由希腊字母贝塔(β)表示

  • 选择为备择假设的效应大小,又称最小可检测效应

  • 统计功效,通常表示为 1 − α,其中α是所选备择效应大小的误差率

  • 我们实验的样本量,用 N 表示

这四个变量被称为 B.E.A.N.(贝塔、效应大小、阿尔法、样本量 N),为实验确定它们称为“功效分析”。^(4) 对于我们的一键按钮实验,我们已经决定了前三个变量,现在只需确定样本量。接下来我们将看看如何使用传统的统计公式和计算机模拟来完成。

传统的功效分析

统计学家已经开发出了一些公式来确定特定统计测试所需的样本量。考虑到我们将依赖回归而不是测试,你可能会想知道为什么我们要使用这些公式。根据我的经验,这些公式会给出与“真实”所需样本量相同数量级的值。如果你不知道你的样本量应该是 100 还是 100,000,这是获取模拟的合理起始值的一种快速简便的方式(在这个特定示例中,我们最终会得到几乎完全相同的样本量!)。

比例检验是一种标准测试,计算相应样本量的公式在 R 和 Python 中都很容易找到。让我们首先看看 R 中的公式。

根据我们历史数据中的平均预订率 18.25%,选择的 1%效应大小将导致我们处理组的预订率预期为 19.25%。对于参数的标准值——统计显著性=0.05 和功效=0.8——在 R 中相应的公式为:

## R
> effect_size <- ES.h(0.1925,0.1825)
> pwr.2p.test(h = effect_size, n = NULL, sig.level = 0.05, power = 0.8,
                      alternative = "greater")

     Difference of proportion power calculation for binomial distribution
                      (arcsine transformation) 

              h = 0.02562255
              n = 18834.47
      sig.level = 0.05
          power = 0.8
    alternative = greater

NOTE: same sample sizes

pwr包中所有功效分析函数的语法相同,只是效应大小的表示法在不同的公式中有所变化:

  • h是效应大小,基于我们希望观察到的比基线概率增加的概率。

  • n是每组的样本量。

  • sig.level是统计显著性水平。

  • power是统计功效,等于 1 − α。

在输入公式时,您应输入这些变量中的三个变量的值,并将其余一个设置为 NULL。在前述公式中,我们正在计算样本量,因此将n = NULL。

请注意,对于两个比例的检验,出于统计目的,效应大小取决于基线比率;相对于 10%或 90%的基线,从 50%的基线增加 5%更“重要”。幸运的是,pwr包提供了ES.h()函数,它将预期概率和基线概率转化为正确的效应大小公式。

还要注意公式末尾的参数:alternative表示您是否要运行单侧(greaterless)或双侧(two.sided)检验。只要我们的处理不会增加预订率,我们实际上不关心它是否与我们的对照有相同或更低的预订率;无论哪种方式,我们都不会实施它。这意味着我们可以通过设置alternative = 'greater'来运行单侧检验而不是双侧检验。

Python 的代码类似,使用statsmodels.stats.proportion包中的proportion_effectsize()函数:

## Python
effect_size = ssprop.proportion_effectsize(0.194, 0.184)
ssp.tt_ind_solve_power(effect_size = effect_size, 
                       alpha = 0.05, 
                       nobs1 = None, 
                       alternative = 'larger', 
                       power=0.8)
Out[1]: 18950.818821558503

公式返回的样本量是每组 18,800(加减一些小的 R 和 Python 之间的差异),即 37,600 总数,这意味着我们可以在不到四个月的时间内达到必要的样本量。太容易了!使用统计显著性为 0.1 和功效为 0.9 将产生每组 20,000 的样本量,稍长一点。

在我之前部分概述的决策模型中,对于统计显著性为 0.1 和功效为 0.9 的总样本量 40,000 意味着什么?想象一下以下情况:

  • 您运行了非常多的实验,总样本量为 40,000,如描述的那样。

  • 在每种情况下,您的决策规则是,如果比例检验的统计数据的 p 值低于 0.1,那么您将实施 1 点击按钮。

  • 在所有这些实验中,真实的效应大小为 1%。

那么你会发现一个显著的正结果,并在这些实验的 90%(即 0.9)中实施 1-点击按钮;在剩下的 10% 的实验中,你会得到一个空结果,并错误地拒绝实施 1-点击按钮。

对于回归,有一些等价的公式,但仅适用于最简单的情况,我发现即使在这些情况下,它们的复杂性远远超过了它们的实用性。尽管如此,作为我们模拟方法的概念步骤,让我们回顾一下传统的统计方法在回归决策模型方面会是什么样子。让我们在一些模拟数据上运行一个 logistic 回归:

## R (output not shown)
exp_null_data <- hist_data %>%
  slice_sample(n=20000) %>%
  mutate(oneclick = ifelse(runif(20000)>0.5,1,0)) %>%
  mutate(oneclick = factor(oneclick, levels=c(0,1)))
summary(glm(booked ~ oneclick + age + gender, 
                     data = exp_null_data, family = binomial(link = "logit")))
## Python 
exp_null_data_df = hist_data_df.copy().sample(2000)
exp_null_data_df['oneclick'] = np.where(np.random.uniform(0,1,2000)>0.5, 1, 0)
mod = smf.logit('booked ~ oneclick + age + gender', data = exp_null_data_df)
mod.fit(disp=0).summary()
...
               coef     std err    z      P>|z| [0.025    0.975]
Intercept      9.5764    0.621   15.412    0.000    8.359   10.794
gender[T.male] 0.1589    0.136    1.167    0.243    -0.108   0.426
oneclick       0.0496    0.136    0.365    0.715    -0.217   0.316
age           -0.3017    0.017  -17.434    0.000    -0.336  -0.268
...

传统的决策规则是,如果相应的系数(这里约为 0.0475)的 p 值小于 0.1,则认为 1-点击按钮的影响是显著的,并且实施它。由于在这个模拟数据中大约为 0.28,我们会认为效果不显著,并拒绝实施按钮(你的实际数字会随机变化,取决于你的模拟)。

根据这种方法确定我们分析的样本大小将包括确定样本大小,使得在大量的实验中有 90% 的实验中真实效果为 1% 的情况下,我们会得到回归系数的 p 值小于 0.1。但正如我在第七章中所描述的那样,这隐含地对我们的数据进行了统计假设,即数据服从正态分布,这可能会有问题,因此我们将使用 Bootstrap 模拟,正如我们现在将要看到的那样。

注意

对于比例检验的样本大小公式仍然可以作为一个快速的第一步,因为其结果应该与最终需要的样本大小的数量级相同。比例检验的总样本量为 40,000 意味着除非你的其他预测变量具有疯狂的高预测能力,否则你所需的样本大小的数量级将为 10,000,而不是 1,000 或 100,000(即,你的样本大小将有五位数)。我们将从一个暂定样本大小为 20,000 的样本开始我们的模拟,并根据它给我们的有效功率调整该数字向上或向下。

没有统计学的功率分析:自助法模拟

当数据有限且计算是通过手工费力完成时,传统的统计分析是完全合理的。我坚信,它现在已经失去了其实用性:自助法模拟提供了一个更好地反映应用数据分析现实和需求的替代方案。一个实验能有多么错误(例如,说治疗比对照好 1%,而实际上它比对照差 10%)通常比差异为零的可能性更令商业伙伴担忧。^(5)

连接模拟和统计理论

在使用 Bootstrap 模拟时,我们的决策规则不依赖于 p 值。相反,我们会在感兴趣系数的 Bootstrap 置信区间高于某个阈值时实施处理,通常是零。如果验证了统计功效分析的假设,Bootstrap 模拟会产生非常相似且直观相关的结果:

  • 在无效果的尖锐零假设下,我们期望 90% 的置信区间在 90% 的时间内包含零,80% 的置信区间在 80% 的时间内包含零,依此类推。这一属性被称为置信区间的覆盖率,意味着我们用于定义置信区间的百分比相当于统计显著性,即 90% 的置信区间在每个方向上都将具有约 5% 的误报率。在 5% 的情况下,我们会观察到严格为负的置信区间,在另外 5% 的情况下会观察到严格为正的置信区间。

  • 鉴于备择假设,即目标效应大小,我们可以将我们的功效定义为生成真阳性的模拟百分比。例如,如果我们将 1 次点击按钮的效应设定为 1%,模拟大量实验,并观察到我们的 Bootstrap 置信区间中有 75% 是严格为正的,则我们的功效为 75%。

注意

正如我们将在下一章中看到的那样,如果传统统计功效分析的假设验证,Bootstrap 置信区间的覆盖率可能会有所不同。也就是说,90% 的置信区间可能在 90% 的时间内包含零,也可能不包含。这种有效覆盖率代表了实际的假阳性风险,需要将其设置为所需的显著性水平。这只是一个提醒;我们将在 第九章 中详细讨论。

模拟提供了一种非常灵活但透明的方法来确定任何实验所需的样本量,无论数据多么奇怪或业务决策多么复杂。这些优势源于明确声明如何分析数据并编写相应代码,然后再实际运行实验之前,这提供了额外的检查和调整机会。这些好处的对应物是,我们将不得不自己编写更多的代码,而不是依赖现成的公式。我将尝试通过将其分解为直观函数来限制代码的复杂性。

编写我们的分析代码

让我们首先创建一个函数,该函数将输出我们感兴趣的指标,即我们逻辑回归中的 OneClick 系数:

## R
#Metric function
log_reg_fun <- function(dat){
  #Running logistic regression
  log_mod_exp <- glm(booked ~ oneclick + age + gender, 
                     data = dat, family = binomial(link = "logit"))
  summ <- summary(log_mod_exp)
  metric <- summ$coefficients['oneclick1', 'Estimate']
  return(metric)}
## Python
def log_reg_fun(dat_df):
    model = smf.logit('booked ~ oneclick + age + gender', data = dat_df)
    res = model.fit(disp=0)
    coeff = res.params['oneclick']
    return coeff

这只是对我们前面分析的功能性包装,将该函数应用于我们的模拟数据集将返回相同的系数,约为 0.0475。

然后我们计算该指标的 Bootstrap 置信区间,重复使用来自 第七章 的函数:

## R
boot_CI_fun <- function(dat, metric_fun){
  # Setting the number of bootstrap samples
  B <- 100

  boot_metric_fun <- function(dat, J){
    boot_dat <- dat[J,]
    return(metric_fun(boot_dat))}
  boot.out <- boot(data=dat, statistic=boot_metric_fun, R=B)
  confint <- boot.ci(boot.out, conf = 0.90, type = c('perc'))
  CI <- confint$percent[c(4,5)]
  return(CI)}
## Python
def boot_CI_fun(dat_df, metric_fun, B = 100, conf_level = 0.9):
  #Setting sample size
  N = len(dat_df)
  conf_level = conf_level
  coeffs = []

  for i in range(B):
      sim_data_df = dat_df.sample(n=N, replace = True)
      coeff = metric_fun(sim_data_df)
      coeffs.append(coeff)

  coeffs.sort()
  start_idx = round(B * (1 - conf_level) / 2)
  end_idx = - round(B * (1 - conf_level) / 2)
  confint = [coeffs[start_idx], coeffs[end_idx]]  
  return(confint)

同样,我们将采取的决策规则是,只有在 Bootstrap 90% 置信区间严格为正(即不包括零)时我们才会实施该按钮:

## R
decision_fun <- function(dat){
  boot_CI <- boot_CI_fun(dat, metric_fun)
  decision <- ifelse(boot_CI[1]>0,1,0)
  return(decision)}
## Python
def decision_fun(dat_df, metric_fun, B = 100, conf_level = 0.9):
    boot_CI = boot_CI_fun(dat_df, metric_fun, B = B, conf_level = conf_level)
    decision = 1 if boot_CI[0] > 0  else 0
    return decision

这相当于实施按钮的决策规则,只有在 p 值低于 0.10 的阈值时才会实施。你可以自己检查,将这个函数应用到我们的模拟数据集返回 0,正如它应该的那样。

对于给定效应大小和样本大小,我们实验的功效定义保持不变:它是在大量这样的实验中,我们会实施按钮的百分比。现在让我们转向模拟这些大量实验!

功效模拟

接下来,我们将编写我们的函数来运行单个模拟。代码工作如下(编号适用于 R 和 Python):

## R
> single_sim_fun <- function(dat, metric_fun, Nexp, eff_size, B = 100, 
                             conf.level = 0.9){

    #Adding predicted probability of booking ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)
    hist_mod <- glm(booked ~ age + gender + period, 
                    family = binomial(link = "logit"), data = dat)
    sim_data <- dat %>%
      mutate(pred_prob_bkg = hist_mod$fitted.values) %>%
      #Filtering down to desired sample size ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)
      slice_sample(n = Nexp) %>%
      #Random assignment of experimental groups ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)         
      mutate(oneclick = ifelse(runif(Nexp,0,1) <= 1/2, 0, 1)) %>%
      mutate(oneclick = factor(oneclick, levels=c(0,1))) %>%
      # Adding effect to treatment group ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)     
      mutate(pred_prob_bkg = ifelse(oneclick == 1, 
                                    pred_prob_bkg + eff_size, 
                                    pred_prob_bkg)) %>%
      mutate(booked = ifelse(pred_prob_bkg >= runif(Nexp,0,1),1, 0))

    #Calculate the decision (we want it to be 1) ![5](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/5.png) 
    decision <- decision_fun(sim_data, metric_fun, B = B, 
                           conf.level = conf.level)
return(decision)}
## Python 
def single_sim_fun(Nexp, dat_df, metric_fun, eff_size, B = 100,
                   conf_level = 0.9):

    #Adding predicted probability of booking      
    hist_model = smf.logit('booked ~ age + gender + period', data = dat_df)
    res = hist_model.fit(disp=0)
    sim_data_df = dat_df.copy()
    sim_data_df['pred_prob_bkg'] = res.predict()
    #Filtering down to desired sample size   
    sim_data_df = sim_data_df.sample(Nexp)
    #Random assignment of experimental groups             
    sim_data_df['oneclick'] = np.where(np.random.uniform(size=Nexp) <= 0.5, 0, 1)
    # Adding effect to treatment group                      
    sim_data_df['pred_prob_bkg'] = np.where(sim_data_df.oneclick == 1, 
                                            sim_data_df.pred_prob_bkg + eff_size, 
                                            sim_data_df.pred_prob_bkg)
    sim_data_df['booked'] = np.where(sim_data_df.pred_prob_bkg >= \
                                     np.random.uniform(size=Nexp), 1, 0)

    #Calculate the decision (we want it to be 1)                    
    decision = decision_fun(sim_data_df, metric_fun = metric_fun, B = B, 
                            conf_level = conf_level)
    return decision

1

将预测的预订概率添加到数据中。

2

过滤到所需的样本大小。

3

分配实验组。

4

将效果添加到治疗组。

5

应用决策函数并返回其输出。

现在我们可以编写我们的功效函数,针对某个效应大小和样本大小。该函数反复生成实验数据集,然后对它们应用我们的决策函数;它返回我们将实施按钮的实验数据集的分数:

## R
power_sim_fun <- function(dat, metric_fun, Nexp, eff_size, Nsim, 
                          B = 100, conf.level = 0.9){
  power_list <- vector(mode = "list", length = Nsim)
  for(i in 1:Nsim){
    power_list[[i]] <- single_sim_fun(dat, metric_fun, Nexp, eff_size, 
                                      B = B, conf.level = conf.level)}
  power <- mean(unlist(power_list))
  return(power)}
## Python
def power_sim_fun(dat_df, metric_fun, Nexp, eff_size, Nsim, B = 100, 
                  conf_level = 0.9):
    power_lst = []
    for i in range(Nsim):
        print("starting simulation number", i, "\n")
        power_lst.append(single_sim_fun(Nexp = Nexp, dat_df = dat_df, 
                                        metric_fun = metric_fun, 
                                        eff_size = eff_size, B = B, 
                                        conf_level = conf_level))
    power = np.mean(power_lst)
    return(power)

你应该模拟多少数据集?20 是一个很好的起点;它将给你一个嘈杂的估计,但如果你得到 0 或 1 的功效,你就知道你需要调整你的样本大小:

## Python (output not shown)
power_sim_fun(dat_df=hist_data_df, metric_fun = log_reg_fun, Nexp = int(4e4), 
              eff_size=0.01, Nsim=20)
## R
> set.seed(1234)
> power_sim_fun(dat=hist_data, effect_size=0.01, Nexp=4e4, Nsim=20)
[1] 0.9

这个初步估计是 90%的功效;正如我所说的,传统的公式给出了一个合理的起点来开始你的模拟。然后我运行功效模拟函数,分别对 30,000 和 50,000 行进行了 100 次模拟,最后对 35,000 和 45,000 行进行了 200 次模拟。基本上,随着你获得越来越窄的样本大小区间,你希望通过增加模拟次数来提高精度。图 8-6 显示了我的连续迭代的结果。

不同样本大小的功效模拟

图 8-6. 不同样本大小的功效模拟

正如之前宣布的,我们在大约 40,000 处得到了 0.9 的功效。如果需要更精确的结果(例如,我们是否应该得到一个 38,000 的样本大小?41,000 呢?),我们可以继续运行模拟,但对于这个示例来说已经足够了。

现在我们确定了样本大小,我喜欢做的最后一件事是绘制在该样本大小下几个效应大小的功效曲线。这使我们更好地了解,假设实际效应大小为正,我们整体上获得积极结果的可能性有多大。它还允许您更好地向您的业务伙伴传达,您的实验的功效不仅仅定义为一个效应大小。在这里,我们可以看到,从 0.5%的效应到 2%的效应,实验的功效增加了(图 8-7)。

在 N = 40,000 的各种效应大小下进行功效模拟,每个效应大小 200 次模拟,功效=0.9 的虚线

图 8-7. 在 N = 40,000 的各种效应大小下进行功效模拟,每个效应大小 200 次模拟,功效=0.9 的虚线

对于每个效应大小进行 200 次模拟,功效的估计值应该相当准确,尽管仍不完美,如曲线平滑度的缺失所示。换句话说,我们看到效应大小为 2%的功效为 1,并不意味着我们对该效应大小的功效确实是 100%,而是非常接近它。

注意

提醒:如果你的变量相对平滑且近似正态分布,Bootstrap 置信区间的模拟统计显著性应该与正常置信区间非常接近。对于更奇怪的数据(多峰、胖尾等),这种情况可能不再成立,你一定要检查你的模拟统计显著性是否有较大偏差。

如果你愿意,也可以运行一个效应大小为零的模拟。这将给你分析的经验统计显著性。因为我们使用的是 Bootstrap 90%-CI,大约 5%的模拟应该会得出(错误地)实施 1 点击按钮的决策,这就是我们在这里观察到的。

注意

这些绝不是大数据模拟,但它们足够耗时(最长一次在我的笔记本上花了大约半小时),你可能希望提高函数的性能,在做其他事情时让你的代码继续运行,或者两者兼顾。GitHub 上的代码包含我使用 R 中的RfastdoParallel包,以及 Python 中的joblibpsutil包优化的函数。

分析与解释实验结果

在运行实验并收集相应数据后,你可以进行分析。经过所有为了功效估计而进行的模拟分析,最终的分析应该是小菜一碟。我们进行逻辑回归并确定相应的 Bootstrap 90%-CI。由于随机分配,我们知道 1 点击按钮的系数是无偏的——我们不需要控制任何混杂变量。然而,通过添加其他也是预订概率原因的变量,我们可以减少噪声,显著提高估计的准确性:

## Python code (output not shown)
import statsmodels.formula.api as smf
model = smf.logit('booked ~ age + gender + oneclick', data = exp_data_df)
res = model.fit()
res.summary()
## R
> log_mod_exp <- glm(booked ~ oneclick + age + gender, 
                     data = exp_data, family = binomial(link = "logit"))
> summary(log_mod_exp)

...
Coefficients:
             Estimate Std. Error z value    Pr(>|z|)    
(Intercept)  11.94701    0.22601  52.861     < 2e-16 ***
oneclick1     0.15784    0.04702   3.357    0.000789 ***
age          -0.39406    0.00643 -61.282     < 2e-16 ***
genderfemale -0.25420    0.04905  -5.182 0.000000219 ***
...

1 点击按钮的系数为 0.15784,Bootstrap 90%-CI 大约为[0.073; 0.250]。根据我们的决策规则,我们将继续实施 1 点击按钮。

逻辑回归中的系数并不直接可解释,我发现只使用比率比的推荐解决方案在辅助效果时帮助甚微。我更倾向于采用一个经验法则,即生成两份实验数据副本,一份将 1-点击按钮的变量设为每个人都是 1,另一份将其设为 0。通过比较我们的逻辑模型对这两个数据集预测的预订概率,我们可以计算一个非常接近于如果将治疗方案应用于每个人时观察到的效应的“平均”效应。这种方法虽然不科学,但很有用:

## R (output not shown)
> diff_prob_fun <- function(dat, reg_model = log_mod_exp){
    no_button <- dat %>% ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)                                  
      mutate(oneclick = 0) %>%                                                      
      mutate(oneclick = factor(oneclick, levels=c(0, 1))) %>%
      select(age, gender, oneclick)
    button <- dat %>% ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)                                        
      mutate(oneclick = 1) %>% 
      mutate(oneclick = factor(oneclick, levels=c(0, 1))) %>%
      select(age, gender, oneclick)
    #Adding the predictions of the model 
    no_button <- no_button %>% ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)                           
      mutate(pred_mod = predict(object=reg_model, newdata = no_button, 
                                type="response"))
    button <- button %>%
      mutate(pred_mod = predict(object=reg_model, newdata = button, 
                                type="response"))
    #Calculating average difference in probabilities
    diff <- button$pred_mod - no_button$pred_mod ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)   
    return(mean(diff))}
> diff_prob_fun(exp_data, reg_model = log_mod_exp)
## Python
def diff_prob_fun(dat_df, reg_model = log_mod_exp):

    #Creating new copies of data
    no_button_df = dat_df.loc[:, 'age':'gender'] 
    no_button_df.loc[:, 'oneclick'] = 0
    button_df = dat_df.loc[:,'age':'gender']                        
    button_df.loc[:, 'oneclick'] = 1

    #Adding the predictions of the model 
    no_button_df.loc[:, 'pred_bkg_rate'] = res.predict(no_button_df) 
    button_df.loc[:, 'pred_bkg_rate'] = res.predict(button_df)

    diff = button_df.loc[:,'pred_bkg_rate'] \    
    - no_button_df.loc[:,'pred_bkg_rate']
    return diff.mean()
diff_prob_fun(exp_data_df, reg_model = log_mod_exp)
0.007129714313551981

1

我们创建了一个名为no_button的数据集,其中我们将变量oneclick在所有行上设置为零(并将其转换为因子,以便后来的预测函数能够工作)。

2

我们创建了一个名为button的数据集,其中我们将变量oneclick在所有行上设置为一。

3

我们通过使用我们的模型log_mod_exppredict()函数计算了每种情况下的预订预测概率。

4

我们计算了预测概率之间的差异。

我们可以看到,我们的实验人群中的平均效应约为 0.712pp,为正,但低于我们的 1pp 目标。像往常一样,让我们构建 Bootstrap 90%置信区间,大约为[0.705pp; 0.721pp]。这个区间非常窄,并且没有跨过零。因此,我们可以将我们的结果视为在 5%水平上在经验上具有统计显著性。在这种情况下,我们甚至可以更有信心:99.8%置信区间大约为[0.697pp; 0.728pp],距离零很远,因此我们可以将我们的结果视为在(1 − 0.998) / 2 = 0.1%水平上具有显著性。

为了涵盖所有情况,让我们回顾一下我们的决策规则(表 8-4)。这样你就能看到根据观察到的估计效应在统计上显著与否以及经济上显著与否(这里被认为是 1pp 增加),我们该如何做出决策。在当前情况下,我会实施这个按钮,因为它有严格的正效应,而且实施成本较低。

表 8-4. 1-点击预订按钮的决策规则

观察到的估计效应
估计效应 <= 0
观察到的结果的经验统计显著性 “高”(90% Bootstrap 置信区间未跨过 0) 不实施按钮
“低”(90% Bootstrap 置信区间跨过 0) 不实施按钮

最后要注意的是,我们实验人群的平均效应为 0.712pp,与我们的对照组和治疗组之间的简单差异相距甚远,大约为 0.337pp。这是由于我们两个实验组之间的随机差异。我们对照组的平均年龄为 40.63 岁,而治疗组为 40.78 岁。男性比例在治疗组也略高一些。在效应尺寸非常小的情况下,这些微小差异足以混淆直接比较两组:我们的样本量足够大,以至于我们的两组在绝对值上几乎相同,相差约 0.3pp,这在绝对意义上非常接近,但这大约是我们实验效应的一半。

不幸的是,在这个实验中,我们无法做任何关于这一点的调整,因为顾客在到来时是随机分配到两个组中的。但是,如果我们在实验开始时就知道整个实验样本,我们可以通过分层随机化来确保控制组和实验组尽可能相同,我们将在下一章中看到这一点。

结论

在本章中,我们看到了如何设计最简单形式的实验,即在线 A/B 测试和简单随机化。我强调了一个精心设计的实验远非仅仅是向客户推出网站或电子邮件的随机不同版本。您需要确定您的业务目标和目标指标,然后阐明您的干预如何通过行为逻辑与它们联系起来。总体而言,您的业务目标、目标指标、干预和行为逻辑共同构成了您实验的变革理论。

我们接着转向实验设计的定量方面。在这第一章关于实验中,随机分配非常简单,我花了更多时间在功效分析和样本大小计算上。虽然有统计公式可用,我更倾向于使用回归而不是统计检验作为分析工具,以及使用 Bootstrap 置信区间而不是 p 值作为我们估计系数周围不确定性的衡量标准,这导致使用功效模拟而不是公式。在这种情况下,两者的结果几乎相同,但在接下来的两章中,我们将进入更复杂的设计,其中没有可用的公式。

^(1) 这就是为什么制药试验以及越来越多的社会科学实验都是预注册的原因。您不能设定测试一个心脏病药物然后事后决定它对抗脱发有效,因为治疗组的患者发现他们的发际线向前移动;您需要进行第二个试验,本身也要预注册,以测试新的假设效应。

^(2) Wendel(2020)是了解行为障碍和驱动因素、建立强大行为逻辑的重要资源。

^(3) 如果你想知道,你需要使用贝叶斯方法。也许在本书的下一版中我会涉及这个话题!与此同时,Think Bayes(O’Reilly)由 Allen Downey 编写,是我所知道的关于这个主题最易于理解的入门之一。

^(4) 参见 Aberson(2019)。

^(5) Hubbard(2010)是一个很好的资源,如果你想更深入地思考如何在业务中设计有用的测量方法,即使信息非常有限。

第九章:分层随机化

在上一章中,我们看到了随机化的最简单形式:顾客来了,我们像掷一枚象征性的硬币或骰子一样随机决定。正面,他们看到版本 A;反面,他们看到版本 B。概率可能不是 50/50,但是是恒定的,并且独立于顾客特征。没有“我的对照组比我的处理组稍微年长一些,让我们确保下一个千禧一代的顾客进入对照组。”因此,你的对照组和处理组是“概率上等效”的,这是统计学上的说法,即如果你永远运行你的实验,你的两组将与你的总体人群具有完全相同的比例。然而,在实践中,你的实验组可能会有很大的差异。将解释变量添加到最终分析中可以在一定程度上弥补这些不平衡,但正如我们现在将看到的那样,如果我们事先知道谁将参与我们的实验,我们可以做得更好。

在本章中,我将向您介绍分层随机化,这将确保我们的实验组尽可能相似。这显著增强了实验的解释力,特别是在无法获得大样本量时尤为有用。

分层随机化可以应用于任何我们有预先确定的顾客/员工等名单来建立我们的实验组的情况。考虑到 A/B 测试通常涉及对电子邮件或网站进行轻微更改,我本可以选择电子邮件营销活动作为例子。但我想演示更大的业务举措,这些举措通常由公司高管基于他们的“战略感”做出,并且可以进行测试和验证。

这里的业务背景是,默认情况下,AirCnC 为业主提供至少 24 小时的时间来清洁他们的物业,以便安排两次预订之间的间隔。在需求旺盛的市场上,物业一经可订即订,这成为一个重要的限制因素。业务领导人急于减少这一时间并增加每个物业的月度利润。与往常一样,公司内存在两种不同的看法:

  • 财务部门主张为业主提供每次预订至少两晚的最短时长选择权。

  • 客户体验部门认为最短时长会对客户满意度产生不利影响;相反,它主张为业主提供免费的专业清洁公司服务,以换取将清洁窗口从 24 小时缩减到 8 小时。

在业务中经常出现这样的情况。双方都有一些有说服力的论点,涉及问题的不同方面或强调不同的指标(这里是预订利润与客户体验),并且/或者提供支持其立场的案例性证据(“这家其他公司做了 X,所以我们也应该这样做”)。常见的结果是,谁拥有最大的影响力,“赢了”,他们的解决方案被实施,即组织政治起作用。

到了这一步,你可能期望我说一些类似于“但实验可以让你绕过所有政治问题,达到最佳解决方案而不费吹灰之力”的话。我真希望事情能那么简单!事实是,实验在这种情况下可以帮助很大,但它并不是灵丹妙药,原因有两个。

第一个原因是,除非一个解决方案在所有方面都优于另一个,否则在竞争目标之间可能需要进行一些真正的权衡:公司愿意为了利润增加而承受多少客户满意度的降低?这个问题本质上是政治性的,因为公司内不同的利益相关者在这个问题上有不同的偏好。如果你希望你的实验成功,你将不得不尽可能清楚地提出这些权衡,并尽可能提前解决它们。

第二个原因是,你的实验最多会让一方感到满意(如果对照组取得了最佳结果,它也可能使两方都不满意!)。不满意的业务领导人,就像其他不满意的人类一样,在事后很擅长找到合理化的理由:“旧金山湾区与其他高需求市场不同”,“测量客户满意度的调查下降了,但净推荐分数上升了,而 NPS 是更好的衡量‘真实’客户满意度的指标”等等。

这两个原因使得适当地规划和运行你的实验变得更加重要,不仅从实验设计的角度,也从业务角度来看。

实验规划

正如我们在上一章看到的那样,成功规划实验要求我们清楚地阐述其变革理论:

  • 业务目标和目标指标是什么?

  • 我们的干预措施的定义是什么?

  • 这些如何通过我们的行为逻辑相连接?

你现在应该对这个过程很熟悉了,所以我不会详细阐述如何做,我只会快速地介绍一下步骤,让你更深入地理解实验。特别是,我将利用这个机会从行为学的角度指出实验的一些特殊之处。

业务目标和目标指标

这个实验的业务目标,或者说我们试图解决的业务问题,是通过减少高需求市场的停机时间来增加盈利能力。因为向业主提供免费清洁服务的成本很高(财务部门估计每天为$10),我们需要将其纳入我们的分析中。

我们将通过相应地修改我们的目标指标来实现这一点。我们的基本指标是每日平均预订利润;相反,我们将使用每日平均预订利润减去额外成本。这只是意味着我们需要从基本指标中减去$10,这是免费清洁服务的成本。

然而,也有一些担忧,即最短持续时间干预可能会对客户满意度产生负面影响。我们如何考虑这一点?

我看到其他作者提倡的一个解决方案是使用指标的加权平均值(有时称为总体评估标准[OEC])。在我们目前的例子中,这意味着为我们的两个变量分配权重,例如各 50%,然后使用该新指标作为我们的目标。如果您或您的业务伙伴希望,您当然可以自由使用这种方法,但是我建议选择一个独特的目标指标,如果必要,将几个其他“防护”指标列入监视列表,原因有几个。

第一个问题是,业务目标之间的权衡最终是战略性和政治性决策。关于增加利润值得多少 CSAT 点,没有客观上的最佳答案;这取决于组织、其背景和当前的优先事项。在某一时间确定的加权平均值使过程看起来具有技术客观性的外观,但实际上只是固化的主观性。

第二个问题是,一个 OEC 使这些权衡线性化。如果一个 CSAT 点等于$10 百万的利润,那么五个 CSAT 点等于$50 百万的利润。但是,CSAT 点数减少可能意味着更少的满意顾客,而减少五个点可能意味着社交媒体风暴。一美元永远等于另一美元,但对于几乎任何其他事情,一系列小变化通常比单个大变化更可取。盲目依赖 OEC 可能导致更冒险的决策。OEC 方法的支持者可能会反对,显然你不应该盲目依赖它;但如果你要看各种组成部分并且进行公开讨论,我不太确定 OEC 如何帮助。

此外,OEC 将业务干预视为固定。继续使用 1 点 CSAT = $10 百万的等价,以下两个选项将具有相等的评分为零:

  1. 第一个干预措施使利润增加了$1 百万,并使 CSAT 减少了 0.1 点。

  2. 第二个干预措施使利润增加了$50 百万,并使 CSAT 减少了 5 点。

但从行为角度来看存在着很大的差异。第一个选项基本上是一匹死马,几乎没有希望复活,而第二个选项更像是一匹善变的纯种马。通过将其针对特定的客户细分,改变其条款或呈现方式,可能会找到一种在不承担成本的情况下获得其一些好处的方式。在这种意义上,一个具有显著正负效果的干预措施需要进行探索和设计迭代,而不是 OEC 鼓励的二元决策。

最后,我认为在某些情况下,OEC 被用作捷径。假设某个干预措施增加了短期利润,但也增加了流失率的概率。这并不是一个真正的战略折衷:我们应该衡量流失率对生命周期价值的影响,然后确定对利润的净影响。说你的 OEC 将是 90%的短期利润和 10%的流失率效应是一种猜测而不是真正的测量交换率。有了本书的因果行为框架,我们可以比猜测做得更好。

因此,在本章的其余部分,我们将以每日平均预订利润作为我们的单一目标指标,并假设 CSAT 正在背景中监控任何令人担忧的变化。

注意

说得很好,但当我们谈论跟踪客户满意度时,我们指的是哪些客户?如果客户只想预订一个晚上的地点,并被提供一个有最短时长要求的地点,他们可能会决定预订其他地点(无论是在对照组还是免费清洁组),或者完全放弃通过 AirCnC 预订而选择预订酒店。因此,我们不能简单地衡量明确定义的客户群体的客户体验。

不幸的是,这个问题并不罕见:每当您进行一个实验,实验单位的随机分配不是客户时,您都必须问自己这将如何在客户端发挥作用。我们可以利用的是最短时长只会影响那些寻找一晚预订的客户;我们还知道客户在展示可用物业之前会输入他们的期望时长。因此,我们可以跟踪我们实验中所有寻找一晚期望时长的客户,并检查他们最终是在同一物业预订了几晚还是在其他物业预订了一晚,或者最终根本没有预订。每当他们预订时,我们还会跟踪他们如何评价他们的住宿。这显然远非完美,因为我们将为不同客户子群体跟踪不同的指标,但这是我们能做到的最好,也是我认为更多地依靠监测保护变量而不是在 OEC 中聚合它们的另一个例子。

干预定义

在确定成功标准之后,我们需要确保对我们正在测试的内容有清晰的理解。当组织的利益很高时,例如,业务领导人就此问题发生争执时,这尤为重要。在这里,需要指出的是,我们为业主提供设置最短租期的机会,这与强制规定最短租期并非同一概念。同样,业主可能会选择或不选择免费清洁服务。此外,处理方式本身仍可能存在不同解释的空间。免费清洁服务对公司来说究竟是多么彻底且成本高昂?我们强制客户遵守的最短租期是多少?

因为我们的两种干预措施都有些复杂,并且依赖于业主理解提议并选择接受,因此可能通过 UX 研究创建几种不同设计并进行质量测试是一个好主意。此外,在运行实验后,考虑略微不同版本的实施方式也是一个好主意。

最终,您将希望确保所有利益相关者都对实验设计感到满意,并愿意签署批准。这将减少(但不会完全消除!)当结果出来时,他们可能会辩称所测试的内容未能充分代表他们提出的解决方案的风险。

行为逻辑

对于两种处理方式,行为逻辑是不同的:最短租期方法可能会增加每次预订的持续时间和金额,但可能会减少总预订数量;另一方面,免费清洁方法可能会增加预订数量,但由于额外成本而降低每次预订的利润。此外,我们需要考虑到我们的实验处理/干预是提议,业主可以选择接受或不接受(图 9-1)。

考虑中的两种处理方式的 CD

图 9-1. 考虑中的两种处理方式的 CD

数据和包

这一章的GitHub 文件夹包含两个 CSV 文件,其中列出了表 9-1 中的变量。勾号(✓)表示该文件中存在的变量,而叉号(☓)表示该文件中不存在的变量。

表 9-1. 我们数据中的变量

变量描述 chap9-historical_data.csv chap9-experimental_data.csv
ID 物业/业主 ID,1-5000
sq_ft 物业面积,460-1120
tier 物业等级,分类,从 1 到 3,等级递减
avg_review 物业平均评分,0-10
BPday 每日预订利润,目标变量,0-126
Period 历史数据中的月份索引,1-35,实验数据中隐含的 36
Month 年份中的月份,1-12
实验分配,“ctrl”,“treat1”(免费清洁),“treat2”(最小预订时长)
符合 指示所有者是否按其分配的组进行处理的二元变量

在本章中,除了常用的包外,我们还将使用以下软件包:

## R
library(blockTools) # For function block()
library(caret) # For one-hot encoding function dummyVars()
library(scales) # For function rescale()
## Python
import random # For functions sample() and shuffle()
# To rescale numeric variables
from sklearn.preprocessing import MinMaxScaler
# To one-hot encode cat. variables
from sklearn.preprocessing import OneHotEncoder

确定随机分配和样本大小/功效

对于这个实验,我们将一次性根据某一时点所有者列表分配实验组。这使我们有机会通过称为分层的方法显著改善纯随机分配,从一开始就确保我们的两组平衡良好。这将使我们能够从任何给定样本大小中获得更多的统计功效。

因此,我将首先解释随机分配的方法,以便我们可以在功效分析的模拟中使用它。最后,我们将将这些模拟结果与传统的统计功效分析进行比较。

随机分配

在进入分层之前,让我们看看标准随机化会是什么样子。

随机分配级别

我们随机分配的首要考虑是我们将实施和测量实验结果的级别。在前一章中,我讨论了随机分配应该在客户级别还是预订级别进行的问题。在当前情况下,实验治疗的物流,特别是免费清洁的问题,不允许在预订级别进行实施。因此,我们将在物业所有者的级别上进行随机分配,这在 AirCnC 的数据中此时为 5,000 号。

标准随机化

这个过程类似于我们在上一个实验中使用的过程,但更简单,因为它可以离线完成而不是实时进行:首先,我们为实验人口中的每个个体分配一个介于 0 和 1 之间的随机数。然后,我们根据该随机数分配组别:如果 K 是我们想要的组的数量(包括一个控制组),那么所有随机数小于 1/K 的个体属于第一组,所有随机数介于 1/K 和 2/K 之间的个体属于第二组,依此类推。下面的代码展示了使用三组和样本大小(仅用于说明目的)为 5,000 的方法:

## R
no_strat_assgnt_fun <- function(dat, Nexp){
  K <- 3  
  dat <- dat %>%
    distinct(ID) %>%
    slice_sample(n=Nexp) %>%
    mutate(assgnt = runif(Nexp,0,1)) %>%
    mutate(group = case_when(
      assgnt < = 1/K ~ "ctrl",
      assgnt > 1/K & assgnt < = 2/K ~ "treat1",
      assgnt > 2/K ~ "treat2")) %>%
    mutate(group = as.factor(group)) %>%
    select(-assgnt)
  return(dat)
}
no_strat_assgnt <- no_strat_assgnt_fun(hist_data, Nexp = 5000)
## Python
def no_strat_assgnt_fun(dat_df, Nexp, K):
    dat_df = pd.DataFrame({'ID': dat_df.ID.unique()})
    dat_df = dat_df.sample(Nexp)
    dat_df['assgnt'] = np.random.uniform(0,1,Nexp)
    dat_df['group'] = 'ctrl'
    dat_df.loc[dat_df['assgnt'].between(0, 1/K, inclusive=True), 
               'group'] = 'treat1'
    dat_df.loc[dat_df['assgnt'].between(1/K, 2/K, inclusive=False), 
               'group'] = 'treat2'
    del(dat_df['assgnt'])
    return dat_df
no_strat_assgnt = no_strat_assgnt_fun(hist_data_df, Nexp = 5000, K = 3)

这种方法的一个优点是,通过创建一个简单的循环,可以轻松地将其推广到任意数量的组,控制组标记为0,第一个治疗组标记为1,依此类推:

## R
no_strat_assgnt_fun <- function(dat, Nexp, K){
  dat <- dat %>%
    distinct(ID) %>%
    slice_sample(n=Nexp) %>%
    mutate(assgnt = runif(Nexp,0,1)) %>%
    mutate(group = -1) # initializing the “group” variable
  for(i in seq(1,K)){
    dat$group = ifelse(dat$assgnt >= (i-1)/K & dat$assgnt < i/K,i-1,dat$group)} 
  dat <- dat %>%
    mutate(group = as.factor(group)) %>%
    select(-assgnt)
  return(dat)
}
no_strat_assgnt <- no_strat_assgnt_fun(hist_data, Nexp = 5000, K = 4)
## Python
def no_strat_assgnt_fun(dat_df, Nexp, K):
    dat_df = pd.DataFrame({'ID': dat_df.ID.unique()})
    dat_df = dat_df.sample(Nexp)
    dat_df['assgnt'] = np.random.uniform(0,1,Nexp)
    dat_df['group'] = -1 # initializing the “group” variable
    for i in range(K):
        dat_df.loc[dat_df['assgnt'].between(i/K, (i+1)/K, inclusive=True), 
               'group'] = i
    del(dat_df['assgnt'])
    return dat_df   
no_strat_assgnt = no_strat_assgnt_fun(hist_data_df, Nexp = 5000, K = 4)

然而,前一方法的一个问题是,实验组在客户特征上可能不会完全平衡。为了创建平衡的实验组,我们将使用一种称为分层的技术。

分层随机化

为什么纯随机分配不是我们最佳的选择?让我们想象一下,我们正在对 20 名客户进行实验,其中 10 名男性和 10 名女性。如果我们以 50%的概率将每个客户随机分配到对照组或治疗组,我们期望平均每组将有 5 名男性和 5 名女性。“平均”在这里意味着如果我们重复这个分配很多次,平均每组对照组中的男性数量将为 5。但在任何给定的实验中,我们只有 34.4%的机会在每组中确切地得到 5 名男性和 5 名女性,并且基于超几何分布,我们有 8.9%的机会得到 7 名或更多男性在一组中。很明显,随着样本量的增加,这个问题会变得不那么明显。对于 100 名男性和 100 名女性,得到 70 名或更多男性在一组中的概率变得微不足道。但我们不仅关心性别:理想情况下,我们还希望年龄、居住状态、使用模式等方面保持良好的平衡。这将确保我们的结果尽可能适用于我们整个客户群,而不仅仅适用于其中的特定子群。

幸运的是,当我们有幸同时将实验组分配给所有个体时,例如,我们可以比交叉手指并希望最好的做得更好。我们可以对数据进行分层:我们创建了类似客户的“层”,称为 strata,^(2)然后将它们分配到我们的实验组之间。对于我们的 10 名男性和 10 名女性客户,我们将创建一层男性,其中 5 名男性将进入对照组,5 名男性将进入治疗组,女性也是类似。这意味着每个个体仍然有 50%的机会进入任何一组,但我们的对照组和治疗组现在在性别上完全平衡了。

分层可以应用于任意数量的变量。以性别和居住状态为例,我们将创建一个所有来自堪萨斯州的女性的层,并将其平均分配到我们的对照组和治疗组,依此类推。对于大量变量或连续变量,找到完全匹配变得不可能;在我们的数据中,我们可能没有两个年龄相同、属性完全相同的堪萨斯州女性。解决方案是创建“尽可能相似”的个体对,例如,一个 58 岁的女性和一个 900 平方英尺的属性,以及一个 56 岁的女性和一个 930 平方英尺的属性,然后随机分配其中一个到对照组,另一个到治疗组。这样,他们仍然有相同的概率单独进入任何实验组。当每个 strata 只有两个个体时,这也被称为“匹配”,因为我们正在创建匹配的客户对。

像往常一样,直觉是足够清楚的,但实施的细节却是关键。这里有两个步骤:

  1. 给“尽可能相似”的短语赋予数学上的意义。

  2. 高效地浏览我们的数据,将每个客户分配给一对。

我们将用来表达“尽可能相似”的数学概念是距离。距离可以很容易地应用于单个数值变量。如果一个业主年龄为 56 岁,另一个业主年龄为 58 岁,则它们之间的距离为 58 − 56 = 2 年。类似地,我们可以说一个 900 平方英尺的房产和一个 930 平方英尺的房产之间的距离是 30 平方英尺。

第一个复杂之处在于聚合多个数值变量。我们可以简单地将两个数字相加(或者等效地取平均值),并说我们的两个业主之间的“距离”为 2 + 30 = 32 个距离单位。这种方法的问题在于,面积数字比年龄数字要大得多,就像我们在例子中看到的那样。两个业主之间 30 年的差异在行为上可能比他们属性之间 30 平方英尺的差异更重要。可以通过重新缩放所有数值变量,使它们的最小值重置为 0,最大值为 1 来解决这个问题。这意味着最年轻和最年长业主之间的“距离”为 1,最小和最大房产之间的距离也为 1。这并不是一个完美的解决方案,特别是在存在异常值时,但对于大多数目的而言,它是快速且足够好的。

第二个复杂之处来自分类变量。一个城市房和一个公寓之间的“距离”是多少?或者拥有游泳池与否的“距离”是多少?一个常见的解决方案是说,如果它们属于同一类别,则两个属性之间的距离为 0,否则为 1。例如,一个联排别墅和一个独立屋在属性类型变量上的距离为 1。数学上,这通过独热编码来实现:即,我们为每个类别创建尽可能多的二进制 0/1 变量。例如,我们将属性类型=("独立屋","联排别墅","公寓")转换为三个变量,type.housetype.townhousetype.apartment。一个公寓会对变量type.apartment有值 1,对其他两个变量有值 0。这还具有将分类“距离”与数值距离可比性的额外优势。实际上,我们在说联排别墅和公寓之间的差异与最小和最大房产之间的差异一样重要。从行为学的角度来看,这又是一个值得讨论的问题,但这是一个很好的起点,通常也是一个很好的结束点。

我编写了 R 和 Python 函数,通过重新缩放数值变量和对分类变量进行独热编码来准备我们的数据。这只是样板代码,如果你不关心实现细节,可以跳过这部分代码。

## Python code (output not shown)
def strat_prep_fun(dat_df):
    # Extracting property-level variables
    dat_df = dat_df.groupby(['ID']).agg(
        tier = ('tier', 'mean'),
        avg_review = ('avg_review', 'mean'),
        sq_ft = ('sq_ft', 'mean'),
        BPday = ('BPday', 'mean')).reset_index()
    dat_df['tier'] = pd.Categorical(dat_df.tier, categories=[3,2,1], 
                                    ordered = True)
    dat_df['ID'] = dat_df.ID.astype(str)
    num_df = dat_df.copy().loc[:,dat_df.dtypes=='float64'] #Numeric vars 
    cat_df = dat_df.copy().loc[:,dat_df.dtypes=='category'] #Categorical vars

    # Normalizing all numeric variables to [0,1]
    scaler = MinMaxScaler()
    scaler.fit(num_df)
    num_np = scaler.transform(num_df)

    # One-hot encoding all categorical variables
    enc = OneHotEncoder(handle_unknown='ignore')
    enc.fit(cat_df)
    cat_np = enc.transform(cat_df).toarray()

    #Binding arrays
    data_np = np.concatenate((num_np, cat_np), axis=1)
    del num_df, num_np, cat_df, cat_np, enc, scaler
    return data_np
prepped_data_np = strat_prep_fun(hist_data_df)
## R
> strat_prep_fun <- function(dat){
    # Extracting property-level variables
    dat <- dat %>%
      group_by(ID, tier) %>%
      summarise(sq_ft = mean(sq_ft),
                avg_review = mean(avg_review),
                BPday = mean(BPday)) %>%
      ungroup()

    # Isolating the different components of our data
    ID <- dat$ID  # Owner identifier
    dat <- dat %>% select(-ID)
    cat_vars <- dat %>%
      #Selecting categorical variables
      select_if(is.factor) 
    num_vars <- dat %>%
      #Selecting numeric variables
      select_if(function(x) is.numeric(x)|is.integer(x)) 

    #One-hot encoding categorical variables
    cat_vars_out <- data.frame(predict(dummyVars(" ~.", data=cat_vars), 
                                       newdata = cat_vars))

    # Normalizing numeric variables
    num_vars_out <- num_vars %>%
      mutate_all(rescale)

    # Putting the variables back together
    dat_out <- cbind(ID, num_vars_out, cat_vars_out)  %>%
      mutate(ID = as.character(ID)) %>%
      mutate_if(is.numeric, function(x) round(x, 4)) #Rounding for readability

    return(dat_out)}
> prepped_data <- strat_prep_fun(hist_data)
`summarise()` regrouping output by 'ID' (override with `.groups` argument)
> head(prepped_data, 5)
    ID  sq_ft avg_review  BPday tier.3 tier.2 tier.1
1    1 0.3321     0.3514 0.2365      1      0      0
2   10 0.3802     0.7191 0.5231      1      0      0
3  100 0.8370     0.6105 0.6603      0      0      1
4 1000 0.4476     0.4882 0.3843      1      0      0
5 1001 0.3323     0.7276 0.4316      0      1      0

一旦我们准备好数据,第二步就是创建配对。这个计算密集型问题在处理更大的数据时很快变得难以解决(至少是如果你想要最优解)。幸运的是,已经创建了算法可以为您处理这个问题。在 R 中,我们可以使用 blockTools 包中的 block() 函数:

## R
stratified_data <- block(prepped_data, id.vars = c("ID"), n.tr = 3, 
                         algorithm = "naiveGreedy", distance = "euclidean")

该函数的参数是:

id.vars

这是用于标识数据中个体的变量。

n.tr

这是实验组数量,包括对照组。

algorithm

表示要使用的算法的名称。可预见的是,"optimal" 将在整体上产生最佳配对,但在大数据和有限计算能力的情况下可能很快变得不可行;"naiveGreedy" 是计算需求最少且良好的起点。"optGreedy" 通常是在准备进行最终分配时的一个很好的折衷选择。

distance

表示个体之间距离如何计算。"euclidean" 是从高中学来的距离函数,适合我们准备的数据。

函数 block() 返回的层次分配格式冗长,因此我创建了一个便捷的包装器,将其输出转换为可用格式。欢迎在 GitHub 查看其代码:这里我只展示它的输出:

## R
> Nexp <- 4998 #Restricting our data to a multiple of 3
> stratified_data <- block_wrapper_fun(prepped_data, Nexp)
Warning message:
attributes are not identical across measure variables;
they will be dropped 
> head(stratified_data,3)
    ID  sq_ft avg_review  BPday tier.3 tier.2 tier.1  group
1  224 0.6932     0.8167 0.4964      1      0      0 treat1
2 3627 0.4143     0.9290 0.6084      1      0      0 treat1
3 4190 0.6686     0.5976 0.2820      1      0      0 treat1

请注意,5000 不能被 3 整除,因此我们需要随机丢弃两行,最接近且小于 5000 且能被 3 整除的数字是 4,998。比较两种随机分配方法将表明,通过分层随机化获得的实验组要比通过标准随机化获得的组更加相似。

除了帮助减少实验中的噪音外,分层还有助于如果您打算进行亚组或调节分析(我们稍后会在书中讨论)。正如一句话所说,“在你能控制的地方进行[分层],在你无法控制的地方进行随机化”(Gerber 和 Green,2012)。

分层随机化是一种有效且健壮的实验分配方法。其健壮性主要来自其透明性:您始终可以事后检查您的实验组在数值变量的均值和分类变量比例方面是否平衡良好。

另外,因为每对中的个体在任何实验组中的结束概率相同,即使使用不良或错误定义的距离函数,也不会比纯随机分配更差。主要风险在于包含太多无关紧要的变量,这些变量会淹没相关变量。然而,这可以通过仅包含作为因果图或主要人口统计变量的一部分的变量来轻松解决。不要仅仅因为可以而添加大量其他变量。

具有大量类别的分类变量有时也会由于其粗糙性而给您的分层增加噪音。以就业为例,数据科学家与统计学家不同,但直觉上,这种差异小于它们与消防员之间的联合差异。非常精细的变量忽略了这种细微差别,最好用更广泛的类别来替代。

免得这些警告使您丧失信心:分层是有效且稳健的;不要害怕使用它。即使仅基于一些关键人口统计变量进行分层也会显著改善,并应该是您的默认方法。

现在我们已经确定了随机分配的方法,让我们进行功效分析,确定样本大小。

使用 Bootstrap 模拟的功效分析

在与业务伙伴的讨论后,我们确定我们希望在净预订利润(BP)每日增加$2 时达到 90%的功率,因为这是他们感兴趣的最小可观察效果。这意味着对于免费清洁干预(治疗 1),“原始”每日 BP 将增加$12,而对于最短持续时间干预(治疗 2),将增加$2。这不会在任何实质性方面影响我们的分析,因为我们可以通过从免费清洁组物业的 BP/day 中减去$10 的成本来简单地转移结果变量,但我们需要记住并执行。为简单起见,我将只讨论最短持续时间干预在我们的功率分析中的情况。

在这种情况下,模拟方法真正发挥作用,因为通常没有专门的公式来计算功效或样本大小,或者现有的公式变得非常复杂。另一种选择是使用标准公式,这些公式忽略了实际情况的细节(例如,我们实验数据的分层),并且祈祷一切顺利(墨菲定律:可能不会)。

我们的过程与第八章中的相同:

  1. 首先,我们将定义我们的度量函数和决策函数。

  2. 然后我们将创建一个函数,模拟给定样本大小和效果大小的单个实验。

  3. 最后,我们将创建一个函数,模拟大量实验,并计算其中多少结果为真阳性(即,我们的决策函数充分捕捉到效果);真阳性的百分比即为该样本大小的功效。

单次模拟

我们最小持续时间治疗的度量函数如下:

## R
treat2_metric_fun <- function(dat){
  lin_model <- lm(BPday~sq_ft+tier+avg_review+group, data = dat)
  summ <- summary(lin_model)
  coeff <- summ$coefficients['grouptreat2', 'Estimate']
  return(coeff)}
## Python
 def treat2_metric_fun(dat_df):
    model = ols("BPday~sq_ft+tier+avg_review+group", data=dat_df)
    res = model.fit(disp=0)
    coeff = res.params['group[T.treat2]']
    return coeff

治疗 1 的度量函数将类似定义。

我们将从第八章中重用boot_CI_fun()decision_fun()函数。换句话说,我们的决策规则是,如果其 90%置信区间严格高于零,则实施治疗。我将在下文中重复它们的代码,仅作参考:

## R
> boot_CI_fun <- function(dat, metric_fun, B = 100, conf.level = 0.9){
    #Setting the number of bootstrap samples
    boot_metric_fun <- function(dat, J){
      boot_dat <- dat[J,]
      return(metric_fun(boot_dat))}
    boot.out <- boot(data=dat, statistic=boot_metric_fun, R=B)
    confint <- boot.ci(boot.out, conf = conf.level, type = c('perc'))
    CI <- confint$percent[c(4,5)]

    return(CI)}
> decision_fun <- function(dat, metric_fun){
    boot_CI <- boot_CI_fun(dat, metric_fun)
    decision <- ifelse(boot_CI[1]>0,1,0)
    return(decision)}
## Python
def boot_CI_fun(dat_df, metric_fun, B = 100, conf_level = 0.9):
  #Setting sample size
  N = len(dat_df)
  coeffs = []

  for i in range(B):
      sim_data_df = dat_df.sample(n=N, replace = True)
      coeff = metric_fun(sim_data_df)
      coeffs.append(coeff)

  coeffs.sort()
  start_idx = round(B * (1 - conf_level) / 2)
  end_idx = - round(B * (1 - conf_level) / 2)
  confint = [coeffs[start_idx], coeffs[end_idx]]  
  return(confint)

def decision_fun(dat_df, metric_fun, B = 100, conf_level = 0.9):
    boot_CI = boot_CI_fun(dat_df, metric_fun, B = B, conf_level = conf_level)
    decision = 1 if boot_CI[0] > 0  else 0
    return decision

然后,我们可以编写运行单个模拟的函数,其中嵌入了迄今为止我们所看到的逻辑:

## R
single_sim_fun <- function(dat, Nexp, eff_size){

  #Filter the data down to a random month ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)          
  per <- sample(1:35, size=1)
  dat <- dat %>%
    filter(period == per)

  #Prepare the stratified assignment for a random sample of desired size ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png) 
  stratified_assgnt <- dat %>%
    slice_sample(n=Nexp) %>%
    #Stratified assignment
    block_wrapper_fun() %>%
    #extract the ID and group assignment
    select(ID, group)

  sim_data <- dat %>%
    #Apply assignment to full data ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)                           
    inner_join(stratified_assgnt) %>%
    #Add target effect size
    mutate(BPday = ifelse(group == 'treat2', BPday + eff_size, BPday))

  #Calculate the decision (we want it to be 1) ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)    
  decision <- decision_fun(sim_data, treat2_metric_fun)
  return(decision)}
## Python
def single_sim_fun(dat_df, metric_fun, Nexp, eff_size, B = 100, 
                   conf_level = 0.9):

    #Filter the data down to a random month ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png) 
    per = random.sample(range(35), 1)[0] + 1
    dat_df = dat_df.loc[dat_df.period == per]
    dat_df = dat_df.sample(n=Nexp)

    #Prepare the stratified assignment for a random sample of desired size ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)
    assgnt = strat_assgnt_fun(dat_df, Nexp = Nexp)
    sim_data_df = dat_df.merge(assgnt, on='ID', how='inner')

    #Add target effect size ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)
    sim_data_df.BPday = np.where(sim_data_df.group == 'treat2', 
                                 sim_data_df.BPday + eff_size, sim_data_df.BPday)

    #Calculate the decision (we want it to be 1) ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)  
    decision = decision_fun(sim_data_df, metric_fun, B = B, 
                            conf_level = conf_level)
    return decision

1

随机选择一个月份,模仿我们将如何运行实际实验的方式(我们不希望在功效分析中使用相隔 10 年的数据)。

2

为所需大小的样本生成分层随机分配。

3

将分配应用于数据,并将目标效应大小应用于治疗组 2。

4

应用决策函数并返回其输出。

大规模模拟

从那里,我们可以像第八章中一样,对功效模拟应用相同的总体函数(在下面重复引用):

## R
power_sim_fun <- function(dat, Nexp, eff_size, Nsim){
  power_list <- vector(mode = "list", length = Nsim)
  for(i in 1:Nsim){
    power_list[[i]] <- single_sim_fun(dat, Nexp, eff_size)}
  power <- mean(unlist(power_list))
  return(power)}
## Python
def power_sim_fun(dat_df, metric_fun, Nexp, eff_size, Nsim, B = 100, 
                  conf_level = 0.9):
    power_lst = []
    for i in range(Nsim):
        power_lst.append(single_sim_fun(dat_df, metric_fun = metric_fun, 
                                        Nexp = Nexp, eff_size = eff_size, 
                                        B = B, conf_level = conf_level))
    power = np.mean(power_lst)
    return(power)

我们的最大样本量将是 5,000,因为这是 AirCnC 拥有的所有房主的总数。这对于模拟目的来说是一个可以管理的数量,所以让我们首先用这个样本量运行 100 次模拟。如果最终发现我们需要使用整个人口进行实验,那么逐步提高样本量就没有意义了。我们找到了一个功效为 1,这让人感到安慰:所需的样本量小于我们的总人口。从那里开始,我们尝试不同的样本量,随着我们逼近具有 0.90 功效的样本量,逐步增加模拟次数(图 9-2)。

随着模拟次数增加进行迭代的能力模拟,标签指示运行顺序

图 9-2. 迭代式功效模拟,随着模拟次数增加,标签指示运行顺序

看起来样本量为 1,500 就足够了。就像第八章中那样,现在让我们确定在该样本量下各种效应大小的功效曲线(图 9-3)。

检测各种效应大小和显著性的能力(样本量 = 1,500)

图 9-3. 检测各种效应大小和显著性的能力(样本量 = 1,500)

正如您在图 9-3 中所看到的,我们的功效曲线急剧下降,从效应大小为 $2 下降到 $1,效应大小小于 1 时我们的功效几乎为零;也就是说,如果我们假设治疗将 BPday 增加 $1,我们很可能得到一个包含零的置信区间,然后得出结论认为没有效应。在曲线的左端,我们的模拟显著性等于零,而不是预期的 5%。让我们讨论导致这种情况的原因以及我们是否应该担心。

理解功效和显著性的权衡

正如我在上一章中提到的,如果我们的数据“行为良好”(即,正态分布,纯粹随机分配到实验组等),并且没有真实效应,我们期望 90%-CI 90% 的时间包含零,严格高于它的时间 5%,严格低于它的时间 5%。在这里,由于分层随机化,我们的假阳性率似乎低于 5%:我在 500 次模拟中没有观察到任何假阳性。通过减少数据中的噪声,分层随机化还降低了假阳性的风险。

这很好,但在这种情况下可能有点过头了,因为它还将小正效应的功率曲线下降到 1,正如我们在图 9-3 中看到的那样。让我们换个说法:如果我们有 5% 的机会认为存在效应,而实际上没有,那么当存在一个小效应时,我们至少有 5% 的机会认为存在效应。从这个意义上说,显著性为我们提供了一些对于低效应尺寸的“免费”功率。

让我们将先前的功率曲线与较低置信水平的功率曲线进行比较。根据定义,这将给我们更窄的置信区间,意味着更高的显著性和更高的功效,特别是对于小效应尺寸(图 9-4)。

置信水平为 0.90(实线)、0.80(长虚线)、0.60(短虚线)和 0.40(点线)的功率曲线比较

图 9-4。置信水平为 0.90(实线)、0.80(长虚线)、0.60(短虚线)和 0.40(点线)的功率曲线比较

如您所见,使用 40%-CI,我们只会在显著性上获得小幅增长,但在功效上却会大幅增加,功效约为 50%,可以检测到效应尺寸为 0.5。

这是否意味着我们应该使用 40%-CI 而不是 90%-CI?这取决于情况。让我们回到业务问题上。我们的业务合作伙伴要求对于效应尺寸为 2 的功效达到 90%,因为如果效益低于这个值,他们不愿意费心实施任何一种治疗方法。因此,用不包含零的 CI 捕获真实的效应尺寸为 0.5,或者拥有包含零的 CI,从业务角度来看本质上是一样的。无论哪种情况,都不会实施任何治疗。因此,90%-CI 的功率曲线更能反映我们的业务目标。

另一方面,像第八章中的“1-点击按钮”这样的实验,实施的成本和风险是有限的。90% 功率阈值只是一个基准,几乎任何严格正效应都将得到实施。在这种情况下,为小效应尺寸增加功效可能值得略微增加显著性。

更广泛地说,当您的数据或实验设计偏离标准框架时,功效分析不再是将常规数字插入公式的简单事务,而需要理解正在发生的事情并就正确的决策进行判断。幸运的是,功效曲线提供了一个很好的工具,可以在不同情景和不同决策规则下可视化实验的可能结果。

分析和解释实验结果

一旦我们进行了实验,我们可以分析其结果。我们的目标指标——每天平均预订利润是连续的,而不是二元的;因此,两种适当的方法是平均数 T 检验和线性回归。如果您想了解更多关于 T 检验的内容,我会推荐您参考 Gerber 和 Green(2012),而线性回归我会进行介绍。

在进行定量分析之前,请记住,我们无法强制业主将最短停留期设置为两晚或同意减少清洁窗口的持续时间以换取免费清洁服务。我们只能提供他们选择的机会,有些人选择了,而其他人没有。在技术术语上,这种方法被称为鼓励设计,因为我们鼓励受试者接受我们的提议。

鼓励设计非常常见,但它引入了一些额外的考虑,因为现在我们在治疗组中有两类人:那些选择参与的人和那些没有选择的人。出于实际目的,这意味着我们可以尝试回答两个不同的问题:

  • 如果我们为整个所有者群体提供选择治疗的可能性会发生什么情况?

  • 如果我们强制整个业主群体接受治疗,而不给他们选择退出的选项,会发生什么?

第一个问题的答案被称为i**ntention-to-treat(ITT)估计,因为我们打算让人们接受治疗,但我们并不强制。第二个问题更为复杂,仅基于鼓励设计我们无法完全回答(或者至少需要额外的假设),但我们可以通过complier average causal effect(CACE)估计得到比 ITT 估计更接近的近似值。

让我们依次计算这两个估计值。

鼓励干预的意图治疗估计

让我们首先计算 ITT 估计,这将非常简单:它只是实验分配效应的系数,正如我们在前一章中计算的那样。我们是否应该考虑到治疗组中大多数业主并未选择参与的事实?不需要。ITT 系数被选择退出的人所稀释是一个特性,而不是错误:同样的稀释会在更大规模上发生。

让我们为选择参与清洁组的业主减少$10,以考虑额外成本,然后运行线性回归。我们可以分别应用我们的度量函数,但我更喜欢一次运行整个回归,以便能够看到其他系数:

## Python (output not shown)
exp_data_reg_df = exp_data_df.copy()
exp_data_reg_df.BPday = np.where((exp_data_reg_df.compliant == 1) & \
                                 (exp_data_reg_df.group == 'treat2'), 
                                 exp_data_reg_df.BPday -10, 
                                 exp_data_reg_df.BPday)
print(ols("BPday~sq_ft+tier+avg_review+group", 
          data=exp_data_reg_df).fit(disp=0).summary())
## R
> exp_data_reg <- exp_data %>%
    mutate(BPday = BPday - ifelse(group=="treat2" & compliant, 10,0))
> lin_model <- lm(BPday~sq_ft+tier+avg_review+group, data = exp_data_reg)
> summary(lin_model)

...
Coefficients:
             Estimate Std. Error t value        Pr(>|t|)    
(Intercept) 19.232831   3.573522   5.382 0.0000000854103 ***
sq_ft        0.006846   0.003726   1.838          0.0663 .  
tier2        1.059599   0.840598   1.261          0.2077    
tier1        5.170473   1.036066   4.990 0.0000006728868 ***
avg_review   1.692557   0.253566   6.675 0.0000000000347 ***
`grouptreat1`  `0.966938`   `0.888683`   `1.088`          `0.2767`
`grouptreat2` `-0.172594`   `0.888391`  `-0.194`          `0.8460`
...

我们对我们感兴趣的变量,即每天预订利润,进行回归,回归变量包括房产面积、城市等级、顾客评价的平均值以及实验组。Grouptreat1的系数指的是最低持续时间治疗,而Grouptreat2则指免费清洁治疗。

第一项处理平均使BPday增加了约$0.97,但 p 值相对较高,约为 0.27。这表明该系数可能与零没有显著差异,事实上,相应的 Bootstrap 90%-CI 约为[0.002; 2.66]。

如果您运行 T 检验比较第一处理组和对照组,您会发现检验统计量的绝对值为 0.96,接近我们刚刚进行的回归中的系数。同样,对照组和第一处理组之间BPday平均值的原始差异约为 0.85。这会让我们惊讶吗?不会。由于分层,我们的实验组非常平衡,因此其他独立变量在各组之间具有相同的平均效果。这意味着即使不考虑协变量的指标也是无偏的(但是它们的 p 值可能会偏离,因为它们不考虑分层)。

第二项处理在成本后使BPday减少了约$0.17,价值不高。相应的置信区间为[-2.23; 1.61]。

请记住,我们的商业伙伴只有在干预能够在成本之上每天产生额外$2 的BP时才感兴趣。从表面上看,这将排除实施最低持续时间干预,不仅因为统计显著性边缘,而且主要因为缺乏经济意义。即使置信区间的下限恰好高于零,这也不会改变我们商业伙伴的决定。

如果这必须是结局,我们的商业伙伴将不会实施任何鼓励措施。然而,考虑到如果我们在全面强制实施最低持续时间治疗而不让人们选择退出会发生什么可能是值得的。

强制干预的顺从者平均因果估计

当我们设计一种鼓励方案时,我们能否估计强制实施治疗的效果?试图回答这个问题的一种诱人但不正确的方法是,一方面比较选择参与类别(即“接受治疗者”)的业务指标的值,另一方面比较选择退出类别和对照组的值,将后两者合并为“未接受治疗者”。人们可能会假设这种比较反映了在全面实施治疗措施(例如在热门市场设定两晚住宿的最低要求,或单方面缩短清洁时间并提供免费清洁,而不考虑业主的偏好)后的预期结果。然而,事实并非如此,因为选择接受治疗并不是随机的,很可能会产生混杂因素。在接受治疗的群体内部,可能会有选择参与者在某些方面与选择不参与者不同,例如,他们可能有财务需求或其他特征,使他们对自己的财产更加关注和努力(Figure 9-5)。

实验分配是随机的,但接受免费清洁治疗并非如此

图 9-5. 实验分配是随机的,但接受免费清洁治疗并非如此

如果这种相关性是正确的话,那么接受免费清洁服务的人群可能会表现出增加每日预订利润的行为,并且会偏向于高估我们的系数。换句话说,如果我们将选择参与者与选择不参与者进行比较,我们可能会错误地将某些行为的效果归因于此优惠。随机分配确保实验组之间的比较是无偏的,但对于后续的亚组,并不能保证任何东西。

注意

在电子邮件 A/B 测试的情况下,随机化效果受限意味着各种指标(例如开启率、点击率等)的分子应都是实验组的人数,而不是前一阶段的人数。如果有 50%的人打开了您的电子邮件,并且这 50%中有 50%的人点击了电子邮件,则点击率应表示为 25%,而不是 50%。

在鼓励设计中,我们希望治疗组的人员选择加入并接受治疗,但我们也希望对控制组的人员不进行治疗。然而,在某些情况下,我们无法阻止他们接受治疗。在我们的例子中,免费清洁治疗具有完全在我们控制之下的特点:业主可以使用专业的清洁服务,但他们必须支付费用,并且软件中内置了预订之间的时间窗口。因此,没有治疗组之外的人员能够获得这种精确的治疗。然而,对于两晚最低住宿治疗,情况就不那么清晰了:控制组之外的物业业主可能会通过拒绝单晚预订请求来非正式地执行两晚最低住宿(图 9-6)。

实验分配是随机的,但接受最低预订治疗并非如此,并且可以发生在治疗组之外

图 9-6. 实验分配是随机的,但接受最低预订治疗并非如此,并且可以发生在治疗组之外

在最低预订治疗中,我们可以观察到业主存在四种可能的情况:

  1. 处于对照组且无最低住宿要求的情况下

  2. 处于对照组且无最低住宿要求的情况下

  3. 处于治疗组且有两晚最低住宿要求的情况下

  4. 处于治疗组且无最低住宿要求的情况下

这种分类尚未回答我们的问题,但它为我们区分治疗本身的效果与有利于设定两晚最低住宿的未观察因素的效果提供了一些重要的基础。假设我们可以观察这些因素,并按照这些因素的减少顺序对所有业主进行排名。由于随机分配,我们可以假设在未观察因素的分布方面,控制组和治疗组是相对一致的(图 9-7)。

对于控制组中所有价值不明因素足够高的业主,将实施两晚最低住宿(B 组),而控制组其他业主则不会(A 组)。对于治疗组,我们可以合理假设,具有高因素值的业主仍在实施两晚最低住宿,而我们的鼓励干预只是降低了门槛,使一些原本不会实施的业主参与其中(他们一起形成 C 组)。最后,尽管我们尽了最大努力,但那些因素过低的业主仍然没有实施(D 组)。

未观察因素的分布及两组中的观察行为

图 9-7. 未观察因素的分布及两组中的观察行为

在计量经济学术语中,无论其实验分组如何(控制组中的 B 组和治疗组中的对应部分 C 组),总是会接受治疗的受试者被称为始终接受者。永远不会接受治疗的受试者(治疗组中的 D 组和控制组中的对应部分 A 组)可预见地被称为从不接受者。只有在治疗组时才接受治疗的受试者(A 组和 C 组的重叠部分)被称为顺从者

理论上,你可以有第四类别,即只有在控制组中才接受治疗的受试者。他们被称为反抗者,因为他们总是做我们希望他们做的完全相反的事情。心理学中对此的技术术语是反应性。虽然在现实生活中可能会发生(咳嗽,青少年,咳嗽),但在商业环境中很少成为问题,除非你试图强迫人们做他们不想做的事情,我不会帮助你。

根据定义,我们无法观察到实验中未观察到的因素,这意味着我们只能确定两个群体:分配到控制组的始终接受者(B 组)和分配到治疗组的从不接受者(D 组)。我们不知道治疗组中实施两晚最低限制的业主是始终接受者还是顺从者,也不知道不实施的控制组业主是顺从者还是从不接受者。然而,这就是其中的诀窍所在,我们可以在实验组之间抵消始终接受者和从不接受者,以衡量对顺从者的治疗效果,这被称为顺从者平均因果效应(CACE)。CACE 的公式非常简单:^(4)

CACE=ITTP(treated|TG)P(treated|CG)

换句话说,要确定治疗对顺从者的影响,我们只需按照我们先前的 ITT 估计加权,方法是通过我们实验中的非顺从度来衡量:如果我们在两组中都有全面顺从,即在对照组中没有人接受治疗(P(treated|CG) = 0),在治疗组中每个人都接受治疗(P(treated|TG) = 1),这简化为 ITT 估计。在鼓励设计中,我们经常可以阻止控制组的人们接受治疗,但只有一小部分治疗组的人实际接受了治疗。在这种情况下,CACE 是 ITT 的倍数:如果治疗组只有 10% 的人接受治疗,那么我们的效果会被大大削弱,我们的 CACE 等于 10 倍的 ITT。

CACE 在两个方面非常有用:首先,它为我们提供了在全面实施治疗的情况下的效果估计,没有选择退出的可能性。其次,观察 ITT 和 CACE 之间的关系使我们能够区分两种可能的情况:

  • ITT 低,但 P(treated|TG) - P(treated|CG) 高,这意味着干预对顺从者的影响较低,但顺从度较高。

  • 相反,ITT 高,但 P(treated|TG) - P(treated|CG) 低,这意味着干预对顺从者有很大影响,但顺从度低。

在第一种情况下,我们将集中精力提高干预措施的效果,而在第二种情况下,我们将专注于提高接受率,可能通过强制性干预。这些见解也可以帮助我们探索替代设计:也许 8 小时太短了,但 12 小时会更可接受?也许我们不需要提供免费清洁,仅建议业主选择信誉良好的服务提供商就足够了?

在我们目前的实验中,治疗组的接受率平均相当低,大约为 20%:

## R (output not shown)
> exp_data_reg %>%
    group_by(group) %>%
    summarise(compliance_rate = mean(compliant))
## Python
exp_data_reg_df.groupby('group').agg(compliance_rate = ('compliant', 'mean'))
Out[15]: 
        compliance_rate
group                  
ctrl              1.000
treat1            0.238
treat2            0.166

这意味着我们的 CACE 估计比最小持续治疗的 ITT 估计高得多:

CACE[1] = ITT[1]/ComplianceRate[1] = 0.97/0.24 ≈ 4.06

现在,这是一个更有趣的数值。低接受率和高 CACE 表明,我们的干预在实施时基本上是有效的,并且确实产生了价值。我们可以尝试通过改变设计来提高接受率,或者将干预变为强制性。

CACE 的解释非常简洁但狭窄:因为我们(隐式地)比较了相同的人,也就是符合者,跨对照组和治疗组,我们对治疗效果的估计是无偏的。 我们没有无意中捕捉到其他因素的影响。 但是,我们仅为我们人口中的那个狭窄切片进行测量,因此一般性并不是一定的。 符合者可能具有与我们的治疗相互作用的特征。 也就是说,他们可能具有影响(或不仅仅是)是否接受治疗的特征,以及治疗对他们产生的影响有多大。 这就是我们需要从因果关系转向行为视角的地方:我们的治疗是一种提升所有人的潮流,还是参与的人很重要? 例如,在下一章中,我们将看到呼叫中心中的谈话路径的例子。 在这种情况下,符合不仅意味着应用治疗,还意味着努力说服并不仅仅是敷衍了事。

我们的干预是通过 AirCnC 的网站实施的。 无论您从谁那里租用,两晚最少就是两晚最少。 这意味着,如果在全面推广的情况下,我们可以确信我们的治疗将按计划实施,我们可以向业务伙伴发出放行信号。

结论

在前一章中,我们必须在客户连接到网站时“即兴”随机分配实验。 在本章中,我们能够一次性完成随机分配,因此通过创建一对相似的受试者,我们能够对样本进行分层(又称阻断),其中一个受试者被分配到对照组,另一个受试者被分配到治疗组。 尽管这增加了一层复杂性,但它也显著提高了实验的效果(在统计学上称为功效)。 一旦您熟悉了分层,您将会欣赏到即使从小样本中也能提取见解的能力。

我们还引入了第二个复杂性:我们的实验干预是一种鼓励性治疗。 我们为业主提供了可能性,但我们不能强迫他们接受,并且接受并不是随机的。 在这种情况下,我们可以轻松测量鼓励干预本身的效果,但是衡量接受提议的效果(也就是参与治疗)则更加棘手。 幸运的是,我们在实验人口中符合者的 CACE 为该效果提供了一个无偏的估计。 当我们可以假设个人特征与治疗之间没有相互作用时,CACE 可以推广到我们的整个实验人口。 即使我们无法推广到那么远,它也提供了比简单地比较对照组和治疗组(即意图治疗估计)更无偏的估计。

最后,我们进行了多种处理。这并没有从根本上改变任何事情,但也增加了一些复杂性。我建议您在实验旅程中只使用一种处理方式,但我相信长期来看,您会欣赏到运行实验的组织“固定成本”:需要得到所有利益相关者(业务合作伙伴、法律部门等)的批准,并建立技术和数据管道几乎与使用一种处理方式相比几乎没有更多时间。因此,一次性运行多种处理的实验是增加年度测试处理数的关键步骤。

^(1) 感谢 Andreas Kaltenbrunner 指出这是一个超几何分布,而不是二项分布。

^(2) 这是拉丁词层的复数形式,stratum,因此有层化一词。

^(3) 在技术上,有用于分层抽样的函数,如sklearn.utils.resample(),但这些函数不允许像我们这里做的基于距离的匹配。

^(4) 如果您对其来源感兴趣,可以在书籍的 GitHub 仓库中找到推导过程。

第十章:聚类随机化和分层建模

我们上次的实验虽然在概念上简单,但却说明了在商业实验中面临的一些后勤和统计困难。AirCnC 在全国各地有 10 个客户呼叫中心,代表处理预订过程中可能出现的任何问题(例如,付款未成功,房源与图片不符等)。在阅读了《哈佛商业评论》(HBR)关于客户服务的文章^(1)后,客户服务副总裁决定在标准操作程序(SOP)中实施变更:当出现问题时,呼叫中心代表不再重复道歉,而是在互动开始时道歉,然后进入“解决问题模式”,最后向客户提供几个选项。

这个实验面临多重挑战:由于后勤约束,我们只能在呼叫中心的层面上而非代表的层面上进行随机处理,并且在强制执行和测量遵从性方面存在困难。这当然不意味着我们不能或不应该进行实验!

关于随机化约束,我们将看到这使得标准线性回归算法不适用,因此我们应该改用分层线性建模(HLM)。

与以往一样,我们的方法将是:

  • 规划实验

  • 确定随机分配和样本大小/功率

  • 分析实验

规划实验

在本节中,我将快速概述我们的变革理论,以为您提供一些必要的背景和行为基础:

  1. 首先,商业目标和目标指标

  2. 接下来,我们干预的定义

  3. 最后,连接它们的行为逻辑

商业目标和目标指标

基于 HBR 文章,我们成功的标准或目标指标似乎很简单:客户满意度,由电话后通过电子邮件进行的一问调查来衡量。然而,我们很快会看到有复杂性存在,因此在讨论我们正在测试的内容后,我们需要重新审视它。

干预定义

我们正在测试的处理是代表是否接受了新的 SOP 培训并被指示实施。

第一个困难在于治疗的实施。我们根据过去的经验得知,要求代表对不同的客户应用不同的 SOP 非常具有挑战性:要求代表在电话之间随机切换流程会增加他们的认知负荷和不遵从的风险。因此,我们将不得不培训一些代表,并指示他们在所有电话中使用新的 SOP,同时保持其他代表使用旧的 SOP。

即使做出这种修正,遵从仍然面临风险:治疗组的代表可能不一致地实施新的 SOP,甚至根本不实施,而控制组的代表也可能不一致地应用旧的 SOP。显然,这将混淆我们的分析,并使治疗方法看起来与控制组的不同性不那么明显。减轻这个问题的一种方法是首先观察当前 SOP 的遵从情况,通过听取呼叫来运行试点研究,选择几个代表,对他们进行培训,并观察对新 SOP 的遵从情况。事后与试点研究中的代表进行总结讨论可以帮助识别误解和遵从的障碍。不幸的是,在人类进行治疗交付或选择的实验中,通常无法实现 100%的遵从。我们能做的最好努力是尽量测量遵从情况,并在得出结论时加以考虑。

最后,我们存在“泄漏”的风险,即我们的控制组和治疗组之间的“泄漏”。代表是人类,同一呼叫中心的代表之间会互动和交流。鉴于代表受到月平均客户满意度(CSAT)的激励,如果治疗组的代表开始看到显著更好的结果,那么同一呼叫中心的控制组的代表可能会开始改变他们的程序。让控制组的一些人应用治疗方法将混淆两组的比较,并使差异看起来比实际小。因此,我们将在呼叫中心级别应用治疗方法:同一呼叫中心的所有代表将分为治疗组或控制组。

应用在呼叫中心级别而不是在呼叫级别上的治疗对我们的成功标准有影响。如果我们的随机化单位是呼叫中心,那么我们应该在呼叫中心级别衡量 CSAT 吗?这看起来似乎是合理的,但这意味着我们不能使用任何关于个别代表或个别呼叫的信息。另一方面,衡量代表级别的平均 CSAT 甚至呼叫级别的 CSAT 将允许我们使用更多信息,但有两个问题:

  • 首先,如果我们忽略了随机化不是在呼叫级别进行的事实,并使用标准功效分析,我们的结果将存在偏差,因为随机化与呼叫中心变量不可避免地相关;增加样本中的更多呼叫不会改变我们只有 10 个呼叫中心,因此只有 10 个随机化单位的事实。

  • 其次,在我们的数据分析中,由于数据的嵌套结构,我们会遇到麻烦:假设每个代表只属于一个呼叫中心,我们的呼叫中心变量和代表变量之间会存在多重共线性(例如,我们可以为第一个呼叫中心的系数加 1,并在所有该呼叫中心的代表的系数中减去 1,而不改变回归的结果;因此回归的系数基本上是不确定的)。

幸运的是,这个问题有一个简单的解决方案:我们将使用分层模型,它识别我们数据的嵌套结构并适当处理,同时允许我们使用直到呼叫级别的解释变量。^(2) 对于我们的目的,我们不会深入统计细节,只会看如何运行相应的代码和解释结果。分层模型是一个通用框架,可应用于线性和逻辑回归,因此我们仍然处于已知领域内。

行为逻辑

最后,这次实验成功的逻辑很简单:新的 SOP 将使客户在互动过程中感觉更好,这将转化为更高的测量 CSAT(图 10-1)。

我们实验的因果逻辑

图 10-1. 我们实验的因果逻辑

数据和包

本章的 GitHub 文件夹包含两个 CSV 文件,列出了表格 10-1 中的变量。勾号(✓)表示该文件中存在的变量,而叉号(☓)表示不存在的变量。

表格 10-1. 我们数据中的变量

变量描述 chap10-historical_data.csv chap10-experimental_data.csv
Center_ID 10 个呼叫中心的分类变量
Rep_ID 193 名呼叫中心代表的分类变量
Age 客户呼叫时的年龄,20-60
Reason 呼叫原因,“支付”/“物业”
Call_CSAT 客户对通话的满意度,0-10
Group 实验分配,“对照组”/“处理组”

注意,这两个数据集还包含二元变量M6Spend,即在给定预订后六个月内用于后续预订的金额。这个变量仅在第十一章中使用。

在本章中,除了常见的包外,我们还将使用以下包:

## R
library(blockTools) # For function block()
library(caret) # For one-hot encoding function dummyVars()
library(scales) # For function rescale()
library(lme4) # For hierarchical modeling
library(lmerTest) # For additional diagnostics of hierarchical modeling
library(nbpMatching) # To use 'optimal' algorithm in stratified randomization
library(binaryLogic) # For function as.binary()
## Python
"# To rescale numeric variables
from sklearn.preprocessing import MinMaxScaler
# To one-hot encode cat. variables
from sklearn.preprocessing import OneHotEncoder"

分层建模介绍

当您的数据中包含分类变量时,可以使用分层模型(HMs):

  • 跨多个商店的客户交易

  • 跨多个州的租赁物业

  • 等等。

有些情况需要使用 HMs,因为你无法使用传统的分类变量。其中一个主要情况是,如果你有一个分类变量依赖于另一个分类变量(例如,素食者 = {“是”,“否”} 和 口味 = {“火腿”,“火鸡”,“豆腐”,“奶酪”}),即“嵌套”分类变量。然后,多重共线性问题使得使用 HMs 成为必要的方法。这也是为什么它们被称为“分层”模型,即使它们也可以应用于非嵌套的分类的原因之一。

除此之外,如果你有一个分类变量具有大量类别,比如我们示例中的呼叫中心代表 ID,特别是如果其中一些类别在你的数据中只有很少的行数,那么 HMs 也提供了一种更稳健的选择。不详细展开,这种稳健性来自 HMs 中系数的方式,它们包含了来自其他行的一些信息,使它们更接近总体平均值。让我们想象一下,在我们的数据中,一个呼叫中心代表只回答了一个呼叫,其客户满意度(CSAT)异常糟糕。由于该代表只有一个呼叫,我们无法确定该代表还是呼叫才是异常值。分类变量会将“异常性”100% 分配给代表,而 HM 会将其分配给代表和呼叫,即,我们期望该代表的 CSAT 与其他呼叫相比较低,但不像观察到的呼叫那样极端。

最后,在既可以应用分类变量又可以应用 HMs 的情况下(这基本上是任何你有几个非嵌套类别的分类变量的情况!),在解释上有一些细微差别可能会使你更喜欢其中之一。在概念上,分类变量是将数据分为具有内在差异的组,我们希望理解这些差异,而 HM 将组视为从潜在的无限组分布中随机抽取的样本。AirCnC 有 30 个呼叫中心,但它也可以有 10 个或 50 个,我们并不关心第 3 个呼叫中心和第 28 个呼叫中心之间的差异。另一方面,我们想知道与与物业问题相关的呼叫相比,与付款原因相关的呼叫的平均客户满意度是否更高或更低,我们不满意只知道组之间的标准差为 0.3。但再说一遍,这些是解释上的细微差别,所以不要想得太多。

R 代码

让我们简单地回顾一下分层建模的语法,在一个简单的情境下,通过查看我们的历史数据中呼叫客户满意度的决定因素,暂时不考虑 Rep_ID 变量。R 代码如下:

## R
> hlm_mod <- lmer(data=hist_data, call_CSAT ~ reason + age + (1|center_ID))
> summary(hlm_mod)
Linear mixed model fit by REML. t-tests use Satterthwaite's method
 ['lmerModLmerTest']
Formula: call_CSAT ~ reason + age + (1 | center_ID)
   Data: hist_data

REML criterion at convergence: 2052855

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.3238 -0.6627 -0.0272  0.6351  4.3114 

Random effects:
 Groups    Name        Variance Std.Dev.
 center_ID (Intercept) 1.406    1.186   
 Residual              1.122    1.059   
Number of obs: 695205, groups:  center_ID, 10

Fixed effects:
                   Estimate     Std. Error       df      t value   Pr(>|t|)    
(Intercept)       3.8990856    0.3749857      9.0938797   10.40 0.00000238 ***
reasonproperty    0.1994487    0.0026669 695193.0006122   74.79    < 2e-16 ***
age               0.0200043    0.0001132 695193.0008798  176.75    < 2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Correlation of Fixed Effects:
            (Intr) rsnprp
reasnprprty  0.000       
age         -0.011 -0.236

lmer() 函数的语法与传统的 lm() 函数类似,唯一的例外是我们需要在括号中输入聚类变量,这里是 center_ID,并在其前加上 1|。这允许我们的回归截距因呼叫中心而异。因此,我们有每个呼叫中心的一个系数;您可以将这些系数视为带有每个呼叫中心虚拟变量的标准线性回归中会获得的系数类似。

结果的“随机效应”部分涉及聚类变量。每个呼叫中心 ID 的系数未在摘要结果中显示(可以通过命令 coef(hlm_mod) 访问)。相反,我们得到数据在呼叫中心内部和呼叫中心之间变异性的度量,以方差和标准偏差的形式。在这里,呼叫中心之间的数据标准偏差为 1.185;换句话说,如果我们计算每个呼叫中心的 CSAT 均值,然后计算均值的标准偏差,我们将得到与您自己验证的相同值:

## R
> hist_data %>%
    group_by(center_ID)%>%
    summarize(call_CSAT = mean(call_CSAT)) %>%
    summarize(sd = sd(call_CSAT))
`summarise()` ungrouping output (override with `.groups` argument)
# A tibble: 1 x 1
     sd
  <dbl>
1  1.18

残差的标准偏差,这里为 1.059,表示在考虑呼叫中心效应后,我们的数据中剩余的变异性有多大。比较两个标准偏差,我们可以看到呼叫中心效应占数据变异性的一半以上。

结果的“固定效应”部分应该看起来很熟悉:它指示了呼叫级别变量的系数。在这里,我们可以看到,打电话因“财产”问题的客户的平均 CSAT 比打电话因“支付”问题的客户高 0.199,并且我们的客户每增加一岁,平均电话 CSAT 就增加 0.020。

接下来,将 rep_ID 变量作为 center_ID 变量下嵌套的聚类变量加入:

## R
> hlm_mod2 <- lmer(data=hist_data, 
                   call_CSAT ~ reason + age + (1|center_ID/rep_ID),
                   control = lmerControl(optimizer ="Nelder_Mead"))
> summary(hlm_mod2)
Linear mixed model fit by REML. t-tests use Satterthwaite's method
 ['lmerModLmerTest']
Formula: call_CSAT ~ reason + age + (1 | center_ID/rep_ID)
   Data: hist_data
Control: lmerControl(optimizer = "Nelder_Mead")

REML criterion at convergence: 1320850

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-5.0373 -0.6712 -0.0003  0.6708  4.6878 

Random effects:
 Groups           Name        Variance Std.Dev.
 rep_ID:center_ID (Intercept) 0.7696   0.8772  
 center_ID (Intercept) 1.3582   1.1654  
 Residual                     0.3904   0.6249  
Number of obs: 695205, groups:  rep_ID:center_ID, 193; center_ID, 10

Fixed effects:
                  Estimate   Std. Error            df t value   Pr(>|t|)    
(Intercept)     3.90099487   0.37397956      8.73974599   10.43 0.00000316 ***
reasonproperty  0.19952547   0.00157368 695010.05594912  126.79    < 2e-16 ***
age             0.01992162   0.00006678 695010.05053170  298.30    < 2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Correlation of Fixed Effects:
            (Intr) rsnprp
reasnprprty  0.000       
age         -0.007 -0.236

如您所见,通过在 center_ID 后添加 rep_ID 作为聚类变量,用 / 分隔它们来实现。还请注意,我收到一个警告,即模型未能收敛,因此我将优化算法更改为 "Nelder_Mead"。^(4) 固定效应的系数略有不同,但差别不大。

Python 代码

虽然更为简洁,Python 代码的工作方式类似。主要区别在于组别用 groups = hist_data_df["center_ID"] 表示:

## Python 
mixed = smf.mixedlm("call_CSAT ~ reason + age", data = hist_data_df, 
                   groups = hist_data_df["center_ID"])
print(mixed.fit().summary())
            Mixed Linear Model Regression Results
=============================================================
Model:              MixedLM Dependent Variable: call_CSAT    
No. Observations:   695205  Method:             REML         
No. Groups:         10      Scale:              1.1217       
Min. group size:    54203   Log-Likelihood:     -1026427.7247
Max. group size:    79250   Converged:          Yes          
Mean group size:    69520.5                                  
-------------------------------------------------------------
                   Coef. Std.Err.    z    P>|z| [0.025 0.975]
-------------------------------------------------------------
Intercept          3.899    0.335  11.641 0.000  3.243  4.556
reason[T.property] 0.199    0.003  74.786 0.000  0.194  0.205
age                0.020    0.000 176.747 0.000  0.020  0.020
Group Var          1.122    0.407                            
=============================================================

固定效应的系数(即截距、呼叫原因和年龄)与 R 代码中相同。随机效应方差的系数在固定效应的底部表达。在 1.122 处,与 R 值略有不同,这是由于算法的差异,但不会影响我们关心的系数。

在 Python 中,使用嵌套聚类变量的语法也有所不同。我们需要在单独的公式中表示较低级别、嵌套的变量(“方差分量公式”,我缩写为vcf):

## Python
vcf = {"rep_ID": "0+C(rep_ID)"}
mixed2 = smf.mixedlm("call_CSAT ~ reason + age", 
                   data = hist_data_df, 
                   groups = hist_data_df["center_ID"],
                   vc_formula=vcf)
print(mixed2.fit().summary())
            Mixed Linear Model Regression Results
=============================================================
Model:             MixedLM  Dependent Variable:  call_CSAT   
No. Observations:  695205   Method:              REML        
No. Groups:        10       Scale:               0.3904      
Min. group size:   54203    Log-Likelihood:      -660498.6462
Max. group size:   79250    Converged:           Yes         
Mean group size:   69520.5                                   
-------------------------------------------------------------
                   Coef. Std.Err.    z    P>|z| [0.025 0.975]
-------------------------------------------------------------
Intercept          3.874    0.099  38.992 0.000  3.679  4.069
reason[T.property] 0.200    0.002 126.789 0.000  0.196  0.203
age                0.020    0.000 298.301 0.000  0.020  0.020
rep_ID Var         1.904    0.303                            
=============================================================

方差分量公式的语法有些神秘,但直觉很简单。公式本身是一个字典,每个嵌套变量都是一个键。附加到每个键的值表示我们希望该变量具有随机截距或随机斜率(这里的随机意味着“按类别变化”)。随机截距是分类变量的 HM 等效,表示为"0+C(var)",其中var是嵌套变量的名称,即与键相同。随机斜率超出了本书的范围,但例如,如果您希望年龄与通话满意度之间的关系对每个代表都有不同的斜率,方差分量公式将是vcf = {"rep_ID": "0+C(rep_ID)", "age":"0+age"},第二种情况中不需要C()

确定随机分配和样本大小/功效

现在我们已经计划了实验的定性方面,我们需要确定我们将使用的随机分配以及我们的样本大小和功效。在我们之前的两个实验中(第八章 和 第九章),我们有一些目标效应大小和统计功效,并根据此选择了样本大小。在这里,我们将增加一个细节,假设我们的业务伙伴只愿意进行一个月的实验^(5),他们感兴趣捕捉的最小可检测效应是 0.6(即,他们希望确保你有足够的能力捕捉到这个大小的效应,但他们愿意承担效应大小可能较低的风险)。

在这些约束条件下,问题是:我们有多大的能力捕捉这个样本量的差异?换句话说,假设这个差异确实等于 0.6,我们的决策规则将得出治疗组确实比对照组更好的结论的概率是多少?

正如前文所述,我们将使用分层回归分析我们的数据,这将稍微复杂化我们的功效分析,但让我们首先简要回顾一下随机分配的过程。

随机分配

即使我们事先不知道哪些客户会打电话,但对于随机分配来说并不重要,因为我们将在呼叫中心层面进行。 因此,我们可以预先进行,一次性分配控制组和治疗组。 像这样的分组实验中,分层尤其有用,因为我们要随机化的实际单位很少。 在这里,我们是在呼叫中心级别进行随机化的,因此我们希望根据中心的特征进行分层,例如代表数和呼叫指标的平均值。 这样做的代码是第九章中的代码的简化版本,分为数据准备函数和用于阻塞函数的包装器(示例 10-1)。

示例 10-1. 呼叫中心分层随机分配
## R

# Function to prep the data
strat_prep_fun <- function(dat){
  # Extracting property-level variables
  dat <- dat %>%
    group_by(center_ID) %>%   ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)                                  
    summarise(nreps = n_distinct(rep_ID),
              avg_call_CSAT = mean(call_CSAT), 
              avg_age = mean(age),
              pct_reason_pmt = sum(reason == 'payment')/n()) %>%
    ungroup()

  #Isolating the different components of our data
  center_ID <- dat$center_ID  # Center identifier
  dat <- dat %>% select(-center_ID)
  num_vars <- dat %>%
    #Selecting numeric variables
    select_if(function(x) is.numeric(x)|is.integer(x)) 

  #Normalizing numeric variables ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)                                  
  num_vars_out <- num_vars %>%
    mutate_all(rescale)

  #Putting the variables back together
  dat_out <- cbind(center_ID, num_vars_out)  %>%
    mutate(center_ID = as.character(center_ID)) %>%
    mutate_if(is.numeric, function(x) round(x, 4)) #Rounding for readability
  return(dat_out)}

block_wrapper_fun <- function(dat){

  prepped_data <- strat_prep_fun(dat)

  #Getting stratified assignment
  assgt <- prepped_data %>% ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)                                  
    block(id.vars = c("center_ID"), n.tr = 2, 
          algorithm = "optimal", distance = "euclidean") %>%
    assignment() 
  assgt <- assgt$assg$`1` 
  assgt <- assgt %>%
    select(-'Distance')

  assgt <- as.matrix(assgt) %>% apply(2, function(x) as.integer(x))
  return(assgt)} ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png) 

1

我们按center_ID分组并总结我们的聚类变量:我们按中心计算代表人数,计算平均呼叫 CSAT 和客户年龄,并确定其原因是"payment"的呼叫的百分比。

2

我们将所有聚类变量重新缩放为 0 到 1 之间。

3

我们使用blockTools中的block()函数,使用nbpMatching包中的"optimal'算法(对于这么少的呼叫中心,我们可以承担额外的计算)。

4

我们从block()的输出中提取配对信息。

得到的配对信息是:

## R
     Treatment 1 Treatment 2
[1,]           2           3
[2,]           8           9
[3,]           7           6
[4,]           1           5
[5,]          10           4

如前一章所述,Python 中没有block包的等效物,因此我们将使用我在前一章中描述的两个函数来实现这个目的,进行轻微调整(例如,我们在中心层级没有分类变量,因此不需要对它们进行独热编码):

## Python
def strat_prep_fun(dat_df):
  ...

def stratified_assgnt_fun(dat_df, K = 2):
  ...

stratified_assgnt_df = stratified_assgnt_fun(hist_data_df, K=2)

功效分析

使用标准统计公式进行功效分析(在这种情况下,它将是 T 检验的公式)会非常误导,因为它不会考虑到数据中存在的相关性。 Gelman 和 Hill(2006)为层次模型提供了一些具体的统计公式,但我不想沉迷于积累越来越复杂和狭窄的公式。 像往常一样,我们将运行模拟作为我们的万全之策来进行功效分析。

让我们首先定义我们的度量函数:

## R
hlm_metric_fun <- function(dat){
  #Estimating treatment coefficient with hierarchical regression
  hlm_mod <- lmer(data=dat, 
                  call_CSAT ~ reason + age + group + (1|center_ID/rep_ID)
                  ,control = lmerControl(optimizer ="Nelder_Mead")
                  )
  metric <- fixef(hlm_mod)["grouptreat"]
  return(metric)}
## Python
def hlm_metric_fun(dat_df):
    vcf = {"rep_ID": "0+C(rep_ID)"}
    h_mod = smf.mixedlm("call_CSAT ~ reason + age + group", 
                    data = dat_df, 
                    groups = dat_df["center_ID"],
                    re_formula='1',
                    vc_formula=vcf)
    coeff = h_mod.fit().fe_params.values[2]
    return coeff

这个函数返回我们的分层模型中治疗组的系数。 就像我们在前几章中所做的那样,现在让我们为我们的功效分析运行模拟,希望你现在对此很熟悉。 这里唯一需要考虑的额外事项是我们的数据是分层的,即聚类的。 这有两个含义。

首先,我们不能随意从历史数据中随机抽取电话。在我们的实验中,我们预期代表们几乎每个人每次都会接到几乎相同数量的电话;而在真正的随机抽取中,每个代表接到电话数量的变化会显著。我们预期代表们每月会处理大约 1,200 通电话;在真正的随机抽取中,一个代表处理 1,000 通电话,另一个处理 1,400 通电话的情况比在实际中更有可能发生。幸运的是,从编程的角度来看,可以在做随机抽取之前将我们的历史数据按照呼叫中心和代表级别进行分组,这样可以很容易地解决这个问题:

## R
sample_data %<%- filter(dat, month==m) %>%dplyr::group_by(rep_ID) %>%
      slice_sample(n = Nexp) %>% dplyr::ungroup()
## Python
sample_data_df = sample_data_df.groupby('rep_ID').sample(n=Ncalls_rep)\
            .reset_index(drop = True)

当随机性“受限”时使用排列组合

第二个含义在统计水平上更为深刻。我们正在使用分层抽样来配对相似的呼叫中心,并将每一对中的一个分配给对照组,另一个分配给处理组。这很好,因为我们减少了某些呼叫中心特征会偏倚我们分析的风险。但与此同时,这在我们的模拟中引入了一个固定效应:比如说呼叫中心 1 和 5 因为非常相似而被配对在一起。无论我们运行多少次模拟,其中一个将在对照组,另一个将在处理组;我们减少了可能的组合总数。在完全自由的随机化下,有 10!/(5! * 5!) ≈ 252 种不同的方式将 10 个呼叫中心均匀地分配到实验组中,这已经不算多了。而使用分层抽样,只有 2⁵ ≈ 32 种不同的分配方式,因为每个五对中有两种可能的分配方式:(对照组,处理组)和(处理组,对照组)。这意味着即使你运行了 32,000 次模拟,你只会看到 32 种不同的随机分配方式。此外,只有三个月的历史数据,我们每个代表只能生成三个完全不同的(即互斥的)样本,总共有 32 * 3 = 96 种不同的模拟。

这并不意味着我们不应该使用分层抽样;相反,分层抽样在我们实验人口较小的情况下更为关键!然而,这确实意味着如果你运行的模拟比你真正有的不同分配要多得多,那么这样做可能毫无意义,甚至可能具有误导性。

要理解其中的原因,让我们使用一个比喻:想象一个学生决定在考试前(例如 LSAT)扩展他们的词汇量。他们购买了一本学习词典,并计划每天随机查阅其中一个词的定义十次,直到累计一千次,以学习一千个单词。但问题在于:他们的词典里只有 96 个单词!这意味着无论学生多少次查阅随机单词,他们的词汇量都不能增加超过 96 个单词。多次阅读一个单词的定义确实有助于更好地理解和记忆它,但这与查看更多单词的定义是不同的。这也意味着随机查阅定义是一种非常低效的方法。简单地按顺序逐个查阅这 96 个单词要好得多。

这种逻辑同样适用于模拟:通常我们从历史数据中随机抽取来构建模拟实验数据集,而且我们(正确地)将多个模拟重复的概率视为可以忽略不计。在当前情况下,如果我们有一百个呼叫中心,每个中心有一千名代表和十年的数据,我们可以自信地进行数百甚至数千次实验模拟而不用担心。由于我们呼叫中心和代表人数有限,我们最好系统地研究有限的可能性。

让我们看看如何用代码实现这一点。我们有呼叫中心的配对(参见前一小节的 Figure 10-2),我们需要遍历这些配对的 32 种可能的排列组合。第一对由呼叫中心 #7 和 #2 组成,所以一半的模拟将使 #7 成为对照组, #2 成为治疗组,而另一半将使 #2 成为对照组, #7 成为治疗组,依此类推。因此,第一次模拟可能将呼叫中心(7, 9, 3, 10, 4)作为对照组,而第二次模拟将呼叫中心(2, 9, 3, 10, 4)作为对照组。

我们将使用一个技巧来帮助我们轻松地穿过排列。这并不是很复杂,但它依赖于二进制数的性质,这些性质并不直观,因此请准备好并忍耐。任何整数都可以用二进制基数表示为一串零和一。0 是 0,1 是 1,2 是 10,3 是 11,依此类推。这些可以左填充为零,以具有恒定数量的数字。我们希望数字的位数等于对数对的数量,在这里是 5。这意味着 0 是 00000,1 是 00001,2 是 00010,3 是 00011。我们可以用 5 位二进制数表示的最大整数是 31。请注意,这不是巧合,包括 0 作为 00000,我们可以用 5 位二进制数字表示 32 个不同的整数,而 32 就是我们要实现的排列数。因此,我们可以决定第一个模拟,我们将其称为“模拟 00000”,在图 10-2 中的对照组是(7, 9, 3, 10, 4)。从那里开始,每当模拟号码的二进制形式中与对中对应的数字是 1 时,我们就会在对照组和治疗组之间交换一对。例如,对于模拟 10000,我们将交换呼叫中心#7 和#2,得到控制组(2, 9, 3, 10, 4)。这里的魔法发生了:通过从 00000 到 11111,我们将看到所有可能的五对排列!

排列的代码

由于 Python 和 R 之间的索引差异(前者从 0 开始,后者从 1 开始),因此 Python 中的代码要简单一些,因此让我们从相应的代码片段开始:

## Python
for perm in range(Nperm):
    bin_str = f'{perm:0{Npairs}b}'   ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)         
    idx = np.array([[i for i in range(Npairs)], ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)                    
                    [int(d) for d in bin_str]]).T
    treat = [stratified_pairs[tuple(idx[i])] for i in range(Npairs)] ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)

    sim_data_df = sample_data_df.copy()
    sim_data_df['group'] = 'ctrl' ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)   
    sim_data_df.loc[(sim_data_df.center_ID.isin(treat)),'group']\
        = 'treat'

1

我们将排列计数器perm转换为二进制字符串。在 Python 中,有几种方法可以实现这一点。我在这里使用了 F-string。F-string 的语法是f'{exp}',其中表达式exp在格式化为字符串之前进行评估。在表达式内部,Npairs也位于大括号之间,因此它在传递给表达式之前首先进行评估;在第一次评估之后,exp等于perm:05b。冒号左边的第一个术语是要格式化的数字;冒号后面的字母表示要使用的格式,在这里是b表示二进制;紧挨着数字左边的数字表示要使用的总位数(这里是 5);最后,数字左边的任何字符都是用于填充的(这里是 0)。

2

我们将二进制字符串的数字与idx矩阵中的对数计数器匹配。因此,“00000”在 Python 中变为(0010203040)在转置后。

3

我们将idx的行作为索引传递,以指示每对中的哪个元素进入治疗组。也就是说,为了指示第一对的第一个元素应该进入治疗组,我们传递 [0, 0]。对于 00000,我们总是将每对的第一个元素放入治疗组。对于最后一个排列 11111,我们将每对的第二个元素放入治疗组,与 00000 的分配相反。以更复杂的例子来说,对于排列编号为 7 的排列,其二进制格式为 00111,在前两对中将第一个元素放入对照组,最后三对中将第二个元素放入对照组。

4

最后,我们更新了模拟实验数据集,根据其中心 ID 将每行分配到对照组或治疗组。

这个过程在 R 中基本相同,语法上有一些差异:

## R
permutation_gen_fun <- function(i, stratified_pairs){
  Npairs <- nrow(stratified_pairs)
  bin_str <- as.binary(i, n=Npairs) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)  
  idx <- matrix(c(1:Npairs, bin_str), nrow = Npairs)
  idx[,2] <- idx[,2] + 1  ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)                                        
  treat <- stratified_pairs[idx] ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)                           
  return(treat)}

1

perm转换为二进制格式在 R 中使用 as.binary() 函数完成,该函数以第一个参数为要转换的数字,第二个参数为我们想要的总位数(即对数,这里是 5)。

2

因为 R 中的索引从 1 开始而不是从 0 开始,我们需要将idx矩阵第二列的所有元素加 1。因此,对于第一个排列 00000,其中每对的第一个元素进入对照组,idx矩阵为 (1121314151)。对于排列 11111,第二列将由 2 组成;对于 00111,它将由 11222 组成。

3

我们将idx的行作为索引传递,以指示每对中的哪个元素进入治疗组。

permutation_gen_fun() 函数返回治疗组的中心 ID 列表,然后可以在随机分配函数中使用。

功率曲线

现在,我们解决了可能样本量有限的问题,可以继续进行功率分析。请记住,业务伙伴希望在一个月内完成实验,这意味着大约需要 230,000 通话的样本量。我们不再计算 CSAT 0.6 分的阈值所需的样本量和所需功率,而是采用给定的样本量,计算我们在这个阈值下的功率。

让我们首先看统计显著性。请记住,在上一章中,我们的估计器“不自信”:90% 的置信区间超过了 90% 的时间都包括零。即使使用 40% 的置信区间,也只有少量的误报。在这里,我们面临相反的问题:我们的估计器“过于自信”,因为 90% 的置信区间几乎从不包括零,实际上从未包括过:我们的覆盖率为零。图 10-2 展示了从最低到最高排名的 96 个置信区间。

90% 置信区间无效果

图 10-2. 90% 置信区间无效果

我们在 图 10-3 中看到的情况类似于我们在 第七章 中看到的情况,即数据非常有限导致图表中出现不连续性。在这里,随机误差永远不会完全对齐,导致置信区间永远不包括零。相反,我们有四个紧密聚集的置信区间群,尽管我们的置信区间分布在零周围是对称的(即,我们的估计器是无偏的),并且其中一半置信区间非常接近零。从实际的角度来看,这意味着如果我们进行实验,我们不应期望真实值包含在我们的置信区间内。

这并不意味着我们的实验注定要失败,而是我们不应信任我们的置信区间边界,而应依赖我们的决策规则。使用接受任何严格正值置信区间的默认决策规则,我们的显著性为 50%:因为一半的置信区间低于零,另一半高于零,在一半的情况下,我们会观察到一个负系数,并正确地得出结论,即治疗组不比对照组好。图 10-3 绘制了这种决策规则下不同效应大小的功效曲线。

决策阈值为 0 的不同效应大小的功效曲线

图 10-3. 决策阈值为 0 的不同效应大小的功效曲线

正如你所见,我们的功效非常快地达到了 75%,基本上就在刚刚低于零的置信区间群被移动到稍高于零的位置时。之后,我们的功效在一定范围内保持稳定,包括我们的阈值效应大小为 0.6,直到强烈负置信区间群依次移动到高于零的位置。然后,我们的功效接近于 100%,对于效应大小为 1 或更高的情况,我们几乎不太可能看到一个负置信区间。

我们可以向业务伙伴反馈,我们的置信区间不可靠,因此误报的风险很大,但误检的风险非常低。在当前情况下,我们可以通过设定更严格的决策规则,并且只在观察到效应大小为 0.25 或以上时实施干预来做得更好。图 10-4 展示了该决策规则的功效曲线。

决策阈值为 0.25 的不同效应大小的功效曲线

图 10-4. 决策阈值为 0.25 的不同效应大小的功效曲线

正如我们在图 10-4 中所见,通过增加我们的决策阈值,我们降低了功效曲线的左侧。这意味着在小效应大小时有更低的显著性(即较低的假阳性风险),但以牺牲较高的错误否定率(即更高的假阴性风险)为代价。然而,功效曲线的右侧大部分保持不变,这意味着我们对于检测 0.6 效应的能力仍然为 75%。

让我们回顾一下我们的功效分析告诉我们的内容。由于我们计划使用分层随机分配有限数量的有效实验单元(即呼叫中心),我们的实验具有严格的结构,限制了可能的结果。这使得单独的置信区间本身不可靠。然而,我们可以调整我们的决策规则以更高的阈值(即只有观察到 0.25 或更高的效应时,我们才会实施我们的干预)。通过这样做,我们可以在零效应大小的情况下减少假阳性风险,同时保持对目标效应大小的功效足够高。这仍然是一个功效不足的实验,但这是我们作为实验者能提供的最好方案,而我们的业务伙伴将不得不决定他们对这些机会的看法。

警告

注意我们决策阈值 0.25 和目标效应 0.6 之间的差异。根据定义,决策阈值的功效始终为 0.5,我们的目标是在 0.6 效应大小上获得尽可能多的功效。

分析实验结果

一旦我们完成实验,我们就可以收集和分析数据。在事先定义了度量函数的基础上,现在分析就像简单地将其应用于我们的实验数据,然后获取其值的 Bootstrap 90%置信区间一样:

## R (output not shown)
> coeff <- hlm_metric_fun(exp_data)
> print(coeff)
> hlm_CI <- boot_CI_fun(exp_data, hlm_metric_fun)
> print(hlm_CI)
## Python
coeff = hlm_metric_fun(exp_data_df)
print(coeff)
hlm_CI = boot_CI_fun(exp_data_df, hlm_metric_fun)
print(hlm_CI)
0.477903237163797
[0.47434045128179986, 0.4815858577196438]

我们的置信区间非常窄,且完全高于 0.25。根据我们的功效分析,真实效应大小不太可能落在该置信区间内,但它可能比该值更低或更高,因此我们预期的效应大小等于 0.48。因为这高于我们的决策阈值,所以我们会实施这项干预,尽管预期效应大小低于我们的目标。有趣的是,该置信区间远小于基于正态近似得到的那个置信区间(即系数±1.96 * 系数标准误),部分原因是分层随机化。

结论

这就是我们实验设计之旅的结束。在本书的最后部分,我们将看到一些高级工具,这些工具将允许我们深入分析实验数据,但是我们刚刚看到的呼叫中心实验已经是现实生活中实验复杂性的极限了。无法在最低层级进行随机化,并且有一个预先确定的实验运行时间是不愉快但并不罕见的情况。在办公室或商店而不是顾客或员工级别进行随机化是常见的,以避免后勤复杂性和实验组之间的“泄漏”。如果你希望从实验中得到有用的结果,那么利用仿真进行功效分析和分层随机分配几乎是不可避免的;希望现在你已经完全具备了这样做的能力。

设计和运行实验在我看来是行为科学中最有趣的部分之一。当一切顺利时,您可以清晰地衡量业务举措或行为科学干预的影响。但要使一切顺利本身就不是一件小事。流行媒体和商业供应商经常传达这样一种印象,即实验可以像“插入并播放,检查 5%显著性,然后完成!”那么简单,但这是误导的,我试图解决了几个由此产生的误解。

首先,统计显著性和功效经常被误解,这可能导致实验浪费和次优决策。我相信,在应用设置中,放弃 p 值而选择 Bootstrap 置信区间会导致更正确和更相关的结果和解释。

其次,将实验视为纯技术和数据分析问题比采用因果行为方法更容易但成果较少。使用因果图允许您更清楚地表达什么是成功,以及是什么让您相信您的治疗会成功。

在现场实施实验充满困难(请参阅 Bibliography 获取更多资源),不幸的是,每个实验都是不同的,因此我只能给您一些通用建议:

  • 运行现场实验是一门艺术和科学,没有什么可以取代对特定环境的经验。起初从更小更简单的实验开始。

  • 首先,在一个小的试点组上实施治疗,然后观察一段时间并进行广泛的反馈。这将使您尽可能地确保人们理解治疗并相对正确和一致地应用它。

  • 努力想象所有事情可能出错的方式,并防止它们发生。

  • 认识到事情可能出错,尽管如此,还是要在实验中增加灵活性(例如,计划“缓冲”时间,因为事情会比你想象中花费更长时间——人们可能需要一周时间来正确实施治疗,数据可能会延迟等)。

^(1) “‘对不起’ 不足以”,《哈佛商业评论》,2018 年 1 月至 2 月。

^(2) 如果你想了解更多关于这类模型的信息,Gelman 和 Hill(2006)是该主题的经典参考文献。

^(3) 如果你真的想知道,这些系数是呼叫中心平均客户满意度和整体数据平均客户满意度的加权平均值。

^(4) 对于数值模拟,你的情况可能有所不同。感谢 Jessica Jakubowski 建议另一种规格:lmerControl(optimizer ="bobyqa", optCtrl=list(maxfun=2e5))

^(5) 这对你的实验设计不利吗?当然。这不现实吗?非常不幸,并非如此。就像我在做顾问时常说的那样,客户始终是客户。

^(6) 感叹号表示数学运算符阶乘。如果你想更好地理解基础数学,请参阅此 Wikipedia 页面

第五部分:行为数据分析中的高级工具

这是书中的最后部分,一切将会结合在一起。我们将看到行为数据分析的三个强大工具,首先是第十一章中的中和,然后是调解及其衍生的工具变量(IVs)在第十二章中。

中和(Moderation)是一种多功能的数学工具,它使我们能够理解相互作用效应,以及有效透明地划分我们的客户群体。调解(Mediation)使我们能够窥探因果关系的黑箱,并理解一个变量如何影响另一个变量。最后,我将使用工具变量来兑现我关于测量客户满意度对后续客户行为影响的承诺。

中和、调解和工具变量是复杂的主题,也是生动的方法论争论和整本书的主题。但是,我们在本书前面介绍的工具将大大简化这一旅程。首先,使用因果图将为这三种工具提供直观解释。其次,使用自举法(Bootstrap)将使我们能够直接构建置信区间,并完全避开与 p 值相关的方法学复杂性。因此,我们将能够用一行代码绘制深刻而可操作的行为洞察。

第十一章:温和性介绍

将因果和行为视角结合的最令人满意的方面之一是,一个在一种视角下看起来完全无关的事物在另一种视角下可能是完全相同的事物。简单来说,当你有合适的工具时,很多事情确实都是钉子

到目前为止,我们已经使用因果图来理解平均情况下行为的驱动因素:如果温度升高一度,保持所有相关变量恒定,C-Mart 冰淇淋的销量会增加多少?但很多时候,我们不仅仅对那个总体平均感兴趣,我们希望进一步细分:

  • 这个数字对德克萨斯州和威斯康星州的摊位适用吗?如果不是,这意味着我们的数据显示了分段的机会。

  • 这个数字对巧克力和香草冰淇淋适用吗?如果不是,这意味着温度和冰淇淋口味之间存在交互作用

  • 这个数字对低温和高温一样适用吗?如果不是,这意味着温度对销售的影响存在非线性

我们在本章将看到的工具被社会科学家称为温和性分析,它将允许我们以完全相同的方式处理这三种类型的问题。

在查看本章数据和软件包后的第一节,我们将进行温和性的介绍,并看看它如何适用于各种行为情况。因为数学在所有情况下保持不变,我已经收集了所有实际和技术考虑事项,将在最后一节进行审查。

数据和软件包

本章的GitHub 文件夹包含 CSV 文件chap11-historical_data.csv,其中列出了表 11-1 中的变量。

表 11-1. 我们数据中的变量

变量名称 变量描述
天索引,1-20
商店 商店索引,1-50
儿童 二进制 0/1,顾客是否带有幼儿
年龄 客户年龄,20-80
访问持续时间 商店访问持续时间,单位为分钟,3-103
游乐区 二进制 0/1,店铺级别,店内是否设有游乐区
杂货购买 每次访问中在杂货购买上的花费,单位为美元,0-324

在本章中,我们只会使用通用软件包,没有特定于章节的软件包。

温和性的行为类型

温和的正式定义非常简单:它是回归中两个预测变量的乘积。例如,我先前建议过冰淇淋的销售在德克萨斯州和威斯康星州可能会因温度每升高一度而有所增加或减少;这在数学上可以表达如下:

冰淇淋销售 = β[t].温度 + β[s].状态 + β[ts].(温度 * 状态)

温和性可用于理解以下所有行为现象,我们将依次审查:

  • 分段

  • 互动

  • 非线性(即自我调节)

分割

建立相关的客户分段是市场分析的关键任务,也更广泛地是业务分析。我们将讨论如何在观察数据和实验数据中进行这项工作。

观察数据的分割

我们的起点将是 C-Mart 的例子:该公司最近在部分商店引入了游乐区,并且有兴趣了解它如何影响顾客的访问持续时间。由因果图支持的回归分析给出了一个平均因果效应:在我们的数据中所有顾客访问中,商店内有游乐区对访问持续时间的影响是什么?然而,平均值可能具有误导性,并且会隐藏我们人群各个分段之间的大差异。例如,可以合理假设对于有儿童的顾客,游乐区的存在更可能影响访问持续时间。在我们的回归分析中如何考虑这一点呢?您可能认为简单地将 Children 作为 VisitDuration 的另一个预测因素包含进去就可以了,如图 11-1 所示。

包括 Children 作为 VisitDuration 的预测因素

图 11-1. 包括 Children 作为 VisitDuration 的预测因素

该方法的问题在于,它考虑了 ChildrenVisitDuration 的影响,无论是否有游乐区,反之亦然:在进行回归分析时,每个变量都是独立考虑的。每个系数都被设定为最小化总体残差距离,但系数必须对每个变量不考虑其他变量的值而言是相同的。这意味着,PlayAreaVisitDuration 的系数及因此所测得的效应,在数学上被强制成为其在顾客有和没有儿童时的效应的加权平均。同样地,顾客有儿童对效应的测量,也被看作是顾客有游乐区和没有游乐区时效应的加权平均。这可以通过查看相应的方程来确认。我们刚刚画出的 CD 方程显示在 Equation 11-1 中(通常为简单起见,我们省略了常数系数 β[0])。

方程 11-1.

VisitDuration = β[0] + β[p].PlayArea + β[c].Children

因为 PlayAreaChildren 都是二进制变量,所以根据它们是否等于 0 或 1,我们有四种可能的情况:

  1. β[0] 是在没有儿童且没有游乐区的商店中顾客的平均访问持续时间(我们简称为 C = 0, P = 0)。

  2. β[0] + β[c] 是在没有游乐区但有儿童的商店中顾客的平均访问持续时间(C = 1, P = 0)。

  3. β[0] + β[p] 是在有游乐区但没有儿童的商店中顾客的平均访问持续时间(C = 0, P = 1)。

  4. β[0] + β[p] + β[c] 是带有孩子顾客在带有游乐区的商店(C = 1,P = 1)的平均访问持续时间。

这意味着无论顾客是否携带孩子,添加游乐区对影响都没有差异,我们可以轻松验证:

如果Children = 0,添加游乐区的影响是:

VisitDuration(C = [0],P = 1) - VisitDuration(C = 0,P = 0) = (β[0] + β[p]) - (β[0]) = β[p]

如果Children = 1,添加游乐区的影响是:

VisitDuration(C = 1,P = 1) - VisitDuration(C = 1,P = 0) = (β[0] + β[c] + β[p]) - (β[0] + β[c]) = β[p]

这两个方程之间的差异(即为有孩子的顾客比没有孩子的顾客增加游乐区增加访问持续时间的更多量)根据定义为:

[(β[0] + β[c] + β[p]) - (β[0])] - [(β[0] + β[p]) - (β[0])] = β[p] - β[p] = 0

从另一个角度看,我们有四个方程,对应四个平均值,但只有三个系数。一旦我们根据前三个方程设置了β[0],β[c]β[p],如果发现(C = 1,P = 1)的平均访问持续时间不等于β[0] + β[p] + β[c],我们就陷入了困境。我们的算法会尽力找到能够最小化回归误差的值,但我们的估计会有偏差。不幸的是,这恰恰是我们试图解决的问题!换句话说,简单地将Children作为回归中的一个变量并不能解释ChildrenPlayArea之间的相互作用。通过这些方程我们无法确定游乐区的存在是否更多地影响了带有孩子的顾客的访问持续时间。

这就是中介的作用。我们可以通过添加第四个系数来解决问题,用于解释PlayAreaChildren的交互作用(见 Equation 11-2)。

方程式 11-2。

VisitDuration = β[0] + β[p].PlayArea + β[c].Children + β[i].(PlayArea * Children)

对于(C = 1,P = 1)的方程式变为VisitDuration = β[0] + β[p] + β[c] + β[i],我们可以调整系数β[i]来解释交互效应。现在我们有:

  • 如果Children = 0,添加游乐区的影响是(β[0] + β[p])- (β[0]) = β[p]

  • 如果Children = 1,添加游乐区的影响是(β[0] + β[c] + β[p] + β[i])- (β[0] + β[c]) = β[p] + β[i]

这两个方程的差异是:

[(β[0] + β[c] + β[p] + β[i]) - (β[0])] - [(β[0] + β[p] + β[i]) - (β[0])] = β[p] + β[i] - β[p] = β[i]

添加游乐区会使有孩子的顾客的访问持续时间增加β[i]分钟更多,而对没有孩子的顾客则少。

方程 11-2 中的乘法项通常在 CD 上用一个箭头来表示,箭头中间是原始箭头的结束位置(图 11-2)。在这种情况下,变量 Children 被称为 调节变量PlayAreaVisitDuration 之间的关系是 调节的

PlayArea 对 VisitDuration 的影响由顾客是否带孩子来进行调节

图 11-2. PlayArea 对 VisitDuration 的影响由顾客是否带孩子来进行调节

回归软件识别调节作用,并通常包括一个快捷方式:如果只包括两个变量的乘积,软件将识别出你想要计算这些变量的系数:

## Python (output not shown)
ols("duration~play_area * children", data=hist_data_df).fit().summary()
## R
> summary(lm(duration~play_area * children, data=hist_data))
...
Coefficients:
                     Estimate Std. Error t value Pr(>|t|)    
(Intercept)          19.98760    0.01245  1605.5   <2e-16 ***
play_area1            3.95907    0.02097   188.8   <2e-16 ***
children1            10.01527    0.02017   496.6   <2e-16 ***
play_area1:children1 20.98663    0.03343   627.8   <2e-16 ***
...

当你使用上文描述的快捷方式时,你选择的软件将添加 PlayAreaChildren 作为 VisitDuration 的单独预测变量,并且作为 方程 11-2 的估计结果。但是,是否不使用 Children 作为一个单独的项来估算这个方程会更好呢?也就是说:

VisitDuration = β[0] + β[p].PlayArea + β[i].(PlayArea * Children)

简短的回答:不需要。有关更多详细信息,请参考 Hayes (2017) 等参考文献,但基本上,为了使系数达到你想要的效果,请在回归中包括调节变量和被调节变量作为单独的变量,即使它们的系数在经济或统计上并不显著。不要通过软件将它们去除;这是一个特性,而不是错误。

这个回归中的系数与我们列出的四种情况的平均值匹配:

  • 不带孩子去没有游乐区的商店的顾客的平均访问时长是 β[0],也就是截距的系数,即 20 分钟。

  • 不带孩子去有游乐区的商店的顾客的平均访问时长是 β[0] + β[p],即截距和 PlayArea 的系数之和,大约是 20 + 4 = 24 分钟。

  • 带孩子去没有游乐区的商店的顾客的平均访问时长是 β[0] + β[c],即截距和 Children 的系数之和,大约是 20 + 10 = 30 分钟。

  • 带孩子去有游乐区的商店的顾客的平均访问时长是 β[0] + β[c] + β[p] + β[i],即截距、PlayAreaChildrenPlayAreaChildren 之间的交互项的系数之和,大约是 20 + 4 + 10 + 21 = 55 分钟。

换句话说,有游乐区对带孩子的顾客的平均访问时长有很大影响,对不带孩子的顾客的平均访问时长也有一个较小但不可忽视的影响(可能是因为可以轻松无忧地购物)。

这可以用图 11-3 中的视觉表示,Children变量的值位于 x 轴上,VisitDuration位于 y 轴上,我们有两条线,每个PlayArea值对应一条。换句话说,两条线末端的四个点代表我们刚刚看到的四种情况,它们的 y 值与系数匹配。

如果我们的两个变量之间没有交互效应,那么两条线将是平行的,因为有游乐区会将平均访问时长向上移动同样的增量。它们不平行的事实表明PlayArea对带有孩子的顾客影响更大。

交互作用的视觉表示

图 11-3. 交互作用的视觉表示

如果你想把这个图与我们的数学联系起来,左侧两点(对于 C = 0)之间的距离等于β[p],而右侧两点(C = 1)之间的距离是β[p] + β[i]

调节分析的见解使我们能够更好地定位我们的行动,例如,如果 C-Mart 想确定在哪些商店建设下一个游乐区。尽管个别系数β[p]有助于确定游乐区是否在总体上值得投资,但对于商店的优先顺序完全无关紧要。在选择商店时,需要确定与PlayArea变量具有最强交互作用的商店和顾客特征,然后确定哪家商店会因增加游乐区而产生最大的“提升”。

或者,在已经设有游乐区的商店中,调节分析将允许 C-Mart 确定向哪些潜在顾客推广邮件以宣传游乐区的存在。

分割实验数据

分割实验数据的过程与我们刚刚用于观测数据的过程几乎完全相同。因此,我不会重复定量分析,只会指出一些需要注意的细微差别和微妙之处。

在进行实验时,我们通常不仅关心测量样本中治疗的平均效果,还关心确定效果特别强或弱的群体。发送信件或培训员工可能很昂贵;即使财务成本微不足道,如电子邮件一样,也可能存在无形成本,比如让顾客感到恼火。因此,我们通常只想将治疗应用于其有效的人群。

超越成本考虑,个性化是将特定消息或处理针对我们客户群体的特定部分的关键理由。个性化消息的核心理念是目标群体之外的个体可能不会做出反应,甚至可能对消息产生负面反应。如果你有一条适合所有人的消息,那很好,但这并不是个性化。例如,一张促销滑冰刀券可能会惹恼大多数对此无用的人。面向户外爱好者的广告可能会激怒安静的书虫,反之亦然。个性化意味着你在某个子群体上提高了效果,但在另一个子群体上效果降低。

在市场分析中,这种方法通常被称为“提升”分析或建模,因为我们试图识别哪些客户群体对于特定的广告活动或处理能够最大程度地增加他们采取行动的倾向(例如购买或投票),而不管他们的初始倾向如何。这最后一点通常会引起混淆,因此值得进一步解释:识别具有高倾向性的客户可能会有其用途,但不能仅此作为定位的依据。

比如,假设你比较年轻客户(30 岁以下)和老年客户(60 岁以上):

  • 第一组如果不发送电子邮件,采取行动的概率为 20%,如果发送电子邮件,则为 40%。

  • 第二组如果不发送电子邮件,采取行动的概率为 80%,如果发送电子邮件,则为 90%。

对于第一组年轻客户而言,发送电子邮件的效果平均要比对第二组老年客户发送更加有效:这将显著增加采取行动的客户总数。

然而,在现实生活中,由于缺乏适当的对照组,这一事实经常会被掩盖。如果你只给第二组发送电子邮件,并将他们的行为与其他人口进行比较,那么这将大大夸大电子邮件活动的表现效果。

从数学上讲,确定一个高有效处理群体意味着找到是处理变量对感兴趣效果的调节变量的人群特征。在这种意义上,调节分析为我们提供了一个关于提升分析、个性化和更广泛的定位的强大和统一的概念框架。

互动

我们绘制的用于表示分割的 CD 是不对称的:变量PlayArea直接指向VisitDuration,而变量Children则指向第一个箭头。然而,我们的回归方程却是完全对称的;没有任何迹象表明这两个变量中哪个是调节变量,哪个是被调节变量。严格来说,我们可以解释方程 11-2 意味着Children是原因,PlayArea是调节变量。

其中一种表达方式比另一种“正确”吗?当我们有一个个人特征和涉及调节的商业或行为变量时,通常会表示个人特征(例如,是否有子女)为调节变量,并称之为分割。也就是说,游乐区的影响在有子女的客户段和没有子女的客户段之间是不同的。

另一方面,如果两个类型相同的变量之间存在调节,例如两个个人特征或两个业务干预,那么在它们之间引入不对称是没有意义的。例如,我们可以想象,拥有游乐区和休息区都独立地增加了访问时长(对于有子女的客户来说是游乐区,对于没有子女的客户来说是休息区),但是同时拥有两者的效果比它们各自的效果之和更大,因为有子女的客户现在可以使用休息区。在这种情况下,我们可以通过像图 11-4 中描述的那样将箭头连接起来表示调节,并称之为交互作用

Representing symmetrical interactions

图 11-4. 表示对称交互作用

该 CD 的方程将是:

VisitDuration = β[0] + β[p].PlayArea + β[l].LoungeArea + β[p][l].(PlayArea * LoungeArea)

如您所见,该方程与分割的方程完全相同,如果我们愿意,我们可以将LoungeArea称为PlayAreaVisitDuration的影响的调节变量(或反之)。概念上,交互作用让我们思考“整体大于部分之和”的情况,例如互补行为。

注意

顺便说一句,我相信一些新型和更复杂的机器学习方法,如随机森林、XGBoost 或神经网络的一些力量来自于它们捕捉这种相互作用的能力。通过在回归中包含相互作用,我们可以减少性能差距,同时保持回归中我们与因果关系的可解释性。

非线性关系

在许多情况下,原因和结果之间的关系并非线性。它可能具有经济学家所称的“递减收益”:你花费的成本越多,得到的回报越少。例如,一个满意的客户可能比一个不满意的客户购买更多,但一个狂热的客户可能购买的并不比一个快乐的客户多得多。对客户每月发送一封营销邮件可能会增加购买量,但是相比发送 10 封邮件,发送 11 封邮件可能并不会帮助太多,就像图 11-5 左侧面板中描述的那样 Figure 11-5。

相反,因果关系可能存在递增回报,例如初创公司宣传的网络效应:AirCnC 在其网站上拥有的房源越多,吸引的客户就越多;然后,其客户群越广,房主提供其房源的动机就越高,依此类推,就像图 11-5 右侧所示。

变量之间的非线性关系:左侧是递减回报,右侧是递增回报

图 11-5. 变量之间的非线性关系:左侧是递减回报,右侧是递增回报
注意

从数学上讲,左侧的曲线是凹的,右侧的曲线是凸的。一个常见的错误是认为这样的关系不能用线性回归表示。实际上,对于回归来说,“线性”的关键在于预测变量与系数的线性关系,而不是变量本身。Y = β[1].e^(X[1]) + β[2].e^(X[2])是一个正确的线性回归,因为将每个系数乘以 2 会将 Y 乘以 2。相反,Y = e(*β*[1])(.X[1]) + β[2]X[2]不是一个正确的线性回归,因为 Y 与系数没有线性关系。

通过将解释变量取平方(即二次项),我们可以解决变量之间的非线性关系。例如,我们可以将我们刚讨论的营销电子邮件与购买之间的关系建模为:

购买 = β[0] + β[1].邮件 + β[2].邮件*²

添加二次项可以显著提高回归的准确性。在图 11-6 中,您可以看到实线曲线代表具有二次项的线性回归的最佳拟合线比虚线更接近数据点。在这里,每月额外的电子邮件具有递减的影响,这意味着二次项在回归中具有负系数。

线性(虚线)和二次(实线)最佳拟合线

图 11-6. 线性(虚线)和二次(实线)最佳拟合线

然而,二次项无非是一个变量与自身的交互作用。换句话说,两个变量之间的非线性因果关系可以重新构建为自我调节(图 11-7)。

表达自我调节

图 11-7. 表达自我调节

从概念上讲,这意味着每月增加一个额外的电子邮件对当前的业务通常发送的电子邮件数量具有不同的影响。

代码中包含自我调节的语法是:

## R 
summary(lm(Purchases ~ Emails + I(Emails²), data=dat))
## Python
model = ols("Purchases ~ Emails + I(Emails**2)", data=dat_df)
print(model.fit().summary())

在 R 和 Python 中,通过使用身份函数I()来完成,这可以防止线性回归算法尝试解释平方项,而只是将其传递给一般求解器。在 R 中,平方项用符号^表示(1),而在 Python 中用两个乘法符号表示。这也意味着自我调节与传统调节完全相同的验证方式:我们将建立一个 Bootstrap 置信区间,确定是否包含零以及是否在经济上显著。

总结我们对调节的探索,我们看到了它的三种类型,数学上是相同的,但基于涉及的变量类型有不同的解释:

分割

在分割个人特征(例如人口统计变量)中,会调节商业行为的效果,比如实验干预(此时被称为提升分析)。

互动

在互动中,我们观察到同一性质变量之间的调节,例如两个人口统计变量或两个行为变量。

非线性

非线性导致一个变量自我调节其对另一个变量的因果影响。

现在我们对调节的行为解释有了清晰的理解,让我们转向其应用的细节。

如何应用调节

正如我们在前一节中讨论的那样,通过将两个变量的乘积添加到回归中,调节可以用来捕捉各种行为效应。在本节中,我们将转向技术考虑:

  • 何时寻找调节

  • 如何验证它

  • 调节的调节

  • 如何解释调节回归中各个变量的系数

何时寻找调节?

由于调节有很多可能的应用,很容易到处寻找,但作为二阶效应(即对效应的影响),调节通常产生较小的系数,并且存在高假阳性的风险。这在实验数据中尤为真实,因为有强烈的动机寻找显著效应:“确实,电子邮件活动的平均效果几乎为零,但看看堪萨斯州三十岁男性的响应率!”

假设你开始分析观察数据或设计实验。在什么时候应该考虑调节,以及如何将其整合到分析中?首先,我将讨论实验设计阶段。无论数据是观察性的还是实验性的,数据分析阶段的过程都是相同的,所以我将在之后同时涵盖这两种情况。最后,我将涵盖非线性,这很简单:因为只涉及一个变量,你不必真正担心潜在的风险,可以在分析中自由地包含非线性。

在实验设计阶段包括调节。

我将区分两种情况,这将需要不同的方法:

  • 你的分析的主要对象是主效应,不管有没有调节,调节是你分析的次要对象。

  • 你的分析的主要对象是适度。

如果您设计一个实验,其主要目标不是测量调节效应,我的建议很简单:利用调节的可能性来完善您的变化理论,但不要试图调整样本大小。

在第四部分中,我们看到在运行实验之前,您应该通过因果图明确您的变化理论。在这种情况下,重点是平均因果效应,即实验处理在整体上的平均效应,但在许多情况下,我们可以通过调节来完善这一逻辑。

在第八章中,我们设计了一个实验,希望通过提供“1-点击预订”来增加 AirCnC 的预订率。可以想象,这种效应可能会受到年龄的调节影响(图 11-8)。

年龄调节的实验处理

图 11-8. 年龄调节的实验处理

我们改变理论的行为逻辑是,1-点击按钮将缩短预订流程的时间,这本身影响了完成预订的可能性。这意味着在调节方面有两种可能性:年龄调节了 1-点击按钮对预订时间的影响,或者调节了预订时间对完成预订的影响,或者两者都有(图 11-9)。

年龄可能调节两个因果关系

图 11-9. 年龄可能调节两个因果关系

将我们的行为逻辑和潜在的调节因素结合起来非常强大,因为它使我们能够在实验进行之前思考行为影响,并在某些情况下进行分析。

让我们从 CD 左侧的第一个关系开始,即“1-click”按钮和预订持续时间之间的关系。我们在历史数据中没有关于“1-click”按钮的任何数据,因此无法直接测量这种调节效应。然而,如果我们在历史数据中看到年龄和预订持续时间之间的(非混杂的)相关性,这就在一定程度上支持这个想法,因为它暗示了年龄影响了正在发生的认知过程。另一方面,如果年龄与预订持续时间无关,调节将依赖于年轻和年长客户对按钮本身的不同反应。虽然这并非不可能,但这是一条“较窄”的行为路径,根据奥卡姆剃刀原理,最简单的解释通常是正确的。更重要的是,这是一条可测试的行为路径。例如,如果你将婴儿潮一代的样本带入用户体验实验室,并发现他们不信任“1-click”过程,因为他们想要经历所有步骤,那么调节就是非常可能的,你可以将实验对象定位为年轻客户,或至少过采样他们。

对于第二个关系,即预订持续时间与预订完成之间的关系,我们在历史数据中拥有所有必需的变量,这意味着我们可以在甚至运行实验之前确认或否定调节效应,其准确性比单个实验的有限样本量高得多。同样,如果我们确认存在调节效应,我们可以相应调整我们的实验设计(例如,根据调节的方向,仅针对年轻或年长客户)。

这里有一个更广泛的经验教训:通过表达干预的行为逻辑,我们通常可以扩展我们的干预与我们在 CD 中感兴趣的效果之间的链条,通过识别一个或多个已经有数据的中介者来进行这种扩展。然后,我们可以探索中介者与最终效果之间的关系是否被调节。此外,我们还可以探索这种关系是否是自我调节的。在我们的例子中,也许预订率在某一点之前不受预订持续时间的影响,但之后急剧下降,例如,顾客可能并不在乎预订需要 30 秒还是 45 秒,但如果超过 2 分钟,他们会大量放弃这一过程(见图 11-10)。

中介者与最终效果之间的关系是自我调节的

图 11-10. 中介者与最终效果之间的关系是自我调节的

在识别历史数据中的调节变量的过程中,你必须牢记,“相关不意味着因果关系”适用,并且你正在观察的关系可能是混杂的,如图 11-11 所示。

收入是持续时间和预订率关系的混杂变量

图 11-11. Income是持续时间和预订率之间关系的混杂变量

在这种情况下,Income的混杂效应会使DurationBooking之间的关系偏离真实的因果效应(这是您通过一键式按钮实验将要采取行动的)。因此,在评估调节效应时,您需要控制混杂效应。

对于像Age这样在我们实验分配之前就可以观察到的变量,我们可以直接将其用于分配本身。例如,我们可以将我们的实验限制在某个年龄段以下的客户,我们预计会产生最大的效果。

显然,在客户开始预订过程之前,我们无法观察BookingDuration,这意味着我们不能在实验分配中使用它。然而,我们可以尝试识别目标变量的代理:例如,也许预订在周末比工作日长。然后,我们可以使用这些代理来定义我们的实验人群,比如仅对周末的客户运行我们的实验。如果代理和目标变量密切相关,这将很有效,例如,在周末也有一些较短的预订,但 90%的较长持续时间预订发生在周末。相反,如果两个变量只有松散的关系,例如,60%的较长持续时间预订发生在周末,那么您在实验中观察到的结果可能无法很好地推广到工作日的较长持续时间预订。

如果调节不是您实验的主要焦点,即您尚未测量未调节效应。这是因为检测调节和交互作用的能力明显低于检测主效应的能力,这意味着测量调节所需的样本量会显著更大(考虑增加 10-20 倍甚至更多)。这对于尚未确认的平均效应而言,花费大量时间来探索调节的可能性并不划算。

因此,我建议您首先运行第一次实验,重点是准确测量平均未调节效应,然后再测量结果中的调节效应。如果发现任何有希望的调节效应,由于实验未能捕捉到它,其置信区间很可能很大地跨越零点。如果从经济学的角度来看,这种调节效应似乎足够有前景,那么可以运行第二个实验来适当地测量它。

一旦您知道未调节效应大小,您可以决定进行专门测量或确认调节的实验。确定适当样本大小的过程仍然相同:您将设定目标功效和假设/目标效应大小,然后确定在不同样本大小下重复模拟中的假阴性比例,正如我们在以前的功效分析中所做的那样。唯一的区别是,您将使用您的先验知识作为主效应大小,并仅为调节设定目标效应大小。

在数据分析阶段包括调节

您手头有一些数据,可能是观察性的,也可能是您运行的实验。如何在不遇到假阳性的情况下识别相关的调节效应?您将不得不进行渔猎探险,即尝试包括一个变量的调节项,然后是另一个变量,依此类推。接下来,我将提供几条指南,以最小化假阳性的风险。

首先,当您试图调节分类变量对数值变量的影响时,我将为您提供一个粗略但强大的理性检查。数值效应的要求仅意味着您正在进行线性而不是逻辑回归。分类原因的要求可能看起来非常严格,但它适用于所有实验性任务(即实验性任务始终是二元或分类变量)。此外,如果您的原因是数值型的,您可以通过取其四分位数来将其离散化:

## R
hist_data <- hist_data %>% mutate(age_quart = ntile(age, 4))
## Python
hist_data_df['age_quart'] = pd.cut(hist_data_df['age'], 4, 
                                   labels=['q4', 'q3', 'q2', 'q1'],
                                   include_lowest=True)

理性检查是比较您感兴趣的效应在由您感兴趣的原因定义的组之间的标准差。如果在治疗组(对于实验数据)中标准差显著较高,或者在观察性数据中各组之间差异较大,这表明可能存在调节的可能性,您可以有合理的信心继续您的渔猎探险。如果各组之间的标准差相似,则表明没有调节效应;如果您愿意,仍然可以尝试几个潜在的调节变量,但需要对它们有坚实的理论基础。

在这种情况下,“有意义地更高”或“相似”意味着什么?如果您想要严格的理由,可以运行一些统计测试来帮助您确定观察到的差异是否在统计上异常,例如布朗-福斯斯试验^(2)。就我个人而言,我建议您简单地直观判断:相对于组间均值差异,这种差异在经济上是否有意义?

回到 C-Mart 商店游乐区的示例,代码如下:

## R (output not shown)
> hist_data %>% group_by(play_area) %>% summarize(mean = mean(duration), 
                                               sd = sd(duration))
## Python
 hist_data_df.groupby('play_area').agg(M = ('duration', lambda x: x.mean()),
    SD = ('duration', lambda x: x.std()))
Out[22]: 
                   M         SD
play_area                      
0          23.803928   6.970786
1          36.360939  17.111469

在这个例子中,标准差相差 10 分钟绝对是 C-Mart 有兴趣探索的内容,因为组间均值差异大约是 13 分钟。

注意

显然,假阳性的风险随着类别数量的增加而增加。如果您的主要原因是州或职业,那么您可能会看到一些标准差在全面范围内的变化,只是由于随机性和特殊情况。因此,我只会在第一次有相当强的理由的情况下才会考虑这种变量的调节。

减少假阳性风险并使任何可能发现的调节更有意义的一种方法是,将您的分类变量替换为相关数值变量的四分位数。例如,州的政治倾向或平均收入,职业的女性比例或平均教育水平。说 25%收入最低的州比 25%收入最高的州的购买标准差小是一个比说加利福尼亚州的标准差高于密西西比州更健壮且可操作的洞察更多的方法。

假设您的数据通过了这个第一步的合理性检查,第二步是确定调节效应的上限。这里的关键直觉是,调节只能“重新分配”平均效果;它不会增加它。通过视觉示例可以更清楚地说明这一点,以Children作为PlayAreaVisitDuration效果的潜在调节因素。图 11-12 显示了游乐区在我们数据中的平均效果(11.92 分钟),柱子的宽度表示我们人口中没有和有儿童的顾客比例。

不同有无儿童顾客的游乐区的平均效果

图 11-12. 不同有无儿童顾客的游乐区的平均效果

假设游乐区对没有儿童的顾客没有任何负面效果(即它不会产生负面效果),这从行为角度来看是一个合理的假设,尤其是如果游乐区适当隔音的话。这意味着,最多的情况下,整个平均效果来自有儿童的顾客,调节会将整个左侧柱的面积重新分配到右侧柱上,如图 11-13 所示。

完全来自有儿童顾客的平均效果

图 11-13. 完全来自有儿童顾客的平均效果

在这种“最极端的情况”下,让我们计算有儿童顾客群体中的效果大小。根据平均值的定义,我们有:

平均效果大小 = (各个个体效果大小的总和) / (顾客总数)

让我们将分子中的有儿童和没有儿童的顾客分开:

平均效果大小 = (没有儿童顾客的各个个体效果大小总和 + 有儿童顾客的各个个体效果大小总和) / (顾客总数)

如果我们假设对没有孩子的客户没有影响,则分子中的第一项等于零:

平均效应大小 =(具有孩子的客户的个体效应大小总和)/(客户总数

让我们将等式的两边都乘以客户总数:

平均效应大小 * 客户总数 = 具有孩子的客户的个体效应大小总和

然后将等式的两边都除以具有孩子的客户数量:

平均效应大小 * 客户总数 / 具有孩子的客户数量 = 具有孩子的客户的平均效应大小

平均效应大小 * 1 / 客户中具有孩子的比例 = 具有孩子的客户的平均效应大小

为了提高可读性并在前面的示例中插入数字,我们交换了左右两边:

具有孩子的客户的平均效应大小 = 11.92 * 1 / 0.387 = 30.8

换句话说,针对具有孩子的客户的平均效应大小不能高于 30.8 分钟的访问时长,考虑到总体平均效应大小。如果这个数字对经济利益来说太低,那么测量PlayAreaChildren之间的调节就没有意义。除了这个具体的例子之外,公式平均效应大小/段中客户的比例可以应用于任何潜在的调节因素:如果你的客户群体中男女平分,这意味着最多,一种性别的效果是平均效果的两倍,另一种性别没有效果。希望某种方式一种性别会有三倍于平均效果,或者调节将增加两种性别的效果是一种幻想,数学上不可能的。换句话说,我们倾向于关注具有高于平均效应大小的群体,这是可以理解的原因,但对应的是具有低于平均效应大小的群体。

这意味着你搜索潜在的调节因素应该从具有强有力的行为基础或在过去的分析中被发现具有影响力的变量开始,并且产生的子组足够大。超过一定程度的组大小不平等后,搜索就变得毫无意义:如果你有一个将你的客户群体分为 90%-10%的变量,那么即使代表你客户群体 10%的群体根本没有效果,这最多也只会将 90%群体的效果增加到 1/0.9 = 111%的平均效果,增加了 11%。

更广泛地说,调节不能拯救平均效果一般般。它只应该被寻求以给你的治疗方案增加一点点力量;例如,如果你的达到了它的盈亏平衡值或目标值的 90%,或者如果你处于高交易量的环境中,并且你正在尝试提取任何你能够的效率提升。

对于每个你想要测试的变量,过程是相同的。首先,运行一个回归,其中包含实验处理变量和潜在调节变量之间的交互作用。然后,如果发现一个足够大的交互效应,通过构建一个 Bootstrap CI 来确认它。

在这种方式下测试大量假设时,有一些规则,比如邦费隆尼校正,可以减少误报的风险。但出于两个原因,我不建议它们:

  1. 它们通常依赖于零假设统计检验框架,具有正态性假设,显式或隐式地。

  2. 它们可能过于保守,并且在增加假阴性的风险方面是不可接受的。

相反,我建议在可能的情况下通过后续实验验证有潜力的子群体。如果调节效应足够大以至于有商业价值,那么应该考虑进行进一步验证。如果你正在处理实验数据,重复实验在概念上是直接的。如果你正在处理观察数据,你应该运行哪种实验可能并不立即清楚。然而,你的调节效应要有任何经济价值,就必须意味着你计划做一些不同的事情。否则,它只是餐会上的一个有趣细节。不论你要做的是什么,你可能可以以某种方式随机化它,现在你有了一个实验。

在这个过程中的一个关键成功因素是恰当地向业务伙伴传达这样重复测试的结果应被视为暂时性假设,而不是已经证明的事实。当一个实验因为它本来就是一个长期的尝试而产生了零结果时,他们不应该感觉你没有交付。还要记住,由于过程的性质,最终的效果大小可能比你在第一次尝试中发现的要小。

非线性

非线性,即自我调节,在与其他形式的调节相比,代表了一个特殊情况,因为根据定义,只有一个调节变量在考虑范围内,这限制了误报的风险。此外,误报的后果通常是有限的,只要你不从可用数据范围外推断。例如,如果你基本上基于年收入在 25,000 到 75,000 美元之间的顾客测量收入对购买的影响,然后为年收入 25 万美元的顾客推断,即使不考虑调节,误报的风险和后果也是高的(总之,这样推断是一个可怕的想法)。

因此,如果您的数据至少有几百行,通常可以定期测试您感兴趣的原因以进行自我调节。该变量必须是数值型,因为分类变量进行自我调节是不合理的。然后,您应该通过构建 Bootstrap 置信区间来验证自我调节效应,我们稍后将在本节中看到。

除了改进回归模型的拟合度和考虑直观行为效应,例如递减回报之外,将自我调节纳入回归模型中可以提醒您存在隐藏的调节变量。

让我们看一个 C-Mart 的例子:访问持续时间和杂货购买之间的关系。可以想象,非常短的访问代表有针对性的购物行程以购买特定物品,而较长的行程更可能是杂货购物。这将使访问原因成为 访问持续时间杂货购买 之间关系的混淆因素(图 11-14)。

访问原因是访问持续时间和杂货购买之间关系的混淆因素

图 11-14. 访问原因是访问持续时间和杂货购买之间关系的混淆因素

但同时,访问原因 可能会理论上调节 访问持续时间杂货购买 的影响。如果你去商店买生日礼物或锤子,让你在店里逗留更长时间(例如添加一个游乐区)不太可能诱使你购买晒干番茄,而这在你进行杂货购物时可能会起作用(图 11-15)。

访问原因调节访问持续时间对杂货购买的影响

图 11-15. 访问原因调节访问持续时间对杂货购买的影响

如果我们能观察到访问原因,我们就能将其添加到我们的 CD 中,并且它将成为一个明显的混淆+调节案例。假设我们不能观察访问原因,我们只能留下以下可观察的事实:

  • 杂货购买访问持续时间 正相关(既因为真实效应,也因为访问原因的混淆效应)。

  • 增加访问持续时间对较长访问的杂货购买影响更大,而对较短访问的影响较小。

换句话说,访问持续时间杂货购买 之间的关系表现出非线性。自我调节项将提高我们回归的准确性,因此我们应该包含它。

更一般地说,每当您有一个看似自我调节的变量而又没有明确的行为基础时,您都应该探索可能既是该变量的原因又是调节变量的隐藏变量的可能性。

总结一下,寻找相关的调节变量是行为分析的重要部分,但像大多数工具一样,它往往更多的是艺术而非科学。在实验设计阶段,我们可以通过历史数据中调节的中介变量或通过我们的处理进行用户体验测试来加深我们的行为逻辑。在数据分析阶段,钓鱼探险可以发现有前景的潜在调节变量;而自举法置信区间有助于减少假阳性的风险,跟进实验最终是你成功的最佳保证。相反,自我调节则安全地探索并定期包括。

多个调节变量

为了简单起见,到目前为止,我只展示了一个调节变量,但效果可能有多个调节变量。从一个调节变量到多个调节变量的转变很简单;需要牢记的主要细微之处是调节变量是否彼此互动。

平行调节变量

回到我们的 C-Mart 示例,我们可以想象另一个人口统计变量年龄也分别调节了游乐区访问时长的影响,如图 11-16 所示。

游乐区对访问时长的影响具有两个调节变量

图 11-16. 游乐区对访问时长的影响具有两个调节变量

相应的回归方程式如方程式 11-3 所示。

方程式 11-3.

访问时长 = β[0] + β[p].游乐区 + β[c].孩子 + β[a].年龄 + β[pc].(游乐区 * 孩子) + β[pa].(游乐区 * 年龄)

注意,即使年龄访问时长之间没有箭头,方程式中仍包括年龄的个体项。前面提供的警告在这里仍然适用,你需要在回归中包含所有个体项。

方程式 11-3 的解释将是:

  • 游乐区访问时长的影响对有和没有孩子的客户也不同。

  • 游乐区访问时长的影响在年轻和年长客户之间不同。这两个调节效应是彼此独立的。

年龄是一个数值变量,因此其解释必须相应调整:游乐区 * 年龄 的系数代表了年龄相差一年的客户之间游乐区访问时长影响的差异。根据手头的业务问题,我们可以选择:

  • 如果我们想要对访问时长进行精确、因果合理的估计,我们应该保持其数值化,例如确定向某个商店添加游乐区后我们可以预期的销售增长。

  • 或通过适当的“分箱”方式使其分类化。例如,我们可以将年龄转换为“小于 20 岁”,“20 至 40 岁”,“40 岁以上”等分段,使相应的系数更容易用于分段目的的解释。

总之,ChildrenAge形成了一个二维人口统计分割,使我们能够比较例如 28 岁有孩子的人与 45 岁无孩子的人的平均访问持续时间。

因为这两种调节效应彼此独立,我们可以通过对方程 11-3 进行自助法回归来独立验证每一个调节效应,并查看每个调节变量的置信区间(请参见下一小节)。

注:

这也意味着这两个调节变量之间没有顺序之分,它们可以互换使用:在图 11-16 中,Children排在前面,但Age同样可以先出现。这并不重要。

多个调节变量之间的逻辑同样适用于相同性质变量之间的交互作用。例如,我们可以让ChildrenAgeGender都进行交互作用(图 11-17)。

年龄和性别与儿童均有交互作用

图 11-17. 年龄和性别都与儿童有交互作用

对应的回归方程为:

VisitDuration = β[0] + β[c].Children + β[a].Age + β[g].Gender + β[ca].(Children * Age) + β[cg].(Children * Gender)

最后,回归可以包括多个每个自我调节的变量。在这里,分析将独立进行每一个变量。

总体而言,添加多个独立的调节变量非常简单,但有时也有必要假设这些调节变量之间存在交互作用,接下来我们将详细讨论。

交互作用的调节变量

分析PlayAreaVisitDuration的影响时,可以理解其受到ChildrenAge的共同调节。但在有儿童的顾客中,我们也可以想象,访问持续时间的增加取决于顾客的年龄,例如,如果祖父母不像父母那样带孩子去玩区,那么ChildrenPlayAreaVisitDuration的调节效应本身也会受到Age的调节,这在社会科学中称为“调节的调节”(图 11-18)。

调节的调节

图 11-18. 调节的调节

图 11-19 展示了调节调节的作用,每个子图代表我们顾客中的一个年龄组。我们可以看到,从年轻到老的顾客,ChildrenPlayAreaVisitDuration的影响逐渐减弱(说起来很拗口!),因为 C=1 的两点之间的距离在子图中逐渐减小。

不同年龄组之间的调节调节

图 11-19. 不同年龄组之间的调节调节

相应的方程如下所示:

VisitDuration = β[0] + β[p].PlayArea + β[c].Children + β[a].Age + β[pc].(PlayArea * Children) + β[pa].(PlayArea * Age)+ β[ca].(Children * Age)+ β[pca].(PlayArea * Children * Age)

这个方程与 方程 11-3 相同,只是最后增加了一个三向项。让我们进行回归分析(注意我只输入了回归中的三向交互项,软件会自动添加所有单独的变量和两向交互项)。

## R (output not shown)
> summary(lm(duration~play_area * children * age, data=hist_data))
## Python
ols("duration~play_area * children * age", data=hist_data_df).fit().summary()

...
                        coef     std err    t        P>|t|    [0.025   0.975]
-----------------------------------------------------------------------------
Intercept              20.0166    0.037  534.906    0.000    19.943    20.090
play_area               3.9110    0.063   62.014    0.000     3.787     4.035
children                9.9983    0.061  165.012    0.000     9.880    10.117
play_area:children     29.1638    0.101  290.105    0.000    28.967    29.361
age                    -0.0006    0.001   -0.820    0.412    -0.002     0.001
play_area:age           0.0010    0.001    0.806    0.420    -0.001     0.003
children:age            0.0003    0.001    0.297    0.767    -0.002     0.003
play_area:children:age -0.1637    0.002  -86.139    0.000    -0.167    -0.160
...

我们输出中的三元项系数是负的,经济上也是有意义的,90% 的置信区间大约是 [–0.1671 ; –0.1590],这个区间足够狭窄,让我们对它不可能接近于零感到自信。

调节的调节遵循简单调节的相同逻辑和规则。因此,它是对称的,这意味着我们可以解释 Age 作为调节 Children 的调节效果,或者可以看作是 Children 调节 Age 的调节效果。

从行为角度来看,调节的调节的解释取决于底层逻辑是分段还是交互:

  • 如果我们有两个个人特征变量调节一个业务特征或业务行为,我们将其解释为分段。例如,这种情况出现在游乐区,其中 ChildrenAge 调节了 PlayArea 变量的效果。直观地说,这意味着我们有一个二维分段,其中一个维度的调节效果沿着另一个维度增加或减少(例如,当 Age 增加时,有孩子的调节效果减少)。

  • 对于三个同类型的变量,我们将其解释为三个变量之间的三向交互作用,如 图 11-20 中所示。

三个业务特征变量之间的三向交互作用

图 11-20. 三个业务特征变量之间的三向交互作用

区分的重点不是学究式的,而是提醒您跟踪分析目标。在回归中找到和验证预测变量之间的调节确实很好,但您打算如何利用这些信息呢?您可以改变业务行为,有时也可以改变业务特征,但您只能针对客户的个人特征进行定位。同样地,如果您发现某种客户行为调节了业务行为的效果(例如,交叉销售电子邮件营销在最近到店的客户中效果最佳),这些信息可以用来定位展现这种行为的客户,或者鼓励客户首先表现出这种行为。

注意

严格来说,调节的调节也可以应用于非线性,通过具有立方项(例如,Purchases = β[0] + β[1].Emails + β[2].Emails² + β[3].Emails³),但这种情况非常罕见,更多是一个缺乏实际应用的好奇心。

此时,您可能已经开始头痛。调节效应很快会变得非常复杂。我向您展示了三路交互以让您知道这是可能的,但我不会在没有合理强大的行为基础的情况下走这条路。除此之外,理论上我们可以有五路甚至十二路的交互效应(“调节的调节…”),但是仅仅加入尽可能多的交互项会导致虚假阳性,最终形成令人眼花缭乱但无意义的模型。简单直接的调节效应通常已经足够,而且可以在增加的复杂性成本下为分析增添显著的力量。

使用自助法验证调节效应

到目前为止,我们只看了调节项的回归系数,而没有看 p 值。但是和任何其他回归系数一样,我们的调节系数也受到不确定性和抽样变异性的影响。在调节中更加重要考虑不确定性,因为它是“二阶”效应(即对变量的效应的效应),通常比“一阶”效应小得多。

让我们回到 C-Mart 玩耍区的例子。我们对PlayAreaChildren之间的调节效应的估计系数为β[pc] = 21。像往常一样,我们将使用自助法模拟来确定我们观察到的值的确定程度。让我们从可用的历史数据中每次抽取 1,000 个随机样本,每个样本包含 10,000 行,并运行与上一节相同的回归。交互系数的值分布显示在图 11-21 中。

交互系数的自举值分布(来自 10,000 行的 1,000 个样本)

图 11-21. 交互系数的自举值分布(1k 个 10k 行样本)

在第七章中,我们看到增加样本数量可以增加直方图的平滑度和置信区间估计的准确性,增加样本大小可以减少值的离散度。我们是否需要进行这些操作取决于手头的业务问题:

  • 如果问题是,“是否存在任何调节效应?”(即,系数β[pc]是否与零不同?),那么图 11-21 已经让我们能够明确回答“是”,并且没有必要深入挖掘。

  • 如果问题更为不确定,例如,“我们有多大把握交互系数高于 20.5?”那么我们将不得不增加样本大小。现在,直方图超过 20.5 朝左边延伸(而且相当多)。低于 20.5 的每个值代表一个 Bootstrap 模拟,其中调节系数低于该阈值。接下来,让我们增加 Bootstrap 样本大小至 20 万行(图 11-22)。

与交互系数相关的 Bootstrap 值分布(20 万行的 1 千个样本)

图 11-22. 与交互系数相关的 Bootstrap 值分布(20 万行的 1 千个样本)

正如您所看到的,这些值现在更加集中在 21 附近,并且安全地远离 20.5。第二次模拟足以回答我们的业务问题。

在当前情况下,我们的历史数据中总共约有 600,000 行,因此在通过干跑验证代码后,我们本可以直接跳转到这个样本大小,而几乎不会遇到什么麻烦(请记住,您不应该使用比您抽样数据量更大的 Bootstrap 样本大小)。但如果您的历史数据有 1,000 万或 1 亿行呢?如果不是几分钟内回答您的业务问题,您将花费数小时或数天等待模拟那么大数量样本的结果;当然,您的最终置信区间将非常窄,例如[20.9999; 21.0001],但如果问题仅是“高于 20.5 吗?”那将是完全浪费时间。这就是为什么我想向您展示逐步增加 Bootstrap 样本大小的过程,而不是直接跳转到您的历史数据大小。

总结一下,运行小规模 Bootstrap 模拟以确保您的代码正确运行后,应根据需要增加样本数量或样本大小来回答您的业务问题。这个迭代过程的额外好处是激发您的批判意识,避免您自动运行分析。这适用于所有形式的调节:分割(包括实验数据的分割),交互和自调节。

解释个别系数

我在本章中多次提到,参与调节的变量必须作为回归中的个别变量包括,即使您不打算使用它们或它们看起来不显著。然而,如果您想使用它们,这涉及到一些微妙之处,我们现在将进行审查。

让我们开始比较以下两个回归(为简单起见,仅包括年龄,而不是游戏区域之外的变量):

访问时长 = β[0] + β[p0].游戏区域 + β[a0].年龄

and

访问时长 = β[1] + β[p1].游戏区域 + β[a1].年龄 + β[pa1].(游戏区域 * 年龄)

乍一看,第二个方程的解释似乎与第一个方程相同,只是增加了调节项。但事实并非如此。β[p0] 和 β[p1] 并不相等,也没有相同的含义,β[a0] 和 β[a1] 也是如此。

让我们通过绘制 1,000 个数据点的样本来直观地显示差异,为了可读性,将它们表示在 Age * VisitDuration 平面上(图 11-23 和 11-24)。在这两幅图中,都绘制了两条回归线:一条是有游乐区的顾客,另一条是没有游乐区的顾客。

图 11-23 表示第一个没有调节项的方程。这两条线的斜率相同,等于 β[a0] = -0.024,而两条线之间的(常数)距离等于 β[p0] = 12.56。

带游乐区(实心点,实线)和不带游乐区(交叉点,虚线)的 1,000 个数据点和回归线,不带调节项

图 11-23. 1,000 个数据点的样本和回归线,带有游乐区(实心点,实线)和没有游乐区(交叉点,虚线),没有调节项

图 11-24 展示了第二个方程的对应图。这些线不是平行的。这意味着我们不能只说,“β[p1]代表两条回归线之间的垂直距离”,我们必须说明我们测量这个垂直距离的年龄。同样地,我们不能只说“β[a1]代表这些回归线的斜率”,我们必须选择我们正在引用的这些线(即,我们必须说明我们测量该斜率的PlayArea的值)。

这对于商业角度来看有重要的影响。如果商业伙伴问,“游乐区对访问时长有何影响?”而你依赖于第一个没有调节项的方程,你可以回答,“该影响等于 β[p0]。”然而,如果你已经确定这两个变量之间存在显著的调节效应,并且你想依赖于第二个方程(正如你应该的那样),你的答案应该是商业伙伴所畏惧的“嗯,这取决于”。

幸运的是,只要稍加注意,这个问题可以很容易地通过两种可能的方式解决:

  • 设置有意义的参考点

  • 在业务决策层面计算影响

带游乐区(实心点,实线)和不带游乐区(交叉点,虚线)的 1,000 个数据点和回归线,带调节项

图 11-24. 1,000 个数据点的样本和回归线,带有游乐区(实心点,实线)和没有游乐区(交叉点,虚线),带有调节项

设置有意义的参考点

第一个解决方案是为我们的变量设置有意义的参考点。首先让我们回到我们的方程,并适度进行:

VisitDuration = β[1] + β[p1].PlayArea + β[a1].Age + β[pa1].(PlayArea * Age)

如果我们将客户的年龄设置为零,则我们的新生客户的预计访问时长为β[1](没有游乐区)和β[1] + β[p1](有游乐区)。因此,β[p1]实际上是游乐区对年龄为零客户访问时长的影响。然而,新生儿客户并非我们的主要目标,我们可以通过对Age变量进行归一化来进一步改进。一个典型的方法是将其设置为我们数据集中的平均年龄:

## R
> centered_data <- hist_data %>% mutate(age = age - mean(age))
## Python
centered_data_df = hist_data_df.copy()
centered_data_df['age'] = centered_data_df['age'] \
    .subtract(centered_data_df['age'].mean())

这样做会将PlayArea的系数从 15.85 减少到 12.56,我们可以说,“游乐区对访问时长的影响是 12.56 分钟 对我们数据中平均客户年龄的影响。” 同样,因为PlayArea是一个二元变量,β[a1]通常是没有游乐区客户的线斜率。您可以通过颠倒PlayArea的级别(即将 1 或“Y”设置为默认值,0 或“N”设置为变更值,具体取决于二元变量的构成方式)来改变这一点:

## R
> centered_data <- hist_data %>%
    mutate(play_area = factor(play_area, levels=c('1','0')))
## Python
centered_data_df['play_area'] = centered_data_df['play_area']

改变PlayArea的默认级别将β[a1]的系数从 0 改变到−0.07(在图 11-24 中上部实线的斜率)。

你应该如何设置变量的默认值?这取决于手头的业务问题以及变量的性质:

  • 在某些情况下,数值变量的相关默认值不是平均值,而是最小值、最大值或相关子组的平均值(例如,有孩子客户的平均年龄,或者有游乐区店铺客户的平均年龄与全局平均值相比)。特别是在处理诸如儿童数量或呼叫数量等计数变量时,零可能比平均值更好作为参考点。

  • 对于二元变量,相关默认值通常是现状。例如,在考虑增加新游乐区时,您将为PlayArea设置默认值为 0,而在考虑现有游乐区时,您将为其设置 1。

  • 对于如性别或州等分类变量,除非有一个有意义的参考点,你可以将其默认设置为最常见的类别。

为涉及适度的所有变量设置有意义的参考点具有计算简单性的优势,通常也很直接。然而,随着涉及变量数量的增加,这种方法可能会迅速变得繁琐。例如,假设我们将性别和州之间的交互添加到我们的回归模型中:

VisitDuration = β[1] + β[p1].PlayArea + β[a1].Age + β[pa1].(PlayArea * Age) + β[g1].Gender + β[s1].State + β[gs1].(Gender * State)

现在β[p][1]代表参考年龄和性别、参考状态下的顾客对游乐区的影响。“对于 43 岁加州女性顾客,游乐区的影响为 10”已经有点啰嗦了,如果变量不独立的话,可能会变得毫无意义:我们有比男性更多的女性顾客,我们在加州的顾客比其他州多,总体平均年龄为 43 岁并不意味着我们有很多 43 岁的女性加州顾客,甚至一个也没有。这就引出了另一种解决方案,在我们的样本中计算平均效果。

在业务决策层面计算效果

从科学的角度来看,前一种方法的主要优势之一是它提供了一个系数的单一数字,这个数字可以推测地应用于完全不同的情况,或者至少可以与其他情况中得到的数字进行比较。然而,应用分析的目标并不是为了测量而测量,而是为了指导业务决策(如果你正在进行数据分析,但不知道可能出现什么决策,那么你需要与你的经理交流,因为你们中至少有一个人的工作做得不对)。因此,另一种替代方法是计算您感兴趣的效果变量的价值,无论有没有这个决定。

比如说,C-Mart 眼下需要做的决定是在哪个店铺设置游乐区。为了回答这个问题,我们无需确定游乐区的单一“平均”效果,实际上这样做反而会事倍功半。相反,对于今天没有游乐区的每个店铺,我们可以直接确定如果添加游乐区,额外平均访问时长会增加多少。流程如下(标注数字在 R 和 Python 中通用):

## Python (output not shown)
def business_metric_fun(dat_df):
    model =  ols("duration~play_area * (children + age)", data=dat_df) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png)
    res = model.fit(disp=0)
    action_dat_df = dat_df[dat_df.play_area == 0].copy() ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)          
    action_dat_df['pred_dur0'] = res.predict(action_dat_df) ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)  
    action_dat_df.play_area = 1 ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)                                  
    action_dat_df['pred_dur1'] = res.predict(action_dat_df) ![5](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/5.png)        
    action_dat_df['pred_dur_diff'] = \  ![6](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/6.png)                           
        action_dat_df.pred_dur1 - action_dat_df.pred_dur0
    action_res_df = action_dat_df.groupby(['store_id']) \  ![7](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/7.png)  
        .agg(mean_dur_diff=('pred_dur_diff', 'mean'), 
             tot_dur_diff=('pred_dur_diff', 'sum'))
    return action_res_df
action_res_df = business_metric_fun(hist_data_df)
action_res_df.describe()
## R
business_metric_fun <- function(dat){
  mod_model <- lm(duration~play_area * (children + age), data=dat) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/1.png) 
  action_dat <- dat %>%
    filter(play_area == 0) ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/2.png)   
  action_dat <- action_dat %>%
    mutate(pred_dur0 = predict(mod_model, action_dat)) %>%  ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/3.png)
    mutate(play_area = factor('1', levels=c('0', '1'))) ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/4.png)  
  action_dat <- action_dat %>%
    mutate(pred_dur1 = predict(mod_model, action_dat)) %>% ![5](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/5.png)
    mutate(pred_dur_diff = pred_dur1 - pred_dur0) %>%  ![6](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/6.png) 
    dplyr::group_by(store_id) %>%  ![7](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/bhv-dt-anls-r-py/img/7.png)   
    summarise(mean_d = mean(pred_dur_diff), sum_d = sum(pred_dur_diff))
  return(action_dat)}
action_summ_dat <- business_metric_fun(hist_data)
summary(action_summ_dat)

    store_id      mean_d          sum_d       
 3      : 1   Min.   :10.41   Min.   :109941  
 4      : 1   1st Qu.:11.26   1st Qu.:129817  
 5      : 1   Median :11.80   Median :143079  
 7      : 1   Mean   :11.95   Mean   :144616  
 8      : 1   3rd Qu.:12.25   3rd Qu.:155481  
 9      : 1   Max.   :14.43   Max.   :207647  
 (Other):27

1

运行并保存模型以便用于预测。

2

选择今天没有游乐区的店铺。

3

在当前情况下添加预测访问时长pred_dur0

4

将二进制变量PlayArea0更改为1

5

确定添加游乐区后的预测访问时长pred_dur1

6

计算两者之间的差异。

7

在店铺级别进行聚合,可以查看额外平均或总时长(平均更直观,但总数与业务结果更直接相关,因为它有利于较大的店铺)。

我们随后可以选择具有游乐区最高收益的商店。关键是,您可以自行检查,在过程的开始处将数值变量居中会导致完全相同的最终结论。从数学上讲,这是因为我们正在从差异中减去两个项中的相同数量:

(VisitDuration[i1] - mean(VisitDuration)) - (VisitDuration[i][0] - mean(VisitDuration)) = VisitDuration[i][1] - VisitDuration[i][0]

这里 VisitDuration[i][1] 和 VisitDuration[i][0] 分别是预测的访问持续时间,无论是否有游乐区(不考虑当前条件)。因此,从决策中心的角度来看,参考点和居中,或缺乏居中,都是无关紧要的,您不必再担心这一点。

总结一下:添加调节项会改变所涉变量的各个系数的值和解释。这是因为调节的定义是这些系数在“任何地方”都不相同,而调节会改变它们被测量的基准值。因此,必须将各个系数的解释视为与所涉变量的相关参考点(通过居中可以调整)或整个数据集相关,但与手头的决策相关。

结论

行为科学的一个关键原则之一是“行为是个人和环境的函数”。通常认为这句话意味着我们可以通过改变环境来影响行为。这当然是真的,但我也喜欢把它看作是一个提醒,即平均值只是平均值,勤奋的行为分析师应该通过调节分析进行深入挖掘。特别是,我认为最近一些经典心理学实验未能复制的失败最好不是解释为“没有效果”,而是“效果受到人口特征(即个人)和实验条件(即环境)的强烈调节”。

这可能看起来只是学术上的问题,但事实并非如此。在商业环境中,许多双方都拿出了以案例为支持证据的激烈讨论,这些讨论可以有益地以调节的方式重新构建。东海岸真实的情况在西海岸可能并不适用。一个培训计划可能对经验不足的员工有效,但对有经验的员工无效,反之亦然。根据定义,没有调节的回归将无法阐明这两种情况中的任何一种。

这使得调节分析成为行为分析工具中非常有价值但经常被忽视的补充。在本章中,我们看到它可以在观察和实验数据中非常简单地应用:在回归模型中添加两个变量之间的乘法项。在下一章,也是最后一章,我们将转向另一个关键的行为数据分析工具,中介分析。

^(1) 插字符号是你键盘上 6 键的符号。

^(2) 在 R 中作为onewaytests packagebf.test()函数提供,并在 Python 中作为scipy包的stats.levene()函数提供,其中参数center='median'

第十二章:调解和工具变量

在上一章中,我们看到调节能够通过揭示特定群体中关系更强或更弱的特征,打开因果关系的黑匣子。调解指的是链条中两个变量之间存在中介变量的情况;它通过理解起作用的因果机制——因果效应的“如何”,提供了另一种探索黑匣子的方式。

这对我们框架中因果和行为两方面都有多重好处。从因果角度看,调解减少了误报的风险,如果不充分考虑它可能会使我们的分析产生偏差。从行为角度看,调解帮助我们更好地设计和理解实验。在某种程度上,调解并不新鲜,本章中的大多数论点都可以总结为“尽可能扩展您的 CD 链,至少在开始时如此”。但我认为这样的简化会对你不利,因为寻找调解因素是许多科学发现的根源之一。“但为什么?”是确认两个变量之间因果关系后最好的后续问题之一。客户满意度增加了留存率,但为什么?是因为它减少了寻找替代品的可能性,还是因为它提高了客户对公司的看法?

调解还为本书中最后一个工具,即工具变量(IVs),提供了一个很好的过渡。IVs 可以像激素一样增强调解,使我们能够回答否则难以解决的问题。正如本书开头所承诺的,我们将获得关于客户满意度对后续购买行为影响的无偏估计,而 IVs 使这成为可能。

在接下来的部分,我将在 C-Mart 商店游乐区示例(来自第十一章)的背景下向你介绍调解,并展示调解如何使因果行为分析更加有效。然后在第二部分,我们将进入盛大结局:工具变量。

调解

让我们继续使用上一章的 C-Mart 示例,假设 C-Mart 现在有兴趣测量游乐区对杂货购买的影响(图 12-1)。

我们感兴趣的关系

图 12-1. 我们感兴趣的关系

根据先前的分析,C-Mart 的管理层认为访问持续时间是这种关系的关键因果机制。也就是说,他们认为PlayArea导致VisitDuration,进而导致GroceryPurchases。显然,我们可以忽略这一假设,并直接分析图 12-1 中显示的关系,建立相应的回归、置信区间等。然而,我们将确认并测量这一因果机制,因为考虑它具有多重好处:

  • 通过调解,我们可以理解其中的机制,并生成可操作的见解。

  • 在某些情况下,不考虑调解可能会导致因果估计的偏误。

让我们在接下来的两个小节中更详细地回顾这些好处,然后我将进入测量调解的技术考虑。

理解因果机制

辨识和测量调解的首个好处是提供所涉因果机制的解释。相关性并非总是因果关系,但从行为角度理解实际发生的事情是对虚假相关的强有力保护。如果你有两个相关的变量,但不确定这种相关是否是因果关系,找到并验证其间的中介者提供了非常强有力的证据表明这种关系是因果的。此时,错误的最可能来源是逆向因果关系——因果性的方向相反。 (另一种可能性是这三个变量恰好分别与其他两个存在虚假相关性;这是三个与随机事件的虚假相关性,而不是一个,这是一个极不可能发生的事件。)

调解在发现和设计阶段都是很有效的补充。在发现阶段(我在上一章中称之为“钓鱼远征”),识别可能的中介者可以帮助头脑风暴过程。即使中介者不可观察(例如信念或情感),仅仅考虑它可能会导致找到可测量的调节因素,并为其提供合理性。我们在第十一章中看到,PlayAreaGroceryPurchases之间的关系受Children的调节,这在直觉上是立即理解的。然而,认识到这种关系是通过VisitDuration调解的可以给我们提供其他可能的调节因素的线索。例如,如果调解是完全的,那么在接近闭店时间的访问中,总体效果可能会较弱,这是如果我们忽视VisitDuration的角色将不会立即显现的事情(图 12-2)。

PlayArea 和 GroceryPurchases 之间的关系

图 12-2. PlayArea 和 GroceryPurchases 之间的关系

我们可以通过检查PlayAreaGroceryPurchases的影响在接近闭店时间开始的访问中是否较弱来探索这一假设,这将进一步提供关于效果性质的信息。

仲裁也可用于设计或改进业务流程和信息传递。太多时候,分割练习以“A 段显示比 B 段更强烈的效果”而告终。这只在更好地定位预先存在的治疗方法(如营销活动)方面有用。理解调节作用的机制为构思提供了有用的见解。确认儿童游乐区的调节效应由访问持续时间中介,我们可以通过提供额外的停车验证或靠近游乐区的小吃站等福利来支持它。我们还可以用(可能更便宜的)迷你电影院替换游乐区,放映卡通片。

因果偏差

仲裁不仅仅是一个“好用”的工具。在某些情况下,不考虑它可能会引入我们因果估计中的偏差。

最简单的情况是,当我们试图衡量一个变量对另一个变量(总体)的影响时,但在回归中无意中包括一个中介作为控制变量时,就会发生这种情况。假设我们已经在个体水平上测量了在第二章建议的方向上,在超市购买的影响。可以想象,游乐区不仅影响了无论如何都会来的客户的访问时间,还吸引了新的客户。为了考虑这种因果路径,我们需要重新设计我们在商店水平上的 CD(图 12-3)。

在商店水平上重新设计 CD

图 12-3. 在商店水平上重新设计 CD

注意,尽管顾客水平上的平均购物金额完全由平均访问持续时间中介,但在商店水平上的总杂货销售情况并非如此。您测量行为的级别是有影响的!

在图 12-3 中,很明显顾客数游乐区杂货销售影响的中介。然而,我们很容易想象,调查这种效果的人会决定在回归中包括顾客数作为控制变量:杂货销售 = β[p].游乐区 + β[c].顾客数。毕竟,商店的顾客基数大小肯定会影响其总杂货销售量,并且可能会影响选择建立游乐区的商店(图 12-4)。

商店的顾客基数是混杂因素

图 12-4. 商店的顾客基数是混杂因素

图 12-4 提出了一个因果难题:商店的顾客基数,即那些离商店购物距离内的潜在顾客数量,可能是 PlayAreaGrocerySales 之间关系的混杂变量。但与此同时,CustomersNumber 是这种关系的一个介导变量。这种情况需要对相关的回归进行深思熟虑的控制——也就是说,所有依赖变量为 CustomersNumberGrocerySales 的回归。

可以通过在这些回归中添加周到的控制变量来实现这一点。例如,安装游乐区前一年的顾客数量是顾客基数的一个很好的代理变量,并且根据定义不会捕捉到安装游乐区的任何影响。或者,我们可以选择另一个代理变量,比如 Rural/Urban 的分类变量。

再次说明“包罗万象”方法对变量包含的危险性:仅仅因为当前顾客数量在我们的情况下可用且相关,并不意味着它应自动包含为控制变量。

识别介导作用

当我在 第三章 中介绍 CD 的基本组成部分时,我提到 mediator 是一个链条中连接两个其他变量的变量,如 图 12-5 中所示。

有游乐区对杂货购买的影响通过访问持续时间介导

图 12-5. 有游乐区对杂货购买的影响通过访问持续时间介导

VisitDurationPlayArea 的效应,也是 GroceryPurchases 的原因。因此,在这个 CD 中,它是 PlayAreaGroceryPurchases 的影响的介导变量。

假设 图 12-5 中的 CD 完全揭示了整个故事,PlayAreaGroceryPurchases 没有其他影响,除了通过 VisitDuration 的路径;在保持 VisitDuration 恒定的情况下改变 PlayArea 不会改变 GroceryPurchasesPlayAreaGroceryPurchases 的影响被称为“完全”或“完全介导”。或者,我们可以想象 PlayArea 除了通过 VisitDuration 路径之外,对 GroceryPurchases 还有直接影响的情况(图 12-6)。

部分介导

图 12-6. 部分介导

这被称为“部分”介导。即使 VisitDuration 不能完全解释 PlayAreaGroceryPurchases 的总效应,它仍然是一个介导变量。从 PlayAreaGroceryPurchases 的直接效应可能是真正的直接效应——没有中介变量参与——或者它可能代表我们不知道或不感兴趣分析的一个或多个其他介导变量(在这种情况下,我们已经合并了相应的链条)。

就像寻找潜在的调节变量一样,寻找中介者也是一个试探性和试错性的寻找过程,但是搜索范围(因此误报风险)要小得多。由于只有在行为上有强有力的理由支持时,才应该调查潜在的中介者,因此候选者的数量是明显受限的。此外,确认中介者涉及多次回归分析,这也降低了误报的风险。

鉴于考虑到中介者的好处和不考虑中介者的风险,您应该始终在分析中至少作为第一步包括相关的中介者。一旦确定了要分析的链条和要忽略的链条,您就可以安全地折叠后者(例如,在分析中只起次要作用的变量之间的中介者)。

与调节变量类似,搜索过程包括找到一个潜在的候选项,然后通过 Bootstrap 置信区间来验证它。在我们的例子中,VisitDuration是一个明显的候选项,因此让我们看看我们如何确认和测量其中介作用。

测量中介

测量中介是直接的但有点繁琐。它归结为运行多次回归分析来估计以下内容:

  • PlayAreaGroceryPurchases的总效应

  • PlayAreaGroceryPurchases的效应,即通过VisitDuration介导的间接效应

  • PlayAreaGroceryPurchases的效应,即不通过VisitDuration介导的直接效应

如果我们找不到间接、介导的路径的证据,我们应该否定我们的暂定中介者。相反,如果我们找不到直接路径的证据,那么效应就是完全通过中介实现的。百分之百的总效应通过中介实现 是总结这些证据的常见和有用方式。我们将通过考虑中介者是一个二元变量的特殊情况来结束这一部分。

总效应

我们首先通过在回归中不包括VisitDuration的情况下对PlayAreaGroceryPurchases进行回归来确定总效应:

## R (output not shown)
summary(lm(grocery_purchases~play_area, data=hist_data))
## Python
ols("grocery_purchases~play_area", data=hist_data_df).fit().summary()
...
          coef       std err    t           P>|t|   [0.025  0.975]
Intercept 49.1421    0.047      1036.494    0.000   49.049  49.235
play_area 27.6200    0.079      349.485     0.000   27.465  27.775
...

总效应约为 27.6,这意味着增加游乐区平均会使购物支出增加 27.6 美元,不持续访问时长恒定

中介效应

PlayAreaGroceryPurchases的影响,通过VisitDuration介导,可以通过将PlayAreaVisitDuration的影响和VisitDurationGroceryPurchases的影响相乘得到。这是直观的:如果游乐区增加了平均访问时间 X 分钟,并且每增加一分钟的访问时间就会使购物支出增加 Y 美元,那么增加游乐区将使购物支出增加 X * Y 美元。

第一个回归分析是关于PlayAreaVisitDuration之间的箭头。它产生的系数约为 12.6(增加游乐区大约会使平均访问时长增加约 12.6 分钟):

## R (output not shown)
summary(lm(duration~play_area, data=hist_data))
## Python
ols("duration~play_area", data=hist_data_df).fit().summary()
...
    coef  std err    t    P>|t| [0.025      0.975]
Intercept 23.8039    0.018 1287.327   0.000  23.768  23.840
play_area 12.5570    0.031 407.397    0.000  12.497  12.617
...

第二个回归是针对 VisitDurationGroceryPurchases 之间的箭头。然而,在这个回归中,我也会包括 PlayArea。回顾一下 图 12-6 并记住混淆变量的定义,我们可以看到如果中介仅为部分中介(即,从 PlayAreaGroceryPurchases 存在直接箭头),那么 PlayAreaVisitDurationGroceryPurchases 之间关系的一个混淆变量。因此,它必须被默认包含在回归中。运行一个回归,将我们的主要原因和中介变量作为解释变量,分别得到系数为 0.16(增加一个游戏区域会使每次访问的平均杂货购买额增加约 $0.16,保持访问持续时间恒定)和 2.2(将访问持续时间增加一分钟会使每次访问的平均杂货购买额增加约 $2.20):

## Python (output not shown)
ols("grocery_purchases~duration+play_area", data=hist_data_df).fit().summary()
## R
summary(lm(grocery_purchases~duration+play_area, data=hist_data))
...
Coefficients:
             Estimate Std. Error  t value Pr(>|t|)    
(Intercept) -2.917728   0.047329  -61.647  < 2e-16 ***
duration     2.187025   0.001695 1290.410  < 2e-16 ***
play_area1   0.157477   0.046419    3.393 0.000693 ***
...

在这个非常简单的例子中,这三个是唯一涉及的变量,但在现实生活中,你还必须在每个回归中包括任何向因变量的箭头的其他变量。

我之前提到,主要原因应该“默认情况下”包含在回归中。然而,有时主要原因和中介变量可能相关性非常密切,以至于在回归中同时包含它们会产生多重共线性。这通常是由于完全中介引起的,表现为系数方向相反(即,主要原因和中介大多相互抵消)且 p 值很大的可疑大系数。在最糟糕的情况下,你的分析软件甚至可能放弃并返回一个错误消息,而不是完成回归。每当将主要原因包含在回归中会使中介变量的系数变得混乱时,请不要包含主要原因。

最后,你可能还会遇到更复杂的情况,比如两个中介之间有额外的箭头,即,其中一个中介也是另一个中介的原因(图 12-7)。这不仅仅是一个理论上的可能性;这实际上时有发生,特别是在行为数据中。像那样的情况无法使用捷径,你需要记住我们在 第二部分 学到的内容:解决混淆的背门标准和其他因果图规则仍然适用,并会告诉你应该包括和不应该包括在回归中的变量。

其中一个中介影响另一个中介

图 12-7. 其中一个中介影响另一个中介

被中介的效应等于沿着中介链的两个系数的乘积(即,VisitDurationPlayArea 回归的系数和 GroceryPurchasesVisitDuration 回归的系数):

MediatedEffect ≈ 12.6 * 2.2 ≈ 27.5

从那里,我们可以计算出被中介的总效应的百分比:

中介百分比 = 中介效应 / 总效应 ≈ 27.5 / 27.6 ≈ 99.5%

这个百分比的自举 90%置信区间大约是[0.9933; 0.9975]。

直接效应

直接、未中介效应等于在GroceryPurchases回归中PlayAreaVisitDuration的系数:未中介效应 ≈ 0.16。通过这种方法,我们可以计算未中介的总效应百分比:

未中介百分比 = 未中介效应 / 总效应 ≈ 0.16 / 27.6 ≈ 0.5%

换句话说,从技术上讲,总效应并没有完全中介。然而,出于实际目的,我们可以忽略未中介的效应。请注意,“未中介效应”始终是与特定中介物相关联的。如果您有两个中介者共同完全中介了总效应,第一个的中介效应将等于第二个的未中介效应,反之亦然。

在遇到多重共线性情况并且无法像前文描述那样将主要原因包含在回归中时,效应很可能是完全中介的。您可以通过计算未中介的总效应百分比来确认这一点:

未中介百分比 = (总效应 - 中介效应) / 总效应

如果您通过这种方法获得了经济上显著的未中介效应,这意味着您手头的因果结构比您想象的更加复杂。那么是时候以批判的眼光重新审视您的 CD 了。也许您的主要原因和中介者有进一步的共同原因?或者可能存在几个中介者之间的相互关系?

注意

在这个例子中,中介者与其原因和效应都呈正相关。如果沿链的两个系数中有一个是负的,中介者也可能产生负效应。如果发生这种情况,并且我们的原因和感兴趣的效应之间有另一个中介者或直接效应,那么中介者将减少总效应。在这种情况下,您的第一个中介者可能代表总效应的-25%,而直接效应(或另一个中介者)将代表总效应的 125%,但效应比例的总和仍将是 100%。这是完全正常和预期的,所以不要让它困扰你。

如果你觉得这很令人困惑,那你不是一个人。社会科学家们几十年来一直在辩论是否将显著总效应作为中介分析的先决条件。但有许多完全合法的调节或自我调节现象,中介物可以补偿直接效应。例如,C-Mart 可能发现其价格变动并未像预期那样影响其销量,因为这些变动引起了竞争对手的相应价格变动。

当中介物是二元变量时

在这个例子中,中介是一个数值变量,所以我们可以通过乘以涉及的两个箭头的系数来轻松获得中介效应。当中介是一个二元变量时,仍然可以通过分析量化中介效应,即通过使用方程,但公式变得更加复杂。

如果我们称兴趣的原因为 X,中介为 M,感兴趣的效果为 Y,则中介和最终效果的回归方程变为:

P(M = 1) = logistic(α[0] + α[X].X)

Y = β[0] + β[X].X + β[M].M

请注意,第一个方程现在是一个逻辑回归,适用于二元变量。与线性回归预测 M 的值不同,我们现在预测它将取值 1 的概率,P(M = 1)。我们可以在第二个方程中替换该概率:

Y = β[0] + β[X].X + β[M].P(M = 1)

直接影响仍然很容易计算:如果 X 增加 1,直接效应增加 β[X] 的 Y。但是间接效应现在面临额外挑战,因为 X 对 M 的影响不是线性的。因此,必须确定 X 对 M 的影响在某个 X 的值上。这个问题类似于我们在第十一章中遇到的调节问题,可能的解决方案也是相同的:

  • 定义一个全局参考点,例如我们数据中 X 的平均值。

  • 或者计算我们数据中每一行的中介效应和中介百分比,然后计算它们各自的平均值。

如同在第十一章中的情况一样,我推荐第二种方法,根据需要修改以适应手头的业务决策。

工具变量

中介本身代表了行为数据分析工具箱的重要补充,但它也是通向另一个强大工具称为工具变量(IVs)的基石。简而言之,IVs 利用已知的中介关系来减少我们系数中的混杂偏倚。

最强大的用例之一是使用实验来回答更广泛,通常更难的问题。我将通过一个涉及顾客满意度的例子来说明这个应用,这是最受关注的业务指标之一,但也公认是最难测量的。

正如在第二章中首次提到的,AirCnC 的领导希望了解客户满意度(CSAT)对其关键绩效指标之一——预订后六个月内支出金额(M6Spend)的影响。我们将重复使用第十章中的实验数据,该实验探讨了呼叫中心程序变更对客户满意度的影响:“与其在出现问题时反复道歉,呼叫中心代表应在互动开始时道歉,然后进入‘解决问题模式’,最后提供多个选择给客户。”

数据

本章的GitHub 文件夹包含了第十章中实验数据的副本。这次我们将在分析中包含M6Spend变量。表 12-1 概述了本章数据中的变量。

表 12-1. 我们数据中的变量

变量描述 chap10-experimental_data.csv
Center_ID 10 个呼叫中心的分类变量
Rep_ID 193 名呼叫中心代表的分类变量
Age 客户呼叫时的年龄,20-60
Reason 呼叫原因,“付款”/“物业”
Call_CSAT 客户对通话的满意度,0-10
Group 实验分配,“ctrl”/“treat”
M6Spend 预订后六个月内的支出金额

软件包

在本节中,我们将使用以下特定软件包进行工具变量:

## Python
from linearmodels.iv import IV2SLS
## R
library(ivreg)

理解和应用 IV

一旦您熟悉了 CD 和中介,理解 IV 背后的思想就变得相对简单了:

假设两个变量之间存在完全中介关系,并且中介与最终变量之间的关系存在混杂因素。那么,您可以通过将总效应的系数除以第一个中介的系数(即第一个变量与中介之间的关系)来获得该关系的无偏估计。

看看我们的示例是什么样子,让我们首先绘制我们感兴趣的变量的 CD。我们想要衡量CSATM6Spend之间的因果关系。高 CSAT 可能会增加接下来几个月内的预订金额,但这种关系也受到未测量的混杂因素的影响,包括个性特征如开放性。最后,我们有关于我们的实验处理的数据,我们知道这影响了CSAT,感谢我们在第十章的实验(图 12-8)。

我们感兴趣的变量的 CD

图 12-8. 我们感兴趣的变量的 CD

正如您所看到的,在这个 CD 中,CSATGroupM6Spend 之间的一个中介,但 CSATM6Spend 之间的关系被 Openness 的混杂效应上升偏置。

在理想的情况下,我们会有关于 开放性 变量的数据,并且能够运行两个真实的回归(方程 12-1 和 方程 12-2)。

方程 12-1.

CSAT = β[g][1].Group + β[o1].Openness

方程 12-2.

M6Spend = β[c][2].CSAT + β[o][2].Openness

但是,即使我们确实获得了有关开放性的数据(例如,通过调查),我们怎么能确定是否有另一个混杂因素潜伏在角落里呢?

从数学上稍微退后一步,这个问题将我们带到了行为分析的核心:客户满意度是业务成功的重要标准,但由于它受到影响个体行为的无数未观察到的个人特征的强烈影响,我们无法希望识别和控制所有这些特征。这个问题在因果行为框架之外无法令人满意地解决,但在其中处理起来很简单。

回到数学上——让我们按照之前阐述的直觉进行思考:

  1. 计算最左边关系的系数,即 GroupCSAT 之间的关系。

  2. 计算 GroupM6Spend 的总效应的系数。

  3. 通过将步骤 2 的总效应除以步骤 1 的最左边关系的系数,计算 CSATM6Spend 的效应的系数。

步骤 1:最左边关系

因为 Group 是随机分配的,与 Openness 不相关,所以我们可以运行以下回归而不是 方程 12-1:

CSAT = β[g][1].Group

我们对 β[g][1] 的估计是无偏的,并且可以根据需要插入到 方程 12-1 中,因为它是真正的因果系数。

步骤 2:总效应

总效应的方程(方程 12-3)被称为 简化 回归(我们将其索引为“r”),因为它折叠了变量之间的链条。

方程 12-3. 方程 R

M6Spend = β[gr].Group (Eq. R)

对于调解的第一个臂的同样原因,我们对 β[gr] 的估计是无偏的。

步骤 3:感兴趣的关系

这就是魔术发生的地方:正如在关于调解的前一部分讨论的那样,β[gr] = β[c][2] * β[g][1]。我们可以将这个方程重写为 β[c][2] = β[gr] / β[g][1]。因为等号右边的所有变量都是无偏的,左边的一个也是无偏的。对于 β[c][2] 的这个估计是无偏的。

换句话说,如果我们能找到一个(称为 工具)的变量,它是我们感兴趣的原因的原因,但与混杂因素和感兴趣的效应无关,我们可以消除两个变量之间的关系:

  • 第一个条件(称为 独立性假设)是为了使简化后的回归不受偏斜,但幸运的是,对于随机分配,它总是成立的。

  • 第二个条件(称为 排除限制)可以重新表述为说仪器与感兴趣的效果之间的关系必须完全由感兴趣的原因进行中介。它对于方程 β[gr] = β[c][2] * β[g][1] 必须成立。不幸的是,这不能在数学上证明(这将需要知道第二个中介系数,这就是我们要找的!)必须基于定性因果考虑进行假设——例如,在这种情况下,实验组分配不太可能影响 M6Spend 除了 CSAT 链以外的任何因素。

测量

那就是直觉。我们当然可以手工计算所有相应的回归,但正如我们这些被宠坏了的二十一世纪的数据分析师所期望的那样,有一个包可以做到这一点。

首先,我们将通过运行第一中介的线性回归和总效应的线性回归来进行两项合理性检查(即,简化方程)。如果其中任何一个的系数非常接近零(由 Bootstrap CI 决定),那么这将危及我们的 IV 回归。在大多数情况下,您将希望包括一些其他与您感兴趣的变量有因果关系的协变量。在我们来自第十章的例子中,Age(呼叫者的年龄)和 Reason(呼叫原因)预测了 CSAT 并与 Group 一起包括。我们也应该在这里包括它们:

## Python (output not shown)
ols("call_CSAT~group+age+reason", data=exp_data_df).fit(disp=0).summary()
ols("M6Spend~group+age+reason", data=exp_data_df).fit(disp=0).summary()
## R
summary(lm(call_CSAT~group+age+reason, data=exp_data))
summary(lm(M6Spend~group+age+reason, data=exp_data))
..
Coefficients:
               Estimate Std. Error t value Pr(>|t|)    
(Intercept)    4.103826   0.011790  348.07   <2e-16 ***
grouptreat     0.540633   0.006291   85.94   <2e-16 ***
age            0.020202   0.000280   72.14   <2e-16 ***
reasonproperty 0.200590   0.006600   30.39   <2e-16 ***
...
Coefficients:
               Estimate Std. Error  t value         Pr(>|t|)    
(Intercept)    99.93195    0.43976  227.242          < 2e-16 ***
grouptreat      1.61687    0.23465    6.891 0.00000000000557 ***
age            -1.46785    0.01044 -140.536          < 2e-16 ***
reasonproperty  0.44458    0.24615    1.806           0.0709 . 
...

幸运的是,两个系数都与零安全地不同,因此我们可以转向我们的 IV 回归。

Python 代码

在 Python 中,我们将使用 linearmodels 包:

## Python 
iv_mod = IV2SLS.from_formula('M6Spend ~ 1 + age + reason + [call_CSAT ~ group]', 
                             exp_data_df).fit()
iv_mod.params
Out[8]: 
Intercept             87.658610
reason[T.property]    -0.155326
age                   -1.528264
call_CSAT              2.990706
Name: parameter, dtype: float64

IV2SLS.from_formula() 函数的语法与 ols() 几乎相同。预测变量,我们感兴趣的效果,位于波浪号(“~”)的左侧,预测变量位于右侧,第一阶段回归写在括号中。有两件事需要注意:

  • 您需要明确地在预测变量中包括一个常数(“1”)。

  • 与您感兴趣的变量相关的其他协变量(在这里,AgeReason)也应在此处包括在外,而不是在括号内。请注意,它们将自动包括在第一阶段回归中。您的第一阶段回归公式只需要包括介质,也就是我们感兴趣的原因,以及仪器。

Python 函数的输出是有限的,但这就是我们所需要的。call_CSATM6Spend 的影响约为每单位 $2.99,比 M6Spendcall_CSAT 的天真、有偏回归少约 $1:

## Python
ols("M6Spend~call_CSAT+age+reason", data=exp_data_df).fit(disp=0).summary()
...
                     coef    std err      t     P>|t| [0.025  0.975]
Intercept           83.2283   0.536    155.302   0.000   82.178  84.279
reason[T.property]  -0.3582   0.245     -1.461   0.144   -0.839   0.122
call_CSAT            4.0019   0.076     52.767   0.000    3.853   4.151
age                 -1.5488   0.010   -147.549   0.000   -1.569  -1.528
...

无偏效果的 Bootstrap 90% CI 约为 [2.26; 3.89]。

R 代码

在 R 中,我们将使用 ivreg 包:

## R
> iv_mod <- ivreg::ivreg(M6Spend~call_CSAT + age + reason | group + age + reason, 
    data=exp_data)
> summary(iv_mod)

Call:
ivreg::ivreg(formula = M6Spend ~ call_CSAT + age + reason | group + 
    age + reason, data = exp_data)

Residuals:
   Min     1Q Median     3Q    Max 
-86.82 -35.01 -17.94  19.92 706.58 

Coefficients:
               Estimate Std. Error  t value         Pr(>|t|)    
(Intercept)    87.65861    1.93745   45.244          < 2e-16 ***
call_CSAT       2.99071    0.43165    6.929 0.00000000000426 ***
age            -1.52826    0.01358 -112.540          < 2e-16 ***
reasonproperty -0.15533    0.25968   -0.598             0.55    

Diagnostic tests:
                    df1    df2 statistic p-value    
Weak instruments      1 231655  7384.847  <2e-16 ***
Wu-Hausman            1 231654     5.667  0.0173 *  
Sargan                0     NA        NA      NA    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 56.12 on 231655 degrees of freedom
Multiple R-Squared: 0.0925,	Adjusted R-squared: 0.09249 
Wald test:  7019 on 3 and 231655 DF,  p-value: < 2.2e-16

幸运的是,ivreg软件包的作者们努力使ivreg()函数尽可能地与lm()相似,无论是在语法还是输出上。公式中唯一的区别在于现在有两个回归器的列表,用竖线“|”分隔。变量应放置在这里:

  • 感兴趣的原因,这里的call_CSAT,只出现在条的左侧。

  • 仪器(这里的Group)仅出现在条的右侧。

  • 其他解释变量对我们感兴趣的因果关系的贡献,这里的AgeReason,都出现在条中。

ivreg()的输出也与lm()的输出非常相似。我们感兴趣的值是call_CSAT的系数。ivreg()还返回 IV 的几个诊断结果,这些诊断结果测试我们模型中各种关系的强度。在这里,call_CSATM6Spend的影响约为每单位 2.99 美元。相应的 Bootstrap 90%置信区间约为[2.26; 3.89]。

注意

在我们的简单例子中,这两个估计之间的差异来自Openness。在现实生活中,我们不太可能能够自信地识别所有起作用的混杂因素,我们可以将其标记为“未知的心理因素”。然而,即使没有测量这些混杂因素,我们也可以通过CSAT来测量它们对M6Spend的影响:这些混杂因素的变化导致CSAT增加 1 分,也会导致M6Spend增加约 1 美元。现在我们可以进行调查来衡量开放性或任何其他未知的心理因素,有了这个知识,我们可以说,每点CSATM6Spend的混杂效应大约值得 1 美元。我们知道我们不知道什么,如果你问我的话,这挺酷的。

应用 IV:常见问题解答

在前面的例子中,我们利用 IV 来分析实验数据:利用实验数据去除因果关系中的混杂因素。这是它们最直接和有效的用途之一,但不是唯一的用途。一旦你熟悉了 IV,你会开始问自己:“还有什么其他的...?”为每种潜在用例构建完整的例子会显得多余,但指出最常见的用法是值得的:

我能在纯观察数据下使用 IV 吗?

是的,过程与实验数据完全相同,但因为独立性假设在这种情况下并不是确定的,所以你需要确保它成立。我经常想出一个潜在的仪器,稍后才意识到它与最终效应之间还有另一个潜在联系隐藏在背后。

我能在具有二进制最终效应的情况下使用 IV 吗?

在这种情况下,系数之间的关系比线性回归复杂得多。R 包ivprobit()允许您使用工具变量进行概率回归,但据我所知,对于逻辑回归却没有这样的解决方案。

结论

我们到此为止。在本书的开头,我承诺我们将衡量客户满意度对业务指标的因果影响,我们也确实做到了:“客户满意度的每增加一单位,接下来六个月的支出增加 $2.99。”没有冗长的警示和脚注,没有“相关性不等于因果关系”的波折——这是一个清晰明了的结果。希望在你的思想中打开了一个全新的机会世界。客户满意度、忠诚计划会员、品牌认知度:衡量所有这些模糊和偏见概念对业务的影响已经在您的掌握之中。而且很可能,您已经拥有所需的数据。两年前市场营销部门是否进行了一个试验,给客户提供折扣,如果他们加入您的忠诚计划?那么只需提取相应的数据并应用一行公式进行 IV 回归。当然,这种简单性并非凭空而来。这是经过长时间的发展,因为它需要:

  • 对客户满意度和支出的明确定义和理解的变量,正如我们在第 I 部分中所见

  • 正确的因果图,正如我们在第 II 部分中发现的

  • 工具让我们能够处理不确定性,而无需记住一堆统计测试,正如我们在第 III 部分中所见

  • 精心设计和深入分析的实验,正如我们在第 IV 部分中所探讨的

  • 最后,对中介和调节的理解,正如我们在第 V 部分中所学

最后说一些稍微不那么正式的话:人们经常注意到,儿童对世界有着无穷的好奇心(“为什么天空是蓝色的?”)。当然,这种好奇心实际上并不是无限的,在某个时刻大多数孩子会停止问这么多问题。我真诚地希望这本书能在你心中重新点燃一些孩子般的好奇心——让你被世界(特别是人们)所迷惑,并且你会思考,“但为什么呢?”当然,在声称任何功劳之前,我必须认真考虑到我可能弄错了因果关系,而你可能从一开始就拥有这一切(图 12-9)。

我提到过相关性不等于因果关系吗?

图 12-9. 我提到过相关性不等于因果关系吗?

第十三章:参考文献

  • Aberson, Christopher L. 行为科学的应用功效分析。英国阿宾顿:Routledge, 2019。

  • Angrist, Joshua D., 和 Jörn-Steffen Pischke. 大部分无害的计量经济学:实证主义者的同伴。普林斯顿,新泽西州:普林斯顿大学出版社, 2009. 这是一本应用计量经济学的首选参考书,具备研究生水平的数学和统计基础。

  • Angrist, Joshua D., 和 Jörn-Steffen Pischke. 掌握“度量学”:从原因到效果的路径。普林斯顿,新泽西州:普林斯顿大学出版社, 2014. 这是他们 2009 年经典著作的更易读版本,增加了功夫熊猫的参考!

  • Antonio, Nuno, Ana de Almeida, 和 Luis Nunes. “酒店预订需求数据集。” 数据简报 22 (2019 年 2 月): 41-49. https://doi.org/10.1016/j.dib.2018.11.126

  • Bertrand, Marianne, 和 Sendhil Mullainathan. “艾米丽和格雷格比拉基沙和贾马尔更受雇用吗?劳动力市场歧视的实地实验。” 美国经济评论 94, no. 4 (2004): 991-1013。

  • Cohen, Jacob. 行为科学的统计功效分析, 第二版。英国阿宾顿:Routledge, 2013. 这是一本关于功效分析的经典著作。和大多数经典一样,它在计算机实现方面略显过时,但仍然是一个思考统计功效更深层次含义的重要资源。

  • Cunningham, Scott. 因果推理:混音带。康涅狄格州纽黑文:耶鲁大学出版社, 2021. 这是对 Angrist/Pischke 书籍的一种非常易读的变体,主要面向学术界。

  • Davison, A. C.,  和 D. V. Hinkley. 引导方法及其应用。英国剑桥:剑桥大学出版社, 1997。

  • Efron, Bradley, 和 R. J. Tibshirani. 引导的介绍。英国阿宾顿:Chapman and Hall/CRC, 1994. Efron 是引导方法的“发明者”,这本书为具备本科数学和统计学基础的人提供了一个良好的入门介绍。

  • Eyal, Nir.  成瘾:如何建立习惯形成的产品。纽约:Portfolio, 201.

  • Funder, David C. 人格之谜。纽约:W. W. Norton & Company, 2016. 这是一本极好的“高级介绍”,涵盖了人格心理学的范畴,填补了通俗科学与学术研究之间的差距。

  • Gelman, Andrew, 和 Jennifer Hill. 利用回归和多层次/分层模型进行数据分析。英国剑桥:剑桥大学出版社, 2006. 提供了对多层次(即分层)数据的更深入分析,但需要更高的数学和统计知识门槛。

  • Gerber, Alan S., 和 Donald P. Green. 现场实验:设计、分析和解释。纽约:W. W. Norton & Company, 2012. 这是我深入了解实验设计的首选书籍。它包含了大量的统计和数学公式,但对于任何具备一定数量级背景的人来说都应该可以理解。如果你想用统计检验分析实验数据,这是我推荐的资源。

  • Glennerster, Rachel,和 Kudzai Takavarasha. 随机评估的实践指南. 普林斯顿,新泽西州:普林斯顿大学出版社,2013 年。这是一个易于理解的入门书籍,介绍了在发展经济学和援助领域中的实验设计,深入探讨了在现实世界中进行实验的经验挑战(即非在线环境)。

  • Gordon, Brett R.,等。 “广告测量方法的比较:来自 Facebook 大型实地实验的证据。” 市场科学,INFORMS,38 卷,2 期(2019 年):193-225。

  • Hayes, Andrew F. 引导到中介、调节和条件过程分析:基于回归的方法,第二版。纽约:吉尔福德出版社,2017 年。

  • Hernan, Miguel. 因果图:在得出结论之前绘制假设edX.org 在线课程(访问日期 2021 年 5 月 6 日)。

  • Hubbard, Douglas W. 如何测量任何东西:在商业中找到无形资产的价值. 霍博肯,新泽西州:Wiley 出版社,2010 年。这是一个关于商业测量的深刻且易于理解的视角。

  • Jose, Paul E. 进行统计中介与调节。纽约:吉尔福德出版社,2013 年。尽管更加专注于学术研究并使用更加神秘的软件(也就是说,在学术界和相关领域之外神秘!),但这本书是对 Hayes(2017)的有用补充。

  • Josse, Julie,Nicholas Tierney,和 Nathalie Vialaneix. “CRAN 任务视图:缺失数据。” 综合 R 档案网络,https://cran.r-project.org/web/views/MissingData.html

  • Kahneman, Daniel. 思考,快与慢. 纽约:法拉尔,斯特劳斯和吉鲁克斯出版社,2013 年。这是行为科学领域的经典之作,作者是该领域的重要人物。

  • Little, Roderick J. A.,和 Donald B. Rubin. 带有缺失数据的统计分析,第三版。霍博肯,新泽西州:Wiley 出版社,2019 年。这本经典书籍的第三版由该领域的知名研究人员编写,对统计理论进行了全面审视,十分实用。

  • Meadows, Donella H. 系统思维入门. 白河谷:切尔西绿色出版社,2008 年。这是系统思维的优秀通俗介绍。

  • Pearl, Judea. 因果关系. 英国剑桥:剑桥大学出版社,2009 年。Pearl 的早期著作,涵盖了详细的研究生级数学内容。

  • Pearl, Judea,和 Dana Mackenzie. 为什么书:因果分析与因果图的新科学. 纽约:基本书籍出版社,2018 年。这是我迄今为止见过的最易理解的因果分析和因果图的入门书籍,作者是该领域的重要研究人员之一。

  • Senge, Peter M. 第五项修炼:学习型组织的艺术与实践. 纽约:货币出版社,2010 年。

  • Shipley, Bill。生物学中的因果与相关性:路径分析、结构方程和因果推断的用户指南,第 2 版。英国剑桥:剑桥大学出版社,2016 年。你不是生物学家?我也不是。尽管如此,这本书仍然帮助我深入理解因果图表,而且由于关于该主题的书籍数量有限,穷人不能挑剔。

  • Taylor, Aaron B., 和 David P. MacKinnon。"使用排列方法测试单一中介模型的四个应用。" 行为研究方法 44, no. 3 (2012 年 9 月): 806-44。

  • Thaler, Richard H., 和 Cass R. Sunstein。Nudge: 改善健康、财富和幸福的决策。纽约:Penguin,2009 年。

  • van Buuren, Stef。灵活插补缺失数据,第 2 版。英国阿宾登:Chapman and Hall/CRC,2018 年。作者使用 R 中的 mice 包进行了非常易懂的展示,并提供了大量具体的例子。作者在他的个人页面上免费提供了这本书,https://stefvanbuuren.name/fimd

  • VanderWeele, Tyler。因果推断中的解释:中介和交互方法。英国牛津:牛津大学出版社,2015 年。在因果分析文献和社会科学关于中介和调节的文献之间架起了一座优秀的桥梁。涵盖了大量其他参考书籍中找不到的更高级的材料。

  • Wendel, Stephen。行为变革设计:应用心理学和行为经济学,第 2 版。Sebastopol:O’Reilly,2020 年。

  • Wilcox, Rand R.。现代统计方法基础:显著提高功率和精度,第 2 版。纽约:Springer,2010 年。我们通常假设我们的数据足够正态分布。Wilcox 表明这是不合理的,并且可能严重偏离分析。这是关于一个高级主题的一本非常易读的书籍。

posted @ 2024-06-17 17:11  绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报