codex/poisson-independent-thinning-expected-counts.py

poisson-independent-thinning-expected-counts.py
"""Draw independent-mark thinning and exact model expectations.

The points and uniform marks are hand-chosen illustrations, not observations.
Run from _media. Tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    arrivals = np.array([0.35, 0.70, 1.25, 1.80, 2.15, 2.55, 2.85, 3.05, 3.70])
    marks = np.array([0.12, 0.60, 0.80, 0.40, 0.20, 0.55, 0.40, 0.90, 0.65])
    probabilities = np.where(arrivals <= 2, 0.25, 0.75)
    keep = marks <= probabilities
    green, orange, blue = "#087e70", "#bd5c22", "#245a91"
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white",
                         "axes.facecolor": "white"})
    fig = plt.figure(figsize=(10, 6.0), layout="constrained")
    grid = fig.add_gridspec(2, 1, height_ratios=[2.7, 1.4])
    ax, table = fig.add_subplot(grid[0]), fig.add_subplot(grid[1])
    fig.suptitle("Independent thinning with a changing retention probability", fontsize=17)
    ax.set_title("Synthetic times and marks — not measured data", fontsize=12, pad=34)
    for a, b, text in [(0, 2, "$p(s)=1/4$"), (2, 4, "$p(s)=3/4$")]:
        ax.text((a+b)/2, 3.70, text, ha="center", fontsize=13)
    ax.axvspan(0, 2, color="#f1f5f8", zorder=0)
    ax.axvline(2, color="#999999", linestyle=":", linewidth=1)
    rows = [(3, "Original: 9", arrivals, blue),
            (1.8, "Retained: 5", arrivals[keep], green),
            (0.6, "Rejected: 4", arrivals[~keep], orange)]
    for y, label, times, color in rows:
        ax.hlines(y, 0, 4, color="#bbbbbb", linewidth=1)
        ax.scatter(times, np.full(len(times), y), color=color, s=52, zorder=4)
    for t, u, retained in zip(arrivals, marks, keep):
        y = 1.8 if retained else 0.6
        color = green if retained else orange
        ax.annotate("", (t, y+0.08), (t, 2.90),
                    arrowprops={"arrowstyle": "->", "color": color, "lw": 1.3})
        ax.text(t, 3.18, f"{u:.2f}", ha="center", fontsize=10)
    ax.text(0.02, 3.48, r"Uniform marks $U_i$ (retain if $U_i\leq p(T_i)$)", fontsize=10)
    ax.set(xlim=(0, 4), ylim=(0.25, 3.96), xticks=np.arange(5), xlabel="Time (minutes)",
           yticks=[r[0] for r in rows], yticklabels=[r[1] for r in rows])
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.tick_params(axis="y", length=0, pad=10)
    table.axis("off")
    table.set_title("Exact expected counts: rate 2 per minute", fontsize=13, pad=9)
    cells = [["Original", "4", "4", "8"],
             ["Retained", r"$2\cdot2\cdot\frac{1}{4}=1$", r"$2\cdot2\cdot\frac{3}{4}=3$", "4"],
             ["Rejected", r"$2\cdot2\cdot\frac{3}{4}=3$", r"$2\cdot2\cdot\frac{1}{4}=1$", "4"]]
    tab = table.table(cellText=cells,
                      colLabels=["Stream", "0–2 minutes", "2–4 minutes", "0–4 minutes"],
                      cellLoc="center", colWidths=[0.19, 0.29, 0.29, 0.23], bbox=[0, 0, 1, 0.92])
    tab.auto_set_font_size(False)
    tab.set_fontsize(12)
    for (row, col), cell in tab.get_celld().items():
        cell.set_edgecolor("#d0d0d0")
        if row == 0:
            cell.set_facecolor("#f1f5f8")
        if col == 0 and row > 0:
            cell.get_text().set_color([blue, green, orange][row-1])
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()