🐙

モンティ・ホール問題をPythonで確かめてみた

に公開

みなさん、モンティ・ホール問題をご存知でしょうか?今回はPythonでモンティ・ホール問題が本当にその通りなのか計算してみました。

モンティ・ホール問題とは?

これは確率の勉強をする時によく出てくる直感に反する結果となるものの例として扱われます。概要についてはWikipediaを参考にすると以下のようにまとめられます。

モンティ・ホール問題(モンティ・ホールもんだい、英: Monty Hall problem)とは、確率論の問題で、ベイズの定理における事後確率、あるいは主観確率の例題の一つとなっている。モンティ・ホール(英語版)(Monty Hall, 本名:Monte Halperin)が司会者を務めるアメリカのゲームショー番組、「Let's make a deal(英語版)[注釈 1]」の中で行われたゲームに関する論争に由来する。一種の心理トリックになっており、確率論から導かれる結果を説明されても、なお納得しない者が少なくないことから、モンティ・ホール・ジレンマ、モンティ・ホール・パラドックスとも称される。「直感で正しいと思える解答と、論理的に正しい解答が異なる問題」の適例とされる。

https://ja.wikipedia.org/wiki/モンティ・ホール問題

モンティ・ホール問題は以下の流れで進められます。

  • 前提条件
    • ドアが三つあり、一つのドアのうしろにプレゼントがあり、残り二つのドアの後ろにはプレゼントはありません
  • 流れ
    1. 回答者はどれか一つのドアを選びます
    2. 司会者は残りのドア二つのうち、ハズレのドアを開けます
    3. 回答者は最初選んだドアを開けるか、残っているもう一つのドアに変更するか選べます
    4. 3で選んだドアを開けます

最後にドアを変えるべきか?

直感的には残る扉は2つしかないのでどちらを選んでも50%の確率で当たるので、変える必要はないように思えますが、条件付き確率を考えると変更した方がプレゼントがもらえる確率が上がります。

まず、一番最初にドアを選ぶ時、選んだドアがプレゼントがもらえるものかどうかは1/3となります。これは直感のままで問題ありません。

では次に司会者が外れドアを開けた状態で残りのドアを変えた時と変えなかった時で確率の変化を考えてみましょう。最初に述べた通り、答えとしては50%になるから変える意味がないというのは間違いになります。これは極めて簡単なロジックで説明することができます。

便宜上、ドアを順番にA、B、Cと名付けます。最初にドアAが選ばれるとして、具体的にどのような流れになるか考えてみましょう。

  • ドアAがプレゼントの場合
    • ドアB、ドアCどちらが開かれても残っている方はハズレなので、ドアを変えると外れてしまう
  • ドアBがプレゼントの場合
    • ドアCがハズレなのでドアCが開かれる。この場合、ドアをBに変更すればプレゼントが当たる
  • ドアCがプレゼントの場合
    • ドアBがハズレなのでドアBが開かれる。この場合、ドアをCに変更すればプレゼントが当たる

この結果から、ドアAを最初に選んだ後にドアを最後に変更したら当たる確率は2/3になります(これは他のドアB、ドアCを選んでも同じロジックが成り立ちます)。そのため、回答者はドアを変更するとプレゼントがもらえる確率がおよそ33%増えることになります。

Pythonで実装してみた

ここまで、モンティ・ホール問題の直感的でない結果について説明しましたが、実際にプログラムで動作させたらどうなるかみてみましょう。
コードの全体かんはこのようになります。

from tqdm import tqdm
import matplotlib.pyplot as plt
from random import randint
from enum import Enum


class PrizeType(Enum):
    Car = 1
    Goat = 2


class MontyHallProblem:
    def __init__(self):
        self.door_prize = {}

    def reset(self):
        self.selected_idx = None
        self.opened_idx = None
        self.car_idx = randint(0, 2)
        self.door_prize = {k: PrizeType.Goat for k in range(3)}
        self.door_prize[self.car_idx] = PrizeType.Car

    def select_first(self, idx):
        self.selected_idx = idx

    def open_goat_door(self):
        for k, v in self.door_prize.items():
            if v == PrizeType.Car:
                continue

            if not k == self.selected_idx:
                self.opened_idx = k
                break

    def change_candidate(self):
        self.selected_idx = [i for i in range(3) if not i in (self.selected_idx, self.opened_idx)][0]

    def is_correct(self) -> bool:
        return self.selected_idx == self.car_idx


problem = MontyHallProblem()
result = {"changed": [], "not_changed": []}
iterations = [10**i for i in range(1, 6)]

for iteration in tqdm(iterations):
    result["changed"].append(0)
    result["not_changed"].append(0)
    
    for i in tqdm(range(iteration), leave=False):
        problem.reset()
        problem.select_first(0)
        problem.open_goat_door()
        result["not_changed"][-1] += int(problem.is_correct()) / iteration
    
    
    for i in tqdm(range(iteration), leave=False):
        problem.reset()
        problem.select_first(0)
        problem.open_goat_door()
        problem.change_candidate()
        result["changed"][-1] += int(problem.is_correct()) / iteration
    

plt.plot(iterations, result["changed"], color="b", label="changed")
plt.plot(iterations, result["not_changed"], color="r", label="not_changed")
plt.legend()
plt.grid()
plt.xlabel("iterations")
plt.ylabel("accuracy")
plt.xscale("log")
plt.savefig("result.png")

まず、以下はドアの後ろにあるものの種別を登録しています。元ネタとなったテレビ番組では商品が車、ハズレがヤギ(Goat)だったということでそれに倣っています。

class PrizeType(Enum):
    Car = 1
    Goat = 2

次に、問題を定義するクラスを以下のように実装しています。

class MontyHallProblem:
    def __init__(self):
        self.door_prize = {}

    def reset(self):
        self.selected_idx = None
        self.opened_idx = None
        self.car_idx = randint(0, 2)
        self.door_prize = {k: PrizeType.Goat for k in range(3)}
        self.door_prize[self.car_idx] = PrizeType.Car

    def select_first(self, idx):
        self.selected_idx = idx

    def open_goat_door(self):
        for k, v in self.door_prize.items():
            if v == PrizeType.Car:
                continue

            if not k == self.selected_idx:
                self.opened_idx = k
                break

    def change_candidate(self):
        self.selected_idx = [i for i in range(3) if not i in (self.selected_idx, self.opened_idx)][0]

    def is_correct(self) -> bool:
        return self.selected_idx == self.car_idx

reset関数は何回も問題を検証するためにリセットするための関数となります。select_first関数は一番最初に回答者が選んだドアのインデックスを登録します。open_goat_door関数は残った二つのドアのうちハズレのドアを開ける関数になります。change_candidate関数は、回答者がドアを開けると選んだ場合にドアのインデックスを変更するための関数になります。is_correct関数はドアがプレゼントのドアかを取得する関数となります。

MontyHallProblemクラスを利用すれば、あとは多くの試行回数を経て勝率が2/3に収束するかを確認します。

problem = MontyHallProblem()
result = {"changed": [], "not_changed": []}
iterations = [10**i for i in range(1, 6)]

for iteration in tqdm(iterations):
    result["changed"].append(0)
    result["not_changed"].append(0)
    
    for i in tqdm(range(iteration), leave=False):
        problem.reset()
        problem.select_first(0)
        problem.open_goat_door()
        result["not_changed"][-1] += int(problem.is_correct()) / iteration
    
    
    for i in tqdm(range(iteration), leave=False):
        problem.reset()
        problem.select_first(0)
        problem.open_goat_door()
        problem.change_candidate()
        result["changed"][-1] += int(problem.is_correct()) / iteration
    

plt.plot(iterations, result["changed"], color="b", label="changed")
plt.plot(iterations, result["not_changed"], color="r", label="not_changed")
plt.legend()
plt.grid()
plt.xlabel("iterations")
plt.ylabel("accuracy")
plt.xscale("log")
plt.savefig("result.png")

今回は複数種類のイテレーション実行を用意し、そのイテレーションの中でドアを変えるときと変えないときでどのように勝率が変わるかを確認してみました。その結果、以下のようなグラフを得ることができました。

この結果から、試行回数を増やすとドアを変更したら勝率がおよそ66%に収束することが確認できました。このことからも、モンティ・ホール問題の勝率は直感に反するものになりうると確認できたと思います。

まとめ

今回は、確率の勉強をする上で有名なモンティ・ホール問題について、本当にそんな結果になるのか?ということについて確かめてみました。このように直感的な考えと反するような結果になる問題については全てのパターンや一部のパターンを書き出してみて確認すると腑に落ちますが、今回はPythonを使って挙動を確認してみました。

Discussion