2008年8月7日木曜日

コイン投げのシミュレーションで大数の法則を実感

数学の思い出

典型的な文系人間で、数学全般にアレルギーがあった。まぁ、小学一年生のころに算数のテスト一発目で 0 点。「繰り上げ」の意味がわからなくて一人居残りさせられ、母真っ青という思い出がある。算数の素質に問題あり。 ^^;

数学の中でも特に「確率」なんてチンプンカンプンだった。大学受験の二次試験のとき、出題されないと思い勉強せずに受験に臨んだら、大問 3 問中の 1 問が確率の問題だと知ったときはマジで終ったと思った。 ^^; そんなこんなで大学では数学なんてやらなくてすむと思いほっとしていたが、統計を使わなくては卒論が書けないので、多少は理解するために読まざる終えなかった。そして、友人に勧められて読んだのがこの本。難しい数式が使われず、読み物のよう。なつかしいなぁ。でも、表紙が変わってる。 (@_@;) 改訂版か。

確率のはなし―基礎・応用・娯楽 (Best selected business books) 確率のはなし―基礎・応用・娯楽 (Best selected business books)
大村 平

by G-Tools

さて、久しぶりに読むとおもしろい。必要に迫られないで自分の興味のままにわからないことを調べるというのであれば、数学の話もおもしろく感じる。今回は再度読むにあたり、折角なのでパソコンを使って実験し、確率を体感したいと思う。確率を理解する難しさは、特に一般化したときの話を具体的な問題に適用するときに、どれがどれに相当するのか訳がわからなくなるということが大きいが、それに加えて確率の実感がわきにくいということもあった。理論的に式を導いたり意味を理解するのは難しいと感じるが、ランダムを使って確率の動きを見ることくらいならできそうだ。

 

大数の法則

さて、確率の話となると「大数の法則」(p24) というのが最初の方にでてくる。大数の法則 – Wikipedia によると、

例えば「コイン投げ」、つまりゆがみも偏りもない"理想的なコイン"を投げて出る表裏を当てるゲームを行うとする。 (…) このとき、コイン投げの試行回数を限りなく増やせば、表が出る回数と裏が出る回数の比率はどちらも 1/2 に近づく。

シミュレーション:大数の法則 law of large numbers 中川雅央 - 滋賀大学 では、「確率 p  の事象に対して n 回試行を行う」場合のシミュレーションを実際に試すことができる。コイン投げであれば、 p を 1/2 に設定した状態が相当する。

このシミュレーションでは、 確率 p を変動させて試すことができることからわかるように、「確率 p の事象」という抽象的なレベルでシミュレーションを表現してる。 JavaScript で書かれているのでソースを見ることができるが、「確率 p の事象」が起こったということをコード上で次のように表現している。

if(Math.random() < p) r++;

p は確率、 r は事象が起きた回数を表わしている。つまりこの表現では、ランダムな数値を生成し、それが設定した確率 p 以下であれば「事象が起こった」と見なすということ。

 

コイン投げのシミュレーション

さて、文系な脳みその自分は、いきなり抽象的な表現をされると訳がわからなくなる。そこで、はじめはもっと限定的で具体的な状況でのシミュレーションを行うことにした。つまり、上記で引用したように、「コインを投げてその裏表の回数を数える」ということをその言葉通り表現してみる。言語は Ruby を使う。

まずは、投げる対象である「コイン」を表現してみる。 コインには表と裏の二つの状態 (@omote) があり、コインを生成したときにその状態が決まる (setState) とする。シュミレーションをするためにコインを投げる (nageru) 必要があり、その結果.表か裏かの状態が変化する。 @omote を見れば、そのコインの現在の状態が表か裏か知ることができる。
# コインを表わすクラス
class Coin
  attr_reader :omote	# 表かどうかの状態を表わす
  def initialize
    setState
  end
  # コインを投げる
  def nageru
    setState
  end
  private
  # コインの表裏の状態を設定する
  def setState
    @omote = if rand().round == 1 then true else false end
  end
end

このコインを n 回数投げて、表が出た割合を調べてみればよい。

 

ところで、これだけだと先ほどのシュミレーションのより限定的な状況を表現しているるだけなので、少し変化をつけてみる。どこを変えるかと言えば、コインを n 回投げるだけではなく、「n 回投げることを x 回繰り返す」ようにしてみる。そして、それぞれの回において表が出た割合がどのくらいだったのかを記録し、適当な基準において結果を表示するようにする。イメージとしては、次のような結果が表示されるようにする。

コインを 20 回投げる試行を 100  回繰り返します。

0   - 15 % :  1

15 – 25 % :  3

25 – 35 % :  8

上記の場合、例えばコインを 20 回投げてすべて裏だったら 0 – 15 % の区間にカウントする。 20 回投げて 4  回表だったら 15 – 25 % の区間にカウントする。以下同じようにして、それを 100 回繰り返す。

 

設計

コードを書く前に、おおよその輪郭を考える。以下に示すように、コインは上記の Coin クラスで、それを投げる人 (Hantei クラス) がいるとする。コインを投げる人は、指定に従ってコインを投げることに専念し、コインを投げた結果は Result クラスに記録していく。

080807-002

結果は集計され、最終的な表示に変換される。この集計を行う Counter クラスでは、Result に記録された結果を読み取り、対応したカウンターでカウントすることによって分析を行う。

これでコイン投げのシュミレーションと結果の表示までのモデル化ができた。効率的ではないけれど ^^; 、やりたいことをそのまま直接的に表現した。

 

実装

# コインを投げて結果を記録するクラス。
class Hantei
  def initialize(coin)
    @coin = coin
    @result = Result.new
  end
  # コインを n 回投げる。
  def nageru(n)
    omote_num = 0	# 表が出た回数
    for i in 0...n
      @coin.nageru
      omote_num += 1 if @coin.omote
    end
    # n 回中表が出た割合を結果として記録する。
    @result.add(omote_num / n.to_f * 100)
  end
  # コインを n 回投げることを x 回繰り返す。
  def repeat(x,n)
    puts "コインを #{n} 回投げる試行を #{x} 回繰り返します。"
    for i in 0...x
      nageru(n)
    end
  end
  # 結果を表示する。
  def result
    @result.print
  end
end

# 結果を保持するクラス。
class Result
  def initialize
    @results = []		# 結果の配列
    @counter = Counter.new	# 集計をするためのもの
  end
  # 結果を追加する
  def add(result)
    @results << result
  end
  # 結果を設定した基準で集計する
  def analyze
    @results.each do |result|
      @counter.add(result)
    end
  end
  # 結果を表示する。
  def print
    analyze
    @counter.result
  end
end

# 結果を集計するためのクラス
class Counter
  INTERVAL = 10
  include Enumerable
  def initialize
    @counterUnits = []
    setCriterion
  end
  # 集計のための基準を設定する
  def setCriterion
    @counterUnits << CounterUnit.new(0,15)
    for i in 0..6
      @counterUnits << CounterUnit.new(15+INTERVAL*i, 15+INTERVAL*i+INTERVAL)
    end
    @counterUnits << CounterUnit.new(85,100)
  end
  def each
    @counterUnits.each do |counterUnit|
      yield counterUnit
    end
  end
  # 結果を追加する
  def add(result)
    @counterUnits.find { |counterUnit|
      counterUnit.lowerLimit <= result and 
      	counterUnit.upperLimit >= result
    }.countUp
  end
  # 集計の結果を表示する
  def result
    @counterUnits.each do |counterUnit|
      printf("%2d -%3d%% : %3d\n",
             counterUnit.lowerLimit, 
             counterUnit.upperLimit, 
             counterUnit.counter)
    end
  end
  
  # 集計のための個々のカウンター
  class CounterUnit
    attr_reader :lowerLimit, :upperLimit, :counter
    def initialize(lowerLimit, upperLimit)
      @lowerLimit = lowerLimit
      @upperLimit = upperLimit
      @counter = 0
    end
    def countUp
      @counter += 1
    end
  end
end

集計のための区間がハードコードされているけれど、とりあえず、まぁいいや。 ^^;

 

結果

上記のクラスを使うには、次のようにする。

hantei = Hantei.new(Coin.new)
hantei.repeat(100,10)
hantei.result

結果は、次のようになる。

コインを 10 回投げる試行を 100 回繰り返します。
 0 - 15% :   1
15 - 25% :   9
25 - 35% :  14
35 - 45% :  21
45 - 55% :  23
55 - 65% :  14
65 - 75% :  11
75 - 85% :   6
85 -100% :   1

パラメータの値を変化させていくと、

コインを 20 回投げる試行を 100 回繰り返します。
 0 - 15% :   0
15 - 25% :   4
25 - 35% :  10
35 - 45% :  32
45 - 55% :  13
55 - 65% :  38
65 - 75% :   3
75 - 85% :   0
85 -100% :   0

コインを 40 回投げる試行を 100 回繰り返します。
 0 - 15% :   0
15 - 25% :   0
25 - 35% :   5
35 - 45% :  34
45 - 55% :  35
55 - 65% :  25
65 - 75% :   1
75 - 85% :   0
85 -100% :   0

コインを 100 回投げる試行を 100 回繰り返します。
 0 - 15% :   0
15 - 25% :   0
25 - 35% :   0
35 - 45% :  27
45 - 55% :  54
55 - 65% :  19
65 - 75% :   0
75 - 85% :   0
85 -100% :   0

コインを 200 回投げる試行を 100 回繰り返します。
 0 - 15% :   0
15 - 25% :   0
25 - 35% :   0
35 - 45% :   7
45 - 55% :  83
55 - 65% :  10
65 - 75% :   0
75 - 85% :   0
85 -100% :   0

コインを 500 回投げる試行を 100 回繰り返します。
 0 - 15% :   0
15 - 25% :   0
25 - 35% :   0
35 - 45% :   0
45 - 55% :  96
55 - 65% :   4
65 - 75% :   0
75 - 85% :   0
85 -100% :   0

コインを投げる回数を増やすにつれて、 1/2 の確率に近付いているのがわかる。