A/B 测试显著性计算器
A/B测试显著性计算器根据两个版本的访客数和转化数,使用双比例z检验计算统计显著性。输入对照组和实验组各自的访客与转化数据,即可得到p值、z分数、置信区间和相对提升百分比,并判断结果是否达到常见的95%置信水平。工具帮助产品经理和营销人员判断新版落地页、按钮文案或邮件标题是否真正优于旧版本,避免因随机波动而做出错误决策。
A/B 测试显著性计算器
输入每个变异的访客数和转化数,检查差异是真实的还是只是噪声。
结果
统计显著
在你选择的置信水平下,差异不太可能是由于偶然。
Chance this gap is just luck — lower is stronger.
How far apart the two rates are, in standard errors.
The likely range for the true difference.
文档
一分钟学会 A/B 测试
你改了个按钮、标题或结账流程。版本 A 是旧的,版本 B 是新的。B 看起来更好——它转化了 15% 的访客,而 A 只有 10%。但这个差距可能只是运气吗,就像掷硬币十次可能出现七次正面一样?
这个计算器正是为了回答这个问题。你输入每个版本有多少人看到以及多少人转化,它会告诉你测量的差异是真实的而不是随机噪声的概率。如果这个概率足够高,你就有了赢家。如果不是,你继续运行测试。
要关注的一个关键数字是 p 值:如果两个版本实际上完全相同,看到至少这么大差距的概率。一个小的 p 值(比如低于 0.05)意味着"这种情况很少会发生,所以这个差异可能是真实的"。
如何使用
- 访客数 — 每个版本有多少独特用户看到。
- 转化数 — 其中有多少人做了你关心的事情(购买、注册、点击)。
- 置信水平 — 在宣布赢家之前你想有多确定。95% 是行业标准。
- 假设 — 双边 问"B 与 A 不同吗?"(更安全,默认);单边 问"B 是否比 A 更好?"(更敏感,但你必须在运行测试前决定方向)。
结果实时更新,输入内容保存在页面 URL 中,所以你可以收藏或分享结果。重置 清除表单;加载样本数据 填入一个完整示例。
一个完整的示例
| 访客数 | 转化数 | 转化率 | |
|---|---|---|---|
| A(对照) | 1,000 | 100 | 10.0% |
| B(变异) | 1,000 | 150 | 15.0% |
这是 5 个百分点的绝对提升和 +50% 相对 提升。计算器返回 p 值约为 0.0007 — 低于 0.05 — 所以在 95% 置信水平下,结果是 统计显著的。大致地说,如果两个版本完全相同,你会在不到千分之一的测试中看到这么大的差距。
数学原理(两比例 z 检验)
原理是标准的 两比例 z 检验,这是在入门统计学中教授的测试,也被主流 A/B 测试工具使用。
-
每组的转化率:
其中 是转化数, 是访客数。
-
合并 率,假设(对于零假设)两组共享一个真实率:
-
差异的标准误:
-
z 分数 — 两个率相差多少个标准误:
-
p 值 来自标准正态分布 。双边:。单边:。
-
显著 当 时,其中 (所以在 95% 时为 0.05)。
计算器还报告差异的 置信区间,用 非合并 标准误计算 :真实差距的合理范围。如果该区间排除 0,结果是显著的。
在 95% 置信水平下,z 值超过 ±1.96(阴影尾部,面积的 5%)算作显著。
诚实地解读结果
- 统计显著性不等于商业显著性。 0.1% 的提升可能在统计上是真实的,但不值得发布。
- 不要偷看和提前停止。 每天检查 p 值,一旦跌破 0.05 就停止会增加假阳性率。提前决定样本量,让测试完成。
- 注意样本量的经验法则。 当每组至少有 ~10 个转化 并且 ~10 个非转化时,正态近似是可靠的。用很小的数字时,只把结果当作方向性的。
- 置信度 ≠ B 更好的概率。 95% 置信水平意味着这个 过程 在长期运行中最多有 5% 的时间是错误的 — 这是频率主义的陈述,不是"95% 的概率 B 赢"。
- 一个测试,一个指标。 同时测试多个指标或变异会增加巧合的机会;纠正它(例如 Bonferroni)或预先注册决定测试的单个指标。
自己计算
每个代码片段计算上面工作示例的双边 p 值。
1=2*(1-NORM.S.DIST(
2 (B2/A2 - B1/A1) /
3 SQRT(((B1+B2)/(A1+A2))*(1-(B1+B2)/(A1+A2))*(1/A1+1/A2)),
4 TRUE))
5' A1,B1 = 对照访客数、转化数;A2,B2 = 变异访客数、转化数
61from math import sqrt, erf
2
3def ab_pvalue(n1, x1, n2, x2, two_sided=True):
4 p1, p2 = x1 / n1, x2 / n2
5 p = (x1 + x2) / (n1 + n2)
6 se = sqrt(p * (1 - p) * (1 / n1 + 1 / n2))
7 z = (p2 - p1) / se
8 phi = lambda v: 0.5 * (1 + erf(v / sqrt(2))) # standard normal CDF
9 return 2 * (1 - phi(abs(z))) if two_sided else 1 - phi(z)
10
11print(ab_pvalue(1000, 100, 1000, 150)) # ~0.00072
121// erf via Abramowitz & Stegun 7.1.26
2function erf(x) {
3 const s = x < 0 ? -1 : 1;
4 x = Math.abs(x);
5 const t = 1 / (1 + 0.3275911 * x);
6 const y = 1 - ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t
7 - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x);
8 return s * y;
9}
10const phi = z => 0.5 * (1 + erf(z / Math.SQRT2));
11
12function abPValue(n1, x1, n2, x2, twoSided = true) {
13 const p1 = x1 / n1, p2 = x2 / n2, p = (x1 + x2) / (n1 + n2);
14 const se = Math.sqrt(p * (1 - p) * (1 / n1 + 1 / n2));
15 const z = (p2 - p1) / se;
16 return twoSided ? 2 * (1 - phi(Math.abs(z))) : 1 - phi(z);
17}
18console.log(abPValue(1000, 100, 1000, 150)); // ~0.00072
191public static double abPValue(int n1, int x1, int n2, int x2) {
2 double p1 = (double) x1 / n1, p2 = (double) x2 / n2;
3 double p = (double) (x1 + x2) / (n1 + n2);
4 double se = Math.sqrt(p * (1 - p) * (1.0 / n1 + 1.0 / n2));
5 double z = (p2 - p1) / se;
6 // Phi via the error function; erf approximated (A&S 7.1.26) separately.
7 double phi = 0.5 * (1 + erf(Math.abs(z) / Math.sqrt(2)));
8 return 2 * (1 - phi);
9}
101static double AbPValue(int n1, int x1, int n2, int x2) {
2 double p1 = (double)x1 / n1, p2 = (double)x2 / n2;
3 double p = (double)(x1 + x2) / (n1 + n2);
4 double se = Math.Sqrt(p * (1 - p) * (1.0 / n1 + 1.0 / n2));
5 double z = (p2 - p1) / se;
6 double phi = 0.5 * (1 + Erf(Math.Abs(z) / Math.Sqrt(2)));
7 return 2 * (1 - phi);
8}
91#include <cmath>
2
3double ab_pvalue(int n1, int x1, int n2, int x2) {
4 double p1 = (double)x1 / n1, p2 = (double)x2 / n2;
5 double p = (double)(x1 + x2) / (n1 + n2);
6 double se = std::sqrt(p * (1 - p) * (1.0 / n1 + 1.0 / n2));
7 double z = (p2 - p1) / se;
8 double phi = 0.5 * (1 + std::erf(std::fabs(z) / std::sqrt(2.0)));
9 return 2 * (1 - phi);
10}
111import "math"
2
3func abPValue(n1, x1, n2, x2 float64) float64 {
4 p1, p2 := x1/n1, x2/n2
5 p := (x1 + x2) / (n1 + n2)
6 se := math.Sqrt(p * (1 - p) * (1/n1 + 1/n2))
7 z := (p2 - p1) / se
8 phi := 0.5 * (1 + math.Erf(math.Abs(z)/math.Sqrt2))
9 return 2 * (1 - phi)
10}
111// erf from the `libm` crate; std has no erf.
2fn ab_pvalue(n1: f64, x1: f64, n2: f64, x2: f64) -> f64 {
3 let (p1, p2) = (x1 / n1, x2 / n2);
4 let p = (x1 + x2) / (n1 + n2);
5 let se = (p * (1.0 - p) * (1.0 / n1 + 1.0 / n2)).sqrt();
6 let z = (p2 - p1) / se;
7 let phi = 0.5 * (1.0 + libm::erf(z.abs() / 2f64.sqrt()));
8 2.0 * (1.0 - phi)
9}
101function ab_pvalue($n1, $x1, $n2, $x2) {
2 $p1 = $x1 / $n1; $p2 = $x2 / $n2;
3 $p = ($x1 + $x2) / ($n1 + $n2);
4 $se = sqrt($p * (1 - $p) * (1 / $n1 + 1 / $n2));
5 $z = ($p2 - $p1) / $se;
6 $phi = 0.5 * (1 + erf(abs($z) / sqrt(2)));
7 return 2 * (1 - $phi);
8}
91def ab_pvalue(n1, x1, n2, x2)
2 p1, p2 = x1.to_f / n1, x2.to_f / n2
3 p = (x1 + x2).to_f / (n1 + n2)
4 se = Math.sqrt(p * (1 - p) * (1.0 / n1 + 1.0 / n2))
5 z = (p2 - p1) / se
6 phi = 0.5 * (1 + Math.erf(z.abs / Math.sqrt(2)))
7 2 * (1 - phi)
8end
91# R has this built in: prop.test does the whole thing.
2prop.test(c(100, 150), c(1000, 1000), correct = FALSE)$p.value # ~0.00072
3常见问题
我需要多大的样本量? 足够让有意义的提升可被检测到的量。作为底线,每个变异至少瞄准几百个转化;使用专用的样本量计算器,输入基线率和最小可检测提升。
双边还是单边? 除非你有明确的、预先承诺的理由只关心改进,否则使用双边。单边会加倍敏感性,但禁止对 B 更差做出反应。
为什么计算器显示置信区间? 它显示真实差异的合理范围,不仅仅是是/否的判决。跨越 0 的宽区间意味着"还没有足够数据"。
这是贝叶斯方法吗? 不是 — 这是频率主义 z 检验,最广泛教授和使用的方法。贝叶斯 A/B 工具报告"B 打败 A 的概率"而不是 p 值;两者都有效,它们回答略微不同的问题。