The sample mean has a beautiful flaw.

It shares everything.

That is exactly why it is efficient for Gaussian noise. Every observation gets the same vote, and the votes average down cleanly. It is also why one absurd draw can move the final number by an absurd amount. The estimator has no internal bulkhead. If a measurement is wild enough, the whole average hears it.

Median-of-means is a small structural change:

split the data into blocks
average inside each block
take the median of those block averages

The block mean is still allowed to be ordinary and efficient most of the time. The median is the firebreak. A few ruined blocks do not get to speak for the whole sample.

This is not a universal “robustness wins” story. It is a high-confidence mean estimation story. The estimator is useful when the target is still the mean, but the distribution may have heavy tails, rare gross errors, or variance that is too visible for asymptotic comfort.

The Construction Is Almost Embarrassingly Small

Given observations \(X_1,\ldots,X_n\), choose an odd number of blocks \(k\). Split the observations into blocks \(B_1,\ldots,B_k\) of roughly equal size and compute:

\[\bar{X}_j = \frac{1}{|B_j|} \sum_{i \in B_j} X_i.\]

Then return:

\[\hat{\mu}_{\mathrm{MOM}} = \mathrm{median} \left( \bar{X}_1,\ldots,\bar{X}_k \right).\]

That is the whole estimator.

The point of the block count is easy to say and easy to misuse. If \(k\) is too small, the median has too few chances to reject bad blocks. If \(k\) is too large, each block mean has too little data and becomes noisy. In the usual analysis, \(k\) is a confidence knob: more blocks buy a smaller failure probability, but the block size pays for it.

A sketch gives the right feel. Suppose the observations are independent with mean \(\mu\) and variance \(\sigma^2\). If a block has size about \(m=n/k\), Chebyshev’s inequality says that one block mean is usually within a constant multiple of:

\[\sigma \sqrt{\frac{k}{n}}.\]

The median fails only if at least half the blocks are bad. Independent blocks turn that majority event into an exponentially small probability in \(k\). Set \(k\) proportional to \(\log(1/\delta)\) and you get a high-probability error of order:

\[\sigma \sqrt{\frac{\log(1/\delta)}{n}},\]

up to constants and feasibility conditions. This is the same confidence shape as a Gaussian tail bound, but it asks only for finite variance in the one-dimensional setting.1

The constants matter in real work. Catoni-style estimators and later median-of-means refinements improve parts of this story, especially when one cares about constants, adaptivity, or multivariate extensions.23 The simple estimator is still worth understanding because the mechanism is visible.

A Lab For Damaged Averages

The browser lab below compares three estimators for a true mean of zero:

  • the ordinary sample mean,
  • a 10% trimmed mean, and
  • median-of-means.

The default scenario is deliberately sharp: 96% of observations are \(N(0,1)\), while 4% are drawn from \(N(0,60^2)\). That is symmetric, so the true mean is still zero. It is also hostile to a plain average because a few rare observations carry enormous leverage.

What The Default Run Says

For the default contaminated setting, the deterministic audit run reports:

n = 120
blocks = 9
repeated samples = 900

sample mean p95 absolute error = 2.127
10% trimmed mean p95 error     = 0.204
median-of-means p95 error      = 0.503
MOM / mean p95                 = 0.24

That result is evidence about this simulation, not a theorem. It says that for this symmetric rare-outlier mixture, the sample mean’s 95th-percentile error is about four times the median-of-means error.

It also says something important about interpretation: the trimmed mean wins this default case. That is not an embarrassment for median-of-means. A fixed trim is excellent when the contamination is symmetric, obvious, and below the trim fraction. The MOM advantage is different: it does not require choosing a numeric clipping threshold, and its proof route is about a majority of block means being good rather than about identifying individual observations as outliers.

Change the scenario to the Gaussian baseline and the ordinary mean becomes the right reference again. In the audit grid, the Gaussian p95 errors are:

Scenario n Blocks Mean p95 Trimmed p95 MOM p95 MOM / mean
4% gross contamination 120 9 2.127 0.204 0.503 0.24
Gaussian baseline 120 9 0.183 0.187 0.232 1.27
Student t, 3 df 180 11 0.141 0.105 0.159 1.12
Centered lognormal 150 9 0.159 0.262 0.171 1.08
Symmetric Pareto tail 180 15 0.689 0.321 0.495 0.72

The rows are deliberately mixed. The lab is not trying to make MOM win every cell. It is trying to show when the firebreak matters and what it costs.

Reproducibility Check

The interactive lab is backed by a CommonJS module, so the same deterministic simulation can run under Node. The sweep reports 35 named checks: seven finite-summary, block, histogram, contamination, Gaussian-baseline, and estimator-consistency invariants across five scenarios.

node - <<'NODE'
const lab = require("./assets/js/median-of-means-lab.js");
const EXPECTED_CASE_SHAPE = [
  "1:contaminated:n=120:blocks=9:trials=900:seed=31:2.127/0.204/0.503:0.24:1.082/0.404:7/7",
  "2:gaussian:n=120:blocks=9:trials=800:seed=7:0.183/0.187/0.232:1.27:0.092/0.117:7/7",
  "3:student:n=180:blocks=11:trials=850:seed=11:0.141/0.105/0.159:1.12:0.073/0.080:7/7",
  "4:lognormal:n=150:blocks=9:trials=850:seed=23:0.159/0.262/0.171:1.08:0.084/0.094:7/7",
  "5:pareto:n=180:blocks=15:trials=800:seed=41:0.689/0.321/0.495:0.72:0.478/0.262:7/7"
];
const EXPECTED_CHECK_NAMES = [
  "all summaries finite",
  "median-of-means equals median block mean",
  "block count is respected",
  "histograms have fixed bin count",
  "contamination favors MOM over sample mean in p95",
  "Gaussian mean remains competitive",
  "trimmed estimator finite"
];
const EXPECTED_CASES = EXPECTED_CASE_SHAPE.length;
const EXPECTED_CHECKS = EXPECTED_CASES * EXPECTED_CHECK_NAMES.length;
const EXPECTED_CHECK_ROWS = Array.from({ length: EXPECTED_CASES }, (_, index) =>
  `${index + 1}:` + EXPECTED_CHECK_NAMES.map((name) => `${name}=true`).join("|")
);

function sameList(actual, expected) {
  return actual.length === expected.length &&
    actual.every((value, index) => value === expected[index]);
}

function caseShape(row) {
  return `${row.caseId}:${row.scenario}:n=${row.n}:blocks=${row.blocks}:` +
    `trials=${row.trials}:seed=${row.seed}:` +
    `${row.meanP95.toFixed(3)}/${row.trimmedP95.toFixed(3)}/${row.momP95.toFixed(3)}:` +
    `${row.momMeanRatio.toFixed(2)}:${row.meanRmse.toFixed(3)}/${row.momRmse.toFixed(3)}:` +
    `${row.passedChecks}/${row.totalChecks}`;
}

function checkRow(row) {
  return `${row.caseId}:` +
    row.checks.map((check) => `${check.name}=${check.ok}`).join("|");
}

function rowDrifts(actual, expected) {
  return {
    missing: expected.filter((row) => !actual.includes(row)),
    extra: actual.filter((row) => !expected.includes(row))
  };
}

function hasRowDrift(drift) {
  return drift.missing.length > 0 || drift.extra.length > 0;
}

const audit = lab.auditGrid();
const failed = audit.cases.filter(
  (row) => !row.passed || row.passedChecks !== row.totalChecks
);
const shape = {
  cases: audit.cases.map(caseShape),
  checkRows: audit.cases.map(checkRow)
};
const checkRowDrifts = rowDrifts(shape.checkRows, EXPECTED_CHECK_ROWS);
const shapeErrors = [];
if (!sameList(shape.cases, EXPECTED_CASE_SHAPE)) {
  shapeErrors.push({ name: "cases", actual: shape.cases, expected: EXPECTED_CASE_SHAPE });
}
audit.cases.forEach((row) => {
  const names = row.checks.map((check) => check.name);
  if (!sameList(names, EXPECTED_CHECK_NAMES)) {
    shapeErrors.push({
      name: `case ${row.caseId} check names`,
      actual: names,
      expected: EXPECTED_CHECK_NAMES
    });
  }
});
if (hasRowDrift(checkRowDrifts)) {
  shapeErrors.push({ name: "checkRows", drift: checkRowDrifts });
}
const summary =
  `${audit.passed}/${audit.total} cases and ${audit.passedChecks}/${audit.totalChecks} checks passed`;
if (
  shapeErrors.length ||
  failed.length ||
  !audit.ok ||
  audit.total !== EXPECTED_CASES ||
  audit.totalChecks !== EXPECTED_CHECKS ||
  audit.passed !== audit.total ||
  audit.passedChecks !== audit.totalChecks
) {
  throw new Error(JSON.stringify({
    summary,
    shapeErrors,
    shape,
    checkRowDrifts,
    failed,
    errors: audit.errors,
    totals: {
      total: audit.total,
      passed: audit.passed,
      passedChecks: audit.passedChecks,
      totalChecks: audit.totalChecks
    }
  }, null, 2));
}
console.table(audit.cases.map((row) => ({
  scenario: row.scenario,
  n: row.n,
  blocks: row.blocks,
  mean95: row.meanP95.toFixed(3),
  trim95: row.trimmedP95.toFixed(3),
  mom95: row.momP95.toFixed(3),
  ratio: row.momMeanRatio.toFixed(2),
  checks: `${row.passedChecks}/${row.totalChecks}`,
  passed: row.passed
})));
console.log(summary);
NODE

The audit checks that summaries are finite, the displayed median-of-means value equals the median of the displayed block means, block counts are respected, histograms keep their fixed bin count, the contamination case really hurts the plain mean, the Gaussian mean remains competitive, and the trimmed estimator is finite.

The implementation is intentionally small enough to inspect: assets/js/median-of-means-lab.js.

The Boundary Of The Trick

Median-of-means is not an outlier detector. It does not tell you which observations are wrong. It only limits how many block averages can control the answer.

It also does not rescue a mean that is not the right estimand. If the data come from a Cauchy distribution, the population mean is not defined. A stable median of block means may still produce a number, but it is not estimating a nonexistent mean.

If the contamination is biased and common enough to hit most blocks, MOM will move too. The firebreak assumes a majority of block means remain informative. That is weaker than assuming every observation is polite, but it is still an assumption.

Finally, block construction matters. Randomly permuting before splitting is a good habit when the input order might carry bursts or time dependence. If the data are serially correlated, clustered, or adversarially ordered, treating contiguous blocks as independent evidence is a modeling choice, not a law of nature.

The best practical reading is:

use the sample mean when the tails are tame enough
use a firebreak when rare blocks can be awful
keep the audit visible either way
  1. Gabor Lugosi and Shahar Mendelson, “Mean estimation and regression under heavy-tailed distributions–a survey”, 2019. 

  2. Olivier Catoni, “Challenging the empirical mean and empirical variance: a deviation study”, 2010, revised 2011. 

  3. Stanislav Minsker, “Efficient median of means estimator”, Proceedings of Machine Learning Research 195, 2023.