codexbayesian-credible-interval.py
bayesian-credible-interval.py
"""Plot two exact-CDF beta credible intervals; tested with Python 3.14.4.
Only NumPy, Matplotlib, and the standard library are required. The CDF is
the analytically integrated polynomial density, evaluated in a positive
Bernstein basis to avoid cancellation near one. Quantiles use bisection,
not SciPy or integration of a sampled plot. Output is written to cwd.
"""
from math import comb, fsum
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def posterior_density(theta):
return 2860 * theta**9 * (1 - theta)**3
def posterior_cdf(theta):
"""CDF of Beta(10,4), equal to 286t^10-780t^11+715t^12-220t^13.
The binomial-tail polynomial has derivative 2860t^9(1-t)^3
and is zero at t=0, so it is the same antiderivative.
"""
if not 0 <= theta <= 1:
raise ValueError("theta must lie in [0, 1]")
return fsum(comb(13, j) * theta**j * (1 - theta)**(13 - j)
for j in range(10, 14))
def posterior_quantile(probability):
"""Invert the exact polynomial CDF by 60 bisection steps."""
if not 0 <= probability <= 1:
raise ValueError("probability must lie in [0, 1]")
if probability in (0, 1):
return float(probability)
low, high = 0.0, 1.0
for _ in range(60):
middle = (low + high) / 2
if posterior_cdf(middle) < probability:
low = middle
else:
high = middle
return (low + high) / 2
def main():
plt.rcParams.update({"font.size": 12, "axes.titlesize": 13})
lower = posterior_quantile(0.025)
upper = posterior_quantile(0.975)
one_sided = posterior_quantile(0.95)
theta = np.linspace(0, 1, 2001)
fig, axes = plt.subplots(1, 2, figsize=(11, 5.1), dpi=120,
facecolor="white", sharey=True)
intervals = [(lower, upper, "Equal-tailed 95% interval",
"2.5% in each excluded tail"),
(0, one_sided, "One-sided 95% upper bound",
"5% in the excluded right tail")]
for ax, (left, right, title, tail_note) in zip(axes, intervals):
ax.plot(theta, posterior_density(theta), color="#007c68", lw=2.6)
inside = np.linspace(left, right, 1001)
ax.fill_between(inside, posterior_density(inside),
color="#56b4a4", alpha=0.5)
for endpoint in (left, right):
ax.vlines(endpoint, 0, posterior_density(endpoint),
color="#005848", linestyle="--", lw=1.5)
ax.text(0.04, 0.94, f"[{left:.6f}, {right:.6f}]\n{tail_note}",
transform=ax.transAxes, fontsize=11, va="top")
ax.text(0.61, 1.15, "Shaded area\n= 0.95", fontsize=12, ha="center")
ax.set(xlim=(0, 1), ylim=(0, 4.1), title=title,
xlabel="Unknown heads probability θ")
ax.grid(color="#e6eaed")
ax.spines[["top", "right"]].set_visible(False)
axes[0].set_ylabel("Posterior probability density")
fig.suptitle("Same Beta(10, 4) posterior; two different 95% ranges", fontsize=16)
fig.text(0.5, 0.035,
"Endpoints solve the exact polynomial CDF. "
"Both shaded regions contain 95% posterior probability.",
ha="center", fontsize=11)
fig.subplots_adjust(left=0.07, right=0.975, bottom=0.17,
top=0.79, wspace=0.14)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
facecolor="white", transparent=False)
plt.close(fig)
print(f"Equal-tailed: [{lower:.12f}, {upper:.12f}], "
f"mass={posterior_cdf(upper) - posterior_cdf(lower):.15f}")
print(f"One-sided: [0, {one_sided:.12f}], "
f"mass={posterior_cdf(one_sided):.15f}")
if __name__ == "__main__":
main()