第6講 確率シミュレーション

乱数を用いた数値実験

Published

April 6, 2026

準備

以下で利用する共通パッケージを読み込む.

library(conflicted)  # 関数名の衝突を警告
conflicts_prefer(    # 優先的に使う関数を指定
    dplyr::filter(),
    dplyr::select(),
    dplyr::lag(),
    )
library(tidyverse)

コイン投げの賭け

問題

以下のようなコイン投げの賭けを考える.

  • Alice と Bob の二人で交互にコインを投げ,最初に表が出た方を勝ちとする.

この賭けの勝率を求めるための確率シミュレーションを行いなさい.

解答欄

解答例

試行を行う関数を定義する.

my_trial <- function(){ 
    while(TRUE){ # 永久に回るループ
        if(sample(c("h","t"),1) == "h"){ # h:head,t:tail
            return("Alice") # Aliceが表を出して終了
        }
        if(sample(c("h","t"),1) == "h"){
            return("Bob") # Bobが表を出して終了
        }
        #' どちらも裏ならもう一度ループ
    }
}
Note

この関数は rbinom を用いても実装できるので試みてみよう

試行を行ってみる. このとき勝った方の名前が表示される.

my_trial(); my_trial(); my_trial() # 3回行ってみる
[1] "Alice"
[1] "Alice"
[1] "Bob"

確率シミュレーションを行ってみる.

#' set.seed(8888) # 実験を再現する場合は適当なシードを指定する
mc <- 10000       # 回数を設定 
my_data <- replicate(mc, my_trial())
glimpse(my_data)      # 結果が mc 次元のベクトルになっていることが確認できる
 chr [1:10000] "Alice" "Alice" "Alice" "Alice" "Alice" "Alice" "Alice" ...

簡単な集計を行う.

table(my_data)    # 頻度を表示する
my_data
Alice   Bob 
 6729  3271 
table(my_data)/mc # 勝率の計算 (全実験回数で除算)
my_data
 Alice    Bob 
0.6729 0.3271 

この賭けは先手が有利であることが確認できる.

確率シミュレーションの例題

以下の確率的な事象のシミュレーションを考えてみなさい.

Buffon の針

2次元平面上に等間隔 d で平行線が引いてある. 長さ l の針を この平面上にランダムに落としたとき, 平行線と交わる確率はいくつか? ただし l\leq d とする.

解答欄

解答例

針を投げる試行を定義する. 試行の周期性から1本の線の回りで問題を考えれば良い. 針の中心位置 x\;(-d/2<x<d/2) と向き \theta\;(0<\theta<2\pi) をランダムに生成する. 針の先と尾の座標の符号が異なれば位置0の線と交わっていると判断できる.

buffon_trial <- function(d, l, verbose = FALSE){ # dとlを指定
    x <- runif(1, min = -d/2, max = d/2) # 位置
    theta <- runif(1, min = 0, max = 2*pi) # 角度
    cross <- FALSE # 交わったかどうかを示す変数
    if((x+l*cos(theta)/2)*(x-l*cos(theta)/2) < 0){
        cross <- TRUE # 交わった場合に書き換え
    }
    if(verbose == TRUE){ # 位置と角度も返す
        return(c(x = x, theta = theta,       # ベクトルで返すので
                 cross = as.numeric(cross))) # データ型を揃える
    } else { # 交わったかどうかだけ返す
        return(cross) # 論理値のまま返す
    }
}

試行を行ってみる.

d <- 10
l <- 7
buffon_trial(d, l); buffon_trial(d, l)
[1] TRUE
[1] FALSE

詳細な情報を表示する場合 (絵を描くときに用いる) は以下のように実行する.

buffon_trial(d, l, verbose = TRUE)
       x    theta    cross 
1.123461 4.945397 0.000000 
buffon_trial(d, l, verbose = TRUE)
        x     theta     cross 
-1.542298  2.978795  1.000000 

試行の結果を図にする.

mc <- 10
replicate(mc, buffon_trial(d, l, verbose = TRUE)) |>
    t() |> # 配列を転置
    as_tibble() |> # tibble に変換
    mutate( # 図に必要な情報を加える
        y = runif(mc, min = -(d+l)/2, max = (d+l)/2), # 中心のy座標をランダムに生成
        x1 = x-l/2*cos(theta), 
        x2 = x+l/2*cos(theta),
        y1 = y-l/2*sin(theta),
        y2 = y+l/2*sin(theta),
        cross = as.logical(cross)) |> # 交わり表すラベル(論理値)
    ggplot() + 
    geom_vline(xintercept = c(-d,0,d)) + # 縦棒を描く
    geom_segment(aes(x = x1, y = y1,       # 始点
                     xend = x2, yend = y2, # 終点
                     colour = cross)) +    # 交わりを表示
    labs(x = "x", y = "y") +
    coord_fixed() # 座標軸の比を1に固定

x\theta の関係を図にする.

mc <- 3000
replicate(mc, buffon_trial(d, l, verbose = TRUE)) |>
    t() |> 
    as_tibble() |> 
    mutate( # 図に必要な情報を加える
        cross = as.logical(cross)) |> # 交わり表すラベル(論理値)
    ggplot(aes(x = x, y = theta, colour = cross)) + 
    geom_vline(xintercept = c(-d/2,d/2)) + # 境界を描く
    geom_hline(yintercept = c(0,2*pi)) +   # 横線を描く
    geom_point() + labs(y = expression(theta)) 

大規模な確率シミュレーションを行う.

mc <- 100000 # 回数を設定 
buffon_data <- replicate(mc, buffon_trial(d, l)) 

簡単な集計を行い,理論値と比較する.

table(buffon_data)      # 頻度 (TRUEが針の交わった回数)
buffon_data
FALSE  TRUE 
55565 44435 
table(buffon_data)/mc   # 確率(推定値)
buffon_data
  FALSE    TRUE 
0.55565 0.44435 
print((2*l)/(pi*d))     # 針の交わる確率 (理論値)
[1] 0.4456338
2*l/d/(table(buffon_data)/mc)["TRUE"] # π の推定値
   TRUE 
3.15067 

Monty Hall 問題

ゲームの参加者の前に閉まった3つのドアがあって, 1つのドアの後ろには景品の新車が, 2つのドアの後ろには外れを意味するヤギがいる. 参加者は新車が置かれたドアを当てると新車がもらえる.

参加者が1つのドアを選択した後, 司会のモンティが残りのドアのうちヤギがいるドアを開けてヤギを見せる. ここで参加者は,最初に選んだドアを残っているドアに変更してもよいと言われる.

参加者はドアを変更すべきだろうか?

解答欄

解答例

クイズに答える試行を定義する. 以下は賞品の位置は固定して解答者をランダムに配置する場合を想定している.

monty_trial <- function(verbose = FALSE){
    #' 賞品は1に置いてあるものとする
    choice <- sample(1:3, size = 1) # 解答者の最初の選択
    if(choice == 1) { # 変えないのが正解の場合
        win <- "stay"
        door <- sample(2:3, size = 1) # Monty が開ける扉 (2か3をランダムに選択)
    } else { # 変えるのが正解の場合
        win <- "change"
        if(choice == 2) { # 2を選択したら3を開ける
            door <- 3
        } else { # そうでなければ(3を選択)2を開ける
            door <-2 
        }
    }
    if(verbose == TRUE){ # 賞品,解答者の選択,Monty が開ける扉を返す
        return(c(prize = 1, choice = choice, door = door))
    } else { # 勝ち負けの条件を返す
        return(win)
    }
}

賞品と解答者をランダムに配置する場合は以下のように定義すればよい. if 文で素朴に書くこともできるが, 集合を操作する関数 setdiff(), union() を利用している.

monty_trial <- function(verbose = FALSE){
    prize  <- sample(1:3, size = 1) # 賞品の置かれた扉
    choice <- sample(1:3, size = 1) # 解答者の最初の選択
    if(prize == choice) { # 変えないのが正解の場合
        win <- "stay"
        door <- sample(setdiff(1:3, prize), size = 1) # Monty が開ける扉 (ランダムに選択)
    } else { # 変えるのが正解の場合
        win <- "change"
        door <- setdiff(1:3, union(prize, choice)) # Monty が開ける扉
    }
    if(verbose == TRUE){ # 賞品,解答者の選択,Monty が開ける扉を返す
        return(c(prize = prize, choice = choice, door = door))
    } else { # 勝ち負けの条件を返す
        return(win)
    }
}

試行を行ってみる.賞品を獲得できた選択(stay/change)が返ってくる.

monty_trial(); monty_trial()
[1] "stay"
[1] "change"

詳細な情報を表示する場合 (絵を描くときに用いる) は以下のようになる. 表示は順に,賞品の位置(prize),最初の選択(choice),開かれたドア(door)を示している.

monty_trial(verbose = TRUE)
 prize choice   door 
     1      1      2 
monty_trial(verbose = TRUE)
 prize choice   door 
     2      1      3 

実験の状況を絵にする.記号は前述のとおり.

mc <- 15
replicate(mc, monty_trial(verbose = TRUE)) |>
    t() |> # 配列を転置
    as_tibble() |> # データフレームに変更
    rowid_to_column(var = "index") |> # 試行の番号を追加
    pivot_longer(!index) |> # index 以外を long format 化
    ggplot(aes(x = index, y = value,
               colour = name, shape = name)) +
    geom_point(size = 4) + # サイズを大きめに表示
    scale_y_continuous(breaks = 1:3)

実験とともに勝率がどのように変化するか図示する.

mc <- 400
replicate(mc, monty_trial()) |>
    as_tibble_col(column_name = "win") |>
    rowid_to_column(var = "index") |>
    mutate(stay_win = win == "stay", # 留まって勝つ場合
           ratio = cumsum(stay_win)/index) |> # 勝率の推移を計算
    ggplot(aes(x = index, y = ratio)) +
    geom_line(colour = "blue") + 
    geom_hline(yintercept = 1/3, colour = "orange") + # 理論値
    ylim(c(0,1)) # y軸の描画範囲を指定

大規模な確率シミュレーションを実行する.

mc <- 50000 # 回数を設定 
monty_data <- replicate(mc, monty_trial()) 

簡単な集計を行う.

table(monty_data)    # 頻度
monty_data
change   stay 
 33204  16796 
table(monty_data)/mc # 確率(推定値)
monty_data
 change    stay 
0.66408 0.33592 

選択を変えた場合の方が賞品を獲得する確率が高いことがわかる.

試行を模擬する関数は,論理式を使って書くこともできる. 関数 sample(x, size) では x に1つの数字 n を渡すと 1:n と解釈されるので,以下では x に文字を渡すようにしている.

monty_trial <- function(change = FALSE, # ドアを変えるか否か
                        verbose = FALSE){
    doors <- LETTERS[1:3]      # ドアの名前 A,B,C 
    prize <-  sample(doors, 1) # 商品のドア
    choice <- sample(doors, 1) # 選んだドア
    monty <-  sample(doors[(doors != prize) & (doors != choice)], 1) # モンティが開いたドア
    if(change){ # 選択を変えた場合の最後に選んだドア
        final <- doors[(doors != choice) & (doors != monty)]
    } else { # 選択を変えない場合
        final <- choice
    }
    if(verbose){ # TRUEの場合
        return(c(prize=prize,choice=choice,monty=monty,final=final))    
    } else {
        return(ifelse(prize == final,"win","loss"))
    }
}

この関数を用いて,簡単な集計を行う.

table(replicate(mc, monty_trial()))/mc              # 最初に選んだドアのままの場合

   loss     win 
0.66416 0.33584 
table(replicate(mc, monty_trial(change = TRUE)))/mc # 選んだドアを変えた場合

   loss     win 
0.33148 0.66852 

秘書問題 (最適停止問題)

以下の条件のもと秘書を1人雇うとする.

  • n 人が応募しており n は既知とする.

  • 応募者には 1 位から n 位まで順位付けできる.

  • 無作為な順序で1人ずつ面接を行う.

  • 毎回の面接後その応募者を採用するか否かを決定する.

  • 不採用にした応募者を後から採用することはできない.

r-1 番までの応募者は採用せず, r 番以降の応募者でそれまで面接した中で最も良い者を採用する” という戦略を取るとき,最適な r はいくつだろうか?

解答欄

解答例

秘書の採用を模擬する試行を定義する.

secretary_trial <- function(n, r, verbose = FALSE){ # nとrを指定
    applicants <- sample(1:n, size = n) # n人の順位をランダムに並べ替える
    ref <- applicants[1:(r-1)] # r-1人目までは参照(最高順位が基準点)
    test <- applicants[r:n]    # r人目以降が採用候補
    idx <- which(test < min(ref)) # 採用候補の中で基準点より良い人を選出
    if(length(idx) == 0) { # 1人も規準良い人がいない場合
        employed <- applicants[n] # 最後の人を採用
    } else {
        employed <- test[idx[1]]  # 最初に基準点を越えた人を採用
    }
    if(verbose == TRUE){ # 全順位も返す
        return(list(applicants = applicants, 
                    employed = employed)) # 2つの異なるベクトルをリストで束ねる
    } else { # 採用した者の順位のみ返す
        return(employed)
    }
}

条件を変えながら試行を行ってみる. applicants は候補者の順位, employed は戦略に従って採用された人の順位を示す.

n <- 10 # 候補者は10名
secretary_trial(n, 2, verbose = TRUE) # 2人目から採用を考える
$applicants
 [1]  9  1  4  3  2  6  8  5  7 10

$employed
[1] 1
secretary_trial(n, 3, verbose = TRUE) # 3人目から採用を考える
$applicants
 [1]  7  8  1  9 10  3  4  2  6  5

$employed
[1] 1
secretary_trial(n, 4, verbose = TRUE) # 4人目から採用を考える
$applicants
 [1]  4  9  5 10  1  7  2  3  6  8

$employed
[1] 1
secretary_trial(n, 5, verbose = TRUE) # 5人目から採用を考える
$applicants
 [1]  7 10  2  9  5  8  3  6  4  1

$employed
[1] 1
secretary_trial(n, 6, verbose = TRUE) # 6人目から採用を考える
$applicants
 [1]  7  9  4  5  8  6 10  2  1  3

$employed
[1] 2

大規模なシミュレーションを実行する. 適当な r に対して,採用された人の順位の頻度表を作成している.

#' set.seed(8888) # 実験を再現したい場合はシードを指定
mc <- 3000
n <- 30 # 候補者数を変えて実験
secretary_data <- tibble(r = NULL,
                         employed = NULL)
for (r in 2:(n-1)) {
    foo <- replicate(mc, secretary_trial(n, r))
    if(r %in% c(2,6,10,14,18,22)) { # いくつか表示
        cat("採用開始: ", r, "\n")
        print(table(foo))
    }
    secretary_data <- bind_rows(secretary_data, # 前の実験結果に追加
                                tibble(r = rep(r,mc),
                                       employed = foo))
}
採用開始:  2 
foo
  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
407 303 277 211 194 160 168 124 138 127  93  93  87  84  76  65  53  52  42  36 
 21  22  23  24  25  26  27  28  29  30 
 45  31  37  29  22  14   9  15   4   4 
採用開始:  6 
foo
  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
922 549 312 233 176 133  92  74  60  58  34  33  26  13  17  21  21  21  18  22 
 21  22  23  24  25  26  27  28  29  30 
 17  21  22  13  16  11  11  24  15  15 
採用開始:  10 
foo
   1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16 
1135  493  257  164   97   68   48   44   37   30   39   38   31   23   30   21 
  17   18   19   20   21   22   23   24   25   26   27   28   29   30 
  31   29   28   30   26   36   40   36   28   31   35   31   32   32 
採用開始:  14 
foo
   1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16 
1121  406  153  110   66   53   42   47   54   43   55   46   46   40   44   53 
  17   18   19   20   21   22   23   24   25   26   27   28   29   30 
  39   44   48   42   43   44   38   50   41   46   33   45   62   46 
採用開始:  18 
foo
  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
994 320 126  87  56  57  64  53  47  65  49  55  61  52  62  59  48  57  55  59 
 21  22  23  24  25  26  27  28  29  30 
 67  51  42  69  57  68  44  49  66  61 
採用開始:  22 
foo
  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
815 181  97  74  70  86  61  63  70  65  69  67  70  63  54  72  79  67  63  65 
 21  22  23  24  25  26  27  28  29  30 
 76  71  90  70  76  76  87  70  62  71 

r でどのような順位が採用されたか分布を図示する. 上記の頻度表の箱ひげ図を r ごとに描いたものである.

secretary_data |>
    ggplot(aes(x = r, y = employed)) +
    geom_boxplot(aes(group = r), # rごとに集計
                 fill = "white", colour ="royalblue") +
    labs(title = paste("n =", n)) # nをタイトルに表示

理論的に良いとされるrの値 (nが十分大きい場合に求めることができるので調べてみよ) を計算する.

n/exp(1) 
[1] 11.03638

r で1位を採用できる確率を図示する.

secretary_data |>
    group_by(r) |> 
    summarize(ratio = mean(employed == 1)) |> # 1位ならTRUE(1)
    ggplot(aes(x = r, y = ratio)) +
    geom_step(colour = "royalblue") + # 階段関数で描画
    geom_vline(xintercept = n/exp(1), colour = "red") + # 理論値
    labs(title = paste("n =", n)) # nをタイトルに表示