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

1つの数ではなく、幅で言う

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

何も変えなくても数字は動きます。では手元の1回の結果を、どう言えば「動く分」まで含めて伝えられるのか。今回は、件数から幅を付ける方法を見ます。

まず結論

「30回中21回通った」を「成功率70%」と1つの数で言うと、次に走らせた回の19回との違いが意味ありげに見えます。件数から幅を付けると、2つの95%区間(信頼区間)はどちらも真の成功率 2/3 を含み、差そのものも統計的に有意ではありません。

0件も同じです。1000件でエラー0件は「エラー率0%」ではありません。Wilson の区間で上端は0.38%。観測した率の左右に幅を足す素朴な作り方は、0件のとき幅まで0に潰れます。

? 読む前に予測

カナリアに流した1000件で、エラーは0件でした。エラー率の95%区間(Wilson)の上端に最も近いのはどれでしょうか。

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

抽象原理

手にした1つの区間は、当たりか外れかのどちらか

同じものを2度測れば、数は動きます。だから1回の結果を1つの数で言うと、次の回との違いがすべて意味ありげに見えます。

そこで、件数から幅を付けて言います。95%区間(信頼区間)です。ただし、この「95%」が何の95%なのかは誤読されやすく、NIST のハンドブックはわざわざ注記しています。

As a technical note, a 95 % confidence interval does not mean that there is a 95 % probability that the interval contains the true mean. The interval computed from a given sample either contains the true mean or it does not. Instead, the level of confidence is associated with the method of calculating the interval.

手にした1つの区間は、真値を含んでいるか、いないかのどちらかです。95%は作り方の性質で、同じ手順で標本を取り区間を作り続けると、長い目で約95%が真値を含む、という意味です(同じ注記の後半 in the long run about 95 % of these intervals would contain the true mean)。

95%は1つの区間ではなく、区間の作り方に付く
真の率固定されていて、直接は見えない
同じ件数で、標本を取り直すたびに
1回目の区間真の率を含む
2回目の区間真の率を含む
3回目の区間外している
何度も繰り返すと
約95%の区間が含む作り方がうまくいけば、の話
  • ・手元の1つの区間が「含む側」かどうかは、区間を見ても分からない
  • ・ベイズ流の信用区間なら「この範囲に入る確率」と読める。流儀の違いで、優劣ではない
具体実物

同じテストを30回ずつ、2度走らせる

第16期『時々落ちるテストには、書かれていない入力がある』で扱う例では、100ms 待つテストを30回ずつ2度走らせています。コードは同じで、通ったのは21回と19回。1つの数で言えば70%と63%です。

件数から Wilson の区間を付けます。計算式はコードに置き、本文では使いません。

import math
from statistics import NormalDist
z = NormalDist().inv_cdf(0.975)
def naive(c, n):
    p = c / n; h = z * math.sqrt(p * (1 - p) / n); return p - h, p + h
def wilson(c, n):
    p = c / n; d = 1 + z * z / n; m = (p + z * z / (2 * n)) / d
    h = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / d; return m - h, m + h
for c, n in ((21, 30), (19, 30), (0, 30), (0, 1000)):
    print(f"{c}/{n}: wilson [{wilson(c, n)[0]:.4f}, {wilson(c, n)[1]:.4f}]  naive [{naive(c, n)[0]:.4f}, {naive(c, n)[1]:.4f}]")
def coverage(f, n, p):
    return sum(math.comb(n, k) * p**k * (1 - p)**(n - k)
               for k in range(n + 1) if f(k, n)[0] <= p <= f(k, n)[1])
for n, p in ((20, 0.05), (100, 0.01)):
    print(f"n={n} p={p}: naive {coverage(naive, n, p):.4f}  wilson {coverage(wilson, n, p):.4f}")
$ python3 range.py(乱数を使わないので、何度走らせても同じ出力)コマンド出力
21/30: wilson [0.5212, 0.8334] naive [0.5360, 0.8640]
19/30: wilson [0.4551, 0.7813] naive [0.4609, 0.8058]
0/30: wilson [0.0000, 0.1135] naive [0.0000, 0.0000]
0/1000: wilson [0.0000, 0.0038] naive [0.0000, 0.0000]
n=20 p=0.05: naive 0.6389 wilson 0.9245
n=100 p=0.01: naive 0.6334 wilson 0.9206

1行目と2行目の区間は、どちらもこのテストの真の成功率 2/3(遅れが100msを超える確率が約3分の1)を含みます。差 +0.07 そのものに素朴な式で95%区間を付けても -0.17〜+0.30 で、0を含みます。70%と63%の違いは、同じテストの揺れで説明がつきます。

幅で言っても、何も言えなくなるわけではありません。成功率が9割を超えるテストだ、という主張はどちらの区間も否定しています。

✓ ここまでの確認

同じ flaky テストを30回ずつ2度走らせ、21回と19回通りました。Wilson の95%区間は [0.521, 0.833] と [0.455, 0.781] です。言えることとして適切なのはどれですか。

抽象原理

0件のとき、素朴な区間は幅ごと消える

よく見かける作り方は、観測した率の左右に同じ幅を足し引きするものです。上の naive がそれで、幅は観測した率そのものから計算します。観測した率が0なら、幅も0です。0件のとき区間は [0, 0] になり、「エラー率はちょうど0」と言い出します。

NIST のハンドブック 7.2.4.1 は比率の区間として Wilson 法を載せ、推奨者として Brown・Cai・DasGupta(2001)と Agresti・Coull(1998)を挙げています。そのうえで、下限が負になりうる通常の式についてこう書きます。

A confidence limit approach that produces a lower limit which is an impossible value for the parameter for which the interval is constructed is an inferior approach.

実際に含む割合(被覆率)で見ると差は大きく出ます。出力の5・6行目です。真の失敗率5%のテストを20回走らせた場合、素朴な区間が5%を含むのは0.6389。20回すべて通る確率が 0.95^20 = 0.3585 あり、そのたびに [0, 0] が真値を外すからです。Wilson は0.9245で、名目の95%には届きません。 Wilson も万能ではなく、素朴な区間よりはるかにましだ、という相対の話です。

件数を増やせば素朴な区間が安全になる、とも限りません。Brown・Cai・DasGupta は率が小さい場合をこう報告しています。

If the true p is 0.005, then the coverage probability of the nominal 95% interval increases monotonically in n all the way up to n = 591 to the level 0.945, only to drop down to 0.792 if n is 592.

1件増やしただけで0.945から0.792へ落ちます。この値は二項分布の総和で再計算して一致を確かめました。

具体実物

カナリアの1000件で、エラーが0件

新しい版をカナリアに出し、1000件のリクエストでエラーは0件でした。出力の4行目で、素朴な区間は [0, 0]、Wilson の区間は [0, 0.0038] です。

言えるのは「エラー率は0%」ではなく、「95%区間で見て、0.38%までは否定できない」 までです。別の作り方(Clopper-Pearson)でも上端は0.37%で、作り方を変えても桁は変わりません。

件数を減らすと、上端は大きく広がります。

0件だったときの Wilson 95%区間の上端(range.py の wilson 関数で計算)表
件数 上端
20 16.11%
100 3.70%
1000 0.38%

20件で0件なら、95%区間で見て、16%程度までは否定できません。「0件だった」は、件数と一緒に言わなければ情報になりません。

✓ ここまでの確認

真の失敗率が5%のテストを20回走らせ、観測率の左右に幅を足す素朴な式で名目95%の区間を作ります。これを何度も繰り返すと、区間が真の5%を含む割合はおよそどれだけですか。

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

flaky テストの21回と19回

70%と63%の2つの数は違って見えます。Wilson 区間は [0.521, 0.833] と [0.455, 0.781] で、どちらも真の成功率 2/3 を含みます。差の区間も0を含みます。違って見える数が、同じものの揺れでした。

range.py の出力(1・2行目)コマンド出力
21/30: wilson [0.5212, 0.8334] naive [0.5360, 0.8640]
19/30: wilson [0.4551, 0.7813] naive [0.4609, 0.8058]
具体例 B

カナリアの0/1000

エラー率0%という数は、問題が無いように見えます。素朴な区間は [0, 0] に潰れますが、Wilson 区間の上端は0.38%です。ゼロに見える数が、ゼロとは限りませんでした。

range.py の出力(4行目)コマンド出力
0/1000: wilson [0.0000, 0.0038] naive [0.0000, 0.0000]

違って見える2つの数と、ゼロに見える1つの数。逆向きの錯覚に見えますが、共通する原因は何でしょうか。

どちらの場面でも、1つの数に直したときに消えた情報を探してください。

この章のまとめ

  • 95%は作り方の性質。手にした1つの区間は、真値を含むか含まないかのどちらか(NIST 1.3.5.2)。
  • 21/30 と 19/30 の Wilson 区間はどちらも真の 2/3 を含み、差の区間も0を含む。違って見える数が、揺れの範囲だった。
  • 観測率の左右に幅を足す素朴な区間は、0件で [0, 0] に潰れる。真の率5%・20回で、実際に含むのは0.6389。
  • Wilson も名目どおりではない(0.9245、0.9206)。素朴な区間よりはるかにまし、という相対の話。
  • 素朴な区間は件数を増やしても安全とは限らない。率0.005で n=591 は0.945、n=592 は0.792。
  • 0/1000 の上端は0.38%、0/20 なら16%。0件は件数と一緒に言う。

自分の現場で確かめる

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

環境:

理解度チェック

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

問題 1 / 3正解: 0

ある標本から95%区間 [0.52, 0.83] が出ました。NIST のハンドブックに沿った読み方はどれですか。

用語集(3語)
95%区間(信頼区間)
標本から作る値の範囲。同じ作り方を繰り返すと、長い目で約95%の区間が真値を含むように作る。手にした1つの区間が真値を含む確率ではない。
被覆率
作った区間が真値を実際に含む割合。名目の95%と一致するとは限らず、率が小さく件数が少ないと素朴な区間は大きく下回る。
Wilson の区間
成功数と件数から比率に付ける区間の1つ。0件でも幅を持ち、下限が負にならない。名目どおりの被覆率を保証するわけではない。