Dev Study
← 評価のための統計
レッスン 5 / 10·約 8 分·第22期 評価のための統計

同じ相手で比べると、ばらつきが消える

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

差にも差のばらつきがあります。今回は、そのばらつきの大半が「どの件を測ったか」から来ている場合に、比べ方でそれを消す方法を見ます。

まず結論

新旧を別々の平均で比べると、件ごとの大小の違いがそのまま差の揺れに入り、小さな改善は埋もれます。同じ件で両方を測り、件ごとの差を取ると、件の違いは引き算で消えます。

ただし効くのは、両方の測定が件ごとに連れ動く(相関がある)ときだけです。件の違いが無ければ、対応を取っても何も変わりません。

? 読む前に予測

大小さまざまな40件(1件 5.8〜111.9ms)を旧版と新版で1回ずつ測りました。新版はどの件でも2%速い設定です。平均の差 -0.52ms を、(ア)新旧それぞれの平均のばらつきから見積もった揺れで割る場合と、(イ)件ごとの差のばらつきから見積もった揺れで割る場合、結果はどうなったでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

揺れの大半は「どの件か」から来ている

ベンチマークの40件は、この模型の1回では、軽い件と重い件で所要時間が20倍近く違います。新版がどの件でも2%速くても、新旧それぞれの平均を比べると、その差を測る物差しには「軽い件と重い件の開き」が丸ごと入ります。2%の改善は、その開きの中に埋もれます。

同じ40件を両方で測ったなら、件ごとに「新 − 旧」を取れます。重い件の重さは両方に同じだけ入っているので、引き算で消えます。残るのは測るたびの揺れと、改善そのものです。

確率の問題集(prob-random-variables-9)が正解にしているとおり、和や差のばらつきを単純に足してよいのは2つが独立(厳密には無相関)なときだけで、連れ動くなら共分散の項が残ります。差の場合、その項は引かれます。連れ動く分だけ、差の揺れは小さくなります。

同じ40件・同じ平均の差を、2通りの揺れで割る(seed=1 の1回)
40件の所要時間5.8〜111.9 ms。件ごとの大小が大きい
どう比べるか
対応なし旧の平均と新の平均。揺れ = 件の大小 + 測定の揺れ
対応あり件ごとに新 − 旧。件の大小は引き算で消える
平均の差 -0.521 ms を揺れで割る
t = -0.09揺れ 5.582 ms
t = -3.53揺れ 0.148 ms
  • ・割られる差は同じ。違うのは割る揺れだけ
  • ・1回の値。繰り返したときの散らばりは後で見る
具体実物

1908年の表に、同じことが書いてある

Student(1908)の Illustration I は、10人の患者に2種類の睡眠薬を使い、それぞれで増えた睡眠時間を並べた表です。

Student (1908) p.20 の表(原本スキャンから転記。小数点の中点を . に、+·7 を +0.7 のように書き換えた。※原本は −1 と印字。同じ行の差 0 と列平均 +0.75 から −0.1 の誤植と判断し、計算には −0.1 を使った)表
Patient 1 (Dextro-) 2 (Laevo-) Difference (2-1)
1 +0.7 +1.9 +1.2
2 -1.6 +0.8 +2.4
3 -0.2 +1.1 +1.3
4 -1.2 +0.1 +1.3
5 -1 (※) -0.1 0
6 +3.4 +4.4 +1.0
7 +3.7 +5.5 +1.8
8 +0.8 +1.6 +0.8
9 0 +4.6 +4.6
10 +2.0 +3.4 +1.4
Mean +0.75 +2.33 +1.58
S.D. 1.70 1.90 1.17

最後の S.D. の行を見てください。元の2列は 1.70 と 1.90 なのに、差の列は 1.17 です。2列が独立なら差の S.D. は √(1.697² + 1.899²) ≈ 2.55 になるはずで、実際はその半分以下です。Student 自身が理由を書いています(p.21)。

The low value of the S.D. is probably due to the different drugs reacting similarly on the same patient, so that there is correlation between the results.

同じ患者は、どちらの薬にも似た反応を示します。2列の相関は計算すると 0.795 です。患者の違いが両方の列に同じ向きで入っているので、差を取ると消えます。

✓ ここまでの確認

Student(1908)は、差の列の S.D. が 1.17 と小さい理由を何と書いていますか。

抽象原理

効き目は、連れ動く度合いで決まる

対応を取るのは「同じ相手の値を物差しにする」ことです。通常の A/B テストでは同じ利用者を両方の群に入れられませんが、同じ考えは使えます。CUPED は、利用者ごとの実験前の値を物差しにします。

The results on Bing’s experimentation system are very successful: we can reduce variance by about 50%, effectively achieving the same statistical power with only half of the users, or half the duration.

同じ論文は、残るばらつきを次のように書きます。

With the optimal choice of θ from Eq (4), we have that ∆cv reduces variance by a factor of ρ² compared to ∆, i.e. var(∆cv) = var(∆)(1 − ρ²).

ρ は実験前の値との相関です。相関が0なら少しも減らず、相関が高いほど大きく減ります。 約50%は Bing の指標での実績であって、どの指標でも半分になるという約束ではありません。論文は、実験前の同じ指標を使うのがたいてい最も効いた、とも書いています。

具体実物

40件のベンチマークで、繰り返して確かめる

冒頭の40件を模型にしました。件の大きさは対数正規で大小さまざま、新版は全件で2%速く、測るたびに2%の揺れが乗ります。1回の t だけでなく、同じ設計を2000回繰り返した散らばりも出します。

import random, sys, math, statistics as st
def trial(rng, n=40):
    items = [rng.lognormvariate(3, 1) for _ in range(n)]  # 40 items, sizes differ a lot (ms)
    A = [t*rng.gauss(1, 0.02) for t in items]             # old: 2% noise per measurement
    B = [t*0.98*rng.gauss(1, 0.02) for t in items]        # new: 2% faster on every item
    d = [b-a for a, b in zip(A, B)]
    t_unp = st.mean(d)/math.sqrt(st.variance(A)/n + st.variance(B)/n)
    t_pair = st.mean(d)/(st.stdev(d)/math.sqrt(n))
    return items, st.mean(d), t_unp, t_pair
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 1
items, md, tu, tp = trial(random.Random(seed))
print(f"seed={seed} items {min(items):.1f}..{max(items):.1f} ms  mean diff={md:.3f} ms")
print(f"  one run : unpaired t={tu:.2f}  paired t={tp:.2f}")
rng = random.Random(seed); R = 2000
res = [trial(rng)[2:] for _ in range(R)]
for k, name in ((0, "unpaired"), (1, "paired")):
    v = [r[k] for r in res]; q = st.quantiles(v, n=20)
    print(f"  {R} runs {name:8s}: median t={st.median(v):.2f}  5-95%: {q[0]:.2f}..{q[-1]:.2f}  |t|>2.02: {sum(abs(x) > 2.02 for x in v)/R:.1%}")
$ python3 paired.py 1; python3 paired.py 2(macOS 26.5.2・Python 3.14.6)コマンド出力
seed=1 items 5.8..111.9 ms mean diff=-0.521 ms
one run : unpaired t=-0.09 paired t=-3.53
2000 runs unpaired: median t=-0.08 5-95%: -0.14..-0.03 |t|>2.02: 0.0%
2000 runs paired : median t=-2.88 5-95%: -4.35..-1.34 |t|>2.02: 82.8%
seed=2 items 2.4..177.7 ms mean diff=-1.025 ms
one run : unpaired t=-0.11 paired t=-3.20
2000 runs unpaired: median t=-0.08 5-95%: -0.14..-0.03 |t|>2.02: 0.0%
2000 runs paired : median t=-2.88 5-95%: -4.40..-1.41 |t|>2.02: 83.5%

|t| が 2.02 を超えたら「検出」と数えています(40組で両側5%の目安)。対応なしは2000回で一度も検出しません。対応ありは8割強で検出します。 冒頭の -3.53 は対応ありの中でも出来の良い側で、繰り返すと t は 5〜95% で -1.3〜-4.4 程度に散ります。「対応を取れば必ずはっきり出る」ではありません。

逆向きも回しました。全件が同じ 20ms で件の違いが無く、測るたびに10%揺れる模型です。検出は対応なし 15.0%・対応あり 15.2%(seed=1)、14.9%・14.1%(seed=2)で、差がありません。(この模型の対応なしは自由度78の閾値 1.9913 で判定しています。2.0227 に揃えても 14.4% で、結論は変わりません) 消すべき件の違いが無ければ、引き算で消えるものもありません。

✓ ここまでの確認

件ごとに差を取っても、検出できる割合が対応なしとほぼ変わらなかったのはどんな40件のときですか。

⇄ 2つを見比べる
具体例 A

大小さまざまな40件

件の大きさが 5.8〜111.9ms と開いていて、揺れの大半が件の違い。件ごとに差を取るとそれが消え、2000回中の検出が 0.0% から 82.8% になった。

具体例 B

全件が同じ 20ms の40件

件の違いが無く、揺れはすべて測るたびの揺れ。差を取っても消えるものが無く、検出は対応なし 15.0%・対応あり 15.2% とほぼ同じだった。

同じ「件ごとに差を取る」操作なのに、片方では劇的に効き、片方では何も変わりません。何が違うのでしょうか。

それぞれの揺れが、どこから来ているかを分けてみてください。

この章のまとめ

  • 新旧を別々の平均で比べると、件ごとの大小がそのまま差の揺れに入る。40件の模型では対応なしの検出は 0.0%。
  • 同じ件で両方を測って件ごとの差を取ると、件の違いが消える。同じ模型で検出は8割強。ただし1回の t は -1.3〜-4.4 程度に散る。
  • Student(1908)は差の S.D. が小さい理由を、同じ患者に似た効き方をする correlation だと書いた。独立なら約2.55、実際は1.17。
  • 効き目は連れ動く度合いで決まる。件の違いが無ければ対応を取っても変わらない(15.0% と 15.2%)。
  • CUPED の約50%は Bing での実績。残るばらつきは相関で決まり、相関0なら減らない。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

CUPED の論文が Bing で報告した「ばらつき約50%減」の読み方として適切なのはどれですか。

用語集(4語)
対応あり(paired)
同じ相手(件・患者・利用者)について両方の条件を測り、相手ごとの差を取って比べるやり方。相手ごとの違いが引き算で消える。
相関
2つの測定が相手ごとに連れ動く度合い。-1〜1。Student の表の2列は 0.795。対応の効き目はこれの大きさで決まる。
t
平均の差を、その差の揺れ(標準誤差)で割った値。本回の実演では |t| が 2.02 を超えた試行を「検出」と数えた(40組の両側5%の目安)。
CUPED
Deng らが WSDM 2013 で示した方法。利用者ごとの実験前の値を使って指標のばらつきを減らす。減り方は実験前の値との相関で決まる。