codexpoisson-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()