Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

擬似パラドックスであるところの「モンティ・ホール問題」をRubyで検証する

はじめに

世の中には擬似パラドックスと呼ばれるものがある。擬似パラドックスの定義とは、「直感的な感覚と、理論的な確率にズレがあるように見える事象のこと」で、それの代表としてモンティ・ホール問題というものがある。この問題は、たびたび取り上げられることがあり、例えば映画の中だと『ラスベガスをやっつけろ』という中で、主人公が数学に強いことの演出として使われたりしている。

元々、このパラドックスの問題については知っていたのだけれど、ディスカバリーチャンネルの『怪しい伝説』を見る機会があって、その中で、このモンティー・ホール問題について実験していたので視聴していたところ、試行回数によって歴然とした差が出てきて、俄然興味が沸いてきた。これは自分でも検証してみなければ、と思い、実際にコードを書いてみたのが、今回の記事。

そもそもモンティ・ホール問題とは何か

概要

モンティ・ホール問題の詳細については、上記のリンクを参考にしてもらうとして、ここではキモだけを説明する。

  1. 前提: ドアが三つある。その中のドアのうち、一つが当たりである。当たりのドアはランダムに決定される。勝利条件は、このドアの中から当たりのドアを見つけることである。
  2. ルール1: プレイヤーは、ドアを一つ選ぶ。
  3. ルール2: そのあと、選んだドア以外の、外れのドアが一つ開く。
  4. ルール3: プレイヤーは、外れのドアが一つ開いたあとに、ドアを変更するかどうかが選べる。

なぜ擬似パラドックスと呼ばれるのか

なぜ擬似パラドックスと呼ばれるかについては、下のことが言える。

  1. 最初のドアについても、外れのドアが一つ開かれたあとに、次のドアを選ぶにしても、三つのドアの中からドアを選ぶことには変わりがないので、確率は変動しないように思える。
  2. 最初のドアと外れのドアが一つ開かれたあとに、確率が変化しないように思える。

という二つのよる、というのが、「自分の直感」として、一瞬「変えないほうがいいのではないか」と思ってしまう理由だと思う。

検証用のコードを書いてみる

数学的な解説は上を参考にしてもらうとして、今回は、これを検証するためのRubyのコードを書くことだ。そのほうが、直感的にもわかりやすい筈だ。

class MontiDoor

  def initialize
    @door = [false, false, true].shuffle
  end

  def choice_door
    Random::rand(3).to_i
  end
  
  def choice_one
    @choice = choice_door
    return self
  end

  def open_door
    @open_door = choice_door
    while (@open_door == @choice || @door[@open_door]) do
      @open_door = choice_door
    end
    return self
  end
  
  def choice_switch
    @previous_choice = @choice
    @door.each_with_index do |door, x|
      if !(x == @previous_choice || x == @open_door)
        @choice = x
      end
    end
    return self
  end

  def debug_print
    puts "======================================"
    puts "Previous Choice :: #{@previous_choice}"
    puts "Open Door :: #{@open_door}"
    puts "Choice Door :: #{@choice}"
    puts "Door :: #{@door}"
    puts "======================================"
    return self
  end
  
  def win?
    @door[@choice]
  end
end

class MontiDoorManager
  attr_accessor :stay_doors, :switch_doors

  def initialize
    @stay_doors = [*0..1000].map do
      doortest = MontiDoor.new
      doortest.choice_one.win?
    end.to_a.select {|x| x}.length

    @switch_doors = [*0..1000].map do
      doortest = MontiDoor.new
      doortest
        .choice_one
        .open_door
        .choice_switch
        .win?
    end.to_a.select {|x| x}.length
  end

  def which_win?
    if @stay_doors > @switch_doors
      "Stay"
    elsif @stay_doors < @switch_doors
      "Swich"
    else
      "Draw"
    end
  end
end

自分自身によるコードレビュー

細かい点になるけれど、この実装についてのポイントと、本当はこうするべきだろうという一つについて指摘しておく。

「答え」を選んでいた場合は、開くドアは「2つのうち1つ」になるが、外れを選んでいた場合は、「当たり以外」になるので、開くドアが「1つ」に限定される。上のコードでは、それが考慮されておらず、闇雲にランダムに選択することになっている(綺麗にやるならば、この辺りを考慮に入れて実装したほうがいい)。

もう一つとしては、これは検証用のコードなので、文字列を返すメソッドを作り、あとで結果をselectによって出力している。下のような感じ。

puts wins.select {|x| "Stay" == x}.length
puts wins.select {|x| "Swich" == x}.length

現実的には、定数宣言を行ったあとに(つまりSTAY = "Stay")、それを参照するようにしたほうがいい。文字列比較にしろ、マジックナンバーにしろ、タイプミスのときに気がつきやすい(エラーになるため)のは、定数を利用してコードを組むことだろう。

あと、MontiDoorManagerselect部分、つまりselect { |x| x }は流石にエレガンスではない。

ちなみに、上の実装において、stay_doorの場合において、open_doorメソッドを経路していないのは、open_doorの部分において、@choiceの変化に影響を与えないので、今回の検証用コードにおいては、このメソッドを経路していない。実験的にopen_doorを経路したほうが良いと感じる場合には、元来的にはopen_doorを経路したほうがいいように感じる。

あと気になるところとしては、インスタンスを無駄に作っている感じもしなくはない。今回は検証用なので、その辺の効率性を考えなかったけれど、本来ならインスタンスを一つ作ってそれを振りなおすみたいな実装のほうがよかったかもしれないとは思う。

結果

結果としては歴然としていて、1000回 * 100回の試行のうち、ドアの選択をそのままにする場合は、ドアを変更するときに比べて、一度も勝利することは無かった。

また数学の検証通り、ドアの選択をそのままにする場合、330回前後の勝率を記録するが、ドアを変更したときには、660回前後の勝率を記録していた。この計測を見た限り、確かに「間違った直感」通りではないことが判明するし、そもそもの確率論的な結論として「勝率が2倍になる」ということが正しかったことがわかる。

参考までに、「ドアの選択を変えなかった場合」と「ドアの選択を変えた場合」の勝率の出力ログについては下のようになった。途中から抜粋する。

…
…
…
Result No.60 >> Stay :: 323 / Switch :: 676
Result No.61 >> Stay :: 322 / Switch :: 666
Result No.62 >> Stay :: 355 / Switch :: 673
Result No.63 >> Stay :: 340 / Switch :: 656
Result No.64 >> Stay :: 328 / Switch :: 677
Result No.65 >> Stay :: 316 / Switch :: 671
Result No.66 >> Stay :: 330 / Switch :: 673
Result No.67 >> Stay :: 343 / Switch :: 664
Result No.68 >> Stay :: 327 / Switch :: 673
Result No.69 >> Stay :: 327 / Switch :: 677
Result No.70 >> Stay :: 318 / Switch :: 678
…
…
…

まとめ

個人的には、こういった「擬似パラドックス」が好きで、たまに集めたりしているのだけれど、今回のものに関しては、頭の中では「そういうものだ」と知っていても、自分でコードを書いて確かめてみると「なるほど」と納得できて良かった。

プログラマというかエンジニアの格言の一つとして「測定せよ」というのがあるけれども、こういうパラドックスを集めていると、直感的な部分に誤りがあり、ちゃんとデータと照らし合わせてみることの重要性がわかったりする。案外、人間の推測というのは間違うことがあり、そういうのは、確かに創造性にとっては重要な部分であることは認めつつ、こういう問題解決の場合には、直感とデータを照らし合わせるということが重要になると思う。

また、こういう確率的な擬似パラドックスに対して、プログラムを通じて検証ができるのも、プログラムが書ける(この程度ならExcelのマクロでもいいかもしれない)ことの利点なので、色々な擬似パラドックスについてコードを書いていくのも楽しいかもしれない。

というわけで、自分が得意なプログラミング言語で、上記のモンティ・ホール問題について解いてみるのは面白いかもしれないので、是非練習用課題として活用してください。それではまた。