grid <- seq(0, 10, length.out = 1000)
dens <- data.frame(x = grid, y = dexp(grid))
p_dens <- ggplot(dens, aes(x, y)) +
geom_line(colour = smu_red, linewidth = 1) +
labs(x = "Spending (tens of dollars)",
y = "Density")Lecture 10: Bootstrap and Resampling Methods
29 September 2026
| Estimate | Computed from | What it cannot tell us on its own |
|---|---|---|
| Average online spending of 10,000 households: $1,946 a year | one sample | how far the population average could be from it |
| Share of surveyed voters backing a candidate | one survey | whether another survey would agree |
| A regression coefficient | one dataset | whether the effect is distinguishable from zero |
Every estimate comes from one sample. Today we ask how much it would move in another sample or to what extent we can quantify the uncertainty given the sample we have: first with theory (the law of large numbers and the central limit theorem), then by resampling the data we already have (the bootstrap).
Other bootstrap intervals, a regression preview and two failure cases follow the main lesson.
Run the examples from the lectures/ directory.
| Data | Unit | Used for |
|---|---|---|
| Simulated draws from an exponential distribution | fictitious online shoppers | sampling, LLN, CLT, bootstrap |
data/web-browsers.csv |
10,000 households, one year of online spending | the bootstrap on real data |
Random numbers come from R’s pseudo-random generator, so set.seed() makes every chunk reproducible. The appendix also uses the boot package.
Two kinds of objects, and why the distinction matters
The population lets us use the language of probability theory to
Sometimes the population is not a literal set of people. What is the population when we observe one time series of US GDP? Such conceptual questions matter, but we set them aside in this course.
Every object we meet in this course is either a population object or a sample object. It is critical to know which.
| Sample object (computed from the data) | Population object (never observed) |
|---|---|
| average \bar{X} | expectation \mathbf{E}[X] |
| proportion | probability |
| histogram | density or mass function |
| model estimate \hat{\beta} | model parameter \beta |
Sample objects are things we see directly in the data. Population objects are things we never see but want to learn about.
The rest of today asks how well a sample object approximates its population counterpart, and how to measure the gap.
Suppose the amount an Amazon customer spends follows an exponential distribution with mean 1, in tens of dollars. dexp() is its density.
Most customers spend little and a few spend a lot.

The shaded area is the population proportion of customers who spend at most $10, or equivalently the probability that a random customer spends at most $10.
pexp() is the cumulative distribution function: pexp(x) is the probability of spending at most x (times ten dollars).
Is this a population or a sample quantity?

| Prefix | Meaning | Exponential | Normal |
|---|---|---|---|
d |
density (or mass) function | dexp() |
dnorm() |
p |
cumulative probability \mathbf{P}(X \le x) | pexp() |
pnorm() |
q |
quantile, the inverse of p |
qexp() |
qnorm() |
r |
random draws | rexp() |
rnorm() |
The same pattern covers unif, binom, pois, t, chisq and many more; see ?Distributions.
The d, p and q functions describe population objects. Only r produces a sample.
## [1] 0.18 0.15 0.14 0.44 2.89 1.23 0.54 0.96 0.15 1.39 0.76 1.24 4.42 1.05 1.04 1.88 0.65 0.34 0.59
## [20] 2.36
This draws 20 customers from the fictitious population.
The econometrician sees only these 20 numbers, never the curve behind them.
The bars between 0 and 1 give the sample fraction spending at most $10: 0.55 against the population value 0.632. The sample mean 1.12 sits closer to \mathbf{E}[X] = 1.

set.seed(1)
sizes <- c(20, 100, 500, 2500)
samples <- lapply(sizes, rexp) # a list of four samples
sample_plot <- function(s) {
ggplot(data.frame(spend = s), aes(spend)) +
geom_histogram(aes(y = after_stat(density)), binwidth = 0.5, boundary = 0,
fill = smu_blue, colour = "white") +
geom_line(data = dens, aes(x, y), colour = smu_red, linewidth = 1) +
coord_cartesian(xlim = c(0, 10), ylim = c(0, 1)) +
labs(title = paste("n =", length(s)), x = "Spending (tens of dollars)", y = "Density")
}
t(sapply(samples, function(s) c(n = length(s), fraction = mean(s <= 1), mean = mean(s))))## n fraction mean
## [1,] 20 0.550 1.0893254
## [2,] 100 0.600 0.9902038
## [3,] 500 0.624 0.9818994
## [4,] 2500 0.618 1.0400167

The histogram tracks the density more closely as n grows, and the fraction and mean settle near 0.632 and 1: the law of large numbers at work.
mean(spend <= 1) and pexp(1): which is a sample object and which a population object?What repeated sampling does to the sample mean
A random sample is n independent draws from the population distribution: \{X_i\}_{i=1}^n = \{X_1, \dots, X_n\}, \qquad \bar{X} = \frac{1}{n} \sum_{i=1}^n X_i .
X denotes an arbitrary draw. Its expectation \mathbf{E}[X] is the average of X in the population, and \text{Var}(X) = \mathbf{E}[(X - \mathbf{E}[X])^2] measures its spread. For the exponential distribution above, \mathbf{E}[X] = 1 and \text{Var}(X) = 1.
Two rules we will use:
| Rule | Statement | Needs |
|---|---|---|
| Linearity | \mathbf{E}[aX + bY] = a\,\mathbf{E}[X] + b\,\mathbf{E}[Y] | nothing |
| Variance of a sum | \text{Var}(\sum_{i=1}^n X_i) = \sum_{i=1}^n \text{Var}(X_i) | independence |
Law of large numbers (LLN). As n gets large, \bar{X} \approx \mathbf{E}[X].
More precisely: for any tolerance \varepsilon > 0, \mathbf{P}\left(\lvert \bar{X} - \mathbf{E}[X] \rvert > \varepsilon\right) \to 0 as n \to \infty.
In words, the average of a large random sample is close to the population expectation. This is what the simulation showed.
We can prove it in three steps: compute \mathbf{E}[\bar{X}], compute \text{Var}(\bar{X}), and combine them.
\begin{aligned} \mathbf{E}[\bar{X}] &= \mathbf{E}\left[\frac{1}{n} \sum_{i=1}^n X_i\right] = \frac{1}{n} \sum_{i=1}^n \mathbf{E}[X_i] \\ &= \frac{1}{n} \sum_{i=1}^n \mathbf{E}[X] = \mathbf{E}[X]. \end{aligned}
The second equality is linearity: the expectation of a sum is the sum of the expectations. The third uses that every X_i has the same distribution as X.
What does \mathbf{E}[\bar{X}] mean?
A single \bar{X} need not be close to \mathbf{E}[X] when n is small; it fluctuates around it. How much is measured by its variance:
\begin{aligned} \text{Var}(\bar{X}) &= \text{Var}\left(\frac{1}{n} \sum_{i=1}^n X_i\right) = \frac{1}{n^2}\, \text{Var}\left(\sum_{i=1}^n X_i\right) \\ &= \frac{1}{n^2} \sum_{i=1}^n \text{Var}(X_i) = \frac{1}{n}\, \text{Var}(X). \end{aligned}
The second equality pulls the constant 1/n out as its square. The third uses independence: the variance of a sum of independent draws is the sum of the variances.
As n grows, \text{Var}(\bar{X}) shrinks towards zero. What does this say about the value of \bar{X}?
Chebyshev’s inequality bounds the probability of a large deviation by the variance:
\mathbf{P}\left(\lvert \bar{X} - \mathbf{E}[X] \rvert > \varepsilon\right) \le \frac{\text{Var}(\bar{X})}{\varepsilon^2} = \frac{\text{Var}(X)}{n\, \varepsilon^2} \longrightarrow 0 .
To recap: the typical value of \bar{X} is \mathbf{E}[X], because \mathbf{E}[\bar{X}] = \mathbf{E}[X], and its spread shrinks with n, because \text{Var}(\bar{X}) = n^{-1}\,\text{Var}(X). Together they give \bar{X} \approx \mathbf{E}[X].
Why we care. To learn the expectation of a population distribution, such as the average amount a customer spends, a large random sample does very well. At n = 2{,}500 the standard deviation of \bar{X} is 1/\sqrt{2500} = 0.02.
By now we understand the population distribution of X: monitor a random Amazon customer who just filled a basket and record the total. Do this for the universe of customers and the histogram of totals is the distribution of X.
Like X, \bar{X} is a random variable, so it has a population distribution too. How do we think of it?
Central limit theorem (CLT). When n is large, the distribution of \bar{X} is close to a normal distribution with the expectation and variance we just derived: \bar{X} \approx \mathcal{N}\!\left(\mathbf{E}[X],\; n^{-1}\,\text{Var}(X)\right).
Formally, \sqrt{n}\,(\bar{X} - \mathbf{E}[X]) converges in distribution to \mathcal{N}(0, \text{Var}(X)), whatever the shape of the distribution of X, provided \text{Var}(X) is finite.
The exponential distribution is far from normal, so this is a strong claim. Let us check it by simulation.
Follow the recipe: draw a sample of size n and compute \bar{X}; do this B times; compare the histogram of \bar{X}_1, \dots, \bar{X}_B with the normal density \mathcal{N}(1, 1/n).
## mean variance 1/n
## 0.998033537 0.009626014 0.010000000
The average of the sample means is close to \mathbf{E}[X] = 1 and their variance is close to \text{Var}(X)/n = 0.01.
mean_plot <- function(m, n) {
grid <- seq(0.6, 1.4, length.out = 400)
normal <- data.frame(x = grid, y = dnorm(grid, mean = 1, sd = 1 / sqrt(n)))
ggplot(data.frame(m), aes(m)) +
geom_histogram(aes(y = after_stat(density)), bins = 30,
fill = smu_blue, colour = "white") +
geom_line(data = normal, aes(x, y), colour = smu_red, linewidth = 1) +
coord_cartesian(xlim = c(0.6, 1.4)) +
labs(title = paste0("n = ", n, ", B = ", length(m)),
x = "Sample mean", y = "Density")
}The red curve is the CLT’s prediction \mathcal{N}(1, 1/n), not a curve fitted to the histogram.

A thousand sample means, each from 100 draws of a skewed population, follow the normal density closely.
## mean variance 1/n
## 0.999790718 0.001994272 0.002000000

Same axes: the spread shrinks by \sqrt{5}, from a standard deviation of 0.10 to 0.045.
Turning the CLT into a statement about one sample
In practice we see one sample and want to know how far \bar{X} could be from \mathbf{E}[X].
The CLT says the spread of \bar{X} is \text{SD}(\bar{X}) = \sqrt{\text{Var}(X)/n}. We do not know \text{Var}(X), so we replace it by its sample analogue:
\text{SE}(\bar{X}) = \frac{\widehat{\text{SD}}(X)}{\sqrt{n}}, \qquad \widehat{\text{SD}}(X) = \sqrt{\frac{1}{n-1} \sum_{i=1}^n (X_i - \bar{X})^2},
where \widehat{\text{SD}}(X) is the sample standard deviation, sd() in R.
A standard error is the estimated standard deviation of an estimator across repeated samples.
\bar{X} \pm 2 \cdot \text{SE}(\bar{X})
| Why … | Because … |
|---|---|
| … normal? | \bar{X} is approximately normal by the CLT. |
| … 2? | The middle 95% of a normal distribution lies within 1.96 standard deviations of its mean, and 1.96 \approx 2. |
| … this SE? | \text{SE}(\bar{X}) approximates \text{SD}(\bar{X}) = \text{Var}(\bar{X})^{1/2}. |
If we knew the distribution of \bar{X}, neither would be needed. Compute the interval for one sample of 500 customers:
## estimate se
## 1.05845529 0.04701586
## [1] 0.9644236 1.1524870
The interval covers the population mean of 1, which in a real application we would not know.
The interval is random and \mathbf{E}[X] = 1 is fixed. Over repeated samples, 95% of the intervals should cover 1. In the R world we can check:
## 10 20 100 500
## 0.8715 0.9185 0.9555 0.9625
With a skewed population and n = 10, the normal approximation is poor and the interval covers too rarely. By n = 100 it keeps its promise.
In the fictitious world of our R code we can draw \bar{X} B times for a very large B. The histogram of those draws is the distribution of \bar{X}, and we can read standard errors and quantiles straight off it.
In reality we never get to do this. We draw one sample \{X_i\}_{i=1}^n and compute one \bar{X}.
We cannot draw more samples from the population. Can we draw from something that resembles it?
Resample the sample to mimic resampling the population
The bootstrap approximates the population distribution of an estimator (here \bar{X}) by the distribution of the same estimator across resamples of the data.
It is all-purpose: the same recipe works for means, medians, ratios, regression coefficients and much more.

This is exactly what the Monte Carlo loop did. It requires the population, which we never have.

Everything below the top panel is the same recipe as before. Only the source of the draws has changed.
“With replacement” matters: a bootstrap sample repeats some observations and omits others. Without replacement, every resample would be the original sample in a different order.
The bootstrap standard error \text{SE}_{\text{boot}} is the standard deviation of \{\bar{X}_b\}_{b=1}^B, an alternative to \widehat{\text{SD}}(X)/\sqrt{n}.
Two 95% confidence intervals:
| Interval | Formula | Uses |
|---|---|---|
| Normal | \bar{X} \pm 2 \cdot \text{SE}_{\text{boot}} | the bootstrap spread and the normal shape |
| Quantile | \bar{X} \pm q_{0.95}, where q_{0.95} is the 0.95 quantile of \lvert \bar{X}_b - \bar{X} \rvert over b | the bootstrap distribution directly |
The quantile interval is preferred, because a few extreme bootstrap draws can inflate \text{SE}_{\text{boot}}. Standard errors are nonetheless reported more often because they are simpler.
We have the sample of n = 500 customers in spend. Put X_1, \dots, X_n in a bag and draw from it n times with replacement:
## [1] 3 4 5 6 6 6 7 8 10 11
## [1] 1.037495
Each observation is left out of a bootstrap sample with probability (1 - 1/n)^n \approx e^{-1} = 0.37, so about 63% of the original observations appear at least once.
## sample_mean boot_se
## 1.0584553 0.0468566
This is the CLT loop with one change: spend[idx] replaces rexp(n). We draw from the sample instead of the population.
In the R world we can also draw the truth for comparison:
grid <- seq(0.85, 1.25, length.out = 400)
draws <- bind_rows(
data.frame(source = "Bootstrap: resample the sample", m = boot_means),
data.frame(source = "Truth: redraw from the population", m = true_means))
normals <- bind_rows(
data.frame(source = "Bootstrap: resample the sample", x = grid,
y = dnorm(grid, mean(spend), sd(spend) / sqrt(n))),
data.frame(source = "Truth: redraw from the population", x = grid,
y = dnorm(grid, 1, 1 / sqrt(n))))
p_compare <- ggplot(draws, aes(m)) +
geom_histogram(aes(y = after_stat(density)), bins = 30, fill = smu_blue, colour = "white") +
geom_line(data = normals, aes(x, y), colour = smu_red, linewidth = 1) +
geom_vline(xintercept = 1, linetype = "dashed") +
facet_wrap(~ source) +
labs(x = "Sample mean", y = "Density")The red curves are the normal approximations: \mathcal{N}(\bar{X}, \text{SE}^2) for the bootstrap and the CLT’s \mathcal{N}(1, 1/n) for the truth. The dashed line marks \mathbf{E}[X] = 1.

Nearly the same shape and spread. The bootstrap distribution is centred at the sample mean \bar{X} = 1.06 rather than at 1, because it is built from the sample.
## bootstrap formula monte_carlo exact
## 0.04685660 0.04701586 0.04604075 0.04472136
## lower upper
## boot_normal 0.9647421 1.152168
## boot_quantile 0.9689625 1.147948
## formula 0.9644236 1.152487
Four routes to the same spread, and three intervals that agree to within 0.005. The truth is close to normal by the CLT, so the bootstrap distribution is too.
The bootstrap on 10,000 households
## [1] 10000 7
## id anychildren broadband hispanic race region spend
## 1 1 0 1 0 white MW 424
## 2 2 1 1 0 white MW 2335
## 3 3 1 1 0 white MW 279
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1 162 510 1946 1523 401338
One row per household: annual online spending in dollars plus a few demographics. The mean is almost four times the median, the sign of a heavy right tail.
p_raw <- ggplot(browser, aes(spend)) +
geom_histogram(bins = 30, fill = smu_blue,
colour = "white") +
labs(x = "Spending (dollars)", y = "Households")
p_log <- ggplot(browser, aes(log(spend))) +
geom_histogram(bins = 30, fill = smu_gold,
colour = "white") +
labs(x = "log(spending)", y = "Households")In dollars, a handful of households with enormous spending stretch the axis. In logs the distribution is roughly bell-shaped.
The CLT still applies to the mean in dollars, but the heavy tail is why a large n helps.

## mean boot_se formula_se
## 1946.43930 79.47364 80.38610
The same loop as before, with browser$spend in place of the simulated sample. The two standard errors differ by about a dollar.
## lower upper
## boot_normal 1787 2105
## boot_quantile 1792 2101
## formula 1786 2107

The average household spends about $1,950 a year online, and the population average is plausibly between $1,790 and $2,105.
The loop changes only in the statistic it computes, so wrap it:
The median of spending has no simple standard-error formula: it involves the density of spending at the median, which we do not know. The bootstrap needs one line:
Households with broadband spend on average about twice as much as those without. To bootstrap the ratio, resample rows, so that each household keeps its own spend and broadband:
## ratio boot_se
## 2.0399882 0.1884974
## [1] 1.663042 2.416935
A delta-method formula for the standard error of a ratio exists, but it is tedious to derive. The bootstrap is three lines. Resampling rows is also how the bootstrap handles regression coefficients (appendix).
You want a 95% interval for (a) the share of households spending more than $5,000 a year and (b) the difference in median spending between households with and without children.
Two kinds of objects. Sample objects (averages, fractions, histograms, estimates) approximate population objects (expectations, probabilities, densities, parameters). Know which is which.
Two theorems. The law of large numbers says \bar{X} \approx \mathbf{E}[X] because \text{Var}(\bar{X}) = \text{Var}(X)/n shrinks. The central limit theorem adds the shape: approximately normal.
One sample, one interval. \text{SE}(\bar{X}) = \widehat{\text{SD}}(X)/\sqrt{n}, and \bar{X} \pm 2 \cdot \text{SE} covers the truth in about 95% of samples once n is moderate.
Resample when there is no formula. The bootstrap draws with replacement from the sample, recomputes the estimator, and reads the standard error and quantiles off the result. It works where the usual interval works.
Distributions, sample, replicate, set.seed.boot package (Canty and Ripley) implements the bootstrap and several confidence-interval methods.More bootstrap intervals, the boot package, a regression preview, and two failure cases
The bootstrap distribution supports several intervals. For the simulated sample of 500 customers:
q <- quantile(boot_means, c(0.025, 0.975))
q95_sim <- unname(quantile(abs(boot_means - mean(spend)), 0.95))
ci <- rbind(boot_normal = mean(spend) + c(-2, 2) * sd(boot_means),
symmetric_quantile = mean(spend) + c(-1, 1) * q95_sim,
percentile = q,
basic = 2 * mean(spend) - rev(q))
colnames(ci) <- c("lower", "upper")
ci## lower upper
## boot_normal 0.9647421 1.152168
## symmetric_quantile 0.9689625 1.147948
## percentile 0.9723376 1.152070
## basic 0.9648407 1.144573
boot packageboot() takes the data and a function of the data and an index vector; boot.ci() computes the intervals.
## original boot_se
## 1946.43930 81.84329
## BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS
## Based on 1000 bootstrap replicates
##
## CALL :
## boot.ci(boot.out = b, type = c("norm", "perc", "basic"))
##
## Intervals :
## Level Normal Basic Percentile
## 95% (1786, 2107 ) (1770, 2091 ) (1802, 2123 )
## Calculations and Intervals on Original Scale
Lecture 10 regresses log spending on broadband access. The pairs bootstrap resamples rows and refits the model:
## Estimate Std. Error
## (Intercept) 5.732062 0.03957177
## broadband 0.555759 0.04356445
## formula_se boot_se
## 0.04356445 0.04427595
The regression standard error assumes errors with constant variance; the pairs bootstrap does not. Here the two agree.
B controls only the Monte Carlo error of the bootstrap approximation. Re-run the mean of spending with different B:
## B100 B1000 B10000
## 84.27191 81.58051 80.48366
A rule of thumb: the Monte Carlo standard error of \text{SE}_{\text{boot}} is roughly \text{SE}_{\text{boot}}/\sqrt{2B}, about 2% of \text{SE}_{\text{boot}} at B = 1{,}000.
Quantile intervals need more replications than standard errors, because they depend on the tails of the bootstrap distribution. B = 1{,}000 or more is common for a 95% interval.
The maximum of m draws from \text{Uniform}(0, 1) estimates the upper end point 1. It is not a smooth function of the data, and the bootstrap breaks:
## sample_max share_at_max boot_sd true_sd
## 0.999366581 0.647000000 0.012257865 0.009201846
About 65% of the bootstrap maxima equal the sample maximum itself, close to the 63% chance that the largest observation is drawn at least once. The bootstrap spread is a third too large, and its shape is wrong.

The true sampling distribution is smooth and skewed towards 1; the bootstrap version is a spike. Estimators that select among models, such as LASSO, are non-smooth in a related way.
Both theorems need moments. The Cauchy distribution has no expectation, and averages of even 100,000 draws do not settle down:
## [1] 0.4744233 5.5317096 -2.2550010 6.8630508 -0.5150997
Heavy tails in economic data, such as firm sizes, city populations or wealth, are usually less extreme than this, but they slow the CLT down: compare the coverage at n = 10 earlier. Log transformations, as with log(spend), tame the tails.
Run once in your R environment if needed:
boot ships with R as a recommended package, so it is usually installed already. The web-browser data are in lectures/data/ and the schematic figures in lectures/images/, so the deck renders without network access.
Where did the estimate come from, and how much would it move in another sample?
The standard error answers with a formula when one exists. The bootstrap answers by resampling when one does not.