Toán ứng dụng

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.

Mục lục
  1. 1. Tỉ lệ chuyển đổi là một tỉ lệ nhị thức
  2. 2. Sai lầm loại I, loại II và cỡ mẫu
  3. 3. Mô phỏng: nhìn mỗi sáng và power đo được
  4. 4. Nhìn sớm có kiểm soát: Pocock và O'Brien–Fleming
  5. 5. Chuyển đổi tăng chưa chắc là lãi tăng
  6. 6. Chia nhóm theo khách và hiệu ứng mới lạ
  7. Những chỗ hay hiểu sai
  8. Đọc tiếp
  9. Nguồ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

Mô phỏngα định trước
02040601247142884336Mô phỏngα định trước

Số lần nhìn

20.000 lần thử, 3.000 khách mỗi nhóm mỗi ngày trong 14 ngày, hạt giống 2026. 14 lần là mỗi sáng, 336 lần là mỗi giờ.
Bảng số liệu
Số lần nhìnMô phỏngα định trước
14,98%5,00%
28,12%5,00%
412,62%5,00%
716,57%5,00%
1422,07%5,00%
2827,70%5,00%
8436,20%5,00%
33645,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

z của lần thử 522Ngưỡng 1,96Ngưỡng O'Brien–Fleming
02461234567891011121314

Ngày

z mỗi sáng khi hai nhóm thật ra bằng nhau. Ngưỡng O'Brien–Fleming tính ở mục 4; ngày 1 và 2 là 7,88 và 5,57, nằm ngoài khung.
Bảng số liệu
Ngàyz của lần thử 522Ngưỡng 1,96Ngưỡng O'Brien–Fleming
10,501,96
21,711,96
32,491,964,55
42,441,963,94
52,181,963,52
61,521,963,22
71,491,962,98
80,881,962,79
90,831,962,63
100,601,962,49
110,341,962,38
120,281,962,28
130,401,962,19
140,341,962,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

Công thứcMô phỏng
02550751005.00010.00020.00030.00037.82642.00050.00060.000

Khách mỗi nhóm

Phát hiện 40% lên 41%, α = 0,05, nhìn một lần. Mỗi điểm mô phỏng là 20.000 lần thử. 37.826 là cỡ mẫu tính ở mục 2, 42.000 là 14 ngày. Trục ngang không theo tỉ lệ.
Bảng số liệu
Khách mỗi nhómCông thứcMô phỏng
5.00017,3%17,8%
10.00030,2%30,1%
20.00053,1%53,3%
30.00070,4%70,9%
37.82680,0%79,6%
42.00083,9%83,9%
50.00089,6%89,4%
60.00094,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

Đọc tiếp

Trong SQL Server

Đọc kế hoạch thực thi

Câu SQL thành kế hoạch thực thi ra sao, lấy kế hoạch ước lượng và thực tế ở đâu, đọc thuộc tính nào trước, và vì sao cùng một thủ tục lúc nhanh lúc chậm.

53 phút đọc