LLM Quest Academy
← Quay lại Blog

A/B testing với Python: từ experiment design đến statistical power

LLM Quest Editorial·20 thg 4, 2026·3 phút đọc
A/B testing với Python: từ experiment design đến statistical power
A/B testing với Python: từ experiment design đến statistical power

A/B test sai = quyết định sai = mất revenue. Đây là pipeline đúng từ design → analysis với code Python cụ thể.

Vì sao 50% A/B test công ty đang chạy là sai

  • Peeking: stop test sớm khi "thấy có ý nghĩa" → false positive
  • Không tính sample size: test 100 user không thấy khác biệt → conclude sai
  • Multiple comparison: test 5 variant cùng lúc → chance có 1 cái "win" ngẫu nhiên cao
  • SRM (sample ratio mismatch): 50/50 split nhưng thực tế 52/48 → có bug tracking
  • Novelty effect: variant mới hấp dẫn tuần đầu, về baseline tuần sau

Experiment design

1. Define metric

  • Primary metric (1 cái): cái bạn quyết định win/lose. Thường là conversion rate hoặc revenue per user.
  • Guardrail metric (2–3 cái): không được tụt. Ví dụ: bounce rate, latency.
  • Secondary metric (nhiều): insight nhưng không decision.

2. Hypothesize

"Thay button CTA từ 'Buy now' → 'Get started' sẽ tăng conversion rate từ 3% → 3.3% (relative +10%)".

3. Tính sample size

Cần trả lời: ít nhất bao nhiêu user để detect lift 10% với power 80%?

from statsmodels.stats.power import NormalIndPower

effect_size = 0.1 * 0.03 / ((0.03 * 0.97) ** 0.5)  # Cohen's h for proportions
analysis = NormalIndPower()
n_per_group = analysis.solve_power(
    effect_size=effect_size,
    alpha=0.05,
    power=0.8,
    alternative='two-sided'
)
print(f"Need {int(n_per_group)} per group")
# Output: Need ~15000 per group

Nếu traffic là 5000/tuần per variant → cần 3 tuần chạy test. Ít hơn = underpowered.

4. Randomization + tracking

import hashlib

def assign_variant(user_id, experiment_name, variants=('A', 'B')):
    h = hashlib.md5(f"{user_id}:{experiment_name}".encode()).hexdigest()
    return variants[int(h, 16) % len(variants)]

Hash-based sticky: cùng user mỗi lần vào được cùng variant. Tránh flipping.

Analysis

SRM check trước

from scipy.stats import chisquare

n_a, n_b = 5023, 4977
expected = [(n_a + n_b) / 2] * 2
chi2, p = chisquare([n_a, n_b], expected)
if p < 0.001:
    print("SRM detected — fix tracking before analysis")

SRM p < 0.001 = có bug. Không phân tích. Fix tracking đã.

Proportion z-test (binary metric)

from statsmodels.stats.proportion import proportions_ztest

count = [151, 168]      # conversions in A, B
nobs = [5023, 4977]     # total users
z, p = proportions_ztest(count, nobs)

from statsmodels.stats.proportion import proportion_confint
ci_a = proportion_confint(151, 5023, alpha=0.05)
ci_b = proportion_confint(168, 4977, alpha=0.05)
print(f"A: {151/5023:.3%}, 95% CI: {ci_a}")
print(f"B: {168/4977:.3%}, 95% CI: {ci_b}")
print(f"z={z:.2f}, p={p:.4f}")

T-test (continuous metric như revenue)

from scipy import stats
import numpy as np

revenue_a = np.array([...])  # per-user revenue
revenue_b = np.array([...])

t, p = stats.ttest_ind(revenue_b, revenue_a, equal_var=False)  # Welch
lift = (revenue_b.mean() - revenue_a.mean()) / revenue_a.mean()
print(f"Lift: {lift:.2%}, p={p:.4f}")

Bootstrap (robust, không assume normal)

def bootstrap_mean_diff(a, b, n_iter=10000):
    diffs = []
    for _ in range(n_iter):
        sa = np.random.choice(a, len(a), replace=True)
        sb = np.random.choice(b, len(b), replace=True)
        diffs.append(sb.mean() - sa.mean())
    return np.percentile(diffs, [2.5, 97.5])

Dùng bootstrap khi distribution không normal (revenue, session duration).

CUPED (variance reduction)

CUPED dùng pre-experiment data để giảm variance, cho phép test nhanh hơn 20–50%:

theta = np.cov(revenue_pre, revenue_post)[0, 1] / np.var(revenue_pre)
revenue_adj = revenue_post - theta * (revenue_pre - revenue_pre.mean())

Dùng revenue_adj thay cho revenue_post trong t-test. Giảm sample size cần ~30%.

Khi nào stop test

  • Đã đủ sample (đã tính trước) VÀ
  • Tất cả guardrail ok
  • Ít nhất 1 tuần đầy (phủ weekly cycle)

KHÔNG stop vì "đã thấy p < 0.05 rồi".

Báo cáo

Template 1 slide:

  • Hypothesis
  • Metric (primary + guardrail)
  • Sample size + duration
  • Result: lift ± CI, p-value
  • SRM check: pass/fail
  • Guardrail check: pass/fail
  • Decision: ship / kill / iterate + lý do

Kết luận

A/B testing đúng = 80% design + 20% analysis. Nếu sample size không đủ, không bắt đầu. Nếu SRM, không phân tích. Nếu primary lose nhưng guardrail win, cân nhắc kỹ thay vì ship.

Khoá Data Science & Analytics Level 3 có module experimentation + case study 3 A/B test thực tế.

#A/B testing#statistics#Python#CUPED#experiment design
Advertisement