This RPubs-ready document develops three central computational ideas in advanced data analysis:
Every major idea is demonstrated through simulation, real data, plots, tables, diagnostics, and student tasks.
After completing the practicals, a student should be able to:
Let \(X_1,\ldots,X_n\) be an observed random sample and let
\[ \widehat\theta = T(X_1,\ldots,X_n) \]
estimate a population parameter \(\theta\). The empirical distribution is
\[ \widehat F_n(x)=\frac{1}{n}\sum_{i=1}^n I(X_i\le x). \]
A nonparametric bootstrap sample is obtained by drawing
\[ X_1^*,\ldots,X_n^* \overset{\text{i.i.d.}}{\sim} \widehat F_n, \]
which is equivalent to sampling the observed data with replacement. The bootstrap statistic is
\[ \widehat\theta^*=T(X_1^*,\ldots,X_n^*). \]
Repeating this \(B\) times produces
\[ \widehat\theta_1^*,\ldots,\widehat\theta_B^*. \]
The bootstrap standard error is
\[ \widehat{\mathrm{SE}}_{\mathrm{boot}}(\widehat\theta) = \left[ \frac{1}{B-1} \sum_{b=1}^{B} \left(\widehat\theta_b^*-\overline{\theta}^*\right)^2 \right]^{1/2}. \]
bootstrap_1d <- function(x, statistic, B = 3000, seed = 2026, ...) {
set.seed(seed)
n <- length(x)
theta_hat <- statistic(x, ...)
theta_star <- replicate(
B,
statistic(sample(x, size = n, replace = TRUE), ...)
)
list(
estimate = theta_hat,
replicates = theta_star,
bias = mean(theta_star) - theta_hat,
se = sd(theta_star),
percentile_ci = unname(quantile(theta_star, c(0.025, 0.975))),
basic_ci = 2 * theta_hat -
rev(unname(quantile(theta_star, c(0.025, 0.975))))
)
}set.seed(12)
x_normal <- rnorm(40, mean = 10, sd = 3)
boot_mean <- bootstrap_1d(x_normal, mean, B = 5000)
mean_summary <- tibble(
Quantity = c("Observed mean", "Bootstrap bias",
"Bootstrap SE", "Formula-based SE",
"Percentile CI lower", "Percentile CI upper"),
Value = c(
boot_mean$estimate,
boot_mean$bias,
boot_mean$se,
sd(x_normal) / sqrt(length(x_normal)),
boot_mean$percentile_ci
)
)
pretty_table(mean_summary, caption =
"Bootstrap analysis of the mean for simulated normal data")| Quantity | Value |
|---|---|
| Observed mean | 9.6497 |
| Bootstrap bias | 0.0046 |
| Bootstrap SE | 0.4165 |
| Formula-based SE | 0.4227 |
| Percentile CI lower | 8.8453 |
| Percentile CI upper | 10.4977 |
tibble(value = boot_mean$replicates) |>
ggplot(aes(value)) +
geom_histogram(aes(y = after_stat(density)),
bins = 38, fill = "#5B8FF9",
color = "white", alpha = 0.9) +
geom_density(color = "#D62728", linewidth = 1.2) +
geom_vline(xintercept = boot_mean$estimate,
color = "#111827", linewidth = 1,
linetype = 2) +
labs(
title = "Bootstrap distribution of the sample mean",
subtitle = "Simulated N(10, 3²) sample, n = 40, B = 5,000",
x = expression(bar(X)^"*"),
y = "Density"
)set.seed(15)
x_exp <- rexp(35, rate = 0.5)
boot_exp_mean <- bootstrap_1d(x_exp, mean, B = 5000)
boot_exp_median <- bootstrap_1d(x_exp, median, B = 5000)
boot_compare <- bind_rows(
tibble(Statistic = "Mean",
Estimate = boot_exp_mean$estimate,
Bias = boot_exp_mean$bias,
SE = boot_exp_mean$se,
Lower = boot_exp_mean$percentile_ci[1],
Upper = boot_exp_mean$percentile_ci[2]),
tibble(Statistic = "Median",
Estimate = boot_exp_median$estimate,
Bias = boot_exp_median$bias,
SE = boot_exp_median$se,
Lower = boot_exp_median$percentile_ci[1],
Upper = boot_exp_median$percentile_ci[2])
)
pretty_table(boot_compare,
caption = "Bootstrap comparison under exponential skewness")| Statistic | Estimate | Bias | SE | Lower | Upper |
|---|---|---|---|---|---|
| Mean | 1.8305 | 0.0087 | 0.3345 | 1.2429 | 2.5404 |
| Median | 1.2149 | 0.0008 | 0.1962 | 0.8265 | 1.5745 |
bind_rows(
tibble(value = boot_exp_mean$replicates, Statistic = "Mean"),
tibble(value = boot_exp_median$replicates, Statistic = "Median")
) |>
ggplot(aes(value, fill = Statistic)) +
geom_density(alpha = 0.55) +
facet_wrap(~ Statistic, scales = "free") +
scale_fill_manual(values = c("Mean" = "#FF9D4D",
"Median" = "#36B37E")) +
labs(
title = "Bootstrap distributions under severe skewness",
subtitle = "The mean and median have visibly different uncertainty",
x = "Bootstrap statistic",
y = "Density"
) +
guides(fill = "none")The most common intervals are:
Normal interval \[ \widehat\theta \pm z_{1-\alpha/2} \widehat{\mathrm{SE}}_{\mathrm{boot}}. \]
Percentile interval \[ \left[ \widehat\theta_{(\alpha/2)}^*, \widehat\theta_{(1-\alpha/2)}^* \right]. \]
Basic interval \[ \left[ 2\widehat\theta-\widehat\theta_{(1-\alpha/2)}^*, 2\widehat\theta-\widehat\theta_{(\alpha/2)}^* \right]. \]
normal_ci <- boot_exp_mean$estimate +
qnorm(c(0.025, 0.975)) * boot_exp_mean$se
ci_table <- tibble(
Method = c("Normal", "Percentile", "Basic"),
Lower = c(normal_ci[1],
boot_exp_mean$percentile_ci[1],
boot_exp_mean$basic_ci[1]),
Upper = c(normal_ci[2],
boot_exp_mean$percentile_ci[2],
boot_exp_mean$basic_ci[2])
)
pretty_table(ci_table,
caption = "Three bootstrap confidence intervals for the mean")| Method | Lower | Upper |
|---|---|---|
| Normal | 1.1749 | 2.4861 |
| Percentile | 1.2429 | 2.5404 |
| Basic | 1.1206 | 2.4181 |
ci_table |>
mutate(Method = factor(Method, levels = rev(Method))) |>
ggplot(aes(y = Method, x = Lower, xend = Upper,
yend = Method, color = Method)) +
geom_segment(linewidth = 3, lineend = "round") +
geom_point(aes(x = Lower), size = 3) +
geom_point(aes(x = Upper), size = 3) +
geom_vline(xintercept = boot_exp_mean$estimate,
linetype = 2, color = "grey25") +
scale_color_manual(values = c(
"Normal" = "#5B8FF9",
"Percentile" = "#36B37E",
"Basic" = "#FF6B6B"
)) +
labs(
title = "Bootstrap confidence-interval comparison",
x = "Parameter value",
y = NULL
) +
guides(color = "none")This simulation checks how often nominal 95% intervals cover the true exponential mean \(2\).
set.seed(25)
one_coverage_run <- function(n = 25, B = 800) {
x <- rexp(n, rate = 0.5)
b <- bootstrap_1d(x, mean, B = B, seed = sample.int(1e7, 1))
normal <- b$estimate + qnorm(c(0.025, 0.975)) * b$se
c(
Normal = normal[1] <= 2 && 2 <= normal[2],
Percentile = b$percentile_ci[1] <= 2 && 2 <= b$percentile_ci[2],
Basic = b$basic_ci[1] <= 2 && 2 <= b$basic_ci[2]
)
}
coverage_raw <- replicate(250, one_coverage_run())
coverage <- rowMeans(coverage_raw)
coverage_table <- tibble(
Method = names(coverage),
Empirical_Coverage = as.numeric(coverage),
Nominal_Coverage = 0.95
)
pretty_table(coverage_table,
caption = "Empirical 95% coverage in an exponential-mean experiment")| Method | Empirical_Coverage | Nominal_Coverage |
|---|---|---|
| Normal | 0.920 | 0.95 |
| Percentile | 0.924 | 0.95 |
| Basic | 0.900 | 0.95 |
coverage_table |>
ggplot(aes(Method, Empirical_Coverage, fill = Method)) +
geom_col(width = 0.68) +
geom_hline(yintercept = 0.95, linetype = 2,
linewidth = 1, color = "#B91C1C") +
scale_y_continuous(labels = percent_format(accuracy = 1),
limits = c(0, 1)) +
scale_fill_manual(values = c(
"Normal" = "#5B8FF9",
"Percentile" = "#36B37E",
"Basic" = "#FF9D4D"
)) +
labs(
title = "Do nominal 95% intervals really cover 95%?",
subtitle = "250 simulated samples; dashed line is nominal coverage",
x = NULL, y = "Empirical coverage"
) +
guides(fill = "none")mtcarscor_stat <- function(data, indices) {
d <- data[indices, ]
cor(d$mpg, d$wt)
}
set.seed(102)
boot_mtcars <- boot(mtcars, statistic = cor_stat, R = 5000)
mtcars_boot_summary <- tibble(
Quantity = c("Observed correlation",
"Bootstrap bias",
"Bootstrap SE"),
Value = c(boot_mtcars$t0,
mean(boot_mtcars$t) - boot_mtcars$t0,
sd(boot_mtcars$t))
)
pretty_table(mtcars_boot_summary,
caption = "Bootstrap analysis of correlation: mpg versus wt")| Quantity | Value |
|---|---|
| Observed correlation | -0.8677 |
| Bootstrap bias | -0.0028 |
| Bootstrap SE | 0.0340 |
tibble(correlation = as.numeric(boot_mtcars$t)) |>
ggplot(aes(correlation)) +
geom_histogram(bins = 35, fill = "#7C3AED",
color = "white", alpha = 0.9) +
geom_vline(xintercept = boot_mtcars$t0,
linetype = 2, linewidth = 1.1) +
labs(
title = "Bootstrap distribution of correlation",
subtitle = "Real data: mtcars, mpg versus wt",
x = "Bootstrap correlation",
y = "Frequency"
)slope_stat <- function(data, indices) {
coef(lm(mpg ~ wt + hp, data = data[indices, ]))["wt"]
}
set.seed(103)
boot_slope <- boot(mtcars, statistic = slope_stat, R = 5000)
fit_mtcars <- lm(mpg ~ wt + hp, data = mtcars)
reg_table <- tibble(
Method = c("Ordinary least squares",
"Pairs bootstrap"),
Estimate = c(coef(fit_mtcars)["wt"], boot_slope$t0),
Standard_Error = c(
summary(fit_mtcars)$coef["wt", "Std. Error"],
sd(boot_slope$t)
)
)
pretty_table(reg_table,
caption = "Model-based and pairs-bootstrap uncertainty for wt")| Method | Estimate | Standard_Error |
|---|---|---|
| Ordinary least squares | -3.8778 | 0.6327 |
| Pairs bootstrap | -3.8778 | 0.7111 |
tibble(slope = as.numeric(boot_slope$t)) |>
ggplot(aes(slope)) +
geom_density(fill = "#22C55E", alpha = 0.55,
color = "#166534", linewidth = 1.1) +
geom_vline(xintercept = boot_slope$t0,
linetype = 2, linewidth = 1) +
labs(
title = "Pairs-bootstrap distribution of the weight coefficient",
subtitle = "Model: mpg ~ wt + hp",
x = "Bootstrap coefficient of wt",
y = "Density"
)residual_bootstrap <- function(model, B = 3000, seed = 2026) {
set.seed(seed)
yhat <- fitted(model)
e <- resid(model) - mean(resid(model))
dat <- model.frame(model)
response_name <- names(dat)[1]
replicate(B, {
y_star <- yhat + sample(e, replace = TRUE)
dat[[response_name]] <- y_star
coef(update(model, data = dat))
})
}
resid_boot <- residual_bootstrap(fit_mtcars, B = 4000)
coef_compare <- tibble(
Coefficient = rownames(t(resid_boot)),
Estimate = coef(fit_mtcars),
Residual_Bootstrap_SE = apply(resid_boot, 1, sd)
)
pretty_table(coef_compare,
caption = "Residual-bootstrap standard errors")| Estimate | Residual_Bootstrap_SE |
|---|---|
| 37.2273 | 1.5282 |
| -3.8778 | 0.5978 |
| -0.0318 | 0.0085 |
For the estimator
\[ \widehat\theta=T(X_1,\ldots,X_n), \]
the \(i\)-th delete-one replicate is
\[ \widehat\theta_{(-i)} = T(X_1,\ldots,X_{i-1},X_{i+1},\ldots,X_n). \]
Let
\[ \overline\theta_{(-\cdot)} = \frac1n\sum_{i=1}^n\widehat\theta_{(-i)}. \]
The jackknife bias estimate is
\[ \widehat{\mathrm{Bias}}_{\mathrm{jack}} = (n-1) \left( \overline\theta_{(-\cdot)}-\widehat\theta \right). \]
The bias-corrected jackknife estimator is
\[ \widehat\theta_{\mathrm{jack}} = n\widehat\theta-(n-1)\overline\theta_{(-\cdot)}. \]
The jackknife variance estimate is
\[ \widehat{\mathrm{Var}}_{\mathrm{jack}} = \frac{n-1}{n} \sum_{i=1}^n \left( \widehat\theta_{(-i)} -\overline\theta_{(-\cdot)} \right)^2. \]
jackknife <- function(data, statistic, ...) {
n <- if (is.data.frame(data) || is.matrix(data)) nrow(data) else length(data)
theta_hat <- statistic(data, ...)
theta_delete <- map_dbl(seq_len(n), function(i) {
reduced <- if (is.data.frame(data) || is.matrix(data)) {
data[-i, , drop = FALSE]
} else {
data[-i]
}
statistic(reduced, ...)
})
theta_bar <- mean(theta_delete)
bias <- (n - 1) * (theta_bar - theta_hat)
variance <- (n - 1) / n *
sum((theta_delete - theta_bar)^2)
pseudovalues <- n * theta_hat - (n - 1) * theta_delete
influence <- (n - 1) * (theta_hat - theta_delete)
list(
estimate = theta_hat,
delete_one = theta_delete,
replicate_mean = theta_bar,
bias = bias,
bias_corrected = theta_hat - bias,
variance = variance,
se = sqrt(variance),
pseudovalues = pseudovalues,
influence = influence
)
}set.seed(206)
x_jack <- rnorm(30, 5, 2)
jk_mean <- jackknife(x_jack, mean)
jack_mean_table <- tibble(
Quantity = c("Mean", "Jackknife bias",
"Jackknife SE", "Usual SE"),
Value = c(jk_mean$estimate,
jk_mean$bias,
jk_mean$se,
sd(x_jack) / sqrt(length(x_jack)))
)
pretty_table(jack_mean_table,
caption = "Jackknife and usual standard error for a mean")| Quantity | Value |
|---|---|
| Mean | 5.2081 |
| Jackknife bias | 0.0000 |
| Jackknife SE | 0.2912 |
| Usual SE | 0.2912 |
tibble(
Deleted = seq_along(jk_mean$delete_one),
Replicate = jk_mean$delete_one
) |>
ggplot(aes(Deleted, Replicate)) +
geom_line(color = "#2563EB", linewidth = 0.8) +
geom_point(aes(color = abs(Replicate - mean(Replicate))),
size = 2.8) +
geom_hline(yintercept = jk_mean$estimate,
linetype = 2) +
scale_color_gradient(low = "#93C5FD", high = "#DC2626") +
labs(
title = "Delete-one means",
subtitle = "Points farther from the dashed line have greater influence",
x = "Deleted observation",
y = "Leave-one-out mean",
color = "Absolute change"
)biased_variance <- function(z) mean((z - mean(z))^2)
set.seed(207)
x_var <- rnorm(18, 0, 3)
jk_var <- jackknife(x_var, biased_variance)
variance_table <- tibble(
Estimator = c("Biased variance, divisor n",
"Jackknife bias-corrected",
"Usual unbiased variance"),
Value = c(jk_var$estimate,
jk_var$bias_corrected,
var(x_var))
)
pretty_table(variance_table,
caption = "Jackknife correction recovers the unbiased variance")| Estimator | Value |
|---|---|
| Biased variance, divisor n | 6.8551 |
| Jackknife bias-corrected | 7.2583 |
| Usual unbiased variance | 7.2583 |
set.seed(208)
ratio_data <- tibble(
x = rgamma(60, shape = 5, rate = 1),
y = 2.4 * x + rnorm(60, sd = 2)
)
ratio_stat <- function(d) mean(d$y) / mean(d$x)
jk_ratio <- jackknife(ratio_data, ratio_stat)
ratio_summary <- tibble(
Quantity = c("Original estimate", "Estimated bias",
"Bias-corrected estimate", "Jackknife SE"),
Value = c(jk_ratio$estimate, jk_ratio$bias,
jk_ratio$bias_corrected, jk_ratio$se)
)
pretty_table(ratio_summary,
caption = "Jackknife analysis of a ratio-of-means estimator")| Quantity | Value |
|---|---|
| Original estimate | 2.3860 |
| Estimated bias | -0.0006 |
| Bias-corrected estimate | 2.3866 |
| Jackknife SE | 0.0538 |
tibble(
Observation = seq_along(jk_ratio$influence),
Influence = jk_ratio$influence
) |>
ggplot(aes(Observation, Influence,
fill = abs(Influence))) +
geom_col() +
scale_fill_gradient(low = "#A7F3D0", high = "#B91C1C") +
geom_hline(yintercept = 0, linewidth = 0.6) +
labs(
title = "Jackknife influence values",
subtitle = "Ratio-of-means simulation",
x = "Observation",
y = "Empirical influence",
fill = "|Influence|"
)irisiris2 <- iris |>
select(Sepal.Length, Petal.Length)
iris_cor <- function(d) cor(d$Sepal.Length, d$Petal.Length)
jk_iris <- jackknife(iris2, iris_cor)
iris_summary <- tibble(
Quantity = c("Correlation", "Bias",
"Bias-corrected correlation", "SE"),
Value = c(jk_iris$estimate, jk_iris$bias,
jk_iris$bias_corrected, jk_iris$se)
)
pretty_table(iris_summary,
caption = "Jackknife correlation analysis for iris")| Quantity | Value |
|---|---|
| Correlation | 0.8718 |
| Bias | -0.0001 |
| Bias-corrected correlation | 0.8719 |
| SE | 0.0173 |
iris_influence <- iris |>
mutate(
Row = row_number(),
Influence = jk_iris$influence
)
top_iris <- iris_influence |>
slice_max(abs(Influence), n = 8) |>
select(Row, Species, Sepal.Length, Petal.Length, Influence)
pretty_table(top_iris,
caption = "Most influential iris observations")| Row | Species | Sepal.Length | Petal.Length | Influence |
|---|---|---|---|---|
| 107 | virginica | 4.9 | 4.5 | -1.1356 |
| 15 | setosa | 5.8 | 1.2 | -0.8556 |
| 16 | setosa | 5.7 | 1.5 | -0.5130 |
| 119 | virginica | 7.7 | 6.9 | 0.4308 |
| 85 | versicolor | 5.4 | 4.5 | -0.4305 |
| 122 | virginica | 5.6 | 4.9 | -0.4137 |
| 19 | setosa | 5.7 | 1.7 | -0.4096 |
| 114 | virginica | 5.7 | 5.0 | -0.3537 |
iris_influence |>
ggplot(aes(Row, Influence, color = Species)) +
geom_hline(yintercept = 0, color = "grey50") +
geom_point(size = 2.2, alpha = 0.85) +
scale_color_manual(values = c(
"setosa" = "#5B8FF9",
"versicolor" = "#F6BD16",
"virginica" = "#E8684A"
)) +
labs(
title = "Observation-level influence on iris correlation",
x = "Row",
y = "Jackknife influence"
)wt_slope <- function(d)
unname(coef(lm(mpg ~ wt + hp, data = d))["wt"])
jk_reg <- jackknife(mtcars, wt_slope)
reg_jk_table <- tibble(
Quantity = c("Slope estimate", "Jackknife bias",
"Bias-corrected slope", "Jackknife SE",
"Model-based SE"),
Value = c(
jk_reg$estimate,
jk_reg$bias,
jk_reg$bias_corrected,
jk_reg$se,
summary(fit_mtcars)$coef["wt", "Std. Error"]
)
)
pretty_table(reg_jk_table,
caption = "Jackknife uncertainty for the wt coefficient")| Quantity | Value |
|---|---|
| Slope estimate | -3.8778 |
| Jackknife bias | 0.0381 |
| Bias-corrected slope | -3.9160 |
| Jackknife SE | 0.7564 |
| Model-based SE | 0.6327 |
tibble(
Car = rownames(mtcars),
Influence = jk_reg$influence
) |>
mutate(Car = reorder(Car, Influence)) |>
ggplot(aes(Car, Influence, fill = Influence > 0)) +
geom_col() +
coord_flip() +
scale_fill_manual(values = c("#EF4444", "#3B82F6")) +
labs(
title = "Which cars influence the weight coefficient?",
x = NULL,
y = "Jackknife influence",
fill = "Positive influence"
)x_med <- c(1, 2, 3, 3, 3, 4, 5)
jk_med <- jackknife(x_med, median)
boot_med <- bootstrap_1d(x_med, median, B = 5000)
failure_table <- tibble(
Method = c("Delete-one jackknife", "Nonparametric bootstrap"),
Standard_Error = c(jk_med$se, boot_med$se)
)
pretty_table(failure_table,
caption = "Median example: the delete-one jackknife can fail")| Method | Standard_Error |
|---|---|
| Delete-one jackknife | 0.0000 |
| Nonparametric bootstrap | 0.5416 |
tibble(median = boot_med$replicates) |>
ggplot(aes(median)) +
geom_bar(fill = "#EC4899", color = "white") +
labs(
title = "Bootstrap distribution of the median",
subtitle = "Jackknife SE is zero, but bootstrap variation is positive",
x = "Bootstrap median",
y = "Frequency"
)Let \(Y\) be observed data and \(Z\) be missing or latent data. The complete-data likelihood is
\[ L_c(\theta;Y,Z) \]
and the observed-data likelihood is
\[ L(\theta;Y) = \sum_z L_c(\theta;Y,z) \]
for discrete \(Z\), or the corresponding integral for continuous \(Z\).
At iteration \(t\), the EM algorithm performs:
E-step \[ Q(\theta\mid\theta^{(t)}) = E_{\theta^{(t)}} \left[ \log L_c(\theta;Y,Z)\mid Y \right]. \]
M-step \[ \theta^{(t+1)} = \arg\max_\theta Q(\theta\mid\theta^{(t)}). \]
Under standard conditions,
\[ \ell(\theta^{(t+1)};Y) \ge \ell(\theta^{(t)};Y). \]
Suppose \(X_i\sim\mathrm{Bernoulli}(p)\), but some outcomes are missing. If the number of observed successes is \(s\), the number of observed failures is \(f\), and \(m\) values are missing, then:
\[ E(S_{\mathrm{mis}}\mid\text{observed},p^{(t)})=m p^{(t)}. \]
The M-step is
\[ p^{(t+1)} = \frac{s+mp^{(t)}}{s+f+m}. \]
em_bernoulli <- function(successes, failures, missing,
p0 = 0.5, tol = 1e-10,
max_iter = 1000) {
p <- p0
history <- tibble(iteration = 0, p = p)
for (iter in seq_len(max_iter)) {
expected_missing_successes <- missing * p
p_new <- (successes + expected_missing_successes) /
(successes + failures + missing)
history <- bind_rows(
history,
tibble(iteration = iter, p = p_new)
)
if (abs(p_new - p) < tol) break
p <- p_new
}
list(estimate = p_new, history = history,
iterations = iter)
}
bern_em <- em_bernoulli(
successes = 38,
failures = 42,
missing = 20,
p0 = 0.15
)
pretty_table(
tibble(
EM_Estimate = bern_em$estimate,
Iterations = bern_em$iterations,
Complete_case_Estimate = 38 / (38 + 42)
),
caption = "EM for Bernoulli data with missing outcomes"
)| EM_Estimate | Iterations | Complete_case_Estimate |
|---|---|---|
| 0.475 | 15 | 0.475 |
bern_em$history |>
ggplot(aes(iteration, p)) +
geom_line(color = "#7C3AED", linewidth = 1.2) +
geom_point(color = "#7C3AED", size = 2) +
labs(
title = "EM convergence for a missing Bernoulli problem",
x = "Iteration",
y = expression(p^{(t)})
)Assume
\[ f(x) = \pi\phi(x;\mu_1,\sigma_1^2) + (1-\pi)\phi(x;\mu_2,\sigma_2^2). \]
Introduce latent indicators
\[ Z_i= \begin{cases} 1,&X_i\text{ belongs to component 1},\\ 0,&X_i\text{ belongs to component 2}. \end{cases} \]
The E-step responsibilities are
\[ \tau_i^{(t)} = P(Z_i=1\mid X_i,\theta^{(t)}) = \frac{ \pi^{(t)} \phi(X_i;\mu_1^{(t)},\sigma_1^{2(t)}) }{ \pi^{(t)} \phi(X_i;\mu_1^{(t)},\sigma_1^{2(t)}) + (1-\pi^{(t)}) \phi(X_i;\mu_2^{(t)},\sigma_2^{2(t)}) }. \]
The M-step updates are
\[ \pi^{(t+1)} = \frac1n\sum_i\tau_i^{(t)}, \]
\[ \mu_1^{(t+1)} = \frac{\sum_i\tau_i^{(t)}X_i} {\sum_i\tau_i^{(t)}}, \qquad \mu_2^{(t+1)} = \frac{\sum_i(1-\tau_i^{(t)})X_i} {\sum_i(1-\tau_i^{(t)})}, \]
with analogous weighted variance updates.
em_gaussian2 <- function(x,
pi0 = 0.5,
mu0 = quantile(x, c(0.3, 0.7)),
sigma0 = rep(sd(x), 2),
tol = 1e-8,
max_iter = 1000) {
pi1 <- pi0
mu1 <- mu0[1]
mu2 <- mu0[2]
s1 <- sigma0[1]
s2 <- sigma0[2]
loglik_history <- numeric()
for (iter in seq_len(max_iter)) {
# E-step
d1 <- pi1 * dnorm(x, mu1, s1)
d2 <- (1 - pi1) * dnorm(x, mu2, s2)
tau <- d1 / pmax(d1 + d2, .Machine$double.eps)
# M-step
pi_new <- mean(tau)
mu1_new <- sum(tau * x) / sum(tau)
mu2_new <- sum((1 - tau) * x) / sum(1 - tau)
s1_new <- sqrt(sum(tau * (x - mu1_new)^2) / sum(tau))
s2_new <- sqrt(sum((1 - tau) * (x - mu2_new)^2) /
sum(1 - tau))
ll <- sum(log(
pi_new * dnorm(x, mu1_new, s1_new) +
(1 - pi_new) * dnorm(x, mu2_new, s2_new)
))
loglik_history <- c(loglik_history, ll)
change <- max(abs(c(
pi_new - pi1,
mu1_new - mu1,
mu2_new - mu2,
s1_new - s1,
s2_new - s2
)))
pi1 <- pi_new
mu1 <- mu1_new
mu2 <- mu2_new
s1 <- max(s1_new, 1e-6)
s2 <- max(s2_new, 1e-6)
if (change < tol) break
}
list(
pi = pi1,
mu = c(mu1, mu2),
sigma = c(s1, s2),
responsibility = tau,
loglik = loglik_history,
iterations = iter
)
}set.seed(302)
n_mix <- 500
z_true <- rbinom(n_mix, 1, 0.38)
x_mix <- ifelse(
z_true == 1,
rnorm(n_mix, -2.2, 0.8),
rnorm(n_mix, 2.3, 1.15)
)
mix_fit <- em_gaussian2(
x_mix,
pi0 = 0.5,
mu0 = c(-1, 1),
sigma0 = c(1.5, 1.5)
)
mix_table <- tibble(
Parameter = c("Mixing probability",
"Mean 1", "Mean 2",
"SD 1", "SD 2",
"Iterations"),
Estimate = c(mix_fit$pi, mix_fit$mu,
mix_fit$sigma, mix_fit$iterations)
)
pretty_table(mix_table,
caption = "EM estimates for a simulated Gaussian mixture")| Parameter | Estimate |
|---|---|
| Mixing probability | 0.3848 |
| Mean 1 | -2.2233 |
| Mean 2 | 2.2590 |
| SD 1 | 0.7384 |
| SD 2 | 1.1927 |
| Iterations | 26.0000 |
grid_x <- seq(min(x_mix) - 1, max(x_mix) + 1,
length.out = 600)
density_df <- tibble(
x = grid_x,
Component_1 =
mix_fit$pi *
dnorm(grid_x, mix_fit$mu[1], mix_fit$sigma[1]),
Component_2 =
(1 - mix_fit$pi) *
dnorm(grid_x, mix_fit$mu[2], mix_fit$sigma[2])
) |>
mutate(Mixture = Component_1 + Component_2)
ggplot(tibble(x = x_mix), aes(x)) +
geom_histogram(aes(y = after_stat(density)),
bins = 42, fill = "#CBD5E1",
color = "white") +
geom_line(data = density_df,
aes(x, Component_1, color = "Component 1"),
linewidth = 1.2) +
geom_line(data = density_df,
aes(x, Component_2, color = "Component 2"),
linewidth = 1.2) +
geom_line(data = density_df,
aes(x, Mixture, color = "Fitted mixture"),
linewidth = 1.5) +
scale_color_manual(values = c(
"Component 1" = "#2563EB",
"Component 2" = "#F97316",
"Fitted mixture" = "#111827"
)) +
labs(
title = "Gaussian-mixture fit obtained by EM",
x = "Observed value",
y = "Density",
color = NULL
)tibble(
Iteration = seq_along(mix_fit$loglik),
LogLikelihood = mix_fit$loglik
) |>
ggplot(aes(Iteration, LogLikelihood)) +
geom_line(color = "#059669", linewidth = 1.25) +
geom_point(color = "#059669", size = 1.7) +
labs(
title = "Observed-data log-likelihood across EM iterations",
subtitle = "The EM monotonicity property is visible",
y = "Log-likelihood"
)starts <- tibble(
start = 1:12,
pi0 = runif(12, 0.15, 0.85),
mu1 = runif(12, min(x_mix), median(x_mix)),
mu2 = runif(12, median(x_mix), max(x_mix)),
sd1 = runif(12, 0.5, 2.5),
sd2 = runif(12, 0.5, 2.5)
)
multi_fit <- pmap_dfr(starts, function(start, pi0, mu1, mu2, sd1, sd2) {
fit <- em_gaussian2(
x_mix,
pi0 = pi0,
mu0 = c(mu1, mu2),
sigma0 = c(sd1, sd2)
)
tibble(
start = start,
final_loglik = tail(fit$loglik, 1),
iterations = fit$iterations,
pi = fit$pi,
mu1 = fit$mu[1],
mu2 = fit$mu[2]
)
})
pretty_table(multi_fit,
caption = "EM results from multiple initial values")| start | final_loglik | iterations | pi | mu1 | mu2 |
|---|---|---|---|---|---|
| 1 | -1025.453 | 21 | 0.3848 | -2.2233 | 2.259 |
| 2 | -1025.453 | 44 | 0.3848 | -2.2233 | 2.259 |
| 3 | -1025.453 | 39 | 0.3848 | -2.2233 | 2.259 |
| 4 | -1025.453 | 45 | 0.3848 | -2.2233 | 2.259 |
| 5 | -1025.453 | 30 | 0.3848 | -2.2233 | 2.259 |
| 6 | -1025.453 | 28 | 0.3848 | -2.2233 | 2.259 |
| 7 | -1025.453 | 26 | 0.3848 | -2.2233 | 2.259 |
| 8 | -1025.453 | 33 | 0.3848 | -2.2233 | 2.259 |
| 9 | -1025.453 | 35 | 0.3848 | -2.2233 | 2.259 |
| 10 | -1025.453 | 46 | 0.3848 | -2.2233 | 2.259 |
| 11 | -1025.453 | 33 | 0.3848 | -2.2233 | 2.259 |
| 12 | -1025.453 | 41 | 0.3848 | -2.2233 | 2.259 |
multi_fit |>
ggplot(aes(factor(start), final_loglik,
fill = final_loglik)) +
geom_col() +
scale_fill_gradient(low = "#93C5FD", high = "#7C3AED") +
labs(
title = "Final log-likelihood from different EM starts",
x = "Starting-value experiment",
y = "Final log-likelihood",
fill = "Log-likelihood"
)faithful eruptionsThe faithful dataset contains eruption durations and
waiting times for the Old Faithful geyser. The eruption duration is
strongly bimodal.
data(faithful)
faithful_fit <- em_gaussian2(
faithful$eruptions,
pi0 = 0.5,
mu0 = c(2, 4.5),
sigma0 = c(0.5, 0.5)
)
faithful_table <- tibble(
Parameter = c("Mixing probability",
"Short-eruption mean",
"Long-eruption mean",
"Short-eruption SD",
"Long-eruption SD",
"Iterations"),
Estimate = c(
faithful_fit$pi,
faithful_fit$mu,
faithful_fit$sigma,
faithful_fit$iterations
)
)
pretty_table(faithful_table,
caption = "Two-component EM fit to Old Faithful eruptions")| Parameter | Estimate |
|---|---|
| Mixing probability | 0.3484 |
| Short-eruption mean | 2.0186 |
| Long-eruption mean | 4.2733 |
| Short-eruption SD | 0.2356 |
| Long-eruption SD | 0.4371 |
| Iterations | 31.0000 |
faith_grid <- seq(min(faithful$eruptions) - 0.2,
max(faithful$eruptions) + 0.2,
length.out = 500)
faith_density <- tibble(
x = faith_grid,
Short =
faithful_fit$pi *
dnorm(faith_grid, faithful_fit$mu[1],
faithful_fit$sigma[1]),
Long =
(1 - faithful_fit$pi) *
dnorm(faith_grid, faithful_fit$mu[2],
faithful_fit$sigma[2])
) |>
mutate(Mixture = Short + Long)
ggplot(faithful, aes(eruptions)) +
geom_histogram(aes(y = after_stat(density)),
bins = 30, fill = "#DDE7F0",
color = "white") +
geom_line(data = faith_density,
aes(x, Short, color = "Short component"),
linewidth = 1.2) +
geom_line(data = faith_density,
aes(x, Long, color = "Long component"),
linewidth = 1.2) +
geom_line(data = faith_density,
aes(x, Mixture, color = "Fitted mixture"),
linewidth = 1.5) +
scale_color_manual(values = c(
"Short component" = "#0EA5E9",
"Long component" = "#F97316",
"Fitted mixture" = "#111827"
)) +
labs(
title = "EM decomposition of Old Faithful eruption duration",
x = "Eruption duration (minutes)",
y = "Density",
color = NULL
)faithfulfaithful_classified <- faithful |>
mutate(
Probability_short = faithful_fit$responsibility,
Assigned_component =
if_else(Probability_short >= 0.5,
"Short eruption", "Long eruption")
)
ggplot(faithful_classified,
aes(eruptions, waiting,
color = Assigned_component,
size = abs(Probability_short - 0.5))) +
geom_point(alpha = 0.8) +
scale_color_manual(values = c(
"Short eruption" = "#0EA5E9",
"Long eruption" = "#F97316"
)) +
scale_size_continuous(
name = "Classification certainty",
range = c(1.5, 4.5)
) +
labs(
title = "EM-based soft classification of Old Faithful eruptions",
x = "Eruption duration",
y = "Waiting time",
color = "Assigned component"
)method_comparison <- tibble(
Method = c("Bootstrap", "Jackknife", "EM algorithm"),
Main_question = c(
"What is the sampling distribution or uncertainty of a statistic?",
"How do deletion, bias, and individual observations affect an estimator?",
"How can likelihood be maximized with missing or latent data?"
),
Repeated_operation = c(
"Resample observations with replacement",
"Delete observations or groups",
"Alternate conditional expectation and maximization"
),
Principal_output = c(
"SE, bias, CI, sampling distribution",
"SE, bias correction, pseudovalues, influence",
"MLEs, posterior responsibilities, completed sufficient statistics"
),
Main_warning = c(
"Resampling scheme must mimic data generation",
"May fail for nonsmooth or dependent data",
"May converge slowly or to a local maximum"
)
)
pretty_table(method_comparison,
caption = "Bootstrap, jackknife, and EM compared")| Method | Main_question | Repeated_operation | Principal_output | Main_warning |
|---|---|---|---|---|
| Bootstrap | What is the sampling distribution or uncertainty of a statistic? | Resample observations with replacement | SE, bias, CI, sampling distribution | Resampling scheme must mimic data generation |
| Jackknife | How do deletion, bias, and individual observations affect an estimator? | Delete observations or groups | SE, bias correction, pseudovalues, influence | May fail for nonsmooth or dependent data |
| EM algorithm | How can likelihood be maximized with missing or latent data? | Alternate conditional expectation and maximization | MLEs, posterior responsibilities, completed sufficient statistics | May converge slowly or to a local maximum |
A useful advanced workflow is:
Because mixture labels can switch, order the component means after every fit.
bootstrap_em <- function(x, B = 300, seed = 2026) {
set.seed(seed)
map_dfr(seq_len(B), function(b) {
xb <- sample(x, replace = TRUE)
fit <- try(
em_gaussian2(
xb,
pi0 = 0.5,
mu0 = quantile(xb, c(0.25, 0.75)),
sigma0 = rep(sd(xb), 2)
),
silent = TRUE
)
if (inherits(fit, "try-error")) {
return(tibble(
pi = NA_real_, mu_low = NA_real_,
mu_high = NA_real_,
sd_low = NA_real_, sd_high = NA_real_
))
}
ord <- order(fit$mu)
tibble(
pi = if (ord[1] == 1) fit$pi else 1 - fit$pi,
mu_low = fit$mu[ord[1]],
mu_high = fit$mu[ord[2]],
sd_low = fit$sigma[ord[1]],
sd_high = fit$sigma[ord[2]]
)
}) |>
drop_na()
}
set.seed(401)
faithful_em_boot <- bootstrap_em(
faithful$eruptions,
B = 400
)
em_uncertainty <- faithful_em_boot |>
pivot_longer(everything(),
names_to = "Parameter",
values_to = "Estimate") |>
group_by(Parameter) |>
summarise(
Mean = mean(Estimate),
SE = sd(Estimate),
Lower = quantile(Estimate, 0.025),
Upper = quantile(Estimate, 0.975),
.groups = "drop"
)
pretty_table(em_uncertainty,
caption = "Bootstrap uncertainty for EM mixture parameters")| Parameter | Mean | SE | Lower | Upper |
|---|---|---|---|---|
| mu_high | 4.2742 | 0.0376 | 4.2017 | 4.3506 |
| mu_low | 2.0229 | 0.0312 | 1.9665 | 2.0889 |
| pi | 0.3516 | 0.0271 | 0.2966 | 0.4008 |
| sd_high | 0.4305 | 0.0333 | 0.3735 | 0.4964 |
| sd_low | 0.2384 | 0.0309 | 0.1907 | 0.3111 |
faithful_em_boot |>
select(mu_low, mu_high) |>
pivot_longer(everything(),
names_to = "Component",
values_to = "Mean") |>
mutate(Component = recode(
Component,
mu_low = "Short-eruption mean",
mu_high = "Long-eruption mean"
)) |>
ggplot(aes(Mean, fill = Component)) +
geom_density(alpha = 0.58) +
facet_wrap(~ Component, scales = "free") +
scale_fill_manual(values = c(
"Short-eruption mean" = "#0EA5E9",
"Long-eruption mean" = "#F97316"
)) +
labs(
title = "Bootstrap uncertainty after EM fitting",
subtitle = "Old Faithful mixture component means",
x = "Bootstrap EM estimate",
y = "Density"
) +
guides(fill = "none")Generate 50 Gamma observations and estimate
\[ CV=\frac{S}{\bar X}. \]
Obtain the bootstrap bias, standard error, percentile interval, and basic interval.
Create a \(2\times2\) table from simulated binary exposure and outcome data. Bootstrap subjects, not table cells, and estimate the uncertainty of the odds ratio.
Use at least three sample sizes and compare both standard errors. Explain why the methods disagree in small samples.
For airquality, estimate the correlation between
Ozone and Temp after removing missing values.
Use jackknife influence values to identify the five most influential
days.
Simulate a two-normal mixture whose means are close together. Fit it from at least ten initial values. Discuss identifiability, slow convergence, and instability.
Fit the Old Faithful mixture model, bootstrap it, and report standard errors and percentile intervals for both component means.
set.seed(502)
n <- 400
exposure <- rbinom(n, 1, 0.45)
prob <- plogis(-1 + 1.1 * exposure)
outcome <- rbinom(n, 1, prob)
d <- tibble(exposure, outcome)
odds_ratio <- function(data) {
tab <- table(data$exposure, data$outcome)
(tab[2, 2] * tab[1, 1]) /
(tab[2, 1] * tab[1, 2])
}
set.seed(503)
or_star <- replicate(5000, {
idx <- sample(seq_len(nrow(d)), replace = TRUE)
odds_ratio(d[idx, ])
})
quantile(or_star, c(0.025, 0.5, 0.975))
sd(or_star)compare_median <- function(n, reps = 250) {
map_dfr(seq_len(reps), function(i) {
x <- rexp(n)
jk <- jackknife(x, median)
bs <- bootstrap_1d(x, median, B = 1000,
seed = sample.int(1e7, 1))
tibble(n = n, jackknife_se = jk$se,
bootstrap_se = bs$se)
})
}
result <- bind_rows(
compare_median(15),
compare_median(30),
compare_median(80)
)Use the bootstrap_em() function from Section 23 and
increase \(B\) to at least 1,000 for a
final report.
## R version 4.6.0 (2026-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 10 x64 (build 19045)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_India.utf8 LC_CTYPE=English_India.utf8
## [3] LC_MONETARY=English_India.utf8 LC_NUMERIC=C
## [5] LC_TIME=English_India.utf8
##
## time zone: Asia/Calcutta
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] boot_1.3-32 scales_1.4.0 knitr_1.51 tibble_3.3.1 purrr_1.2.2
## [6] tidyr_1.3.2 dplyr_1.2.1 ggplot2_4.0.3
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.7.3 cli_3.6.6 rlang_1.2.0 xfun_0.60
## [5] generics_0.1.4 S7_0.2.2 jsonlite_2.0.0 labeling_0.4.3
## [9] glue_1.8.1 htmltools_0.5.9 sass_0.4.10 rmarkdown_2.31
## [13] grid_4.6.0 evaluate_1.0.5 jquerylib_0.1.4 fastmap_1.2.0
## [17] yaml_2.3.12 lifecycle_1.0.5 compiler_4.6.0 RColorBrewer_1.1-3
## [21] pkgconfig_2.0.3 farver_2.1.2 digest_0.6.39 R6_2.6.1
## [25] tidyselect_1.2.1 pillar_1.11.1 magrittr_2.0.5 bslib_0.12.0
## [29] withr_3.0.2 tools_4.6.0 gtable_0.3.6 cachem_1.1.0
Bootstrap approximates repeated sampling. Jackknife approximates sensitivity to deletion. EM transforms latent-data likelihood maximization into a sequence of simpler conditional calculations. The methods solve different problems, but they can be combined—for example, bootstrap can quantify uncertainty after an EM fit.