モンティホール問題を遊びつくせ!
本題はここからだ!
記事①:モンティ・ホール問題をPythonで検証する ~モンティホール・アウトレイジ ep 1~
前回は、やりつくされたモンティホール問題を順当に解決していきました。
しかーし、あれがやりたくて、始めたわけではありません。ここからがアウトレジゾーン突入です。
これを今書いている時点で考えていることもありますが、できれば思わぬ方向に自分をつれていってくれたら楽しいなと思っているので、ライブ感をもってやっていきます。
ところで、この問題の解悦によく出てくる、100の扉で考えたら・・ってやつ、納得いきます?
私は、違和感があるんですよね。それはなんか違うんじゃない?って思いません?
このあたりも検証したいと思います。
(2021年3月13日:学習開始22日目、PyQさんで勉強中!)
今回は、怒りの幕開けの咆哮にふさわしい、日本が誇るHeavyMetalバンドOUTRAGEさんの最新作「RUN RIOT」から「Machete III」を聞きながらプログラムを考えていこうと思います。
(曲名から公式YouTubeに飛びます。これこれ、この疾走感よ。)
ではやっていきましょう。
1.前回の(軽めの)振り返り
モンティホールの問題のルールはこんな感じです。
①:プレイヤーの前に3つのドアがある。
②:ドアに後ろには、1つは自動車(あたり)、2つはヤギ(はずれ)が置かれている。
③:プレイヤーは3つのうち1つのドアを選ぶ。
④:司会のモンティ・ホール(人名かよ)が残りのドアのうち、ヤギ(はずれ)の一つを開く。
⑤:プレイヤーは「最初に選んだドア」か「モンティが開けなかったドア」を選びなおせる。
⑥:すべてのドアを開く。最終的に選んだドアに自動車(あたり)があればもらえる。
で、⑤でドアの選びなおしをやった方が、最初のドアから変えない場合より2倍の確率であたりを引き当てるって話です。
前回は、Pythonでとにかく試行回数を増やして検証したら、たしかにその通りになることが確認できました。
ここで終わりです。普通はね。
2.クレイジーなプレイヤー ~無戦略者の勝率~
モンティホールのゲームで話題になるのは、「ドアの選択をかえるか?かえないか?」です。
つまり、これはゲームが始まる前からどちらの選択が得か戦略をたててゲームに臨んでいるということです。
しかし、ここで「何も考えずにやってきたプレイヤー」がいるとします。
この人は、ランダムでドアを変えるか?変えないか?を適当に判断します。この人の勝率はどのようになるでしょうか?
結局、ドア3つから1つを選び、あとは本人の意思とは関係なくモンティがドアを選択するだけなので、1/3 つまり約33%でしょうか?
もしくは、ドアを変えた場合の勝率66%と変えなかった場合の勝率33%の平均で50%になるのでしょうか?
どう思いますか?ちょっと予想してみてください。
ではここから、Pythonを使って力業で解いてみましょう。プログラム自体は、前回つくったものをちょこちょこっといじればすぐにできます。
改変のポイントは2点です。
①:ドア変更するかしないかはランダムで毎回変わる。初期設定しない。
②:「ドアを変えてあたりが出た回数」と「ドアを変えずにあたりが出た回数」をカウントしておく
プログラムはこうなります。
import random
def sim_choice():
while True:
try:
num_sim = int(input('試行回数を入力してください:')) #試行回数入力、文字列を数字に。
except ValueError: #ValueErrorはnum_simに小数か文字列を入れると発生。
print('試行回数には数字を入れてください。')
else: #正常終了時の処理
print(f'試行回数:{num_sim}回')
break
return num_sim #数字で返す
def monty(num):
sim_count = 0 #これまでの試行回数
car_count = 0 #あたりの回数(あたりは自動車)
y_car_count = 0 #ドアを変えてあたりが出た回数
n_car_count = 0 #ドアを変えずにあたりが出た回数
while True:
if sim_count != num:
door = [1, 2, 3] #ドアは3つ。番号を付けた。
car = random.choice(door) #あたりのドアをランダムで
player = random.choice(door) #プレイヤーが選ぶドアををランダムで
door_change = random.choice(['y', 'n']) #プレイヤーがドアを変えるか?
if car == player: #初めにプレイヤーが正解
door.remove(car) #ドアから、自動車=プレイヤーのドアを外す。
monty_choice = random.choice(door) #montyはヤギのドアから選ぶ
if door_change == 'y':
hantei = 'はずれ' #不正解の場合
elif door_change == 'n':
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
else: #初めにプレイヤーがはずれ
door.remove(car) #ドアから、自動車のドアを外す。
door.remove(player) #ドアから、プレイヤーのドアを外す。
monty_choice = random.choice(door) #montyはヤギのドアから選ぶ
if door_change == 'y': #正解の場合、あたりカウント+1
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
elif door_change == 'n':
hantei = 'はずれ' #不正解の場合
if hantei == 'あたり' and door_change == 'y':
y_car_count += 1
elif hantei == 'あたり' and door_change == 'n':
n_car_count += 1
sim_count += 1
else:
car_prob = 100*car_count/sim_count #あたった確率%
print(f'試行回数{sim_count}、あたり回数{car_count}、あたり確率{car_prob:3f}%')
print(f'ドアを変えてあたり{y_car_count}回、ドアを変えずにあたり{n_car_count}回')
break
sim = sim_choice()
monty(sim)
結果はこうなります。試行回数は100000回です。
試行回数を入力してください:100000
試行回数:100000回
試行回数100000、あたり回数50028、あたり確率50.028000%
ドアを変えてあたり33309回、ドアを変えずにあたり16719回
正解は、ほぼ50%です。どうですか?予想はあたりました?
ちなみに、あたりの分類ではドアを変えた方が2倍あたりを引けています。
これってどういう事なんでしょうか。
結局、『モンティがひとつドアを開ける』という行為が、『強制的に選択しを一つ減らす』ということになるので、最終的には2択になり50%になっていると考えられるのではないかと思います。
あたる確率は、
「ドアを変えると決めている人」>「何も考えていない人」>「変えないと決めている人」
となるのも面白いですね。『下手な考え休むに似たり』ということでしょうか。
3.ドアの数を増やす① ~4つのドア~
ここまで、ドアの数は3としてやってきました。
プログラムを書いていて思ったのは、このゲームで「3」という数は少々特殊であるということです
どういうことか?
ドアが2だとどうでしょうか?
ドア2ではゲームが成立しませんよね。
はじめにヤギのドアを選ぶと、必然的に残りは自動車になるため、モンティはドアを開示することが出来なくなります。
ドアが3ではどうでしょうか?
ドア3の場合は、「最初の選択」と「ドアを変えるかどうか」ですべての結果が決まっています。
では、ドアが4以上の場合はどうでしょ?
ただし、モンティが開示するドアは常に1つとします。
ドアが4以上の場合では、「ドアを変える」と決めていた場合でもさらに2つの選択肢が存在するため、さらに「あたり」と「はずれ」の分岐が発生します。
ドアを増やすとややこしくなります。ということで、ドアを増やします。
まずは、ドア4で行きましょう。
基本的なルールは元と同じです。いったん、何も考えないクレイジーなプレイヤーには帰ってもらって、「ドアを変える」「ドアを変えない」を事前に考えてトライする人に戻ってきてもらいます。
プログラムは、前回つくったものを流用します。修正点は3点です。
①:ドアの数を3→4に変更
②:モンティがドアを選択した後に、「ドアを変える人」はさらに残りのドアから選択する。
③:②を達成するため、
(1)ドア一覧を設置(1~4)
(2)「あたりドア」のランダム選択
(3)「プレイヤーのドア」のランダム選択
(4)ドア一覧から「あたりドア」と「プレイヤーのドア」を除く
(5)「モンティのドア」をランダム選択
(6)ドア一覧から「モンティのドア」を除き、「あたりのドア」を戻す
(7)ドア一覧から「プレイヤーのドア」を選択
という流れで再選択を行う。
これらを修正しつつ、printの内容を修正しました。プログラムは以下です。
import random
def sim_choice():
while True:
try:
num_sim = int(input('試行回数を入力してください:')) #試行回数入力、文字列を数字に。
plyer_choice = input('ドアを変更しますか? YES = y or NO = n :') #ドア変更するか?
except ValueError: #ValueErrorはnum_simに小数か文字列を入れると発生。
print('試行回数には数字を入れてください。')
else: #正常終了時の処理
pass
if plyer_choice == 'y': #ドア変更あり
print(f'試行回数:{num_sim}回、ドア変更あり')
break
elif plyer_choice == 'n': #ドア変更なし
print(f'試行回数:{num_sim}回、ドア変更なし')
break
else:
print('ドアを変更はyかnを入れてください')
return [num_sim, plyer_choice] #リストで返す[試行回数, ドア変y or n]
def monty(num, y_or_n):
sim_count = 0 #これまでの試行回数
car_count = 0 #あたりの回数(あたりは自動車)
while True:
if sim_count != num:
door = [1, 2, 3, 4] #ドアは4つ。番号を付けた。
car = random.choice(door) #あたりのドアをランダムで
player = random.choice(door) #プレイヤーが選ぶドアををランダムで
if car == player: #初めにプレイヤーが正解
door.remove(car) #ドアから、自動車=プレイヤーのドアを外す。
monty_choice = random.choice(door) #montyはヤギのドアから選ぶ
door.remove(monty_choice) #montyのドアから外す
if y_or_n == 'y':
player = random.choice(door)
if player == car:
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
else:
hantei = 'はずれ' #不正解の場合
elif y_or_n == 'n':
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
else: #初めにプレイヤーがはずれ
door.remove(car) #ドアから、自動車のドアを外す。
door.remove(player) #ドアから、プレイヤーのドアを外す。
monty_choice = random.choice(door) #montyはヤギのドアから選ぶ
door.remove(monty_choice) #montyのドアから外す
door.append(car) #montyの自動車のドアを追加
if y_or_n == 'y': #正解の場合、あたりカウント+1
player = random.choice(door)
if player == car:
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
else:
hantei = 'はずれ' #不正解の場合
elif y_or_n == 'n':
hantei = 'はずれ' #不正解の場合
sim_count += 1
print(f'試行No{sim_count}、{hantei}、自動車{car}、プレイヤー{player}、モンティ{monty_choice}')
else:
car_prob = 100*car_count/sim_count #あたった確率%
print(f'試行回数{sim_count}、あたり回数{car_count}、あたり確率{car_prob:3f}%')
break
sim = sim_choice()
monty(sim[0],sim[1])
やはり長くなりました。
ドアが3だった時には意味をなさなかった「モンティの選択」が意味を持ちだしています。
ちなみに、ドアを変えない場合は、モンティの選択は影響せず、最初にあたりを選ぶかどうかだけで決定しているのがよく分かります。(当然といえば当然ですが)
では、動かしてもみましょう。まず、10回、ドア変更ありで動かします。
試行回数を入力してください:10
ドアを変更しますか? YES = y or NO = n :y
試行回数:10回、ドア変更あり
試行No1、あたり、自動車4、プレイヤー4、モンティ2
試行No2、はずれ、自動車3、プレイヤー2、モンティ4
試行No3、あたり、自動車1、プレイヤー1、モンティ4
試行No4、はずれ、自動車2、プレイヤー4、モンティ3
試行No5、あたり、自動車3、プレイヤー3、モンティ1
試行No6、はずれ、自動車1、プレイヤー2、モンティ4
試行No7、あたり、自動車4、プレイヤー4、モンティ3
試行No8、あたり、自動車2、プレイヤー2、モンティ1
試行No9、はずれ、自動車4、プレイヤー3、モンティ1
試行No10、あたり、自動車2、プレイヤー2、モンティ3
試行回数10、あたり回数6、あたり確率60.000000%
60%、結構当たっています。では、毎回の出力はやめて試行回数を増やしてみます。
ドア変更する場合
試行回数を入力してください:100000
ドアを変更しますか? YES = y or NO = n :y
試行回数:100000回、ドア変更あり
試行回数100000、あたり回数37442、あたり確率37.442000%
ドア変更しない場合
試行回数を入力してください:100000
ドアを変更しますか? YES = y or NO = n :n
試行回数:100000回、ドア変更なし
試行回数100000、あたり回数25025、あたり確率25.025000%
なるほど。ドアを変更した場合は約37%、ドアを変更しない場合は約25%となります。
やはり、ドアが増えてもドアを変更した場合の方があたりの確率は高くなりました。
37%?なんか中途半端ですね。何度か試しましたが大体これくらいになります。
ここで少し数学的に考えてみましょう。
①:ドアを変更しない場合
ドアを変更しない場合は、最初に当たりを引くかどうかだけで決まります。
ですので、あたりを引く確率は1/4 = 25%となります。
②:ドアを変更する場合
ドアを変更する場合は、最初にあたりを引いてしまうとそこから選択を変えるので必ずはずれます。
ですので、最初の選択で「はずす」必要があるので3/4です。
そこからドアの変えますが、1つのはずれはモンティに開示されるのでドアは3つ。
さらに、最初に選んだドア以外を選ぶので、ドアの選択肢は2つです。
このうち一つが必ずあたりですので、1/2です。
ということで、あたりを引く確率は3/4 x 1/2 =3/8 = 37.5%。
確かにpythonにやらせた結果と一致しています。
4.ドアの数を増やす② ~100個のドア~
さて、冒頭にも書きましたが、今回はコレをやりたくて進めてきました。
モンティホールの問題で非常によくされる説明として、「ドアを100個にして考える」というのがあります。概要はこうです。
・ドア100個と仮定したモンティホールの説明
①:ドアが100個用意されており、1つは自動車、99個はヤギが入っていると仮定します。
②:プレイヤーがドアを1つ選びます。
③:司会のモンティは答えを知っており、残り99のドアのうち自動車以外の98のドアを開示
④:最初に選んだドアから残ったドアに変えることが出来ます。
⑤:当然、ドアを変えますよね?ドアが3つでもこれと同じです。
と、こんな感じで説明されます。
この説明ってどう思いますか?なるほど、確かにってなります?私はなりません!
私のなんか納得いかない点は2つです。
まず、最後のドアが100個でも3つでも同じでしょ?ってとこ。
いやいやいやいや、それは違うでしょうよ。
もう一つ納得がいかないのが、モンティがドアを開けすぎてる点です。
98のドアを開ける?いやいやいやいや、モンティの開けるドアは1つでしょうよ。
(※とはいうものの、私も一応この説明の言いたい事は理解しています。しかし、この考え方は元の問題で確率で計算してみて納得した人が、そのうえで考えている感じがしてキライです!)
では、ドアに100個登場していただき、モンティは1枚だけドアを選択してもらいましょう。
それでも、あたりを引く確率は、ドアを変えた方が上がるのでしょうか?
次のプログラムは簡単で、さきほどのドアの数を100 = list(range(1,101))とすればよいだけです。ほぼ同じですが、プログラムは下記です。
import random
def sim_choice():
while True:
try:
num_sim = int(input('試行回数を入力してください:')) #試行回数入力、文字列を数字に。
plyer_choice = input('ドアを変更しますか? YES = y or NO = n :') #ドア変更するか?
except ValueError: #ValueErrorはnum_simに小数か文字列を入れると発生。
print('試行回数には数字を入れてください。')
else: #正常終了時の処理
pass
if plyer_choice == 'y': #ドア変更あり
print(f'試行回数:{num_sim}回、ドア変更あり')
break
elif plyer_choice == 'n': #ドア変更なし
print(f'試行回数:{num_sim}回、ドア変更なし')
break
else:
print('ドアを変更はyかnを入れてください')
return [num_sim, plyer_choice] #リストで返す[試行回数, ドア変y or n]
def monty(num, y_or_n):
sim_count = 0 #これまでの試行回数
car_count = 0 #あたりの回数(あたりは自動車)
while True:
if sim_count != num:
door = list(range(1,101)) #ドアは4つ。番号を付けた。
car = random.choice(door) #あたりのドアをランダムで
player = random.choice(door) #プレイヤーが選ぶドアををランダムで
if car == player: #初めにプレイヤーが正解
door.remove(car) #ドアから、自動車=プレイヤーのドアを外す。
monty_choice = random.choice(door) #montyはヤギのドアから選ぶ
door.remove(monty_choice) #montyのドアから外す
if y_or_n == 'y':
player = random.choice(door)
if player == car:
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
else:
hantei = 'はずれ' #不正解の場合
elif y_or_n == 'n':
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
else: #初めにプレイヤーがはずれ
door.remove(car) #ドアから、自動車のドアを外す。
door.remove(player) #ドアから、プレイヤーのドアを外す。
monty_choice = random.choice(door) #montyはヤギのドアから選ぶ
door.remove(monty_choice) #montyのドアから外す
door.append(car) #montyの自動車のドアを追加
if y_or_n == 'y': #正解の場合、あたりカウント+1
player = random.choice(door)
if player == car:
car_count += 1 #正解の場合、あたりカウント+1
hantei = 'あたり'
else:
hantei = 'はずれ' #不正解の場合
elif y_or_n == 'n':
hantei = 'はずれ' #不正解の場合
sim_count += 1
else:
car_prob = 100*car_count/sim_count #あたった確率%
print(f'試行回数{sim_count}、あたり回数{car_count}、あたり確率{car_prob:3f}%')
break
sim = sim_choice()
monty(sim[0],sim[1])
では動かしてみましょう。
実は何回か試してみましたが、この違いを補足するには膨大な試行回数が必要です。試行回数1000万回でやっています。ここまでやると答えを出すまで1分くらいかかります。では結果です。
ドアを変える場合
試行回数を入力してください:10000000
ドアを変更しますか? YES = y or NO = n :y
試行回数:10000000回、ドア変更あり
試行回数10000000、あたり回数101365、あたり確率1.013650%
ドアを変えない場合
試行回数を入力してください:10000000
ドアを変更しますか? YES = y or NO = n :n
試行回数:10000000回、ドア変更なし
試行回数10000000、あたり回数99621、あたり確率0.996210%
合計でモンティは2000万回ドアの開け閉めをしています。腕ちぎれるわ!
で、結果は、
ほんのわずかではありますがドアを変えた方があたりが多く発生しています。
その差はわずか0.01%!
小さな差ですが、あたりの回数でみると1000回の差になっています。
試行回数の力は偉大です。
これも数学的に見ていきましょう。
①:ドアを変更しない場合
ドアを変更しない場合は、最初に当たりを引くかどうかだけで決まるので、1/100 = 1%です。
②:ドアを変更する場合
ドアを変更する場合は、最初に外す→変更して98のドアから当てるということになりますので、
99/100 x 1/98 =99/9800 = 1.0102%
数学的にも0.01%の差になります。
これで、私同様、100個のドアで違和感覚える民もぐっすり眠れますね!
今回はこんなところで!次回、さらなるカオスに突入します。
To Be Continued : モンティホール・アウトレイジ ep 3
PyQさんで勉強中!
コメント