Skip to content
← Back to the journal

Experiment / Example essay

Estimating π with random points

Turn random points into an estimate of a familiar constant, then explore how sample size changes the result.

Download Python notebook ↓

Saved results.

A circle hiding in a square

Imagine a square of side length 1, with a quarter of a unit circle tucked into one corner. The square has area 1; the quarter circle has area π/4.

If we scatter points uniformly across the square, the fraction inside the quarter circle estimates π/4. Multiply that fraction by 4, and we get an estimate of π. A point is inside when x² + y² ≤ 1.

PYTHON / Saved In [1]
import math
import random


def simulate_pi(sample_size, seed):
    if type(sample_size) is not int or not 100 <= sample_size <= 100_000 or sample_size % 100:
        raise ValueError("Sample count must be 100 to 100,000 in steps of 100.")
    if type(seed) is not int or not 0 <= seed <= 2_147_483_647:
        raise ValueError("Seed must be a whole number from 0 to 2,147,483,647.")
    rng = random.Random(seed)
    points = [(rng.random(), rng.random()) for _ in range(sample_size)]
    inside = sum(x * x + y * y <= 1 for x, y in points)
    return points, 4 * inside / sample_size


def print_summary(sample_size, seed, estimate):
    print(f"Points: {sample_size:,}")
    print(f"Seed: {seed}")
    print(f"Estimate of pi: {estimate:.6f}")
    print(f"Absolute error: {abs(estimate - math.pi):.6f}")


# Saved defaults. The widgets below pass their values directly to simulate_pi.
sample_size = 10_000
seed = 42
points, estimate = simulate_pi(sample_size, seed)
print_summary(sample_size, seed, estimate)
Points: 10,000
Seed: 42
Estimate of pi: 3.126000
Absolute error: 0.015593

Make the experiment visible

Blue points fall inside the quarter circle; orange points fall outside it. The figure shows up to the first 800 sampled points so that the pattern stays readable. The estimate above uses every point.

PYTHON / Saved In [2]
from IPython.display import SVG, display


def plot_points(points):
    # Draw up to the first 800 points using only Python's standard library.
    # Blue: inside the quarter circle. Orange: outside.
    parts = [
        '<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480">',
        '<rect width="480" height="480" fill="#f5f5f7"/>',
        '<rect x="40" y="40" width="400" height="400" fill="white" stroke="#9a9aa0"/>',
    ]
    for x, y in points[:800]:
        color = "#0071e3" if x * x + y * y <= 1 else "#c85c31"
        parts.append(
            f'<circle cx="{40 + 400 * x:.2f}" cy="{440 - 400 * y:.2f}" '
            f'r="2" fill="{color}" opacity="0.7"/>'
        )
    parts.append(
        '<path d="M 40 40 A 400 400 0 0 1 440 440" fill="none" stroke="#1d1d1f" stroke-width="2"/>'
    )
    parts.append(
        '<text x="240" y="470" text-anchor="middle" font-family="sans-serif" '
        'font-size="14">x: 0 to 1</text>'
    )
    parts.append(
        '<text x="15" y="240" text-anchor="middle" font-family="sans-serif" '
        'font-size="14" transform="rotate(-90 15 240)">y: 0 to 1</text>'
    )
    parts.append("</svg>")
    svg = "".join(parts)
    return SVG(svg)


display(plot_points(points))
Blue and orange random points inside and outside a quarter circle

What changes with more points?

Choose a sample count and a whole-number random seed, then press Run simulation. Use the same settings to repeat an experiment. The controls below are defined in this notebook using ipywidgets. On the website, use Run all to connect the saved widgets to Python. In Jupyter, run all cells to create the same controls.

Try a larger sample and compare the absolute error. Randomness means the error can go up at an individual step. Across repeated experiments, the typical error decreases roughly in proportion to 1/√n, so reducing that typical error by half takes about four times as many points.

PYTHON / Saved In [3]
# Compare sample counts with the selected seed.
for n in (100, 1_000, 10_000, 100_000):
    _, value = simulate_pi(n, seed)
    print(f"n={n:>7,}   pi≈{value:.5f}   error={abs(value - math.pi):.5f}")
n=    100   pi≈3.12000   error=0.02159
n=  1,000   pi≈3.12800   error=0.01359
n= 10,000   pi≈3.12600   error=0.01559
n=100,000   pi≈3.13728   error=0.00431
PYTHON / Saved In [4]
import ipywidgets as widgets

# Dispose of an earlier panel if this cell is run again.
for previous_widget in globals().get("simulation_widgets", []):
    previous_widget.close()

sample_slider = widgets.IntSlider(
    value=10_000, min=100, max=100_000, step=100,
    description="Samples", continuous_update=False, readout_format=",d",
    layout=widgets.Layout(width="100%"),
)
seed_input = widgets.Text(value="42", description="Seed")
run_button = widgets.Button(description="Run simulation", button_style="primary")
simulation_output = widgets.Output()
simulation_note = widgets.Label(value="Saved results above: 10,000 points, seed 42.")
last_widget_parameters = (10_000, 42)
widget_results_live = False


def read_widget_parameters():
    seed_text = seed_input.value.strip()
    if not seed_text.isascii() or not seed_text.isdigit() or not 0 <= int(seed_text) <= 2_147_483_647:
        raise ValueError("Enter a whole-number seed from 0 to 2,147,483,647.")
    return sample_slider.value, int(seed_text)


def update_simulation_note(change=None):
    try:
        current_parameters = read_widget_parameters()
    except ValueError as error:
        simulation_note.value = str(error)
        run_button.disabled = True
        return
    run_button.disabled = False
    if current_parameters != last_widget_parameters:
        simulation_note.value = "Controls have changed; run simulation to apply them."
    else:
        count, selected_seed = last_widget_parameters
        label = "Live results" if widget_results_live else "Saved results above"
        simulation_note.value = f"{label}: {count:,} points, seed {selected_seed}."


def run_simulation(button):
    global last_widget_parameters, widget_results_live
    run_button.disabled = sample_slider.disabled = seed_input.disabled = True
    try:
        count, selected_seed = read_widget_parameters()
        with simulation_output:
            simulation_output.clear_output(wait=True)
            new_points, new_estimate = simulate_pi(count, selected_seed)
            print_summary(count, selected_seed, new_estimate)
            display(plot_points(new_points))
            print("Sample-count comparison:")
            for n in (100, 1_000, 10_000, 100_000):
                _, value = simulate_pi(n, selected_seed)
                print(f"n={n:>7,}   pi≈{value:.5f}   error={abs(value - math.pi):.5f}")
        last_widget_parameters = (count, selected_seed)
        widget_results_live = True
        simulation_note.value = f"Live results: {count:,} points, seed {selected_seed}."
    except Exception as error:
        simulation_note.value = "Simulation failed. Run simulation to retry."
        with simulation_output:
            simulation_output.clear_output(wait=True)
            print(f"Simulation failed: {error}")
    finally:
        run_button.disabled = sample_slider.disabled = seed_input.disabled = False


sample_slider.observe(update_simulation_note, names="value")
seed_input.observe(update_simulation_note, names="value")
run_button.on_click(run_simulation)
simulation_panel = widgets.VBox([
    sample_slider, seed_input, run_button, simulation_note, simulation_output,
])
simulation_widgets = [sample_slider, seed_input, run_button, simulation_note, simulation_output, simulation_panel]
display(simulation_panel)

Saved notebook widgets. Use Run all to connect them to Python.