codex/tail-probabilities-die-endpoints.py

tail-probabilities-die-endpoints.py
"""Exact fair-die atoms and upper tails; Python 3.14 / NumPy 2.3 / Matplotlib 3.10.

Run from _media. Closed dots include endpoints; white dots exclude them.
All probabilities are analytic model values, rather than measurements.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white"})
    blue, orange, grey = "#236a9e", "#bd5b15", "#b5bcc3"
    fig, axes = plt.subplots(1, 3, figsize=(11.4, 4.2), layout="constrained")
    fig.suptitle("Fair die: an endpoint carries probability 1/6", fontsize=16)
    values = np.arange(1, 7)
    ax = axes[0]
    for value in values:
        color = orange if value >= 4 else grey
        ax.vlines(value, 0, 1 / 6, color=color, linewidth=3)
        ax.plot(value, 1 / 6, "o", color=color, markersize=7)
    ax.axvline(4, color=orange, linestyle=":", linewidth=1.5)
    ax.text(3.5, 0.245, "At threshold 4:\n3 atoms included", ha="center",
            bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})
    ax.set(title="Probability mass", ylabel=r"$P(D=d)$", ylim=(0, 0.31),
           xlim=(0.5, 6.5), xlabel="Die outcome d")
    ax.set_yticks([0, 1 / 6], ["0", "1/6"])

    # The two functions share all plateaus and differ only at integer thresholds.
    for ax, inclusive, color in zip(axes[1:], [True, False], [orange, blue]):
        edges = np.arange(0, 8)
        for left, right in zip(edges[:-1], edges[1:]):
            height = np.count_nonzero(values > left) / 6
            ax.hlines(height, left, right, color=color, linewidth=2.2)
        for value in values:
            upper = np.count_nonzero(values >= value) / 6
            lower = np.count_nonzero(values > value) / 6
            closed, opened = (upper, lower) if inclusive else (lower, upper)
            ax.plot(value, opened, "o", markerfacecolor="white",
                    markeredgecolor=color, markersize=7, markeredgewidth=1.5)
            ax.plot(value, closed, "o", color=color, markersize=7)
        ax.axvline(4, color=color, linestyle=":", linewidth=1.1)
        ax.set(title=r"Inclusive tail $P(D\geq t)$" if inclusive
               else r"Strict tail $P(D>t)$", xlim=(0.5, 6.5),
               ylim=(-0.045, 1.075), xlabel="Threshold t", ylabel="Probability")
        ax.set_yticks([0, 1 / 3, 1 / 2, 1], ["0", "1/3", "1/2", "1"])
        ax.annotate("1/2" if inclusive else "1/3",
                    xy=(4, 1 / 2 if inclusive else 1 / 3), xytext=(4.7, 0.81),
                    arrowprops={"arrowstyle": "->", "color": color}, color=color)
    for ax in axes:
        ax.set_xticks(values)
        ax.grid(axis="y", color="#e6e9ec", linewidth=0.7)
        ax.set_axisbelow(True)
    fig.text(0.5, -0.025, "Exact mathematical model • filled dot: value at the threshold • white dot: excluded endpoint",
             ha="center", fontsize=10)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=100,
                facecolor="white", transparent=False, bbox_inches="tight")
    plt.close(fig)


if __name__ == "__main__":
    main()