こんにちは!
皆さん、円周率\piは知っていますよね?
円の直径に対する円周の長さの比率のことですね!
無理数で、小中学校の時に3.141592…と覚えたりしてましたよね。
学校では「円周 = 直径 \times ~ \pi」などの公式を学習しますが、この\pi自体の値って、どうやって正確に求めたのでしょう?
「円に内接する多角形を使う方法」や「級数展開を使う方法」などにより、過去の偉人たちは円周率を計算をしてきました。
今回は、難しい話は一切なしで、もっと直感的で、「神頼み」なランダムな方法で\piを求めてみます。
その方法は「モンテカルロ法」!
カジノで有名な地名が由来なんて、ちょっとわくわくしますよね!
モンテカルロ法
モンテカルロ法は、乱数を用いて試行を繰り返し、数値計算や問題の近似解を求める手法です。
関数や数式の解析的な解が難しい場合に、ランダムに多数の入力を与え、その出力を統計的に処理して推定結果を得ることができます。
と、難しい話はここまでにして、
アイデアは「ダーツ投げ」
円周率\piをモンテカルロ法で求める方法は、
- 1辺の長さが2の正方形を用意 (中心が(0,0)で、xもyも-1から1の範囲)
- その正方形の中に、半径1の円をぴったりと書く
- この正方形の中に、ランダムに点を打っていく
イメージは、四角の中に目隠ししながらダーツを投げるかんじです。
さて、たくさんダーツを投げた時、「円の中に入ったダーツの数」と「正方形の中に入ったダーツの数 (つまり全数)」の比率は、どうなると思いますか?
直感的に分かるかもしれませんが、この比率は「円の面積」と「正方形の面積」の比率に近づきます。
数式で見てみる
つまり、
- 正方形の面積 : 2 \times 2 = 4
- 円の面積 : \pi \times 1^2 = \pi
なので、
\frac{\textrm{円の面積}}{\textrm{正方形の面積}} = \frac{\pi}{4}
となり、
\frac{\textrm{円の中に入った点の数}}{\textrm{全ての点の数}} \approx \frac{\pi}{4}
となるはずです。
この両辺を4倍すると
\pi \approx 4 \times \frac{\textrm{円の中に入った点の数}}{\textrm{全ての点の数}}
なので、\piの近似値を求める式が完成します。
プログラムで\piを求めて見よう
Pythonで求めてみよう
それでは、モンテカルロ法で\piを近似したいと思います。
また、視覚的にも分かるようにmatplotlibで点を打ってみます!
import random
import matplotlib.pyplot as plt
def generate_points(num_points) -> tuple[list[tuple[float, float]], list[tuple[float, float]]]:
"""
指定された数の点を生成し、円内と円外に分ける関数
:param num_points: 生成する点の数
:return: 円内の点のリストと円外の点のリスト
"""
points_inside = []
points_outside = []
for _ in range(num_points):
# -1から1の範囲でランダムな点を生成
x = random.uniform(-1, 1)
y = random.uniform(-1, 1)
# 原点からの距離の2乗を計算
distance_squared = x**2 + y**2
# 距離の2乗が1以下なら円の中
if distance_squared <= 1:
points_inside.append((x, y))
else:
points_outside.append((x, y))
return points_inside, points_outside
def calculate_pi(points_inside, num_points) -> float:
"""
円内の点の数から円周率を計算する関数
:param points_inside: 円内の点のリスト
:param num_points: 生成した点の総数
:return: 推定された円周率
"""
inside_circle = len(points_inside)
pi_estimate = 4 * inside_circle / num_points
return pi_estimate
def plot_points(points_inside, points_outside, num_points, pi_estimate):
"""
点をプロットする関数
:param points_inside: 円内の点のリスト
:param points_outside: 円外の点のリスト
:param num_points: 生成した点の総数
:param pi_estimate: 推定された円周率
"""
points_inside_x, points_inside_y = zip(*points_inside) if points_inside else ([], [])
points_outside_x, points_outside_y = zip(*points_outside) if points_outside else ([], [])
plt.figure(figsize=(6, 6))
plt.scatter(points_inside_x, points_inside_y, color='blue', s=1, label='Inside Circle')
plt.scatter(points_outside_x, points_outside_y, color='red', s=1, label='Outside Circle')
# 正方形を描く
plt.plot([-1, 1, 1, -1, -1], [-1, -1, 1, 1, -1], 'k-', linewidth=2, label='Square')
# 円を描く
circle = plt.Circle((0, 0), 1, fill=False, color='black', linewidth=2, label='Circle')
plt.gca().add_artist(circle)
plt.title(f'N = {num_points}, Estimated Pi = {pi_estimate:.6f}')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.axis('equal')
plt.xlim(-1.1, 1.1)
plt.ylim(-1.1, 1.1)
plt.show()
def main():
"""
メイン関数
"""
num_points = 1000
points_inside, points_outside = generate_points(num_points)
pi_estimate = calculate_pi(points_inside, num_points)
plot_points(points_inside, points_outside, num_points, pi_estimate)
print(f"Estimated Pi: {pi_estimate}")
if __name__ == "__main__":
main()
この結果は以下のようになります。
全然100個程度では全然近似できないこともありますが、ある程度近い値がでるのではないでしょうか?
main関数の中に書いているnum_pointsの数を1000、10000と増やしていくとかなり近い値になると思います。
実際に一度やってみると
1000個の点の時, Estimated Pi: 3.204 10000個の点の時, Estimated Pi: 3.1644 100000個の点の時, Estimated Pi: 3.13384 1000000個の点の時, Estimated Pi: 3.141312
となり徐々に円周率の値に近づいていることが分かります。
ただ、Pythonでは1000000個でもかなり時間がかかってしまうのでRustでもやってみましょう。

プログラムでやっていることは、
- x座標、y座標ともに-1から1までの範囲でランダムに生成
- その座標が円の中 (つまり、原点からの距離が1以下)なら範囲内のリストに追加、範囲外なら範囲外のリストに追加しています。(ここまでgenerate_points関数)
- そのリストから円内の点の個数と、すべての点の個数から前の章で紹介した式を計算し、円周率の推定値を算出します。(calculate_pi関数)
plot_points関数で範囲内の点と、範囲外の点をグラフに描画しています。
Rustで求めてみよう
Rust版ではグラフの描画はなしで、モンテカルロ法で円周率だけを求めてみたいと思います。
use rand::Rng;
fn calculate_pi(num_samples: u32) -> f64 {
let mut points_inside_circle = 0;
for _ in 0..num_samples {
// (-1,1)の範囲でランダムに点を生成
// rand::random::<f64>() が0以上1未満の数値を生成するため
// 2倍して1を引いて(-1,1)の範囲の数値が出るようにしている
let x: f64 = rand::random::<f64>() * 2.0 - 1.0;
let y: f64 = rand::random::<f64>() * 2.0 - 1.0;
// 点が円の中にあるかどうかを判定
if x * x + y * y <= 1.0 {
points_inside_circle += 1;
}
}
// 円の中に入った点の割合を計算し、πを近似
let pi_approximation = 4.0 * (points_inside_circle as f64 / num_samples as f64);
pi_approximation
}
fn main() {
let num_samples = 100_000_000;
let pi_estimate = calculate_pi(num_samples);
println!("Estimated π: {}", pi_estimate);
}
実行した結果は
Estimated π: 3.14166588
となり、円周率にかなり近づいていることが分かります。
ランダムな点の集まりだったはずなのに、試行回数を増やすと、そこから正確な値が浮かび上がってきました。
これは、確率論の「大数の法則」という現象のおかげなのですが、ちょっと神秘的ですね!
JavaScriptでpiを求めよう
100から10000までの数値を入れて\piの近似値がどんな値になるか確かめてみましょう!
モンテカルロ法で円周率を推定
まとめ
今回は、「モンテカルロ法」を使って、ランダムな点を使って円周率を求めてみました。
この方法は、円周率計算だけでなく、もっと複雑な図形の面積を求めたり、金融工学で将来の株価を予測したり、物流シミュレーションだったりと、実は様々な分野で使われているパワフルな手法です。
最後まで読んで頂き、ありがとうございました!
