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

件数を4倍にして、ばらつきはやっと半分

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

アプリが記録する率は、何も変えなくても揺れます。今回は、その揺れが件数を増やすとどれだけ縮むのか、そして何を1件と数えればよいのかを見ます。

まず結論

率を測ると、何も変えなくても値は揺れます。その揺れの大きさ(標準偏差)は件数の平方根に反比例して縮むので、件数を4倍にしてやっと半分、64倍にしても8分の1です。

この縮み方の前提は、1件1件が独立であることです。同じ利用者の20ページビューを20件と数えると、揺れは件数の見かけほどには縮みません。

? 読む前に予測

真の率が5%のページで、100件ずつ観測率を測ると、その標準偏差は約2.18ポイントです。これを8分の1の約0.27ポイントにするには、1回に何件ずつ測ればよいでしょうか。

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

抽象原理

揺れは件数の平方根でしか縮まない

真の率が5%のページを100人に見せれば、平均5人が申し込みます。ただし毎回5人ではなく、少ない日も多い日もあります。何も変えていないのに、観測率は上下します。

件数を増やすと、この揺れは小さくなります。問題は縮み方です。1件ごとの揺れは、独立な件が集まると一部が打ち消し合います。打ち消し合いは完全ではないので、平均の揺れは件数に反比例するのではなく、件数の平方根に反比例して縮みます。件数を4倍にすると揺れは半分、16倍で4分の1、64倍でやっと8分の1です。

実験基盤の報告も、この関係を定義として置いています。

The Standard Error (Std-Err) of a statistic is the standard deviation of the sampling distribution of the sample statistic [9]. For a mean of 𝑛 independent observations, it is 𝜎̂/√𝑛, where 𝜎̂ is the estimated standard deviation.

(Kohavi ら 2012、§2。[9] は原文の参照番号)

大事なのは independent observations の一語です。√n で割ってよいのは、n 件が互いに独立なときです。

真の率5%の観測率の標準偏差(理論値)
100件2.179ポイント
件数×4 → 揺れ÷2
400件1.090ポイント
件数×4 → 揺れ÷2
1600件0.545ポイント
件数×4 → 揺れ÷2
6400件0.272ポイント
  • ・64倍の件数で、揺れはちょうど8分の1
  • ・前提は、1件1件が独立であること
具体実物

2000回測って、揺れを測る

真の率5%で n 件を観測する実験を、各 n で2000回繰り返し、観測率の標準偏差を出します。標準ライブラリだけで動きます。

# L2: spread (sd) of observed rate shrinks with sqrt(n). true 5%, 2000 repeats per n.
import random, sys, math, statistics as st
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 1
rng = random.Random(seed)
print(f"seed={seed}  sd of observed rate (true 5%), 2000 repeats")
for n in (100, 400, 1600, 6400):
    v = [sum(rng.random() < 0.05 for _ in range(n)) / n for _ in range(2000)]
    print(f"  n={n:5d}: sd={st.stdev(v)*100:.2f}pt  theory={math.sqrt(.05*.95/n)*100:.3f}pt")
print("relative SE at n=1000 (theory, no randomness)")
for p in (0.5, 0.05, 0.005):
    print(f"  p={p}: SE={math.sqrt(p*(1-p)/1000)*100:.3f}pt  SE/p={math.sqrt(p*(1-p)/1000)/p:.1%}")

seed を1・2・3と変えて3回走らせた結果を並べます(macOS、Python 3.14.6)。

観測率の標準偏差(ポイント)。理論値と、seed ごとの実測表
件数 理論 seed=1 seed=2 seed=3
100 2.179 2.18 2.18 2.22
400 1.090 1.09 1.12 1.10
1600 0.545 0.55 0.54 0.55
6400 0.272 0.28 0.27 0.27

実測は理論値のまわりに収まっています。ただし seed=1 だけを見ると2.18と0.28で、比は約7.8です。2000回から出した標準偏差も、それ自体が揺れる推定値だからです。1回の実行の値ではなく、理論値と繰り返しの幅を並べて読みます。

同じスクリプトの後半は、1000件での揺れを率そのもので割った値です。率50%なら3.2%、5%なら13.8%、0.5%なら44.6%(標準偏差0.223ポイント)。珍しい出来事の率ほど、同じ件数で相対的に大きく揺れます。

✓ ここまでの確認

seed=1 の実演では、100件の標準偏差が2.18、6400件が0.28で、比は約7.8でした。理論の比8と合わないことをどう読みますか。

抽象原理

何を1件と数えるか

√n の n は、独立な件数です。では1000ページビューが、50人から20回ずつ来ていたらどうでしょうか。同じ人の20回は、その人が申し込みやすいかどうかを共有します。

全体の真の率を5%にそろえたまま、利用者ごとの率の偏りだけを変えて、各4000回繰り返しました。

# L2 limit: 1000 page views that come from 50 users x 20 PV each are NOT 1000 independent samples.
import random, sys, math, statistics as st
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 1
rng = random.Random(seed)
U, M, R = 50, 20, 4000
def user_rate(kind):
    if kind == "A": return 0.05
    if kind == "B": return 0.02 if rng.random() < 0.5 else 0.08
    if kind == "C": return 0.5 if rng.random() < 0.1 else 0.0
    if kind == "D": return 1.0 if rng.random() < 0.05 else 0.0
def dist_var(kind):   # variance of per-user rate across users
    return {"A": 0.0, "B": 0.0009, "C": 0.1*0.25-0.0025, "D": 0.05*1-0.0025}[kind]
print(f"seed={seed}  50 users x 20 PV = 1000 PV, overall true rate 5%, {R} repeats")
p = 0.05
for kind in "ABCD":
    v = []
    for _ in range(R):
        c = 0
        for u in range(U):
            r = user_rate(kind)
            c += sum(rng.random() < r for _ in range(M))
        v.append(c / (U*M))
    rho = dist_var(kind) / (p*(1-p))            # intra-user correlation
    deff = 1 + (M-1)*rho                          # design effect
    th = math.sqrt(p*(1-p)/(U*M) * deff)
    print(f"  {kind}: sd={st.stdev(v)*100:.2f}pt theory={th*100:.2f}pt rho={rho:.3f} deff={deff:.2f} effective n={U*M/deff:.0f}")
print(f"  ref: independent n=1000 sd={math.sqrt(p*(1-p)/1000)*100:.2f}pt, n=50 sd={math.sqrt(p*(1-p)/50)*100:.2f}pt")
50人×20ページビュー=1000ページビュー、全体の真の率5%表
場面 利用者ごとの率 実効の件数 標準偏差(理論) 実測 seed=1 / seed=2
A 全員5% 1000 0.69 0.70 / 0.68
B 半分2%・半分8% 735 0.80 0.82 / 0.81
C 1割の人が50%、他は0% 100 2.18 2.14 / 2.18
D 5%の人が毎回、他は決して 50 3.08 3.07 / 3.08

実効の件数は、その揺れが独立な何件分の揺れと同じかを表します。C は1000ページビューあるのに、揺れは独立な100件と同じ2.18ポイントです。D では閲覧した50人と同じ件数になります。

ただし「50人なら n=50」と断定するのも誤りです。偏りが中程度の B では735件並みで、独立な1000件との差は小さく済みます。また、独立でも縮まない場合は別にあります。Kohavi らの2012年の論文は、Sessions/user などの指標で実験を長く回しても区間が縮まなかったと報告し、その理由を、ばらつきを平均で割った値が時間とともに変わることに求めています。独立でない数え方とは別の理由なので、混ぜないでください。

具体実物

HyperLogLog のレジスタ数

同じ平方根は、確率的データ構造にも出てきます。HyperLogLog はハッシュ値が一様な乱数のように振る舞うと仮定して、m 個のレジスタから重複を除いた件数を推定します。確率のクイズ群(prob-concentration-stats-9 の解説)は、相対標準誤差を約 1.04/√m、m=1024 なら約3.25% としています。係数1.04の原論文は、この回では照合していません。

縮み方は率の実験と同じ形です。誤差を半分にするには、レジスタを4倍にします。 違うのは払うものです。率の実験は件数、つまりふつうは実験の期間で払い、HyperLogLog はメモリで払います。そして、どちらの見積もりも前提の上に立っています。HyperLogLog ではハッシュ値が一様な乱数のように振る舞うという仮定で、これが崩れれば見積もりは成り立ちません。

✓ ここまでの確認

1000ページビューが、50人から20回ずつ来ていました。全体の真の率は5%です。観測率の揺れについて言えることはどれですか。

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

率の実験の件数

真の率5%で、100件の揺れ2.179ポイントが6400件で0.272ポイント。払うのは件数、ふつうは実験の期間。同じ人の閲覧を別々に数えると、実効の件数は見かけより少なくなる。

具体例 B

HyperLogLog のレジスタ数

相対標準誤差は約1.04÷√m(確率のクイズ群の値)。1024個で約3.25%。払うのはメモリ。ハッシュ値が一様に振る舞うという仮定が前提。

件数を増やす実験と、レジスタを増やすデータ構造。精度の上げ方について共通するのは何でしょうか。

誤差を半分にするには、それぞれ何を何倍にしますか。そして、その計算は何を仮定していますか。

この章のまとめ

  • 観測率の揺れは件数の平方根に反比例して縮む。4倍で半分、64倍で8分の1(2.179→0.272ポイント)。
  • 実演の標準偏差もまた揺れる。seed を変えた繰り返しと理論値を並べて読む。
  • 珍しい率ほど相対的に大きく揺れる。1000件で率0.5%なら、標準偏差は率の44.6%。
  • √n の n は独立な件数。50人×20ページビューは、偏り次第で独立な1000件並みから50件並みの間になる。
  • 区間が縮まない理由は独立性だけではない。ばらつきと平均の比が時間で変わる指標もある。

自分の現場で確かめる

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

環境:

理解度チェック

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

問題 1 / 2正解: 0

真の率0.5%を1000件で測ると、観測率の標準偏差は約0.22ポイントで、率の44.6%にあたります。これの正しい読み方はどれですか。

用語集(3語)
標準偏差
ばらつきの大きさを元の単位で表した量。この回では、同じ条件で率を何度も測ったときに観測率がどれだけ上下するかを指す。
独立
1件の結果が、他の件の結果について何も教えないこと。同じ利用者の複数の閲覧は、その人の傾向を共有するので独立とは限らない。
実効の件数(有効標本サイズ、effective sample size)
独立でない標本の揺れが、独立な何件分の揺れと同じ大きさかを表す。数えた件数より少なくなる。