While preparing the first in an upcoming series of posts on multi-armed bandits, I realized that a post diving deep on a simple Monte Carlo estimate of π would be a useful companion, so here it is.
Monte Carlo methods are a powerful tool for approximating hard- or impossible-to-calculate quantities using randomness and simulation. In my talks on Bayesian computation with PyMC, I like to follow convention by introducing Monte Carlo methods by approximating π as follows.
%matplotlib inline
%config InlineBackend.figure_format = "retina"
from matplotlib import pyplot as plt, ticker
import numpy as np
from scipy.stats import norm
import seaborn as sns
sns.set(color_codes=True)
We begin by generating 5,000 uniformly random points in the unit square 0≤x,y≤1.
# for reproducibility
SEED = 123456789
rng = np.random.default_rng(SEED)
N = 5_000
x, y = rng.uniform(size=(2, N))
Here we visualize these samples.
def make_axes():
_, ax = plt.subplots()
ax.set_aspect("equal")
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.set_xlim(0, 1.01)
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
ax.set_ylim(0, 1.01)
return ax
ax = make_axes()
ax.scatter(x, y, alpha=0.25);
Next we overlay the quarter of the unit circle corresponding to y=√1−x2 over these points.
def quarter_circle(x):
return np.sqrt(1 - x**2)
ax = make_axes()
xs = np.linspace(0, 1, 100)
ax.plot(xs, quarter_circle(xs), c="k");
ax.scatter(x, y, alpha=0.25);
Finally, we highlight those samples that lie inside the unit circle, x2+y2≤1.
in_cirle = x**2 + y**2 <= 1
ax = make_axes()
xs = np.linspace(0, 1, 100)
ax.plot(xs, quarter_circle(xs), c="k");
ax.scatter(x[in_cirle], y[in_cirle], c="C2", alpha=0.25);
ax.scatter(x[~in_cirle], y[~in_cirle], c="C1", alpha=0.25);
A Monte Carlo approximation for π arises from considering the proportion of samples that lie within the unit circle. This proportion is the ratio of the area of the quarter unit circle, π4, to the area of the square, 1. Therefore we expect π4 of the samples to be within the unit circle.
Calculating this quantity and multiplying by four, we have a reasonable, if crude, approximation of π.
4 * in_cirle.mean()
3.12
The application of Monte Carlo methods to multi-armed bandits to be discussed in future posts arises when we examine closely why this intuitive approximation works.
Let
Q={(x,y)∈R2 | 0≤x,y≤1, x2+y2≤1}be the quarter of the unit circle in the first quadrant, and let 1Q be the indicator function of Q,
1Q((x,y))={1if 0≤x,y≤1, x2+y1≤10otherwise.A basic result of probability theory is that the expected value of an indicator function is the probability of its associated set, so
E(1Q)=P(Q)=P(0≤x,y≤1, x2+y2≤1)=π4.The fact that 1Q is an unbiased estimator of P(Q) will enable an efficient implementation of Thompson sampling in the upcoming post series on multi-armed bandits.
To understand exactly why this approximation works, we turn to the law of large numbers. If we denote our random samples as (x1,y1),(x2,y2),…,(xn,yn), the law of large shows that
ˆπn=4nn∑i=11Q((xi,yi))→4⋅E(1Q)=πas n→∞.
We visualize this convergence below.
n = 1 + np.arange(N)
π_hat = 4 * in_cirle.cumsum() / n
fig, ax = plt.subplots()
ax.plot(n, π_hat, label=r"$\hat{\pi}_n$");
ax.axhline(np.pi, c="k", ls="--", label=r"$\pi$");
ax.set_xlim(-50, N);
ax.set_xlabel("$n$");
ax.set_ylim(0.9 * np.pi, 1.1 * np.pi);
ax.set_ylabel("Approximation");
ax.legend();
Note that while ˆπn generally improves as an estimator as n increases, it tends to wander a bit.
While the large law of numbers guarantees that our Monte Carlo approximation will converge to the value of π given enough samples, it is natural to ask how far we expect our approximation to deviate from the true value for any finite sample size. The central limit theorem answers this question. Before invoking this theorem, we calculate the variance of 1Q. Since 02=0 and 12=1, (1Q)2=1Q, and we see that
Var(1Q)=E((1Q)2)−(E(1Q))2=E(1Q)−π216=π4−π216=π4⋅(1−π4).So Var(4⋅1Q)=π⋅(4−π), and by the central limit theorem
√n(ˆπn−π)d→N(0,π⋅(4−π)).By the definition of convergence in distribution,
P(√n(ˆπn−π)≤z)→Φ(z√π⋅(4−π)),where Φ is the cumulative distribution function of the standard normal distribution. By the symmetry of the this distribution,
P(√n|ˆπn−π|≤z)→2⋅Φ(z√π⋅(4−π))−1.Therefore an asymptotic confidence interval with coverage 1−α for |ˆπn−π| has width
√π⋅(4−π)n⋅Φ−1(1−α2).We now add an asymptotic 95% confidence interval to our previous plot.
ALPHA = 0.05
ci_width = np.sqrt(np.pi * (4 - np.pi) / n) * norm.ppf(1 - ALPHA / 2)
fig, ax = plt.subplots()
ax.plot(n, π_hat, label=r"$\hat{\pi}_n$");
ax.axhline(np.pi, c="k", ls="--", label=r"$\pi$");
ax.fill_between(
n, np.pi - ci_width, np.pi + ci_width,
color="k", alpha=0.25,
label=f"{1 - ALPHA:.0%} CI\n(asymptotic)"
);
ax.set_xlim(-50, N);
ax.set_xlabel("$n$");
ax.set_ylim(0.9 * np.pi, 1.1 * np.pi);
ax.set_ylabel("Approximation");
ax.legend();
We see that the approximation remains within this asymptotic confidence interval.
This post is available as a Jupyter notebook here.
%load_ext watermark
%watermark -n -u -v -iv
Last updated: Tue Feb 11 2025 Python implementation: CPython Python version : 3.12.5 IPython version : 8.29.0 numpy : 1.26.4 seaborn : 0.13.2 matplotlib: 3.9.2