codex/kernel-density-bandwidth.py

kernel-density-bandwidth.py
"""Draw bandwidth detail and positive-support leakage; write the PNG to cwd.

Run from _media with: python3 ../kernel-density-bandwidth.py
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
"""

from math import erfc, sqrt
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def gaussian_kde(x, sample, bandwidth):
    displacement = (np.asarray(x)[..., None] - sample) / bandwidth
    return np.exp(-0.5 * displacement**2).mean(axis=-1) / (
        bandwidth * np.sqrt(2 * np.pi))


def main():
    sample = np.array([-1.8, -1.4, -1.1, -0.7, 0.7, 1.1, 1.4, 1.8])
    x = np.linspace(-5.5, 5.5, 6001)
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    fig, axes = plt.subplots(2, 2, figsize=(9.6, 6.5), dpi=120,
                             layout="constrained", facecolor="white")
    for ax, bandwidth, title in zip(
        axes.flat, [0.08, 0.35, 1.2],
        ["Narrow spikes at observations", "Two broad observed groups", "Groups blur together"],
    ):
        ax.plot(x, gaussian_kde(x, sample, bandwidth), color="#2878ad", lw=2.3)
        ax.scatter(sample, np.full(len(sample), -0.018), marker="|", s=100, lw=2,
                   color="#202734", clip_on=False)
        ax.set(title=f"h = {bandwidth:g}: {title}", xlim=(-5.5, 5.5), ylim=(-0.04, 0.8))
        ax.text(0.97, 0.91, "Total area = 1", transform=ax.transAxes,
                ha="right", fontsize=10)

    positive_sample = np.array([0.1, 0.4, 0.9])
    bandwidth = 0.5
    boundary_x = np.linspace(-2.2, 3, 3001)
    ax = axes[1, 1]
    ax.plot(boundary_x, gaussian_kde(boundary_x, positive_sample, bandwidth),
            color="#202734", lw=2.3)
    below_zero = np.linspace(-2.2, 0, 1401)
    ax.fill_between(below_zero, gaussian_kde(below_zero, positive_sample, bandwidth),
                    color="#c34e39", alpha=0.3)
    ax.axvline(0, color="#8a8a8a", lw=1, ls="--")
    ax.scatter(positive_sample, np.full(3, -0.018), marker="|", s=100, lw=2,
               color="#202734", clip_on=False)
    leaked_mass = sum(erfc(v / (bandwidth * sqrt(2))) / 2 for v in positive_sample) / 3
    ax.annotate(f"Below zero: {100 * leaked_mass:.2f}%", xy=(-0.35, 0.18),
                xytext=(-2.08, 0.54), fontsize=10, color="#9f3829",
                arrowprops={"arrowstyle": "->", "color": "#9f3829"})
    ax.text(0.98, 0.9, "Sample: 0.1, 0.4, 0.9\nKnown support: x >= 0",
            transform=ax.transAxes, ha="right", fontsize=10)
    ax.set(title="h = 0.5: positive data leak below zero", xlim=(-2.2, 3), ylim=(-0.04, 0.8))
    for ax in axes.flat:
        ax.set(xlabel="Outcome x", ylabel="Probability density", facecolor="white")
        ax.spines[["top", "right"]].set_visible(False)
        ax.grid(axis="y", color="#e8e8e8", lw=0.7)
    fig.savefig(Path.cwd() / "kernel-density-bandwidth.png", facecolor="white",
                transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()