背景
实验工程中用到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 分组流程
缺点:这种算法思想类似于 贪心算法,样本再选择进入哪个组之前,要每次不断进行大量计算,直接分配概率和间接分配概率,还有平衡系数,分布函数等。其中涉及到一些数学思想,可能理解起来比较费时间。作为各家公司内部数据驱动工程,不会透露太多细节,查阅很久,也没找到具体的一些算法设计模式。
思考可以尝试的想法
场景举例,业务开发中,开发了一种新的学习体验功能,但不确定是否适合全量推广,即想采用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,在进行实验,重复以上步骤,判断是否超过差异阀值。 注意:尽量使选出来的实验组或者 对照组都是经过每个监督指标计算控制过的
总的步骤如下图:
后续优化
1 可采用 4 * 5 * 5 的分法,最终子桶树为整数100,方便 A/B测试分组时,直接取整数流量。
2 波动性较大,不易分均匀的,尽量放在后面去划分。
总结
日常工作中,需要用到业务开发解决某种问题,可以先是进行详细的技术调研,自己独立思考,结合自己的想法,尝试实践一下,要是足以满足业务场景,那最好啦。随着时间流逝,后续可以依次优化迭代,做得更好。