- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
このコースの芯は「その差は、何も変えなくても出る大きさではないのか」です。最初に、何も変えないときの数字の動き方を見ます。
まず結論
同じページを利用者2群に見せて何も変えずに比べても(A/A テスト)、率は群ごとに違って出ます。真の率5%・各群1000人なら、相対で20%以上ずれる確率は約31%です。
改善の数字を読む前に「何も変えなかったら、どれくらい動くのか」を知っておく必要があります。入力を固定して消せる揺れと、固定すると比べる意味が消える揺れは、分けて扱います。
真の購入率がどちらも5%の同じページを、各1000人の2群に見せて比べます(何も変えていません)。2群の率が相対で20%以上ずれる確率はどれくらいでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
同じものを見せても、見る人が違う
新しいボタンで購入率が5.4%から5.8%に上がった。そう聞いたら、まず問うべきことがあります。ボタンを変えなかったら、数字はどれくらい動いていたのか。
これを実際に確かめる実験が A/A テストです。Kohavi らは2007年の論文でこう定義しています。
Instead of an A/B test, you exercise the experimentation system, assigning users to one of two groups, but expose them to exactly the same experience.
2群に分けて、両方に全く同じものを見せる。それでも率は違って出ます。ページは同じでも、どちらの群に誰が入ったかが毎回違うからです。買う気の強い人が片方に少し多く入れば、それだけで率は動きます。
- ・揺れの出どころはページではなく、群に入った人の顔ぶれ
- ・真の率5%・各群1000人なら、差の標準偏差は理論上約0.97ポイント(5%に対して約2割)
同じ論文は、A/A で何が起きるべきかも書いています。
the Null hypothesis should be rejected about 5% of the time when a 95% confidence level is used
Null hypothesis(帰無仮説)は、ここでは「何も変わっていない世界」のことです。何も変えていないのに、約5%の回は「統計的に有意」と判定されるのが正常だ、という意味です。
真の率5%・各群1000人の A/A を10回
標準ライブラリだけの模型です。両群とも真の率を5%に固定し、1000人ずつ「買ったかどうか」を乱数で決めます。
# L1: A/A test. true rate 5% in both groups, 1000 users each, 10 runs. stdlib only.
import random, sys, math
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 1
rng = random.Random(seed)
print(f"seed={seed} true 5% both, n=1000 each, 10 runs")
rels = []
for i in range(10):
a = sum(rng.random() < 0.05 for _ in range(1000))
b = sum(rng.random() < 0.05 for _ in range(1000))
rel = (b - a) / a * 100
rels.append(rel)
print(f" run{i}: A={a/10:.1f}% B={b/10:.1f}% diff={(b-a)/10:+.1f}pt rel={rel:+.0f}%")
print(f" range of rel: {min(rels):+.0f}% .. {max(rels):+.0f}%")
seed=1 true 5% both, n=1000 each, 10 runsrun0: A=5.4% B=5.8% diff=+0.4pt rel=+7%run1: A=5.8% B=5.0% diff=-0.8pt rel=-14%run2: A=4.8% B=4.2% diff=-0.6pt rel=-12%run3: A=5.2% B=5.5% diff=+0.3pt rel=+6%run4: A=5.5% B=6.5% diff=+1.0pt rel=+18%run5: A=6.0% B=3.7% diff=-2.3pt rel=-38%run6: A=5.5% B=4.9% diff=-0.6pt rel=-11%run7: A=4.9% B=5.2% diff=+0.3pt rel=+6%run8: A=5.1% B=5.1% diff=+0.0pt rel=+0%run9: A=4.5% B=4.2% diff=-0.3pt rel=-7%range of rel: -38% .. +18%
冒頭の「5.4%から5.8%」は run0 です。何も変えていません。run4 では +18%、run5 では −38% 動きました。
ただし、この10回は seed=1 の1組にすぎません。seed=3 では最大 +51% が出ました。10回を1組として2000組回すと、各組の最小の中央値は約 −26%、最大の中央値は約 +35% でした。正側に伸びやすいのは、相対差の分母が A だからです。
1回の A/A で相対差が20%以上になる確率を二項分布で厳密に計算すると 0.3095、10%以上なら 0.6122 です。「統計的に有意」と出た割合は、seed=1 と seed=2 でそれぞれ2万回模擬すると5.2%と4.8%でした。どちらも論文の言う約5%です。
A/A テストを100回回し、95%の基準で約5回「統計的に有意」と出ました。Kohavi らの定義に照らした読み方として適切なのはどれですか。
揺れには、消してよいものと消せないものがある
2012年の論文で、Kohavi らは A/A をこう評価しています。
The A/A test has been our most useful tool in identifying issues in practical systems. We strongly recommend that every practical system continuously run A/A tests.
有用なのは、揺れの大きさを先に知っておけるからです。2007年の論文も用途を2つ挙げます。検出力の計算に使うばらつきを集めること、そして実験基盤そのものを検査することです。A/A で効果が分かる、とは書いていません。 何も変えていないのですから。
ここで一般化できます。「同じもの」を2回測って数字が違うとき、その間に毎回違う入力が入っています。問うべきは、その入力を固定してよいかです。
- 固定してよい入力(テストデータ、実行環境など)なら、固定して揺れを消す
- 固定すると比べる意味が消える入力(誰が群に入るか)なら、揺れの大きさを知ったうえで読む
後者の揺れを件数や設計で縮める方法は、このコースで順に見ていきます。
入力を固定すると縮むベンチマーク
対比のために、同じ処理を20回計るベンチマークを作りました。100万要素のリストから値を探す data.index の時間です。random は毎回探す値を乱数で作り直し、fixed は毎回同じ値(500000)を探します。
# L1 contrast: "same benchmark" timed 20 times. mode random = new input each run, fixed = same input.
import random, sys, time, statistics as st
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 1
rng = random.Random(seed)
data = list(range(1_000_000))
def run(target):
t0 = time.perf_counter(); data.index(target); return (time.perf_counter() - t0) * 1000
for mode in ("random", "fixed"):
ts = [run(rng.randrange(len(data)) if mode == "random" else 500_000) for _ in range(20)]
print(f"seed={seed} {mode:6s} min={min(ts):6.2f}ms median={st.median(ts):6.2f}ms max={max(ts):6.2f}ms max/min={max(ts)/min(ts):7.1f}")
seed=1 random min= 0.16ms median= 2.73ms max= 5.15ms max/min= 32.1seed=1 fixed min= 2.68ms median= 2.71ms max= 2.75ms max/min= 1.0seed=1 random min= 0.16ms median= 2.69ms max= 5.03ms max/min= 31.8seed=1 fixed min= 2.69ms median= 2.72ms max= 2.80ms max/min= 1.0
入力を作り直すと、同じ「ベンチマーク」が 0.16〜5.15 ms に散りました。探す値が先頭に近いか奥にあるかで、仕事の量そのものが違うからです。入力を固定すると 2.68〜2.75 ms に縮みます。スクリプトを10回走らせ直しても、fixed は全回が 2.63〜3.03 ms に収まりました。
それでも完全には一致しません。同じ seed の2回目も少し違います。残りは入力以外の揺れです。この Mac では原因を特定していませんが、周波数の変動などの実行環境が候補で、固定できる場合もあります。なお ms の値は機械ごとに変わります。見るべきは値そのものではなく、random と fixed の幅の違いです。
A/A のずれを、ベンチマークと同じく「入力を固定する」で消そうとして、同じ1000人の同じ記録を A 群・B 群として2回集計しました。どうなりますか。
A/A テスト(真の率5%・各群1000人)
揺れの出どころは、誰がどちらの群に入ったか。これは実験の中身そのものなので固定できない。1回で相対20%以上ずれる確率は約31%。消すのではなく、大きさを知って読む。
相対差 確率10%以上 0.612220%以上 0.309530%以上 0.1285
ベンチマーク(data.index を20回)
揺れの大半は、毎回作り直していた入力から来ていた。入力を固定すると 0.16〜5.15 ms が 2.68〜2.75 ms に縮んだ。残った揺れは原因未特定だが、実行環境など固定できる候補がある。
入力 最小〜最大毎回作り直す 0.16〜5.15 ms固定する 2.68〜2.75 ms
片方は入力を固定すれば揺れが縮み、もう片方は固定できません。改善を主張する数字を見たとき、この違いから何を最初に確かめるべきでしょうか。
「その数字を、何も変えずにもう一度取ったらどれくらい動くか」を、両方で考えてみてください。
この章のまとめ
- A/A テストは、2群に全く同じ体験を見せる実験。それでも率は群ごとに違って出る。
- 揺れの出どころは、ページではなくどちらの群に誰が入ったか。
- 真の率5%・各群1000人の1回で、相対20%以上ずれる確率は約31%(厳密計算0.3095)。
- 10回の幅は seed ごとに変わる。1組の幅を一般化しない。
- 95%の基準で約5%の回は「統計的に有意」と出るのが正常。0回が正常なのではない。
- 揺れには、入力を固定して消せるもの(ベンチマーク)と、固定すると比べる意味が消えるもの(A/A)がある。後者は大きさを知って読む。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
Kohavi らは A/A テストの用途として何を挙げていますか。
用語集(3語)
- A/A テスト
- 利用者を2群に分けるが、両方に全く同じ体験を見せる実験。Null Test とも呼ばれる。ばらつきの把握と実験基盤の検査に使う。
- 何も変わっていない世界
- 比べている2つに本当は違いが無い、という想定。統計の用語では帰無仮説と呼ぶ。A/A はこの世界を実際に作って回す実験である。
- seed(乱数の種)
- 疑似乱数の出発点。同じ seed なら同じ乱数列が出る。1つの seed の結果は1回分の標本にすぎない。
出典(一次ソース)
- Kohavi, Henne, Sommerfield. Practical Guide to Controlled Experiments on the Web: Listen to Your Customers not to the HiPPO. KDD 2007(著者版 PDF、Page 4 §3.1・§5.2.1) ↗
- Kohavi, Deng, Frasca, Longbotham, Walker, Xu. Trustworthy Online Controlled Experiments: Five Puzzling Outcomes Explained. KDD 2012(§2 Background and Terminology) ↗