Thử A/B mã giảm giá: khi nào được tin kết quả
Thử mã giảm giá trên trang thanh toán: kiểm định hai tỉ lệ, cỡ mẫu, vì sao nhìn kết quả mỗi sáng đẩy dương tính giả từ 5% lên 22%, và cách nhìn sớm an toàn.
Thứ Hai 07/09/2026, nhóm khuyến mãi của BanHang bắt đầu thử mã GIAM3, giảm 3%, trên trang thanh toán online: nhóm A thấy trang cũ, nhóm B thấy mã điền sẵn. Kế hoạch ghi 14 ngày. Sáng thứ Năm 10/09, sau ba ngày, mỗi nhóm có 9.000 khách: A 3.576 đơn (39,73%), B 3.740 đơn (41,56%), p = 0,013. Nhóm dừng thử và bật mã cho mọi khách từ trưa. Đầu tháng 10, chuyển đổi vẫn quanh 40%, còn đơn nào cũng bớt 3% tiền. Diễn biến là lần thử số 522 trong mô phỏng ở mục 3, nơi mã không đổi chuyển đổi; tên mã và các tỉ lệ là minh họa. Chạy đủ 14 ngày, lần thử đó cho p = 0,735.
Đọc nhanh
- Nhìn kết quả mỗi sáng và dừng ngay khi p < 0,05 biến mức sai 5% thành 22%: trong 20.000 lần thử mô phỏng với hai nhóm bằng nhau, nhìn 14 lần cho 22,07% lần thử "thắng". Nhìn mỗi giờ cho 45,97%.
- Định cỡ mẫu trước khi chạy. Phát hiện 40% lên 41% với α = 0,05 và power 80% cần 37.826 khách mỗi nhóm, tức 12,6 ngày ở 3.000 khách mỗi nhóm mỗi ngày; chạy tròn 14 ngày. Mô phỏng đo power 79,6% ở cỡ mẫu đó, công thức cho 80,0%.
- Muốn nhìn mỗi ngày thì dùng ngưỡng định trước. O'Brien–Fleming (7,88 ở ngày 1, giảm dần về 2,11 ở ngày 14) giữ sai lầm loại I ở 5,00% và power 82,5%, so với 83,9% khi chỉ nhìn một lần.
- Chọn metric đích trước khi chạy. Với giá trị đơn 4.000.000 đ và biên lãi gộp 12% (minh họa), mã giảm 3% phải đưa chuyển đổi từ 40% lên 53,3% mới giữ nguyên lãi gộp mỗi khách; lên 41% thì lãi gộp mỗi khách giảm 23,1%.
1. Tỉ lệ chuyển đổi là một tỉ lệ nhị thức
Mỗi khách được chia nhóm là một lần thử có hai kết quả: đặt đơn hoặc không. Nếu các khách độc lập và cùng xác suất p, số đơn trong n khách theo phân phối nhị thức, và tỉ lệ đo được p̂ có sai số chuẩn √(p·(1 − p)/n). Với p = 40% và n = 9.000, đó là 0,52 điểm phần trăm; chênh lệch của hai nhóm có sai số chuẩn √2 · 0,52 = 0,73 điểm. Hai nhóm cùng 40% vẫn lệch nhau quá 1,96 · 0,73 = 1,43 điểm ở 5% số lần thử. Đơn vị đếm là khách, không phải lượt xem trang (mục 6).
Kiểm định hai tỉ lệ (z-test) đặt giả thuyết không H0: pA = pB, ước lượng p chung từ cả hai nhóm, rồi đo chênh lệch bằng số sai số chuẩn. Φ là hàm phân phối chuẩn tắc. Hai dòng đầu là công thức, phần dưới tính tay với số sáng thứ Năm:
z = (p̂B − p̂A) / √(p̂ · (1 − p̂) · (1/nA + 1/nB)), p̂ = (xA + xB) / (nA + nB)
p = 2 · (1 − Φ(|z|))
p̂A = 3.576 / 9.000 = 0,39733 p̂B = 3.740 / 9.000 = 0,41556
p̂ = 7.316 / 18.000 = 0,40644
SE = √(0,40644 · 0,59356 · 2 / 9.000) = 0,007322
z = (0,41556 − 0,39733) / 0,007322 = 0,018222 / 0,007322 = 2,489
p = 2 · (1 − Φ(2,489)) = 0,013
Khoảng tin cậy 95% của chênh lệch dùng sai số chuẩn không gộp, √(p̂A·(1 − p̂A)/nA + p̂B·(1 − p̂B)/nB) = 0,732 điểm: 1,82 ± 1,96 · 0,732, tức từ 0,39 đến 3,26 điểm. Ngày 14 của cùng lần thử có 16.705 và 16.753 đơn trên 42.000 khách mỗi nhóm: chênh 0,11 điểm, khoảng tin cậy từ −0,55 đến 0,78 điểm, p = 0,735.
p = 0,013 nghĩa là: nếu mã không đổi gì, xác suất thấy |z| từ 2,489 trở lên là 1,3%. Nó không phải xác suất mã vô dụng, và không đo mã mạnh hay yếu: đó là nguyên tắc 2 và 5 trong tuyên bố năm 2016 của Hiệp hội Thống kê Mỹ (ASA). Nguyên tắc 3 thêm rằng quyết định kinh doanh không nên chỉ dựa vào việc p vượt một ngưỡng. Phép tính còn giả định chỉ nhìn một lần, ở cỡ mẫu định trước. Nhóm khuyến mãi đã nhìn ba lần.
2. Sai lầm loại I, loại II và cỡ mẫu
Sai lầm loại I, hay dương tính giả, là kết luận mã có tác dụng khi nó không có; xác suất của nó là α, thường 5%. Sai lầm loại II, hay âm tính giả, là bỏ sót một mã có tác dụng thật; xác suất là β. Lực kiểm định (power) 1 − β là xác suất bắt được một hiệu ứng có kích thước cho trước, nên phải chọn trước mức nhỏ nhất đáng phát hiện (minimum detectable effect, MDE). BanHang chọn 40% lên 41% (minh họa). Công thức cỡ mẫu mỗi nhóm cho hai tỉ lệ, cũng là công thức trong power.prop.test của R, và phép tính tay với α = 0,05, power 80%:
n = [ z(1 − α/2) · √(2·p̄·(1 − p̄))
+ z(1 − β) · √(p₁·(1 − p₁) + p₂·(1 − p₂)) ]² / (p₂ − p₁)²
p̄ = (p₁ + p₂) / 2
p̄ = 0,405 √(2 · 0,405 · 0,595) = √0,48195 = 0,69423
√(0,40 · 0,60 + 0,41 · 0,59) = √0,48190 = 0,69419
z(0,975) = 1,9600 z(0,80) = 0,8416
n = (1,9600 · 0,69423 + 0,8416 · 0,69419)² / 0,01²
= (1,3607 + 0,5842)² / 0,0001 = 1,9449² / 0,0001 ≈ 37.826 khách mỗi nhóm
Quy tắc nhanh n = 16σ²/Δ² mà Kohavi và cộng sự (2009) dùng cho cùng α và power cho số gần như vậy: 16 làm tròn từ 2 · (1,96 + 0,84)² = 15,7, và 16 · 0,24 / 0,01² = 38.400. Δ bị bình phương, nên mức tăng 0,5 điểm cần 151.007 khách mỗi nhóm, còn 2 điểm chỉ cần 9.492.
Trang thanh toán online có khoảng 6.000 khách mới vào thử mỗi ngày, 3.000 mỗi nhóm (minh họa): 37.826 / 3.000 = 12,6 ngày. Kohavi và cộng sự khuyên chạy ít nhất một đến hai tuần rồi kéo dài theo bội số của tuần, để các thứ trong tuần có mặt như nhau. Lần thử chạy 14 ngày, 42.000 khách mỗi nhóm, power 83,9%. Quy trình đúng của một lần thử:
flowchart TD A["Chọn metric đích, MDE, α = 0,05, power 80%"] --> B["Cỡ mẫu 37.826 khách mỗi nhóm: 12,6 ngày, làm tròn lên 14"] B --> C["Chia nhóm theo KhachHangId"] C --> D["Chạy đủ 14 ngày: mỗi ngày chỉ xem lỗi và tỉ lệ chia nhóm"] D --> E["Ngày 14: tính z, p, khoảng tin cậy một lần"] D -.->|"cần dừng sớm"| F["Chỉ dừng khi vượt ngưỡng O'Brien–Fleming định trước"] E --> G["Có ý nghĩa và qua mức hòa vốn thì bật mã, không thì giữ trang cũ"]
3. Mô phỏng: nhìn mỗi sáng và power đo được
Mô phỏng chạy 20.000 lần thử, mỗi lần 14 ngày, mỗi nhóm 3.000 khách mỗi ngày chia đều 24 giờ. Phần đầu cho hai nhóm cùng 40%, nên mọi lần "thắng" là dương tính giả; "nhìn k lần" là tính z ở k mốc cách đều và dừng ở mốc đầu tiên có |z| ≥ 1,96. Phần cuối đo power với B thật sự là 41%, nhìn một lần. Cần Python 3.12 trở lên, numpy và scipy; hạt giống 2026. Trên laptop Intel Core Ultra 5 125U, Windows 11, script chạy hết khoảng 5 giây (trung vị ba lần, dao động theo tải máy). Lưu thành ab_test.py; kết quả ở ngay dưới:
import sys
import numpy as np
import scipy
from scipy.stats import norm
P_A, P_B = 0.40, 0.41 # chuyển đổi nền và mức muốn phát hiện, minh họa
NGAY, MOI_NGAY = 14, 3_000 # 14 ngày, 3.000 khách mỗi nhóm mỗi ngày, minh họa
LAN_THU = 20_000
Z = norm.ppf(0.975) # 1,96: ngưỡng hai phía của α = 0,05
def co_mau(p1, p2, luc=0.80):
pb = (p1 + p2) / 2
tu = (Z * np.sqrt(2 * pb * (1 - pb))
+ norm.ppf(luc) * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2)))
return (tu / (p2 - p1)) ** 2
def luc_cong_thuc(n, p1, p2):
pb = (p1 + p2) / 2
return norm.cdf((np.sqrt(n) * abs(p2 - p1) - Z * np.sqrt(2 * pb * (1 - pb)))
/ np.sqrt(p1 * (1 - p1) + p2 * (1 - p2)))
def z_hai_ti_le(xa, xb, n): # n khách mỗi nhóm, phương sai gộp
p = (xa + xb) / (2 * n)
return (xb - xa) / n / np.sqrt(p * (1 - p) * 2 / n)
def tich_luy(rng, pa, pb, buoc, so_buoc): # số đơn cộng dồn, mỗi dòng một lần thử
xa = rng.binomial(buoc, pa, (LAN_THU, so_buoc)).cumsum(axis=1)
xb = rng.binomial(buoc, pb, (LAN_THU, so_buoc)).cumsum(axis=1)
return xa, xb, buoc * np.arange(1, so_buoc + 1)
def dong(ten, so, dang): # in một dòng kết quả
print(f"{ten:<20}", " ".join(f"{x:{dang}}" for x in so))
if __name__ == "__main__":
rng = np.random.default_rng(2026)
print(f"Python {sys.version.split()[0]}, numpy {np.__version__},",
f"scipy {scipy.__version__}")
n_can = co_mau(P_A, P_B)
print(f"Cỡ mẫu {n_can:,.0f} khách mỗi nhóm = {n_can / MOI_NGAY:.1f} ngày")
# 1. Hai nhóm bằng nhau. Bước 1 giờ = 125 khách mỗi nhóm, nhìn k lần cách đều
xa, xb, n = tich_luy(rng, P_A, P_A, MOI_NGAY // 24, NGAY * 24)
z = z_hai_ti_le(xa, xb, n)
so_lan, vuot, gia = [1, 2, 4, 7, 14, 28, 84, 336], np.abs(z) >= Z, []
for k in so_lan:
moc = np.arange(1, k + 1) * (NGAY * 24 // k) - 1 # k mốc cách đều, theo giờ
gia.append(vuot[:, moc].any(axis=1).mean())
dong("Số lần nhìn:", so_lan, "6d")
dong("Dương tính giả (%):", 100 * np.array(gia), "6.2f")
# 2. Lần thử 522, câu chuyện ở đầu bài: z mỗi sáng
i, sang = 522, np.arange(1, NGAY + 1) * 24 - 1
dong("z mỗi sáng, lần 522:", z[i, sang], ".2f")
for d in (3, 14):
j = sang[d - 1]
print(f" ngày {d}: mỗi nhóm {n[j]}, A {xa[i, j]} đơn, B {xb[i, j]} đơn,"
f" p = {2 * norm.sf(abs(z[i, j])):.3f}")
# 3. Power: công thức so với mô phỏng, nhìn một lần ở cỡ mẫu c
co = np.array([5000, 10000, 20000, 30000, round(n_can), 42000, 50000, 60000])
do = []
for c in co:
xa1, xb1 = rng.binomial(c, P_A, LAN_THU), rng.binomial(c, P_B, LAN_THU)
do.append(np.mean(np.abs(z_hai_ti_le(xa1, xb1, c)) >= Z))
dong("Khách mỗi nhóm:", co, "6d")
dong("Power công thức (%):", 100 * luc_cong_thuc(co, P_A, P_B), "6.1f")
dong("Power mô phỏng (%):", 100 * np.array(do), "6.1f")
Python 3.14.3, numpy 2.4.4, scipy 1.17.1
Cỡ mẫu 37,826 khách mỗi nhóm = 12.6 ngày
Số lần nhìn: 1 2 4 7 14 28 84 336
Dương tính giả (%): 4.98 8.12 12.62 16.57 22.07 27.70 36.20 45.97
z mỗi sáng, lần 522: 0.50 1.71 2.49 2.44 2.18 1.52 1.49 0.88 0.83 0.60 0.34 0.28 0.40 0.34
ngày 3: mỗi nhóm 9000, A 3576 đơn, B 3740 đơn, p = 0.013
ngày 14: mỗi nhóm 42000, A 16705 đơn, B 16753 đơn, p = 0.735
Khách mỗi nhóm: 5000 10000 20000 30000 37826 42000 50000 60000
Power công thức (%): 17.3 30.2 53.1 70.4 80.0 83.9 89.6 94.2
Power mô phỏng (%): 17.8 30.1 53.3 70.9 79.6 83.9 89.4 94.1
Nhìn mỗi sáng trong 14 ngày đẩy dương tính giả từ 5% lên 22%
Hai nhóm cùng chuyển đổi 40%, dừng ngay lần đầu |z| ≥ 1,96
Số lần nhìn
Bảng số liệu
| Số lần nhìn | Mô phỏng | α định trước |
|---|---|---|
| 1 | 4,98% | 5,00% |
| 2 | 8,12% | 5,00% |
| 4 | 12,62% | 5,00% |
| 7 | 16,57% | 5,00% |
| 14 | 22,07% | 5,00% |
| 28 | 27,70% | 5,00% |
| 84 | 36,20% | 5,00% |
| 336 | 45,97% | 5,00% |
Nhìn một lần cho 4,98%, đúng α trong sai số mô phỏng √(0,05 · 0,95 / 20.000) = 0,15 điểm. Nhìn mỗi sáng cho 22,07%, mỗi giờ 45,97%: mỗi lần nhìn thêm là một cơ hội nữa cho nhiễu vượt 1,96. Các lần nhìn không độc lập. Giá trị z của ngày 4 chứa toàn bộ dữ liệu ngày 3 và tương quan với z ngày 3 theo hệ số √(3/4) = 0,87, nên 14 lần nhìn cho 22% chứ không phải 1 − 0,95¹⁴ = 51%. Bảng của Armitage, McPherson và Rowe (1969) cho 2 lần nhìn 8,3%, 4 lần 12,6%, khớp mô phỏng.
Lần thử 522 vượt 1,96 ba ngày liền rồi về 0,34
Ngày
Bảng số liệu
| Ngày | z của lần thử 522 | Ngưỡng 1,96 | Ngưỡng O'Brien–Fleming |
|---|---|---|---|
| 1 | 0,50 | 1,96 | |
| 2 | 1,71 | 1,96 | |
| 3 | 2,49 | 1,96 | 4,55 |
| 4 | 2,44 | 1,96 | 3,94 |
| 5 | 2,18 | 1,96 | 3,52 |
| 6 | 1,52 | 1,96 | 3,22 |
| 7 | 1,49 | 1,96 | 2,98 |
| 8 | 0,88 | 1,96 | 2,79 |
| 9 | 0,83 | 1,96 | 2,63 |
| 10 | 0,60 | 1,96 | 2,49 |
| 11 | 0,34 | 1,96 | 2,38 |
| 12 | 0,28 | 1,96 | 2,28 |
| 13 | 0,40 | 1,96 | 2,19 |
| 14 | 0,34 | 1,96 | 2,11 |
Lần thử 522 được chọn vì nó vượt 1,96 ở sáng ngày 3 như câu chuyện, rồi vượt tiếp ở ngày 4 và 5. Ba ngày liền "có ý nghĩa" vẫn là nhiễu: ngày 14, z còn 0,34. Ngưỡng O'Brien–Fleming ở mục 4 không bị vượt ngày nào.
Power đo bằng mô phỏng bám sát công thức
Khách mỗi nhóm
Bảng số liệu
| Khách mỗi nhóm | Công thức | Mô phỏng |
|---|---|---|
| 5.000 | 17,3% | 17,8% |
| 10.000 | 30,2% | 30,1% |
| 20.000 | 53,1% | 53,3% |
| 30.000 | 70,4% | 70,9% |
| 37.826 | 80,0% | 79,6% |
| 42.000 | 83,9% | 83,9% |
| 50.000 | 89,6% | 89,4% |
| 60.000 | 94,2% | 94,1% |
Ở cỡ mẫu đã tính, mô phỏng đo 79,6%, công thức cho 80,0%. Ở cả tám cỡ mẫu, hai con số lệch nhau nhiều nhất 0,5 điểm phần trăm, khoảng hai lần sai số mô phỏng (0,28 điểm quanh 80%). Sau ba ngày, 9.000 khách mỗi nhóm, power chỉ là 27,6%: kể cả khi mã tăng đúng 1 điểm, phần lớn lần thử chưa thể thấy điều đó vào sáng thứ Năm.
4. Nhìn sớm có kiểm soát: Pocock và O'Brien–Fleming
Nhóm khuyến mãi có lý do để nhìn sớm: mã làm hỏng trang thanh toán thì phải tắt ngay. Thiết kế tuần tự nhóm (group sequential) định trước số lần nhìn và ngưỡng cho từng lần, sao cho khi H0 đúng, xác suất vượt ngưỡng ở bất kỳ lần nào vẫn là 5%. Bonferroni dùng mức α/K mỗi lần; bất đẳng thức Boole bảo đảm tổng không quá α, và K = 14 cho ngưỡng z(1 − 0,05/28) = 2,91. Pocock (1977) dùng một ngưỡng chung, chọn để tổng đúng bằng α. O'Brien và Fleming (1979) dùng ngưỡng c·√(K/k) ở lần nhìn k: rất cao lúc đầu, gần 1,96 ở lần cuối.
Hằng số của Pocock và O'Brien–Fleming không có công thức đóng. Code dưới tính chúng bằng tích phân số: z ở lần nhìn k là S_k/√k, với S_k là tổng k bước chuẩn độc lập, nên chỉ cần lan truyền mật độ của S_k qua từng lần nhìn và cắt phần đã vượt ngưỡng. Theo bài giảng ở mục Nguồn, khi cỡ mẫu lớn, dãy z của kiểm định hai tỉ lệ có đúng phân phối đồng thời đó. Code kiểm lại bảng đã công bố, rồi mô phỏng năm cách nhìn trên dữ liệu nhị thức, hạt giống 2027. Lưu thành nhin_som.py cạnh ab_test.py; script chạy hết khoảng 4 giây:
import numpy as np
from scipy.optimize import brentq
from scipy.signal import fftconvolve
from scipy.stats import norm
from ab_test import P_A, P_B, NGAY, MOI_NGAY, Z, z_hai_ti_le, tich_luy
H = 0.005
S = np.arange(-16, 16 + H / 2, H) # lưới cho S_k = Z_k·√k, tổng của k bước N(0, 1)
PHI = norm.pdf(S)
def xs_vuot(nguong): # H0: P(|Z_k| ≥ nguong[k−1] ở ít nhất một lần nhìn k)
f = PHI.copy() # mật độ của S_k trên những đường chưa vượt ngưỡng
for k, c in enumerate(nguong, start=1):
if k > 1:
f = fftconvolve(f, PHI, mode="same") * H
f *= np.clip((c * np.sqrt(k) - np.abs(S)) / H + 0.5, 0, 1) # cắt phần vượt
return 1 - f.sum() * H
def pocock(K):
return brentq(lambda c: xs_vuot(np.full(K, c)) - 0.05, 1.9, 4)
def obf(K): # trả về c; ngưỡng ở lần nhìn k là c·√(K/k)
k = np.arange(1, K + 1)
return brentq(lambda c: xs_vuot(c * np.sqrt(K / k)) - 0.05, 1.5, 4)
armitage = [f"K = {K}: {xs_vuot(np.full(K, Z)):.3f}" for K in (2, 5, 10, 20)]
print("Ngưỡng 1,96, nhìn K lần:", ", ".join(armitage))
for K in (5, 10, NGAY):
c = obf(K)
print(f"K = {K}: Pocock {pocock(K):.3f},",
f"O'Brien-Fleming {c:.3f} (lần đầu {c * np.sqrt(K):.3f})")
K = NGAY
nguong = {
"Mỗi ngày, 1,96": np.full(K, Z),
"Bonferroni": np.full(K, norm.ppf(1 - 0.05 / (2 * K))),
"Pocock": np.full(K, pocock(K)),
"O'Brien-Fleming": obf(K) * np.sqrt(K / np.arange(1, K + 1)),
"Một lần, ngày 14": np.r_[np.full(K - 1, np.inf), Z],
}
print("O'Brien-Fleming theo ngày:", *(f"{v:.2f}" for v in nguong["O'Brien-Fleming"]))
rng = np.random.default_rng(2027)
z0 = z_hai_ti_le(*tich_luy(rng, P_A, P_A, MOI_NGAY, K)) # H0: hai nhóm bằng nhau
z1 = z_hai_ti_le(*tich_luy(rng, P_A, P_B, MOI_NGAY, K)) # H1: B thật sự hơn 1 điểm
print(f"{'cách nhìn':<17} {'tích phân':>9} {'dương tính giả':>14}",
f"{'power':>6} {'số ngày TB (H1)':>15}")
for ten, b in nguong.items():
v0, v1 = np.abs(z0) >= b, np.abs(z1) >= b
ngay = np.where(v1.any(axis=1), v1.argmax(axis=1) + 1, K)
print(f"{ten:<17} {xs_vuot(b):>9.2%} {v0.any(axis=1).mean():>14.2%}"
f" {v1.any(axis=1).mean():>6.1%} {ngay.mean():>15.1f}")
Ngưỡng 1,96, nhìn K lần: K = 2: 0.083, K = 5: 0.142, K = 10: 0.193, K = 20: 0.248
K = 5: Pocock 2.413, O'Brien-Fleming 2.040 (lần đầu 4.562)
K = 10: Pocock 2.555, O'Brien-Fleming 2.087 (lần đầu 6.598)
K = 14: Pocock 2.615, O'Brien-Fleming 2.107 (lần đầu 7.882)
O'Brien-Fleming theo ngày: 7.88 5.57 4.55 3.94 3.52 3.22 2.98 2.79 2.63 2.49 2.38 2.28 2.19 2.11
cách nhìn tích phân dương tính giả power số ngày TB (H1)
Mỗi ngày, 1,96 21.96% 21.89% 90.2% 6.2
Bonferroni 2.19% 2.04% 60.0% 10.4
Pocock 5.00% 4.92% 71.7% 9.2
O'Brien-Fleming 5.00% 4.86% 82.5% 10.3
Một lần, ngày 14 5.00% 4.77% 83.9% 14.0
Dòng đầu khớp bảng của Armitage và cộng sự: 0,083, 0,142, 0,193, 0,248. Hằng số K = 5 và K = 10 khớp bảng Pocock (2,413, 2,555) và O'Brien–Fleming (2,040, 2,087) trong bài giảng tới ba chữ số thập phân; riêng ngưỡng lần đầu của K = 10 ra 6,598, bảng ghi 6,600. Bảng nguồn tính cho một phía α = 0,025, gần như trùng hai phía α = 0,05 vì xác suất một đường đi chạm cả hai biên rất nhỏ. Cột "tích phân" là sai lầm loại I tính theo xấp xỉ chuẩn, cột kế bên là mô phỏng.
O'Brien–Fleming giữ đúng 5% và gần như không tốn power: 82,5% so với 83,9% khi chỉ nhìn ngày 14. Khi mã thật sự hơn 1 điểm, lần thử dừng trung bình ở ngày 10,3. Pocock dừng sớm hơn, ngày 9,2, nhưng mất 12 điểm power vì ngưỡng cuối 2,615 cao hơn 1,96 nhiều. Bonferroni bỏ qua tương quan giữa các lần nhìn nên chỉ dùng 2,19% trong 5% được phép, và mất 24 điểm power.
Ngưỡng định trước chỉ đúng khi lịch nhìn đúng như kế hoạch. Nhìn ngoài lịch, hay thêm vài ngày vì kết quả "gần đạt", là quay về mục 3. Khi lịch nhìn không cố định được, hàm chi tiêu α (alpha spending) của Lan và DeMets (1983) định trước phần α được tiêu theo tỉ lệ dữ liệu đã có, và không đòi các lần nhìn cách đều.
5. Chuyển đổi tăng chưa chắc là lãi tăng
Giả sử mã có tác dụng thật, đúng bằng MDE: 40% lên 41%. Với giá trị đơn trung bình 4.000.000 đ và biên lãi gộp 12% (cả hai minh họa), mã giảm 120.000 đ mỗi đơn, lấy thẳng vào lãi:
| Mỗi khách vào trang thanh toán | A, không mã | B, GIAM3, chuyển đổi 41% |
Chênh |
|---|---|---|---|
| Tiền mỗi đơn | 4.000.000 | 3.880.000 | |
| Lãi gộp mỗi đơn | 480.000 | 360.000 | |
| Doanh thu mỗi khách | 0,40 · 4.000.000 = 1.600.000 | 0,41 · 3.880.000 = 1.590.800 | −0,6% |
| Lãi gộp mỗi khách | 0,40 · 480.000 = 192.000 | 0,41 · 360.000 = 147.600 | −23,1% |
Tính ngược mức hòa vốn: doanh thu mỗi khách giữ nguyên khi B đạt 1.600.000 / 3.880.000 = 41,24%; lãi gộp mỗi khách giữ nguyên khi B đạt 192.000 / 360.000 = 53,33%, tức tăng 13,3 điểm. Lần thử thiết kế để phát hiện 1 điểm chuyển đổi trả lời một câu hỏi không quyết định được gì. Trong lần thử 522, mã không đổi chuyển đổi, nên sau khi bật cho mọi khách, lãi gộp mỗi khách còn 0,40 · 360.000 = 144.000 đ, giảm một phần tư. Mã giảm giá còn tác động theo cách khó đoán: Kohavi và cộng sự kể một trang thanh toán mất 90% doanh thu sau khi đổi sang bản mới có ô nhập mã; theo các tác giả, ô mã là thay đổi quyết định, vì khách không có mã bắt đầu nghĩ mình đang trả đắt.
Kohavi và cộng sự gọi metric quyết định là OEC (overall evaluation criterion): chuyển đổi, doanh thu, lợi nhuận, giá trị vòng đời của khách, hay một tổ hợp có trọng số. OEC phải chọn trước khi chạy, vì chọn sau giữa nhiều metric làm tăng xác suất gặp kết quả có vẻ ý nghĩa do may. Metric tiền có phương sai lớn hơn tỉ lệ 0/1 nhiều: trong ví dụ của họ, phát hiện thay đổi 5% của chuyển đổi cần dưới 122.000 người dùng, của doanh thu cần hơn 409.000. Đơn BanHang trải từ ốp lưng 190.000 đ tới điện thoại 8.990.000 đ, nên đo lãi gộp mỗi khách cần nhiều hơn 37.826 khách mỗi nhóm; tính bằng 16σ²/Δ² với σ ước lượng từ dữ liệu cũ hoặc từ một lần thử A/A. Lãi gộp theo nhóm hàng lấy từ phần giảm giá đã chia vào từng dòng đơn trong dbo.GiamGiaDong.
6. Chia nhóm theo khách và hiệu ứng mới lạ
Chia theo lượt xem thì khách vào trang thanh toán hai lần có thể thấy mã ở lần đầu, không thấy ở lần sau, và vẫn gõ mã đã nhớ khi đang ở nhóm A: nhóm A bị lẫn tác động của mã. Các lượt xem của cùng một khách cũng không độc lập, trái với giả định của sai số chuẩn ở mục 1. Kohavi và cộng sự đặt ba yêu cầu cho cách chia nhóm: mỗi người có xác suất như nhau vào mỗi nhóm, cùng một người luôn vào cùng nhóm, và cách chia của hai thí nghiệm không tương quan.
Băm KhachHangId ghép với tên lần thử đáp ứng cả ba mà không cần lưu nhóm. Kohavi và cộng sự thử nhiều hàm băm trên một triệu mã người dùng liên tiếp: chỉ MD5 không tạo tương quan giữa các thí nghiệm, SHA-256 gần đạt. Lưu thành chia_nhom.py, chỉ cần thư viện chuẩn:
import hashlib
from collections import Counter
from math import erfc, sqrt
def nhom(khach_hang_id: int, ma_thu: str = "giam3-2026-09") -> str:
h = hashlib.md5(f"{ma_thu}:{khach_hang_id}".encode(), usedforsecurity=False)
return "B" if int.from_bytes(h.digest()[:8], "big") % 100 < 50 else "A"
khach = range(1, 200_001)
dem = Counter(nhom(k) for k in khach)
z = (dem["B"] - len(khach) / 2) / sqrt(len(khach) / 4)
print(dict(dem), f"z = {z:.2f}, p = {erfc(abs(z) / sqrt(2)):.2f}")
cung = sum(nhom(k) == nhom(k, "combo-2026-10") for k in khach) / len(khach)
print("Cùng nhóm ở thử khác:", cung)
{'A': 99991, 'B': 100009} z = 0.04, p = 0.97
Cùng nhóm ở thử khác: 0.498505
Dòng đầu là phép kiểm lệch tỉ lệ chia nhóm (sample ratio mismatch, SRM): z = (100.009 − 100.000) / √(200.000 / 4) = 9 / 223,6 = 0,04. Với hai nhóm, kiểm định chi bình phương cho cùng p vì χ² = z². Ở lần thử khác, 49,85% số khách vào cùng nhóm, sát 50% của hai lần chia độc lập. Fabijan và cộng sự (2019) ghi khoảng 6% thí nghiệm ở Microsoft có SRM, với ví dụ 821.588 và 815.482 người dùng: z = 3.053 / √(1.637.070 / 4) = 3.053 / 639,7 = 4,77, p ≈ 1,8 · 10⁻⁶. Họ ví SRM như cơn sốt: triệu chứng chung của nhiều loại lỗi dữ liệu, phải tìm ra nguyên nhân trước khi tin kết quả.
Kohavi và cộng sự còn mô tả hai hiệu ứng ngược chiều khi có cái mới: người dùng tò mò bấm thử khắp nơi, hoặc quen cách cũ nên lúc đầu kém hiệu quả. Cả hai đòi chạy nhiều tuần, và một cách kiểm là tính OEC riêng cho người dùng mới. Với GIAM3, so chênh lệch tuần 1 với tuần 2, và tách riêng khách lần đầu mua trong thời gian thử. Chênh lệch lớn mấy ngày đầu rồi nhỏ dần có thể là hiệu ứng mới lạ, cũng có thể là nhiễu như lần thử 522. Cả hai đều là lý do không dừng sớm.
Những chỗ hay hiểu sai
- "p = 0,013 nghĩa là 98,7% chắc mã có tác dụng." p được tính khi giả sử mã không có tác dụng; nó không phải xác suất của giả thuyết. Lần thử 522 có p = 0,013 ở ngày 3 trong khi mã không làm gì.
- "Ba ngày liền p < 0,05 thì chắc rồi." Lần thử 522 có p < 0,05 ở ngày 3, 4, 5 và p = 0,735 ở ngày 14.
- "Không có ý nghĩa thống kê tức là mã vô dụng." Ở 42.000 khách mỗi nhóm, power là 83,9%: cứ 100 mã thật sự tăng 1 điểm thì khoảng 16 mã bị bỏ sót. Khoảng tin cậy ngày 14 của lần thử 522, từ −0,55 đến 0,78 điểm, chưa loại được mức tăng 0,5 điểm.
- "Chuyển đổi tăng là lãi tăng." Mã giảm 3% đưa chuyển đổi từ 40% lên 41% làm lãi gộp mỗi khách giảm 23,1% trong ví dụ ở mục 5.
Đọc tiếp
- SLO thanh toán và cảnh báo burn rate: cùng hiện tượng ở giám sát: quy tắc "tỉ lệ lỗi 5 phút trên 1%" đánh giá mỗi phút cho 261 page giả trong 90 ngày mô phỏng.
- Đánh giá một hệ RAG: tinh chỉnh nhiều vòng trên cùng một golden set cũng là nhìn nhiều lần, và cách giữ riêng một phần chỉ để chấm.
- Chia tiền không lệch một đồng: chia tiền giảm của mã vào từng dòng đơn trong
dbo.GiamGiaDong, dữ liệu để tính lãi gộp sau giảm giá.
Nguồn
- Ron Kohavi, Roger Longbotham, Dan Sommerfield, Randal M. Henne, Controlled experiments on the web: survey and practical guide, Data Mining and Knowledge Discovery 18, 2009: mục 2.1, 3.1, 3.2, 3.6, 5.1, 6.2.3, 6.2.5.
- Aleksander Fabijan và cộng sự, Diagnosing Sample Ratio Mismatch in Online Controlled Experiments, KDD 2019.
- American Statistical Association, Statement on Statistical Significance and P-Values, 07/03/2016; bản đầy đủ: Wasserstein và Lazar, The American Statistician 70(2), 2016.
- University of Washington, SISCER, Lecture 4: Interim Monitoring: Efficacy: bảng sai lầm loại I khi nhìn nhiều lần (Armitage, McPherson, Rowe, JRSS A 132, 1969), bảng ngưỡng Pocock và O'Brien–Fleming, hàm chi tiêu α của Lan và DeMets.
- Wikipedia, Pocock boundary và O'Brien–Fleming boundary: ngưỡng p của Pocock cho hai phía α = 0,05; dẫn Pocock, Biometrika 64(2), 1977 và O'Brien, Fleming, Biometrics 35(3), 1979.
- R, mã nguồn
power.prop.test: công thức power và cỡ mẫu cho hai tỉ lệ.