A/Btest:组间的差异性检验,统计功效以及反选样本量,附python底层实现代码

先概括一下:本文主要阐述了A/Btest中组间差异的比率检验(单比率检验,双比率检验),统计功效,以及何通过显著性水平还有统计功效反实验所需选样本量。使用python对着三个功能进行实现,并封装成类,方便直接调用。如果A/B test中包含多组人群,可以两两进行比较,也可以直接利用方差分析判断不同组间是否存在差异(方差分析建立在样本独立,正态分布和方差齐性假设上,但实际上随机抽样时,样本独立,方差分析中F检验对正态分布不敏感,且方差不差太多(2倍以上)方差分析的结果基本都可以认为是有效的)。

一、A/B test

在产品发布,运营等场景我们都会遇到A/B test。A/B test通常为同一个目标,设计两种方案,将两种方案随机投放市场中。

A/B test让组成成分相同(相似)用户去随机体验两种方案之一,根据观测结果,判断哪个方案效果更好,结果可以通过CTR或者下单率来衡量。最终我们选择CTR或者下单率更优的版本作为线上应用的版本。

现实场景中我们避不开几个问题:

  1. A/B test两组人群的转化效果是否存在差异——假设检验
  2. 我们正确判断出A/B test两组人群有差异的把握有多大——统计功效
  3. 在一定显著性水平和和统计功效下,我们需要选定多少样本量进行试验——反选样本量

上面3个问题对顺利进行A/B test至关重要,在网上找到了一个很好的课件,还有一些具体的例子,非常容易理解。下面结合课件对这三个问题进行阐述。

关于三者的求解,本文利用python从底层进行了实现。其实网上有很多统计软件。但是共感觉有点杂乱,于是照着原理从底层编写,按照自己的方式编写后感觉清爽了很多。

一、单比率检验

对于A/B test中两组人群的对比中,我们需要对比的是ctr,转化率等指标。而ctr,用户转化率等指标,都是01分布,即二项分布。因此可以使用比率检验的方法进行假设检验。如果A/B test中包含多组人群,可以两两进行比较,也可以直接利用方差分析组间差异的判断。
在这里插入图片描述

1.1.单比率检验
现在有这样一种情景,我们新发布了一个版本或新上了一个活动,并选了一批人进行试验,我们想要知道发布了这个版本或新上活动后新的样本是否和原来有明显差异。我们可以使用单比率检验。(与平常假设检验无差别。构造统计量,看统计量是否在拒绝域内。正常是T统计量,这里由于是二项分布,n*p>5时可以认为是正态分布,即Z统计量)
在这里插入图片描述
在这里插入图片描述
接下来是单比率检验的一个简单例子:在这里插入图片描述
1.2.单比率检验的统计功效
在上面我们阐述了在显著性水平 α \alpha α下一组样本的统计指标是否与原来存在差异的方法。在概率统计中,我们知道,假设检验中有两类错误,第一类错误是“弃真”,即当零假设正确时,我们拒绝的概率,记为 α \alpha α;第二类错误是“纳伪”,即零假设错误时,我们却没有拒绝的概率记为 β \beta β

由定义可知上面的 α \alpha α β \beta β都是关于零假设的条件概率,实际上我们所说的显著性水平对应的就是第一类错误概率 α \alpha α

现在假设我们再比较一组样本与另一组是否存在差异时,我们拒绝了零假设,即认为两组有差异,我们需要进一步知道我们正确拒绝了零假设的概率 p o w e r power power,我们把这个概率叫做统计功效

实际上统计功效 p o w e r power power就是1-零假设错误时,我们却没有拒绝的概率(第二类错误概率)。即 p o w e r = 1 − β power=1-\beta power=1β

在这里插入图片描述

1.3. 反选样本量
在实验室,我们总会预先设定一个显著性水平 α \alpha α和统计功效 β \beta β。根据上面统计功效的公式,实际上我们可以反推出我们需要的样本量:
在这里插入图片描述

二、双比率检验

在A/B test是,我们同上的做法是在同一层试验中,选用两个或多个版本(活动)进行同时实现,此时我们需要比较两组样本的差异性。于是我们就需要用到双比率检验

2.1.双比率检验
在这里插入图片描述
下面是双比率检验一个简单的例子:
在这里插入图片描述
在这里插入图片描述

2.2.双比例检验的统计功效

与前面所说的统计功效一样,这里我们比较两组样本时的统计功效:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3. 双比例检验反选样本数
注意:下面例子的反求样本数量的计算出现错误,读者可以自行计算。我这里重新求出来下面两个例子的结果是1782和15022。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
实际上根据样本的分布、是否相互独立,方差是否已知等条件,反求样本量的方法如下:
在这里插入图片描述
(上图来自https://www.datasciencecentral.com/profiles/blogs/determining-sample-size-in-one-picture

三、比率检验、统计功效以及反选样本量的python实现

本文利用python从底层进行了实现,并针对课件上的例子进行了求解。

其实网上有很多统计软件。但是共感觉有点杂乱,于是照着公式从底层编写,按照自己的方式编写后感觉清爽了很多,可以直接对接其他程序联合使用。

# -*- coding: utf-8 -*-
"""
Created on Tue Mar 31 13:53:28 2019

@author: nbszg
"""
import math as m
import numpy as np
import scipy.stats as st

class AB_test_ratio_test(object):
    def single_Z_test(self, p_theta, p_real, sample_n, alpha=0.05, method='two sides'):
        '''
        输入参数,输出样本指标是否与原来(总体)相同
        p_theta:新样本组(实验组)的转化率,点击率等
        p_real:原来的的转化率,点击率等
        sample_n:新样本组(实验组)的样本容量
        alpha:显著性水平
        method:检测区间,有'two sides'、'one sides larger'、'one sides smaller'三种,代表双侧,右侧和左侧检验
        
        return 是否可以拒绝原假设和统计量Z值
        '''
        # 构造Z统计量
        Z = (p_theta - p_real) / m.sqrt(p_real * (1 - p_real) / sample_n)
        # 我们总是希望拒绝H0!!!
        # H0:p_theta=p_real, H1:p_theta!=p_real
        if method == 'two sides':
            if abs(Z) > st.norm.ppf(1 - alpha / 2):
                return "Z is: {} , refuse H0, there's difference between p_theta and p_real".format(Z)
            else:
                return "Z is: {} ,can't refuse H0".format(Z)
        # H0:p_theta<=p_real, H1:p_theta>p_real
        elif method == 'one sides larger':
            if Z > st.norm.ppf(1 - alpha):
                return "Z is: {} , refuse H0, p_theta is larger than p_real".format(Z)
            else:
                return "Z is: {} , can't refuse H0".format(Z)
        # H0:p_theta>=p_real, H1:p_theta<p_real
        elif method == 'one sides smaller':
            if Z < st.norm.ppf(alpha):
                return "Z is: {} , refuse H0, p_theta is smaller than p_real".format(Z)
            else:
                return "Z is: {} , can't refuse H0".format(Z)
        else:
            raise ValueError("there's no method named: {0}".format(method))
    
    
    def single_power_cul(self, p_theta, p_real, sample_n, alpha=0.05, method='two sides'):
        '''
        输入参数,输出统计功效
        p_theta:新样本组(实验组)的转化率,点击率等
        p_real:原来的的转化率,点击率等
        sample_n:新样本组(实验组)的样本容量
        alpha:显著性水平
        method:检测区间,有'two sides'、'one sides larger'、'one sides smaller'三种,代表双侧,右侧和左侧检验
        
        return 检验的统计功效power
        '''
        # 求总体sigma
        sigma_p = m.sqrt(p_real * (1 - p_real) / sample_n)
        # 求B组
        s_p = m.sqrt(p_theta * (1 - p_theta) / sample_n)
        #power of p_theta!=p_real
        if method == 'two sides':
            fai_right = 1 - st.norm.cdf((p_real - p_theta + st.norm.ppf(1 - alpha / 2) * sigma_p) / s_p)
            fai_left = st.norm.cdf((p_real - p_theta - st.norm.ppf(1 - alpha / 2) * sigma_p) / s_p)
            power = fai_right + fai_left
        #power of p_theta>p_real
        elif method == 'one sides larger':
            fai_right = 1 - st.norm.cdf((p_real - p_theta + st.norm.ppf(1 - alpha) * sigma_p) / s_p)
            power = fai_right
        #power of p_theta<p_real
        elif method == 'one sides smaller':
            fai_left = st.norm.cdf((p_real - p_theta - st.norm.ppf(1 - alpha) * sigma_p) / s_p)
            power = fai_left
        else:
            raise ValueError("there's no method named: {0}".format(method))
    
        return power

    def single_sample_n(self, p_theta, p_real, alpha=0.05, beta=0.9, method='two sides'):
        '''
        输入参数,输出统计功效
        p_theta:新样本组(实验组)的转化率,点击率等
        p_real:原来的的转化率,点击率等
        alpha:显著性水平
        beta:想要到达的功效power
        
        return 达到检验功效所需要的最小样本量
        '''
        # 先求分母
        denominator = pow(2 * m.asin(m.sqrt(p_real)) - 2 * m.asin(m.sqrt(p_theta)), 2)
        # H0:p_theta=p_real, H1:p_theta!=p_real
        if method == 'two sides':
            numerator = pow(st.norm.ppf(1 - alpha / 2) + st.norm.ppf(beta), 2)
            sample_n = numerator / denominator
        elif method == 'one sides larger' or method == 'one sides smaller':
            numerator = pow(st.norm.ppf(1 - alpha) + st.norm.ppf(beta), 2)
            sample_n = numerator / denominator
        else:
            raise ValueError("there's no method named: {0}".format(method))
    
        return sample_n
    
    def two_Z_test(self, p_theta, p_gamma, sample_theta, sample_gamma, alpha=0.05, method='two sides', d=0):
        '''
        输入参数,输出两个总体的转化率,点击率是否可以认为不同
        p_theta:样本组1(对照组A)的转化率,点击率等
        p_gamma:样本组2(实验组B)的转化率,点击率等
        sample_theta:样本组1(对照组A)样本容量
        sample_gamma:样本组2(实验组B)样本容量
        alpha:显著性水平
        method:检测区间,有'two sides'、'one sides larger'、'one sides smaller'三种,代表双侧,右侧和左侧检验
        d:样本组1(对照组A)和样本组2(实验组B)的设定差异
        
        return 样本组1(对照组A)和样本组2(实验组B)的转化率,点击率是否存在差异和统计量Z
        '''
        # 构造Z统计量
        Z = (p_theta-p_gamma-d) / m.sqrt((p_theta*(1-p_theta)/sample_theta) + (p_gamma*(1-p_gamma)/sample_gamma))
        # 我们总是希望拒绝H0!!!
        # H0:p_theta=p_gamma, H1:p_theta!=p_gamma
        if method == 'two sides':
            if abs(Z) > st.norm.ppf(1 - alpha / 2):
                return "Z is: {} , refuse H0, there's difference between p_theta and p_gamma".format(Z)
            else:
                return "Z is: {} , can't refuse H0".format(Z)
        # H0:p_theta<=p_real, H1:p_theta>p_gamma
        elif method == 'one sides larger':
            if Z > st.norm.ppf(1-alpha):
                return "Z is: {} , refuse H0, p_theta is larger than p_gamma".format(Z)
            else:
                return "Z is: {} , can't refuse H0".format(Z)
        # H0:p_theta>=p_real, H1:p_theta<p_gamma
        elif method == 'one sides smaller':
            if Z < st.norm.ppf(alpha):
                return "Z is: {} , refuse H0, p_theta is smaller than p_gamma".format(Z)
            else:
                return "Z is: {} , can't refuse H0".format(Z)
        else:
            raise ValueError("there's no method named: {0}".format(method))
    
    def two_power_cul(self, p_theta, p_gamma, sample_theta, sample_gamma, alpha=0.05, method='two sides', d=0):
        '''
        输入参数,输出两个总体的差异检验的统计功效
        p_theta:样本组1(对照组A)的转化率,点击率等
        p_gamma:样本组2(实验组B)的转化率,点击率等
        sample_theta:样本组1(对照组A)样本容量
        sample_gamma:样本组2(实验组B)样本容量
        alpha:显著性水平
        method:检测区间,有'two sides'、'one sides larger'、'one sides smaller'三种,代表双侧,右侧和左侧检验
        d:样本组1(对照组A)和样本组2(实验组B)的设定差异
        
        return 样本组1(对照组A)和样本组2(实验组B)的差异检验的统计功效power
        '''
        sigma_denominator = m.sqrt((p_theta*(1-p_theta)/sample_theta) + (p_gamma*(1-p_gamma)/sample_gamma))
        p_avg = (p_theta+p_gamma)/2
        sigma_numerator = m.sqrt(2*p_avg*(1-p_avg)/((sample_theta+sample_gamma)/2))
        #power of p_theta!=p_gamma + d
        if method == 'two sides':
            fai_right = 1 - st.norm.cdf((p_gamma + d - p_theta + st.norm.ppf(1-alpha/2) * sigma_numerator) / sigma_denominator)
            fai_left = st.norm.cdf((p_gamma + d - p_theta - st.norm.ppf(1-alpha/2) * sigma_numerator) / sigma_denominator)
            power = fai_right + fai_left
        #power of p_theta>p_gamma + d
        elif method == 'one sides larger':
            fai_right = 1 - st.norm.cdf((p_gamma + d - p_theta + st.norm.ppf(1-alpha) * sigma_numerator) / sigma_denominator)
            power = fai_right
        #power of p_theta<p_gamma + d
        elif method == 'one sides smaller':
            fai_left = st.norm.cdf((p_gamma + d - p_theta - st.norm.ppf(1-alpha) * sigma_numerator) / sigma_denominator)
            power = fai_left
        else:
            raise ValueError("there's no method named: {0}".format(method))
    
        return power
    
    def two_sample_n(self, p_theta, p_gamma, alpha=0.05, beta=0.9, method='two sides'):
        '''
        输入参数,输出两个总体的差异检验的统计功效
        p_theta:样本组1(对照组A)的转化率,点击率等
        p_gamma:样本组2(实验组B)的转化率,点击率等
        alpha:显著性水平
        beta:想要到达的功效power
        method:检测区间,有'two sides'、'one sides larger'、'one sides smaller'三种,代表双侧,右侧和左侧检验
        d:样本组1(对照组A)和样本组2(实验组B)的设定差异
        
        return 每个样本组的所需的最小样本数
        '''
        # 先求分母
        denominator = pow(2 * m.asin(m.sqrt(p_gamma)) - 2 * m.asin(m.sqrt(p_theta)), 2)
        # H0:p_theta=p_real, H1:p_theta!=p_real
        if method == 'two sides':
            numerator = 2 * pow(st.norm.ppf(1 - alpha/2) + st.norm.ppf(beta), 2)
            sample_n = numerator / denominator
        elif method == 'one sides larger' or method == 'one sides smaller':
            numerator = 2 * pow(st.norm.ppf(1 - alpha) + st.norm.ppf(beta), 2)
            sample_n = numerator / denominator
        else:
            raise ValueError("there's no method named: {0}".format(method))
    
        return sample_n
        
if __name__=='__main__':
    ratio_test = AB_test_ratio_test()
    print('example1 Z test is:', ratio_test.single_Z_test(0.018, 0.02,sample_n=500,  method='two sides'),'\n')
    print('example1 power is:', ratio_test.single_power_cul(0.018, 0.02,sample_n=500,  method='two sides'),'\n')
    print('example2 need sample is:', ratio_test.single_sample_n(0.018, 0.02,  method='two sides'),'\n')
    print('example3 Z test is:', ratio_test.two_Z_test(0.18, 0.2,sample_theta=1600,sample_gamma=2000,  method='two sides'),'\n')
    print('example4 Z test is:', ratio_test.two_Z_test(0.18, 0.2,sample_theta=1600,sample_gamma=2000,  method='two sides'),'\n')
    print('example5 Z test is:', ratio_test.two_Z_test(0.025, 0.007,sample_theta=3000,sample_gamma=3000,  method='one sides larger', d=0.005),'\n')
    print('example5 power is:', ratio_test.two_power_cul(0.025, 0.007,sample_theta=3000,sample_gamma=3000,  method='one sides larger'),'\n')
    print('example6 power is:', ratio_test.two_power_cul(0.025, 0.007,sample_theta=3000,sample_gamma=3000,  method='one sides larger', d=0.005),'\n')
    print('example7 need sample is:', ratio_test.two_sample_n(0.025, 0.012, method='one sides larger'),'\n')
    print('example8 need sample is:', ratio_test.two_sample_n(0.025, 0.02, method='one sides larger'),'\n')

程序运行结果如下:
在这里插入图片描述
对比课件中的结果,除去计算精度误差,结果可以认为与课件中是相等的。

参考文献:
假设检验与样本数量分析④——单比率检验、双比率检验

  • 7
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值