codex/stochastic-process-paths.py

stochastic-process-paths.py
"""Generate stochastic-process-paths.png in cwd.

Tested with Python 3.14.4, numpy 2.3.5, matplotlib 3.10.7.
The discrete-time panels share Bernoulli draws; the continuous-time panel
simulates independent exponential gaps, not a discretized arrival grid.
"""

from pathlib import Path

import matplotlib

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


def main():
    rng = np.random.default_rng(20261011)
    n = 24
    bits = rng.integers(0, 2, size=(3, n))
    walks = np.column_stack((np.zeros(3, dtype=int), np.cumsum(2 * bits - 1, axis=1)))
    assert np.all(np.abs(np.diff(walks, axis=1)) == 1)
    rate, horizon = 2.0, 4.0
    arrivals = []
    for _ in range(3):
        t, events = 0.0, []
        while True:
            t += rng.exponential(1 / rate)
            if t > horizon:
                break
            events.append(t)
        arrivals.append(np.array(events))
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axes = plt.subplots(3, 1, figsize=(9.5, 8.2), dpi=120,
                             facecolor="white")
    colors = ["#2878ad", "#c05a2a", "#24805e"]
    for j, color in enumerate(colors):
        # Small offsets make coincident binary observations visible.
        axes[0].scatter(np.arange(1, n + 1), bits[j] + (j - 1) * 0.055,
                        color=color, s=25, label=f"Outcome {j + 1}")
        axes[1].plot(np.arange(n + 1), walks[j], "o-", color=color,
                     markersize=3, lw=1.3)
        times = np.concatenate(([0.0], arrivals[j], [horizon]))
        counts = np.concatenate((np.arange(len(arrivals[j]) + 1), [len(arrivals[j])]))
        axes[2].step(times, counts, where="post", color=color, lw=1.8)
    axes[0].set(title="Bernoulli process: a fresh 0 or 1 at each integer time",
                ylabel=r"$B_n$", ylim=(-0.18, 1.18), yticks=[0, 1],
                xlabel="Integer time n", xlim=(0, n + 1))
    fig.legend(*axes[0].get_legend_handles_labels(), loc="upper center",
               bbox_to_anchor=(0.5, 0.947), ncol=3, fontsize=9, frameon=False)
    axes[1].set(title=r"Random walk: accumulate the same signs $2B_n-1$",
                ylabel=r"$S_n$", xlabel="Integer time n", xlim=(0, n))
    axes[1].axhline(0, color="gray", lw=0.7)
    axes[2].set(title=r"Poisson process: count arrivals at random times ($\lambda=2$)",
                ylabel=r"$N_t$", xlabel="Continuous time t", xlim=(0, horizon))
    for ax in axes:
        ax.set_facecolor("white")
        ax.grid(alpha=0.15)
    fig.suptitle("A stochastic process describes a whole random history", fontsize=15)
    fig.text(0.5, 0.018,
             "Three simulated outcomes; seed 20261011. Binary dots are offset slightly.\n"
             "Random-walk segments connect integer observations; Poisson stairs are right-continuous.",
             ha="center", fontsize=9)
    fig.tight_layout(rect=(0, 0.055, 1, 0.92), h_pad=1.6)
    fig.savefig(Path.cwd() / "stochastic-process-paths.png", facecolor="white", transparent=False)
    plt.close(fig)
    print("Walk endpoints:", walks[:, -1].tolist())
    print("Poisson counts at t=4:", [len(x) for x in arrivals])
    print("P(S_4=0)=6/16=3/8; P(N_1=0)=exp(-2)")


if __name__ == "__main__":
    main()