codexweighted-centering-oblique-projection.py
weighted-centering-oblique-projection.py
"""Weighted centering versus Euclidean orthogonal projection; Python 3.14.
Run through Make from the mirrored _media working directory. The points are
computed from the defining matrices rather than positioned for appearance.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
a = np.array([0.25, 0.75])
x = np.array([4.0, 2.0])
centered = x - np.ones(2) * (a @ x)
orthogonal = x - a * ((a @ x) / (a @ a))
assert np.allclose(centered, [1.5, -0.5])
assert np.allclose(orthogonal, [3, -1])
fig, ax = plt.subplots(figsize=(7, 5), dpi=100, facecolor='white')
ax.set_facecolor('white')
u = np.linspace(-0.4, 4.9, 200)
ax.plot(u, -u / 3, color='#25734a', lw=2)
ax.axhline(0, color='#bbc1c8', lw=0.8)
ax.axvline(0, color='#bbc1c8', lw=0.8)
ax.annotate('', xy=centered, xytext=x,
arrowprops={'arrowstyle': '-|>', 'color': '#c45e25', 'lw': 2.4})
ax.annotate('', xy=orthogonal, xytext=x,
arrowprops={'arrowstyle': '-|>', 'color': '#7b8491', 'lw': 1.5,
'linestyle': '--'})
ax.scatter(*x, s=50, color='#265fa2', zorder=4)
ax.scatter(*centered, s=55, color='#25734a', zorder=4)
ax.scatter(*orthogonal, s=45, facecolor='white', edgecolor='#7b8491', zorder=4)
ax.annotate(r'$x=(4,2)$', x, xytext=(10, 8), textcoords='offset points', fontsize=12)
ax.annotate(r'$P_a x=(1.5,-0.5)$', centered, xytext=(-105, -24),
textcoords='offset points', color='#25734a', fontsize=11)
ax.annotate('Euclidean orthogonal\nprojection (3, -1)', orthogonal,
xytext=(14, -42), textcoords='offset points', color='#606975', fontsize=10)
ax.text(1.05, 1.65, 'Centering subtracts\nthe same value\nfrom both coordinates',
fontsize=10, color='#a64e1e')
ax.text(3.75, -0.82, r'$a^T y=0$', color='#25734a', fontsize=12)
ax.set_title('Weighted centering follows the constant-vector direction\n'
r'$a=(1/4,3/4),\quad a^T x=2.5$', fontsize=13, pad=14)
ax.set_xlabel('First coordinate')
ax.set_ylabel('Second coordinate')
ax.set_xlim(-0.6, 5.1)
ax.set_ylim(-2.0, 3.0)
ax.set_aspect('equal')
ax.set_xticks(range(0, 6))
ax.set_yticks(range(-2, 4))
ax.spines[['top', 'right']].set_visible(False)
fig.tight_layout()
fig.savefig(Path(__file__).with_suffix('.png').name, dpi=100, facecolor='white')
plt.close(fig)
if __name__ == '__main__':
main()