codex/empirical-distribution-step-construction.py

empirical-distribution-step-construction.py
"""Explain empirical counting and ties; tested with Python 3.14 / Matplotlib 3.10.7."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    sample = np.array([1, 1, 2, 4, 6])
    values, counts = np.unique(sample, return_counts=True)
    cumulative = np.cumsum(counts)
    heights = cumulative / sample.size
    edges = np.r_[0, values, 7]
    blue, orange = "#1767a1", "#c36b13"
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False})
    fig, (rug, ax) = plt.subplots(
        2, 1, figsize=(9, 5.9), sharex=True,
        gridspec_kw={"height_ratios": [1, 3.5], "hspace": 0.15},
        layout="constrained", facecolor="white",
    )
    fig.suptitle("From five observations to four jumps", fontsize=17)
    rug.scatter(sample, [0.35, 0.70, 0.35, 0.35, 0.35], s=90, color=orange)
    rug.text(1.2, 0.70, "two observations at 1", va="center", color=orange)
    rug.text(4.7, 0.72, "sample: [1, 1, 2, 4, 6]", va="center")
    rug.set_ylim(0, 1)
    rug.set_yticks([])
    rug.set_ylabel("Observed\nvalues", rotation=0, ha="right", va="center")
    rug.spines[["left", "right", "bottom"]].set_visible(False)
    rug.tick_params(axis="x", bottom=False)
    levels = np.r_[0, heights]
    for left, right, height in zip(edges[:-1], edges[1:], levels):
        ax.hlines(height, left, right, color=blue, lw=2.7)
    previous = np.r_[0, heights[:-1]]
    ax.vlines(values, previous, heights, color=blue, alpha=0.45, linestyle=":")
    ax.scatter(values, previous, s=65, facecolor="white", edgecolor=blue, zorder=4)
    ax.scatter(values, heights, s=65, color=blue, zorder=5)
    ax.annotate("tie: one jump of 2/5", xy=(1, 0.2), xytext=(2.2, 0.12),
                arrowprops={"arrowstyle": "->", "color": orange}, color=orange)
    ax.annotate(r"$F_5(3)=3/5$", xy=(3, 0.6), xytext=(3.15, 0.40),
                arrowprops={"arrowstyle": "->", "color": blue}, color=blue)
    ax.scatter([3], [0.6], marker="x", s=60, color=blue, zorder=6)
    ax.text(0.03, 0.96, "filled = value at the jump\nopen = excluded height",
            transform=ax.transAxes, va="top", fontsize=10,
            bbox={"facecolor": "white", "edgecolor": "#dddddd", "pad": 6})
    ax.set_xlim(0, 7)
    ax.set_ylim(-0.045, 1.08)
    ax.set_xticks(range(8))
    ax.set_yticks(np.arange(6) / 5, ["0", "1/5", "2/5", "3/5", "4/5", "1"])
    ax.set_xlabel(r"Threshold $x$ (count observations $\leq x$)")
    ax.set_ylabel(r"Empirical fraction $F_5(x)$")
    ax.grid(axis="y", alpha=0.2)
    count_ax = ax.secondary_yaxis("right", functions=(lambda y: 5*y, lambda y: y/5))
    count_ax.set_yticks(range(6))
    count_ax.set_ylabel("Cumulative count")
    fig.savefig(Path(__file__).stem + ".png", dpi=140, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()