codex/central-limit-distributions.py

central-limit-distributions.py
"""Generate central-limit-distributions.png in the caller's working directory.

Tested with Python 3.14.4, numpy 2.3.5, matplotlib 3.10.7.
Uses exact binomial probabilities (successive convolutions), not simulations.
Run from _media with MPLCONFIGDIR set to a writable temporary directory.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def binomial_probabilities(n, p):
    probabilities = np.array([1.0])
    for _ in range(n):
        probabilities = np.convolve(probabilities, [1 - p, p])
    return probabilities


def main():
    p = 0.2
    grid = np.linspace(-4, 5, 700)
    normal_density = np.exp(-grid**2 / 2) / np.sqrt(2 * np.pi)
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axes = plt.subplots(2, 2, figsize=(10, 6.7), dpi=120,
                             facecolor="white", sharex=True, sharey=True)
    for ax, n in zip(axes.flat, [1, 5, 25, 100]):
        probabilities = binomial_probabilities(n, p)
        counts = np.arange(n + 1)
        scale = np.sqrt(n * p * (1 - p))
        positions = (counts - n * p) / scale
        spacing = 1 / scale
        assert np.isclose(probabilities.sum(), 1)
        assert np.isclose(np.dot(positions, probabilities), 0, atol=1e-12)
        assert np.isclose(np.dot(positions**2, probabilities), 1)
        ax.bar(positions, probabilities / spacing, width=spacing,
               color="#2b7bba", alpha=0.7, edgecolor="white", linewidth=0.5,
               label="Exact mass / lattice spacing")
        ax.plot(grid, normal_density, color="#b24a23", lw=2.3,
                label="Standard normal density")
        ax.set(title=f"n = {n} trials", xlim=(-4, 5), ylim=(0, 0.48))
        ax.grid(axis="y", alpha=0.18)
    for ax in axes[-1]:
        ax.set_xlabel(r"Standardized count $z = (k - np)/\sqrt{np(1-p)}$")
    for ax in axes[:, 0]:
        ax.set_ylabel("Probability per unit z")
    handles, labels = axes[0, 0].get_legend_handles_labels()
    fig.legend(handles, labels, loc="lower center", ncol=2, frameon=False,
               bbox_to_anchor=(0.5, 0.025))
    fig.suptitle("Central limit theorem: skewed Bernoulli trials, p = 0.2",
                 fontsize=15, y=0.98)
    fig.text(0.5, 0.005,
             "Each blue rectangle has area P(K = k); every panel has mean 0 and variance 1.",
             ha="center", fontsize=10)
    fig.tight_layout(rect=(0, 0.085, 1, 0.95))
    fig.savefig(Path.cwd() / "central-limit-distributions.png",
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()