A/B测试

在现代数据分析中,判断两个数值样本是否来自同一个潜在分布的过程称为“A/B测试”。这个名称指的是两个样本的标签A和B。

我们将通过一个例子来展开这个方法。数据来自一个大型医院系统的新生儿样本。尽管抽样是分多个阶段进行的,但我们将把它视为简单随机样本。Deborah Nolan和Terry Speed合著的Stat Labs提供了关于更大数据集的详细信息,本数据集即从中抽取。

[In ]:
from datascience import *
path_data = '../../../assets/data/'
import numpy as np

import matplotlib
matplotlib.use('Agg')
%matplotlib inline
import matplotlib.pyplot as plots
plots.style.use('fivethirtyeight')

吸烟者与非吸烟者

表格 births 包含了1,174对母婴的以下变量:婴儿出生体重(盎司)、妊娠天数、母亲年龄(周岁)、母亲身高(英寸)、孕期体重(磅),以及母亲在怀孕期间是否吸烟。

[In ]:
births = Table.read_table(path_data + 'baby.csv')
births
Birth Weight | Gestational Days | Maternal Age | Maternal Height | Maternal Pregnancy Weight | Maternal Smoker
120          | 284              | 27           | 62              | 100                       | False
113          | 282              | 33           | 64              | 135                       | False
128          | 279              | 28           | 64              | 115                       | True
108          | 282              | 23           | 67              | 125                       | True
136          | 286              | 25           | 62              | 93                        | False
138          | 244              | 33           | 62              | 178                       | False
132          | 245              | 23           | 65              | 140                       | False
120          | 289              | 25           | 62              | 125                       | False
143          | 299              | 30           | 66              | 136                       | True
140          | 351              | 27           | 68              | 120                       | False
... (1164 rows omitted)

该研究的目的之一是观察母亲吸烟是否与出生体重相关。让我们看看关于这两个变量我们能得出什么结论。

我们首先只选择 Birth WeightMaternal Smoker 列。样本中有715名非吸烟母亲和459名吸烟母亲。

[In ]:
smoking_and_birthweight = births.select('Maternal Smoker', 'Birth Weight')
[In ]:
smoking_and_birthweight.group('Maternal Smoker')
Maternal Smoker | count
False           | 715
True            | 459

我们来比较一下非吸烟母亲和吸烟母亲所生婴儿的出生体重分布。为了生成两个叠加的直方图,我们将使用带有可选参数 grouphist 函数,该参数是一个列标签或索引。表格的行首先按此列分组,然后为每组绘制一个直方图。

[In ]:
smoking_and_birthweight.hist('Birth Weight', group = 'Maternal Smoker')
Two overlaping histograms are shown. The x-axis is labeled 'Birth Weight' and the y-axis is 'Percent per unit.' One histogram is shown in gold for 'Maternal Smoker=True' and has some bars that are taller than the other histogram's bars on the left. The second histogram is in dark blue for 'Maternal Smoker=False.' The blue bars are taller than the gold bars on the right hand side.

吸烟母亲所生婴儿的体重分布似乎略微向左偏移,而非吸烟母亲的分布则在右侧。吸烟母亲所生婴儿的平均体重似乎低于非吸烟母亲的婴儿。

这引发了一个问题:这种差异仅仅是随机变异,还是反映了更大总体中的分布差异?有没有可能总体中两个分布没有差异,而我们看到的样本差异仅仅是由于碰巧被选中的母亲造成的?

提出假设

我们可以通过假设检验来尝试回答这个问题。我们要检验的随机模型是:总体中没有潜在差异;样本中的分布差异仅由随机性造成。

形式上,这就是原假设。我们需要弄清楚如何在此假设下模拟一个有用的统计量。但作为开始,让我们先陈述两个自然的假设。

原假设: 在总体中,吸烟母亲和非吸烟母亲所生婴儿的出生体重分布相同。样本中的差异是由随机性造成的。

备择假设: 在总体中,吸烟母亲所生婴儿的平均出生体重低于非吸烟母亲的婴儿。

检验统计量

备择假设比较了两组的平均出生体重,并指出吸烟母亲组的平均值较小。因此,使用两组均值之差作为统计量是合理的。

我们按照“吸烟组平均体重 $-$ 非吸烟组平均体重”的顺序进行减法。该统计量的小值(即大的负值)将支持备择假设。

检验统计量的观测值约为 $-9.27$ 盎司。

[In ]:
means_table = smoking_and_birthweight.group('Maternal Smoker', np.average)
means_table
Maternal Smoker | Birth Weight average
False           | 123.085
True            | 113.819
[In ]:
means = means_table.column(1)
observed_difference = means.item(1) - means.item(0)
observed_difference
-9.266142572024918

在下面的模拟中,我们将反复计算这样的差异,因此我们将定义一个函数来完成这项工作。该函数接受两个参数:

  • 数据表的名称
  • 包含布尔分组变量的列标签

它返回 True 组和 False 组的均值之差。

你很快就会明白我们为什么要指定这两个参数。现在,只需检查函数是否返回了应有的结果。

[In ]:
def difference_of_means(table, group_label):
    """Takes: name of table,
    column label that indicates the group to which the row belongs
    Returns: Difference of mean birth weights of the two groups"""
    reduced = table.select('Birth Weight', group_label)
    means_table = reduced.group(group_label, np.average)
    means = means_table.column(1)
    return means.item(1) - means.item(0)

为了检查函数是否正常工作,我们用它来计算样本中两组平均出生体重的观测差异。

[In ]:
difference_of_means(births, 'Maternal Smoker')
-9.266142572024918

这与之前计算的 observed_difference 值相同。

在原假设下预测统计量

为了了解统计量在原假设下如何变化,我们需要弄清楚如何在该假设下模拟统计量。一种基于“随机置换”的巧妙方法正好可以实现这一点。

如果潜在总体中两个分布没有差异,那么一个出生体重在母亲吸烟方面标记为 TrueFalse 应该对平均值没有影响。因此,思路是在所有母亲中随机打乱所有标签。这被称为“随机置换”(random permutation)。

打乱标签确保了 True 标签的计数不变,False 标签的计数也不变。这对于模拟均值差与原始均值差的可比性很重要。在课程后面我们将看到,样本量会影响样本均值的变异性。

计算两个新组的均值差:母亲被随机标记为吸烟者的婴儿的平均体重,与其余母亲被随机标记为非吸烟者的婴儿的平均体重之差。这就是原假设下检验统计量的一个模拟值。

让我们看看如何实现。从数据开始总是个好主意。我们已经将表格缩减为仅包含我们需要的列。

[In ]:
smoking_and_birthweight
Maternal Smoker | Birth Weight
False           | 120
False           | 113
True            | 128
True            | 108
False           | 136
False           | 138
False           | 132
False           | 120
True            | 143
False           | 140
... (1164 rows omitted)

表格中有1,174行。要打乱所有标签,我们将不放回地随机抽取1,174行。这样样本将包含表格中的所有行,但顺序是随机的。

我们可以使用Table的方法 sample,并带上可选参数 with_replacement=False。我们不需要指定样本量,因为默认情况下,sample 会抽取与表格行数相同数量的样本。

[In ]:
shuffled_labels = smoking_and_birthweight.sample(with_replacement = False).column(0)
original_and_shuffled = smoking_and_birthweight.with_column('Shuffled Label', shuffled_labels)
[In ]:
original_and_shuffled
Maternal Smoker | Birth Weight | Shuffled Label
False           | 120          | True
False           | 113          | False
True            | 128          | False
True            | 108          | True
False           | 136          | False
False           | 138          | False
False           | 132          | True
False           | 120          | False
True            | 143          | True
False           | 140          | False
... (1164 rows omitted)

现在,每个婴儿的母亲在 Shuffled Label 列中有一个随机分配的吸烟/非吸烟标签,而原始标签在 Maternal Smoker 列中。如果原假设为真,所有标签的随机重新排列都应该具有相同的可能性。

让我们看看两个随机标记组的平均体重有多大差异。

[In ]:
shuffled_only = original_and_shuffled.select('Birth Weight','Shuffled Label')
shuffled_group_means = shuffled_only.group('Shuffled Label', np.average)
shuffled_group_means
Shuffled Label | Birth Weight average
False          | 119.277
True           | 119.752

两个随机选择组的平均值比两个原始组的平均值要接近得多。我们可以使用函数 difference_of_means 来找出这两个差异。

[In ]:
difference_of_means(original_and_shuffled, 'Shuffled Label')
0.4747109100050153
[In ]:
difference_of_means(original_and_shuffled, 'Maternal Smoker')
-9.266142572024918

但是,不同的打乱方式是否会导致组平均值之间有更大的差异?为了了解变异性,我们必须多次模拟这个差异。

和往常一样,我们首先定义一个函数来模拟原假设下检验统计量的一个值。这只需要将我们上面写的代码收集起来即可。

该函数名为 one_simulated_difference_of_means。它不接受参数,并返回通过随机打乱所有标签形成的两组平均出生体重之差。

[In ]:
def one_simulated_difference_of_means():
    """Returns: Difference between mean birthweights
    of babies of smokers and non-smokers after shuffling labels"""
    
    # array of shuffled labels
    shuffled_labels = births.sample(with_replacement=False).column('Maternal Smoker')
    
    # table of birth weights and shuffled labels
    shuffled_table = births.select('Birth Weight').with_column(
        'Shuffled Label', shuffled_labels)
    
    return difference_of_means(shuffled_table, 'Shuffled Label')   

多次运行下面的单元格,观察输出如何变化。

[In ]:
one_simulated_difference_of_means()
-0.058299434770034964

置换检验

基于数据随机置换的检验称为“置换检验”(permutation tests)。我们在这个例子中就在进行这样的检验。在下面的单元格中,我们将多次模拟检验统计量(两个随机形成的组的平均出生体重之差),并将这些差异收集到数组中。

[In ]:
differences = make_array()

repetitions = 5000
for i in np.arange(repetitions):
    new_difference = one_simulated_difference_of_means()
    differences = np.append(differences, new_difference)                               

数组 differences 包含了5,000个检验统计量的模拟值:在标签被随机分配的情况下,吸烟组平均体重与非吸烟组平均体重之间的差异。

检验的结论

下面的直方图展示了这5,000个值的分布。它是在原假设下模拟的检验统计量的经验分布。这是基于原假设对检验统计量的预测。

[In ]:
Table().with_column('Difference Between Group Means', differences).hist()
print('Observed Difference:', observed_difference)
plots.title('Prediction Under the Null Hypothesis');
Observed Difference: -9.266142572024918
Histogram titled 'Prediction Under the Null Hypothesis' with 'Difference Between Group Means' on the x-axis and 'Percent per unit' on the y-axis. The histogram is bell shaped ranging between -3 to 3 and centered around 0.

注意分布大致以0为中心。这很合理,因为在原假设下,两组的平均值应该大致相等。因此,组平均值之差应该在0左右。

原始样本中的观测差异约为 $-9.27$ 盎司,这个值甚至没有出现在直方图的横轴刻度上。统计量的观测值与原假设下统计量的预测行为不一致。

检验的结论是:数据支持备择假设而非原假设。这支持了吸烟母亲所生婴儿的平均出生体重低于非吸烟母亲所生婴儿的假设。

如果你想计算经验p值,请记住统计量的低值支持备择假设。

[In ]:
empirical_p = np.count_nonzero(differences <= observed_difference) / repetitions
empirical_p
0.0

经验p值为0,意味着在5,000个置换样本中,没有一个产生的差异达到-9.27或更低。这只是一个近似值。得到该范围差异的精确概率并非为0。但根据我们的模拟,它极其微小,因此我们可以拒绝原假设。

另一个置换检验

我们可以使用相同的方法来比较吸烟者和非吸烟者的其他属性,例如年龄。两组年龄的直方图显示,在样本中,吸烟的母亲往往更年轻。

[In ]:
smoking_and_age = births.select('Maternal Smoker', 'Maternal Age')
smoking_and_age.hist('Maternal Age', group = 'Maternal Smoker')
Two overlaping histograms are shown. The x-axis is 'Maternal Age' and the y-axis is 'Percent per unit.' The histograms are differentiated by color. In gold is 'Maternal Smoker=True' and in dark blue is 'Maternal Smoker=False.' The gold histogram appears shifted left compared to the dark blue histogram, and both have similar shapes.

平均年龄的观测差异约为 $-0.8$ 年。

让我们重写比较出生体重的代码,改为比较吸烟者和非吸烟者的年龄。

[In ]:
def difference_of_means(table, group_label):
    """Takes: name of table,
    column label that indicates the group to which the row belongs
    Returns: Difference of mean ages of the two groups"""
    reduced = table.select('Maternal Age', group_label)
    means_table = reduced.group(group_label, np.average)
    means = means_table.column(1)
    return means.item(1) - means.item(0)
[In ]:
observed_age_difference = difference_of_means(births, 'Maternal Smoker')
observed_age_difference
-0.8076725017901509

请记住,差异计算为吸烟者的平均年龄减去非吸烟者的平均年龄。负号表示吸烟者平均更年轻。

这种差异是由于随机性造成的,还是反映了总体中的潜在差异?

和之前一样,我们可以使用置换检验来回答这个问题。如果两组年龄的潜在分布相同,那么基于置换样本的差异的经验分布将预测统计量应如何因随机性而变化。

我们将遵循与任何模拟相同的过程。首先编写一个函数,返回均值差的一个模拟值,然后编写一个 for 循环来模拟大量这样的值并将它们收集到数组中。

[In ]:
def one_simulated_difference_of_means():
    """Returns: Difference between mean ages
    of smokers and non-smokers after shuffling labels"""
    
    # array of shuffled labels
    shuffled_labels = births.sample(with_replacement=False).column('Maternal Smoker')
    
    # table of ages and shuffled labels
    shuffled_table = births.select('Maternal Age').with_column(
        'Shuffled Label', shuffled_labels)
    
    return difference_of_means(shuffled_table, 'Shuffled Label')   
[In ]:
age_differences = make_array()

repetitions = 5000
for i in np.arange(repetitions):
    new_difference = one_simulated_difference_of_means()
    age_differences = np.append(age_differences, new_difference)

观测差异位于原假设下模拟差异的经验分布的尾部。

[In ]:
Table().with_column(
    'Difference Between Group Means', age_differences).hist(
    right_end = observed_age_difference)
# Plotting parameters; you can ignore the code below
plots.ylim(-0.1, 1.2)
plots.scatter(observed_age_difference, 0, color='red', s=40, zorder=3)
plots.title('Prediction Under the Null Hypothesis')
print('Observed Difference:', observed_age_difference)
Observed Difference: -0.8076725017901509
Histogram titled 'Prediction Under the Null Hypothesis' with 'Difference Between Group Means' on the x-axis and 'Percent per unit' on the y-axis. The histogram is bell shaped and centered at 0, ranging between approx -1 and 1. There is a red dot at approx -0.75 and portion of the histogram to the red dot's left is shaded gold. The gold shaded region is small.

再一次,模拟差异的经验分布大致以0为中心,因为模拟是在原假设(两组分布之间没有差异)下进行的。

检验的经验p值是模拟差异中等于或小于观测差异的比例。这是因为差异的低值支持备择假设,即吸烟者平均更年轻。

[In ]:
empirical_p = np.count_nonzero(age_differences <= observed_age_difference) / 5000
empirical_p
0.0108

经验p值约为1%,因此结果是统计显著的。检验支持吸烟者平均更年轻的假设。