PHPでモンティ・ホール問題をシミュレーション

(1/1)
モンティ・ホール問題
モンティ・ホール問題とは、条件付き確率に関する有名な問題であるが、これがアメリカのテレビ番組「Let's Make A Deal」の中でゲームとして行われたことから、番組司会者モンティ・ホールの名前をとって、そう名付けられた。
ゲームのルールは次の通り――
  1. プレイヤーの前に A ・ B ・ C の 3 つのドアがあり、3 つとも扉は閉じている。
  2. そのうち 1 つのドアの奥には景品の自動車があり、残りはハズレのヤギがいる。
  3. プレイヤーはドアを 1 つ選択する。ここではドアを開けない。
  4. 正解のドアを知っているモンティは、残ったドアのうちハズレのドアを開ける。
  5. モンティは「今なら選択を変更してもいいですよ」とプレイヤーに問いかける。
――プレイヤーは最初の選択を変更した方が得だろうか、変更しない方が得だろうか、どちらでも結果は変わらないだろうか。

サンプル・プログラムの実行例

モンティ・ホール問題
サンプル・プログラムを実行すると、最初の選択を変更した方が得策であることが分かる。
ランダムに 1 万回のドア開け試行を行い、ドアを変えない場合のアタリ確率は  mimetex  に、変えた場合のアタリ確率は  mimetex  に収束することが分かる。試行回数は横軸に対数目盛として示している。
この結論については。いくつかの数学的解説があるが、条件付き確率の問題として、機械学習で紹介したベイズの定理を用いるのが王道である。

サンプル・プログラム

準備

グラフ描画には JpGraph を利用する。導入方法については、「PHP と JpGraph で人口ピラミッドを表示する」を参照のこと。"jpgraph_log.php" は、対数グラフを描くために必要なモジュールだ。

0026: //エラー表示用フォント・ファイル名;各自の環境に合わせて
0027: define('FONT', '../../../../common/font/ipagp.ttf');
0028: 
0029: //JpGraph(各自の環境に合わせて)
0030: require_once('jpgraph/jpgraph.php');
0031: require_once('jpgraph/jpgraph_log.php');
0032: require_once('jpgraph/jpgraph_line.php');
0033: 
0034: //ドア識別子
0035: define('DOOR_LABEL', 'ABC');
0036: 
0037: //試行回数
0038: $Counter = 10000;

3 つのドア A ・ B ・ C は定数 DOOR_LABEL に、試行回数は変数 $Counter にあらかじめ用意しておく。これらは変更可能である。
ドア識別子を A ・ B ・ C ・ D‥‥のように増やすことも可能で、増やしたとしてもドアを変更した方がアタリ確率がアップすることが分かる。

解説:モンティ・ホール問題

0052: /**
0053:  * マイクロ秒により乱数器に種まき
0054: */
0055: function make_seed() {
0056:     list($usec$sec) = explode(' ', microtime());
0057:     mt_srand((float) $sec + ((float) $usec * 100000));
0058: }
0059: 
0060: /**
0061:  * ドア識別子をランダムに1つ返す
0062:  * @param なし
0063:  * @return string ドア識別子
0064: */
0065: function rand_door() {
0066:     $l = strlen(DOOR_LABEL) - 1;
0067:     $i = (int)mt_rand(0, $l);
0068: 
0069:     return substr(DOOR_LABEL$i, 1);
0070: }
0071: 
0072: /**
0073:  * 3つのドアを用意する
0074:  * @param array $doors ドア配列
0075:  * @return なし
0076: */
0077: function makeDoors(&$doors) {
0078:     $l = strlen(DOOR_LABEL) - 1;
0079: 
0080:     //アタリ
0081:     $hit = rand_door();
0082: 
0083:     //ドアにアタリ/ハズレを記入
0084:     for ($i = 0; $i <= $l$i++) {
0085:         $c = substr(DOOR_LABEL$i, 1);
0086:         $doors[$c]['hit']    = ($c == $hit) ? TRUE : FALSE;
0087:         $doors[$c]['status'] = FALSE;      //状態=閉じている
0088:     }
0089: }
0090: 
0091: /**
0092:  * まだ閉じているドアをランダムに1つ選ぶ
0093:  * @param array $doors ドア配列
0094:  * @return string ドア識別子/NULL:すべて空いている
0095: */
0096: function getRandomDoor($doors) {
0097:     //閉じたままのドアがあるかどうか調べる
0098:     $flag = FALSE;
0099:     foreach ($doors as $item) {
0100:         if ($item['status'] == FALSE) {
0101:             $flag = TRUE;
0102:             break;
0103:         }
0104:     }
0105:     if ($flag == FALSEreturn NULL;
0106: 
0107:     //ドアを1つ開ける
0108:     do {
0109:         $key = rand_door();
0110:     } while ($doors[$key]['status'] == TRUE);
0111: 
0112:     return $key;
0113: }
0114: 
0115: /**
0116:  * モンティ・ホール問題(1回)
0117:  * @param なし
0118:  * @return array(介入なし,介入あり)
0119: */
0120: function MontyHall_once() {
0121:     $doors = array();
0122:     makeDoors($doors);
0123: 
0124:     //プレイヤーがドアを1つ選ぶ
0125:     $user = getRandomDoor($doors);
0126:     $doors[$user]['status'] = TRUE;
0127: 
0128:     //モンティがドアを1つ開ける
0129:     make_seed();
0130:     do {
0131:         $monty = getRandomDoor($doors);
0132:     } while ($doors[$monty]['hit'] == TRUE);
0133:     $doors[$monty]['status'] = TRUE;
0134: 
0135:     //プレイヤーはドアを変えない
0136:     if ($doors[$user]['hit'])  $res0 = TRUE;
0137:     else                        $res0 = FALSE;
0138: 
0139:     //プレイヤーはドアを変える
0140:     $user = getRandomDoor($doors);
0141:     if ($doors[$user]['hit'])  $res1 = TRUE;
0142:     else                        $res1 = FALSE;
0143: 
0144:     return array($res0$res1);
0145: }

ドアを開ける作業は、ユーザー関数 MontyHall_once で行う。
冒頭に記したゲームのルールを、そのままプログラムに落とし込んである。

0148: /**
0149:  * モンティ・ホール問題
0150:  * @param int   $n = 試行回数
0151:  * @return array 確率配列
0152: */
0153: function MontyHall($n) {
0154:     $prob = array();
0155:     $cnt0 = 0;
0156:     $cnt1 = 0;
0157: 
0158:     for ($i = 0; $i < $n$i++) {
0159:         list($res0$res1) = MontyHall_once();
0160:         if ($res0)      $cnt0++;
0161:         if ($res1)      $cnt1++;
0162:         $prob[$i][0] = (double)$cnt0 / ($i + 1);
0163:         $prob[$i][1] = (double)$cnt1 / ($i + 1);
0164:     }
0165: 
0166:     return $prob;
0167: }

試行回数だけ繰り返し、そのときの確率を配列に格納していくのがユーザー関数 MontyHall である。

解説:グラフを描く

0169: /**
0170:  * グラフ描画
0171:  * @param array  $items データ
0172:  * @param int    $year_min 最小年
0173:  * @param int    $year_max 最大年
0174:  * @return object graphオブジェクト
0175: */
0176: function drawGraph($items$n) {
0177:     $plotdata = array();
0178: 
0179:     //プロット配列を作成
0180:     for ($i = 0; $i < $n$i++) {
0181:         $data_x[$i]  = $i;                       //横軸
0182:         $data_y0[$i] = $items[$i][0];
0183:         $data_y1[$i] = $items[$i][1];
0184:     }
0185: 
0186:     //グラフ描画処理開始
0187:     $graph = new Graph(600, 500, 'auto');
0188:     $graph->SetScale('loglin', 0.0, 1.0);        //X軸:対数,Y軸:小数
0189:     $graph->SetBackgroundGradient('#FFFFCC','#FFFFCC', GRAD_HOR, BGRAD_MARGIN);
0190: 
0191:     //タイトル、凡例などの設定
0192:     $graph->img->SetImgFormat('png');
0193:     $graph->img->SetMargin(60, 60, 20, 40);
0194:     $graph->title->SetFont(FF_PGOTHIC, FS_NORMAL, 16);
0195:     $graph->title->Set('モンティ・ホール問題');
0196:     $graph->xaxis->title->SetFont(FF_PGOTHIC);
0197:     $graph->xaxis->title->Set('試行回数');
0198:     $graph->yaxis->title->SetFont(FF_PGOTHIC);
0199:     $graph->yaxis->title->Set('当たり確率');
0200:     $graph->yscale->ticks->Set(0.1);
0201:     $graph->yaxis->SetTitleMargin(40);
0202: 
0203:     //ドアを変更しない場合
0204:     $plotdata[0] = new LinePlot($data_y0);
0205:     $plotdata[0]->SetLegend('ドアを変更しない'); //凡例
0206:     $graph->Add($plotdata[0]);
0207: 
0208:     //ドアを変更する場合
0209:     $plotdata[1] = new LinePlot($data_y1);
0210:     $plotdata[1]->SetLegend('ドアを変更する');     //凡例
0211:     $graph->Add($plotdata[1]);
0212: 
0213:     //色指定は最後に
0214:     $plotdata[0]->SetColor('#0000FF');          //色
0215:     $plotdata[0]->SetWeight(2);               //太さ
0216:     $plotdata[1]->SetColor('#FF0000');          //色
0217:     $plotdata[1]->SetWeight(2);               //太さ
0218: 
0219:     //凡例
0220:     $graph->legend->SetFont(FF_PGOTHIC);                   //フォント
0221:     $graph->legend->SetColumns(1);                         //表示列数
0222:     $graph->legend->SetPos(0.15, 0.85, 'right', 'bottom');    //位置
0223: 
0224:     return $graph;
0225: }

目的のグラフは、「PHP で対数グラフ(ムーアの法則)を描く」とは逆で、X軸が対数で、Y軸が確率という形である。ユーザー関数 drawGraph で描画する。

参考サイト

(この項おわり)
header