codexconditional-mean-residual.py
conditional-mean-residual.py
"""Show exact conditional-mean residuals; tested on Python 3.14."""
from fractions import Fraction
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
outcomes = [Fraction(x) for x in (1, 3, 5, 11)]
means = [Fraction(x) for x in (2, 2, 8, 8)]
residuals = [x - m for x, m in zip(outcomes, means)]
overall = sum(outcomes) / 4
centered = [m - overall for m in means]
products = [r * m for r, m in zip(residuals, centered)]
mse = sum(r * r for r in residuals) / 4
baseline = sum((x - overall) ** 2 for x in outcomes) / 4
assert residuals == [-1, 1, -3, 3] and sum(residuals) == 0
assert sum(residuals[:2]) == sum(residuals[2:]) == 0
assert products == [3, -3, -9, 9] and sum(products) == 0
assert mse == 5 and baseline == 14
# Unequal weights: the four probabilities are 3/8, 3/8, 1/8, 1/8.
weights = [Fraction(3, 8), Fraction(3, 8), Fraction(1, 8), Fraction(1, 8)]
shifted_mean = sum(p * x for p, x in zip(weights, outcomes))
shifted_within = sum(p * r * r for p, r in zip(weights, residuals))
shifted_between = sum(p * (m - shifted_mean) ** 2 for p, m in zip(weights, means))
shifted_total = sum(p * (x - shifted_mean) ** 2 for p, x in zip(weights, outcomes))
assert shifted_mean == Fraction(7, 2) and shifted_within == 3
assert shifted_between == Fraction(27, 4) and shifted_total == Fraction(39, 4)
assert shifted_within + shifted_between == shifted_total
print(f'residuals={residuals}; cross products={products}; covariance=0; '
f'MSE={mse}, baseline={baseline}; unequal weights: '
f'mean={shifted_mean}, within={shifted_within}, '
f'between={shifted_between}, total={shifted_total}')
plt.rcParams.update({'font.size': 11, 'axes.spines.top': False,
'axes.spines.right': False})
blue, orange = '#2878ac', '#c26916'
fig, (ax, errors) = plt.subplots(1, 2, figsize=(10, 4.5), layout='constrained',
gridspec_kw={'width_ratios': [1.25, 1]},
facecolor='white')
for y, rs, color, label in ((1, residuals[:2], blue, 'A: errors -1, +1'),
(0, residuals[2:], orange, 'B: errors -3, +3')):
values = list(map(float, rs))
ax.plot(values, [y, y], color=color, lw=4, alpha=.45)
ax.scatter(values, [y, y], color=color, s=80, zorder=3)
ax.scatter(0, y, color=color, marker='D', s=65, zorder=4)
ax.text(0, y - .23, label, ha='center', color=color)
ax.axvline(0, color='#444444', ls='--', lw=1.5)
ax.set(xlim=(-4, 4), ylim=(-.6, 1.5), xticks=np.arange(-3, 4),
yticks=[0, 1], yticklabels=['Group B', 'Group A'],
xlabel='Residual R = X - E[X | Y]', title='Both group error means are zero')
errors.bar([0, 1], [float(baseline), float(mse)], width=.55,
color=['#a4a4a4', blue])
errors.text(0, 7, '14', ha='center', va='center', fontsize=15)
errors.text(1, 2.5, '5', ha='center', va='center', color='white', fontsize=15)
errors.annotate('Gain = 9', xy=(1, 14), xytext=(1, 9), ha='center', va='center',
arrowprops={'arrowstyle': '-|>', 'color': '#555555'})
errors.set(xticks=[0, 1], xticklabels=['Predict 5', 'Predict group mean'],
ylim=(0, 16), ylabel='Mean squared prediction error',
title='Information improves prediction')
errors.set_axisbelow(True)
errors.yaxis.grid(alpha=.2)
fig.suptitle('Uncorrelated residuals can still have group-dependent spread', fontsize=14)
fig.savefig(Path.cwd() / 'conditional-mean-residual.png', dpi=120,
facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()