モンティ・ホール問題のプログラムを作り、実体験してみます。
以下の環境で動作確認をしています。
環境: Windows パソコン、Microsoft Edge ブラウザ
背景 ~ “条件つき確率”を体験してみる
興味本位で、機械学習や統計学あたりの本を斜め読みすると、よく、モンティ・ホール問題が出てきます。
少しすると忘れていて、別の本を読むと、また出てきます。
条件つき確率の説明のあたりで出てくるのですが、仕事や実体験として経験しないと、言葉だけの説明ではなかなか身につきません。
ということで、JavaScript を使って、モンティ・ホール問題のプログラムを作ってみることにします。
よくわかっていないとプログラムを作ることができません。プログラムを作ることで理解が深まります。
また、作ったプログラムで遊んでみると、条件つき確率の意味するところが実体験として腹に落ちるといった側面もあります。
プログラムを作らないまでも、多少、興味がある、本で読んだことがあるといった方は、以下にサンプルを貼っておきますので遊んでみてください。確かにそうだなといった実感が得られるかと思います。
関係する計算式なども書き出して整理しておくことにします。
設定手順
① 下記を参考にパソコン内にフォルダとテキストファイルを作り、下記のサンプルスクリプトをコピー&ペーストで貼りつけて保存します。
例: c:\user\bayes1/monty_hall_problem1.html
② ①をダブルクリックしてブラウザで起動します。
→ ブラウザ画面でスクリプトが起動したら、設定まで完了です。
使い方
動くサンプル
動くサンプルを以下に貼っておきます。
すでにモンティ・ホール問題を知っている人は、遊んでみてください。
書籍などでよく紹介されているままのプログラムにしてあります。
使い方の詳細
③ ステップ1: プログラムを起動し、どれか1つのボタンを選択(クリック)します。数値のボタンのうち、どれか1つが当たりとなっています。1つを選択すると、当たりのボタンの位置は維持したまま、ハズレのボタンがグレーアウトします。残りのボタンのいずれかには、必ず「当たり」があります。
④ ステップ2: 残りのボタンの中で、1つを選択(クリック)します。ステップ1で選んだ番号を変更するチャンスがあります。選択を変えるのか、変えないのか、腹決めします。
⑤ ステップ3: 当たりかハズレかが表示されます。当たりの番号が赤色で表示されます。また、得点(”score: xx% “)が表示されます。
ゲームを繰り返す場合は、[next] ボタン(または、数値のボタン)をクリックします。すると、ステップ1(③)に戻り、ゲームを繰り返すことができます。
何回かゲームを繰り返し、当たった確率(score)がどの程度の数値に収束していくのか確認してみてください。
※ ゲームをやりなおす場合は、[clear] ボタンをクリックします。すると、パラメータを初期化して、初期状態(③)に戻ります。
※ 上記の④で、最初の選択にこだわって変更をしない選択をし続けた場合のスコアと、選択をつねに変え続けた場合のスコアを比較してみてください。
※ ステップ2で、ハズレに関する情報を知ったとき、選択を変えると勝率が変わる(上がる)のかどうかが議論になります。
選択肢が3つのとき、当たる確率は 1/3 です。したがって、選択を変えても変えなくても、当たる確率は 1/3 ではないかと考える人がいます(間違い)。また、ステップ2で、選択肢が2択になったのだから、当たる確率は 1/2 になったと考える人がいます(間違い)。
何も知らないで選んだ場合の確率と、ハズレがどれかなどといった情報を得た後での確率(事後確率)が変わるのか否かが議論となります。
実際に試して確認してみてください。
※ 画面上段の数値の欄を変えることで、ボタンの数を変更できます。
10個程度にボタンを増やして、変更し続けると勝率がどのようになるのか確認してみてください。
※ また、画面下の [show] ボタンをクリックすることで、各ステップでのすべての履歴を表示できます。
各行で、以下のフォーマットとしています。
当たりの番号 + ” ” + ユーザが最初に選択した番号 + ” ” + ユーザが2回目に選択した番号
当然ながら、ゲームをするとき、当たり番号の情報を得ながら進めると、正答率を 100% にすることも、意図的に 0% にすることも可能です。情報の有無により、確率(結果)が変わりうることになります。
計算式のまとめ
条件つき確率の式を挙げておきます。
$$ P(A|B) = \frac{P(A\, \cap \, B)}{P(B)} … (*01)$$
ここで、各パラメータの定義は以下のとおりです。
A: A という事象
B: B という事象
P(A): A という事象が起こる確率
P(B): B という事象が起こる確率
\(P(A \cap B)\): A および B の事象が起こる確率
上の式を形式的に変形します。
$$ P(A\, \cap \, B) = P(A|B)・P(B) = P(B|A)・P(A)$$
すると、以下のベイズの式が求められます。
改めて、各パラメータの定義を書き出しておくと以下のとおりです。
P(A): A という事象が起こる確率(事前確率)
P(B): B という事象が起こる確率
P(A|B): B という事象が起きているとき、A が起こる確率(事後確率)
P(B|A): A という事象が起きているとき、B が起こる確率(尤度)
モンティ・ホール問題の事例で選択肢が3つの場合は、上記の式を使って計算することが一般的なようです。
地道に解いてもよいかもしれませんが、ボタン(選択肢)の数を n 個としたときのように、汎用化した場合は、以下のように余事象の概念を使ったほうが計算が楽です。
1回目(ステップ1)で当たりを選ぶ確率は 1/n です。したがって、ステップ1以降で判断を何も変更しない場合、当たる確率は 1/n となります。 … (*03)
すると、1回目でハズレとなる確率は (n-1)/n です。このとき、ステップ2で選択をかならず変えるものとします。すると、1回目でハズレを選択していた場合、2回目ではかならず当たる(確率100%で当たる)ことになります。
ステップ1でハズレとなり、なおかつ、ステップ2で選択をかならず変えるものとすると、トータルで当たりを選択する確率は、 (n-1)/n × 1.0 となります。
当たる確率は、(n-1)/n となります。 … (*04)
選択を変えないときの当たる確率は 1/n、選択を変えるときの当たる確率は (n-1)/n となります。
たとえば、n = 3 の場合、選択を変えない場合に当たる確率は約33% (*03) ですが、選択を変えると当たる確率は約66% (*04)となることになります。
何も情報が得られないときの確率(事前確率)と、当たりハズレに関する何らかの情報(条件)を得た後での確率(事後確率)は、一般には異なりうるということになります。
上記のプログラムで、ボタンの数を10個などとして実際に動かしてみると、実感できると思います。
また、プログラムを作ってみると/プログラム内部の処理をたどってみると、上記の確率の計算が自然であることがわかるかと思います。
スクリプトの説明
・ clear1() 関数は、初期化を行う関数です。アプリの起動時と [clear] ボタンをクリックした際に実行するようにしてあります。
・ shuffle1() 関数は、当たり、ハズレのボタン番号を設定する関数です。具体的には、まず、ボタン番号を並べて配列 a1 に入れます。つぎに、配列の内部を乱数で入れ替えていくことで、ボタン番号のランダムな並びをつくります。
a1[0] は、当たりとなるボタン番号です。a1[1] は、ユーザーがステップ1で当たりを選択していたとき、選択肢として残すボタン番号です。
ユーザーがステップ1でハズレを選択していた場合は、ユーザーが選択したボタン番号と、当たりとなるボタン番号を選択肢として残すようにします。
上記のステップ1とステップ2で、当たりとなるボタン番号と、選択肢として残すボタン番号の2つを、重複のないよう設定する必要が生じます。そこで、配列にボタン番号を格納してランダムに並べ替えることで、重複が起こらないようにしています。
・ nex1() 関数が、モンティ・ホール問題の全体のステップを決めている関数です。step1 が、表示されるステップ1~3に対応した変数となっています。
ステップ1の段階では、上記の shuffle1() で乱数から当たり番号等を決めます。ユーザーがボタンを選択すると、ステップ2に進みます。
ステップ2では、ユーザーが選択したボタン番号と当たりのボタン番号を残し、他のボタン番号を選択できないようにしています。
もし、ステップ1で、ユーザーが選択したボタン番号と当たりのボタン番号が一致していた場合は、追加で、あらかじめ配列で定めてあったボタン番号を選択できるようにします。あるいは、ユーザーの選択がハズレであった場合は、当たりのボタン番号を選択できるようにします。
モンティ・ホール問題で、場合分けが生じるのはこの1か所であり、プログラム内の if 文1か所が対応しています。前述の確率の計算の2つのパターン ((*03)、(*04)) がそれぞれ対応しています。
ステップ3では、ユーザーが最終的に選択したボタン番号が当たりとなっているか否かを判定し、結果を表示します。
ゲームの繰り返し回数と、当たった数から、スコア score1 を計算し、表示しています。
・ <body> タグ以降で、HTML の画面の設定をしています。
まとめ
モンティ・ホール問題のプログラムを作ってみました。
本を読むだけではなく実際にプログラムを作って動かしてみると、条件つき確率の意味するところを実体験することができます。
ランダムに変動する株価があったとして、企業業績や増配・減配の発表があると、妥当な株価水準となるよう期待値が上下するのに似た感じです。
得られるものは単なる情報なのですが、実体のない単なる情報が、確率や期待値、実体経済などに影響してくるといったところも面白いところです。
他にもプログラムなどを作って公開しています。興味のある方は参照してみてください。
関連リンク
・ 投票行動を正規分布で把握する 【JavaScript】
・ 機械学習で株価予測 【Python】
・ 乾燥時間の機械学習を行ってみる 【Python & Tensorflow】
・ 任意の単語間の相関係数を求めてみる 【Python & Google Trends】
サンプルスクリプト
モンティ・ホール問題のプログラム
monty_hall_problem1.html
<!DOCTYPE html>
<html lang="ja">
<title>monty hall problem v0.1</title>
<head>
<meta charset="UTF-8"/>
<script>
window.onload = load1;
function load1() {
clear1();
}
let step1 = 0;
let n1_max = 3; // number of buttons
let n1_win = 0;
let n1_score = 0.0;
let n1_count0 = 1;
let n1_count1 = 0; // total counts
let n1_count2 = 0; // correct counts
let n1_selected0 = 1; // selected button
let n1_selected1 = 0; // first button
let n1_selected2 = 0; // second button
let a1 = [];
function select1() {
let obj1 = document.getElementById("select1");
let str1 = obj1.options[obj1.selectedIndex].value;
let str2 = obj1.options[obj1.selectedIndex].text;
n1_max = parseInt(obj1.options[obj1.selectedIndex].value);
clear1();
}
function clear1() {
step1 = 0;
n1_win = 0;
n1_score = 0;
n1_count0 = 1;
n1_count1 = 0;
n1_count2 = 0;
n1_selected0 = 1;
n1_selected1 = 0;
n1_selected2 = 0;
for (let i1=0; i1<10; i1++) {
let str1 = "button" + String(i1+1).padStart(2, "0");
document.getElementById(str1).disabled = true;
document.getElementById(str1).style.visibility = "hidden";
}
document.getElementById("textarea1").value = "";
document.getElementById("div1").innerHTML = " <br> ";
document.getElementById("div2").innerHTML = "";
next1();
}
function shuffle1() {
let n0 = n1_max;
a1 = [];
for (let i1 = 0; i1 < n0; i1++) {
a1.push(i1+1);
}
let n1 = a1.length-1; // Fisher-Yates shuffle
for (let i1 = 0; i1 < a1.length-1; i1++) {
let n2 = Math.floor(Math.random() * (n1+1));
let n3 = a1[n1];
a1[n1] = a1[n2];
a1[n2] = n3;
n1 = n1 - 1;
}
return a1;
}
function next1() {
step1 = step1 + 1;
let str1 = "";
if (step1 == 1) {
n1_count1 = n1_count1 + 1;
a1 = shuffle1();
console.log(a1);
n1_win = a1[0];
for (let i1=0; i1<n1_max; i1++) {
str1 = "button" + String(i1+1).padStart(2, "0");
document.getElementById(str1).disabled = false;
document.getElementById(str1).style.visibility = "visible";
document.getElementById(str1).style.color = "#000000";
}
add_string1(a1[0], 0);
str1 = "[" + String(n1_count1).padStart(3, "0") + "] ステップ1:<br>どれか1つを選択してください。<br> ";
document.getElementById("div1").innerHTML = str1;
} else if (step1 == 2) {
n1_selected1 = n1_selected0;
add_string1(n1_selected1, 0);
for (let i1=0; i1<n1_max; i1++) {
str1 = "button" + String(i1+1).padStart(2, "0");
document.getElementById(str1).disabled = true;
document.getElementById(str1).style.color = "#cccccc";
}
str1 = "button" + String(a1[0]).padStart(2, "0");
document.getElementById(str1).disabled = false;
document.getElementById(str1).style.color = "#000000";
if (n1_selected1 == n1_win && a1.length > 1) {
str1 = "button" + String(a1[1]).padStart(2, "0");
document.getElementById(str1).disabled = false;
document.getElementById(str1).style.color = "#000000";
} else {
str1 = "button" + String(n1_selected1).padStart(2, "0");
document.getElementById(str1).disabled = false;
document.getElementById(str1).style.color = "#000000";
}
str1 = "[" + String(n1_count1).padStart(3, "0") + "] ステップ2:<br>ハズレを減らしました。変更することができます。<br>好きな1つを選択してください。";
document.getElementById("div1").innerHTML = str1;
} else if (step1 == 3) { // check
n1_selected2 = n1_selected0;
add_string1(n1_selected2, 1);
str1 = "button" + String(a1[0]).padStart(2, "0");
document.getElementById(str1).style.color = "#ff0000";
if (n1_selected2 == a1[0]) { // win
n1_count2 = n1_count2 + 1;
str1 = "[" + String(n1_count1).padStart(3, "0") + "] ステップ3:<br>当たりです。<br>続ける場合は [next] をクリックしてください。";
document.getElementById("div1").innerHTML = str1;
} else { // lose
str1 = "[" + String(n1_count1).padStart(3, "0") + "] ステップ3:<br>ハズレです。<br>続ける場合は [next] をクリックしてください。";
document.getElementById("div1").innerHTML = str1;
}
n1_score = parseFloat(n1_count2)/parseFloat(n1_count1) * 100;
str1 = "score: " + n1_score.toFixed(2) + "% = " + n1_count2 + "/" + n1_count1 ;
document.getElementById("div2").innerHTML = str1;
step1 = 0;
}
}
function add_string1(val1, ret1) {
let str2 = document.getElementById("textarea1").value + String(val1).padStart(2, "0") + " ";
if (ret1 > 0) {
str2 = str2 + "\n";
}
document.getElementById("textarea1").value = str2;
}
function button1(n0) {
n1_selected0 = n0;
next1();
}
function show1() {
let str1 = document.getElementById("textarea1").style.width;
if (str1 == "10px") {
document.getElementById("textarea1").style.width = "100px";
document.getElementById("textarea1").style.height = "200px";
} else {
document.getElementById("textarea1").style.width = "10px";
document.getElementById("textarea1").style.height = "10px";
}
}
</script>
<style>
</style>
</head>
<body style="background-color:#eeeeee; font-family: helvetica, arial, sans-serif, 'Meiryo UI', Meiryo; ">
<button id="clear1" onclick="clear1()">clear</button>
<button id="next1" onclick="next1()">next</button>
<select id="select1" style="width:60px;" onchange="select1()">
<option value="01">1</option>
<option value="02">2</option>
<option value="03" selected>3</option>
<option value="04">4</option>
<option value="05">5</option>
<option value="06">6</option>
<option value="07">7</option>
<option value="08">8</option>
<option value="09">9</option>
<option value="10">10</option>
</select> <div id="div2" style="display:inline-block;"></div>
<div id="div1"><br></div><br>
<button id="button01" style="font-size:20px; width:80px; height:80px;" onclick="button1( 1)">01</button>
<button id="button02" style="font-size:20px; width:80px; height:80px;" onclick="button1( 2)">02</button>
<button id="button03" style="font-size:20px; width:80px; height:80px;" onclick="button1( 3)">03</button>
<button id="button04" style="font-size:20px; width:80px; height:80px;" onclick="button1( 4)">04</button>
<button id="button05" style="font-size:20px; width:80px; height:80px;" onclick="button1( 5)">05</button> <br><br>
<button id="button06" style="font-size:20px; width:80px; height:80px;" onclick="button1( 6)">06</button>
<button id="button07" style="font-size:20px; width:80px; height:80px;" onclick="button1( 7)">07</button>
<button id="button08" style="font-size:20px; width:80px; height:80px;" onclick="button1( 8)">08</button>
<button id="button09" style="font-size:20px; width:80px; height:80px;" onclick="button1( 9)">09</button>
<button id="button10" style="font-size:20px; width:80px; height:80px;" onclick="button1(10)">10</button>
<br><br>
<br><br>
<button id="show1" onclick="show1()">show</button><br>
<textarea id="textarea1" style="width:10px; height:10px;"></textarea><br>
</body>
</html>
