Dictionary of Applied Machine Learning · mean

mean — Python demo

Numerical companion to the entry mean: it recomputes what the entry states and prints one line per check

One block per paragraph of the entry (marked [P...]): each block verifies numerically what the corresponding statement asserts. Self-contained (numpy/matplotlib only), fixed seed.

Run it without installing anything:
uv run https://dictionaryofml.org/terms/mean.py
uv downloads this script and the pinned NumPy and Matplotlib it needs, then runs it; the script fetches any input file it uses. To keep the output files, download mean.py into a folder and run uv run mean.py there. With NumPy and Matplotlib already installed, python3 mean.py, from any directory — it writes its output files into the current directory. Fixed seeds, so the printed numbers reproduce exactly. Download mean.py · Notebook · Open in Colab

The script, block by block

One cell per block of the script: the code, and what that code printed when it last ran here

setup

"""
mean.py — numerical companion to the glossary entry 'mean'.

One block per paragraph of the entry (marked [P...]): each block verifies
numerically what the corresponding statement asserts. Self-contained
(numpy/matplotlib only), fixed seed.

Blocks
------
[P-def]     The mean of a random vector is its expectation, the Lebesgue
            integral of x with respect to its probability distribution:
            the Monte Carlo average of iid draws converges to the exact
            (analytic) mean as the sample grows.
[P-sample]  A dataset D defines a discrete RV x~ = x^(I) with I uniform on
            {1,...,m}; the mean of x~ equals the sample mean
            (1/m) sum_r x^(r) exactly.
[P-argmin]  For an RV with finite second moment, E{x} minimizes the risk
            E{||x - c||^2}: the analytic mean beats every candidate c on a
            grid, and the average loss over the sample has its minimum at
            the sample mean.
[P-erm]     Featureless regression: ERM with squared error loss over
            labels y^(1..m) is solved by the sample mean — the closed-form
            minimizer of (1/m) sum_r (y^(r) - h)^2 equals np.mean(y), and
            every other h has larger average loss (setting of Fig. 1,
            m = 5 labels with mean 4).
[P-robust]  Outliers and the mean: moving one of m data points by delta
            shifts the sample mean by exactly delta/m, without bound. With
            at most gamma*m points corrupted, the gamma-trimmed mean and the
            median stay bounded however far those points are moved.

Outputs
-------
mean.png : preview figure (checking only).

Data generated by pythondemos/mean.py.
"""

import numpy as np
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path

OUT_DIR = Path(__file__).parent

rng = np.random.default_rng(42)
report = []


def check(name, ok):
    report.append((name, bool(ok)))
    print(f"  [{'ok' if ok else 'FAIL'}] {name}")

P-def

The mean of a random vector is its expectation, the Lebesgue integral of x with respect to its probability distribution: the Monte Carlo average of iid draws converges to the exact (analytic) mean as the sample grows.

# Mean = expectation = integral of x dP(x). Monte Carlo average of iid
# draws from N(mu, C) converges to the analytic mean mu.
print("[P-def] Monte Carlo average converges to the analytic mean")
mu = np.array([1.0, -2.0])
A = np.array([[1.0, 0.0], [0.5, 0.8]])
errs = []
for m in [10**2, 10**4, 10**6]:
    x = rng.standard_normal((m, 2)) @ A.T + mu
    errs.append(np.linalg.norm(x.mean(axis=0) - mu))
print(f"    |MC mean - mu| for m=1e2,1e4,1e6: "
      f"{errs[0]:.4f}, {errs[1]:.4f}, {errs[2]:.4f}")
check("MC error shrinks monotonically", errs[0] > errs[1] > errs[2])
check("MC mean at m=1e6 within 3e-3 of mu", errs[2] < 3e-3)
[P-def] Monte Carlo average converges to the analytic mean
    |MC mean - mu| for m=1e2,1e4,1e6: 0.0467, 0.0109, 0.0007
  [ok] MC error shrinks monotonically
  [ok] MC mean at m=1e6 within 3e-3 of mu

P-sample

A dataset D defines a discrete RV x~ = x^(I) with I uniform on {1,...,m}; the mean of x~ equals the sample mean (1/m) sum_r x^(r) exactly.

# Discrete RV x~ = x^(I), I uniform on {1,...,m}: its mean is exactly the
# sample mean of the dataset.
print("[P-sample] dataset-induced discrete RV has mean = sample mean")
D = rng.normal(size=(7, 2))
probs = np.full(7, 1 / 7)                      # P(I = r) = 1/m
mean_discrete = (probs[:, None] * D).sum(axis=0)
check("E{x^(I)} equals (1/m) sum_r x^(r)",
      np.allclose(mean_discrete, D.mean(axis=0)))
[P-sample] dataset-induced discrete RV has mean = sample mean
  [ok] E{x^(I)} equals (1/m) sum_r x^(r)

P-argmin

For an RV with finite second moment, E{x} minimizes the risk E{||x - c||^2}: the analytic mean beats every candidate c on a grid, and the average loss over the sample has its minimum at the sample mean.

# E{x} = argmin_c E{||x - c||^2}. Empirically: risk(c) >= risk(mean) for
# every c on a grid, with equality only at c = mean.
print("[P-argmin] the mean minimizes the expected squared distance")
x = rng.standard_normal((200000, 2)) @ A.T + mu
def emp_risk(c):
    return np.mean(np.sum((x - c) ** 2, axis=1))
risk_mean = emp_risk(x.mean(axis=0))
grid = [x.mean(axis=0) + d for d in
        (np.array([0.5, 0]), np.array([-0.3, 0.4]), np.array([0, -1.0]))]
check("risk(mean) < risk(c) for all offset candidates c",
      all(emp_risk(c) > risk_mean for c in grid))
[P-argmin] the mean minimizes the expected squared distance
  [ok] risk(mean) < risk(c) for all offset candidates c

P-erm

Featureless regression: ERM with squared error loss over labels y^(1..m) is solved by the sample mean — the closed-form minimizer of (1/m) sum_r (y^(r) - h)^2 equals np.mean(y), and every other h has larger average loss (setting of Fig. 1, m = 5 labels with mean 4).

# Featureless regression (Fig. 1 setting): labels 2,5,3,6,4; ERM with the
# squared error loss is solved by the sample mean h_hat = 4.
print("[P-erm] featureless ERM with squared loss = sample mean")
y = np.array([2.0, 5.0, 3.0, 6.0, 4.0])
h_grid = np.linspace(0, 8, 1601)
emp = np.array([np.mean((y - h) ** 2) for h in h_grid])
h_hat = h_grid[np.argmin(emp)]
check("grid minimizer equals np.mean(y) = 4", abs(h_hat - y.mean()) < 5e-3)
check("sample mean is 4 (entry Fig. 1)", np.isclose(y.mean(), 4.0))
check("every other h has larger average loss",
      np.all(emp >= np.mean((y - y.mean()) ** 2) - 1e-12))
[P-erm] featureless ERM with squared loss = sample mean
  [ok] grid minimizer equals np.mean(y) = 4
  [ok] sample mean is 4 (entry Fig. 1)
  [ok] every other h has larger average loss

P-robust

Outliers and the mean: moving one of m data points by delta shifts the sample mean by exactly delta/m, without bound. With at most gamma*m points corrupted, the gamma-trimmed mean and the median stay bounded however far those points are moved.

# The entry's claim: moving one data point by delta shifts the sample mean
# by delta/m, so a single corrupted point carries it arbitrarily far, while
# the trimmed mean and the median do not move.
print("[P-robust] outliers move the mean without bound, the robust summaries stay bounded")

base = rng.normal(size=101)
m_pts = base.size
for delta in (10.0, 1e3, 1e6):
    moved = base.copy()
    moved[0] += delta
    check(f"one point moved by {delta:g}: mean shifts by delta/m",
          np.isclose(moved.mean() - base.mean(), delta / m_pts))

def trimmed(a, gamma):
    """Discard the floor(gamma*len) smallest and largest, average the rest."""
    k = int(np.floor(gamma * a.size))
    return np.sort(a)[k: a.size - k].mean() if a.size - 2 * k > 0 else np.nan

GAMMA = 0.1
n_bad = int(np.floor(GAMMA * m_pts))          # at most gamma*m corrupted

def summaries(delta):
    bad = base.copy()
    bad[:n_bad] += delta
    return bad.mean(), trimmed(bad, GAMMA), np.median(bad)

# The robust summaries are NOT unchanged -- corrupting points changes which
# order statistics are averaged -- but they stay BOUNDED as the corruption
# grows without bound, which is the property that matters.
rows = [summaries(d) for d in (1e3, 1e6, 1e9)]
mean_shift = [abs(r[0] - base.mean()) for r in rows]
trim_shift = [abs(r[1] - trimmed(base, GAMMA)) for r in rows]
med_shift = [abs(r[2] - np.median(base)) for r in rows]
print(f"    delta=1e3,1e6,1e9 -> mean moves by "
      f"{mean_shift[0]:.3g}, {mean_shift[1]:.3g}, {mean_shift[2]:.3g}")
print(f"                        trimmed moves by "
      f"{trim_shift[0]:.3g}, {trim_shift[1]:.3g}, {trim_shift[2]:.3g}")
check(f"mean diverges with delta ({n_bad} of {m_pts} corrupted)",
      mean_shift[2] > 1e6)
check("trimmed mean stays bounded as delta grows",
      max(trim_shift) < 1.0 and trim_shift[2] == trim_shift[0])
check("median stays bounded as delta grows",
      max(med_shift) < 1.0 and med_shift[2] == med_shift[0])

# ------------------------------------------------------------ preview
fig, ax = plt.subplots(1, 2, figsize=(9, 3.2))
ax[0].loglog([1e2, 1e4, 1e6], errs, "o-")
ax[0].set_xlabel("m"); ax[0].set_ylabel("|MC mean - mu|")
ax[0].set_title("[P-def] average approaches the mean")
ax[1].plot(h_grid, emp)
ax[1].axvline(y.mean(), ls="--", c="k")
ax[1].set_xlabel("h"); ax[1].set_ylabel("average loss")
ax[1].set_title("[P-erm] minimum at sample mean")
fig.tight_layout()
fig.savefig(OUT_DIR / "mean.png", dpi=110)
print(f"\n{sum(ok for _, ok in report)}/{len(report)} checks passed")
assert all(ok for _, ok in report)
[P-robust] outliers move the mean without bound, the robust summaries stay bounded
  [ok] one point moved by 10: mean shifts by delta/m
  [ok] one point moved by 1000: mean shifts by delta/m
  [ok] one point moved by 1e+06: mean shifts by delta/m
    delta=1e3,1e6,1e9 -> mean moves by 99, 9.9e+04, 9.9e+07
                        trimmed moves by 0.257, 0.257, 0.257
  [ok] mean diverges with delta (10 of 101 corrupted)
  [ok] trimmed mean stays bounded as delta grows
  [ok] median stays bounded as delta grows

13/13 checks passed
Preview figure produced by mean.py
The preview figure the block P-robust writes when the script runs