codex/mark-recapture-overlap-schematic.py

mark-recapture-overlap-schematic.py
"""Draw exactly 40 distinct observed individuals in two overlapping sample sets.

Tested with Python 3.14.4 and Matplotlib 3.10.7 (Debian +dfsg1 build).
Run from _media; write an opaque white PNG with this script's basename to CWD.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch


def main():
    first = set(range(1, 21))
    second = set(range(16, 41))
    assert (len(first), len(second), len(first & second), len(first | second)) == (20, 25, 5, 40)
    groups = (
        (sorted(first - second), (0.65, 1.75, 2.85), "#24669b"),
        (sorted(first & second), (4.15,), "#754a9f"),
        (sorted(second - first), (5.45, 6.55, 7.65, 8.75), "#168173"),
    )
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(9.6, 5.2), dpi=100)
    ax.add_patch(FancyBboxPatch((0.0, 0.35), 4.6, 6.0, boxstyle="round,pad=0.12",
                              facecolor="#edf5fc", edgecolor="#24669b", linewidth=2))
    ax.add_patch(FancyBboxPatch((3.7, 0.35), 5.65, 6.0, boxstyle="round,pad=0.12",
                              facecolor="#eaf7f3", edgecolor="#168173", linewidth=2))
    ax.add_patch(FancyBboxPatch((3.7, 0.35), 0.9, 6.0, boxstyle="round,pad=0.12",
                              facecolor="#f1eafb", edgecolor="none"))
    # Each identifier occupies one position, including the five shared identifiers.
    plotted_ids = []
    for ids, columns, color in groups:
        positions = [(x, y) for y in (5, 4, 3, 2, 1) for x in columns]
        assert len(ids) == len(positions)
        ax.scatter([x for x, y in positions], [y for x, y in positions],
                   s=150, color=color, edgecolor="white", linewidth=0.8, zorder=4)
        plotted_ids.extend(ids)
    assert len(plotted_ids) == len(set(plotted_ids)) == 40
    assert sum(len(collection.get_offsets()) for collection in ax.collections) == 40
    ax.text(2.3, 7.05, "First sample A: M = 20", ha="center", color="#24669b", weight="bold")
    ax.text(6.7, 7.05, "Second sample B: C = 25", ha="center", color="#168173", weight="bold")
    ax.text(1.75, 5.85, "First only: 15", ha="center", color="#24669b")
    ax.text(4.15, 5.82, "Shared\nR = 5", ha="center", color="#754a9f", fontsize=10)
    ax.text(7.1, 5.85, "Second only: 20", ha="center", color="#168173")
    ax.text(4.65, -0.25, "Distinct observed individuals: 15 + 5 + 20 = 40", ha="center", weight="bold")
    ax.set(xlim=(-0.3, 9.7), ylim=(-0.75, 7.5))
    ax.axis("off")
    fig.suptitle("Mark and recapture: overlap carries information", y=0.97, fontsize=16, weight="bold")
    fig.text(0.5, 0.035, "One dot = one individual.  Unobserved individuals are not drawn.  Constructed toy counts.",
             ha="center", fontsize=11)
    fig.subplots_adjust(left=0.045, right=0.98, top=0.87, bottom=0.13)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()