A/B Testing Python 2026: SciPy & statsmodels (Hướng Dẫn)

Hướng dẫn A/B testing trong Python 2026 với SciPy, statsmodels và PyMC 5: power analysis, t-test, z-test, Bayesian, CUPED và các sai lầm thường gặp.

A/B Testing Python 2026: SciPy Guide

Cập nhật: 18 Tháng 9, 2026

A/B testing trong Python là phương pháp so sánh hai biến thể (control và treatment) của một sản phẩm hoặc mô hình bằng thí nghiệm ngẫu nhiên có kiểm soát, rồi dùng thống kê suy diễn (chủ yếu qua scipy.stats và statsmodels.stats) để kết luận xem sự khác biệt có ý nghĩa thống kê hay không. Bài viết này đi qua toàn bộ quy trình: từ thiết kế thí nghiệm, power analysis, kiểm định t-test và z-test, cho tới Bayesian testing với PyMC 5 và variance reduction bằng CUPED. Tất cả đều có code chạy được và diễn giải toán học đằng sau mỗi bước.

Thú thật, ở dự án gần nhất tôi từng ship một feature "thắng" A/B test với p = 0.03, và tuần sau nó biến mất trong metric production. Sau khi debug, hoá ra team đã peek dữ liệu ba lần rồi mới dừng. Bài học đó khiến tôi viết bài này kỹ hơn phần lớn tài liệu tôi từng đọc trên mạng.

  • Bắt đầu bằng power analysis (statsmodels.stats.power) để tính sample size cần thiết trước khi chạy thí nghiệm. Không được peeking.
  • Dùng scipy.stats.ttest_ind(equal_var=False) (Welch's t-test) cho continuous metric, và statsmodels.stats.proportion.proportions_ztest cho conversion rate.
  • CUPED (Deng et al., 2013) giảm variance 20–60% bằng biến pre-experiment, tương đương gấp đôi power mà không cần thêm user.
  • Bayesian A/B testing với PyMC 5 trả về P(B > A) trực tiếp, tránh diễn giải sai p-value cho stakeholder.
  • Khi chạy nhiều test đồng thời, dùng Benjamini–Hochberg FDR thay vì Bonferroni để giữ power.
  • Sequential testing (mSPRT hoặc always-valid CI) cho phép peek an toàn nếu bạn cần stop sớm khi có signal.

A/B testing trong Python là gì và tại sao statistical testing quan trọng?

A/B testing (trong thống kê học gọi là randomized controlled trial, viết tắt RCT) là kỹ thuật đo tác động nhân quả của một thay đổi lên chỉ số kinh doanh. Người dùng được phân chia ngẫu nhiên vào nhóm control (giữ nguyên) và nhóm treatment (áp dụng thay đổi), rồi so sánh chỉ số quan tâm: conversion rate, revenue per user, click-through rate, hoặc thời gian trên trang.

Tính ngẫu nhiên đảm bảo rằng bất kỳ khác biệt nào giữa hai nhóm về mặt kỳ vọng đều do treatment gây ra chứ không phải do confounding (biến gây nhiễu). Đây là điểm mà nhiều tài liệu mạng bỏ qua: không có randomization thì bạn chỉ đang mô tả tương quan, không được phép suy luận nhân quả.

Nhưng randomization chỉ đảm bảo tính chính xác về kỳ vọng thôi. Với bất kỳ mẫu hữu hạn nào, luôn có sampling noise. Statistical testing chính là bộ công cụ định lượng noise đó và trả lời câu hỏi cốt lõi: "khác biệt tôi quan sát được có nằm ngoài dao động ngẫu nhiên hay không?". Trong Python năm 2026, hai thư viện chuẩn phục vụ mục đích này là SciPy stats (kiểm định phi tham số, phân phối, bootstrap) và statsmodels (power analysis, proportion tests, GLMs, multiple testing correction). Cuốn Trustworthy Online Controlled Experiments (Kohavi, Tang, Xu, Cambridge University Press, 2020) là tham chiếu bắt buộc cho bất kỳ ai xây dựng experimentation platform.

Trong pipeline sản xuất, dữ liệu thí nghiệm thường được lưu ở Parquet hoặc DuckDB rồi load vào pandas hoặc Polars để phân tích. Nếu bạn chưa quen với truy vấn SQL trên DataFrame, hãy đọc bài DuckDB Python 2026. Đó là công cụ tuyệt vời để agg dữ liệu thí nghiệm hàng triệu dòng trước khi test.

Thiết kế thí nghiệm và tính sample size với power analysis

Trước khi chạy bất kỳ thí nghiệm nào, bạn phải chọn bốn tham số: significance level (α, thường 0.05, tức xác suất chấp nhận false positive), power (1 − β, thường 0.80, xác suất phát hiện được hiệu nếu nó thực sự tồn tại), minimum detectable effect (MDE, hiệu ứng nhỏ nhất mà bạn quan tâm), và baseline variance của metric. Từ bốn tham số đó, sample size cần thiết trên mỗi nhóm được tính bằng công thức Cohen (1988). statsmodels đóng gói sẵn API:

from statsmodels.stats.power import TTestIndPower, NormalIndPower
from statsmodels.stats.proportion import proportion_effectsize

# 1. Continuous metric: revenue per user
#    Baseline mean = $10, std = $8, MDE = $0.20 (2% lift)
effect_size = 0.20 / 8  # Cohen's d = 0.025 (rất nhỏ)
n_per_group = TTestIndPower().solve_power(
    effect_size=effect_size,
    alpha=0.05,
    power=0.80,
    alternative='two-sided',
)
print(f"Sample size mỗi nhóm: {n_per_group:,.0f}")   # ~25,000

# 2. Proportion metric: conversion rate
#    Baseline 5%, target 5.5% (10% relative lift)
h = proportion_effectsize(0.055, 0.05)               # arcsine-transformed effect
n = NormalIndPower().solve_power(effect_size=h, alpha=0.05, power=0.80)
print(f"Sample size mỗi nhóm: {n:,.0f}")             # ~31,000

Cohen's d là hiệu ứng chuẩn hoá, bằng khác biệt trung bình chia cho độ lệch chuẩn gộp. Với d = 0.025 (rất nhỏ), bạn cần khoảng 25.000 mẫu mỗi nhóm để đạt 80% power. Với conversion rate, transform arcsine của Cohen (h) được dùng thay vì d thô. Nó ổn định variance ở gần biên 0 và 1.

Kohavi và đồng tác giả nhấn mạnh trong Trustworthy Online Controlled Experiments rằng power analysis phải là bước bắt buộc trong quy trình launch experiment. Không có nó, bạn không biết kết quả "no significant difference" nghĩa là "không có hiệu ứng" hay "không đủ dữ liệu để phát hiện". Với các nền tảng như Optimizely và Statsig, tính toán này được tự động, nhưng khi tự viết pipeline bạn phải làm rõ.

Two-sample t-test cho continuous metric với SciPy

Với chỉ số liên tục (revenue per user, thời gian trên trang, số session), kiểm định phổ biến là Welch's t-test. Không như Student's t-test cổ điển, Welch không giả định phương sai bằng nhau giữa hai nhóm, và trong thực tế nó gần như không bao giờ tệ hơn Student. SciPy 1.14 (2024) đã đặt equal_var=False làm giá trị mặc định được khuyến nghị:

import numpy as np
from scipy import stats

# Giả lập dữ liệu thí nghiệm: 25k user mỗi nhóm
rng = np.random.default_rng(seed=42)
control   = rng.normal(loc=10.00, scale=8.0, size=25_000)
treatment = rng.normal(loc=10.20, scale=8.0, size=25_000)   # +2% lift

# Welch's t-test
res = stats.ttest_ind(treatment, control, equal_var=False, alternative='two-sided')
print(f"t = {res.statistic:.3f}, p = {res.pvalue:.4f}, df = {res.df:.1f}")

# Khoảng tin cậy 95% cho hiệu số
diff = treatment.mean() - control.mean()
se   = np.sqrt(treatment.var(ddof=1)/len(treatment)
               + control.var(ddof=1)/len(control))
ci   = stats.t.interval(0.95, df=res.df, loc=diff, scale=se)
print(f"Lift: ${diff:.3f} (95% CI: [${ci[0]:.3f}, ${ci[1]:.3f}])")

Nếu p-value dưới α và khoảng tin cậy không chứa 0, ta bác bỏ null hypothesis. Nhưng đừng dừng ở p-value. Hãy luôn báo cáo effect size và confidence interval. American Statistical Association (2016) đã ra tuyên bố chính thức chỉ trích việc lạm dụng ngưỡng p < 0.05, nhấn mạnh rằng p-value không phải là xác suất hypothesis đúng.

Khi dữ liệu vi phạm nặng giả định normality (chẳng hạn revenue có phân phối heavy-tailed, log-normal, hoặc zero-inflated), dùng Mann–Whitney U (stats.mannwhitneyu) hoặc bootstrap khoảng tin cậy. Bootstrap thường phù hợp hơn cho A/B testing trên revenue vì nó không giả định phân phối:

def mean_diff(x, y, axis):
    return x.mean(axis=axis) - y.mean(axis=axis)

res_boot = stats.bootstrap(
    data=(treatment, control),
    statistic=mean_diff,
    method='BCa',          # Bias-corrected accelerated (Efron 1987)
    n_resamples=10_000,
    random_state=rng,
    vectorized=True,
)
lo, hi = res_boot.confidence_interval
print(f"Bootstrap 95% CI: [${lo:.3f}, ${hi:.3f}]")

Bootstrap BCa (bias-corrected accelerated) hiệu chỉnh cho skewness của phân phối thống kê. Đây là mặc định hợp lý cho hầu hết chỉ số online. Với 10.000 resamples, chi phí tính toán trên 50.000 điểm chỉ vài giây trên laptop hiện đại.

Two-proportion z-test cho conversion rate với statsmodels

Với chỉ số nhị phân (converted / not converted, clicked / not clicked, retained / churned), dùng proportions_ztest từ statsmodels. Đây là Wald test dựa trên xấp xỉ chuẩn của phân phối binomial, chính xác khi mỗi nhóm có ít nhất 30 conversions và sample size đủ lớn để n·p·(1−p) > 5.

from statsmodels.stats.proportion import proportions_ztest, proportion_confint

# Control: 1,530 conversions / 31,000 users (4.94%)
# Treatment: 1,720 conversions / 31,000 users (5.55%)
successes = np.array([1720, 1530])
n_obs     = np.array([31_000, 31_000])

z_stat, p_val = proportions_ztest(successes, n_obs, alternative='two-sided')
print(f"z = {z_stat:.3f}, p = {p_val:.4f}")

# CI cho từng nhóm — Wilson score interval chính xác với p gần 0 hoặc 1
for label, s, n in zip(["Treatment", "Control"], successes, n_obs):
    lo, hi = proportion_confint(s, n, alpha=0.05, method='wilson')
    print(f"{label}: {s/n:.4f} (95% CI: [{lo:.4f}, {hi:.4f}])")

# Relative lift với delta method CI
p_t, p_c = successes[0]/n_obs[0], successes[1]/n_obs[1]
rel_lift = (p_t - p_c) / p_c
se_rel   = np.sqrt(p_t*(1-p_t)/(n_obs[0]*p_c**2)
                   + p_c*(1-p_c)/(n_obs[1]*p_c**2))
print(f"Relative lift: {rel_lift:.2%} ± {1.96*se_rel:.2%}")

Bayesian A/B testing với PyMC 5

Frequentist testing trả về p-value: xác suất quan sát dữ liệu ít nhất là cực đoan này giả định null đúng. Đây là khái niệm mà ngay cả nhà thống kê chuyên nghiệp cũng hay diễn giải sai. Bayesian A/B testing đảo ngược câu hỏi. Nó tính trực tiếp P(treatment > control | data), thứ mà stakeholder thực sự muốn biết. Kết quả có thể diễn giải cho product manager mà không cần một khoá thống kê 3 tín chỉ.

import pymc as pm
import arviz as az

with pm.Model() as model:
    # Prior Beta(1, 1) = uniform trên [0, 1], weakly informative
    p_a = pm.Beta("p_a", alpha=1, beta=1)
    p_b = pm.Beta("p_b", alpha=1, beta=1)

    # Likelihood binomial
    pm.Binomial("obs_a", n=31_000, p=p_a, observed=1530)
    pm.Binomial("obs_b", n=31_000, p=p_b, observed=1720)

    # Đại lượng quan tâm
    lift     = pm.Deterministic("lift", p_b - p_a)
    rel_lift = pm.Deterministic("rel_lift", (p_b - p_a) / p_a)

    idata = pm.sample(2000, tune=1000, target_accept=0.9,
                      random_seed=42, chains=4)

# P(B > A): con số stakeholder muốn biết
prob_b_beats_a = float((idata.posterior["lift"] > 0).mean())
print(f"P(treatment > control): {prob_b_beats_a:.3f}")

# Expected loss nếu chọn nhầm B khi thực ra A tốt hơn
loss_choose_b = float(idata.posterior["lift"].where(
    idata.posterior["lift"] < 0, other=0).mean())
print(f"Expected loss chọn nhầm B: {abs(loss_choose_b):.5f}")

az.plot_posterior(idata, var_names=["rel_lift"], hdi_prob=0.95)

Kết quả tiêu biểu: P(B > A) = 0.998 với 95% HDI (highest density interval) của relative lift là khoảng [+2.4%, +19.1%]. Cách diễn giải cho product manager: "có 99.8% xác suất version B tốt hơn, mức tăng kỳ vọng nằm giữa 2.4% và 19.1%". PyMC 5 (2024, sampler Nutpie viết bằng Rust) chạy nhanh hơn PyMC 4 tới 5 lần với model kích thước trung bình.

Tiêu chíFrequentist (SciPy / statsmodels)Bayesian (PyMC 5)
Đầu ra chínhp-value, khoảng tin cậyPosterior distribution, P(B > A), expected loss
Diễn giải cho stakeholderTrừu tượng, dễ hiểu saiTrực quan, dạng xác suất
Prior informationKhông dùng đượcTích hợp qua prior distribution
Peeking / stop sớmCần correction (SPRT hoặc always-valid CI)An toàn nếu prior cố định từ đầu
Chi phí tính toánMillisecondsVài giây đến vài phút (MCMC)
Sample size nhỏKém chính xácVẫn cho posterior hợp lý
Regulator acceptanceRất cao (chuẩn mực từ 1930s)Đang tăng (FDA dùng cho drug trials)

CUPED: kỹ thuật giảm variance từ Microsoft

Kỹ thuật CUPED (Controlled-experiment Using Pre-Experiment Data) do Deng, Xu, Kohavi và Walker (Microsoft ExP, 2013) đề xuất là kỹ thuật giảm variance được dùng nhiều nhất trong ngành experimentation. Ý tưởng thế này: nếu bạn có metric của cùng người dùng ở giai đoạn pre-experiment (X), bạn có thể regress metric thí nghiệm Y lên X và test trên residual. Variance của residual thấp hơn nhiều so với Y gốc vì phần lớn "user-specific noise" đã bị hấp thụ vào X.

import numpy as np
from scipy import stats

def cuped(y_treat, y_ctrl, x_treat, x_ctrl):
    """
    y_*: metric trong thời gian thí nghiệm
    x_*: metric cùng user trong pre-experiment window (thường 2-4 tuần trước)
    Trả về: metric đã điều chỉnh và hệ số theta tối ưu.
    """
    x_all  = np.concatenate([x_treat, x_ctrl])
    y_all  = np.concatenate([y_treat, y_ctrl])
    # Theta tối ưu theo Deng et al. (2013), công thức (2)
    theta  = np.cov(x_all, y_all, ddof=1)[0, 1] / np.var(x_all, ddof=1)
    x_bar  = x_all.mean()
    y_treat_adj = y_treat - theta * (x_treat - x_bar)
    y_ctrl_adj  = y_ctrl  - theta * (x_ctrl  - x_bar)
    return y_treat_adj, y_ctrl_adj, theta

# Giả lập: user có metric tương quan giữa hai giai đoạn
x_treat_pre = rng.normal(loc=9.0, scale=7.0, size=25_000)
x_ctrl_pre  = rng.normal(loc=9.0, scale=7.0, size=25_000)
treatment   = 0.7 * x_treat_pre + rng.normal(loc=3.14, scale=5.0, size=25_000)
control     = 0.7 * x_ctrl_pre  + rng.normal(loc=3.00, scale=5.0, size=25_000)

y_t_adj, y_c_adj, theta = cuped(treatment, control, x_treat_pre, x_ctrl_pre)
res = stats.ttest_ind(y_t_adj, y_c_adj, equal_var=False)
var_before = np.var(treatment, ddof=1)
var_after  = np.var(y_t_adj,   ddof=1)

print(f"theta = {theta:.3f}")
print(f"Var reduction: {1 - var_after/var_before:.1%}")
print(f"p-value sau CUPED: {res.pvalue:.4f}")

Trong pipeline sản xuất, dữ liệu pre-experiment thường được join với dữ liệu experimental qua user_id. Với bảng lớn, Polars nhanh hơn pandas 5–10 lần cho join scale hàng chục triệu dòng. Deng và đồng tác giả báo cáo trên dữ liệu Bing rằng CUPED giảm variance 20–60% cho các chỉ số hành vi phổ biến. Tương đương tăng power như thể bạn có gấp đôi sample size, hoàn toàn miễn phí. Booking.com và Netflix cũng đã công bố kết quả tương tự trong các engineering blog post.

Peeking problem và sequential testing

Peeking, tức nhìn kết quả trước khi đạt sample size dự kiến, làm phồng false positive rate rất nghiêm trọng. Với thí nghiệm chạy 14 ngày và peek mỗi ngày, α thực tế có thể vọt từ 5% lên hơn 25% (Johari et al., 2017). Tôi hit bug này ở dự án đầu tiên: dashboard cập nhật real-time và stakeholder nào cũng "chỉ liếc thôi". Kết quả là một loạt "winners" giả. Có ba cách xử lý phổ biến:

  1. Không peek. Cam kết trước sample size và chờ đến hết. Đơn giản nhất, và là mặc định trong nhiều tổ chức không có infrastructure thống kê chuyên sâu.
  2. Sequential Probability Ratio Test (Wald, 1945) hoặc mSPRT (Johari et al., 2017). Cho phép stop sớm với giới hạn Type I error được kiểm soát chặt chẽ. Optimizely dùng mSPRT trong sản phẩm Stats Engine.
  3. Always-valid confidence intervals (Howard et al., Ann. Statist., 2021). CI mở rộng động theo thời gian để đảm bảo coverage đồng thời trên toàn bộ chuỗi quan sát.
# Always-valid CI cho hiệu Bernoulli, theo Howard et al. 2021 §4
import numpy as np

def always_valid_ci(succ_a, n_a, succ_b, n_b, alpha=0.05, rho=0.05):
    p_a  = succ_a / n_a
    p_b  = succ_b / n_b
    diff = p_b - p_a
    var  = p_a*(1-p_a)/n_a + p_b*(1-p_b)/n_b
    n    = n_a + n_b
    # Time-uniform Chernoff radius
    radius = np.sqrt(2 * n * (rho + var)
                     * np.log(np.sqrt(n*rho + 1) / alpha))
    return diff - radius, diff + radius

lo, hi = always_valid_ci(1720, 31_000, 1530, 31_000, alpha=0.05)
print(f"Always-valid 95% CI cho lift: [{lo:.4f}, {hi:.4f}]")

Nếu team của bạn không có tài nguyên viết SPRT thủ công, cách đơn giản nhất là dùng Bayesian testing (mục trên) với threshold cố định trên P(B > A). Bayesian testing không có "peeking problem" theo nghĩa Frequentist, miễn là bạn cam kết prior trước khi nhìn dữ liệu.

Multiple testing correction: Bonferroni và Benjamini–Hochberg

Khi bạn phân tích 20 subgroup hoặc 20 metric cùng lúc với α = 0.05, xác suất có ít nhất một false positive là 1 − 0.95²⁰ ≈ 64%. Đây là vấn đề family-wise error rate (FWER) và cần correction phù hợp.

from statsmodels.stats.multitest import multipletests

pvals = np.array([0.001, 0.008, 0.032, 0.047, 0.11, 0.20, 0.35])

# Bonferroni: bảo thủ, kiểm soát FWER ≤ α
reject, pvals_bf, _, _ = multipletests(pvals, alpha=0.05, method='bonferroni')
print("Bonferroni p_adj:", pvals_bf.round(4))
print("Reject:          ", reject)

# Benjamini-Hochberg: kiểm soát FDR ≤ α, giữ được power
reject, pvals_bh, _, _ = multipletests(pvals, alpha=0.05, method='fdr_bh')
print("BH p_adj:        ", pvals_bh.round(4))
print("Reject:          ", reject)

Với A/B testing exploratory (chạy nhiều metric hoặc nhiều segment), Benjamini–Hochberg (1995) là mặc định hợp lý. Nó kiểm soát false discovery rate (tỷ lệ false positive trong số các kết luận positive) thay vì kiểm soát chặt family-wise error, do đó giữ được power. Bonferroni chỉ nên dùng khi bạn thực sự cần đảm bảo không có false positive nào. Ví dụ: thử nghiệm y khoa, safety metrics, hoặc guardrail metrics quan trọng.

Sai lầm thường gặp khi làm A/B testing

  • Sample ratio mismatch (SRM): nhóm A có 51.2% traffic thay vì 50%. SRM báo hiệu bug randomization và làm mọi kết quả không đáng tin. Không phải khác biệt do treatment, mà do dữ liệu bị lệch. Test bằng chi-square: stats.chisquare([n_a, n_b], f_exp=[total/2, total/2]). Nếu p < 0.001, dừng phân tích và tìm nguyên nhân trong logging hoặc assignment.
  • Simpson's paradox: tổng thể cho thấy treatment tốt hơn, nhưng khi tách theo device / country / user cohort lại ngược lại. Luôn kiểm tra kết quả theo segment quan trọng, kết hợp với feature engineering pipeline để chuẩn hoá segment trước khi phân tích.
  • Novelty effect: tuần đầu tiên user tương tác với thay đổi mới lạ, sau đó hiệu ứng giảm dần về mức thật. Chạy thí nghiệm ít nhất 2 tuần, hoặc 2 chu kỳ nghiệp vụ (business cycle). Với thay đổi UI lớn, tối thiểu 4 tuần.
  • Winner's curse: lift đo được từ các A/B test "thắng" thường phóng đại 20–30% do selection bias. Bạn chỉ ship winners, và winners trong sampling noise có xu hướng là những case bạn bị overestimation. Dùng shrinkage estimator (empirical Bayes, James–Stein) để hiệu chỉnh.
  • Không tính effect size và CI: báo cáo chỉ p-value là chưa đủ, luôn kèm khoảng tin cậy và relative lift. ASA (2016) đã cảnh báo rõ ràng về việc lạm dụng ngưỡng p < 0.05.
  • Dùng metric quá noise: revenue-per-user có coefficient of variation rất cao (thường > 2). Xem xét transform log(1+x) hoặc winsorize outlier ở percentile 99 trước khi test. Với pipeline batch, xem pandas 3.0 features cho analytics để tối ưu bộ nhớ.

Cuối cùng, hãy nhớ rằng thống kê chỉ là một phần của quy trình. Randomization đúng, logging đúng, và câu hỏi kinh doanh đúng vẫn quan trọng hơn công thức nào bạn dùng. Test đúng câu hỏi sai vẫn cho ra kết luận sai. Không có kiểm định nào cứu được điều đó.

Câu hỏi thường gặp

Khi nào dùng t-test và khi nào dùng z-test trong A/B testing?

Dùng Welch's t-test (scipy.stats.ttest_ind(equal_var=False)) cho continuous metric như revenue, thời gian trên trang hay số session. Không cần giả định phương sai bằng nhau. Dùng two-proportion z-test (statsmodels.stats.proportion.proportions_ztest) cho binary metric như conversion rate khi mỗi nhóm có ít nhất 30 conversions và n·p·(1−p) > 5. Với sample nhỏ hơn hoặc proportion cực (gần 0 hoặc 1), chuyển sang Fisher's exact test.

Sample size cho A/B test tính như thế nào?

Chọn trước bốn tham số: α (thường 0.05), power 1 − β (thường 0.80), minimum detectable effect (MDE) và baseline variance. Với continuous metric, dùng statsmodels.stats.power.TTestIndPower().solve_power(). Với proportion, dùng NormalIndPower kết hợp proportion_effectsize. MDE phải là "smallest effect that matters for business", được chọn trước khi nhìn dữ liệu để tránh bias.

Bayesian A/B testing khác gì Frequentist testing?

Bayesian trả về xác suất trực tiếp P(treatment > control | data), chính là con số stakeholder muốn biết. Frequentist trả về p-value và confidence interval, những khái niệm dễ diễn giải sai (p-value không phải xác suất hypothesis đúng). Bayesian cho phép tích hợp prior information và stop sớm mà không cần correction, miễn là prior được cam kết từ đầu. Cái giá: chi phí MCMC và cần thảo luận về prior.

CUPED giảm variance bằng cách nào và tăng power bao nhiêu?

CUPED (Deng et al., 2013) regress metric thí nghiệm Y lên metric pre-experiment X của cùng user, rồi test trên residual Y − θX. Vì X và Y tương quan cao, residual có variance thấp hơn Y đáng kể. Trong thực nghiệm của Microsoft và Booking.com, giảm variance 20–60% (tương đương gấp đôi power mà không cần thêm user). Điều kiện: X phải đo được trước treatment và tương quan > 0.3 với Y.

Peeking problem là gì và khi nào nó xảy ra?

Peeking là hành động nhìn kết quả A/B test trước khi đạt sample size cam kết. Với Frequentist testing cổ điển, peek nhiều lần làm phồng α thực tế: peek hằng ngày trong 14 ngày có thể đưa false positive rate từ 5% lên 25% (Johari et al., 2017). Cách xử lý: cam kết sample size trước và không nhìn; hoặc dùng sequential testing (mSPRT, always-valid CI); hoặc chuyển sang Bayesian testing với threshold cố định trên P(B > A).

Multiple testing correction nào phù hợp cho A/B testing với nhiều metric?

Với 5–50 metric hoặc segment exploratory, dùng Benjamini–Hochberg (statsmodels.stats.multitest.multipletests(method='fdr_bh')). Nó kiểm soát false discovery rate và giữ được power tốt hơn Bonferroni nhiều. Chỉ dùng Bonferroni khi bạn thực sự cần đảm bảo không có false positive nào: safety metrics, guardrail metrics, hoặc thử nghiệm y khoa.

Dr. Elena Vasquez
Về Tác Giả Dr. Elena Vasquez

Data scientist with a PhD in computational statistics. Translates papers into pandas one notebook at a time.