放气门

2015年1月18日,印第安纳波利斯小马队与新英格兰爱国者队进行了美国橄榄球联合会(AFC)冠军赛,以决定哪支球队进入超级碗。赛后,有人指控爱国者队的橄榄球未按规定充气到要求的气压;球更软。这可能是一个优势,因为更软的球可能更容易接住。

在接下来的几周里,美式橄榄球界充斥着指控、否认、理论和猜疑:媒体效仿20世纪70年代的“水门事件”(Watergate),将这一事件称为“放气门”(Deflategate)。美国国家橄榄球联盟(NFL)委托进行了独立分析。在这个例子中,我们将自己对数据进行分析。

气压通常以每平方英寸磅数(psi)为单位。NFL规则规定,比赛用球的充气压力必须在12.5 psi至13.5 psi之间。每支球队使用12个球。球队负责维护自己橄榄球的气压,但比赛官员会检查球。在AFC冠军赛开始前,爱国者队的所有球都在约12.5 psi。小马队的大部分球在约13.0 psi。然而,这些赛前数据并未被记录。

在第二节比赛中,小马队拦截了一个爱国者队的球。在场边,他们测量了该球的气压,确定其低于12.5 psi的阈值。他们立即通知了官员。

在中场休息时,所有比赛用球被收集起来进行检查。两名官员Clete Blakeman和Dyrol Prioleau测量了每个球的气压。

以下是数据。每行对应一个橄榄球。气压以psi为单位计量。被小马队拦截的爱国者队球在中场休息时未接受检查。小马队的大部分球也未被检查——官员们时间不够,不得不将球交还以进行下半场比赛。

[In ]:
from datascience import *
%matplotlib inline
path_data = '../../../assets/data/'
import matplotlib.pyplot as plots
plots.style.use('fivethirtyeight')
import numpy as np
[In ]:
football = Table.read_table(path_data + 'deflategate.csv')
football.show()
<IPython.core.display.HTML object>

对于被检查的15个球中的每一个,两名官员得到了不同的结果。对同一物体进行重复测量得到不同结果并不罕见,特别是当测量由不同人员执行时。因此,我们将为每个球分配其两次测量的平均值。

[In ]:
football = football.with_column(
    'Combined', (football.column(1)+football.column(2))/2
    ).drop(1, 2)
football.show()
<IPython.core.display.HTML object>

乍看之下,爱国者队的橄榄球气压明显低于小马队的球。由于比赛过程中一定程度的放气是正常的,独立分析师决定计算从比赛开始以来的气压下降值。回忆一下,爱国者队的球开始时都在约12.5 psi,小马队的球在约13.0 psi。因此,爱国者队球的气压下降计算为12.5减去中场时的气压,小马队球的气压下降计算为13.0减去中场时的气压。

我们可以通过首先设置一个起始值数组来计算每个橄榄球的气压下降。为此,我们需要一个由11个值组成的数组,每个值为12.5,以及另一个由4个值组成的数组,每个值为13.0。我们将使用NumPy函数 np.ones,它接受一个计数作为参数,并返回一个包含那么多元素的数组,每个元素为1。

[In ]:
np.ones(11)
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
[In ]:
patriots_start = 12.5 * np.ones(11)
colts_start = 13 * np.ones(4)
start = np.append(patriots_start, colts_start)
start
array([12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5,
       13. , 13. , 13. , 13. ])

每个橄榄球的气压下降是起始气压与综合气压测量值之间的差值。

[In ]:
drop = start - football.column('Combined')
football = football.with_column('Pressure Drop', drop)
football.show()
<IPython.core.display.HTML object>

看起来爱国者队的气压下降比小马队更大。让我们看看两组各自的平均下降值。我们不再需要综合评分了。

[In ]:
football = football.drop('Combined')
football.group('Team', np.average)
Team     | Pressure Drop average
Colts    | 0.46875
Patriots | 1.20227

爱国者队的平均下降约为1.2 psi,而小马队约为0.47 psi。

现在的问题是,为什么爱国者队橄榄球的气压下降平均比小马队大。这可能是由随机性造成的吗?

提出假设

随机性是如何引入的?这里没有任何东西是被随机选择的。但我们可以提出一个随机模型:假设爱国者队的11个下降值看起来像是所有15个下降值中随机抽取的11个样本,而小马队的下降值是剩余的4个。这是一个完全指定的随机模型,我们可以在此模型下模拟数据。这就是原假设

对于备择假设,我们可以认为爱国者队的下降值平均来说太大了,不像从所有下降值中随机抽取的样本。

检验统计量

一个自然的统计量是两个平均下降值之间的差值,我们将计算为“爱国者队平均下降 - 小马队平均下降”。该统计量的大值将支持备择假设。

[In ]:
observed_means = football.group('Team', np.average).column(1)

observed_difference = observed_means.item(1) - observed_means.item(0)
observed_difference
0.733522727272728

这个正差值反映了爱国者队橄榄球的平均气压下降大于小马队的事实。

正如我们在上一节中所做的那样,我们将编写一个函数来计算两组平均下降值之间的差异。函数 difference_of_means 接受两个参数:

  • 数据表的名称
  • 包含两个组标签的列标签

它返回两组平均下降值之间的差异。我们将和之前一样,按照爱国者队下降值减去小马队下降值来计算差异。

[In ]:
def difference_of_means(table, group_label):
    reduced = table.select('Pressure Drop', 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(football, 'Team')
0.733522727272728

这与我们之前找到的 observed_difference 值相同。

在原假设下预测统计量

如果原假设成立,那么哪些球被标记为爱国者队、哪些被标记为小马队应该无关紧要。两组下降值的分布应该是相同的。我们可以通过随机打乱球队标签来模拟这一点。

[In ]:
shuffled_labels = football.sample(with_replacement=False).column(0)
original_and_shuffled = football.with_column('Shuffled Label', shuffled_labels)
original_and_shuffled.show()
<IPython.core.display.HTML object>

所有组的平均值比较如何?

[In ]:
difference_of_means(original_and_shuffled, 'Shuffled Label')
-0.5619318181818183
[In ]:
difference_of_means(original_and_shuffled, 'Team')
0.733522727272728

当球队标签被随机分配给橄榄球时,两队的平均下降值比实际比赛中的两组更接近。

置换检验

现在是进行熟悉步骤的时候了。我们将通过反复置换橄榄球并将随机集合分配给两队,在原假设下反复模拟检验统计量。

和往常一样,我们首先编写一个函数 one_simulated_difference,该函数在随机打乱橄榄球的球队标签后,返回标记为爱国者队和小马队的两组平均气压下降值差异的一个模拟值。

[In ]:
def one_simulated_difference():
    shuffled_labels = football.sample(with_replacement = False
                                                    ).column('Team')
    shuffled_table = football.select('Pressure Drop').with_column(
        'Shuffled Label', shuffled_labels)
    return difference_of_means(shuffled_table, 'Shuffled Label')   

现在我们可以使用 for 循环和这个函数创建一个数组 differences,其中包含在原假设下模拟的10,000个检验统计量值。

[In ]:
differences = make_array()

repetitions = 10000
for i in np.arange(repetitions):
    new_difference = one_simulated_difference()
    differences = np.append(differences, new_difference)

检验的结论

要计算经验p值,重要的是回想一下备择假设:爱国者队的下降值太大,不能仅由随机变异的结果来解释。

爱国者队更大的下降值支持备择假设。因此,p值是(在原假设下计算的)得到等于或大于我们观测值0.733522727272728的检验统计量的概率。

下图可视化了这一计算。它包含原假设下检验统计量的经验分布,观测统计量在横轴上用红色标记,p值对应的区域用金色阴影表示。

[In ]:
Table().with_column(
    'Difference Between Group Averages', differences).hist(
    left_end = observed_difference
)
plots.ylim(-0.1, 1.4)
plots.scatter(observed_difference, 0, color='red', s=30, zorder=3)
plots.title('Prediction Under the Null Hypothesis')
print('Observed Difference:', observed_difference)
Observed Difference: 0.733522727272728
Histogram titled 'Predction Under the Null Hypothesis' with 'Difference Between Group Averages' on the x-axis and 'Percent per unit' on the y-axis. The data is bell shaped centered at 0 and extending between approximately -0.75 and 0.75. There is a red dot just below 0.75 and the data of the histogram to the right of the dot is shaded gold. The gold shaded region is very small.

目测来看,p值似乎相当小。我们可以通过计算来确认。

[In ]:
empirical_p = np.count_nonzero(differences >= observed_difference) / 10000
empirical_p
0.0026

正如该检验之前的示例一样,分布的主体以0为中心。在原假设下,爱国者队的下降值是所有15个下降值的随机样本,小马队也是如此。因此,两组下降值的平均值应大致相等,其差值应在0左右。

但检验统计量的观测值距离分布中心相当远。用任何合理的“小”的临界值来判断,经验p值都很小。因此,我们最终拒绝随机性的原假设,并得出结论:爱国者队的下降值太大,不能仅反映随机变异。

独立调查团队考虑物理定律,用几种不同的方式分析了数据。最终报告指出:

“爱国者队比赛用球的平均气压下降比小马队球高出0.45至1.02 psi,具体取决于关于所用测量仪器的各种可能假设,并假设爱国者队球的初始气压为12.5 psi,小马队球为13.0 psi。”

——NFL委托的关于2015年1月18日AFC冠军赛的调查报道

我们的分析显示平均气压下降约为0.73 psi,接近“0.45至1.02 psi”区间的中心,因此与官方分析一致。

请记住,我们的假设检验并未确定差异为什么不是由随机性造成的。建立因果关系通常比运行假设检验更复杂。

但在橄榄球界,那个至关重要的问题是因果关系:问题是爱国者队橄榄球的气压过量下降是否是故意的。如果你对调查人员给出的答案感到好奇,这里是完整报告