【確率②】理論値と実践値の交差点:Pythonで捉える「確率収束の瞬間」

Python

前回の記事では、Pythonを使って「全事象」を漏れなく書き出し、理論上の確率を計算する方法を学びました。
しかし、現実の世界は、複雑で全パターンを数え上げるなど不可能な事象に満ちあふれています。

そこで登場するのが「シミュレーション」です。

「計算で答えを出す」のではなく、「コンピューターの中で実際に何万回も試してみる」。
一見、泥臭い力技に聞こえる手法ですが、現代のAIやデータ分析を支える強力な武器となるのです。

今回の記事では、サイコロ振りを題材にPythonで実験を行い、「理論上の確率(理論値)」と「実験から得られた確率(実践値)」がどのように出会い、重なっていくのかを体験します。
理論と実験の交差点を通り過ぎた先で、「確率が収束する瞬間」を目にすることになるでしょう。

第1章:「確率」を再定義する ~ 数学的な定義とコンピュータの視点

前回記事の数え上げによる確率計算を、数学では「古典的確率」(またはラプラスの定義)と呼びます。

一方、今回扱うシミュレーションの世界では、もう一つの重要な視点が必要になります。

1.1. 確率の基本定義:すべては「比率」である

確率の定義は非常にシンプルです。

\(\displaystyle P(A) = \frac{\text{事象 } A \text{ が起こる数}}{\text{すべての試行回数}}\)

「数え上げ」では、数学的にすべての可能性を把握できていたため、分母は「全事象の数」でした。

しかし、現実には全事象を数え上げることが不可能なケース(例えば、複雑な株価の変動や、無数のパターンが存在する気象予測など)が多々あります。
そうした「全貌が見えない世界」において、分母を「実際に試行(トライ)した回数」に置き換え、実験データから確率を導き出すのが、今回のシミュレーションのアプローチです。

1.2. 統計的確率:やってみなければ分からない?

「サイコロを振して1の目が出る確率は \(\displaystyle \frac{1}{6}\) である」というのは、すべての目が「同様に確からしい」と仮定した「理論値」です。
一方で、実際にサイコロを10回、100回と振って、1が出た回数をカウントして導き出す確率を「統計的確率」と呼びます。

ここで重要なのは、統計的確率は、試行回数によってその姿を変えるという点です。

  • 試行回数が少ないとき: 10回振って1が3回出れば、統計的確率は \(3/10 = 0.3\) になります。理論値(約 \(0.166\))とは大きくかけ離れ、「運」や「偶然」に支配されます。
  • 試行回数が多いとき: 1,000回、1万回、100万回……。回数を重ねるごとに、この数値は「理論値」へと吸い寄せられるように近づいていきます。

もちろん、サイコロの形状を見て「どの目も平等に出るはずだ」と判断することもできます。
しかし、現実の複雑な問題では、その前提が正しいのか、あるいはどんなルールで動いているのかを確認できないことが多々あります。

そんな時、「実際に試行を積み重ねることで、背後にあるルール(確からしさ)をあぶり出す」という統計的なアプローチが強力な武器になるのです。

1.3. 理論値と実験値を結びつける「試行」

前回記事では、「同様に確からしい」というルールに基づき、計算によって確率(理論値)を導き出しました。これは、ルールから結論を導く「演繹」のプロセスです。

対して、今回のシミュレーションは、その演繹的なルールが、現実においてどのように実現されるかをプログラミングによる圧倒的な試行回数で確かめる実験です。

次の章から、ルールに従ってコンピュータに「試行」をさせ、その結果がどれほど「理論上の正解」に肉薄するかを観察していきます。

第2章:シミュレーションの第一歩 ~ randomモジュール

コンピューターに「試行」をさせるために、まず「偶然」という要素をプログラムに組み込みます。

2.1. Pythonに「偶然」を任せる

Pythonの標準ライブラリである random モジュールを使うと、指定した範囲からランダムに数値を取り出す「乱数」を生成できます。

この「乱数」によってデジタルなサイコロを表現します。

以下のプログラムをコーディングしてください。

sample2.py
import random

# 1から6までの整数をランダムに一つ選ぶ(サイコロ1回分)
result = random.randint(1, 6)
print(f"出目: {result}")

試しに数回実行してみると、実行するたびサイコロの出目が変化するはずです。(連続で同じ出目になる場合ももちろんあり得ます)

注意:random.randint の範囲指定について

前回使用した itertools や、Pythonの一般的なスライス(range など)では、「最後の一歩手前まで」(つまり、未満)を範囲とすることが多いですが、この random.randint(a, b)\(a\) 以上 \(b\) 以下、つまり「両端の数値を含む」という直感的な仕様になっています。

これにより、サイコロの「1から6までの目が同様に確からしく出る」という設定を、そのままコードに落とし込むことができます。

2.2. 試行回数を「N回」に拡張する

2.1. で作成したプログラムでは、処理が1回しか試行されません。

シミュレーションの真価は、1回きりの結果を眺めることではなく、それを数万回、数十万回と繰り返したあとの「比率」を確認するプロセスで発揮されます。
Pythonでは for 文を使うことで、膨大な試行回数を1行で指定することができ、瞬時に実行できます。

作成した sample2.py を、次のように編集してください。
ただし、まだ実行しても何も表示されません

現時点での結果を print で表示させても良いのですが、1万回も繰り返すため、画面が膨大な出力で埋まってしまいます。
そのため、まずはコンピュータに「サイコロを振らせる(試行)」ステップのみをプログラミングしており、その「結果を記録する(集計)」ステップは分けて考えます。

sample2.py
import random

# 試行回数を決める
trials = 10000

# 1万回の試行を行う(ここではまだ画面に表示させない)
for _ in range(trials):
    result = random.randint(1, 6)
    # TODO: ここで「どの目が何回出たか」をカウントする処理が必要

第3章:膨大なデータを集計する ~ リストの利用

1万回の試行を行っても、その結果を何も処理せず捨ててしまっては「比率」を計算することができません。
よって、ここでは、コンピュータに「どの目が何回出たか」を記憶させる方法を解説します。

3.1. 「情報の入れ物」を用意してカウントする

1から6までの出目を数えるために、あらかじめ6つの「カウンター」を用意します。
Pythonではリスト (List) を使うことで、これをスマートに管理できます。

sample2.py コード全文は以下のようになります。

sample2.py
import random

trials = 10000
# [1の目, 2の目, 3の目, 4の目, 5の目, 6の目] の出現回数を格納するリスト
counts = [0, 0, 0, 0, 0, 0]

for _ in range(trials):
    result = random.randint(1, 6)
    # 出た目に対応するインデックスの数値を1増やす
    counts[result - 1] += 1

print(f"各出目の回数: {counts}")

3.2. コードの説明

このプログラムを理解するには、Pythonの「リスト(List)」という仕組みについて知る必要があります。

リストとは、複数のデータを一列に並べて管理するための入れ物、箱をイメージすればOKです。
プログラミングの専門用語で言えば、「データ型」と言います。

今回のプログラムでは、サイコロの1から6までの出目が、それぞれ何回ずつ出たのかを記憶させたいので、6つのデータを入れられる箱が必要ですね。

それがコードで言うと [0, 0, 0, 0, 0, 0] です。
プログラムが実行されるまでは、サイコロの目はどれも出ていないので、6つの箱にはゼロが入っています。

3.2.1. インデックス(箱の番号)のルール

このリストと呼ばれる「箱」からデータを取り出したり、中身を書き換えたりするときは、左から何番目かという「番号」で指定します。
この番号をインデックスと呼びます。

ここで一つだけ、プログラミング特有のルールを覚える必要があります。
それは、インデックスは「0」から数え始めるということです。

  • 1つ目のデータ:インデックス 0
  • 2つ目のデータ:インデックス 1
  • ……
  • 6つ目のデータ:インデックス 5

3.2.2. インデックスの「ズレ」解消

コードにある counts[result - 1] += 1 は、この「0から始まるルール」に対応するための処理です。

と言っても、意味が分かりづらいですよね。

result は、result = random.randint(1, 6) という処理で取得されています。
先に説明したとおり、random.randint で取得した結果は1から6までのいずれかです。
となると、インデックスは0から始まるため、ズレが生じてしまいます。
そのズレを解消し、counts というリストの0番目を指し示せるようにマイナス1しています。

そして、最終的に欲しいのは、「その目が出た回数」なので、回数を増加させるためにプラス1しているというコードです。

第4章:実験値は理論値に肉薄するか

いよいよ、集計した「各出目の回数」を「比率」に変換し、前回の記事で導き出した「理論値」と比較します。

4.1. 回数から「比率(実験値)」を計算する

1万回振った結果、例えば「1の目」が1,660回出たとします。このとき、1の目が出る割合は \(1660 \div 10000 = 0.166\) と計算できます。

このように、実際にやってみた結果から得られる確率のことを、第1章で触れた通り「実験値(統計的確率)」と呼びます。

Python のプログラムによって記録した1から6までの出目の回数を、それぞれ全体の試行回数(trials)で割ることで、この数値を算出します。

以下のコードを、sample2.py の最後に追加してください。

sample2.py
# ここより上のコードは省略

# 実験値(比率)を表示する
print("--- 実験値の計算結果 ---")
for i in range(6):
    ratio = counts[i] / trials
    print(f"{i+1}の目: {ratio:.4f}")

4.2. プログラムの完成形(sample2.py)

今回のシミュレーションの「完成形」となるコード全文を掲載します。

「試行する(サイコロを振る)」ステップと、「表示する(計算結果を出す)」ステップを明確に分けて記述しています。

sample2.py
import random

# 全体の試行回数を設定
trials = 10000
# 1〜6の目のカウンター(初期状態はすべて0)
counts = [0, 0, 0, 0, 0, 0]

# --- 【試行ステップ】 ---
for _ in range(trials):
    result = random.randint(1, 6)
    counts[result - 1] += 1

# --- 【表示ステップ】 ---
print(f"各出目の回数: {counts}")
print("--- 実験値の計算結果 ---")

for i in range(6):
    # 各出目の回数を全体の回数で割り、比率(実験値)を出す
    ratio = counts[i] / trials
    # 小数点以下4桁まで表示
    print(f"{i+1}の目: {ratio:.4f}")

試しに実行してみると、次のような結果になりました。

実行結果
各出目の回数: [1702, 1644, 1644, 1681, 1647, 1682]
--- 実験値の計算結果 ---
1の目: 0.1702
2の目: 0.1644
3の目: 0.1644
4の目: 0.1681
5の目: 0.1647
6の目: 0.1682

理論値では \(1 \div 6\) で、約 \(0.1666…\) となるので、理論値にかなり肉薄する実験値になったと言えるのではないでしょうか。

これが正しく「確率の収束」です。

10回や20回程度の試行では、たまたま特定の目が連続して出るなどの「偏り」が大きく影響します。
しかし、試行回数を1万回、10万回と増やしていくことで、一時的な偏りは全体の膨大なデータの中に埋もれていき、最終的には理論上の数値(\(0.1666…\))へと限りなく近づいていくのです。

まとめ

今回の記事では、試行における実験値が、試行回数を増やすことによってどこまで「理論値」に近づけるのかを、Pythonを使って検証しました。

  • 演繹的な正しさ(理論値): サイコロの構造から考えれば、\(1 \div 6\)(約 \(0.1667\))になるはず。
  • 帰納的な正しさ(実験値): 実際に1万回振ってみたら、どの目も \(0.16\) 〜 \(0.17\) 程度の割合で出た。

1万回という、人間には不可能な回数の試行をコンピューターに肩代わりさせることで、私たちは「理論の正しさ」を実験によって裏付けることができました。

このように、理論を立て(演繹)、それを実験で確かめる(帰納)というプロセスは、データサイエンスや科学の世界における基本の「キ」です。
Pythonという強力な道具を手に入れたことで、机上の空論だけで終わらせる必要はありません。

「本当かな?」と思ったら、プログラムを書いて確かめてみる。
そんなシミュレーションの第一歩を、この記事を通じて踏み出したのです。

コメント

タイトルとURLをコピーしました