Estimating Pi on Pi Day
You’ve probably heard that March 14 is unofficially (or maybe officially in some circles) recognized as Pi Day. That’s because the digits 3-1-4 match the first three digits in the number pi (π = 3.14159265…). I’ve always said there’s no better way to celebrate made-up holidays than by doing math, so let’s get started.
One way to estimate pi is to analyze the area-ratio of a square and a circle inscribed within it. There’s a bit to unpack from that statement so let me add a visual aid:
Here we have a circle circumbscribed by a square. That’s a fancy way of saying the square’s overall width is the same as the circle’s overall width. In more formal terms, the circle’s radius is length R and the square’s sides are length 2R.
A circle’s area is defined as π*r², where r is the radius. Notice that this expression includes pi, so if we’re trying to estimate pi then we’re starting to get somewhere. A square’s area is even easier to calculate: [side]*[side].
Now look at what happens if we divide the above circle’s area by the area of its square counterpart:
The R² terms cancel and we’re left with a constant: π/4. We can then simply multiply by 4 to solve for pi.
So where does Python come in? Well, assuming for some reason that we can’t draw the shapes and measure them, the next-best approach might be to randomly generate points within the square and check how many fall within the circle.
If the points are uniformly distributed across the x and y axes, the probability of a point falling within a shape is proportional to the shape’s area. If, as we saw above, [Area of Circle] / [Area of Square] is equal to π/4, then π/4 percent of the points will fall within the circle. That’s about 78.5%. We can run a simulation to randomly generate thousands of points, calculate this ratio, and then multiply by 4 to estimate the value of pi.
Begin by randomly generating 10,000 points within a square of size 2R. This should be enough data to provide a reasonable estimate.
We’ll locate the center of the figure at (0, 0) and set R equal to 1.0. This will keep the code simple, but the final result would be no different if we used a larger square centered at (8, -4.5), for example.
import matplotlib.pyplot as plt from random import uniform from math import sqrt, pi R = 1.0 scatter_x, scatter_y = [], [] for _ in range(10000): scatter_x.append(uniform(-R, R)) scatter_y.append(uniform(-R, R))
With data in hand, check how many points fall within an inscribed circle of radius R. The easiest approach is to test whether a point lies within 1.0 units of the center (0, 0).
inside_circle = 0 for x, y in zip(scatter_x, scatter_y): distance = sqrt(x**2 + y**2) if distance < R: inside_circle += 1
Now we know how many points are within the circle. And we know all 10,000 points fall within the square because that’s how we designed the simulation.
inside_square = len(scatter_x)
These numbers are analogous to the area of each shape, since the probability of a point landing within a shape is equal to its share of the entire figure.
So we have estimates of each shape’s area, and we know their ratio should be equal to π/4. We’re almost there!
ratio = inside_circle / inside_square pi_estimate = ratio * 4
And we’re there! Just like that.
Before moving on we should calculate how close the simulation came to pi’s true value.
error_percent = (pi_estimate - pi) / pi * 100
Rather than printing the estimate to screen, let’s display everything on a nice plot.
Because the Matplotlib code is so simple I’ll skip the commentary. But as always, the full code will be available at the end of this post.
Without further ado, our estimate of pi:
With 10,000 randomly generated points we came within 0.42% of the actual value (3.14159265…). That’s not bad! Maybe we couldn’t build a particle accelerator with that sort of precision but we could certainly write blog posts all day.
Obviously with more data comes more precision. I put together an animated GIF to show how an estimate emerges from a larger simulation. Error generally trends toward zero but it’s not a straight path!
Download the Matplotlib style.
Full code:
import matplotlib.pyplot as plt from random import uniform from math import sqrt, pi R = 1.0 scatter_x, scatter_y = [], [] for _ in range(10000): scatter_x.append(uniform(-R, R)) scatter_y.append(uniform(-R, R)) inside_circle = 0 for x, y in zip(scatter_x, scatter_y): distance = sqrt(x**2 + y**2) if distance < R: inside_circle += 1 inside_square = len(scatter_x) ratio = inside_circle / inside_square # = (pi*R²) / (4*R²) = pi/4 pi_estimate = ratio * 4 error_percent = (pi_estimate - pi) / pi * 100 plt.style.use("wollen_dark.mplstyle") fig, ax = plt.subplots(figsize=(8, 8)) ax.scatter(scatter_x, scatter_y, alpha=0.3, zorder=1) ax.add_patch(plt.Circle((0, 0), R, facecolor="None", edgecolor="white", linewidth=3.0)) ax.add_patch(plt.Rectangle((-R, -R), R*2, R*2, facecolor="None", edgecolor="white", linewidth=3.0)) results_message = f"n = {len(scatter_x):,}\n" \ f"pi (est.): {pi_estimate:.4f}\n" \ f"error = {error_percent:+.2f}%" ax.text(0, 0, results_message, font="Ubuntu Condensed", size=13, ha="center", va="center", bbox={"boxstyle": "round", "facecolor": "#303030", "alpha": 0.9, "pad": 0.35}) plt.savefig("pi_estimate.png", dpi=200)