JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

設計一個 A/B 測試

在你動程式之前:真正重要的指標、你要隨機分派的單位、護欄指標,以及先把假設寫下來。

從決策出發,而不是從儀表板出發

想像你在一個線上學習網站工作。一位產品經理有個點子:在首頁加上一張「繼續學習」卡片,讓回訪的學生能直接跳回他們的課程。大家都喜歡這個點子。接下來最誘人的做法,就是把它做出來、推送給所有人,然後盯著儀表板看。一週後「完成的課程數」上升了 3%,團隊互相擊掌,於是這張卡片就被宣告成功了。

我們在上一篇已經看過為什麼這是個陷阱:當所有人都拿到這項改動時,你就沒有一個誠實的比較對象。也許課程數上升是因為剛好碰上考試季、或是因為一封行銷信,而不是因為那張卡片。解方是隨機對照實驗:用一個A/B 測試把使用者隨機分成對照組(維持目前的首頁)和實驗組(看到新卡片),再比較兩組。隨機分派讓兩組在其他每一個面向上都相似,因此兩組之間的差異就可以歸功於那張卡片。

但有件事初學者常常跳過:一個 A/B 測試的好壞,完全取決於它的設計,而設計發生在你寫下任何一行程式之前。設計這個測試,意思是事先決定好:你究竟要量什麼、要怎麼把使用者分組、什麼樣的結果算成功,以及什麼樣的結果會讓你放棄。這篇指南就是那份起飛前的檢查清單。把它做好,後面的分析幾乎會無聊到沒事做;跳過它,再聰明的統計也救不了你。

最能讓人想清楚的起手問題,不是「我們該量什麼?」,而是「這個測試能讓我們做出什麼決策?」實驗是一台回答問題的機器,而模糊的問題只會得到模糊的答案。如果你沒辦法把這句話補完——「如果結果是 ___,我們就會 ___」——那你還沒準備好上線。

選擇整體評估準則(OEC)

設計的核心,是你將用來評斷這個測試的那個指標。我們稱它為整體評估準則(OEC,overall evaluation criterion)——也常直接叫做「主要指標」。它是那一個數字(偶爾是一小組固定的數字),用來捕捉這項改動是否真正對使用者、對商業都有益。現在就把它指定出來的整個用意,是要你在看到資料之前先做出承諾,這樣你之後才無法挑出剛好看起來不錯的那個指標。

為什麼要堅持基本上只用一個指標?因為如果你追蹤二十個指標,又允許自己「只要其中任何一個變正就宣告勝利」,那你幾乎一定會純靠運氣「贏」。在常用的 5% 門檻下量二十個獨立指標,就算這項改動毫無作用,至少有一個看起來「顯著」的機率大約是 64%。這就是多重比較的陷阱,而最乾淨的防禦,就是事先決定哪一個指標才是最終裁決。

一個好的 OEC 具備四個特性。它要靈敏(在你能跑測試的時間內,當改動真的有意義時,它確實會動)。它要與你真正在乎的長期目標一致。它要難以被操弄。而且它要能在實驗期間內被乾淨地量到。這些特性常常彼此衝突。以我們的學習網站為例,「卡片的點擊數」靈敏又好量,卻很淺、又容易被灌水——一張花俏的卡片就算沒人多學到東西,也照樣有人點。「四週留存率」與真正的目標漂亮地一致,但在一個兩週的測試裡實在太慢、量不出來。一個合理的折衷,是介於兩者之間的指標:每位每週活躍使用者完成的課程數。它落在漏斗中夠深的位置,足以反映真實價值,卻又動得夠快,讓你讀得到。

「繼續學習」卡片的轉換漏斗:在所有看到卡片的人當中,有些人點擊、較少人開始上課、完成的人又更少。淺層指標(點擊)位於頂端;我們信賴的 OEC(完成的課程數)靠近底部,最接近真實價值。

一張由上而下逐漸收窄的漏斗圖:看到卡片 → 點擊卡片 → 開始上課 → 完成課程,每一階段都是更窄的色帶;底部那一階段被標示為選定的 OEC。

這裡有個著名的警語,叫做古德哈特定律(Goodhart's law):當一個量度變成了目標,它就不再是個好的量度。當一個團隊被要求把點擊數最大化的那一刻,就會有人把卡片做得更大、更紅、更難關掉——點擊數飆升、學習量卻沒有,使用者還被惹惱。這正是為什麼你要挑一個難以被操弄的 OEC,並把它和下一節的護欄指標搭配使用。OEC 告訴你贏了沒有;護欄指標則確保你不是靠作弊贏的。

有時候,沒有任何單一的原始指標能捕捉「好」,於是團隊會用幾個經標準化的子指標、加上權重,組成一個複合式 OEC。用文字來說:把每個子指標重新縮放成可比較的單位,乘上一個反映它有多重要的權重,再全部加起來。用符號表示,其中每個 z 是一個標準化後的子指標、每個 w 是它的權重:

\text{OEC} = w_1 z_1 + w_2 z_2 + \cdots + w_k z_k

複合式 OEC,是各個標準化子指標的加權總和。要誠實面對:這些權重是人為的判斷,不是從資料裡讀出來的事實——請在測試之前就決定好,並把理由寫下來。

  1. 把候選指標從淺(點擊)排到深(長期留存/營收)列出來。
  2. 對每一個都問:在我們的時間窗內它靈敏嗎?和真正的目標一致嗎?容易被操弄嗎?
  3. 在規劃的跑測時間內,挑出仍能可靠變動的那個最深指標。
  4. 釘死恰好一個 OEC(或一個極小的固定複合指標)作為決策指標——然後就此打住。

護欄指標:不造成傷害

孤立地最佳化單一數字是危險的,因為一項改動可能拉高你的 OEC,卻在暗地裡毀掉別的東西。護欄指標是一個你並不打算去改善、但堅決不容許它受害的數字——就像山路上的護欄,它們不是目的地,只是防止你開下懸崖。你要事先把它們和 OEC 一起決定好。

護欄指標分兩種口味。品質與信任型護欄保護使用者體驗:頁面載入時間、當機或錯誤率,以及無障礙性。商業型護欄保護公司:每位使用者帶來的營收、訂閱取消數、客服工單量。以「繼續學習」卡片來說,兩個明顯的護欄是頁面載入時間(更重的首頁可能讓所有人變慢)和每週取消率(一張咄咄逼人的卡片可能惹惱使用者、讓他們離開)。如果這張卡片提高了完成課程數,卻也讓頁面慢了 400 毫秒、還推高了取消率,那就不算贏——那是一個你必須自覺去做的取捨,而不是不小心發生的。

所以真正的決策規則有兩個部分,而不是一個:只有在 OEC 有意義地上升、且每個護欄都維持在它的容忍範圍內時,才上線。現在就把這兩半都寫下來,可以防止那個太過人性的舉動——事後把護欄的退步給找理由開脫(「喔,那個延遲的損失大概只是雜訊啦」)。

隨機分派單位:使用者、工作階段,還是叢集?

隨機分派單位,就是你為它擲硬幣的那個東西——你隨機指派到對照組或實驗組的那個實體。常見的選擇有:個別使用者、單次工作階段(session)或造訪、單一請求,或是一整群人(一個班級、一所學校、一座城市)。這聽起來像是技術細節,但它悄悄地同時決定了你的測試有多值得信賴、又有多靈敏。

產品實驗的預設選擇是使用者。原因是一致性:同一個人每次回來,都應該看到相同的體驗。如果你用工作階段來隨機分派,一個學生可能週一看到「繼續學習」卡片、週二不見了、週三又跑回來。這種閃爍本身就令人困惑,而且還會在同一個人身上把實驗與對照混在一起,讓比較變得模糊。用穩定的使用者 ID 來分派,體驗才會連貫。

不過這裡有個真實的取捨。更細的單位(工作階段、請求)給你多得多的單位數,意味著更高的統計檢定力、能更快讀到結果——但代價是體驗會閃爍、而且會被汙染。更粗的單位(叢集)有時是非用不可的。當一位使用者的處理會影響到另一位使用者的結果時——社交功能、買賣雙方互動的市集,或是一起讀書的同學——對個別使用者做隨機分派,會讓處理效果跨越邊界外洩。我們稱這個為干擾(interference,或稱溢出/網路效應):那個「讓我們能比較兩組」的「無溢出」假設,就悄悄地破掉了。解方是對整個叢集做隨機分派(例如把整所學校指派到實驗組或對照組),讓溢出留在同一個桶子裡。

最後,分派本身應該是「決定性且可重現」的,而不是每個請求都重新擲一次硬幣。標準的技巧是把一個穩定的 ID 和實驗名稱一起做雜湊(hash),再讀出桶號。同一個使用者永遠落在同一組,任何人之後都能重新算出分派結果,而且新增一個實驗也不會擾動到舊的實驗。

# Deterministic, reproducible assignment (pseudocode)
bucket  = hash(user_id + ':' + experiment_id) % 100
variant = 'treatment' if bucket < 50 else 'control'
# Same user_id -> same bucket -> same variant, every time.
把穩定的使用者 ID 與實驗名稱一起雜湊來分桶,能得到穩定、50/50、可重現的分組。

先把假設與成功標準寫下來

在測試上線之前,把完整的計畫寫下來——而且最好鎖進一份共享文件裡:假設、OEC 與護欄、你在乎的效果大小、你要跑多久、你要怎麼分析,以及決策規則。這個習慣叫做「預先登錄」(pre-registration),它是整個實驗領域裡最便宜的保險。

它之所以重要,既是統計上的、也是心理上的。如果你只在看到資料之後才決定什麼算成功,那麼每個測試都會成功——你總能找到某個指標、某個次群體、某一週,看起來不錯。這裡住著兩宗有名的罪。p 值操弄(p-hacking)是一個接一個地試各種分析,直到有東西越過界線為止。HARKing(Hypothesizing After the Results are Known,在知道結果後才提出假設)則是把假設發明成剛好符合你找到的東西,再裝作你一開始就預測到了。預先登錄能瓦解這兩者,因為假設與指標在任何資料存在之前就已經釘死了。

用白話把假設說清楚,並把它接回你先前認識的那套正式機制。虛無假設是那個無聊的預設:卡片對 OEC 毫無影響。對立假設是你的賭注:卡片提高了每位活躍使用者的完成課程數。檢定接著問一個精確的問題——如果虛無假設為真、卡片其實什麼也沒做,那麼出現像這樣極端的資料會有多令人驚訝?這份驚訝由 p 值來概括,你再拿它和一個事先選定的顯著水準(幾乎總是 5%)比較。關鍵在於:p 值「不是」卡片有效的機率;它是「假設卡片什麼也沒做」的前提下、出現這麼極端資料的機率。現在、在看到資料之前就把顯著水準固定下來,是誠實地預先登錄的一部分。

預先登錄也會逼你事先承諾最小可偵測效果(MDE)與樣本數。MDE 是「小到剛好仍值得上線」的那個最小改善幅度——不是你「能」偵測到的最小值,而是你「在乎」的最小值。這裡也是效果量與務實面相遇之處:0.1% 的提升也許是真的,卻仍不值得那些工程成本。你堅持要捕捉的 MDE 越小,需要的使用者就越多,因為微小的訊號會被雜訊埋沒。比較兩組平均數時有個粗略的拇指規則——在常用的 5% 顯著水準與 80% 檢定力下——每組需要的使用者數,大約是變異數的十六倍除以效果的平方:

n \approx \frac{16\,\sigma^2}{\Delta^2}

每組樣本數的粗估:n 隨雜訊(σ²,變異數)增大,並隨你想捕捉的效果(Δ)快速縮小。把 MDE 砍半,樣本大約會變成四倍。下一篇指南會正式推導這條式子。

一個互動式的樣本數計算器:設定基準指標、你在乎的 MDE、你的顯著水準與檢定力,它就回傳每組需要的使用者數與預期的跑測時間。我們會在下一篇完整深入檢定力分析樣本數計算——在這裡,只要先感受一下這些旋鈕如何彼此取捨就好。

一個計算器面板,有控制基準率、最小可偵測效果、顯著水準與檢定力的滑桿;輸出框顯示每組所需的樣本數與預估的跑測天數。

  1. 用一句白話寫下假設(改了什麼、哪個指標、往哪個方向)。
  2. 釘死 OEC、各個護欄,以及每個護欄的容忍度。
  3. 設定 MDE(小到仍值得上線的最小效果)、顯著水準,以及目標檢定力。
  4. 算出所需樣本數與跑測時間;承諾跑到那時才停,不提早。
  5. 把決策規則與分析方法寫下來,並在上線前請另一個人審閱。

埋點與紀錄的陷阱

一個實驗只能量到你紀錄下來的東西。你忘了擷取的資料就永遠消失了,所以埋點(instrumentation,也就是你的程式所紀錄的事件與欄位)是設計的一部分,而不是事後才補的東西。把 OEC 與護欄需要的每一個事件都列出來(曝光、點擊、開始上課、完成課程、頁面載入時間),並在你信任任何一個結果之前,確認每一個事件都真的正確觸發。

抓出壞掉的資料管線,最便宜的方法是 A/A 測試:把使用者分成兩組、但給予完全相同的體驗,然後檢查分析是否真的找不到差異。如果一個 A/A 測試竟然回報出「顯著」的效果,那你的隨機分派、紀錄或分析就壞了——在你真正跑任何 A/B 之前,先把它修好。先跑一次 A/A,或在背景持續跑一個 A/A 當作煙霧偵測器。

有三個陷阱會一再咬人。第一,曝光紀錄,也叫做「觸發」(triggering):只計入那些真的有機會看到改動的使用者。如果「繼續學習」卡片只對回訪的學生顯示,卻把那些根本看不到它的全新訪客也算進來,只會用一群測試根本動不了的人去稀釋效果。第二,樣本比例不符(SRM):你本來想要 50/50 的分組,卻觀察到,比方說 53/47。這個落差代表有東西在不均勻地掉使用者——某一組更常失敗的轉址、機器人流量,或遺失的日誌——而一個出現 SRM 的測試,就不該被信任,沒有例外。第三,是平常的資料衛生:重複事件、機器人過濾、時區邊界,以及在負載下默默掉列的紀錄。

-- Per-variant sanity check: is the split ~50/50, and what is the metric?
SELECT
  variant,
  COUNT(DISTINCT user_id)        AS users,        -- compare for SRM
  AVG(lessons_completed)         AS lessons_per_user
FROM experiment_exposures e
JOIN user_weekly_activity a USING (user_id)
WHERE experiment_id = 'continue_learning_v1'
GROUP BY variant;
在讀取效果之前,先用眼睛掃一下兩組的使用者數(它們應該接近相等)以及每組的 OEC。數量一面倒,就是樣本比例不符,是個停止標誌。

一份可以重複使用的設計範本

上面說的一切,都能塞進單獨一頁——一份你在上線前填好、之後永久保存的「實驗單頁文件」。重複使用同一份範本,能把設計從一場英勇的壯舉變成一個習慣,讓審閱變快,而且(因為它有版本控制)讓每一個結果在數個月後都可重現——當有人問起「等等,我們到底測了什麼?」的時候。

experiment: continue_learning_card_v1
owner: jovana
hypothesis: >
  Showing a 'Continue learning' card on the home page
  raises lessons completed per weekly active user.
oec: lessons_completed_per_wau          # the one decision metric
guardrails:
  - page_load_time_p95                  # must not increase materially
  - weekly_cancellation_rate            # must not increase materially
randomization_unit: user                # analyze per user too
variants:
  control:   current_home
  treatment: home_with_continue_card
mde: 0.02                  # +2% relative; smallest effect worth shipping
significance_level: 0.05
power: 0.80
sample_size_per_arm: 42000
planned_duration_days: 14  # do not stop early (no peeking)
analysis: two-group test on TRIGGERED users; report effect with 95% CI
decision_rule: >
  Ship only if the OEC interval is positive and excludes 0,
  AND no guardrail interval shows meaningful harm.
一份填空式的實驗規格。把它和你的程式放在一起做版本控制,讓「設計」——而不只是「結果」——成為永久紀錄的一部分。

注意這份範本悄悄替你擋掉了哪些東西——而這一切都在上線前就決定好了。預先承諾跑測時間,能防範偷看(一看到指標漂亮就停手,這會嚴重灌大偽陽性——那是後面某篇指南的主題)。指定單一 OEC,能防範指標大雜燴。明確規定只看觸發使用者與隨機分派單位,能防範稀釋與壞掉的標準誤。而那條事先寫好的決策規則,能防範你未來那個一廂情願的自己。

  1. 決策寫成一句話(什麼結果導向什麼行動)。
  2. 選定恰好一個 OEC;列出護欄與它們的容忍度。
  3. 選定隨機分派單位,且分析使用同一單位。
  4. 釘死 MDE、顯著水準、檢定力、樣本數與跑測時間。
  5. 用 A/A 測試驗證埋點;SRM 檢查自動化。
  6. 整份單頁文件經過審閱並提交到版本控制。

這就是設計 A/B 測試的工夫:大部分的可信度,都是在實驗開跑「之前」買到的——靠著選對指標、選對單位、訂出合理的護欄,以及一條你在還誠實的時候就寫好的決策規則。下一篇指南會把你在這裡承諾的 MDE 與樣本數,變成真正的數字——樣本數檢定力的數學——讓你在按下開始之前,就確切知道要跑多久。把這些單頁文件保存在一起,也帶給你可重現性,以及一份不斷增長、記錄著你的團隊已經學到什麼的組織記憶。