codex/jensen-mean-and-spread.py

jensen-mean-and-spread.py
"""Render two exact two-point Jensen inequalities, with a fixed mean, to cwd."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    d = np.linspace(0, 2, 401)
    mean = 2.0
    square_average = ((mean - d) ** 2 + (mean + d) ** 2) / 2
    exp_average = (np.exp(mean - d) + np.exp(mean + d)) / 2
    assert np.allclose(square_average - mean**2, d**2)
    assert np.allclose(exp_average, np.exp(mean) * np.cosh(d))
    assert np.isclose(0.25 * 1**2 + 0.75 * 3**2 - (0.25 * 1 + 0.75 * 3)**2, 0.75)
    plt.rcParams.update({"font.size": 12, "axes.titlesize": 14})
    fig, axes = plt.subplots(1, 2, figsize=(10.8, 5.2), dpi=120, facecolor="white")
    curves = [(square_average, mean**2, r"$f(x)=x^2$", r"$E[f(X)]=4+d^2$"),
              (exp_average, np.exp(mean), r"$f(x)=e^x$", r"$E[f(X)]=e^2\cosh d$")]
    for ax, (average, baseline, title, label) in zip(axes, curves):
        ax.plot(d, average, color="#206a9d", lw=3, label=label)
        ax.axhline(baseline, color="#be7521", lw=2, linestyle="--",
                   label=r"$f(E[X])=f(2)$")
        ax.fill_between(d, baseline, average, color="#206a9d", alpha=0.12)
        height = float(np.interp(1, d, average))
        ax.plot([1, 1], [baseline, height], color="#2f8a68", lw=3)
        ax.scatter([1, 1], [baseline, height], color=["#be7521", "#206a9d"],
                   s=45, zorder=4)
        ax.annotate(f"At d = 1, gap = {height - baseline:.3f}",
                    xy=(1, (baseline + height) / 2), xytext=(0.08, 0.78),
                    textcoords="axes fraction", fontsize=11,
                    arrowprops={"arrowstyle": "->", "color": "#2f8a68"})
        ax.set(xlim=(0, 2), ylim=(0, max(average) * 1.17),
               title=title, xlabel="Half-spread d", ylabel="Function value / average")
        ax.legend(loc="upper left", frameon=False, fontsize=11)
        ax.grid(color="#e5e9eb")
        ax.spines[["top", "right"]].set_visible(False)
    fig.suptitle("Two equally likely inputs: 2 − d and 2 + d; their mean stays 2", fontsize=15)
    fig.subplots_adjust(left=0.065, right=0.985, top=0.84, bottom=0.13, wspace=0.24)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()