自从到了 marketing 团队,接触了大量 AB Testing 的需求。这里做一下简单总结。
什么是 AB Testing
产品经理、设计师在设计需求的实现时,可能会有多条路径或者说多个方案来达成一个业务目标。
在经过评审之后,一般会权衡利弊,得出一个平衡了用户体验、业务目标和开发成本的最终方案。
但也可能存在一种情况:方案 A、B、C 各有优劣,没办法通过主观判断和讨论得出几个方案谁更优、收益更好。
有一个方法是,同时上线 A、B、C,并将用户流量平均分配到这三者,在固定时间周期后通过埋点等数据来分析哪一个方案效果更好(收益,访问量,订单转化率,下载量等等指标),最后选择出最优方案。
这就是 AB Testing。
例子
marketing 团队大部分 AB 实验都是与 C 端展示相关的,例如文案的调整,模块的设计样式或相对位置等。
例如一个驱使用户下载 App 的 banner:

针对这一个简单的 banner,实验的情况就有多种多样:
实验的种类非常多,一般由产品经理来构思。并且需要确定:
- 实验的人群,地区
- 实验的平台 web / mweb
- 实验的划分
- 划分后每个实验组流量的分配量
- 埋点,曝光 or 行为
- …
- …
关键点和注意事项
关键点:
流量划分
每一个访问的用户,都会有一个设备 ID。通过设备 ID,将用户等量划分,并生成一个 cookie 值,记录用户命中的实验和分组。前端根据命中的实验展示不一样的效果。
实验目标
实验只面向某个地区的群体,或者某类设备的群体。
数据采集
当实验组、控制组被命中时,根据需要上报曝光或者行为事件。作为最后统计的依据。
需要注意的事项:
确保实验的变量唯一
试验周期不宜过长,一般控制在 2 到 3 周,最好不要超过一个月
分析实验结果时,排除其他因素在实验中的影响。
前端实现
前提:获取实验命中情况
流量划分和用户命中由后端计算。前端需要在 node 层请求 api 获取当前用户命中的分组情况(放在客户端请求可能会造成页面闪动和数据误报), 如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| router.get('/home', async function (ctx) { let pageData = {} let experiments = await fetch.get('/experiments/group') pageData.experiments = experiments
ctx.render('home.html', pageData) })
|
这段代码表示,用户命中了实验 1 的 group1 分组,命中了实验 2 的 ctrl 控制组。
首页 home.html 中 js 拿到实验命中情况,渲染出对应的样式。
旧有的实现
在旧的项目中,前端的技术栈是 渲染模板 handlebars + koa,后续引入了 Vue 的开发模式。
实验的实现方式没有做统一,公共代码只提供获取分组的方式,获取之后做什么完全由开发自己控制,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { ABTestTool } from '../ABTestingTool'
export function abtestExperiment1() { ABTestTool({ notation: 'experiment1' }, (exp) => { if (exp['experiment1'] === 'group1') { } if (exp['experiment1'] === 'group2') { $('body').append(`<style>.theme_activities { display: flex;}</style>`) } if (exp['experiment1'] === 'group3') { } }) }
export { abtestExperiment1 }
|
亦或者使用了 Vue 的页面的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { ABTestTool } from '../ABTestingTool' import hitComponent1 from 'com1.vue' import hitComponent2 from 'com2.vue'
let hitComponent = {} export function abtestExperiment1() { ABTestTool({ notation: 'experiment1' }, (exp) => { if (exp['experiment1'] === 'group1') { hitComponent = hitComponent1 } if (exp['experiment1'] === 'group2') { hitComponent = hitComponent2 } }) }
abtestExperiment1()
export { hitComponent }
|
这些实现形式不统一,往往比较杂乱。实验完成后,剔除实验代码也没有统一的办法。
新的实现
handlebars + koa 旧项目的代码将逐渐被 nuxt 替代。强行统一实现的话投入产出比太低。
所以这里只考虑 nuxt 上的实现,旧项目的逻辑保持不变,逐步淘汰。
- 定制规范。因为实验代码原则是临时性质的,为了便于维护和代码整洁,代码的编写需要遵循统一的规范:
- 通过需求单号标识实验代码、静态文件资源、多语言标记,并且合理添加带有单号标记的注释以便代码删除或回退
- 实验代码尽量集中,最大限度的减少对主业务逻辑的侵染,理想情况下,实验代码应该是易移除,可插拔,位置集中,标识明晰的
- 原则上不需要实验开发人员关注到数据上报发送,对于需求依赖的埋点,需要开发集中体现在实验代码中,并且事件命名带有实验标识,以在数据分析时,区别正常业务发的埋点。
- …
- …
- 思路
这里使用了高阶函数的思路,入参为一个配置,AB 函数内部计算命中哪一个实验,最后生成一个 Vue 组件。
代码模拟:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template> <business-component /> ... <abtest-xxxx></abtest-xxxx> ... <abtest-xxxx> <business-component /> </abtest-xxxx> ... </template> <script> import AB, { IABTestingConfigs } from '~/components/common/ab-testing/index.ts' @Component({ components: { abtestXXXX: AB({ experimentName: 'Chat_Btn_Design_AB_1_Mweb', groupComponents: { '实验组1名称': () => import('~/experiments/abtestXXXX/variant1.vue'), '实验组2名称': () => import('~/experiments/abtestXXXX/variant2.vue') }, autoDataSend: true } as IABTestingConfigs) }) </script>
|
在 ~/experiments/abtestXXXX/variant1.vue
实验代码中,需要说明实验的细节,开始结束时间,需求单,如
- AB 函数的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| export default function (config) { const { experimentName, groupComponents } = config
class AB extends Vue { get hitExperiment() { const hitExp = store.state.itExperiment const hitExpGroupName = hitExp.group.name return { id: 'xxx', component: groupComponents groupName: hitExp.group.name, } } mounted() { autoDataSend && this.customTrack() } customTrack() { if (this.hitExperiment) { this.sendTracking() } } render(h: any) { if (this.hitExperiment) { return h(this.hitExperiment.component, { props: { hitExperimentName: experimentName, hitGroupName: this.hitExperiment.groupName, hitExperimentId: this.hitExperiment.id, hitGroupId: this.hitExperiment.groupId, customTrack: this.customTrack, }, attrs: this.$attrs, on: { ...this.$listeners, }, }) } else { return this.$slots.default ? h('div', this.$slots.default) : null } } } return AB }
|
利用高阶组件,可以返回实际,命中的分组对应的组件,展示在页面中。
以上
-
版权声明: 本博客所有文章除特别声明外,均采用
CC BY 4.0 CN协议
许可协议。转载请注明出处!
Жизнь, как качели - то вверх, то вниз.