This example first simulates a simple linear regression dataset and then uses a manually implemented Metropolis–Hastings Markov chain Monte Carlo (MCMC) algorithm to estimate three unknown parameters:
The assumed linear regression model is
\[ y_i = a x_i + b + \varepsilon_i, \]
where
\[ \varepsilon_i \sim N(0,\sigma^2). \]
Equivalently,
\[ y_i \mid a,b,\sigma \sim N(a x_i+b,\sigma^2). \]
The true parameter values used to generate the simulated data are
\[ a=5,\qquad b=0,\qquad \sigma=10. \]
In a real analysis, these parameters are unknown. They are specified here only to generate simulated data and to evaluate whether the Bayesian estimates recover values close to the truth.
set.seed(123)
set.seed(123) fixes the random-number seed.
This program uses random-number generators such as:
rnorm() for normal random variables;runif() for uniform random variables.Without a fixed seed, the simulated data and MCMC results will change every time the code is run.
Using the same seed makes the analysis reproducible and is useful for:
trueA <- 5
trueB <- 0
trueSd <- 10
These values define the true data-generating parameters:
\[ a=5, \]
\[ b=0, \]
and
\[ \sigma=10. \]
Therefore, the true data-generating model is
\[ y_i = 5x_i + \varepsilon_i, \]
where
\[ \varepsilon_i \sim N(0,10^2). \]
The objects trueA, trueB, and
trueSd are used only to simulate data. The Bayesian
analysis below estimates the parameters from the simulated
observations.
sampleSize <- 31
The sample size is
\[ n=31. \]
Therefore, the dataset contains 31 values of \(x\) and 31 corresponding values of \(y\).
x <- (-(sampleSize - 1) / 2):
((sampleSize - 1) / 2)
x
Because
\[ \frac{31-1}{2}=15, \]
the predictor values are
\[ -15,-14,-13,\ldots,-1,0,1,\ldots,13,14,15. \]
These values are symmetric around zero, so
\[ \bar{x}=0. \]
Centering the predictor around zero has two common advantages:
y <- trueA * x +
trueB +
rnorm(
n = sampleSize,
mean = 0,
sd = trueSd
)
This code generates the outcome according to
\[ y_i = a x_i+b+\varepsilon_i. \]
Substituting the true parameter values gives
\[ y_i = 5x_i+0+\varepsilon_i, \]
where
\[ \varepsilon_i\sim N(0,10^2). \]
The following code generates 31 independent normal errors:
rnorm(
n = sampleSize,
mean = 0,
sd = trueSd
)
Each outcome therefore consists of a systematic component and a random component:
\[ y_i=\text{linear trend}+\text{random error}. \]
In this example,
\[ y_i=5x_i+\varepsilon_i. \]
plot(
x,
y,
main = "Simulated Data",
xlab = "x",
ylab = "y",
pch = 19
)
Because the true slope is positive,
\[ a=5, \]
the scatterplot should show a clear increasing trend.
However, because the residual standard deviation is 10, the observations do not fall exactly on a straight line.
log_likelihood <- function(param) {
a <- param[1]
b <- param[2]
sigma <- param[3]
if (!is.finite(sigma) || sigma <= 0) {
return(-Inf)
}
predicted_mean <- a * x + b
log_density <- dnorm(
y,
mean = predicted_mean,
sd = sigma,
log = TRUE
)
return(sum(log_density))
}
The input param is a vector containing three
parameters:
\[ \text{param}=(a,b,\sigma). \]
The following lines extract the parameters:
a <- param[1]
b <- param[2]
sigma <- param[3]
Thus:
param[1] is the slope \(a\);param[2] is the intercept \(b\);param[3] is the residual standard deviation \(\sigma\).For example,
param <- c(5, 0, 10)
represents
\[ a=5,\qquad b=0,\qquad \sigma=10. \]
if (!is.finite(sigma) || sigma <= 0) {
return(-Inf)
}
A normal standard deviation must satisfy
\[ \sigma>0. \]
The parameter is invalid if \(\sigma\) is:
NA;NaN;Inf;-Inf.In these cases, the function returns
\[ -\infty. \]
Because
\[ \log(0)=-\infty, \]
returning -Inf tells the MCMC algorithm that this
parameter combination has zero probability and should be rejected.
This check prevents dnorm() from receiving an invalid
standard deviation.
predicted_mean <- a * x + b
For each observation, the conditional mean is
\[ \mu_i=ax_i+b. \]
Therefore,
\[ E(y_i\mid a,b,\sigma)=ax_i+b. \]
predicted_mean is a vector of length 31. Its \(i\)th element is
\[ a x_i+b. \]
log_density <- dnorm(
y,
mean = predicted_mean,
sd = sigma,
log = TRUE
)
The model assumes
\[ y_i\mid a,b,\sigma\sim N(ax_i+b,\sigma^2). \]
The normal density for a single observation is
\[ p(y_i\mid a,b,\sigma) = \frac{1}{\sqrt{2\pi}\sigma} \exp\left[ -\frac{(y_i-ax_i-b)^2}{2\sigma^2} \right]. \]
Taking logarithms gives
\[ \log p(y_i\mid a,b,\sigma) = -\frac{1}{2}\log(2\pi) -\log(\sigma) -\frac{(y_i-ax_i-b)^2}{2\sigma^2}. \]
The argument
log = TRUE
instructs R to calculate the log-density directly.
This is numerically more stable than multiplying many small probability densities.
return(sum(log_density))
Assuming conditional independence of observations, the joint likelihood is
\[ L(a,b,\sigma) = \prod_{i=1}^{n} p(y_i\mid a,b,\sigma). \]
Taking logarithms converts the product into a sum:
\[ \log L(a,b,\sigma) = \sum_{i=1}^{n} \log p(y_i\mid a,b,\sigma). \]
Therefore, log_likelihood() returns the joint
log-likelihood for the complete dataset.
log_prior <- function(param) {
a <- param[1]
b <- param[2]
sigma <- param[3]
if (
a < 0 ||
a > 10 ||
sigma <= 0 ||
sigma > 30
) {
return(-Inf)
}
log_prior_a <- dunif(
a,
min = 0,
max = 10,
log = TRUE
)
log_prior_b <- dnorm(
b,
mean = 0,
sd = 5,
log = TRUE
)
log_prior_sigma <- dunif(
sigma,
min = 0,
max = 30,
log = TRUE
)
return(
log_prior_a +
log_prior_b +
log_prior_sigma
)
}
if (
a < 0 ||
a > 10 ||
sigma <= 0 ||
sigma > 30
) {
return(-Inf)
}
Under the current prior specification, the slope must satisfy
\[ 0\leq a\leq10, \]
and the residual standard deviation must satisfy
\[ 0<\sigma\leq30. \]
A proposed parameter value outside these ranges has prior density zero and log-prior
\[ -\infty. \]
For example, the following values are automatically rejected:
\[ a=-1, \]
\[ a=12, \]
\[ \sigma=-2, \]
and
\[ \sigma=35. \]
log_prior_a <- dunif(
a,
min = 0,
max = 10,
log = TRUE
)
The slope prior is
\[ a\sim U(0,10). \]
Its density is
\[ p(a)= \begin{cases} \frac{1}{10}, & 0\leq a\leq10,\\ 0, & \text{otherwise}. \end{cases} \]
This prior means that:
Within the support,
\[ \log p(a)=\log\left(\frac{1}{10}\right). \]
log_prior_b <- dnorm(
b,
mean = 0,
sd = 5,
log = TRUE
)
The intercept prior is
\[ b\sim N(0,5^2). \]
This means that:
Approximately 95% of the prior probability lies within
\[ 0\pm1.96\times5, \]
or approximately
\[ (-9.8,9.8). \]
log_prior_sigma <- dunif(
sigma,
min = 0,
max = 30,
log = TRUE
)
The prior for the residual standard deviation is
\[ \sigma\sim U(0,30). \]
This assigns equal prior density to values between 0 and 30.
Because a standard deviation must be strictly positive, the code separately excludes
sigma <= 0
Uniform priors are convenient for teaching. In formal Bayesian analyses, alternatives may include:
return(
log_prior_a +
log_prior_b +
log_prior_sigma
)
The code assumes prior independence:
\[ p(a,b,\sigma) = p(a)p(b)p(\sigma). \]
Taking logarithms gives
\[ \log p(a,b,\sigma) = \log p(a) + \log p(b) + \log p(\sigma). \]
Therefore, the three log-prior contributions can be added.
log_posterior <- function(param) {
lp <- log_prior(param)
if (!is.finite(lp)) {
return(-Inf)
}
return(
log_likelihood(param) + lp
)
}
Bayes’ theorem is
\[ p(a,b,\sigma\mid y) = \frac{ p(y\mid a,b,\sigma)p(a,b,\sigma) }{ p(y) }. \]
Because the denominator \(p(y)\) does not depend on the parameters,
\[ p(a,b,\sigma\mid y) \propto p(y\mid a,b,\sigma)p(a,b,\sigma). \]
Taking logarithms gives
\[ \log p(a,b,\sigma\mid y) = \log p(y\mid a,b,\sigma) + \log p(a,b,\sigma) + C, \]
where \(C\) does not depend on the parameters.
Therefore, the MCMC algorithm only needs
\[ \log p(y\mid a,b,\sigma) + \log p(a,b,\sigma). \]
lp <- log_prior(param)
The function first calculates the log-prior.
If the parameter vector lies outside the prior support,
log_prior() returns -Inf.
if (!is.finite(lp)) {
return(-Inf)
}
In that situation, there is no need to calculate the likelihood.
This:
return(
log_likelihood(param) + lp
)
The function returns
\[ \text{log-likelihood}+\text{log-prior}. \]
That is,
\[ \log \widetilde{p}(a,b,\sigma\mid y), \]
the unnormalized log-posterior density.
Metropolis–Hastings only requires posterior-density ratios, so the normalizing constant does not need to be calculated.
proposal_function <- function(param) {
rnorm(
n = 3,
mean = param,
sd = c(0.1, 0.5, 0.3)
)
}
At each MCMC iteration, a new candidate parameter vector is generated near the current parameter vector.
Let the current state be
\[ \theta^{(t)} = \left( a^{(t)},b^{(t)},\sigma^{(t)} \right). \]
The proposal distribution is
\[ a^*\sim N(a^{(t)},0.1^2), \]
\[ b^*\sim N(b^{(t)},0.5^2), \]
and
\[ \sigma^*\sim N(\sigma^{(t)},0.3^2). \]
Equivalently,
\[ a^*=a^{(t)}+\varepsilon_a, \]
\[ b^*=b^{(t)}+\varepsilon_b, \]
and
\[ \sigma^*=\sigma^{(t)}+\varepsilon_\sigma, \]
where
\[ \varepsilon_a\sim N(0,0.1^2), \]
\[ \varepsilon_b\sim N(0,0.5^2), \]
and
\[ \varepsilon_\sigma\sim N(0,0.3^2). \]
This is called a random-walk proposal distribution.
The code
sd = c(0.1, 0.5, 0.3)
specifies:
If the proposal step sizes are too small:
If the proposal step sizes are too large:
Proposal scales should therefore be assessed using acceptance rates, trace plots, autocorrelation, and effective sample sizes.
run_metropolis_MCMC <- function(
start_value,
iterations
) {
chain <- matrix(
NA_real_,
nrow = iterations + 1,
ncol = 3
)
colnames(chain) <- c(
"slope",
"intercept",
"sigma"
)
chain[1, ] <- start_value
accepted <- 0
for (i in seq_len(iterations)) {
current <- chain[i, ]
proposal <- proposal_function(current)
log_acceptance_ratio <-
log_posterior(proposal) -
log_posterior(current)
if (
is.finite(log_acceptance_ratio) &&
log(runif(1)) < min(0, log_acceptance_ratio)
) {
chain[i + 1, ] <- proposal
accepted <- accepted + 1
} else {
chain[i + 1, ] <- current
}
}
return(
list(
chain = chain,
acceptance_rate = accepted / iterations
)
)
}
The function has two inputs:
start_value
which specifies the initial parameter vector, and
iterations
which specifies the number of Metropolis iterations.
chain <- matrix(
NA_real_,
nrow = iterations + 1,
ncol = 3
)
The matrix chain stores the parameter values at every
iteration.
The three columns correspond to
\[ a,\quad b,\quad \sigma. \]
The number of rows is
\[ \text{iterations}+1, \]
because the first row stores the initial value.
For example, if
iterations <- 10000
then the matrix has dimension
\[ 10001\times3. \]
The first row stores the initial parameter vector, and the final row stores the result after 10,000 updates.
NA_real_ initializes the matrix as a numeric matrix.
colnames(chain) <- c(
"slope",
"intercept",
"sigma"
)
The three columns are named:
slope;intercept;sigma.The slope can then be extracted using
chain[, "slope"]
rather than only
chain[, 1]
chain[1, ] <- start_value
The initial parameter vector is stored in the first row.
accepted <- 0
accepted records the number of accepted proposals.
Each accepted proposal increases the counter:
accepted <- accepted + 1
The acceptance rate is
\[ \text{Acceptance rate} = \frac{\text{number of accepted proposals}} {\text{total number of iterations}}. \]
for (i in seq_len(iterations)) {
seq_len(iterations) generates
\[ 1,2,\ldots,\text{iterations}. \]
Therefore, the loop runs the specified number of times.
current <- chain[i, ]
At iteration \(i\), the current state is
\[ \theta^{(t)} = \left( a^{(t)},b^{(t)},\sigma^{(t)} \right). \]
proposal <- proposal_function(current)
A candidate parameter vector is generated:
\[ \theta^* = (a^*,b^*,\sigma^*). \]
log_acceptance_ratio <-
log_posterior(proposal) -
log_posterior(current)
The general Metropolis–Hastings acceptance probability is
\[ \alpha = \min\left[ 1, \frac{ p(\theta^*\mid y) q(\theta^{(t)}\mid\theta^*) }{ p(\theta^{(t)}\mid y) q(\theta^*\mid\theta^{(t)}) } \right]. \]
Because the proposal distribution is a symmetric normal random walk,
\[ q(\theta^*\mid\theta^{(t)}) = q(\theta^{(t)}\mid\theta^*). \]
The proposal-density ratio cancels, leaving
\[ \alpha = \min\left[ 1, \frac{ p(\theta^*\mid y) }{ p(\theta^{(t)}\mid y) } \right]. \]
Define
\[ r= \frac{ p(\theta^*\mid y) }{ p(\theta^{(t)}\mid y) }. \]
Then
\[ \log r = \log p(\theta^*\mid y) - \log p(\theta^{(t)}\mid y). \]
This is exactly what log_acceptance_ratio
calculates.
A less stable implementation might calculate
exp(
log_posterior(proposal) -
log_posterior(current)
)
However, large differences can produce numerical overflow or underflow:
exp(1000) becomes Inf;exp(-1000) becomes 0.The revised implementation performs the comparison directly on the log scale and avoids this problem.
if (
is.finite(log_acceptance_ratio) &&
log(runif(1)) < min(0, log_acceptance_ratio)
)
The standard Metropolis rule generates
\[ u\sim U(0,1) \]
and accepts the proposal if
\[ u<\alpha. \]
Because
\[ \alpha=\min(1,r), \]
we have
\[ \log\alpha=\min(0,\log r). \]
Therefore, the acceptance rule can be written as
\[ \log u<\min(0,\log r). \]
This corresponds to
log(runif(1)) < min(0, log_acceptance_ratio)
If
\[ \log r>0, \]
then
\[ r>1. \]
The proposal has higher posterior density than the current state, so
\[ \alpha=1. \]
The proposal is always accepted.
If
\[ \log r<0, \]
then
\[ 0<r<1. \]
The proposal is still accepted with probability \(r\).
For example, if
\[ \log r=-1.609, \]
then
\[ r=\exp(-1.609)\approx0.20. \]
The proposal is accepted approximately 20% of the time.
Allowing occasional moves to lower-posterior-density states helps the chain explore the full posterior distribution rather than becoming trapped near one high-density location.
chain[i + 1, ] <- proposal
accepted <- accepted + 1
If the proposal is accepted,
\[ \theta^{(t+1)}=\theta^*. \]
The acceptance counter is increased by one.
chain[i + 1, ] <- current
If the proposal is rejected,
\[ \theta^{(t+1)}=\theta^{(t)}. \]
Therefore, consecutive rows of the MCMC chain may be identical. This is expected and indicates that a proposal was rejected.
return(
list(
chain = chain,
acceptance_rate = accepted / iterations
)
)
The function returns a list containing:
chain: the complete MCMC parameter chain;acceptance_rate: the proportion of accepted
proposals.start_value <- c(
slope = 4,
intercept = 0,
sigma = 10
)
The initial values are
\[ a^{(0)}=4, \]
\[ b^{(0)}=0, \]
and
\[ \sigma^{(0)}=10. \]
The initial values must lie within the prior support:
\[ 0\leq a\leq10 \]
and
\[ 0<\sigma\leq30. \]
Otherwise, the initial log-posterior may be -Inf, and
the algorithm may not start properly.
fit <- run_metropolis_MCMC(
start_value = start_value,
iterations = 10000
)
The algorithm performs 10,000 Metropolis updates.
The returned object is stored in fit.
chain <- fit$chain
dim(chain)
head(chain)
chain is a \(10001\times3\) matrix.
The columns represent:
The first row contains the initial values, and the following 10,000 rows contain the updated parameter values.
fit$acceptance_rate
The acceptance rate is
\[ \text{Acceptance rate} = \frac{ \text{number of accepted proposals} }{ \text{total number of iterations} }. \]
For a low-dimensional random-walk Metropolis algorithm, an acceptance rate around 20% to 50% may sometimes be reasonable, but this is only a rough guideline.
Acceptance rate alone cannot determine whether an MCMC chain is efficient.
For example:
The acceptance rate should be considered together with:
burn_in <- 5000
post_chain <- chain[
(burn_in + 1):nrow(chain),
,
drop = FALSE
]
dim(post_chain)
The early part of an MCMC chain may be influenced by the initial values.
Samples generated before the chain reaches a stable region are often called burn-in or warm-up samples.
This code removes the first 5,000 rows and retains rows
\[ 5001,5002,\ldots,10001. \]
The argument
drop = FALSE
ensures that the result remains a matrix.
Because the first row stores the initial value, the current code removes the first 5,000 rows of the stored chain.
To remove both the initial value and the first 5,000 updates explicitly, one could instead use
post_chain <- chain[
(burn_in + 2):nrow(chain),
,
drop = FALSE
]
The two approaches differ by only one sample and usually produce nearly identical summaries.
posterior_mean <- colMeans(post_chain)
posterior_mean
colMeans() calculates the mean of the MCMC draws for
each parameter.
The posterior mean of the slope is approximated by
\[ E(a\mid y) \approx \frac{1}{M} \sum_{m=1}^{M}a^{(m)}. \]
The posterior mean of the intercept is approximated by
\[ E(b\mid y) \approx \frac{1}{M} \sum_{m=1}^{M}b^{(m)}. \]
The posterior mean of the standard deviation is approximated by
\[ E(\sigma\mid y) \approx \frac{1}{M} \sum_{m=1}^{M}\sigma^{(m)}. \]
Because the true values are
\[ a=5,\qquad b=0,\qquad \sigma=10, \]
the posterior means should generally be close to these values, although they will not be exactly equal.
Differences may arise because of:
posterior_interval <- apply(
post_chain,
2,
quantile,
probs = c(0.025, 0.5, 0.975)
)
posterior_interval
For each parameter, the code calculates:
The 50th percentile is the posterior median.
The 2.5th and 97.5th percentiles form an equal-tailed 95% Bayesian credible interval:
\[ \left[ q_{0.025},q_{0.975} \right]. \]
For example, if the 95% credible interval for the slope is
\[ (4.6,5.4), \]
the Bayesian interpretation is:
Given the model, prior distributions, and observed data, the posterior probability that the slope lies between 4.6 and 5.4 is 95%.
This differs from the interpretation of a frequentist confidence interval.
posterior_summary <- data.frame(
Parameter = colnames(post_chain),
Mean = colMeans(post_chain),
Median = apply(post_chain, 2, median),
SD = apply(post_chain, 2, sd),
Lower_95 = apply(
post_chain,
2,
quantile,
probs = 0.025
),
Upper_95 = apply(
post_chain,
2,
quantile,
probs = 0.975
)
)
posterior_summary
The summary table includes:
comparison <- data.frame(
Parameter = c(
"slope",
"intercept",
"sigma"
),
True_value = c(
trueA,
trueB,
trueSd
),
Posterior_mean = posterior_mean
)
comparison
Because this is a simulation study, the true parameter values are known.
The table compares
\[ \text{true parameter values} \]
with
\[ \text{posterior means}. \]
In real applications, the true parameter values are unknown, so this direct comparison is not available.
par(mfrow = c(3, 1))
plot(
chain[, "slope"],
type = "l",
main = "Trace Plot: Slope",
xlab = "Iteration",
ylab = "Slope"
)
abline(
h = trueA,
lty = 2
)
plot(
chain[, "intercept"],
type = "l",
main = "Trace Plot: Intercept",
xlab = "Iteration",
ylab = "Intercept"
)
abline(
h = trueB,
lty = 2
)
plot(
chain[, "sigma"],
type = "l",
main = "Trace Plot: Sigma",
xlab = "Iteration",
ylab = "Sigma"
)
abline(
h = trueSd,
lty = 2
)
par(mfrow = c(1, 1))
A trace plot displays parameter values across MCMC iterations.
A well-behaved chain generally:
The horizontal dashed lines show the true parameter values.
In a real analysis, the true parameter values are unknown, so these reference lines would not be available.
par(mfrow = c(3, 1))
hist(
post_chain[, "slope"],
breaks = 30,
main = "Posterior Distribution of Slope",
xlab = "Slope",
probability = TRUE
)
lines(
density(post_chain[, "slope"]),
lwd = 2
)
abline(
v = trueA,
lty = 2
)
hist(
post_chain[, "intercept"],
breaks = 30,
main = "Posterior Distribution of Intercept",
xlab = "Intercept",
probability = TRUE
)
lines(
density(post_chain[, "intercept"]),
lwd = 2
)
abline(
v = trueB,
lty = 2
)
hist(
post_chain[, "sigma"],
breaks = 30,
main = "Posterior Distribution of Sigma",
xlab = "Sigma",
probability = TRUE
)
lines(
density(post_chain[, "sigma"]),
lwd = 2
)
abline(
v = trueSd,
lty = 2
)
par(mfrow = c(1, 1))
The posterior histograms show the posterior distributions of the three parameters.
They can be used to examine:
par(mfrow = c(3, 1))
acf(
post_chain[, "slope"],
main = "Autocorrelation: Slope"
)
acf(
post_chain[, "intercept"],
main = "Autocorrelation: Intercept"
)
acf(
post_chain[, "sigma"],
main = "Autocorrelation: Sigma"
)
par(mfrow = c(1, 1))
MCMC samples are generally not independent.
Autocorrelation plots measure the dependence between a draw and earlier draws in the chain.
If autocorrelation decreases slowly, this may indicate that:
The following section contains the complete analysis without the detailed explanation.
set.seed(123)
#-----------------------------
# 1. Generate simulated data
#-----------------------------
trueA <- 5
trueB <- 0
trueSd <- 10
sampleSize <- 31
x <- (-(sampleSize - 1) / 2):
((sampleSize - 1) / 2)
y <- trueA * x +
trueB +
rnorm(
n = sampleSize,
mean = 0,
sd = trueSd
)
plot(
x,
y,
main = "Simulated Data",
xlab = "x",
ylab = "y",
pch = 19
)
#-----------------------------
# 2. Log-likelihood
#-----------------------------
log_likelihood <- function(param) {
a <- param[1]
b <- param[2]
sigma <- param[3]
if (!is.finite(sigma) || sigma <= 0) {
return(-Inf)
}
predicted_mean <- a * x + b
log_density <- dnorm(
y,
mean = predicted_mean,
sd = sigma,
log = TRUE
)
return(sum(log_density))
}
#-----------------------------
# 3. Log-prior
#-----------------------------
log_prior <- function(param) {
a <- param[1]
b <- param[2]
sigma <- param[3]
if (
a < 0 ||
a > 10 ||
sigma <= 0 ||
sigma > 30
) {
return(-Inf)
}
log_prior_a <- dunif(
a,
min = 0,
max = 10,
log = TRUE
)
log_prior_b <- dnorm(
b,
mean = 0,
sd = 5,
log = TRUE
)
log_prior_sigma <- dunif(
sigma,
min = 0,
max = 30,
log = TRUE
)
return(
log_prior_a +
log_prior_b +
log_prior_sigma
)
}
#-----------------------------
# 4. Log-posterior
#-----------------------------
log_posterior <- function(param) {
lp <- log_prior(param)
if (!is.finite(lp)) {
return(-Inf)
}
return(
log_likelihood(param) + lp
)
}
#-----------------------------
# 5. Proposal distribution
#-----------------------------
proposal_function <- function(param) {
rnorm(
n = 3,
mean = param,
sd = c(0.1, 0.5, 0.3)
)
}
#-----------------------------
# 6. Metropolis algorithm
#-----------------------------
run_metropolis_MCMC <- function(
start_value,
iterations
) {
chain <- matrix(
NA_real_,
nrow = iterations + 1,
ncol = 3
)
colnames(chain) <- c(
"slope",
"intercept",
"sigma"
)
chain[1, ] <- start_value
accepted <- 0
for (i in seq_len(iterations)) {
current <- chain[i, ]
proposal <- proposal_function(current)
log_acceptance_ratio <-
log_posterior(proposal) -
log_posterior(current)
if (
is.finite(log_acceptance_ratio) &&
log(runif(1)) < min(0, log_acceptance_ratio)
) {
chain[i + 1, ] <- proposal
accepted <- accepted + 1
} else {
chain[i + 1, ] <- current
}
}
return(
list(
chain = chain,
acceptance_rate = accepted / iterations
)
)
}
#-----------------------------
# 7. Run MCMC
#-----------------------------
start_value <- c(
slope = 4,
intercept = 0,
sigma = 10
)
fit <- run_metropolis_MCMC(
start_value = start_value,
iterations = 10000
)
chain <- fit$chain
fit$acceptance_rate
## [1] 0.7789
#-----------------------------
# 8. Remove burn-in
#-----------------------------
burn_in <- 5000
post_chain <- chain[
(burn_in + 1):nrow(chain),
,
drop = FALSE
]
#-----------------------------
# 9. Posterior summaries
#-----------------------------
posterior_mean <- colMeans(post_chain)
posterior_interval <- apply(
post_chain,
2,
quantile,
probs = c(0.025, 0.5, 0.975)
)
posterior_mean
## slope intercept sigma
## 4.7819260 -0.2442336 10.1932665
posterior_interval
## slope intercept sigma
## 2.5% 4.340071 -3.9133242 7.962467
## 50% 4.785049 -0.1688851 10.064711
## 97.5% 5.165065 2.8609128 13.220178
#-----------------------------
# 10. Summary table
#-----------------------------
posterior_summary <- data.frame(
Parameter = colnames(post_chain),
Mean = colMeans(post_chain),
Median = apply(post_chain, 2, median),
SD = apply(post_chain, 2, sd),
Lower_95 = apply(
post_chain,
2,
quantile,
probs = 0.025
),
Upper_95 = apply(
post_chain,
2,
quantile,
probs = 0.975
)
)
posterior_summary
## Parameter Mean Median SD Lower_95 Upper_95
## slope slope 4.7819260 4.7850494 0.2111181 4.340071 5.165065
## intercept intercept -0.2442336 -0.1688851 1.7005650 -3.913324 2.860913
## sigma sigma 10.1932665 10.0647108 1.3701324 7.962467 13.220178
#-----------------------------
# 11. Trace plots
#-----------------------------
par(mfrow = c(3, 1))
plot(
chain[, "slope"],
type = "l",
main = "Trace Plot: Slope",
xlab = "Iteration",
ylab = "Slope"
)
plot(
chain[, "intercept"],
type = "l",
main = "Trace Plot: Intercept",
xlab = "Iteration",
ylab = "Intercept"
)
plot(
chain[, "sigma"],
type = "l",
main = "Trace Plot: Sigma",
xlab = "Iteration",
ylab = "Sigma"
)
par(mfrow = c(1, 1))
#-----------------------------
# 12. Posterior distributions
#-----------------------------
par(mfrow = c(3, 1))
hist(
post_chain[, "slope"],
breaks = 30,
probability = TRUE,
main = "Posterior Distribution of Slope",
xlab = "Slope"
)
lines(
density(post_chain[, "slope"]),
lwd = 2
)
hist(
post_chain[, "intercept"],
breaks = 30,
probability = TRUE,
main = "Posterior Distribution of Intercept",
xlab = "Intercept"
)
lines(
density(post_chain[, "intercept"]),
lwd = 2
)
hist(
post_chain[, "sigma"],
breaks = 30,
probability = TRUE,
main = "Posterior Distribution of Sigma",
xlab = "Sigma"
)
lines(
density(post_chain[, "sigma"]),
lwd = 2
)
par(mfrow = c(1, 1))
This program uses the Metropolis–Hastings algorithm to fit the Bayesian linear regression model
\[ y_i\mid a,b,\sigma \sim N(ax_i+b,\sigma^2). \]
The main steps are:
After sufficiently many iterations, the MCMC samples approximate
\[ p(a,b,\sigma\mid y). \]
These samples can then be used to estimate:
For example, the posterior probability that the slope is greater than zero can be estimated by
mean(post_chain[, "slope"] > 0)
## [1] 1
This approximates
\[ P(a>0\mid y). \]
In this example, the prior already restricts the slope to the interval from 0 to 10, so this probability will be equal or very close to 1.