4つの実験をルールどおりに判定する
目標
SRM・z検定・信頼区間・標本サイズ・Holm補正を、標準ライブラリで計算する関数を作り、4つの実験の結果を、決められたルールで判定します。
なぜ重要なのか
小さなチームのA/Bテストは、標本が少なく、毎日覗き込み、結果がなければ分けて見ます。3つとも、偶然を発見に変える習慣です。ルールを先に決めて、そのルールどおりに計算して初めて、実験が製品の意思決定を守ってくれます。
用意するもの
/opt/fixtures/founder/ab/exp1.csv:user_id,variant,day,segment,converted(A対照、B変更、14日、セグメント10個)/opt/fixtures/founder/ab/exp2.csv:user_id,variant,converted(50:50の設計)/opt/fixtures/founder/ab/exp3_daily.csv:day,a_users,a_conv,b_users,b_conv(A/A実験の日付別の増分)/opt/fixtures/founder/ab/exp4.csv:variant,users,conversions(controlとv1–v5)
定義
- SRM: 期待50:50に対するカイ二乗適合度、自由度1。
chi2 = Σ(관측−기대)²/기대(韓国語で「観測値」「期待値」を意味する語です)、p = math.erfc(math.sqrt(chi2/2))。p < 0.001ならSRM。 - z検定:
diff = pb − pa。zは、プール比率p = (ca+cb)/(na+nb)の標準誤差√(p(1−p)(1/na+1/nb))で、両側p = 2(1−Φ(|z|))。 - 95%信頼区間:
diff ± Φ⁻¹(0.975)·√(pa(1−pa)/na + pb(1−pb)/nb)(非プール)。Φはstatistics.NormalDist()。 - 判定: p < 0.05でci_low > 0 →
"ship_b"、p < 0.05でci_high < 0 →"keep_a"、それ以外は"inconclusive"。 - 標本サイズ(片側): 読み物の式、α = 0.05の両側、検出力0.8、
math.ceil。 - Holm: pを昇順に、k=0,1,…番目で、
p·(m−k) ≤ 0.05なら棄却し、最初にそうでなければ止める。 - すべてのp・比率・差は、小数第6位で四捨五入。
ステップ
/root/founder/ab/ab.pyに、srm(n_a, n_b)→{"chi2": x, "p": y}を作り、/root/founder/ab/srm.jsonに、exp1・exp2それぞれの{"a": n, "b": n, "chi2": x, "p": y, "srm": true/false}を書く。ab.pyに、ztest(ca, na, cb, nb)→{"diff", "z", "p", "ci_low", "ci_high"}を作る(ca・cbは転換数、na・nbはユーザー数)。/root/founder/ab/result.jsonに、exp1のa_users,a_conv,b_users,b_conv,diff,p,ci_low,ci_high,decisionを書く。ab.pyにsample_size(p_base, mde)を作り、/root/founder/ab/power.jsonに、baseline(exp1のAのコンバージョン率、小数第4位)、mde(0.01)、n_per_arm(= sample_size(baseline, 0.01))、exp1_min_arm(exp1の両側のユーザー数のうち小さいほう)、powered(exp1_min_arm ≥ n_per_arm)を書く。/root/founder/ab/peeking.jsonに、exp3のdaily_p(1日目から累積の合計で計算したp、21個)、first_significant_day(累積p < 0.05の最初の日、なければnull)、final_p、final_decision(最終日の累積値による判定)を書く。ab.pyに、holm(pvalues)(名前→pの辞書 → 棄却される名前のソート済みリスト)を作り、/root/founder/ab/multi.jsonに、exp4のp(v1–v5それぞれ、controlとの比較)、naive(p < 0.05の名前、ソート済み)、holmを書く。/root/founder/ab/segments.jsonに、exp1のセグメント10個それぞれのp、naive(p < 0.05、ソート済み)、holmを書く。/root/founder/ab/decision.jsonに、exp1(判定)、exp2_trustworthy(SRMがなければtrue)、exp3_decision(最終日の判定)、exp4_ship(Holmで棄却されたリスト)、segment_claims(Holmで棄却されたセグメントのリスト)を書く。
参考
from statistics import NormalDist; N = NormalDist(); N.cdf(z); N.inv_cdf(0.975)- よくある間違い: 信頼区間にプール標準誤差を使うこと、片側p、増分を累積せずに日付別のpを出すこと、補正なしでセグメントを報告すること、SRMがある実験をそのまま判定することです。
標本の比率から確認する
/root/founder/ab/ab.pyにsrm(n_a, n_b)を作り、/root/founder/ab/srm.jsonに、exp1・exp2のa・b・chi2・p・srmを書く。
期待値は(n_a+n_b)/2です。自由度1のカイ二乗の裾の確率は、math.erfc(math.sqrt(chi2/2))で求められます。SRMの基準はp < 0.001です。
2つの比率のz検定と95%信頼区間
ab.pyにztest(ca, na, cb, nb)を作る。diff・z・p(両側)・ci_low・ci_highを小数第6位で。
検定統計量はプール比率の標準誤差、信頼区間は、それぞれの比率の標準誤差(非プール)です。Φはstatistics.NormalDist().cdf、臨界値はinv_cdf(0.975)です。
exp1を判定する
/root/founder/ab/result.jsonに、exp1のa_users・a_conv・b_users・b_conv・diff・p・ci_low・ci_high・decisionを書く。
判定ルール: p < 0.05で下限 > 0ならship_b、p < 0.05で上限 < 0ならkeep_a、それ以外はinconclusiveです。
この実験は、1%ポイントを捉えられるほど大きかったか
ab.pyにsample_size(p_base, mde)を作り、/root/founder/ab/power.jsonに、baseline・mde・n_per_arm・exp1_min_arm・poweredを書く。
p2 = p1 + mde、p̄ = (p1+p2)/2、zの値は、inv_cdf(0.975)とinv_cdf(0.8)です。かっこの中全体をmdeで割って2乗し、切り上げます。
A/A実験を毎日覗き込むと
/root/founder/ab/peeking.jsonに、exp3のdaily_p(累積21個)・first_significant_day・final_p・final_decisionを書く。
exp3_daily.csvは、その日の増分です。1日目から足していき、その日までの累積の合計で、ztestを呼んでください。日付は1から数えます。
バリアント5つ: Holm補正
ab.pyにholm(pvalues)を作り、/root/founder/ab/multi.jsonに、exp4のp(v1–v5)・naive・holmを書く。
各バリアントを、controlとztestします(controlがa側)。Holmは、昇順のk番目(0から)で、p·(m−k) ≤ 0.05を見て、最初に失敗したら止めます。
セグメント10個に分けて見ると
/root/founder/ab/segments.jsonに、exp1のセグメント10個のp・naive・holmを書く。
セグメントごとに、AとBを別々に数えて、同じztestを呼び、10個のpに同じholmを適用します。
実験ごとのデプロイ判断
/root/founder/ab/decision.jsonに、exp1・exp2_trustworthy・exp3_decision・exp4_ship・segment_claimsを書く。
前のステップのファイルから集めます。SRMがある実験は、信頼できない実験です。途中観察の反例の結論は、最初に有意になった日ではなく、最終日の判定です。