A/B测试分组如何优化

背景

实验工程中用到A/B 测试,验证某种功能的效果。而对照组和实验组,需要差异性小于某个阀值,例如0.4%,实验才被认为有效。

业界普遍使用完全随机分组法(Complete Randomization,CR),即对用户ID字段进行哈希后对100取模,得到一个结果值,再将结果值相同的用户分入同一个桶。基于这种方式,日常发现实验组,对照组在一些关键指标上存在差异,差异较大时,实验结果的可信性将会产生影响。

分组可选方案

1 AA测试规避法

基于完全随机抽样在基准指标的先天不确定性, 业内在技术层面不足以规避的前提下, 会采用AA 测试的模式进行基准指标验证 . 即对于不同分组的用户使用同种策略, 进行实验空跑.

观察这两组用户在基准指标上是否存在统计显著性的不同. 若不同, 则说明两组样本本身存在差异, 需要重新分组, 直到基准指标基本一致。

重新跑实验,看看两组的分布不均会不会消失,可参考 A/B实验之辛普森悖论和解决方法

2 RR(Rerandomization)

AA测试的工程进阶即RR(Rerandomization),  即在每次CR分组之后, 验证CR的分组结果组建差异是否小于实验设定阈值(比如我们认为差异小于0.1则组间差异可忽略).

相对于CR而言, RR是通过牺牲计算时间, 进行分组尝试. 即AA测试的工程自动化.  

缺点:个人感觉不适合采用,因为如果差异太大,有可能造成永久循环,还是得不到一个较好的方案结果

3 自适应分组算法(Adaptive)

Adaptive分组方法可以在只分组一次的情况下,让选定的观测指标在分组后每组分布基本一致,可以极大的缩小相对误差。

相比于传统的CR分组,Adaptive分组的算法更加复杂,在遍历人群进行分组的同时,每个组都需要记录目前为止已经分配的样本数,以及已经分配的样本在选定的观测指标上的分布情况。

从分流人群中拿到下一个要分的对象后,会对实验的各个组进行计算,计算该对象如果分配到本组,本组的观测指标分布得分情况。然后综合各个组的预分配得分情况,得到最终各个组对于该实验对象的分配概率。

可产考目前滴滴,美团数据驱动工程化搭建的所研发的Adaptive 自适应算法。

Adaptive 分组流程

image.png

缺点:这种算法思想类似于 贪心算法,样本再选择进入哪个组之前,要每次不断进行大量计算,直接分配概率和间接分配概率,还有平衡系数,分布函数等。其中涉及到一些数学思想,可能理解起来比较费时间。作为各家公司内部数据驱动工程,不会透露太多细节,查阅很久,也没找到具体的一些算法设计模式。

思考可以尝试的想法

场景举例,业务开发中,开发了一种新的学习体验功能,但不确定是否适合全量推广,即想采用AB测试进行实验,且需要个别指标差异性均在0.4%以下。

监督指标设立举例

指标举例 指标定义
用户年龄 0~1岁、1~2岁、2~3岁、3~4岁、4~5岁、5~6岁、6~7岁、7~8岁等用户每个年龄段的占比
用户类型 付费用户, 普通用户
用户完课率 时间均值相同

分成两种模式:

占比模式:一个实验用户,可能身上存在某一种属性的某种类型,例如年龄段,用户类型等。在分布的时候,需要各组之间占比差异较小。

均值模式:该用户对应了某个值,例如用户学习时间,学习的课程等。在分布的时候需要整组内这个值的均值与其他组比较时差异较小。

如何解决占比模式的差异性?

使用普遍思想,一分为二,尽可能均匀划分到实验桶和对照桶中。

举例:

用户类型则抽成两个类型,升级别和体验课转入两种,分别统计这两种类型的人数,随后都一分为二划到两组中。

参考代码:

public List<List<UserObject>> getByUserType(List<UserObject> userObjectList){

       List<List<UserObject>> ans = new ArrayList<>();

       List<UserObject> part1 = new ArrayList<>();

       List<UserObject> part2 = new ArrayList<>();


      //找出不同类型用户

      Map<Integer, List<UserObject>> detailsMap01 = userObjectList.stream()

     .collect(Collectors.groupingBy(UserObject::getUserType));
 

      for(List<UserObject> list:detailsMap01.values()){

         partitionList(list,part1,part2);
      }

     ans.add(part1);

     ans.add(part2);

     return ans;
}
复制代码

如何解决均值模式的差异性?

有可考察的 分割数组成相同平均数的小数组 算法思想,但上述算法里是找到均值完全相同的两子数组,而对于AB实验,只要平均值尽可能相似,差异较小即可。

这里提供一个比较简单近似算法思想:

这些自然数按从大到小排序,然后依次分配到 两组中,每次往当前和最小的组最后添加。

数组划分代码可参考:

public List<List<UserObject>> getByDevideAvgList(List<UserObject> allUserList){

    allUserList.sort((o1, o2) -> o2.getCompleteCoureScore() - o1.getCompleteCoureScore());

    int sum1=0,sum2=0;
    List<UserObject> part1 = new ArrayList<>();
    List<UserObject> part2 = new ArrayList<>();
   for(UserObject userObject:allUserList){
      if(sum1 > sum2){
         part2.add(userObject);
       }else{
        part1.add(userObject);
      }
  }

  List<List<UserObject>> ans = new ArrayList<>();
  ans.add(part1);
  ans.add(part2);
  return ans;
}
复制代码

一维指标与多维指标联系思考

CR完全随机分组法,站在用户id角度上,对 userId 进行哈希后取模,此操作已经非常随机均匀了,可各个指标差异性还是不如人愿。换个角度看问题,哪个指标有差异,就出手尽量均匀化哪里。

当进行AB实验时,如果只有一个监督指标,当然可以比较准确的控制差异性。然而,有多个指标,要从多个维度实验时,该怎样每个指标差异性都能尽量控制到呢?这是一个值得思考的问题。

首先,可以认为每个监督指标它们都是各自独立的,不会相互影响,换句话说,如果它们两三个之间是有联系的,那么则可以取其中一个监督指标代表即可,不用分割出来。

在用户分组时,可采用 指标标号排序法,随机确定指标的执行顺序,指标间互不影响,则可以达到手动干预差异性效果。

具体实验操作步骤

步骤一:随机确定指标执行顺序

用户年龄,完课率,类型三个指标分别定标号,1,2,3,随机确定先执行顺序,例如可按123顺序进行一次实验。

步骤二:依次执行划分成组

每次执行完,组数都会分裂,两边尽可能差异性较小,均匀。

步骤三:从实验A桶和B桶中分别选出一个进行实验

第一次分化的时候,即已经分好了两个桶。选实验组和对照组时候,从两个桶里分别取。例如,取 A桶子组1 和 B桶子组1 进行实验。可选好几组进行实验。

如果只要一个实验组和对照组,要交叉取组,类似这样 A桶子组1 + A桶子组3+B桶子组1+B桶子组3,这样做为了尽量使每个子组都是经过指标计算过的。

步骤四:重试

验证实验组和对照组之间各指标差异,如差异太大,可设置重试几次,重排指标执行顺序再进行分组

把步骤一的执行顺序变化,如改为213,在进行实验,重复以上步骤,判断是否超过差异阀值。 注意:尽量使选出来的实验组或者 对照组都是经过每个监督指标计算控制过的

总的步骤如下图:

image.png

后续优化

1 可采用 4 * 5 * 5 的分法,最终子桶树为整数100,方便 A/B测试分组时,直接取整数流量。

2 波动性较大,不易分均匀的,尽量放在后面去划分。

总结

日常工作中,需要用到业务开发解决某种问题,可以先是进行详细的技术调研,自己独立思考,结合自己的想法,尝试实践一下,要是足以满足业务场景,那最好啦。随着时间流逝,后续可以依次优化迭代,做得更好。

猜你喜欢

转载自juejin.im/post/7042187982400389128