Properties of Estimators, Sufficiency, UMVUE, and GMM
Big picture: least squares, maximum likelihood, and Bayesian estimation all answer a version of the same question — which parameter value fits the data best? They just define “best” differently.
Key Insight
Under the right assumptions, these three different ideas lead to the exact same estimator. The next two slides show why.
Suppose \(Y = f(X;\theta) + \epsilon\) with \(\epsilon \sim N(0, \sigma^2)\).
The likelihood of the data is a product of normal densities: \[\text{MLE} = \arg\max_\theta \prod_{i=1}^n \frac{1}{\sqrt{2\pi\sigma^2}} e^{-\frac{(y_i - f(x_i;\theta))^2}{2\sigma^2}}\]
Taking logs and dropping the parts that don’t involve \(\theta\)… \[\text{MLE} = \arg\min_\theta \sum_{i=1}^n (y_i - f(x_i; \theta))^2 = \textbf{Least Squares}\]
Takeaway
If the errors are normal, least squares is maximum likelihood – no coincidence that both are so popular.
With a prior belief \(p(\theta)\) about the parameter, the MAP estimator maximizes the posterior distribution: \[\text{MAP} = \arg\max_\theta p(\theta \mid y) = \arg\max_\theta \; p(y\mid\theta)\cdot p(\theta)\]
The shape of the prior determines which familiar method you end up with:
Gaussian prior
\(p(\theta) \propto e^{-\lambda\|\theta\|^2}\)
\[\text{MAP} = \textbf{Ridge Regression}\]
Laplace prior
\(p(\theta) \propto e^{-\lambda\|\theta\|_1}\)
\[\text{MAP} = \textbf{LASSO}\]
Takeaway
“Regularization” in machine learning is a Bayesian prior in disguise.
Intuition: imagine repeating your study many times, each time computing \(\hat\theta\) from a fresh sample.
Formal Definition
\(\hat\theta\) is unbiased for \(\theta\) if \[\operatorname{E}[\hat\theta] = \theta \quad \text{for all } \theta \in \Theta\] Its bias is \(\operatorname{Bias}(\hat\theta) = \operatorname{E}[\hat\theta] - \theta\).
Unbiased
Biased
That last plot wasn’t a static image — it’s matplotlib, executed when the deck renders:
n = np.linspace(2, 50, 200)
fig, ax = plt.subplots(figsize=(5.4, 4.2))
ax.plot(n, np.ones_like(n), color=MYRED, linestyle='--', label=r'True $\sigma^2=1$')
ax.plot(n, (n - 1) / n, color=MYBLUE, linewidth=2.6, label=r'$\hat\sigma^2$ (biased)')
ax.plot(n, np.ones_like(n), color=MYGREEN, linewidth=2.6, label=r'$S^2$ (unbiased)')Why this matters
Change the formula, re-render, and the figure updates itself — same idea works for R ({r} chunks) or a genuinely interactive widget instead of a static figure.
Each dot is the estimate from one (simulated) repeated sample. Watch where the dots — and their running average — land relative to the true \(\theta\) as you add more samples.
{
// Small deterministic PRNG so the demo is reproducible on every render
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function randn(rng) {
let u = 0, v = 0;
while (u === 0) u = rng();
while (v === 0) v = rng();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
const theta = 3; // true parameter value
const biasAmt = 1.2; // systematic offset of the biased estimator
const N = 150;
const noiseSeed = mulberry32(11);
const noise = Array.from({ length: N }, () => randn(noiseSeed) * 0.9);
const drawsUnbiased = noise.map((e) => theta + e);
const drawsBiased = noise.map((e) => theta + biasAmt + e);
const k = k_bias;
const width = 760, height = 260;
const xMin = -1, xMax = 7;
const xScale = (x) => ((x - xMin) / (xMax - xMin)) * (width - 80) + 50;
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:640px; height:auto; display:block; margin:0 auto;` });
function panel(yBase, draws, label, color) {
svg.append(el("line", { x1: 50, y1: yBase, x2: width - 15, y2: yBase, stroke: "#999" }));
svg.append(el("line", { x1: xScale(theta), y1: yBase - 40, x2: xScale(theta), y2: yBase + 8, stroke: "#333", "stroke-dasharray": "3,3" }));
svg.append(el("text", { x: xScale(theta), y: yBase - 45, "text-anchor": "middle", "font-size": 11 }, "true θ"));
const jitterSeed = mulberry32(3);
for (let i = 0; i < k; i++) {
const jitter = (jitterSeed() - 0.5) * 22;
svg.append(el("circle", { cx: xScale(draws[i]), cy: yBase - 10 - Math.abs(jitter), r: 3, fill: color, "fill-opacity": 0.55 }));
}
const avg = draws.slice(0, k).reduce((a, b) => a + b, 0) / k;
svg.append(el("line", { x1: xScale(avg), y1: yBase - 58, x2: xScale(avg), y2: yBase + 8, stroke: color, "stroke-width": 3 }));
svg.append(el("text", { x: xScale(avg), y: yBase + 22, "text-anchor": "middle", "font-size": 12, "font-weight": "bold", fill: color }, `avg = ${avg.toFixed(2)}`));
svg.append(el("text", { x: 15, y: yBase, "text-anchor": "start", "font-size": 13, "font-weight": "bold" }, label));
}
panel(115, drawsUnbiased, "Unbiased", "#005AB4");
panel(235, drawsBiased, "Biased", "#C80000");
svg.append(el("text", { x: width / 2, y: 18, "text-anchor": "middle", "font-size": 14, fill: "#C80000", "font-weight": "bold" }, `n = ${k} simulated samples`));
return svg;
}Think of an estimator as arrows thrown at a target, where the bullseye is the true \(\theta\):
Drag the slider to fire more shots at each target and see the pattern emerge.
{
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function randn(rng) {
let u = 0, v = 0;
while (u === 0) u = rng();
while (v === 0) v = rng();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
const configs = [
{ label: "Low bias, low variance", bx: 0, by: 0, sd: 0.10, color: "#008200", seed: 1 },
{ label: "Low bias, high variance", bx: 0, by: 0, sd: 0.42, color: "#005AB4", seed: 2 },
{ label: "High bias, low variance", bx: 0.55, by: 0.4, sd: 0.10, color: "#C80000", seed: 3 },
{ label: "High bias, high variance", bx: 0.55, by: 0.4, sd: 0.42, color: "#9400D3", seed: 4 },
];
const cell = 190, gap = 24, rTarget = 68, cols = 2;
const width = cols * cell + gap + 20;
const height = 2 * cell + gap + 20;
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:400px; height:auto; display:block; margin:0 auto;` });
configs.forEach((cfg, idx) => {
const col = idx % cols, row = Math.floor(idx / cols);
const cx = col * (cell + gap / 2) + cell / 2 + 10;
const cy = row * (cell + gap / 2) + cell / 2 + 45;
[1, 0.66, 0.33].forEach((frac, i) => {
svg.append(el("circle", { cx, cy, r: rTarget * frac, fill: i % 2 === 0 ? "#f5f5f5" : "#ddd", stroke: "#999" }));
});
svg.append(el("circle", { cx, cy, r: 3, fill: "#333" }));
const rng = mulberry32(cfg.seed);
for (let i = 0; i < shots_bv; i++) {
const dx = (cfg.bx + randn(rng) * cfg.sd) * rTarget;
const dy = (cfg.by + randn(rng) * cfg.sd) * rTarget;
svg.append(el("circle", { cx: cx + dx, cy: cy + dy, r: 3.2, fill: cfg.color, "fill-opacity": 0.75 }));
}
svg.append(el("text", { x: cx, y: cy - rTarget - 12, "text-anchor": "middle", "font-size": 12, "font-weight": "bold" }, cfg.label));
});
return svg;
}Definition and Decomposition
\[\operatorname{MSE}(\hat{\theta}) = \underbrace{\operatorname{Var}(\hat{\theta})}_{\text{Variance}} + \underbrace{[\operatorname{Bias}(\hat{\theta})]^2}_{\text{Bias}^2}\]
This is why we don’t only look for unbiased estimators: a slightly biased estimator with much lower variance can have lower MSE overall.
Drag the slider to move along the complexity axis and read off the trade-off at that point.
{
const bias2fn = (x) => 2 * Math.exp(-0.3 * x) + 0.2;
const varfn = (x) => 0.1 * x + 0.1;
const msefn = (x) => bias2fn(x) + varfn(x);
const width = 620, height = 460;
const xMin = 0, xMax = 10, yMax = 2.6;
const padL = 55, padB = 40, padT = 20, padR = 15;
const xScale = (x) => padL + (x - xMin) / (xMax - xMin) * (width - padL - padR);
const yScale = (y) => height - padB - (Math.min(y, yMax) / yMax) * (height - padB - padT);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:520px; height:auto; display:block; margin:0 auto;` });
function curve(fn, color, lw) {
let d = "";
for (let x = xMin; x <= xMax; x += 0.05) {
const y = fn(x);
d += (x <= xMin + 1e-9 ? "M" : "L") + xScale(x).toFixed(1) + "," + yScale(y).toFixed(1) + " ";
}
svg.append(el("path", { d, fill: "none", stroke: color, "stroke-width": lw }));
}
curve(bias2fn, "#005AB4", 2.4);
curve(varfn, "#C80000", 2.4);
curve(msefn, "#008200", 3.2);
// axes
svg.append(el("line", { x1: padL, y1: yScale(0), x2: width - padR, y2: yScale(0), stroke: "#333" }));
svg.append(el("line", { x1: padL, y1: yScale(0), x2: padL, y2: yScale(yMax), stroke: "#333" }));
svg.append(el("text", { x: width / 2, y: height - 6, "text-anchor": "middle", "font-size": 24 }, "Model complexity"));
svg.append(el("text", { x: 14, y: height / 2, "text-anchor": "middle", "font-size": 24, transform: `rotate(-90 14 ${height / 2})` }, "Error"));
// moving marker at chosen complexity
const xC = complexity;
const b2 = bias2fn(xC), v = varfn(xC), m = msefn(xC);
svg.append(el("line", { x1: xScale(xC), y1: yScale(0), x2: xScale(xC), y2: yScale(yMax), stroke: "#6E6E6E", "stroke-dasharray": "4,3" }));
[[b2, "#005AB4"], [v, "#C80000"], [m, "#008200"]].forEach(([val, color]) => {
svg.append(el("circle", { cx: xScale(xC), cy: yScale(val), r: 4.5, fill: color }));
});
// legend
const legend = [["Bias²", "#005AB4"], ["Variance", "#C80000"], ["MSE", "#008200"]];
legend.forEach(([label, color], i) => {
const lx = width - 150, ly = padT + 14 + i * 18;
svg.append(el("line", { x1: lx, y1: ly, x2: lx + 22, y2: ly, stroke: color, "stroke-width": 3 }));
svg.append(el("text", { x: lx + 28, y: ly + 4, "font-size": 12 }, label));
});
svg.append(el("text", { x: padL + 6, y: padT + 4, "font-size": 12, fill: "#6E6E6E" },
`At complexity = ${xC.toFixed(1)}:`));
svg.append(el("text", { x: padL + 6, y: padT + 20, "font-size": 12, fill: "#6E6E6E" },
`Bias² = ${b2.toFixed(2)}, Variance = ${v.toFixed(2)}, MSE = ${m.toFixed(2)}`));
return svg;
}For \(X_1, \ldots, X_n \sim N(\mu, 1)\), compare two estimators of \(\mu\):
Their mean squared errors are: \[\operatorname{MSE}(\hat{\mu}_1) = \frac{1}{n} \qquad\qquad \operatorname{MSE}(\hat{\mu}_2) = \frac{c^2}{n} + (1-c)^2\mu^2\]
Takeaway
When \(\mu\) is close to 0, the biased \(\hat\mu_2\) can have smaller MSE than the unbiased \(\bar{X}\). This is the idea behind shrinkage estimators like Ridge regression.
Adjust \(n\) and \(c\) and see where the shrunk estimator beats the unbiased one (shaded region).
n (sample size)
{
const n = n_shrink, c = c_shrink;
const mse1 = (mu) => 1 / n; // unbiased X̄
const mse2 = (mu) => (c * c) / n + (1 - c) * (1 - c) * mu * mu; // shrunk c·X̄
const width = 700, height = 320;
const muMax = 2, yMax = Math.max(0.6, mse2(muMax)) * 1.1;
const padL = 55, padB = 40, padT = 20, padR = 15;
const xScale = (mu) => padL + (mu + muMax) / (2 * muMax) * (width - padL - padR);
const yScale = (y) => height - padB - (Math.min(y, yMax) / yMax) * (height - padB - padT);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:620px; height:auto; display:block; margin:0 auto;` });
// shaded region where mse2 < mse1: mse2(mu) = mse1 solved at mu = sqrt((1-c^2)/(n*(1-c)^2))
const muStar = Math.sqrt((1 - c * c) / (n * (1 - c) * (1 - c)));
const shadeX1 = xScale(Math.max(-muMax, -muStar));
const shadeX2 = xScale(Math.min(muMax, muStar));
svg.append(el("rect", { x: shadeX1, y: padT, width: Math.max(0, shadeX2 - shadeX1), height: height - padB - padT, fill: "#008200", "fill-opacity": 0.10 }));
function curve(fn, color, lw) {
let d = "";
for (let mu = -muMax; mu <= muMax; mu += 0.02) {
const y = fn(mu);
d += (mu <= -muMax + 1e-9 ? "M" : "L") + xScale(mu).toFixed(1) + "," + yScale(y).toFixed(1) + " ";
}
svg.append(el("path", { d, fill: "none", stroke: color, "stroke-width": lw }));
}
curve(mse1, "#005AB4", 2.6);
curve(mse2, "#C80000", 2.6);
svg.append(el("line", { x1: padL, y1: yScale(0), x2: width - padR, y2: yScale(0), stroke: "#333" }));
svg.append(el("line", { x1: padL, y1: yScale(0), x2: padL, y2: yScale(yMax), stroke: "#333" }));
svg.append(el("text", { x: width / 2, y: height - 6, "text-anchor": "middle", "font-size": 12 }, "μ (true mean)"));
svg.append(el("text", { x: 14, y: height / 2, "text-anchor": "middle", "font-size": 12, transform: `rotate(-90 14 ${height / 2})` }, "MSE"));
const legend = [[`MSE(μ̂₁) = 1/n (unbiased)`, "#005AB4"], [`MSE(μ̂₂) = c²/n + (1−c)²μ² (shrunk)`, "#C80000"]];
legend.forEach(([label, color], i) => {
const lx = padL + 10, ly = padT + 14 + i * 18;
svg.append(el("line", { x1: lx, y1: ly, x2: lx + 22, y2: ly, stroke: color, "stroke-width": 3 }));
svg.append(el("text", { x: lx + 28, y: ly + 4, "font-size": 12 }, label));
});
svg.append(el("text", { x: width - 250, y: padT + 4, "font-size": 12, fill: "#008200" },
`shrinkage wins for |μ| < ${muStar.toFixed(2)} (n=${n}, c=${c.toFixed(2)})`));
return svg;
}Consistency of \(\bar{X}_n\) rests on the Law of Large Numbers: as we add more draws, the running average settles down near the true \(\mu\). Drag the slider to reveal more of the path.
{
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function randn(rng) {
let u = 0, v = 0;
while (u === 0) u = rng();
while (v === 0) v = rng();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
const mu = 2, sigma = 1.5, Nmax = 500;
const rng = mulberry32(7);
const draws = Array.from({ length: Nmax }, () => mu + randn(rng) * sigma);
const runningAvg = [];
let sum = 0;
for (let i = 0; i < Nmax; i++) { sum += draws[i]; runningAvg.push(sum / (i + 1)); }
const n = n_lln;
const width = 720, height = 300;
const padL = 55, padB = 40, padT = 20, padR = 15;
const yMin = mu - 3, yMax = mu + 3;
const xScale = (i) => padL + (i / Nmax) * (width - padL - padR);
const yScale = (y) => height - padB - ((y - yMin) / (yMax - yMin)) * (height - padB - padT);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:620px; height:auto; display:block; margin:0 auto;` });
// true mean reference line
svg.append(el("line", { x1: padL, y1: yScale(mu), x2: width - padR, y2: yScale(mu), stroke: "#C80000", "stroke-dasharray": "4,3" }));
svg.append(el("text", { x: width - padR, y: yScale(mu) - 6, "text-anchor": "end", "font-size": 11, fill: "#C80000" }, "true μ"));
// running average path
let d = "";
for (let i = 0; i < n; i++) {
d += (i === 0 ? "M" : "L") + xScale(i).toFixed(1) + "," + yScale(runningAvg[i]).toFixed(1) + " ";
}
svg.append(el("path", { d, fill: "none", stroke: "#005AB4", "stroke-width": 2.4 }));
svg.append(el("circle", { cx: xScale(n - 1), cy: yScale(runningAvg[n - 1]), r: 4.5, fill: "#005AB4" }));
// axes
svg.append(el("line", { x1: padL, y1: height - padB, x2: width - padR, y2: height - padB, stroke: "#333" }));
svg.append(el("line", { x1: padL, y1: padT, x2: padL, y2: height - padB, stroke: "#333" }));
svg.append(el("text", { x: width / 2, y: height - 6, "text-anchor": "middle", "font-size": 12 }, "n (number of draws)"));
svg.append(el("text", { x: 14, y: height / 2, "text-anchor": "middle", "font-size": 12, transform: `rotate(-90 14 ${height / 2})` }, "Running average"));
svg.append(el("text", { x: padL + 6, y: padT + 4, "font-size": 13, fill: "#005AB4", "font-weight": "bold" },
`n = ${n}, X̄ₙ = ${runningAvg[n - 1].toFixed(3)}`));
return svg;
}Unbiasedness is about being right on average for a given \(n\). Consistency instead asks: does the estimator get closer and closer to the truth as we collect more data?
Definition
\(\hat{\theta}_n\) is consistent if \(\hat{\theta}_n \xrightarrow{P} \theta\) as \(n \to \infty\), i.e. for every \(\epsilon > 0\): \[\lim_{n \to \infty} P\left(|\hat{\theta}_n - \theta| > \epsilon\right) = 0\]
An Easy Way to Check It
If both of these hold, \(\hat\theta_n\) is automatically consistent: \[\lim_{n \to \infty} \operatorname{Bias}(\hat{\theta}_n) = 0 \qquad \text{and} \qquad \lim_{n \to \infty} \operatorname{Var}(\hat{\theta}_n) = 0\]
As \(n\) grows, the sampling distribution of \(\hat\theta_n\) concentrates more and more tightly around the true value \(\theta\). Drag the slider.
{
const theta = 2, sigma = 1;
const std = sigma / Math.sqrt(n_consist);
const width = 760, height = 420, xMin = -2, xMax = 6, yMax = 9;
const xScale = x => (x - xMin) / (xMax - xMin) * (width - 70) + 55;
const yScale = y => height - 45 - (Math.min(y, yMax) / yMax) * (height - 65);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:580px; height:auto; display:block; margin:0 auto;` });
let d = "";
for (let x = xMin; x <= xMax; x += 0.02) {
const y = Math.exp(-0.5 * ((x - theta) / std) ** 2) / (std * Math.sqrt(2 * Math.PI));
d += (x <= xMin + 1e-9 ? "M" : "L") + xScale(x).toFixed(1) + "," + yScale(y).toFixed(1) + " ";
}
d += `L${xScale(xMax)},${yScale(0)} L${xScale(xMin)},${yScale(0)} Z`;
svg.append(el("path", {d, fill: "#005AB4", "fill-opacity": 0.15, stroke: "none"}));
let d2 = "";
for (let x = xMin; x <= xMax; x += 0.02) {
const y = Math.exp(-0.5 * ((x - theta) / std) ** 2) / (std * Math.sqrt(2 * Math.PI));
d2 += (x <= xMin + 1e-9 ? "M" : "L") + xScale(x).toFixed(1) + "," + yScale(y).toFixed(1) + " ";
}
svg.append(el("path", {d: d2, fill: "none", stroke: "#005AB4", "stroke-width": 3}));
svg.append(el("line", {x1: xScale(theta), y1: yScale(0), x2: xScale(theta), y2: yScale(yMax),
stroke: "#6E6E6E", "stroke-dasharray": "4,3"}));
svg.append(el("text", {x: xScale(theta), y: yScale(yMax) - 8, "text-anchor": "middle",
fill: "#6E6E6E", "font-size": 13}, "true θ"));
svg.append(el("line", {x1: 55, y1: yScale(0), x2: width - 15, y2: yScale(0), stroke: "#333"}));
svg.append(el("line", {x1: 55, y1: yScale(0), x2: 55, y2: yScale(yMax), stroke: "#333"}));
for (let x = -2; x <= 6; x += 2) {
svg.append(el("text", {x: xScale(x), y: yScale(0) + 18, "text-anchor": "middle", "font-size": 11}, String(x)));
}
svg.append(el("text", {x: width / 2, y: height - 5, "text-anchor": "middle", "font-size": 13}, "θ̂"));
svg.append(el("text", {x: 15, y: height / 2, "text-anchor": "middle", "font-size": 13,
transform: `rotate(-90 15 ${height / 2})`}, "Density"));
svg.append(el("text", {x: 65, y: 28, "font-size": 17, fill: "#C80000", "font-weight": "bold"}, `n = ${n_consist}`));
return svg;
}Among all unbiased estimators, some are more precise than others. Efficiency asks: is \(\hat\theta\) the most precise unbiased estimator possible?
It turns out there is a hard floor on how small the variance of an unbiased estimator can ever be — no amount of cleverness can beat it.
That floor is the Cramér-Rao Lower Bound.
Fisher Information
\[I(\theta) = -\operatorname{E}\left[\frac{\partial^2 \ln f(X;\theta)}{\partial \theta^2}\right]\] Roughly: how sharply peaked the likelihood is. More information means the data pins down \(\theta\) more precisely.
Cramér-Rao Lower Bound (CRLB)
For any unbiased estimator \(\hat\theta\) based on \(n\) observations: \[\operatorname{Var}(\hat{\theta}) \geq \frac{1}{nI(\theta)}\] An unbiased estimator that achieves this bound is called efficient.
For \(X_i \sim N(\mu, \sigma^2)\), estimating \(\mu\):
Conclusion
\(\operatorname{Var}(\bar{X})\) exactly meets the CRLB, so \(\bar{X}\) is an efficient estimator of \(\mu\) — no unbiased estimator can do better.
Drag the slider to change \(n\): notice \(\operatorname{Var}(\bar{X})\) sits exactly on the CRLB floor, while a wasteful-but-unbiased estimator stays above it.
{
const sigma2 = 4;
const crlb = (n) => sigma2 / n;
const varXbar = (n) => sigma2 / n; // meets the bound exactly
const varInefficient = (n) => 1.6 * sigma2 / n; // e.g. using only 60% of the data
const width = 700, height = 300;
const nMax = 60, yMax = crlb(2) * 1.05;
const padL = 55, padB = 40, padT = 20, padR = 15;
const xScale = (n) => padL + (n / nMax) * (width - padL - padR);
const yScale = (y) => height - padB - (Math.min(y, yMax) / yMax) * (height - padB - padT);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:620px; height:auto; display:block; margin:0 auto;` });
function curve(fn, color, lw, dash) {
let d = "";
for (let n = 2; n <= nMax; n += 0.5) {
const y = fn(n);
d += (n <= 2 ? "M" : "L") + xScale(n).toFixed(1) + "," + yScale(y).toFixed(1) + " ";
}
const attrs = { d, fill: "none", stroke: color, "stroke-width": lw };
if (dash) attrs["stroke-dasharray"] = dash;
svg.append(el("path", attrs));
}
curve(crlb, "#C80000", 4, "6,3");
curve(varXbar, "#005AB4", 2.2);
curve(varInefficient, "#9400D3", 2.4);
svg.append(el("line", { x1: padL, y1: yScale(0), x2: width - padR, y2: yScale(0), stroke: "#333" }));
svg.append(el("line", { x1: padL, y1: yScale(0), x2: padL, y2: yScale(yMax), stroke: "#333" }));
svg.append(el("text", { x: width / 2, y: height - 6, "text-anchor": "middle", "font-size": 12 }, "n"));
svg.append(el("text", { x: 14, y: height / 2, "text-anchor": "middle", "font-size": 12, transform: `rotate(-90 14 ${height / 2})` }, "Variance"));
const nSel = n_eff;
svg.append(el("line", { x1: xScale(nSel), y1: yScale(0), x2: xScale(nSel), y2: yScale(yMax), stroke: "#6E6E6E", "stroke-dasharray": "4,3" }));
[[crlb(nSel), "#C80000"], [varXbar(nSel), "#005AB4"], [varInefficient(nSel), "#9400D3"]].forEach(([val, color]) => {
svg.append(el("circle", { cx: xScale(nSel), cy: yScale(val), r: 4.5, fill: color }));
});
const legend = [["CRLB = σ²/n", "#C80000"], ["Var(X̄) (efficient)", "#005AB4"], ["Inefficient unbiased estimator", "#9400D3"]];
legend.forEach(([label, color], i) => {
const lx = width - 230, ly = padT + 14 + i * 18;
svg.append(el("line", { x1: lx, y1: ly, x2: lx + 22, y2: ly, stroke: color, "stroke-width": 3 }));
svg.append(el("text", { x: lx + 28, y: ly + 4, "font-size": 12 }, label));
});
svg.append(el("text", { x: padL + 6, y: padT + 4, "font-size": 13, fill: "#333" }, `n = ${nSel}`));
return svg;
}Do we really need to keep the entire dataset to estimate \(\theta\), or does a short summary contain all the useful information?
Definition
A statistic \(T(X)\) is sufficient for \(\theta\) if, once you know \(T(X)\), the rest of the data tells you nothing more about \(\theta\).
Example: to estimate the probability of heads \(p\) from \(n\) coin flips, the order of heads and tails is irrelevant — only the total count of heads matters. That count is a sufficient statistic.
Factorization Theorem
\(T(X)\) is sufficient for \(\theta\) if and only if the likelihood splits as \[f(x;\theta) = g(T(x), \theta) \cdot h(x)\] i.e. \(\theta\) only ever enters through \(T(x)\).
Example: Bernoulli trials
For \(X_1, \ldots, X_n \sim \text{Ber}(p)\), the likelihood is \[L(p) = p^{\sum x_i}(1-p)^{n-\sum x_i}\] Since \(p\) only appears through \(\sum x_i\), the statistic \(T(X) = \sum_{i=1}^n X_i\) is sufficient for \(p\).
All sequences with the same count \(T = \sum x_i\) are interchangeable for learning \(p\). Adjust \(n\) and \(T\): the bar shows how many raw sequences collapse into that one value of \(T\).
n (number of flips)
{
function choose(n, k) {
if (k < 0 || k > n) return 0;
k = Math.min(k, n - k);
let result = 1;
for (let i = 0; i < k; i++) result = (result * (n - i)) / (i + 1);
return result;
}
const n = n_suff, kSel = Math.min(k_suff, n);
const counts = Array.from({ length: n + 1 }, (_, k) => choose(n, k));
const maxCount = Math.max(...counts);
const width = 700, height = 260;
const padL = 55, padB = 40, padT = 20, padR = 15;
const barW = (width - padL - padR) / (n + 1);
const yScale = (y) => height - padB - (y / maxCount) * (height - padB - padT);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:620px; height:auto; display:block; margin:0 auto;` });
counts.forEach((c, k) => {
const x = padL + k * barW;
const y = yScale(c);
const isSel = k === kSel;
svg.append(el("rect", { x: x + 1, y, width: Math.max(1, barW - 2), height: height - padB - y, fill: isSel ? "#C80000" : "#005AB4", "fill-opacity": isSel ? 0.9 : 0.55 }));
});
svg.append(el("line", { x1: padL, y1: height - padB, x2: width - padR, y2: height - padB, stroke: "#333" }));
svg.append(el("text", { x: width / 2, y: height - 6, "text-anchor": "middle", "font-size": 12 }, "T = number of heads"));
svg.append(el("text", { x: padL + 6, y: padT + 4, "font-size": 13, fill: "#C80000", "font-weight": "bold" },
`n = ${n}: C(${n}, ${kSel}) = ${counts[kSel].toLocaleString()} different sequences all give T = ${kSel}`));
return svg;
}Idea: if you have an unbiased estimator that doesn’t yet use a sufficient statistic, you can always improve it (or at worst leave it unchanged) by conditioning on that sufficient statistic.
Theorem (Rao-Blackwell)
Let \(\hat{\theta}\) be an unbiased estimator of \(\theta\), and let \(T\) be sufficient for \(\theta\). Then \[\hat{\theta}^* = \operatorname{E}[\hat{\theta} \mid T]\] is also unbiased, and \(\operatorname{Var}(\hat{\theta}^*) \leq \operatorname{Var}(\hat{\theta})\).
For \(X_1, \ldots, X_n \sim \text{Ber}(p)\):
Result
\(\hat{p}^{\,*} = \bar{X}\): the sample mean, which uses all the data and has smaller variance than \(\hat{p} = X_1\).
Each dot is one repeated experiment of \(n=8\) coin flips with true \(p=0.4\). Compare the spread of the crude estimator to the Rao-Blackwellized one.
{
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const p = 0.4, nFlips = 8, N = 150;
const rng = mulberry32(21);
const experiments = Array.from({ length: N }, () => {
const flips = Array.from({ length: nFlips }, () => (rng() < p ? 1 : 0));
return { crude: flips[0], rb: flips.reduce((a, b) => a + b, 0) / nFlips };
});
const k = k_rb;
const width = 720, height = 250;
const xMin = -0.15, xMax = 1.15;
const xScale = (x) => 50 + ((x - xMin) / (xMax - xMin)) * (width - 65);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:620px; height:auto; display:block; margin:0 auto;` });
function panel(yBase, key, label, color) {
svg.append(el("line", { x1: 50, y1: yBase, x2: width - 15, y2: yBase, stroke: "#999" }));
svg.append(el("line", { x1: xScale(p), y1: yBase - 40, x2: xScale(p), y2: yBase + 8, stroke: "#333", "stroke-dasharray": "3,3" }));
svg.append(el("text", { x: xScale(p), y: yBase - 45, "text-anchor": "middle", "font-size": 11 }, "true p"));
const jitterSeed = mulberry32(5);
for (let i = 0; i < k; i++) {
const jitter = (jitterSeed() - 0.5) * 22;
svg.append(el("circle", { cx: xScale(experiments[i][key]), cy: yBase - 10 - Math.abs(jitter), r: 3, fill: color, "fill-opacity": 0.5 }));
}
const avg = experiments.slice(0, k).reduce((a, e) => a + e[key], 0) / k;
svg.append(el("line", { x1: xScale(avg), y1: yBase - 58, x2: xScale(avg), y2: yBase + 8, stroke: color, "stroke-width": 3 }));
svg.append(el("text", { x: xScale(avg), y: yBase + 22, "text-anchor": "middle", "font-size": 12, "font-weight": "bold", fill: color }, `avg = ${avg.toFixed(2)}`));
svg.append(el("text", { x: 15, y: yBase, "text-anchor": "start", "font-size": 12, "font-weight": "bold" }, label));
}
panel(100, "crude", "Crude: p̂ = X₁", "#9400D3");
panel(205, "rb", "Rao-Blackwellized: p̂* = X̄", "#008200");
svg.append(el("text", { x: width / 2, y: 18, "text-anchor": "middle", "font-size": 13, fill: "#C80000", "font-weight": "bold" }, `${k} repeated experiments`));
return svg;
}Rao-Blackwell tells us that conditioning on a sufficient statistic never hurts. UMVUE takes this to its logical conclusion: is there an unbiased estimator that beats every other unbiased estimator, for every value of \(\theta\)?
Definition
\(\hat{\theta}^*\) is the Uniformly Minimum Variance Unbiased Estimator (UMVUE) if it is unbiased and \[\operatorname{Var}(\hat{\theta}^*) \leq \operatorname{Var}(\hat{\theta})\] for every other unbiased \(\hat\theta\), and every \(\theta\).
Theorem (Lehmann-Scheffé)
If \(T\) is a complete sufficient statistic and \(\hat\theta = g(T)\) is unbiased, then \(\hat\theta\) is automatically the UMVUE.
What does “complete” mean?
Loosely: \(T\) has no “leftover” unbiased noise in it. Formally, \(T\) is complete if \[\operatorname{E}[g(T)] = 0 \; \forall \theta \quad \implies \quad g(T) = 0 \text{ almost surely}\]
In practice: once you’ve found a complete sufficient statistic, any unbiased function of it is automatically the best possible unbiased estimator.
Good to Know
Classical method of moments: write down as many equations (moment conditions) as you have parameters, then solve them exactly.
But what if you have more valid equations than parameters? You can’t satisfy all of them exactly at once, so instead you get as close as possible to satisfying all of them simultaneously.
This is the idea behind GMM
Suppose economic theory implies a set of conditions that should hold on average at the true parameter value:
\[\operatorname{E}[g(X_i, \theta)] = 0, \qquad g: \mathbb{R}^d \times \mathbb{R}^p \to \mathbb{R}^q\]
Their sample counterparts will rarely hit zero exactly, so we make them as close to zero as possible:
\[\bar{g}_n(\theta) = \frac{1}{n}\sum_{i=1}^n g(X_i, \theta)\]
GMM Estimator
\[\hat{\theta}_{GMM} = \arg\min_\theta \; \bar{g}_n(\theta)'W_n\bar{g}_n(\theta)\] where \(W_n\) is a \(q \times q\) weight matrix that decides how much each condition counts.
Not all moment conditions are equally reliable — noisier ones should count for less.
Two-Step Procedure
With the optimal weight matrix, GMM achieves the smallest possible asymptotic variance among all choices of \(W\) — the same spirit as the Cramér-Rao bound, for a much broader class of estimators.
Model: \(y = X\beta + \epsilon\), but \(\operatorname{E}[X'\epsilon] \neq 0\) — \(X\) is endogenous, so OLS is biased.
Fix: find instruments \(Z\) that are correlated with \(X\) but uncorrelated with \(\epsilon\): \[\operatorname{E}[Z'\epsilon] = 0 \quad \Longrightarrow \quad g(y,X,Z;\beta) = Z'(y - X\beta)\]
GMM / IV Estimator
\[\hat{\beta}_{GMM} = (X'ZW_nZ'X)^{-1}X'ZW_nZ'y\]
Each line is the running estimate as the sample grows. Drag the slider to add more observations and watch which estimator finds the true \(\beta\).
{
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function randn(rng) {
let u = 0, v = 0;
while (u === 0) u = rng();
while (v === 0) v = rng();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
const trueBeta = 2, endogBias = 0.7, Nmax = 500;
const rngOLS = mulberry32(31), rngIV = mulberry32(32);
const drawsOLS = Array.from({ length: Nmax }, () => trueBeta + endogBias + randn(rngOLS) * 1.1);
const drawsIV = Array.from({ length: Nmax }, () => trueBeta + randn(rngIV) * 1.6);
function runningAvg(draws) {
const out = [];
let sum = 0;
for (let i = 0; i < draws.length; i++) { sum += draws[i]; out.push(sum / (i + 1)); }
return out;
}
const avgOLS = runningAvg(drawsOLS), avgIV = runningAvg(drawsIV);
const n = n_iv;
const width = 720, height = 300;
const padL = 55, padB = 40, padT = 20, padR = 15;
const yMin = trueBeta - 2, yMax = trueBeta + 3;
const xScale = (i) => padL + (i / Nmax) * (width - padL - padR);
const yScale = (y) => height - padB - ((y - yMin) / (yMax - yMin)) * (height - padB - padT);
const svgns = "http://www.w3.org/2000/svg";
function el(tag, attrs, text) {
const e = document.createElementNS(svgns, tag);
for (const key in attrs) e.setAttribute(key, attrs[key]);
if (text != null) e.textContent = text;
return e;
}
const svg = el("svg", { viewBox: `0 0 ${width} ${height}`, style: `width:100%; max-width:620px; height:auto; display:block; margin:0 auto;` });
svg.append(el("line", { x1: padL, y1: yScale(trueBeta), x2: width - padR, y2: yScale(trueBeta), stroke: "#008200", "stroke-dasharray": "4,3" }));
svg.append(el("text", { x: width - padR, y: yScale(trueBeta) - 6, "text-anchor": "end", "font-size": 11, fill: "#008200" }, "true β"));
function path(avg, color) {
let d = "";
for (let i = 0; i < n; i++) d += (i === 0 ? "M" : "L") + xScale(i).toFixed(1) + "," + yScale(avg[i]).toFixed(1) + " ";
svg.append(el("path", { d, fill: "none", stroke: color, "stroke-width": 2.4 }));
svg.append(el("circle", { cx: xScale(n - 1), cy: yScale(avg[n - 1]), r: 4.5, fill: color }));
return avg[n - 1];
}
const finalOLS = path(avgOLS, "#C80000");
const finalIV = path(avgIV, "#005AB4");
svg.append(el("line", { x1: padL, y1: height - padB, x2: width - padR, y2: height - padB, stroke: "#333" }));
svg.append(el("line", { x1: padL, y1: padT, x2: padL, y2: height - padB, stroke: "#333" }));
svg.append(el("text", { x: width / 2, y: height - 6, "text-anchor": "middle", "font-size": 12 }, "n (sample size)"));
svg.append(el("text", { x: 14, y: height / 2, "text-anchor": "middle", "font-size": 12, transform: `rotate(-90 14 ${height / 2})` }, "β̂ₙ"));
const legend = [[`OLS → ${finalOLS.toFixed(2)} (biased)`, "#C80000"], [`IV/GMM → ${finalIV.toFixed(2)} (consistent)`, "#005AB4"]];
legend.forEach(([label, color], i) => {
const lx = padL + 8, ly = padT + 14 + i * 18;
svg.append(el("line", { x1: lx, y1: ly, x2: lx + 22, y2: ly, stroke: color, "stroke-width": 3 }));
svg.append(el("text", { x: lx + 28, y: ly + 4, "font-size": 12 }, label));
});
return svg;
}J-Test for Over-Identification
When \(q > p\), the extra moment conditions can be used to test the model itself: \[J = n \cdot \bar{g}_n(\hat{\theta})'W\bar{g}_n(\hat{\theta}) \xrightarrow{d} \chi^2_{q-p}\] A large \(J\) suggests some moment condition — and so the model — may be wrong.
Key Insight
Many familiar estimators are just GMM with a particular choice of moment conditions and weights.
Good fit when…
Watch out for…
| Property | MM | GMM | LS | MLE | UMVUE |
|---|---|---|---|---|---|
| Always exists | ✅ | ✅ | ✅ | ✅ | ❌ |
| Unbiased | ❌ | ❌ | ✅¹ | ❌ | ✅ |
| Consistent | ✅ (usually) | ✅ | ✅ | ✅ | ✅ |
| Asymptotically efficient | ❌ | ❌ | ✅² | ✅ | ✅ |
| Needs a distribution | ❌ | ❌ | ❌ | ✅ | ✅ |
¹ Linear models ² Normal errors
Rules of Thumb
So far we’ve focused on finding a single “best guess” \(\hat\theta\) — but that’s only part of the story.
Interval Estimation
Bayesian Methods
Robust Estimation
Modern Extensions