codexrandom-walk-endpoint-distributions.py
random-walk-endpoint-distributions.py
"""Exact unit-step endpoint PMFs, using math.comb rather than SciPy.
Tested with Python 3.14 and the root pyproject NumPy/Matplotlib environment.
Run through make _media/random-walk-endpoint-distributions.png; output to CWD.
"""
from math import comb, sqrt
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def endpoint_pmf(n, p):
endpoints = 2 * np.arange(n + 1) - n
masses = np.array([comb(n, b) * p**b * (1 - p)**(n - b)
for b in range(n + 1)])
mean = n * (2 * p - 1)
variance = 4 * n * p * (1 - p)
assert np.isclose(masses.sum(), 1, atol=1e-14, rtol=0)
assert np.isclose(endpoints @ masses, mean, atol=1e-12, rtol=0)
assert np.isclose((endpoints - mean)**2 @ masses, variance,
atol=1e-12, rtol=0)
assert np.all((endpoints - n) % 2 == 0)
return endpoints, masses
def main():
plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
cases = [(20, 0.5, "#2274a5"), (80, 0.5, "#43876d"),
(80, 0.65, "#d27727")]
fig, axes = plt.subplots(1, 3, figsize=(12, 5.4), dpi=120,
facecolor="white", sharey=True)
for ax, (n, p, color) in zip(axes, cases):
endpoints, masses = endpoint_pmf(n, p)
mean = n * (2 * p - 1)
variance = 4 * n * p * (1 - p)
sigma = sqrt(variance)
ax.vlines(endpoints, 0, masses, color=color, lw=1.3)
ax.scatter(endpoints, masses, color=color, s=11, zorder=3)
ax.axvline(mean, color="#182b40", ls="--", lw=1.5)
ax.plot([mean - sigma, mean + sigma], [0.196, 0.196],
color="#182b40", lw=2, marker="|", markersize=10)
ax.text(mean, 0.203, r"Mean $\pm$ one SD", ha="center", fontsize=9)
ax.text(0.04, 0.74,
f"Mean = {mean:g}\nVariance = {variance:g}\nSD = {sigma:.3f}",
transform=ax.transAxes, fontsize=10,
bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.9})
ax.set(title=f"n = {n}, p = {p:.2f}", xlabel=r"Endpoint $s$ (even integers)",
xlim=(-n - 2, n + 2), ylim=(0, 0.228))
ax.set_xticks(np.linspace(-n, n, 5, dtype=int))
ax.grid(axis="y", color="#edf0f3")
ax.set_axisbelow(True)
ax.spines[["top", "right"]].set_visible(False)
print(f"n={n}, p={p}: mass={masses.sum():.16g}, "
f"mean={endpoints @ masses:.12g}, variance={variance:.12g}, "
f"SD={sigma:.9f}, SD/n={sigma/n:.9f}, spacing/SD={2/sigma:.9f}")
axes[0].set_ylabel(r"Probability $P(S_n=s)$")
fig.suptitle("Exact endpoint probabilities: spread, drift, and parity", fontsize=16)
fig.text(0.5, 0.06,
"Full support shown: -n, -n+2, ..., n. Odd endpoints have probability zero.\n"
"Stems show binomial masses, not a continuous density or simulation histogram.",
ha="center", fontsize=10)
fig.subplots_adjust(left=0.065, right=0.985, bottom=0.22, top=0.83, wspace=0.16)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()