Closed15

【実験】1%は100回やっても63%らしい

nanasinanasi

1%で起こる事象は、100回試行しても63%の確率でしか起こらない。
もう少し具体的に言うと、1%で起こる事象Aを出すべく100回試行した際、少なくとも一回事象Aが起こる確率は約63%である。

といっても、私のような確率初心者にとっては、いくら理論がそうだと言っていても直感的には信じがたい話である。100%でないことくらいはわかるけど...
こういうときに役に立つのはいつだってプログラミング。ということでコードを書いて試していく。


用語メモ

  • 事象(event): 試行の結果として起こりうる出来事
  • 試行(experiment): ある結果を得るために行う行為や操作
  • 確率(probability): ある事象が起こる可能性の度合い
63%を計算で求める場合

まず確率とは、望ましい結果の数 / すべての結果の数で求めることができる。
しかしこれは試行回数が1回の場合の確率であり、試行回数が複数回(今回なら100回)の場合に1回でも起こるかの確率を求める場合は変わってくる。

求め方は1 - 事象が起こらない結果の確率、つまり1 - 余事象が起こる確率となる。
この余事象が起こる確率は、1(100%) - 1%つまり99%を100回連続で引くかどうか、という話になる。
これは単純に0.99^100で求められるらしい。

ということで、最終的な式は1 - 0.99^100となり、結果はだいたい63%になる。

nanasinanasi

確率についてここで解説したような入門的なところから学びたい場合は、中学や高校の数学の教科書をひっぱり出すか、ChatGPTなどのお好みのAiに解説してもらおう。

nanasinanasi

TypeScriptの場合

interface EventResult {
  winning: boolean, // 事象が起こったか
  count: number // 何回目で起こったか、デバッグや結果を眺めるとき用
}

// 1%で起こる事象を100回やる
// 計算的には63%の確率でwinningがtrueになる
function randomEvent100(): EventResult {
  let result: EventResult | null = null
  for (const count of Array(100).keys()) {
    const winning = Math.random() < 0.01
    if (winning) {
      result = { winning, count }
      break
    }
  }
  if (result === null) result = { winning: false, count: 100 }
  return result
}

// 100回やったときに出るかどうかを試行回数分繰り返し、確率にする
function experiment(trials: number) {
  const array = Array(trials).fill(0).map(randomEvent100)
  const probability = array.filter(r => r.winning).length / trials
  return { probability, results: array }
}

const TRIALS = 1000000
const { probability } = experiment(TRIALS)
console.log(`確率: ${probability * 100}% (試行回数: ${TRIALS})`)
nanasinanasi

コード解説

  1. randomEvent100関数は、100回試行した結果1%で起こる事象が起こったかどうかを返す
  2. experiment関数は、randomEvent100trial引数で指定された試行回数実行する
  3. experimentrandomEvent100の呼び出し結果たちの中で何回事象が起こったかを返す
  4. あとはTRIALS定数で試行回数を決め、experimentを呼び出して出力するだけ
nanasinanasi

TRIALS定数は、最初は1000くらいで一度試しておいたほうがいい。
それで大丈夫そうなら10倍ずつくらい上げていけば、過度に重くなることはないと思う。

nanasinanasi

そして、このコードを実行すると、だいたい63.4%くらいになる。

確率: 63.3768% (試行回数: 1000000)
確率: 63.368% (試行回数: 1000000)
確率: 63.429899999999996% (試行回数: 1000000)
...
nanasinanasi

Scalaの場合

case class EventResult(winning: Boolean, count: Int)

def randomEvent = {
  import scala.util.Random
  Random.nextInt(100) == 1
}

def randomEvent100 = {
  val itr = LazyList.continually(randomEvent).take(100)
  EventResult(
    winning = itr.exists(identity),
    count = itr.indexWhere(identity)
  )
}

def experiment(trials: Int): Float = {
  val itr = LazyList.continually(randomEvent100).take(trials)
  itr.count(_.winning).toFloat / trials
}

val TRIALS = 100000
val probability = experiment(TRIALS)
println(s"確率: ${probability * 100}% (試行回数: $TRIALS 回)")
nanasinanasi

やってることはTypeScriptとほぼ同じ。

変更点:

  • randomEvent関数が増えた
  • experimentrandomEvent100の1行目がだいぶ似通ることになった
  • TypeScriptではやらなかったIterator=ScalaではLazyListでの実装になった
nanasinanasi

おまけ:

  • 10%が10回で引ける確率: 0.6513215599 (式: 1 - 0.9 ** 10)
  • 1%が10回で引ける確率: 0.6339676587267709 (式: 1 - 0.99 ** 100)
  • 0.1%が1000回で引ける確率: 0.6323045752290363 (式: 1 - 0.999 ** 1000)
  • 0.01%が10000回で引ける確率: 0.6321389535670295 (式: 1 - 0.9999 ** 10000)

徐々に減っていくらしい...

nanasinanasi

おまけ2:

この式の0.9999 ** 10000、つまり(1 - 1 / n) ** nの部分、nが大きければ大きいほど自然対数eの逆数?に近づいてくらしい。
つまりnを無限とした場合、この式は1 / eって置き換えられるらしい。

なお、eは銀行の金利の分割に例えられるらしい。

\text{1回だけ増える:} \\ \quad \left(1 + 1\right)^1 = 2 \\[6pt] \text{半年に2回増える:} \\ \quad \left(1 + \frac{1}{2}\right)^2 = 1.5^2 = 2.25 \\[6pt] \text{四半期に4回増える:} \\ \quad \left(1 + \frac{1}{4}\right)^4 = 1.25^4 \approx 2.4414 \\[6pt] \text{12分割:} \\ \quad \left(1 + \frac{1}{12}\right)^{12} \approx 2.6130 \\[6pt] \text{100分割:} \\ \quad \left(1 + \frac{1}{100}\right)^{100} \approx 2.7048

要するに(1 + 1 / n) ** nって式のnを無限大にした数値がeって定義できるらしい。
そして、この式ってさっきのやつとほぼ同じである。違いは足し算か引き算か。
だからeが計算式に出てくるらしい。ってChatGPTが言ってた。

こんなところでeが出てくるとは思わなかった...

nanasinanasi

おまけ3:

1 / n の確率をn回試すと、だいたい63.2%(1 - 1 / e)の確率で1回以上当たる」現象は、どうやらeの話も含めて結構有名らしい。

こうした例は、プログラマやデータサイエンティスト、数学を使う人のあいだでは常識に近いです。(ChatGPTより)

プログラマーの間でも常識なんだって(流れ弾

nanasinanasi

終わり
確率は奥が深いということがわかった

このスクラップは4ヶ月前にクローズされました