codex/random-walk-paths-and-drift.py

random-walk-paths-and-drift.py
"""Illustrate unit-step random walks; tested with Python 3.14, NumPy/Matplotlib.

Run through make _media/random-walk-paths-and-drift.png. Output goes to CWD.
The shaded mean +/- one standard deviation is a scale, not a coverage claim.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    rng = np.random.default_rng(1905)
    n = np.arange(81)
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    fig, axes = plt.subplots(1, 2, figsize=(10.8, 5.4), dpi=120,
                             facecolor="white", sharex=True, sharey=True)
    for ax, p, count, colors in zip(
        axes, (0.5, 0.65), (4, 2),
        (("#2274a5", "#d27727", "#43876d", "#9258a2"),
         ("#2274a5", "#d27727")),
    ):
        mean = n * (2 * p - 1)
        sigma = np.sqrt(4 * n * p * (1 - p))
        ax.fill_between(n, mean - sigma, mean + sigma, color="#dfe5ec",
                        label=r"Mean $\pm$ one SD")
        for i, color in enumerate(colors):
            steps = np.where(rng.random(80) < p, 1, -1)
            path = np.concatenate(([0], np.cumsum(steps)))
            assert np.all(np.abs(np.diff(path)) == 1)
            assert np.all((path - n) % 2 == 0)
            ax.plot(n, path, color=color, lw=1.5, alpha=0.95,
                    label=f"Path {i + 1}")
        ax.plot(n, mean, color="#182b40", ls="--", lw=2.2,
                label=r"Mean $n(2p-1)$")
        ax.set(title=f"{'Fair' if p == 0.5 else 'Biased'}: p = {p:.2f}; {count} paths",
               xlabel="Step count n", xlim=(0, 80), ylim=(-23, 46))
        ax.set_xticks([0, 20, 40, 60, 80])
        ax.grid(color="#edf0f3")
        ax.set_axisbelow(True)
        ax.spines[["top", "right"]].set_visible(False)
        ax.legend(loc="upper left", fontsize=9, frameon=False)
    axes[0].set_ylabel(r"Position $S_n$ (unit steps)")
    fig.suptitle("Independent steps accumulate into wandering paths", fontsize=16)
    fig.text(0.5, 0.045,
             "Seed 1905; illustrative paths. Shading measures spread at each n;\n"
             "it is not a confidence interval or a guarantee that a path stays inside.",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=0.075, right=0.985, bottom=0.22, top=0.83, wspace=0.13)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()