A/B测试
什么是A/B测试
想象一下,你是一位魔法师,手中握有两种不同配方的魔药。为了找出哪一种更能增强魔法力量,你决定在不知情的学徒中进行一场秘密测试。这就是A/B测试的概念源头——它源自生物医学领域的「双盲测试」,其中参与者被随机分为两组,分别接受安慰剂和实际药物,以此评估药物的效果。
在数字世界里,我们的“魔药”是APP或者Web应用的新功能或设计改动。我们会将用户随机分成两个群体:一组体验新策略(A),另一组则继续使用现有版本(B)。通过对比两组用户的互动数据,我们可以科学地判断新产品特性是否带来了预期的改进。如果新策略证明有效,那么就像发现了一种更强大的魔药一样,我们就会用它来替代旧有方案。
为什么要进行A/B测试
- 假设你想要知道一款新的魔法书是否能提高学生的成绩。如果你只在同一群学生中先后测试不同的书籍,或者在不同班级同时测试,结果可能会受到时间变化或学生个体差异的影响。因此,不采用A/B测试的话:
- 假设1:即使是对同一人群的前后比较,由于环境、心情等因素的变化,行为本身可能已经有所不同。
- 假设2:当面对不同人群时,即便是在相同的时间段内,各群体的行为模式也可能千差万别。
- A/B测试的强大之处在于它能够最大限度地减少这些外部因素对实验结果的干扰,提供最纯净的“因果效应”测量方法,确保我们真正了解所做改变的实际影响。
A/B测试流程
要施展这个神奇的实验法术,你需要遵循以下步骤:
设定咒语(假设)
:首先明确你想验证的问题或假设。划分阵营(分配)
:接着,把你的用户像分院帽那样公平地分成几个小组。施放幻象(变体)
:为每个小组展示独特的内容或体验。记录预言(追踪)
:仔细观察并记录每组成员的行为轨迹。揭示真相(结果)
:最后分析数据,得出结论。
- 对于开发人员来说,这门技艺的关键在于两点:
编织法阵(接入A/B Test)
:根据用户所属的实验组,编写代码以实现不同的逻辑路径。封印卷轴(实验后处理)
:无论实验成功与否,都需要清理代码,移除不再需要的部分,以便维持系统的整洁与高效。
- 记住,每一次成功的A/B测试都是通往更好用户体验的一块基石。
GrowthBook
集成GrowthBook SDK
- 在数字产品的世界里,每一次更新都是一场冒险。为了确保新的特性和功能能为用户带来真正的价值,我们需要一个可靠的向导——这就是GrowthBook登场的时刻。
准备工作
- 让我们以Java为例,探索如何将GrowthBook的SDK集成到我们的项目中。想象一下,你正在建立一座通往未知世界的桥梁:
- 未连接状态:此时,你的应用程序就像是一座孤岛,与外界隔绝。
- 安装依赖并首次请求:一旦你安装了必要的依赖项,并成功发出获取激活特征接口的请求,这座桥梁便开始构建。通过下面的代码片段,你可以建立起与GrowthBook的第一次对话:
1
2
3
4
5
6
7
8
9
10private static final String FEATURES_ENDPOINT = "http://10.211.10.238:3100/api/features/sdk-MYY3Wl3O5NFQYksN";
public String getFeatures() throws Exception {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url(FEATURES_ENDPOINT)
.get()
.build();
return JSONObject.parseObject(okHttpClient.newCall(request).execute().body().string()).getString("features");
}
编写回调
- 每当有用户触发实验时,我们就需要捕捉这一刻的信息,并将其安全地保存起来。这不仅是对历史的铭记,也是对未来分析的基础。GrowthBook为我们提供了一个回调函数,可以用于处理每次实验触发时的数据存储逻辑:
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
26private final TrackingCallback trackingCallback = new TrackingCallback() {
public <ValueType> void onTrack(Experiment<ValueType> experiment, ExperimentResult<ValueType> result) {
log.info("用户触发了实验: " + experiment.toJson());
log.info("被分配到的变体: " + result.toJson());
experimentInfoMapper.insert(new ExperimentInfo()
.setUserId(result.getHashValue())
.setExperimentKey(experiment.getKey())
.setVariations(JSONObject.toJSONString(experiment.getVariations()))
.setWeights(JSONObject.toJSONString(experiment.getWeights()))
.setIsActive(experiment.getIsActive())
.setCoverage(experiment.getCoverage())
.setNamespace(JSONObject.toJSONString(experiment.getNamespace()))
.setForce(experiment.getForce())
.setHashAttribute(experiment.getHashAttribute())
.setFeatureId(result.getFeatureId())
.setVariationValue(JSONObject.toJSONString(result.getValue()))
.setVariationId(result.getVariationId())
.setInExperiment(result.getInExperiment())
.setHashUsed(result.getHashUsed())
.setHashValue(result.getHashValue())
.setTimestamp(LocalDateTime.now())
);
}
};
定制用户属性
- 为了让实验更加精准,我们需要为每位用户提供定制化的属性。这些属性不仅包括内置的基本信息,还可以包含自定义的标签,以便更好地理解和预测用户行为。例如,在后端项目中,我们可以使用用户的token作为ID,确保同一用户始终获得一致的实验体验
1
2
3
4
5JSONObject userAttributesObj = new JSONObject();
userAttributesObj.put("id", JwtHelper.getUserId(request.getHeader("Authorization"))); // 使用用户token
// 添加更多属性...
String userAttributesJson = userAttributesObj.toString();
growthBook.setAttributes(userAttributesJson);
构建GrowthBook实例
- 接下来,是时候创建GrowthBook实例了。这个步骤就像是给每个用户编织一条独一无二的命运之路,根据他们的特性决定他们将体验到的产品版本。我们将之前获取的特征JSON、用户属性以及回调函数组合在一起:
- 创建GrowthBook实例
1
2
3
4
5
6
7GBContext context = GBContext.builder()
.featuresJson(featuresJson) // 获取的开启特征JSON
.attributesJson(userAttributesObj.toString()) // 用户属性
.trackingCallback(trackingCallback) // 实验跟踪回调
.build();
GrowthBook growthBook = new GrowthBook(context);
调用getFeatureValue
- 最后,当我们调用getFeatureValue方法时,GrowthBook会基于传入的用户属性和预设规则,智能地选择最适合的路径。这一过程如同解开命运的谜题,指引着产品的发展方向:
1
String value = growthBook.getFeatureValue("my-feature", "defaule_value");
后端落地方案
- 为了简化流程,建议在拦截器中初始化用户属性,这样可以确保每个请求都能自动携带正确的用户信息,而无需在每个接口中重复设置。这一步骤就像是给每个请求附上了一张隐形的通行证,使其顺利通行于系统的各个角落
1
2
3
4
5
6
7
8
9
10
11
12
public class HeaderInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 组装用户属性,可以从请求头中获取信息
JSONObject userAttributes = new JSONObject();
userAttributes.put("id", JwtHelper.getUserId(request.getHeader("Authorization")));
request.setAttribute("userAttributes", userAttributes.toString());
return true;
}
}
后端接口
- 当一切准备就绪,剩下的就是让GrowthBook带领我们进入实际的应用场景。通过以下示例代码,我们可以看到如何在一个简单的GET接口中实现A/B测试的逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public String runABTest(HttpServletRequest request) throws Exception {
String featuresJson = getFeatures();
String userAttributesJson = (String) request.getAttribute("userAttributes");
GBContext context = GBContext.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributesJson)
.trackingCallback(trackingCallback)
.build();
GrowthBook growthBook = new GrowthBook(context);
// 这里的blue是当growthBook服务不可用时的默认值,不会因为growthBook挂掉导致严重问题
String variant = growthBook.getFeatureValue("special_button_color", "blue");
log.info("输出结果:{}", variant);
// 根据variant执行不同的业务逻辑;这里也可以写if-else判断
your_method(variant);
return variant;
} 在这个过程中,我们还考虑到了代码的可维护性,尽量减少硬编码的if-else结构,转而采用参数化的方式调用方法,既保持了灵活性,又降低了未来调整的成本。
通过以上步骤,你已经掌握了如何利用GrowthBook进行A/B测试的艺术。无论是优化用户体验还是推动业务增长,GrowthBook都将是你不可或缺的伙伴。
设计实验
设计一个成功的A/B测试,就像是精心策划一场魔法仪式,每个步骤都至关重要。让我们一步步揭开这个神秘的过程。
- 创建特征:编织你的魔咒
首先,你需要定义你想要测试的新特性或改动。这一步骤如同选择魔法咒语中的关键词一样重要。你可以通过GrowthBook的界面来创建这些特征,并为它们赋予独特的标识符。 - 指定规则:设定咒语的力量范围
接下来,为每一个特征指定激活条件。这意味着定义在什么情况下该特性应该对用户可见或生效。你可以根据用户的属性(如地理位置、设备类型等)来精细地控制这一过程。 - 构建实验基础信息:建立魔法实验室
在开始实验之前,确保所有必要的信息都已经准备就绪。包括实验的目标、假设以及预期的结果。这些都是后续评估实验成功与否的重要依据。 - 编写分流策略:规划命运的分岔路
这里是你决定如何将用户按比例分配到不同实验组的地方 - 选择目标用户:召唤你的试验者
最后,确定哪些用户将参与此次实验。这可能涉及到筛选出特定类型的用户,或者简单地随机选取一部分人作为样本。
- 创建特征:编织你的魔咒
编写测试代码
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
public String runABTest(HttpServletRequest request) throws Exception {
// 获取并初始化 GrowthBook 实例
GBContext context = GBContext.builder()
.featuresJson(getFeatures())
.attributesJson((String) request.getAttribute("userAttributes"))
.trackingCallback(trackingCallback)
.build();
GrowthBook growthBook = new GrowthBook(context);
// 根据用户属性和特征规则进行分流
String variant = growthBook.getFeatureValue("special_button_color", "default");
log.info("输出结果:{}", variant);
return variant;
}
public Map<String, Integer> abTestStatistics() {
OkHttpClient client = new OkHttpClient();
Map<String, Integer> resultCount = new HashMap<>();
// 模拟100次请求以获取统计数据
for (int i = 0; i < 100; i++) {
try {
Request request = new Request.Builder()
.url("http://localhost:8180/paperReduction/api/test/ab")
.header("X-Special-Header", "true")
.get()
.build();
Response response = client.newCall(request).execute();
String result = response.body().string();
resultCount.merge(result, 1, Integer::sum);
} catch (Exception e) {
log.error("请求失败: {}", e.getMessage());
}
}
log.info("A/B 测试分布结果: {}", resultCount);
return resultCount;
}根据上述代码运行的结果,我们观察到流量分配为50%,即只有半数的流量执行了实验,而另一半则保持默认设置(例如按钮颜色为蓝色)。这样的结果有助于我们初步了解不同变体之间的差异。
1
2curl http://localhost:8180/paperReduction/api/test/ab/stats
{"red":9,"green":15,"blue":65,"yellow":11}
数据分析
- 数据分析是理解实验影响的关键。它帮助我们将原始数据转化为有价值的见解,从而指导未来的决策。
创建事实表
- 为了更好地组织和查询数据,我们需要构建一个包含所有相关信息的事实表。这就像是一本记录了每次实验细节的魔法之书。
- GrowthBook并没有准确的获取字段数据类型,我们需要调整某些字段
创建指标
Proportion(比例):衡量某个事件发生的频率,如点击率或转化率。它适用于二元事件,并能提供关于成功率的直接洞察。
Retention(留存率):评估用户在一段时间内的持续活跃程度。这对于了解产品的粘性和用户忠诚度尤为重要。
Mean(平均值):计算用户行为的平均水平,如订单金额或会话时长。它对于连续性数据非常有用,但需要注意的是,极值可能会扭曲结果。
Ratio(比率):对比两个相关数值的比例关系,比如每次会话的平均点击数。这种方法能够揭示复杂的行为模式,超越简单的比例或平均值所能表达的内容。
示例
- 创建一个用户支付金额总和的平均值
可以看到周期范围内的用户平均金额变化及分布
从实验数据中,我们发现绿色按钮似乎更受用户欢迎。在统计学中,我们一般认为置信水平达到95%以上才能确认结果的有效性。
- 一旦实验结束,我们可以设置胜利的一方(在这里是绿色按钮)成为新的标准配置,所有流量都将被导向这一选项。
调用接口验证1
2curl http://localhost:8180/paperReduction/api/test/ab/stats
{"green":100}
注意事项
在正式开展A/B测试之前,务必先进行AA测试,以保证实验系统的可靠性和准确性。AA测试的作用包括:
- 分流均匀性检验:确保实验组和对照组之间的用户分配是公平且随机的。
- 埋点检查:验证数据收集机制是否正常工作,避免因数据质量问题导致误判。
- 历史数据重播:模拟真实场景下的数据流,检测系统性能并生成p值分布图,以此判断实验环境是否稳定。
- 通过遵循这些最佳实践,我们可以确保每一次A/B测试都能带来有价值的成果,助力产品不断优化和发展。