JavaScriptで実証するモンティ・ホール問題

JavaScriptで実証するモンティ・ホール問題

December 24, 2021

はい!みなさんお待ちかね、プログラミングで遊んでみようのコーナーです!

今回は記念すべき第一回!そして第二回目の予定は無いのでなんと今回が最終回です!

プログラミングを使って、モンティ・ホール問題に取り組んでみましょう!

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

元ネタはアメリカの長寿番組『Let’s Make a Deal』中に登場したゲームで、それを元にスティーブ・セルビンがルールを明確に定めた確率の問題を作成した。

その番組司会はモンティ・ホール。問題の名称は彼に由来する。

  1. プレイヤーの前には A,B,C の 3 つのドアがあり、その奥には当たりが 1 つ、ハズレが 2 つ用意されている。
  2. プレイヤーがドアを 1 つ選択する(この時点では開かない)。
  3. モンティは正解のドアを把握しており、残された 2 つのうちから必ずハズレの方のドアを開ける(残された 2 つが両方ともハズレの場合はどちらかをランダムに開ける)。これはプレイヤーの回答に関わらず必ず行われ、これらのルールはプレイヤーも認識している。
  4. モンティは「今なら選択を変更して構いませんよ?」とプレイヤーに問いかける。

さて、このときプレイヤーは最初の選択を変更するべきか、否か?

モンティ・ホール問題とは - ニコニコ大百科

詳細を知りたい方は上記引用元をご参照くださいませ。

上のリンク先にも記載はありますが 正解は「プレイヤーは最初の選択を変更するべき」 です。

しかしながらその正解にすぐには納得できないという人も少なく無いはず。

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

(中略)

確率論から導かれる結果を説明されても、なお納得しない者が少なくないことから、ジレンマあるいはパラドックスとも称される。「直感で正しいと思える解答と、論理的に正しい解答が異なる問題」の適例とされる。

モンティ・ホール問題 - Wikipedia

さて、論より証拠ということで、プログラミングを使ってモンティ・ホール問題の正解が本当に正しいのか実証してみましょう!

JavaScript で実証するモンティ・ホール問題 #

前置き #

(プログラミング経験者の方へ)

以降に記載するコードは、実際のプレイヤーとモンティの動きを再現するように書いていたり、プログラミングを始めたばかりの方にも分かりやすいように意図的に冗長に書いていたりします。

ロジック的にもコードのスタイリング的もよりクリーンなコードを書きたいという方は適宜ご自由に改変してくださいませ。

コーディング編 #

それではさっそく一緒にコーディングしていきましょう!

1. ドアを用意する。 #

まずは3つのドアを用意しましょう。

// 1. ドアを用意する。
const doorsLength = 3;
const doors = Array(doorsLength).fill(null);

doors は3つのドアを表す配列になっています。

// doors -> [null, null, null]

2. アタリとハズレをドアに配置する。 #

現在ドアの後ろには何も配置されていませんので、各ドアの後ろにアタリとハズレを配置していきましょう。

// 1. ドアを用意する。
const doorsLength = 3;
const doors = Array(doorsLength).fill(null);

// 2. アタリとハズレをドアに配置する。
const winNumber = Math.floor(Math.random() * doorsLength);
doors[winNumber] = 'o';
doors.forEach((el, i) => {
  if (el !== 'o') {
    doors[i] = 'x';
  }
});

o がアタリをx がハズレを意味しています。o がランダムで配列のどこかに配置され、残りは x になります。

ここまでで doors は以下のようになります。

// doors -> ['x', 'x', 'o']

3. プレイヤーがドアを選択する。 #

プレイヤーがドアを選択します。ランダムで1つドアを選びます。

// 1. ドアを用意する。
const doorsLength = 3;
const doors = Array(doorsLength).fill(null);

// 2. アタリとハズレをドアに配置する。
const winNumber = Math.floor(Math.random() * doorsLength);
doors[winNumber] = 'o';
doors.forEach((el, i) => {
  if (el !== 'o') {
    doors[i] = 'x';
  }
});

// 3. プレイヤーがドアを選択する。
const yourChoice = Math.floor(Math.random() * doorsLength);

4. 残されたドアからモンティがハズレのドアを開ける。 #

モンティがハズレのドアを1つ開けてくれます。もしプレイヤーがハズレのドアを選択していれば、モンティはもう1つの(プレイヤーが選択していない)ハズレのドアを開けてくれます。もしプレイヤーがアタリのドアを選択していれば、モンティは2つのハズレのドアからランダムで1つを開けてくれます。

// 1. ドアを用意する。
const doorsLength = 3;
const doors = Array(doorsLength).fill(null);

// 2. アタリとハズレをドアに配置する。
const winNumber = Math.floor(Math.random() * doorsLength);
doors[winNumber] = 'o';
doors.forEach((el, i) => {
  if (el !== 'o') {
    doors[i] = 'x';
  }
});

// 3. プレイヤーがドアを選択する。
const playersChoice = Math.floor(Math.random() * doorsLength);

// 4. 残されたドアからモンティがハズレのドアを開ける。
let montysChoice = doors.indexOf('o');
while (montysChoice === playersChoice) {
  montysChoice = Math.floor(Math.random() * doorsLength);
}

doors.forEach((_, i) => {
  if (i !== playersChoice && i !== montysChoice) {
    doors[i] = null;
  }
});

これが終わると doors は以下のようになります。null はすでにドアが開けられているという意味です。

// doors -> ['x', null, 'o']

5. プレイヤーが選択を変えるまたは変えない。 #

プレイヤーは選択を変えることもできますし、変えなくても構いません。変えた場合と変えない場合、どちらも表現します。

// 1. ドアを用意する。
const doorsLength = 3;
const doors = Array(doorsLength).fill(null);

// 2. アタリとハズレをドアに配置する。
const winNumber = Math.floor(Math.random() * doorsLength);
doors[winNumber] = 'o';
doors.forEach((el, i) => {
  if (el !== 'o') {
    doors[i] = 'x';
  }
});

// 3. プレイヤーがドアを選択する。
const playersChoice = Math.floor(Math.random() * doorsLength);

// 4. 残されたドアからモンティがハズレのドアを開ける。
let montysChoice = doors.indexOf('o');
while (montysChoice === playersChoice) {
  montysChoice = Math.floor(Math.random() * doorsLength);
}

doors.forEach((_, i) => {
  if (i !== playersChoice && i !== montysChoice) {
    doors[i] = null;
  }
});

// 5. プレイヤーが選択を変えるまたは変えない。
const unchangedPlayersChoice = playersChoice;
const changedPlayersChoice = montysChoice;

unchangedPlayersChoice がプレイヤーがもともと選択していたドアを、changedPlayersChoice がプレイヤーが選択を変えたドア(=モンティが残してくれたドア)を意味しています。

6. 複数回試行して勝率を求める。 #

さて、ここまでで準備は完了です。ここまでで作成したコードを利用して、プレイヤーが選択を変えた場合と変えなかった場合でどちらの勝率が高いのかを求めます。

以下のコードを追加します。

  1. アタリのドアを選ぶことのできた回数のカウンターを用意します。
  2. アタリのドアを選ぶことのできた回数をカウントします。
  3. 繰り返し試行します。
  4. 最終結果を出力します。
// 6-1. 複数回試行して勝率を求める。 - アタリのドアを選ぶことのできた回数のカウンターを用意します。
let unchangedPlayersChoicesWinCount = 0;
let changedPlayersChoiceWinCount = 0;

// 6-3. 複数回試行して勝率を求める。 - 繰り返し試行します。
const trialCount = 10000;

for (let i = 0; i < trialCount; i++) {
  // 1. ドアを用意する。
  const doorsLength = 3;
  const doors = Array(doorsLength).fill(null);

  // 2. アタリとハズレをドアに配置する。
  const winNumber = Math.floor(Math.random() * doorsLength);
  doors[winNumber] = 'o';
  doors.forEach((el, i) => {
    if (el !== 'o') {
      doors[i] = 'x';
    }
  });

  // 3. プレイヤーがドアを選択する。
  const playersChoice = Math.floor(Math.random() * doorsLength);

  // 4. 残されたドアからモンティがハズレのドアを開ける。
  let montysChoice = doors.indexOf('o');
  while (montysChoice === playersChoice) {
    montysChoice = Math.floor(Math.random() * doorsLength);
  }

  doors.forEach((_, i) => {
    if (i !== playersChoice && i !== montysChoice) {
      doors[i] = null;
    }
  });

  // 5. プレイヤーが選択を変えるまたは変えない。
  const unchangedPlayersChoice = playersChoice;
  const changedPlayersChoice = montysChoice;

  // 6-2. 複数回試行して勝率を求める。 - アタリのドアを選ぶことのできた回数をカウントします。
  if (doors[unchangedPlayersChoice] === 'o') {
    unchangedPlayersChoicesWinCount++;
  }

  if (doors[changedPlayersChoice] === 'o') {
    changedPlayersChoiceWinCount++;
  }
}

// 6-4. 複数回試行して勝率を求める。 - 最終結果を出力します。
console.log(`選択を変えない場合にアタリを選んだ回数:${unchangedPlayersChoicesWinCount}回`);
const unchangedPlayersChoicesWinRate = (unchangedPlayersChoicesWinCount / trialCount) * 100;
console.log(`選択を変えない場合にアタリを選んだ確率:${unchangedPlayersChoicesWinRate}%`);

console.log(`選択を変えた場合にアタリを選んだ回数:${changedPlayersChoiceWinCount}回`);
const changedPlayersChoicesWinRate = (changedPlayersChoiceWinCount / trialCount) * 100;
console.log(`選択を変えた場合にアタリを選んだ確率:${changedPlayersChoicesWinRate}%`);

trialCount = 10000; としています。今回は 10,000 回繰り返してみることとします。

結果編 #

早速実行してみます。出力された結果は以下となりました。

選択を変えない場合にアタリを選んだ回数:3354回
選択を変えない場合にアタリを選んだ確率:33.54%
選択を変えた場合にアタリを選んだ回数:6646回
選択を変えた場合にアタリを選んだ確率:66.46%

選択を変えた方がアタリの確率が高くなりました。つまり 「プレイヤーは最初の選択を変更するべき」 ということは正しいことが数字でも示されました。

選択を変えない場合にアタリを引く確率は約3分の1(33.3%)、一方で選択を変えた場合にアタリを引く確率は3分の2(66.6%)となるのですが、計算結果上も実際にそのようになっていることがわかります。

ドアの数を増やしてみる。 #

今回はドアの数を3つとして試しましたが、ドアの数をさらに多くすると結果はより明白になります。

先のコードの中の const doorsLength = 3;const doorsLength = 10000; として、ドアが 10,000 個だった場合の実験をしてみましょう。

そうすると結果は以下の通りとなりました。

選択を変えない場合にアタリを選んだ回数:1回
選択を変えない場合にアタリを選んだ確率:0.01%
選択を変えた場合にアタリを選んだ回数:9999回
選択を変えた場合にアタリを選んだ確率:99.99%

上記を見れば、選択を変えた方が良さそうだということは明白ですね。(選択を変えない場合でもちゃんと1回アタリを引いているのがすごい!)

おわり #

以上、プログラミングで遊んでみようのコーナーでした!

みなさん素敵なプログラミングライフをお過ごしください!

 制作・著作
 --------
  当サイト