codex/total-variation-maximal-coupling.py

total-variation-maximal-coupling.py
"""Show the joint mass and marginals of an explicit maximal coupling."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle


def main():
    joint = np.array([[0.1, 0.2, 0.1, 0.0], [0.0, 0.1, 0.0, 0.0],
                      [0.0, 0.0, 0.2, 0.0], [0.0, 0.0, 0.0, 0.3]])
    p, q = joint.sum(axis=1), joint.sum(axis=0)
    fig, axes = plt.subplots(1, 2, figsize=(9, 4.6), layout="constrained",
                             gridspec_kw={"width_ratios": [1.25, 1]})
    fig.set_facecolor("white")
    ax = axes[0]
    ax.imshow(joint, cmap="Blues", vmin=0, vmax=0.35)
    labels = ["A", "B", "C", "D"]
    ax.set(xticks=range(4), xticklabels=labels, yticks=range(4),
           yticklabels=labels, xlabel="Y (law Q)", ylabel="X (law P)")
    ax.set_title("Joint probability mass")
    for row in range(4):
        for col in range(4):
            ax.text(col, row, f"{joint[row, col]:.1f}", ha="center", va="center",
                    color="white" if joint[row, col] >= 0.2 else "#163548", fontsize=13)
        ax.add_patch(Rectangle((row - 0.49, row - 0.49), 0.98, 0.98,
                               fill=False, edgecolor="#d4880b", linewidth=2.5))
    ax.set_xticks(np.arange(-0.5, 4), minor=True)
    ax.set_yticks(np.arange(-0.5, 4), minor=True)
    ax.grid(which="minor", color="white", linewidth=2)
    ax.tick_params(which="minor", bottom=False, left=False)
    right = axes[1]
    right.axis("off")
    statements = [
        "Row sums = P", "   (0.4, 0.1, 0.2, 0.3)",
        "Column sums = Q", "   (0.1, 0.3, 0.3, 0.3)",
        "Outlined diagonal: agreement", "   Pr(X = Y) = 0.7",
        "Off diagonal: mismatch", "   Pr(X ≠ Y) = 0.3",
        "This is the smallest possible", "mismatch for these two laws.",
    ]
    for i, line in enumerate(statements):
        right.text(0.04, 0.94 - i * 0.09, line, va="top", fontsize=12,
                   weight="bold" if i in [0, 2, 4, 6] else "normal")
    assert np.allclose(p, [0.4, 0.1, 0.2, 0.3])
    assert np.allclose(q, [0.1, 0.3, 0.3, 0.3])
    fig.suptitle("A maximal coupling retains all common probability mass", fontsize=14)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=130, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()