极客时间已完结课程限时免费阅读

31 | 统计意义(下):如何通过显著性检验,判断你的A/B测试结果是不是巧合?

31 | 统计意义(下):如何通过显著性检验,判断你的A/B测试结果是不是巧合?-极客时间

31 | 统计意义(下):如何通过显著性检验,判断你的A/B测试结果是不是巧合?

讲述:黄申

时长11:30大小10.55M

你好,我是黄申,今天我们接着来聊显著性检验。
上一节,我介绍了差异显著性检验的概念,它是指从统计的角度来说,差异的产生有多大的概率、是不是足够可信。这点和数值差异的大小是有区别的。既然我们不能通过差异的大小来推断差异是否可信,那么有没有什么方法,可以帮助我们检验不同数据分布之间,是否存在显著差异呢?具体的方法有不少,比如方差分析(F 检验)、t 检验、卡方检验等等。我这里以方差分析为例,来讲这个方法是如何帮助我们解决 AB 测试中的问题。

方差分析

方差分析(Analysis of Variance, ANOVA),也叫 F 检验。这种方法可以检验两组或者多组样本的均值是否具备显著性差异。它有四个前提假设,分别是:
随机性:样本是随机采样的;
独立性:来自不同组的样本是相互独立的;
正态分布性:组内样本都来自一个正态分布;
方差齐性:不同组的方差相等或相近。
根据第三个前提,我们假设数据是正态分布,那么分布就有两个参数,一个是平均数,一个是方差。如果我们仅仅知道两个分组的平均值,但并不知道它们的方差相差多大,那么我们所得出的两个分布是否有显著差异的结论就不可靠了。
为了突出重点,我们先假设咱们的数据都符合上述四个前提,然后我来详细讲解一下方差分析的主要思想。最后,我会通过 Python 语言来验证各个假设和最终的 F 检验结果。
这里,我使用之前提到的 A/B 测试案例,通过方差分析来检验多种算法所产生的用户转化率有没有显著性差异。我们把“转化率”称为“因变量”,把“算法”称为“因素”。这里我们只有算法一个因素,所以所进行的方差分析是单因素方差分析。在方差分析中,因素的取值是离散型的,我们称不同的算法取值为“水平”。如果我们比较算法 a 和 b,那么 a 和 b 就是算法这个因素的两个水平。
我们假设只有两种算法 a 和 b 参与了 A/B 测试。为了检验这些算法导致的转化率,是不是存在显著的差异,我们进行一个为期 10 天的测试,每天都为每种算法获取一个转化率。具体的数据我列在下面这张表格中。
我使用 来表示这种表格中的数据, 表示第 次采样(或第 天), 表示第 种水平(或第 种算法)。以上面这张表格为例,
如果我们把每种算法导致的转化率看作一个数据分布,那么方差分析要解决的问题就是:这两个转化率分布的均值,是不是相等。如果我把两种数据分布的均值记作μ1 和μ2,那么原假设 H0 就是μ1=μ2。而对立假设 H1 就是μ1 <> μ2。
之前我们提到,差异是不是显著性,关键要看这个差异是采样的偶然性引起的,还是分布本身引起的。方差分析的核心思想也是围绕这个展开的,因此它计算了三个数值:SST、SSM 和 SSE。SST 表示所有采样数据的因变量方差(Total Sum of Squares),我把它的计算公式列在这里。
在这个公式中, 如前所说,表示了第 天第 种算法所导致的转化率。而 表示了 10 天里,2 种算法全部 20 个数据的平均值。SSM 表示数据分布所引起的方差,我们称它为模型平方和(Sum Of Squares for Model),它的计算公式如下:
在这个公式中, 为水平 下的观测数量,在我们的案例中为 10。 为第 个水平的平均值,在案例中为算法 a 或算法 b 在这 10 天的平均值。 表示的是某个算法的采样均值和所有采样均值之间的差异, 是相应的权重。我们这里的两个算法都被测试了 10 天,所以权重相同。根据我们的案例,SSM 是 0.00018。SSE 表示采样引起的方差,我们称它为误差平方和(Sum of Squaress for Error)。它的计算公式如下:
根据我们的案例,SSE 是 0.01652。我们刚刚介绍的三个统计量,SST、SSM 和 SSE 这三者的关系其实是这样的:
你可以把这三者的公式代入,自己证明一下等式是否成立。由此可以看出,SST 是由 SSM 和 SSE 构成的。如果在 SST 中,SSM 的占比更大,那么说明因素对因变量的差异具有显著的影响;如果 SSE 的占比更大,那么说明采样误差对因变量的差异具有更显著的影响。我们使用这两部分的比例来衡量显著性,并把这个比例称为 F 值。具体公式如下:
在这个公式中,s 是水平的个数,n 为所有样本的总数量,s-1 为分布的自由度,n-s 为误差的自由度。你可能对自由度这个概念比较陌生,这里我稍微解释一下。
自由度(degree of freedom),英文缩写是 df,它是指采样中能够自由变化的数据个数。对于一组包含 n 个数据的采样来说,如果方差是一个固定值,那么只有 n-1 个数据可以自由变化,最后一个数的取值是给定的方差和其他 n-1 个数据决定的,而不由它自己随意变化,所以自由度就是 n-1。这也是为什么在计算一组数的方差时,我们在下面这个公式中使用的除数是 n-1,而不是 n。
回到方差分析,对于 SSM 来说,如果 SSM 是固定的,那么对于 s 个水平来说,只能有 s-1 个组数据自由变化,而最后一组数据必须固定,所以对应于 SSM 的自由度为 s-1。对于 SSE 来说,如果 SSE 是固定的,那么对于 n 个采样、s 个水平数据来说,只有 n-s 个数据是可以自由变化的。因为每个水平中,都要有一个数据需要保证该组的平均值 而无法自由变化。
在我们的案例中,s 为不同算法的个数,也就是水平的个数 s 为 2,采样数据的个数 n 为 20,所以分布的自由度为 2-1=1,误差的自由度为 20-2=18。
在我们的案例中,F=(0.00018/(2-1))/(0.01652/(20-2))=0.196125908。有了 F 值,我们需要根据 F 检验值的临界表来查找对应的 P 值。我列出了这张表的常见内容,你可以看看。
通过这张表以及 n 和 m 的值,我们可以找到,在显著性水平α为 0.05 的时候,F 值的临界值。如果大于这个临界值,那么 F 检验的 P 值就会小于显著性水平α,证明差异具有显著性。
在咱们的案例中,n=20,m=s-1=1,所以对应的 F 值为 4.414。而我们计算得到的 F 值为 0.196,远远小于 4.414,因此说明差异没有显著性。虽然算法 a 所导致的平均转化率要比算法 b 的相对高出约 2%(要注意,2% 的相对提升在转化率中已经算很高了),但是由于差异没有显著性,所以这个提升的偶然性很大,并不意味着算法 a 比算法 b 更好。
如果需要,你可以在网上相关的统计资料里查找到完整的 F 检验临界值表。

使用 Python 代码进行验证

除了手动的计算,我们还可以用一些 Python 的代码来验证手动计算是不是准确。
首先,我们要确保自己安装了 Python 的扩展包 statsmodels。如果没有安装,你可以在命令行中输入下面这行:
pip install -U statsmodels
我们可以把下列数据输入一个 oneway.csv 文件。
algo,ratio
a,0.29
a,0.36
a,0.32
a,0.29
a,0.34
a,0.24
a,0.27
a,0.29
a,0.31
a,0.27
b,0.29
b,0.33
b,0.31
b,0.30
b,0.31
b,0.26
b,0.25
b,0.30
b,0.28
b,0.29
安装完了 statsmodels,并建立了数据文件 oneway.csv,我们就可以运行下面这段 Python 代码来进行 F 检验了。
import pandas as pd
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm
import scipy.stats as ss
# 读取数据,d1对应于算法a,d2对应于算法b
df = pd.read_csv("/Users/shenhuang/Data/oneway.csv") #设置为你自己的文件路径
d1 = df[df['algo'] == 'a']['ratio']
d2 = df[df['algo'] == 'b']['ratio']
# 检测两个水平的正态性
print(ss.normaltest(d1))
print(ss.normaltest(d2))
# 检测两个水平的方差齐性
args = [d1, d2]
print(ss.levene(*args))
# F检验的第一种方法
print(ss.f_oneway(*args))
# F检验的第二种方法
model = ols('ratio ~ algo', df).fit()
anovat = anova_lm(model)
print(ano
我们假设用于 A/B 测试的两个算法是相互独立且随机的,所以这里只检测了正态分布性和方差齐性。
其中,ss.normaltest 分别测试了两个水平的正态分布性,两次结果如下:
NormaltestResult(statistic=0.16280747339563784, pvalue=0.9218214431590781)
NormaltestResult(statistic=0.4189199849120419, pvalue=0.8110220857858036)
ss.normaltest 的原假设是数据符合正态分布,两次检验 P 值都是远远大于 0.05 的,所以原假设成立,这两者都符合正态分布。
而 ss.levene 分析了两者的方差齐性,同样 P 值都是远远大于 0.05,因此符合方差齐的前提。
LeveneResult(statistic=0.7944827586206901, pvalue=0.38450823419725666)
ss.f_oneway 和 anova_lm 都可以进行 F 检验。ss.f_oneway 给出的结果比较简洁。
F_onewayResult(statistic=0.19612590799031476, pvalue=0.663142430745588)
而 anova_lm 提供了更多的信息,但是两种 F 检验函数都证明了我们之前的手动推算结果是正确的。
df sum_sq mean_sq F PR(>F)
algo 1.0 0.00018 0.000180 0.196126 0.663142
Residual 18.0 0.01652 0.000918 NaN NaN

总结

方差分析可以帮助我们检测差异的显著性,它分析的内容是受一个或多个因素影响的因变量在不同水平分组的差异。不过单因素的方差分析要求因变量属于正态分布总体,并具有方差齐性。如果因变量的分布明显的是非正态,或者方差的差异很显著,那么我们就不能直接使用这种方法。对于方差不齐的情况,我们可以选择适当的函数,例如对数、倒数等等,对原始数据进行转换,直到方差齐性变得显著,或者剔除明显属于“均值±标准差”之外的数据。
当然,对于非正态分布的数据,我们也可以使用非参数的分析。非参数检验是在总体的方差知道很少的情况下,利用样本数据对总体分布形态等进行推断的方法。名字中的“非参数”的由来,就是因为这种检验方法在推断过程中不涉及有关总体分布的参数,而只是进行分布位置、分布形状之间的比较,因此不受总体分布的限定,适用范围比较广。常见的非参数检验包括二项分布检验、K-S 检验、卡方检验等等。

思考题

请尝试使用 Python 语言实现你自己的方差分析函数,然后通过测试数据来比较你实现的函数和 Python 扩展包里的函数(例如 statsmodels.stats.anova.anova_lm 或 scipy.stats.f_oneway)。
欢迎留言和我分享,也欢迎你在留言区写下今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 3

提建议

上一篇
30 | 统计意义(上):如何通过显著性检验,判断你的A/B测试结果是不是巧合?
下一篇
32 | 概率统计篇答疑和总结:为什么会有欠拟合和过拟合?
unpreview
 写留言

精选留言(13)

  • 强哥
    2019-03-01
    我们这面的ab test计算显著性用的是t检验,不知道跟f检验的区别是什么?对于非参数检验的方法可以用bootstraping吧!分析师对这方面比较有研究

    作者回复: t检验可以用pairwise的t检验,一般用于两组对比,而f检验可以进行多组(多个水平)的检验

    7
  • 罗耀龙@坐忘
    2020-04-23
    茶艺师学编程 老师,我是这么理解方差分析的: 对于要考察的数据(符合正态分布),要想得出差异具有显著性,那么SSM/分布自由度要比SSE/误差自由度大上不少才行(分子比分母大不少)。 有意义的数据要比噪音要多,不然事情就是随机漫步或者是偶然(没有为什么)发生的。 不知道我这样子理解有没有问题?
    展开

    作者回复: 对,就是这个意思

    共 3 条评论
    5
  • 动摇的小指南针
    2019-05-26
    方差检验的前提是符合正态分布,那么针对用户转化率算法a和b而言,怎么理解这种分布呢,是指在某种用户特征分类的x坐标上,转化效果y坐标符合正态分布吗?

    作者回复: 是的,可以进行采样数据可视化来初版判断

    3
  • 叮当猫
    2019-04-15
    请问F检验临界值表是怎么计算出来的?

    作者回复: 这个问题比较复杂,简单的来说,基于一些假设我们可以画出在不同的自由度之下,不同的F值曲线。和正态分布类似,根据这个曲线,我们可以确定某个α的值,让F曲线在α值右侧的面积小于0.05或者0.01等等。

    2
  • mickey
    2019-02-26
    请问,显著性水平α为什么要取0.05?

    作者回复: 这是个好问题,0.05是业界的常见标准,约定俗成。也不一定要取0.05,0.1或者是0.02,0.01,只要不是太大都可以。至于多“大”算“大”,看你愿意接受偶然性的程度,没有明显的界定,当然0.3,0.5这种就肯定太大了。

    2
  • mickey
    2019-02-26
    算法a所导致的平均转化率要比算法b的相对高出约2% 是怎么计算出来的?

    作者回复: (a均值-b均值) / (b均值),是相对百分比

    2
  • yaya
    2019-02-25
    笔记:两组样本的差异可能是由 1.采样造成的差异 2.数据分布不同造成的差异 如果要判断更多的是由哪种差异造成的,可以计算他们的比值。 采样的差异计算 各个数据到每个分布中心的距离和比如对第j水平来说,就是数据到j水平的距离,所有采样的差异就是所有水平的差异之和 分布造成的差异计算,就是各水平均值到所有均值的差异和 这两个差异我能理解,但是他们对应的量纲应该是不同的,就是他们不是同一基准下的差异但是为什么引入自由度就可以了呢?采样差异的自由度计算为什么要保证各水平均值不变,我没能理解
    展开

    作者回复: 量纲在物理学里用得比较多,我的理解是它代表了不同的含义,比如这里代表转化率的单位。假设转化率的定义是#click/#pageview,那么这里的两个水平的量纲都是这种次数的比例,量纲应该是相同的。 对于自由度的计算,如果平均值发生了变化,那么方差就要重新计算了。

    2
  • 建强
    2020-07-26
    简单写了一个Python程序,实现了SST、SSM、SSE、F值的计算,P值就不知道怎么算了,程序代码如下: # 方差分析 import pandas as pd import numpy as np class VarianceAnaly: def __init__(self,sampledata): self.sampledata = sampledata total_sum = 0 total_count = 0 for col in list(self.sampledata.columns): total_sum += self.sampledata[col].sum() total_count += self.sampledata[col].count() self.total_var = np.round(total_sum / total_count,4) def sst(self): result = pd.DataFrame(index=self.sampledata.index, columns = self.sampledata.columns) total_sum = 0 for col in list(result.columns): result[col] = (self.sampledata[col] - self.total_var) ** 2 total_sum += result[col].sum() return np.round(total_sum,6) def ssm(self): total_sum = 0 for col in list(self.sampledata.columns): total_sum += ((self.sampledata[col].mean() - self.total_var) ** 2) * self.sampledata[col].count() return np.round(total_sum,6) def sse(self): total_sum = 0 for col in list(self.sampledata.columns): total_sum += sum((self.sampledata[col] - self.sampledata[col].mean()) ** 2) return np.round(total_sum,6) # 计算样本的F值 def f_value(self): total_columns = len(self.sampledata.columns) total_sample_nums = sum(self.sampledata.count()) f = self.ssm() * (total_sample_nums - total_columns) / (self.sse() * (total_columns - 1)) return np.round(f,6) def test(): sampledata = pd.DataFrame({'algo_a':[0.29,0.36,0.32,0.29,0.34,0.24,0.27,0.29,0.31,0.27], 'algo_b':[0.29,0.33,0.31,0.30,0.31,0.26,0.25,0.30,0.28,0.29]}) test_var = VarianceAnaly(sampledata) print('sst=',test_var.sst()) print('ssm=',test_var.ssm()) print('sse=',test_var.sse()) print('F=',test_var.f_value()) if __name__ == '__main__': test()
    展开
    1
  • arcsinx
    2021-07-26
    老师,p>0.05 可以接受原假设吗?我记得一般是反证,使p<0.05之后拒绝原假设?

    作者回复: 对,如果你设置p的阈值为0.05,那么大于0.05就表示接受原假设,否则拒绝原假设。当然,如果要求没那么严苛,可以适当放大p的阈值

  • 撒冷之王
    2021-07-21
    你好, 例子中自变量数目 m = s-1 = 1; 不是太明白为什么 m 是根据 s-1 算出来的呢? 如果是文中的例子,自变量数目不就是“不同的算法” 这一个自变量吗(m = 1 就直接得出了)

    作者回复: 这里“不同的算法”包括a和b,所以不同算法的数量为2,m=s-1=2-1=1

  • Geek3340
    2021-05-18
    1.H0是一定要用均值进行假设,而且一定是假设两组或者多组均值相等,是原假设吗?H1是不相等吗? 2. “虽然算法a所导致的平均转化率要比算法b的相对高出约2%”,如果10天的平均值是0.298对比0.292,哪里来的高出2%呢?3. 如果方差分析,结果表明差异没有显著性,那下一步需要找其他什么分析方法来检验a/b测试哪个好呢? 4. 如何判断两组或多组数据一定是正态分布?如果不是正态分布,那怎么办呢?

    作者回复: 1. 对,H0假设是两者没有区别,来自同一个数据分布。H1表示相反意见。 2. 第二个原文没有说清楚是相对提升,也是0.292/0.298 = 1.02 3. 如果没有显著性,可以使用其他例如t检验、CHI方等等。如果多个结果都显示出没有显著的差异,那么a/b测试的结论就是两者没有统计上的差异

  • wuqg
    2020-12-11
    老师,除了pvalue还有置信区间来分析ab测试,您也给讲讲吧。

    作者回复: 这个问题很好,不过留言回复里说不清,极客时间和人民邮电出版社明年初就会联合发行我所著的《程序员的数学基础课-从理论到Python实战》,这本书是以本专栏为基础,新增了部分内容,对置信区间和AB测试有更为详细的解释

  • Paul Shan
    2019-09-17
    F值反应了不同样本的差异是否由系统因素引起,而非采样的随机性引起的参数。 F值可以由样本的观察值计算得到。