codex/bernoulli-sufficient-statistic-grouped-sequences.py

bernoulli-sufficient-statistic-grouped-sequences.py
"""Group all four-trial binary samples by their sufficient sum.

Run from _media with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
The PNG is written to the working directory with this script's basename.
"""

from itertools import product
from math import comb
from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch


def main():
    groups = [[x for x in product((0, 1), repeat=4) if sum(x) == t] for t in range(5)]
    assert [len(group) for group in groups] == [comb(4, t) for t in range(5)]
    fig, ax = plt.subplots(figsize=(10, 6.2), dpi=110, facecolor="white")
    ax.set(xlim=(0, 10), ylim=(0, 6.2))
    ax.axis("off")
    ax.text(0.2, 5.94, "Sixteen sequences, five sufficient-statistic groups", fontsize=17, weight="bold")
    ax.text(0.2, 5.58, "Independent Bernoulli(p) observations; fixed sample size n = 4", fontsize=12)
    ax.text(0.2, 5.02, "Sum T", fontsize=12, weight="bold")
    ax.text(1.3, 5.02, "Possible observed arrangements", fontsize=12, weight="bold")
    ax.text(8.3, 5.02, "P(X = x | T)", fontsize=12, weight="bold")
    colors = ["#edf2f8", "#e8f2ee", "#e7eefc", "#f8f0e3", "#f5eaf1"]
    for t, group in enumerate(groups):
        y = 4.45 - 0.85 * t
        ax.add_patch(FancyBboxPatch((0.15, y - 0.29), 9.65, 0.66, boxstyle="round,pad=0.02", facecolor=colors[t], edgecolor="#d4dae2"))
        ax.text(0.5, y, str(t), fontsize=16, weight="bold", va="center")
        for j, x in enumerate(group):
            ax.text(1.4 + j * 1.12, y, "".join(map(str, x)), fontsize=15, family="monospace", va="center", bbox={"boxstyle": "round,pad=0.25", "facecolor": "white", "edgecolor": "#72849a"})
        ax.text(8.65, y, "1" if len(group) == 1 else f"1/{len(group)}", fontsize=16, va="center", ha="center")
    ax.text(0.2, 0.42, r"Group mass: $P_p(T=t)=\binom{4}{t}p^t(1-p)^{4-t}$", fontsize=13)
    ax.text(0.2, 0.04, "For 0 < p < 1, every row is possible; p changes row masses, not within-row weights.", fontsize=11)
    fig.subplots_adjust(left=0.02, right=0.99, bottom=0.04, top=0.98)
    fig.savefig(Path.cwd() / f"{Path(__file__).stem}.png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()