モンティホール問題の解が 50% にならないことを暗号論的擬似乱数を使って検証してみる
モンティホール問題とは?
モンティホール問題とは、以下のような確率の問題です。
3 つの閉じたドアがあります。
そのうちの 1 つのドアの先には景品の新車があり、残り 2 つのドアの先にはハズレを意味するヤギがいます。プレイヤーは新車があるドアを開けると新車をもらうことができます。
プレイヤーがドアを 1 つ選ぶと、司会者のモンティホールは、残りの 2 つのドアのうちヤギがいるドアを 1 つ開けてヤギを見せます。
ここでプレイヤーは、最初に選んだドアから、まだ開けられていないもう 1 つのドアに変更することができます。
さて、プレイヤーはドアを変更するべきでしょうか? (どちらのほうが新車を当てられる確率が高いでしょうか?)
「司会者がハズレのヤギのドアを開けたんだから、残るドアは 2 つで、ドアを変更してもしなくても確率 1/2 でしょ?」と思った方。実はこれは不正解です。
正解は、「ドアを変更したほうが当たる確率が高い」です。ドアを変更しなかった場合は当たる確率 1/3 で、ドアを変更した場合は当たる確率 2/3 になります。
なぜこのような結果になるのかについては以下の動画で解説されています。
Twitterのアンケートが意外な結果に!「モンティホール問題変形バージョン」
検証してみよう
さて、答えはわかったとしても、本当にそうなるのかがまだ納得できないという人もいるかもしれません。
なので、途中でドアを変更した場合と変更しない場合で 100 万回試行してみて、本当にその確率に沿う結果になるのかどうかを検証するプログラムを書いてみました。
結果だけ知りたい方は「検証結果」までジャンプしてください。
プログラム
実際に書いたプログラムが以下になります。Ruby で実装しました。
# frozen_string_literal: true
require 'optparse'
require 'securerandom'
options = {}
OptionParser.new do |opt|
opt.on('-d NUMBER', '--doors=NUMBER') { |v| options[:doors_count] = v }
opt.on('-t NUMBER', '--trials=NUMBER') { |v| options[:trials] = v }
opt.parse!(ARGV)
end
DOORS_COUNT = options[:doors_count].to_i >= 3 ? options[:doors_count].to_i : 3
TRIALS = options[:trials].to_i >= 1 ? options[:trials].to_i : 100
def main
correct_count = { changed: 0, never: 0 }
TRIALS.times do
correct_count[:changed] += 1 if try(changed: true)
end
TRIALS.times do
correct_count[:never] += 1 if try(changed: false)
end
puts "Number of doors: #{DOORS_COUNT} doors"
puts "Number of trials: #{TRIALS} times each"
puts
puts "Changed: #{correct_count[:changed]} times (Actual: #{((correct_count[:changed].to_f / TRIALS.to_f) * 100).round(2)}%, Expected: #{(((DOORS_COUNT - 1).to_f / DOORS_COUNT.to_f) * 100).round(2)}%)"
puts "Never: #{correct_count[:never]} times (Actual: #{((correct_count[:never].to_f / TRIALS.to_f) * 100).round(2)}%, Expected: #{((1.0 / DOORS_COUNT.to_f) * 100).round(2)}%)"
end
def try(changed:)
doors = Array.new(DOORS_COUNT, false)
doors[SecureRandom.random_number(DOORS_COUNT)] = true
first_choice = SecureRandom.random_number(DOORS_COUNT)
doors = reveal(doors, first_choice)
doors[second_choice(doors, first_choice, changed: changed)]
end
def reveal(doors, first_choice)
if doors[first_choice] # the first choice is correct
leftover = nil
loop do
leftover = SecureRandom.random_number(DOORS_COUNT)
break if leftover != first_choice
end
doors.map.with_index { |door, index| door if door || index == leftover }
else # the first choice is incorrect
doors.map.with_index { |door, index| door if door || index == first_choice }
end
end
def second_choice(doors, first_choice, changed:)
return first_choice unless changed
second_choice = nil
doors.each_with_index do |door, index|
unless door.nil? || index == first_choice
second_choice = index
break
end
end
second_choice
end
main
なお、元々のモンティホール問題は 3 つのドアですが、これが 10 個のドアだったり 100 個のドアだったりしても検証できる[1]ようなプログラムになっています。DOORS_COUNT
がドアの数を表しています。デフォルトは 3 です。
また、試行回数を変えることもできます。TRIALS
が試行回数を表しています。デフォルトは 100 です。
引数に -d NUMBER
を入れるとドアの数を変更できます。NUMBER
には数値が入ります。また、-t NUMBER
を入れると試行回数を変更できます。たとえば -t 100000
とすると 10 万回試行します。
main
途中でドアを変更した場合と変更しない場合に分けて何回か試行した結果を出力するメソッドです。
correct_count
に結果 (当たりを引いた回数) を入れていきます。correct_count[:changed]
が途中でドアを変えた場合の当たりの回数、correct_count[:never]
が途中でドアを変えなかった場合の当たりの回数です。
try
試行するメソッドです。当たりのドアを引けば true
、ハズレのドアを引けば false
を返します。
すべての要素が false
の配列 doors
を用意して、その中のどこかにランダムで true
を入れます。true
が新車があるドア、false
がヤギがいるドアということです。
first_choice
に配列 doors
の取りうる添字の値をランダムで入れます。これが、プレイヤーがドアを一つ選んだ状態となります。プレイヤーはもちろん答えを知らないのでランダムに値を取得します。
reveal
司会者が、残りのドアのうちヤギがいるドアを開けるメソッドです。
プレイヤーが選んだドア (first_choice
で指定した要素) と、新車があるドア (true
の要素) 以外をすべて nil
にした配列を返します。
ただし、もしプレイヤーが最初から新車のあるドア (true
の要素) を選んでいた場合は、全部のドアを開けてしまうと問題になりませんので、その場合は選ばれていないドア (すべてヤギのドアです) のうち 1 つのドアをランダムに選び、その要素はそのままにして、それ以外の要素 (もちろんプレイヤーが選んだドアを除く) をすべて nil
にします。
nil
が入っている要素は、司会者によってヤギがいる (ハズレである) ことが明らかになっている[2]ドアを意味します。
second_choice
プレイヤーが 2 回目に選んだドアを返すメソッドです。配列 doors
の取りうる添字の値を返します。
引数として受け取る changed
には true
または false
が入ります。true
はドアを途中で変えたことを意味し、false
はドアを変えないことを意味します。
ドアを変えない場合は first_choice
と同じということなので、そのまま first_choice
の値を返します。
ドアを変える場合は nil
ではない要素であり、かつ first_choice
とは違う値を返します。司会者が、ヤギがいるドアを開けたあとは、最初に選んだドアと、まだ開けられていないドアの 2 つしかないので、変えるとしたら一意に定まります。
検証結果
では実際に検証してみましょう。100 万回試行してみます。
$ ruby monty_hall_problem.rb -t 1000000
Number of doors: 3 doors
Number of trials: 1000000 times each
Changed: 666764 times (Actual: 66.68%, Expected: 66.67%)
Never: 333181 times (Actual: 33.32%, Expected: 33.33%)
わかりやすく表にまとめると以下のとおりです。
途中でドアを変更した場合 | 途中でドアを変更しなかった場合 | |
---|---|---|
当たりを引いた回数 (100 万回中) | 666,764 回 | 333,181 回 |
正解率 (小数点第 3 位四捨五入) | 66.68% | 33.32% |
本当の確率 (小数点第 3 位四捨五入) | 66.67% | 33.33% |
ドアを変更した場合はほぼ 2/3 の確率で正解し、ドアを変更しなかった場合はほぼ 1/3 の確率で正解しました。
というわけで、ドアを変更してもしなくても確率 50% というのは間違いで、たしかに変更したほうが正解率が高くなる、ということがわかりました。
おまけ: 暗号論的擬似乱数を使った理由
たいていの言語には、(ただの) 擬似乱数と暗号論的擬似乱数の 2 種類あります。
Ruby にももちろん両方あります。
irb(main):001:0> rand
=> 0.3204722194100945
irb(main):001:0> require 'securerandom'
=> true
irb(main):002:0> SecureRandom.rand
=> 0.6596803273103891
今回のプログラムでは、ただの擬似乱数ではなく暗号論的擬似乱数 (SecureRandom.random_number
) を使いました。その理由は、ただの擬似乱数だと偏りが発生する可能性があるから[3] です。
だからといって、暗号論的擬似乱数が全く偏りの生じない真の乱数列であるというわけではないのですが、暗号に使われている乱数ということもあって、予測できないほど偏りの生じない乱数であるとはいえます。
今回は数学的な確率を求める検証だったので、なるべく偏りの生じない乱数を使用したほうが良いと判断し、暗号論的擬似乱数を使用しました。
Discussion