learn-statistics-with-phoebe / Builder session 6 of 10
Learn Statistics with Phoebe · Builder session 6 of 10

Estimation and maximum likelihood

Every number you report about Lumen - the mean order value, the conversion rate, the spread - is an estimate of a truth you never see. This session is about doing that honestly: what makes an estimator good, why std quietly divides by N-1, and the one principle that generates almost every estimator you will ever use - maximum likelihood. We prove each claim in code, on Lumen's own numbers.

🟠 Getting real Builders · DA / DE / DS Python · scipy / numpy 45 min
0-3 · Setup 3-15 · Estimators & bias 15-40 · Build the MLE 40-45 · Wrap
Part 0

An estimate is a bet - here is how to make a good one

You compute ov.mean() and call it "the average order value." But it is really your best guess at a population mean you can never fully observe. Estimation theory asks the sharp question behind that guess: across all the samples you might have drawn, does your recipe land on the truth on average (unbiased), and does it home in as data grows (consistent)? And when there is no obvious recipe, maximum likelihood hands you one.

Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today A clean grip on estimator vs estimate, bias, and consistency; a numerical proof of why sample variance divides by N-1 (Bessel's correction); and maximum likelihood estimation built two ways - a hand-rolled log-likelihood grid that peaks exactly at Lumen's 3.2% conversion rate, and scipy's .fit / minimize doing the same job for you.
Part 1 · the vocabulary in code

Estimator, bias, consistency, and the likelihood 7 min live

Four ideas. The first three judge an estimator; the fourth manufactures one from scratch.

What makes an estimator good: two separate questions Unbiased: centered Consistent: narrows ideal estimator Bias asks if an estimator is centered right; consistency asks if it tightens as n grows.
🔍 Click to zoom - a good estimator is both centered right and tightens with more data
log-likelihood candidate p MLE peak makes the data most probable 0.01 0.032 0.05 0.08 MLE of a proportion = the sample proportion itself. p-hat = converted.mean() = 0.032 Maximum likelihood picks the parameter value that makes the observed Lumen data most probable. For a proportion that value is exactly the observed rate, 3.2%.
🔍 Click to zoom - the log-likelihood of Lumen's conversion data peaks at p-hat = 0.032, the sample proportion
LiveThe four ideas, in order3 min▶
  • Estimator vs estimate. An estimator is the recipe - the function of the data, like "take the sample mean." An estimate is the number that recipe spits out on one particular sample. The recipe is what we judge; the number is just today's output.
  • Bias. An estimator is unbiased if, averaged over all possible samples, it lands on the true value. Bias is the systematic gap between the average estimate and the truth - not the noise on any one sample.
  • Consistency. An estimator is consistent if it converges to the truth as n grows. Unbiased is about being centered right; consistent is about tightening onto the target with more data.
  • Likelihood. Given a candidate parameter value, the likelihood is the probability of having observed exactly the data you did. Maximum likelihood estimation (MLE) chooses the parameter value that makes your data most probable - the peak of the curve above.
Why we divide by N-1 (Bessel's correction) Sample variance uses 1/(N-1), not 1/N. The reason: you measure spread around the sample mean, but the sample mean is itself pulled toward your data, so squared deviations from it are systematically too small. Dividing by N (ddof=0) therefore underestimates the true variance. Dividing by N-1 (ddof=1) corrects the bias exactly. You already spent one degree of freedom estimating the mean.
★ everyone builds · prove it in code

Prove the bias correction, then build the MLE ★ 22 min · everyone builds

We treat the full 50,000-row Lumen dataset as the population (so we know the true answers), then sample from it and watch the estimators behave. First the N vs N-1 story, then maximum likelihood two ways.

N vs N-1, numerically. Take the whole dataset as the population, sample n=30 repeatedly, and compare the two variance recipes to the truth.

bessel.py
import numpy as np, pandas as pd

df = pd.read_parquet("lumen_orders.parquet")
ov = df["order_value"].to_numpy()

true_var = ov.var(ddof=0)          # variance of the whole population ~ 38^2 ~ 1450
rng = np.random.default_rng(7)

biased, unbiased = [], []
for _ in range(5000):
    s = rng.choice(ov, size=30, replace=False)
    biased.append(np.var(s, ddof=0))    # divide by N   - the tempting one
    unbiased.append(np.var(s, ddof=1))  # divide by N-1 - Bessel

print("true variance      :", round(true_var, 1))
print("mean of /N   (ddof=0):", round(np.mean(biased), 1))    # LOW - underestimates
print("mean of /N-1 (ddof=1):", round(np.mean(unbiased), 1))  # matches true_var
# ddof=0 lands ~ (n-1)/n = 29/30 of the truth; ddof=1 is unbiased.

The ddof=0 average sits about 3% low - exactly the factor (n-1)/n = 29/30. The ddof=1 average lands on the truth. That is Bessel's correction, not as a rule to memorize but as a fact you just measured.

The N-1 correction, measured over repeated n=30 samples True variance 100% ddof=1 estimate (N-1) 100% ddof=0 estimate (N) ~97% Averaged over many n=30 samples, ddof=0 sits low by exactly the factor (n-1)/n = 29/30.
🔍 Click to zoom - dividing by N instead of N-1 leaves variance about 3% low

MLE of a proportion = the sample proportion. Build the log-likelihood of the conversion data over a grid of candidate p and find its peak.

mle_proportion.py
rng = np.random.default_rng(42)
converted = rng.random(ov.size) < 0.032   # the Lumen conversion flag, ~3.2% true

k = converted.sum()          # number of conversions
n = converted.size           # number of sessions
print("observed rate:", round(k / n, 4))   # ~ 0.032

# log-likelihood of Binomial(n, p) over a grid of candidate p
p_grid = np.linspace(0.001, 0.08, 800)
loglik = k * np.log(p_grid) + (n - k) * np.log(1 - p_grid)

p_hat = p_grid[np.argmax(loglik)]
print("MLE p-hat    :", round(p_hat, 4))    # ~ 0.032 - the peak
print("sample prop  :", round(k / n, 4))    # ~ 0.032 - identical
# The MLE of a proportion IS the sample proportion. The curve peaks at 3.2%.
State it plainly For a proportion, maximum likelihood gives you back exactly the observed rate - the MLE of p is converted.mean() = 0.032. The grid and the curve are not doing anything mysterious; they are confirming that the value which makes Lumen's conversion data most probable is the rate you already counted.

MLE via scipy. Same principle, real machinery: fit a distribution to order value, or minimize the negative log-likelihood by hand.

mle_scipy.py
from scipy import stats
from scipy.optimize import minimize

# (a) let scipy fit a lognormal to the right-skewed order values
shape, loc, scale = stats.lognorm.fit(ov, floc=0)   # floc=0 pins location
print("lognorm sigma-hat:", round(shape, 3))   # ~ 0.55 - the value b1 generated with
print("lognorm scale    :", round(scale, 1))   # ~ exp(mu), near the median

# (b) roll your own: minimize the NEGATIVE log-likelihood of a Normal on log(ov)
logs = np.log(ov)
def neg_loglik(params, data):
    mu, sigma = params
    if sigma <= 0:
        return np.inf
    return -np.sum(stats.norm.logpdf(data, loc=mu, scale=sigma))

res = minimize(neg_loglik, x0=[logs.mean(), 1.0], args=(logs,))
mu_hat, sigma_hat = res.x
print("mu-hat   :", round(mu_hat, 3))     # ~ 4.0  (matches log-mean)
print("sigma-hat:", round(sigma_hat, 3))  # ~ 0.55 (matches log-std, ddof=0)
# MLE of a Normal mean IS the sample mean; MLE of its variance divides by N.

A confidence interval for an estimate, by bootstrap. A quick callback to b5 - wrap any estimate in a CI without a formula.

boot_ci.py
rng = np.random.default_rng(42)
B = 2000
boot = np.empty(B)
for b in range(B):
    boot[b] = rng.choice(ov, size=ov.size, replace=True).mean()

ci = np.percentile(boot, [2.5, 97.5])
print("estimate      :", round(ov.mean(), 2))          # ~ 74
print("95% bootstrap CI:", [round(ci[0], 2), round(ci[1], 2)])  # ~ [73.7, 74.3]
# Same tool works for the MLE of any parameter - resample, refit, read the spread.
Real world

When a Lumen analyst reports "conversion is 3.2%," maximum likelihood is the quiet justification: 3.2% is the single value that makes the month's click-and-buy data most probable. And when finance asks "how sure are you," the bootstrap CI is the answer - not a hand-wave, a range you can defend. Estimation is where a number stops being a spreadsheet cell and becomes a claim you can stand behind. Next session turns that range into a formal confidence interval.

Before Session 7

This week ◐ 40 min total

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why does sample variance divide by N-1 instead of N?

Squared deviations are measured around the sample mean, which is itself pulled toward the data, so they come out too small. Dividing by N (ddof=0) is biased low by exactly (n-1)/n; dividing by N-1 (ddof=1) makes the estimator unbiased.

2 · You build the log-likelihood of Lumen's converted flag over a grid of candidate p. Where does it peak?

Maximum likelihood picks the parameter that makes the observed data most probable. For a binomial proportion that value is exactly the observed rate, so the curve peaks at converted.mean() ~= 0.032.

3 · What is the difference between an estimator and an estimate?

An estimator like "take the sample mean" is a rule. An estimate is what that rule outputs on a particular dataset. Bias and consistency are properties of the estimator, judged across all samples, not of any single estimate.

Source material

What this session covers

This builder session covers ~80% of the computational estimation content in the sources below, on the Lumen dataset. MLE is lightly covered in most intro syllabi - we lean harder on it here because it is the principle behind nearly every estimator you will meet later.

✓Estimator vs estimate, bias, consistency; N-1 proved numericallyPart 1 + demo
✓MLE by grid, by scipy .fit, and by minimize on the neg log-likelihooddemo
◐OpenIntro Statistics ch. 5 - Foundations for inferencetheory spine; free PDF
◐StatQuest - Maximum Likelihood, clearly explainedshort video; watch first
◐StatQuest - Why we divide by N-1 (Bessel's correction)MLE is lightly covered elsewhere by design

Builder Session 6 cheat sheet · pin this

Estimator vs estimateThe recipe vs the number. We judge the recipe (bias, consistency), report the number.
BiasSystematic gap between the average estimate and the truth. Not the noise on one sample.
N-1 (Bessel)np.var(x, ddof=1). Dividing by N is biased low by (n-1)/n; N-1 is unbiased.
LikelihoodProbability of the observed data given a parameter. MLE = the value that maximizes it.
MLE of a proportion= the sample proportion. Lumen's converted MLE is 0.032, full stop.
MLE in scipystats.lognorm.fit / norm.fit, or minimize on the negative log-likelihood.