codex/quantile-generalized-inverse.py

quantile-generalized-inverse.py
"""Show generalized inverse CDFs with jump conventions; output a PNG to cwd."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def quantile_x(p):
    return np.array([0., 1., 3.])[np.searchsorted([.2, .7, 1.], p, side="left")]


def main():
    levels = np.array([.1, .5, .9])
    assert np.array_equal(quantile_x(levels), [0, 1, 3])
    assert quantile_x(.2) == 0 and quantile_x(.7) == 1
    # A deterministic uniform grid illustrates probability-band widths, not IID sampling.
    grid = (np.arange(1000) + .5) / 1000
    counts = [np.count_nonzero(quantile_x(grid) == outcome) for outcome in [0, 1, 3]]
    assert counts == [200, 500, 300]
    assert np.allclose(np.sqrt(levels)**2, levels)
    assert np.isclose(np.sqrt(.5), 1/np.sqrt(2))
    plt.rcParams.update({"font.size": 12, "axes.titlesize": 14})
    fig, (discrete, continuous) = plt.subplots(1, 2, figsize=(11, 5.5), dpi=120, facecolor="white")
    colors = ["#396c99", "#298268", "#b67a2e"]
    bands = [(0, .2, 0), (.2, .7, 1), (.7, 1, 3)]
    for (left, right, outcome), color in zip(bands, colors):
        discrete.axvspan(left, right, color=color, alpha=.08)
        discrete.plot([left, right], [outcome, outcome], color=color, lw=3)
        discrete.scatter([left], [outcome], facecolor="white", edgecolor=color, s=55, zorder=5)
        discrete.scatter([right], [outcome], facecolor=color if right < 1 else "white",
                         edgecolor=color, s=55, zorder=5)
        discrete.text((left+right)/2, 3.55, f"Width {right-left:.1f}", ha="center", color=color, fontsize=11)
    for level, output in zip(levels, quantile_x(levels)):
        discrete.plot([level, level], [-.32, output], color="#a0aeb8", linestyle=":", lw=1)
        discrete.scatter([level], [output], color="#283a46", s=42, zorder=6)
    discrete.set(xlim=(-.025, 1.025), ylim=(-.38, 3.9), title="Quantiles of the three-value law",
                 xlabel="Probability level p", ylabel=r"Outcome $Q_X(p)$")
    discrete.set_yticks([0, 1, 3])
    p = np.linspace(0, 1, 800)
    continuous.plot(p, np.sqrt(p), color="#298268", lw=3)
    for level, output, color in zip(levels, np.sqrt(levels), colors):
        continuous.plot([level, level], [0, output], color="#a0aeb8", linestyle=":", lw=1)
        continuous.scatter([level], [output], color=color, s=60, zorder=5)
    continuous.scatter([0, 1], [0, 1], facecolor="white", edgecolor="#298268", s=55, zorder=6)
    continuous.annotate(r"$Q_Y(0.5)=1/\sqrt{2}$" + "\n≈ 0.70710678", xy=(.5, np.sqrt(.5)),
                        xytext=(.04, .87), fontsize=12, color="#20614c",
                        arrowprops={"arrowstyle": "->", "color": "#20614c"},
                        bbox={"facecolor": "white", "edgecolor": "none", "alpha": .9})
    continuous.set(xlim=(-.025, 1.025), ylim=(-.04, 1.08), title=r"For $F_Y(y)=y^2$: $Q_Y(p)=\sqrt{p}$",
                   xlabel="Probability level p", ylabel=r"Outcome $Q_Y(p)$")
    for ax in (discrete, continuous):
        ax.set_xticks([0, .1, .2, .5, .7, .9, 1])
        ax.grid(color="#e6eaed")
        ax.spines[["top", "right"]].set_visible(False)
    fig.suptitle("Quantiles choose the first outcome reaching a probability level", fontsize=16, y=.985)
    fig.text(.5, .042, "A uniform draw lands in each probability band with that band's width",
             ha="center", fontsize=12)
    fig.subplots_adjust(left=.065, right=.98, bottom=.20, top=.82, wspace=.25)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()