"""Compare jackknife variance and bias calculations; tested with Python 3.14.4. Dependencies: Matplotlib and NumPy from the root pyproject.toml. Run through make _media/jackknife-variance-bias.png. The opaque white PNG is written in the current directory with the script's basename (1200 x 720). """ from math import isclose, sqrt from pathlib import Path import matplotlib.pyplot as plt import numpy as np def calculations(data): """Recompute each mean and centered divisor-m variance after deletion.""" n = data.size reduced = [np.delete(data, i) for i in range(n)] means = np.array([row.mean() for row in reduced]) variances = np.array([row.var(ddof=0) for row in reduced]) contributions = (n - 1) / n * (means - means.mean())**2 empirical = data.var(ddof=0) bias = (n - 1) * (variances.mean() - empirical) return means, variances, contributions, empirical, bias, empirical - bias def main(): data = np.array([1., 2., 3., 4., 10.]) means, variances, parts, empirical, bias, corrected = calculations(data) np.testing.assert_allclose(means, [4.75, 4.5, 4.25, 4, 2.5], atol=0, rtol=0) np.testing.assert_allclose(variances, [9.6875, 11.25, 12.1875, 12.5, 1.25], atol=0, rtol=0) np.testing.assert_allclose(parts, [0.45, 0.2, 0.05, 0, 1.8], atol=1e-14, rtol=0) assert empirical == 10 and variances.mean() == 9.375 and bias == -2.5 assert corrected == 12.5 and data.var(ddof=1) == 12.5 assert isclose(parts.sum(), 2.5) and isclose(parts.sum(), data.var(ddof=1) / data.size) # Check both algebraic identities on further deterministic data, including n=2. for other in (np.array([2., 7.]), np.array([3., 3., 3.]), np.array([-4., 0., 1., 2., 6., 11.])): _, _, other_parts, _, _, other_corrected = calculations(other) assert isclose(other_corrected, other.var(ddof=1), rel_tol=1e-12, abs_tol=1e-12) assert isclose(other_parts.sum(), other.var(ddof=1) / other.size, rel_tol=1e-12, abs_tol=1e-12) blue, orange, green = "#176b91", "#b94a15", "#237347" plt.rcParams.update({"font.size": 12, "axes.spines.top": False, "axes.spines.right": False}) fig = plt.figure(figsize=(10, 6), layout="constrained", facecolor="white") grid = fig.add_gridspec(2, 2, height_ratios=[2.6, 1.45], width_ratios=[1, 1.15]) left = fig.add_subplot(grid[:, 0]) right = fig.add_subplot(grid[0, 1]) comparison = fig.add_subplot(grid[1, 1]) fig.suptitle("One set of deletions, two different targets\nData (1, 2, 3, 4, 10)", fontsize=16) indices = np.arange(data.size) colors = [blue] * 4 + [orange] left.bar(indices, parts, color=colors, width=0.65) for i, value in enumerate(parts): left.text(i, value + 0.035, f"{value:g}", ha="center", va="bottom") left.set_xticks(indices, [f"Omit\n{x:g}" for x in data]) left.set(ylim=(0, 2.55), ylabel="Contribution to variance estimate of the mean") left.set_title("Variance of the sample mean", fontsize=13, pad=12) left.text(0.5, 0.94, r"$(4/5)(\bar{x}_{(-i)}-4)^2$" + "\nSum = 2.5; SE = √2.5 ≈ 1.581", transform=left.transAxes, ha="center", va="top", fontsize=12) left.grid(axis="y", color="#e2e2e2", linewidth=0.6) left.set_axisbelow(True) right.axhline(variances.mean(), color=green, linestyle="--", linewidth=1.4, label="Deletion average = 9.375") right.scatter(indices, variances, color=colors, s=55, zorder=3) for i, value in enumerate(variances): right.annotate(f"{value:g}", (i, value), xytext=(0, 10), textcoords="offset points", ha="center", fontsize=10) right.set_xticks(indices, [f"Omit {x:g}" for x in data]) right.set(xlim=(-0.6, 4.6), ylim=(0, 15.5), ylabel="Divisor-4 variance") right.set_title("Bias correction of the empirical variance", fontsize=12, pad=12) right.grid(axis="y", color="#e2e2e2", linewidth=0.6) right.legend(loc="lower left", framealpha=1, fontsize=10) comparison.barh([1, 0], [empirical, corrected], color=[blue, green], height=0.5) comparison.set_yticks([1, 0], ["Original", "Corrected"]) comparison.set(xlim=(0, 15), ylim=(-0.5, 1.7), xlabel="Variance estimate of individual observations") for y, value in zip([1, 0], [empirical, corrected]): comparison.text(value + 0.25, y, f"{value:g}", va="center", fontsize=11) comparison.set_title("Estimated bias = 4(9.375 − 10) = −2.5\n" "Corrected = 10 − (−2.5) = 12.5", fontsize=11, pad=10) comparison.grid(axis="x", color="#e2e2e2", linewidth=0.6) comparison.set_axisbelow(True) comparison.spines["left"].set_visible(False) comparison.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} (1200 x 720)") print(f"s²={data.var(ddof=1):g}; Vmean={parts.sum():g}; SE={sqrt(parts.sum()):.12f}") print(f"Variance contributions={parts.tolist()}") print(f"Deletion empirical variances={variances.tolist()}") print(f"T={empirical:g}; average={variances.mean():g}; bias={bias:g}; corrected={corrected:g}") print("Additional identity checks passed: n=2, constant data, signed six-point data.") if __name__ == "__main__": main()