codex/pairwise-versus-joint-independence.py

pairwise-versus-joint-independence.py
"""All eight XOR patterns and all three pair marginals; Python 3.14.4.

Uses the repository's matplotlib/numpy environment. Writes the PNG to CWD.
"""

from itertools import product
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    joint = np.zeros((2, 2, 2))
    for x, y in product((0, 1), repeat=2):
        joint[x, y, x ^ y] = 0.25
    patterns = list(product((0, 1), repeat=3))
    # Check every triple, including the four impossible patterns.
    for x, y, z in patterns:
        assert joint[x, y, z] == (0.25 if z == x ^ y else 0)
    assert joint.sum() == 1
    marginals = [joint.sum(axis=axis) for axis in range(3)]
    assert all(np.array_equal(m, [0.5, 0.5]) for m in
               [joint.sum(axis=(1, 2)), joint.sum(axis=(0, 2)), joint.sum(axis=(0, 1))])
    # Each of the four cells of every pair is 1/4.
    for marginal in marginals:
        for i, j in product((0, 1), repeat=2):
            assert marginal[i, j] == 0.25
    assert joint[0, 0, 0] == 0.25 != 0.5 ** 3
    plt.rcParams.update({"font.size": 12, "text.color": "#182332", "axes.labelcolor": "#182332"})
    fig = plt.figure(figsize=(9.4, 6.5), layout="constrained", facecolor="white")
    grid = fig.add_gridspec(2, 3, height_ratios=[1.25, 1])
    ax = fig.add_subplot(grid[0, :])
    positions = np.arange(8)
    masses = [joint[state] for state in patterns]
    ax.bar(positions, masses, width=0.64, color="#287eac", zorder=3,
           label="Actual joint law: Z = X XOR Y")
    ax.axhline(0.125, color="#bd5a37", linestyle="--", linewidth=2,
               label="Product of marginals: 1/8 in every cell", zorder=4)
    for position, mass in zip(positions, masses):
        ax.text(position, mass + 0.008, "1/4" if mass else "0", ha="center", fontsize=12)
    ax.set_xticks(positions, ["".join(map(str, pattern)) for pattern in patterns])
    ax.set_yticks([0, 0.125, 0.25], ["0", "1/8", "1/4"])
    ax.set_ylim(0, 0.37)
    ax.set_xlabel("All eight (X, Y, Z) patterns")
    ax.set_ylabel("Probability")
    ax.set_title("Four allowed triples, four impossible triples", fontsize=13)
    ax.grid(axis="y", color="#e1e7ed", zorder=0)
    ax.spines[["top", "right"]].set_visible(False)
    ax.legend(loc="upper right", fontsize=10, frameon=False)
    pairs = [(joint.sum(axis=2), "X", "Y"),
             (joint.sum(axis=1), "X", "Z"),
             (joint.sum(axis=0), "Y", "Z")]
    for i, (matrix, first, second) in enumerate(pairs):
        panel = fig.add_subplot(grid[1, i])
        panel.imshow(matrix, vmin=0, vmax=0.5, cmap="Blues")
        for row, col in product((0, 1), repeat=2):
            panel.text(col, row, "1/4", ha="center", va="center", fontsize=17, color="#182332")
        panel.set_xticks([0, 1], [f"{second} = 0", f"{second} = 1"])
        panel.set_yticks([0, 1], [f"{first} = 0", f"{first} = 1"])
        panel.set_xticks([0.5], minor=True)
        panel.set_yticks([0.5], minor=True)
        panel.grid(which="minor", color="white", linewidth=2)
        panel.tick_params(which="both", length=0)
        panel.set_title(f"Pair ({first}, {second}) is independent", fontsize=11, pad=10)
        panel.set_xlabel("Each row and column sums to 1/2", fontsize=9)
    fig.suptitle("Pairwise independence does not imply joint independence", fontsize=16, weight="bold")
    fig.savefig(Path.cwd() / "pairwise-versus-joint-independence.png",
                dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()