codexnormal-location-influence-curves.py
normal-location-influence-curves.py
"""Population influence curves for two normal location-scale models.
Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; the output basename matches this root generator.
"""
from math import erf, sqrt
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
k = 1.345
normalization = erf(k / sqrt(2))
colors = ["#2166ac", "#b35806", "#1b7837"]
fig, axes = plt.subplots(1, 2, figsize=(9.6, 4.3), layout="constrained")
for ax, mu, sigma in zip(axes, [0.0, 3.0], [1.0, 2.0]):
x = np.linspace(mu - 4 * sigma, mu + 4 * sigma, 801)
z = (x - mu) / sigma
ax.plot(x, x - mu, color=colors[0], label="Mean", lw=2.2)
# The jump has no connecting vertical segment; the derivative at mu is 0.
for sign in [-1, 1]:
mask = sign * z > 0
ax.plot(x[mask], sigma * sqrt(np.pi / 2) * np.sign(z[mask]),
color=colors[1], lw=2.2,
label="Median" if sign == -1 else None)
ax.scatter([mu], [0], color=colors[1], s=28, zorder=5)
ax.plot(x, sigma * np.clip(z, -k, k) / normalization,
color=colors[2], lw=2.2, label=f"Huber, k = {k}")
ax.axhline(0, color="#777777", lw=0.7)
ax.axvline(mu, color="#777777", lw=0.7)
ax.set(title=rf"$F = N({mu:g}, {sigma:g}^2)$; fixed scale $s={sigma:g}$",
xlabel="Contamination location x", ylabel="Influence IF(x; T, F)")
ax.grid(alpha=0.2)
ax.legend(loc="upper left", fontsize=9)
fig.suptitle("Normal-location influence: an unbounded mean and bounded alternatives", fontsize=12)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=120,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()