This note is a standalone RPubs lecture note on Bayesian statistical inference for students and researchers in the basic sciences. The emphasis is not on black-box Bayesian software. The aim is to understand the probability environment, prior, likelihood, posterior, posterior prediction, uncertainty quantification, simulation verification, and model checking.
The scientific flavor is deliberately astronomical:
sunspot.year time
series;MASS::galaxies
dataset;Core message. Bayesian inference is probabilistic inference: unknown scientific quantities are represented through a probability distribution, updated by data, and then used for estimation, prediction, and decision-making under uncertainty.
A probability model begins with a probability space
\[ (\Omega,\mathcal F,P), \]
where:
A random variable is a measurable function
\[ Y:\Omega\to \mathcal Y. \]
In statistics, we observe a realized value \(y\) of \(Y\), and then infer something about an unknown parameter \(\theta\) or latent scientific quantity.
A statistical model is a family of probability laws
\[ \mathcal P = \{P_\theta:\theta\in\Theta\}, \]
where \(\Theta\) is the parameter space. In an astronomical example, \(\theta\) could be a solar-activity rate, a galaxy-cluster velocity center, an expansion slope, or a latent state in a filtering problem.
In a frequentist statistical environment, \(\theta\) is fixed but unknown. Probability statements are made about repeated samples \(Y\sim P_\theta\).
In a Bayesian statistical environment, \(\theta\) is also uncertain and is assigned a prior probability measure \(\Pi\) on \((\Theta,\mathcal T)\):
\[ \theta\sim \Pi. \]
Then data are generated conditionally:
\[ Y\mid \theta \sim P_\theta. \]
After observing \(Y=y\), we update from prior to posterior.
Suppose \(Y\) has density or probability mass function \(p_\theta(y)\). The likelihood is
\[ L(\theta;y)=p_\theta(y), \]
viewed as a function of \(\theta\) after observing \(y\). The likelihood is not a probability distribution over \(\theta\) by itself.
If the prior has density \(\pi(\theta)\), Bayes’ formula gives
\[ \pi(\theta\mid y) = \frac{p_\theta(y)\pi(\theta)}{\int_\Theta p_u(y)\pi(u)\,du}. \]
The denominator
\[ m(y)=\int_\Theta p_\theta(y)\pi(\theta)\,d\theta \]
is called the marginal likelihood or evidence.
For a measurable set \(B\subseteq \Theta\), the posterior probability is
\[ \Pi(B\mid Y=y) = \frac{\int_B p_\theta(y)\,\Pi(d\theta)} {\int_\Theta p_\theta(y)\,\Pi(d\theta)}. \]
Interpretation. A posterior statement such as \(P(\theta>0\mid y)=0.98\) means: under the assumed model and prior, after observing the data, 98% of the posterior probability mass lies on positive values of \(\theta\).
After obtaining \(\pi(\theta\mid y)\), common Bayesian summaries are:
| Quantity | Definition | Meaning |
|---|---|---|
| Posterior mean | \(E(\theta\mid y)\) | squared-error optimal estimate |
| Posterior median | median of \(\pi(\theta\mid y)\) | robust central estimate |
| MAP | \(\arg\max_\theta \pi(\theta\mid y)\) | posterior mode |
| Credible interval | interval \(C(y)\) with \(\Pi(C(y)\mid y)=0.95\) | uncertainty interval for \(\theta\) |
| Posterior tail probability | \(P(\theta>c\mid y)\) | probability of scientific statement |
| Posterior predictive | \(p(\tilde y\mid y)\) | uncertainty for future or replicated data |
The posterior predictive distribution is
\[ p(\tilde y\mid y)=\int p(\tilde y\mid \theta)\pi(\theta\mid y)\,d\theta. \]
This is central in scientific work because science often needs prediction with uncertainty, not only parameter estimation.
Suppose \(Y\mid p\sim\text{Binomial}(n,p)\), where \(p\) is an unknown probability. Use the prior
\[ p\sim\text{Beta}(a,b). \]
If \(Y=y\), the posterior is
\[ p\mid y\sim \text{Beta}(a+y,b+n-y). \]
This is a conjugate model: prior and posterior belong to the same family.
# True data-generating probability
p_true <- 0.63
n <- 40
y <- rbinom(1, size = n, prob = p_true)
# Prior Beta(a,b)
a <- 2
b <- 2
# Posterior Beta(a+y, b+n-y)
a_post <- a + y
b_post <- b + n - y
p_grid <- seq(0.001, 0.999, length.out = 1000)
prior <- dbeta(p_grid, a, b)
lik <- dbinom(y, size = n, prob = p_grid)
lik_scaled <- lik / max(lik) * max(prior)
post <- dbeta(p_grid, a_post, b_post)
df <- data.frame(
p = rep(p_grid, 3),
density = c(prior, lik_scaled, post),
curve = rep(c("Prior Beta(2,2)", "Scaled likelihood", "Posterior"), each = length(p_grid))
)
ggplot(df, aes(p, density, color = curve)) +
geom_line(linewidth = 1.2) +
geom_vline(xintercept = p_true, linetype = "dashed", color = "black") +
labs(
title = "Bayesian updating in a Binomial experiment",
subtitle = paste0("Observed y = ", y, " successes out of n = ", n, "; dashed line is true p = ", p_true),
x = "p", y = "density / scaled likelihood", color = ""
)Posterior numerical summary:
beta_summary <- data.frame(
quantity = c("observed successes", "sample proportion", "posterior mean", "posterior median", "95% credible lower", "95% credible upper", "P(p > 0.5 | data)"),
value = c(
y,
y/n,
a_post/(a_post+b_post),
qbeta(0.5, a_post, b_post),
qbeta(0.025, a_post, b_post),
qbeta(0.975, a_post, b_post),
1 - pbeta(0.5, a_post, b_post)
)
)
knitr::kable(beta_summary, digits = 4)| quantity | value |
|---|---|
| observed successes | 28.0000 |
| sample proportion | 0.7000 |
| posterior mean | 0.6818 |
| posterior median | 0.6846 |
| 95% credible lower | 0.5387 |
| 95% credible upper | 0.8092 |
| P(p > 0.5 | data) | 0.9931 |
A Bayesian credible interval is not defined by repeated-sampling coverage, but repeated simulation is still useful for understanding behavior. Here we simulate many datasets from the same true \(p\), compute the posterior 95% interval, and check how often it contains the true value.
set.seed(12)
B <- 1000
n <- 40
p_true <- 0.63
cover <- numeric(B)
post_mean <- numeric(B)
for (r in seq_len(B)) {
yr <- rbinom(1, n, p_true)
lo <- qbeta(0.025, a + yr, b + n - yr)
hi <- qbeta(0.975, a + yr, b + n - yr)
cover[r] <- (lo <= p_true && p_true <= hi)
post_mean[r] <- (a + yr) / (a + b + n)
}
res <- data.frame(
simulation_quantity = c("empirical containment of true p", "mean posterior mean", "SD of posterior means"),
value = c(mean(cover), mean(post_mean), sd(post_mean))
)
knitr::kable(res, digits = 4)| simulation_quantity | value |
|---|---|
| empirical containment of true p | 0.9570 |
| mean posterior mean | 0.6196 |
| SD of posterior means | 0.0675 |
hist_df <- data.frame(post_mean = post_mean)
ggplot(hist_df, aes(post_mean)) +
geom_histogram(bins = 35, fill = "steelblue", color = "white") +
geom_vline(xintercept = p_true, color = "red", linewidth = 1.1) +
labs(
title = "Repeated simulation: posterior mean across experiments",
subtitle = "Red line is the true p. The posterior mean varies because the data vary.",
x = "posterior mean", y = "count"
)Suppose
\[ Y_i\mid \mu \sim N(\mu,\sigma^2),\qquad i=1,\ldots,n, \]
where \(\sigma\) is known. Use prior
\[ \mu\sim N(\mu_0,\tau_0^2). \]
Then
\[ \mu\mid y\sim N(\mu_n,\tau_n^2), \]
where
\[ \tau_n^2=\left(\frac{1}{\tau_0^2}+\frac{n}{\sigma^2}\right)^{-1}, \qquad \mu_n=\tau_n^2\left(\frac{\mu_0}{\tau_0^2}+\frac{n\bar y}{\sigma^2}\right). \]
This formula shows shrinkage: the posterior mean is a precision-weighted compromise between prior mean and sample mean.
set.seed(7)
mu_true <- 2.4
sigma <- 1.2
n <- 25
y <- rnorm(n, mu_true, sigma)
mu0 <- 0
tau0 <- 3
tau_n2 <- 1 / (1/tau0^2 + n/sigma^2)
mu_n <- tau_n2 * (mu0/tau0^2 + n*mean(y)/sigma^2)
mu_grid <- seq(-1.5, 4.5, length.out = 1000)
prior <- dnorm(mu_grid, mu0, tau0)
post <- dnorm(mu_grid, mu_n, sqrt(tau_n2))
lik_scaled <- dnorm(mu_grid, mean(y), sigma/sqrt(n))
lik_scaled <- lik_scaled / max(lik_scaled) * max(post)
ndf <- data.frame(
mu = rep(mu_grid, 3),
density = c(prior, lik_scaled, post),
curve = rep(c("Prior", "Sampling information (scaled)", "Posterior"), each = length(mu_grid))
)
ggplot(ndf, aes(mu, density, color = curve)) +
geom_line(linewidth = 1.2) +
geom_vline(xintercept = mu_true, linetype = "dashed", color = "black") +
labs(
title = "Normal mean inference: prior + likelihood = posterior",
subtitle = "The posterior shrinks the sample mean toward the prior mean according to precision.",
x = expression(mu), y = "density", color = ""
)summary_normal <- data.frame(
quantity = c("sample mean", "prior mean", "posterior mean", "posterior SD", "95% lower", "95% upper"),
value = c(mean(y), mu0, mu_n, sqrt(tau_n2), qnorm(0.025, mu_n, sqrt(tau_n2)), qnorm(0.975, mu_n, sqrt(tau_n2)))
)
knitr::kable(summary_normal, digits = 4)| quantity | value |
|---|---|
| sample mean | 2.9568 |
| prior mean | 0.0000 |
| posterior mean | 2.9380 |
| posterior SD | 0.2392 |
| 95% lower | 2.4691 |
| 95% upper | 3.4069 |
A very simple astronomical model says that recession velocity \(v_i\) is approximately proportional to distance \(d_i\):
\[ v_i = H d_i + \epsilon_i, \qquad \epsilon_i\sim N(0,\sigma^2). \]
Here \(H\) is a slope parameter. In cosmology this resembles Hubble’s law, but here it is only a toy demonstration.
Let
\[ H\sim N(H_0,s_0^2). \]
With known \(\sigma\), the posterior is again normal:
\[ H\mid v,d \sim N(m_n,s_n^2), \]
where
\[ s_n^2=\left(\frac{1}{s_0^2}+\frac{\sum_i d_i^2}{\sigma^2}\right)^{-1}, \qquad m_n=s_n^2\left(\frac{H_0}{s_0^2}+\frac{\sum_i d_i v_i}{\sigma^2}\right). \]
set.seed(2026)
n <- 45
H_true <- 70
sigma_v <- 250
D <- sort(runif(n, 5, 180))
V <- H_true * D + rnorm(n, 0, sigma_v)
H0 <- 65
s0 <- 20
sn2 <- 1 / (1/s0^2 + sum(D^2)/sigma_v^2)
mn <- sn2 * (H0/s0^2 + sum(D*V)/sigma_v^2)
ols_slope <- sum(D*V) / sum(D^2)
H_grid <- seq(45, 95, length.out = 1000)
prior_H <- dnorm(H_grid, H0, s0)
post_H <- dnorm(H_grid, mn, sqrt(sn2))
dh <- data.frame(distance = D, velocity = V)
p1 <- ggplot(dh, aes(distance, velocity)) +
geom_point(size = 2.3, alpha = 0.8) +
geom_abline(intercept = 0, slope = H_true, color = "black", linetype = "dashed", linewidth = 1) +
geom_abline(intercept = 0, slope = ols_slope, color = "orange", linewidth = 1) +
geom_abline(intercept = 0, slope = mn, color = "blue", linewidth = 1) +
labs(
title = "Toy astronomical velocity--distance relation",
subtitle = "Black dashed: true slope; orange: least squares; blue: Bayesian posterior mean.",
x = "distance (toy units)", y = "recession velocity (toy units)"
)
print(p1)p2 <- ggplot(data.frame(H = rep(H_grid, 2), density = c(prior_H, post_H), curve = rep(c("prior", "posterior"), each = length(H_grid))),
aes(H, density, color = curve)) +
geom_line(linewidth = 1.2) +
geom_vline(xintercept = H_true, linetype = "dashed") +
labs(title = "Posterior uncertainty for the expansion-like slope", x = "H", y = "density", color = "")
print(p2)h_summary <- data.frame(
quantity = c("true H", "least-squares slope", "posterior mean", "posterior SD", "95% lower", "95% upper"),
value = c(H_true, ols_slope, mn, sqrt(sn2), qnorm(0.025, mn, sqrt(sn2)), qnorm(0.975, mn, sqrt(sn2)))
)
knitr::kable(h_summary, digits = 3)| quantity | value |
|---|---|
| true H | 70.000 |
| least-squares slope | 69.789 |
| posterior mean | 69.787 |
| posterior SD | 0.437 |
| 95% lower | 68.931 |
| 95% upper | 70.643 |
Scientific measurements can be contaminated by peculiar velocities, calibration problems, or wrong identification. If the likelihood assumes perfect Gaussian noise, an outlier can strongly affect the posterior.
We compare two likelihoods for the same slope \(H\):
\[ v_i\mid H \sim N(Hd_i,\sigma^2), \]
and a heavier-tailed Student likelihood
\[ \frac{v_i-Hd_i}{\sigma}\sim t_\nu. \]
The Student likelihood is less surprised by extreme residuals and therefore more robust.
set.seed(2027)
V_bad <- V
V_bad[which.max(D)] <- V_bad[which.max(D)] + 2600
H_grid <- seq(40, 105, length.out = 1200)
log_prior <- dnorm(H_grid, H0, s0, log = TRUE)
loglik_normal <- sapply(H_grid, function(H) sum(dnorm(V_bad, mean = H * D, sd = sigma_v, log = TRUE)))
loglik_t <- sapply(H_grid, function(H) sum(dt((V_bad - H * D)/sigma_v, df = 3, log = TRUE) - log(sigma_v)))
normalize_logpost <- function(logp) {
w <- exp(logp - max(logp))
w / sum(w)
}
post_norm_w <- normalize_logpost(log_prior + loglik_normal)
post_t_w <- normalize_logpost(log_prior + loglik_t)
grid_mean <- function(x, w) sum(x * w)
grid_quantile <- function(x, w, probs) {
cw <- cumsum(w)
sapply(probs, function(p) x[which(cw >= p)[1]])
}
mn_norm <- grid_mean(H_grid, post_norm_w)
mn_t <- grid_mean(H_grid, post_t_w)
ci_norm <- grid_quantile(H_grid, post_norm_w, c(0.025, 0.975))
ci_t <- grid_quantile(H_grid, post_t_w, c(0.025, 0.975))
p_bad <- ggplot(data.frame(distance = D, velocity = V_bad), aes(distance, velocity)) +
geom_point(size = 2.4, alpha = 0.8) +
geom_abline(intercept = 0, slope = H_true, linetype = "dashed", color = "black", linewidth = 1) +
geom_abline(intercept = 0, slope = mn_norm, color = "red", linewidth = 1) +
geom_abline(intercept = 0, slope = mn_t, color = "blue", linewidth = 1) +
labs(
title = "Outlier-contaminated velocity--distance data",
subtitle = "Red: Gaussian posterior mean; blue: robust Student-t posterior mean; dashed: true slope.",
x = "distance", y = "velocity"
)
print(p_bad)post_df <- data.frame(
H = rep(H_grid, 2),
posterior_mass = c(post_norm_w, post_t_w),
model = rep(c("Gaussian likelihood", "Student-t likelihood"), each = length(H_grid))
)
ggplot(post_df, aes(H, posterior_mass, color = model)) +
geom_line(linewidth = 1.2) +
geom_vline(xintercept = H_true, linetype = "dashed") +
labs(
title = "Posterior under model misspecification",
subtitle = "Heavy-tailed likelihood protects inference from one extreme observation.",
x = "H", y = "posterior grid mass", color = ""
)robust_table <- data.frame(
model = c("Gaussian likelihood", "Student-t likelihood"),
posterior_mean = c(mn_norm, mn_t),
lower_95 = c(ci_norm[1], ci_t[1]),
upper_95 = c(ci_norm[2], ci_t[2])
)
knitr::kable(robust_table, digits = 3)| model | posterior_mean | lower_95 | upper_95 |
|---|---|---|---|
| Gaussian likelihood | 71.091 | 70.250 | 71.931 |
| Student-t likelihood | 69.997 | 68.949 | 71.009 |
Lesson. Bayesian inference is not automatically safe. It is conditional on the likelihood and prior. A wrong likelihood can produce confidently wrong conclusions. Robust Bayes changes the probability model so that unusual observations do not dominate the inference.
The built-in R dataset sunspot.year contains yearly
sunspot numbers from 1700 to 1988 and has 289 observations. Sunspots are
a classical observable of solar activity.
sun <- data.frame(
year = as.numeric(time(sunspot.year)),
count = as.numeric(sunspot.year)
)
knitr::kable(head(sun, 8), caption = "First few observations of yearly sunspot numbers")| year | count |
|---|---|
| 1700 | 5 |
| 1701 | 11 |
| 1702 | 16 |
| 1703 | 23 |
| 1704 | 36 |
| 1705 | 58 |
| 1706 | 29 |
| 1707 | 20 |
ggplot(sun, aes(year, count)) +
geom_line(color = "darkorange", linewidth = 0.8) +
labs(
title = "Yearly sunspot numbers, 1700--1988",
subtitle = "Solar activity is cyclic and nonstationary; a simple iid model is only a first approximation.",
x = "year", y = "sunspot number"
)For a count \(Y\), a basic model is
\[ Y_i\mid \lambda \sim \text{Poisson}(\lambda), \qquad \lambda\sim\text{Gamma}(a,b), \]
where the Gamma distribution is parameterized by shape \(a\) and rate \(b\). If \(y_1,\ldots,y_n\) are observed, then
\[ \lambda\mid y\sim \text{Gamma}\left(a+\sum_i y_i,\, b+n\right). \]
We compare average activity before and after 1850. This is deliberately simple and should not be mistaken for a serious solar-cycle model.
early <- subset(sun, year < 1850)$count
late <- subset(sun, year >= 1850)$count
a0 <- 1
b0 <- 0.05
post_early <- c(shape = a0 + sum(early), rate = b0 + length(early))
post_late <- c(shape = a0 + sum(late), rate = b0 + length(late))
S <- 20000
lambda_early <- rgamma(S, shape = post_early["shape"], rate = post_early["rate"])
lambda_late <- rgamma(S, shape = post_late["shape"], rate = post_late["rate"])
sun_table <- data.frame(
period = c("1700--1849", "1850--1988"),
n_years = c(length(early), length(late)),
sample_mean = c(mean(early), mean(late)),
posterior_mean_rate = c(mean(lambda_early), mean(lambda_late)),
lower_95 = c(quantile(lambda_early, 0.025), quantile(lambda_late, 0.025)),
upper_95 = c(quantile(lambda_early, 0.975), quantile(lambda_late, 0.975))
)
knitr::kable(sun_table, digits = 3)| period | n_years | sample_mean | posterior_mean_rate | lower_95 | upper_95 |
|---|---|---|---|---|---|
| 1700–1849 | 150 | 43.795 | 43.781 | 42.720 | 44.864 |
| 1850–1988 | 139 | 53.814 | 53.799 | 52.574 | 55.031 |
p_late_gt_early <- mean(lambda_late > lambda_early)
cat("Posterior probability that late-period mean sunspot rate exceeds early-period rate:", round(p_late_gt_early, 4), "\n")## Posterior probability that late-period mean sunspot rate exceeds early-period rate: 1
rate_df <- data.frame(
lambda = c(lambda_early, lambda_late),
period = rep(c("1700--1849", "1850--1988"), each = S)
)
ggplot(rate_df, aes(lambda, fill = period, color = period)) +
geom_density(alpha = 0.25, linewidth = 1.1) +
labs(
title = "Posterior distributions for mean yearly sunspot rate",
subtitle = "Gamma--Poisson model: useful as a first uncertainty calculation, but too simple for solar cycles.",
x = expression(lambda), y = "posterior density", fill = "", color = ""
)If \(Y_i\sim \text{Poisson}(\lambda)\), then \(\operatorname{Var}(Y_i)\approx E(Y_i)\). Sunspot data have cyclicity, dependence, and overdispersion. A posterior predictive check makes this visible.
y_all <- sun$count
n_all <- length(y_all)
post_all <- c(shape = a0 + sum(y_all), rate = b0 + n_all)
lambda_all <- rgamma(2000, shape = post_all["shape"], rate = post_all["rate"])
var_to_mean <- function(x) var(x) / mean(x)
obs_vtm <- var_to_mean(y_all)
ppc_vtm <- sapply(lambda_all, function(lam) var_to_mean(rpois(n_all, lam)))
ppc_df <- data.frame(vtm = ppc_vtm)
ggplot(ppc_df, aes(vtm)) +
geom_histogram(bins = 40, fill = "skyblue", color = "white") +
geom_vline(xintercept = obs_vtm, color = "red", linewidth = 1.2) +
labs(
title = "Posterior predictive check for Poisson sunspot model",
subtitle = "Red line is observed variance/mean ratio. The simple iid Poisson model misses solar-cycle variability.",
x = "variance / mean", y = "posterior predictive frequency"
)ppc_sun_table <- data.frame(
statistic = c("observed variance/mean", "posterior predictive mean", "posterior predictive 95% lower", "posterior predictive 95% upper", "P(T_rep >= T_obs)"),
value = c(obs_vtm, mean(ppc_vtm), quantile(ppc_vtm, 0.025), quantile(ppc_vtm, 0.975), mean(ppc_vtm >= obs_vtm))
)
knitr::kable(ppc_sun_table, digits = 4)| statistic | value |
|---|---|
| observed variance/mean | 32.0529 |
| posterior predictive mean | 1.0000 |
| posterior predictive 95% lower | 0.8482 |
| posterior predictive 95% upper | 1.1712 |
| P(T_rep >= T_obs) | 0.0000 |
Scientific lesson. A Bayesian posterior can be mathematically correct for the model and still scientifically inadequate if the model ignores time dependence. This is why posterior predictive checking is an essential part of Bayesian workflow.
The MASS::galaxies dataset contains velocities in km/sec
for 82 galaxies from six well-separated conic sections of a survey of
the Corona Borealis region. The documentation notes that multimodality
in such surveys is evidence for voids and superclusters in the far
universe.
gal <- as.numeric(MASS::galaxies) / 1000 # thousand km/sec
gal_df <- data.frame(velocity = gal)
summary_gal <- data.frame(
n = length(gal),
mean = mean(gal),
sd = sd(gal),
median = median(gal),
min = min(gal),
max = max(gal)
)
knitr::kable(summary_gal, digits = 3, caption = "Galaxy velocities in 1000 km/sec")| n | mean | sd | median | min | max |
|---|---|---|---|---|---|
| 82 | 20.828 | 4.564 | 20.834 | 9.172 | 34.279 |
ggplot(gal_df, aes(velocity)) +
geom_histogram(aes(y = after_stat(density)), bins = 18, fill = "gray85", color = "white") +
geom_density(color = "darkblue", linewidth = 1.2) +
geom_rug(alpha = 0.6) +
labs(
title = "Galaxy velocities: visual evidence of non-Gaussian structure",
subtitle = "A single normal distribution is likely too simple for clustered cosmic structure.",
x = "velocity (1000 km/sec)", y = "density"
)As a first model, suppose
\[ Y_i\mid \mu,\sigma^2\sim N(\mu,\sigma^2). \]
Use the conjugate Normal–Inverse-Gamma prior:
\[ \mu\mid \sigma^2\sim N(\mu_0,\sigma^2/\kappa_0), \qquad \sigma^2\sim \text{Inv-Gamma}(\alpha_0,\beta_0). \]
nig_posterior <- function(y, mu0 = 20, kappa0 = 0.01, alpha0 = 2, beta0 = 20) {
n <- length(y)
ybar <- mean(y)
ss <- sum((y - ybar)^2)
kappa_n <- kappa0 + n
mu_n <- (kappa0 * mu0 + n * ybar) / kappa_n
alpha_n <- alpha0 + n / 2
beta_n <- beta0 + 0.5 * ss + (kappa0 * n * (ybar - mu0)^2) / (2 * kappa_n)
list(mu_n = mu_n, kappa_n = kappa_n, alpha_n = alpha_n, beta_n = beta_n)
}
sample_nig <- function(post, S = 5000) {
sigma2 <- 1 / rgamma(S, shape = post$alpha_n, rate = post$beta_n)
mu <- rnorm(S, mean = post$mu_n, sd = sqrt(sigma2 / post$kappa_n))
data.frame(mu = mu, sigma = sqrt(sigma2))
}
max_gap <- function(x) max(diff(sort(x)))post_gal <- nig_posterior(gal)
draw_gal <- sample_nig(post_gal, S = 8000)
post_sum_gal <- data.frame(
parameter = c("mu", "sigma"),
posterior_mean = c(mean(draw_gal$mu), mean(draw_gal$sigma)),
lower_95 = c(quantile(draw_gal$mu, 0.025), quantile(draw_gal$sigma, 0.025)),
upper_95 = c(quantile(draw_gal$mu, 0.975), quantile(draw_gal$sigma, 0.975))
)
knitr::kable(post_sum_gal, digits = 3)| parameter | posterior_mean | lower_95 | upper_95 |
|---|---|---|---|
| mu | 20.819 | 19.818 | 21.781 |
| sigma | 4.520 | 3.893 | 5.278 |
mu_df <- data.frame(mu = draw_gal$mu)
ggplot(mu_df, aes(mu)) +
geom_density(fill = "lightgreen", alpha = 0.5, color = "darkgreen", linewidth = 1.1) +
labs(
title = "Posterior distribution for the mean galaxy velocity under a single-normal model",
x = expression(mu~"(1000 km/sec)"), y = "posterior density"
)A single normal model can estimate a mean and variance, but it may not reproduce multimodality. We use the largest gap between sorted velocities as a simple discrepancy statistic:
\[ T(y)=\max_i \{y_{(i+1)}-y_{(i)}\}. \]
Large gaps suggest separated clusters.
set.seed(19)
Spp <- 2000
idx <- sample(seq_len(nrow(draw_gal)), Spp)
pp_gaps <- numeric(Spp)
for (s in seq_len(Spp)) {
yy <- rnorm(length(gal), mean = draw_gal$mu[idx[s]], sd = draw_gal$sigma[idx[s]])
pp_gaps[s] <- max_gap(yy)
}
obs_gap <- max_gap(gal)
pp_gap_df <- data.frame(max_gap = pp_gaps)
ggplot(pp_gap_df, aes(max_gap)) +
geom_histogram(bins = 40, fill = "lavender", color = "white") +
geom_vline(xintercept = obs_gap, color = "red", linewidth = 1.2) +
labs(
title = "Posterior predictive check for a single-normal galaxy model",
subtitle = "Red line: observed largest velocity gap. Tail behavior suggests model inadequacy if red is extreme.",
x = "largest gap between sorted velocities", y = "posterior predictive frequency"
)gap_table <- data.frame(
statistic = c("observed largest gap", "posterior predictive mean gap", "posterior predictive 95% lower", "posterior predictive 95% upper", "Bayesian p-value P(T_rep >= T_obs)"),
value = c(obs_gap, mean(pp_gaps), quantile(pp_gaps, 0.025), quantile(pp_gaps, 0.975), mean(pp_gaps >= obs_gap))
)
knitr::kable(gap_table, digits = 4)| statistic | value |
|---|---|
| observed largest gap | 5.6780 |
| posterior predictive mean gap | 2.8984 |
| posterior predictive 95% lower | 1.1520 |
| posterior predictive 95% upper | 6.5742 |
| Bayesian p-value P(T_rep >= T_obs) | 0.0485 |
A full Bayesian mixture model is beyond this introductory note. But we can show a useful hybrid idea:
This is not a replacement for a full Bayesian mixture, but it is pedagogically useful.
set.seed(25)
km <- kmeans(gal, centers = 3, nstart = 50)
cl_means <- tapply(gal, km$cluster, mean)
ord <- order(cl_means)
cluster_ordered <- match(km$cluster, ord)
gal_cl <- data.frame(
velocity = gal,
cluster = factor(cluster_ordered, labels = c("low-velocity group", "middle group", "high-velocity group"))
)
ggplot(gal_cl, aes(velocity, fill = cluster)) +
geom_histogram(position = "identity", alpha = 0.55, bins = 18, color = "white") +
geom_rug(aes(color = cluster), alpha = 0.7) +
labs(
title = "Exploratory clustering of galaxy velocities",
subtitle = "ML-like clustering proposes groups; Bayesian inference can quantify group centers and uncertainty.",
x = "velocity (1000 km/sec)", y = "count", fill = "", color = ""
)cluster_levels <- levels(gal_cl$cluster)
cluster_results <- do.call(rbind, lapply(cluster_levels, function(cl) {
ycl <- gal_cl$velocity[gal_cl$cluster == cl]
pst <- nig_posterior(ycl, mu0 = mean(gal), kappa0 = 0.01, alpha0 = 2, beta0 = 20)
dr <- sample_nig(pst, S = 6000)
data.frame(
cluster = cl,
n = length(ycl),
sample_mean = mean(ycl),
posterior_mean_center = mean(dr$mu),
lower_95_center = quantile(dr$mu, 0.025),
upper_95_center = quantile(dr$mu, 0.975)
)
}))
knitr::kable(cluster_results, digits = 3, caption = "Bayesian uncertainty for exploratory galaxy velocity groups")| cluster | n | sample_mean | posterior_mean_center | lower_95_center | upper_95_center | |
|---|---|---|---|---|---|---|
| 2.5% | low-velocity group | 7 | 9.710 | 9.713 | 8.041 | 11.318 |
| 2.5%1 | middle group | 70 | 21.245 | 21.242 | 20.727 | 21.742 |
| 2.5%2 | high-velocity group | 5 | 30.564 | 30.538 | 27.300 | 33.811 |
Lesson for scientific data science. ML can help detect structure; Bayesian inference can quantify uncertainty about that structure. In scientific work, these two are often complementary rather than competitors.
A prior is not merely a mathematical nuisance. It expresses a probability distribution on unknown quantities before the current data are used.
| Prior type | Meaning | Scientific use |
|---|---|---|
| Subjective prior | expresses expert knowledge or previous experiments | instrument calibration, known physical ranges |
| Objective/reference prior | designed from model structure, often to be weakly informative | default inference when little prior information is available |
| Weakly informative prior | rules out absurd values while remaining broad | stabilizes high-dimensional or weak-data problems |
| Conjugate prior | algebraically convenient prior family | transparent teaching and fast computation |
| Hierarchical prior | parameters share information through common hyperparameters | partial pooling across telescopes, regions, species, experiments |
Small data can be sensitive to prior choice. Large data usually reduce this sensitivity, unless the likelihood is misspecified.
set.seed(99)
n_small <- 8
p_true <- 0.7
y_small <- rbinom(1, n_small, p_true)
priors <- data.frame(
prior = c("Uniform Beta(1,1)", "Skeptical Beta(2,8)", "Optimistic Beta(8,2)", "Moderate Beta(3,3)"),
a = c(1, 2, 8, 3),
b = c(1, 8, 2, 3)
)
prior_sens <- priors
prior_sens$post_mean <- (priors$a + y_small) / (priors$a + priors$b + n_small)
prior_sens$lower_95 <- qbeta(0.025, priors$a + y_small, priors$b + n_small - y_small)
prior_sens$upper_95 <- qbeta(0.975, priors$a + y_small, priors$b + n_small - y_small)
prior_sens$prob_gt_half <- 1 - pbeta(0.5, priors$a + y_small, priors$b + n_small - y_small)
knitr::kable(prior_sens, digits = 3, caption = paste0("Prior sensitivity with small data: y = ", y_small, " out of n = ", n_small))| prior | a | b | post_mean | lower_95 | upper_95 | prob_gt_half |
|---|---|---|---|---|---|---|
| Uniform Beta(1,1) | 1 | 1 | 0.600 | 0.299 | 0.863 | 0.746 |
| Skeptical Beta(2,8) | 2 | 8 | 0.389 | 0.184 | 0.617 | 0.166 |
| Optimistic Beta(8,2) | 8 | 2 | 0.722 | 0.501 | 0.897 | 0.975 |
| Moderate Beta(3,3) | 3 | 3 | 0.571 | 0.316 | 0.808 | 0.709 |
plot_prior_sens <- do.call(rbind, lapply(seq_len(nrow(priors)), function(i) {
data.frame(
p = p_grid,
density = dbeta(p_grid, priors$a[i] + y_small, priors$b[i] + n_small - y_small),
prior = priors$prior[i]
)
}))
ggplot(plot_prior_sens, aes(p, density, color = prior)) +
geom_line(linewidth = 1.1) +
labs(
title = "Prior sensitivity when data are small",
subtitle = "Different priors lead to visibly different posteriors when n is small.",
x = "p", y = "posterior density", color = "prior"
)Bayesian inference becomes decision-making when we specify an action \(a\) and a loss function \(L(a,\theta)\). The Bayes action minimizes posterior expected loss:
\[ a^*(y)=\arg\min_a E\{L(a,\theta)\mid y\}. \]
Examples:
In natural science, decision problems include observing-time allocation, alert thresholds, telescope follow-up scheduling, quality control, hazard ranking, and experiment design.
A responsible Bayesian analysis usually follows this loop:
Bayesian inference is not the claim that a model is true. It is a coherent way to reason conditionally on a model. Scientific honesty requires checking and revising the model.
## R version 4.4.0 (2024-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: Asia/Calcutta
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] MASS_7.3-65 knitr_1.51 ggplot2_4.0.2
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.6.5 cli_3.6.2 rlang_1.1.4 xfun_0.56
## [5] otel_0.2.0 generics_0.1.4 S7_0.2.0 jsonlite_1.8.8
## [9] labeling_0.4.3 glue_1.7.0 htmltools_0.5.9 sass_0.4.10
## [13] scales_1.4.0 rmarkdown_2.30 grid_4.4.0 tibble_3.2.1
## [17] evaluate_1.0.5 jquerylib_0.1.4 fastmap_1.2.0 yaml_2.3.12
## [21] lifecycle_1.0.5 compiler_4.4.0 dplyr_1.1.4 RColorBrewer_1.1-3
## [25] pkgconfig_2.0.3 rstudioapi_0.18.0 farver_2.1.2 digest_0.6.35
## [29] R6_2.6.1 tidyselect_1.2.1 dichromat_2.0-0.1 pillar_1.11.1
## [33] magrittr_2.0.3 bslib_0.10.0 withr_3.0.2 tools_4.4.0
## [37] gtable_0.3.6 cachem_1.1.0
Berger, J. O. (1985). Statistical Decision Theory and Bayesian Analysis. Springer.
Gelman, A., Carlin, J. B., Stern, H. S., Dunson, D. B., Vehtari, A., and Rubin, D. B. (2013). Bayesian Data Analysis. Chapman and Hall/CRC.
Gelman, A., Vehtari, A., Simpson, D., et al. (2020). Bayesian workflow. arXiv:2011.01808.
Gelman, A. and Shalizi, C. R. (2013). Philosophy and the practice of Bayesian statistics. British Journal of Mathematical and Statistical Psychology, 66, 8–38.
Jaynes, E. T. (2003). Probability Theory: The Logic of Science. Cambridge University Press.
Robert, C. P. (2007). The Bayesian Choice. Springer.
Venables, W. N. and Ripley, B. D. (2002). Modern Applied Statistics with S. Springer.