codex/bernoulli-likelihood-and-log.py

bernoulli-likelihood-and-log.py
"""Plot likelihood comparisons for ten Bernoulli trials; Python 3.14."""

from math import comb
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    p = np.linspace(0, 1, 2001)
    colors = {2: "#007c83", 5: "#7460a8", 8: "#c4511c"}
    fig, axes = plt.subplots(1, 2, figsize=(10.6, 4.7), dpi=100,
                             facecolor="white")
    for k, color in colors.items():
        peak = k / 10
        raw = p**k * (1 - p)**(10 - k)
        peak_height = peak**k * (1 - peak)**(10 - k)
        relative = raw / peak_height
        with np.errstate(divide="ignore"):
            difference = k * np.log(p / peak) + (10 - k) * np.log(
                (1 - p) / (1 - peak))

        # Verify the plotted log comparison against the direct product, including
        # zero-height boundaries, rather than against another log formula.
        assert np.allclose(np.exp(difference), relative, rtol=1e-12, atol=1e-14)
        assert np.isclose(p[np.argmax(raw)], peak)
        assert raw[0] == raw[-1] == 0
        axes[0].plot(p, relative, color=color, lw=2.5,
                     label=f"k = {k}: fitted p = {peak:.1f}")
        axes[1].plot(p, difference, color=color, lw=2.5)
        for ax, height in zip(axes, (1, 0)):
            ax.plot(peak, height, "o", color=color, ms=5)
            ax.axvline(peak, color=color, lw=0.9, alpha=0.5, linestyle=":")

    assert comb(10, 8) == 45
    assert np.isclose(0.8**8 * 0.2**2, 0.0067108864)
    assert np.isclose(comb(10, 8) * 0.8**8 * 0.2**2, 0.301989888)
    assert np.isclose(np.trapezoid(p**8 * (1 - p)**2, p), 1 / 495,
                      rtol=1e-9, atol=1e-12)
    # All-failure/all-success samples have boundary maxima on the closed domain.
    assert p[np.argmax((1 - p)**10)] == 0
    assert p[np.argmax(p**10)] == 1

    axes[0].set(title="Relative likelihood: peak height = 1",
                ylabel="L(p) / L(fitted p)", ylim=(0, 1.08))
    axes[1].set(title="Log-likelihood difference: peak = 0",
                ylabel="ℓ(p) − ℓ(fitted p)", ylim=(-12, 0.8))
    axes[0].axhline(np.exp(-2), color="#777777", lw=1, linestyle="--")
    axes[1].axhline(-2, color="#777777", lw=1, linestyle="--")
    axes[0].text(0.98, np.exp(-2) + 0.02, "e⁻² ≈ 0.1353", ha="right",
                 fontsize=10, color="#555555")
    axes[1].text(0.98, -1.7, "−2 corresponds to e⁻²", ha="right",
                 fontsize=10, color="#555555")
    handles, labels = axes[0].get_legend_handles_labels()
    fig.legend(handles, labels, loc="upper center", bbox_to_anchor=(0.5, 0.91),
               ncol=3, fontsize=9.5, frameon=False)
    for ax in axes:
        ax.set(xlim=(0, 1), xlabel="Unknown success probability p")
        ax.set_xticks(np.linspace(0, 1, 6))
        ax.grid(color="#e7e7e7", linewidth=0.7)
        ax.spines[["top", "right"]].set_visible(False)
        ax.set_facecolor("white")
        ax.title.set_fontsize(11)
    fig.suptitle("Ten independent Bernoulli trials: k = 2, 5, or 8 successes",
                 fontsize=13, y=0.97)
    fig.text(0.5, 0.08,
             "Neither vertical axis is a probability density over p. "
             "Ordered data and count data give the same rescaled curves.",
             ha="center", fontsize=10)
    fig.text(0.5, 0.035,
             "At p = 0 and p = 1: L = 0 and ℓ = −∞ "
             "(outside the right panel's finite range).",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=0.075, right=0.98, bottom=0.23, top=0.77,
                        wspace=0.25)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()