codex/student-t-small-sample-confidence-interval.py

student-t-small-sample-confidence-interval.py
"""Original chosen-data illustration of a normal mean t interval; PNG in CWD.

Assumes iid normal sampling with unknown population standard deviation.
The five displayed values are chosen for arithmetic, not real measurements.
Tested with Python 3.14, matplotlib 3.10.7. Uses no SciPy.
"""
from math import sqrt
from pathlib import Path
from statistics import NormalDist, mean, variance

import matplotlib.pyplot as plt


def t4_cdf(t):
    a = t / sqrt(4 + t * t)
    return 0.5 + 0.75 * a - 0.25 * a ** 3


def t4_quantile(p):
    if not 0 < p < 1:
        raise ValueError("probability must lie strictly between zero and one")
    lo, hi = -1.0, 1.0
    while t4_cdf(lo) > p:
        lo *= 2
    while t4_cdf(hi) < p:
        hi *= 2
    for _ in range(100):
        mid = (lo + hi) / 2
        if t4_cdf(mid) < p:
            lo = mid
        else:
            hi = mid
    return (lo + hi) / 2


def main():
    chosen = [8, 9, 10, 11, 12]
    n = len(chosen)
    average = mean(chosen)
    s2 = variance(chosen)
    se = sqrt(s2 / n)
    critical = t4_quantile(0.975)
    t_margin = critical * se
    z_margin = NormalDist().inv_cdf(0.975) * se
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False, "axes.spines.right": False})
    fig, axes = plt.subplots(2, 1, figsize=(10, 5.6), sharex=True, height_ratios=[1, 1.65], layout="constrained", facecolor="white")
    upper, lower = axes
    upper.scatter(chosen, [0] * n, s=85, color="#444444", zorder=3)
    for value in chosen:
        upper.annotate(str(value), (value, 0), xytext=(0, 14), textcoords="offset points", ha="center")
    upper.set_yticks([0], ["Chosen values"])
    upper.set_ylim(-0.45, 0.65)
    upper.set_title(r"$n=5,\quad\bar{x}=10,\quad s^2=2.5,\quad s/\sqrt{n}=\sqrt{0.5}$", fontsize=13)
    for y, margin, color, label, style in [(1, t_margin, "#245c9c", "95% t interval (df = 4)", "-"), (0, z_margin, "#b14628", "Normal critical value\nwith estimated SE", "--")]:
        left, right = average - margin, average + margin
        lower.plot([left, right], [y, y], color=color, lw=4, ls=style, solid_capstyle="butt")
        lower.scatter([left, right], [y, y], color=color, marker="|", s=240, linewidths=2)
        lower.scatter([average], [y], color=color, s=50, zorder=4)
        lower.annotate(f"[{left:.6f}, {right:.6f}]", (average, y), xytext=(0, 14), textcoords="offset points", ha="center", color=color)
    lower.set_yticks([1, 0], ["95% t interval (df = 4)", "Normal critical value\nwith estimated SE"])
    lower.set_ylim(-0.65, 1.7)
    lower.set_xlim(7.5, 12.5)
    lower.set_xticks([8, 9, 10, 11, 12])
    lower.axvline(average, color="#999999", lw=1, ls=":", zorder=0)
    lower.grid(axis="x", alpha=0.2)
    lower.set_xlabel("Population mean being estimated (same units as chosen values)")
    lower.text(0.5, 0.01, f"t margin = {t_margin:.6f}; normal margin = {z_margin:.6f} (too small)", transform=lower.transAxes, ha="center", fontsize=10)
    fig.suptitle("Small normal sample: unknown spread requires a wider interval", fontsize=15)
    fig.supxlabel("Chosen arithmetic example; exact 95% coverage assumes iid normal sampling with unknown σ.", fontsize=10)
    fig.savefig(Path(__file__).stem + ".png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()