codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2005/iii/paper-46-manova-geometry.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2005/iii/paper-46-manova-geometry.py
"""Original MANOVA geometry; Python 3.14.4 / NumPy 2.3.5 / Matplotlib 3.10.7.
Output the basename PNG to CWD; respect the caller's MPLCONFIGDIR.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse


def main():
    covariance = np.array([[1.0, 0.94], [0.94, 1.0]])
    means = [np.array([-0.35, 0.35]), np.array([0.35, -0.35])]
    colors = ["#2369a5", "#cd6325"]
    values, vectors = np.linalg.eigh(covariance)
    major = vectors[:, -1]
    angle = np.degrees(np.arctan2(major[1], major[0]))
    fig, axes = plt.subplots(1, 3, figsize=(12, 4.5), dpi=100, facecolor="white", constrained_layout=True)
    for mean, color, name in zip(means, colors, ["Group A", "Group B"]):
        axes[0].add_patch(Ellipse(mean, 2*np.sqrt(values[-1]), 2*np.sqrt(values[0]), angle=angle, facecolor=color, alpha=0.14, edgecolor=color, lw=2))
        axes[0].plot(*mean, "o", color=color, label=name)
    axes[0].set(xlim=(-1.8, 1.8), ylim=(-1.8, 1.8), xlabel="Response 1", ylabel="Response 2", title="One-SD covariance contours")
    axes[0].set_aspect("equal")
    axes[0].legend(loc="upper left", fontsize=9)
    projections = [np.array([1., 0.]), np.array([1., -1.])/np.sqrt(2)]
    for ax, direction, title in zip(axes[1:], projections, ["Overlapping marginal response", "A low-variance joint contrast"]):
        variance = direction @ covariance @ direction
        grid = np.linspace(-3, 3, 900)
        for mean, color in zip(means, colors):
            projected_mean = mean @ direction
            density = np.exp(-(grid-projected_mean)**2/(2*variance))/np.sqrt(2*np.pi*variance)
            ax.plot(grid, density, color=color, lw=2)
        ax.set(xlabel="Response 1" if ax is axes[1] else r"$(X_1-X_2)/\sqrt{2}$", ylabel="Density", title=title)
        ax.spines[["top", "right"]].set_visible(False)
    fig.suptitle("Joint mean separation depends on covariance geometry", fontsize=15)
    fig.savefig(Path.cwd() / "paper-46-manova-geometry.png", facecolor="white", transparent=False)
    plt.close(fig)

if __name__ == "__main__":
    main()