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 VÀ
- Í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ế.