codex/past-exam-of-the-individual-differences-and-clinical-psychology-course-of-the-year-2-of-the-psychology-experimental-course-of-the-university-of-oxford/2022-social-intelligence-factor-rotation.py

past-exam-of-the-individual-differences-and-clinical-psychology-course-of-the-year-2-of-the-psychology-experimental-course-of-the-university-of-oxford/2022-social-intelligence-factor-rotation.py
#!/usr/bin/env python3
"""Constructed factor-rotation illustration; outputs its PNG into the CWD.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
The loadings are illustrative values, not participant data.
"""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt


def main():
    loading = np.array([[.8, 0.0], [.6, .1], [.1, .7], [0.0, .8]])
    theta = np.deg2rad(35.0)
    rotation = np.array([[np.cos(theta), -np.sin(theta)],
                         [np.sin(theta), np.cos(theta)]])
    rotated = loading @ rotation
    residual = np.diag(1.0 - np.sum(loading**2, axis=1))
    covariance = loading @ loading.T + residual
    covariance_rotated = rotated @ rotated.T + residual
    assert np.allclose(rotation @ rotation.T, np.eye(2), atol=1e-14)
    assert np.allclose(covariance, covariance_rotated, atol=1e-14)
    assert np.linalg.eigvalsh(covariance).min() > 0
    assert np.allclose(np.diag(covariance), 1.0)
    plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 11})
    fig, axes = plt.subplots(1, 2, figsize=(8.0, 3.7), dpi=100)
    fig.patch.set_facecolor('white')
    for ax, data, title in zip(axes, [loading, rotated],
                                ['Initial factor loadings', 'After a 35° rotation']):
        ax.set_facecolor('white')
        ax.imshow(data, cmap='RdBu', vmin=-1, vmax=1, aspect='auto')
        ax.set_xticks([0, 1], ['Factor 1', 'Factor 2'])
        ax.set_yticks(range(4), [f'Indicator {j+1}' for j in range(4)])
        ax.set_title(title, fontweight='bold', pad=10)
        for (i, j), value in np.ndenumerate(data):
            ax.text(j, i, f'{value:.2f}', va='center', ha='center',
                    color='white' if abs(value) > .65 else '#172b3a', fontsize=12)
        ax.tick_params(length=0)
        for spine in ax.spines.values():
            spine.set_visible(False)
    fig.suptitle('Different axes, identical modeled covariance',
                 fontsize=15, fontweight='bold', color='#19364b', y=.96)
    fig.text(.5, .105, r'$\Lambda\Lambda^T+\Psi=(\Lambda Q)(\Lambda Q)^T+\Psi$',
             ha='center', fontsize=14, color='#19364b')
    fig.text(.5, .035, 'Constructed example: four indicators, two factors; no study data',
             ha='center', fontsize=10, color='#455e70')
    fig.subplots_adjust(left=.13, right=.98, top=.77, bottom=.23, wspace=.55)
    destination = Path.cwd() / (Path(__file__).stem + '.png')
    fig.savefig(destination, dpi=100, facecolor='white', transparent=False,
                metadata={'Software': 'Original codex-wiki matplotlib generator'})
    plt.close(fig)


if __name__ == '__main__':
    main()