codexjackknife-leave-one-out.py
jackknife-leave-one-out.py
"""Draw the five deletions for a sample mean; tested with Python 3.14.4.
Dependencies: Matplotlib and NumPy from the root pyproject.toml.
Run through make _media/jackknife-leave-one-out.png. The opaque white PNG
is written in the current directory with the script's basename (1080 x 600).
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import numpy as np
def main():
data = np.array([1., 2., 3., 4., 10.])
n = data.size
means = np.array([np.delete(data, i).mean() for i in range(n)])
assert data.mean() == 4
np.testing.assert_allclose(means, [4.75, 4.5, 4.25, 4, 2.5], atol=0, rtol=0)
assert means.mean() == data.mean()
blue, orange, gray = "#176b91", "#b94a15", "#707070"
plt.rcParams.update({"font.size": 12, "axes.spines.top": False,
"axes.spines.right": False})
fig, (table, plot) = plt.subplots(
1, 2, figsize=(9, 5), gridspec_kw={"width_ratios": [1.15, 1]},
layout="constrained", facecolor="white")
fig.suptitle("Jackknife: delete each observation once\n"
"Data (1, 2, 3, 4, 10); full mean = 4", fontsize=16)
table.set(xlim=(-1.8, 4.65), ylim=(5.9, -1.15))
table.axis("off")
table.set_title("Each row retains four observations", fontsize=12, pad=14)
table.text(-1.65, -0.65, "Omit", color=gray, va="center")
for j in range(n):
table.text(j, -0.65, f"x{j + 1}", ha="center", va="center", color=gray)
for i in range(n):
row_color = orange if i == n - 1 else blue
table.text(-1.65, i + 0.3, f"{data[i]:g}", color=row_color, va="center")
for j, value in enumerate(data):
table.text(j, i + 0.3, f"{value:g}", ha="center", va="center",
color=gray if j == i else row_color, fontsize=15)
if i == j:
table.plot([j - 0.24, j + 0.24], [i + 0.12, i + 0.48],
color=gray, linewidth=1.5)
table.plot([-1.75, 4.45], [i + 0.8, i + 0.8], color="#e2e2e2", lw=0.7)
table.text(1.25, 5.6, "Crossed value is omitted for that row only",
ha="center", fontsize=10, color=gray)
positions = np.arange(n)
for i, value in enumerate(means):
color = orange if i == n - 1 else blue
plot.plot([data.mean(), value], [i, i], color=color, linewidth=2.5)
plot.plot(value, i, "o", color=color, markersize=8)
plot.annotate(f"{value:g}", (value, i), xytext=(0, -16),
textcoords="offset points", ha="center", color=color)
plot.axvline(data.mean(), color="#333333", linestyle="--", linewidth=1.3)
plot.set(xlim=(2.15, 5.1), ylim=(4.85, -0.85), xlabel="Mean of retained observations")
plot.set_yticks(positions, [f"Omit {value:g}" for value in data])
plot.set_xticks([2.5, 3, 3.5, 4, 4.5, 5])
plot.set_title("Deletion means: largest shift when 10 is omitted", fontsize=11, pad=14)
plot.grid(axis="x", color="#e2e2e2", linewidth=0.6)
plot.legend(handles=[Line2D([0], [0], color="#333333", linestyle="--",
label="Full mean = 4")],
loc="lower right", fontsize=10, framealpha=1)
plot.spines["left"].set_visible(False)
plot.tick_params(axis="y", length=0)
output = Path.cwd() / (Path(__file__).stem + ".png")
fig.savefig(output, dpi=120, facecolor="white", transparent=False)
plt.close(fig)
print(f"Saved {output} (1080 x 600)")
print(f"mean={data.mean():g}; deletion means={means.tolist()}; average={means.mean():g}")
print(f"Deletion effects={(means - data.mean()).tolist()}")
if __name__ == "__main__":
main()