A/B 테스트 결과가 통계적으로 유의미한지 확인합니다. 방문자 및 전환을 입력하여 p-값, z-점수, 신뢰 구간 및 상대 상승을 얻습니다.
버튼, 헤드라인, 또는 체크아웃 흐름을 변경했습니다. 버전 A는 이전 버전, 버전 B는 새로운 버전입니다. B는 보기에는 더 좋아 보이고 - 방문자의 15%가 전환된 반면 A는 10%였습니다. 그런데 이 차이가 단순한 운일 수도 있지 않을까요? 동전을 10번 던져서 앞면이 7번 나올 수 있는 것처럼요?
이 계산기가 정확히 그 질문에 답변합니다. 각 버전을 본 사람의 수와 전환된 사람의 수를 입력하면, 측정한 차이가 실제인지 아니면 무작위 오류인지 확률을 알려줍니다. 그 확률이 충분히 높으면, 승자가 있는 것입니다. 그렇지 않으면, 테스트를 계속 진행합니다.
주의해야 할 한 가지 숫자는 p-값입니다: 두 버전이 실제로 동일했다면 이 정도 크기의 차이를 볼 수 있는 확률입니다. 작은 p-값(예: 0.05 미만)은 "이런 일이 운으로 거의 일어나지 않으므로, 차이는 아마도 실제"라는 의미입니다.
결과는 실시간으로 업데이트되고 입력은 페이지 URL에 저장되므로, 결과를 북마크하거나 공유할 수 있습니다. 재설정은 양식을 지우고, 샘플 데이터 로드는 작업된 예를 채웁니다.
| 방문자 | 전환 | 비율 | |
|---|---|---|---|
| A (제어) | 1,000 | 100 | 10.0% |
| B (변형) | 1,000 | 150 | 15.0% |
이는 5 포인트 절대 상승이고 +50% 상대 상승입니다. 계산기는 약 0.0007의 p-값을 반환합니다 — 0.05 미만 — 따라서 95% 신뢰도에서 결과는 통계적으로 유의미합니다. 대략적으로, 두 버전이 정말로 동일했다면, 천 번의 테스트 중 한 번 미만으로 이 정도 크기의 차이를 볼 수 있을 것입니다.
뒷면에서 이는 표준 양비율 z-검정이고, 입문 통계에서 가르치는 동일한 검정이며 주류 A/B 도구에서 사용됩니다.
각 그룹의 전환율:
여기서 는 전환이고 은 방문자입니다.
합산된 비율, (귀무가설에 대해) 두 그룹이 하나의 참 비율을 공유한다고 가정:
차이의 표준 오류:
z-점수 — 두 비율이 얼마나 많은 표준 오류만큼 떨어져 있는지:
p-값은 표준 정규 분포 에서 나옵니다. 양측: . 단측: .
유의미함은 일 때, 여기서 (95%에서 0.05).
계산기는 또한 신뢰 구간을 차이에 대해 보고하고, 비합산된 표준 오류 로 계산됩니다: 참 차이에 대한 합리적인 범위입니다. 그 구간이 0을 제외하면, 결과는 유의미합니다.
95% 신뢰도에서 ±1.96 표준 오류를 초과하는 z (음영 처리된 꼬리, 면적의 5%)가 유의미한 것으로 계산됩니다.
각 스니펫은 위의 작업된 예에 대해 양측 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 = control visitors, conversions ; A2,B2 = variation visitors, conversions
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 도구는 p-값 대신 "B가 A를 이길 확률"을 보고합니다. 둘 다 유효하고 약간 다른 질문에 답합니다.
귀하의 워크플로에 유용할 수 있는 더 많은 도구를 발견하세요.