Capturing Direct, Indirect, and Mediation Effects with Bayesian Statistics - Private-Projects237/Statistics GitHub Wiki

Overview

This wikipage will be recalculating estimates from the examples used in Intermediate Path Analysis Examples in R. The goal is to understand how to capture direct, indirect, and mediation effects using Bayesian statistics. Specifically, the brm() function from the 'brms' package. To validate that the estimate we are obtaining are actually representing what we think they are, we will be comparing the outputs specifically to the examples from the path analysis wiki.

Example 1: Multiple Regression (No Multicollinearity)

Generating the data

# These are the same examples from the 'Intermediate Path Analysis Examples.R'
# Load required packages
library(brms)
library(tidyverse)
library(MASS)    
library(kableExtra)

# Set seed for reproducibility
set.seed(123)

# Set population parameters
b0 = .5
b1 = -.05
b2 = -.06
b3 = -.04
sig = .12 # more appropriate for logistic model

# Step 1: Generate synthetic data
n <- 5000  # Sample size

# Create a correlation matrix (moderate correlations)
cov_matrix <- matrix(c(
  1.0, 0.5, 0.4,
  0.5, 1.0, 0.55,
  0.4, 0.55, 1.0
), nrow = 3)

# Generate predictors from multivariate normal distribution
predictors <- mvrnorm(n, mu = c(0, 0, 0), Sigma = cov_matrix)

# Create data frame with new variable names
data <- data.frame(
  x1 = predictors[, 1],  # anxiety
  x2 = predictors[, 2],  # depression
  x3 = predictors[, 3]   # stress
)

# Generate a linear predictor with noise
data$y <- b0 + b1 * data$x1 + b2 * data$x2 + b3 * data$x3 + rnorm(n, mean =0, sd = sig)

# Force the outcome to be between 0 and 100
data$y <- pmax(0, pmin(1, data$y ))

Specifying and running the model

# Run the Bayesian Model
mul_bay <- brm(
  y ~ x1 + x2 + x3,  # Same formula as lm()
  data = data,
  family = gaussian(),  # Continuous outcome (default link = identity)
  chains = 4,
  cores = 4,
  seed = 123
)


Model Output

# Model output summary
summary(mul_bay)

# Extract the parameters
round(fixef(mul_bay),2) # fixed effects
round(VarCorr(mul_bay)$residual__$sd,2) # sigma
round(bayes_R2(mul_bay), 2) # r^2
check_collinearity(mul_bay) # Checks multicollinearity
suppressMessages(suppressWarnings(
  standardize_parameters(mul_bay, method = "refit", verbose = FALSE))) # Standardized betas- removes annoying message

There is a lot of good information that is presented from the model summary. Starting at the top, we see that the model was y ~ x1 + x2 + x3, which is the multiple regression we intended to model. Then we see that there are 5000 observations within the dataset, which is correct. Moving to the 'Regression Coefficients', here we see all of the unstandardized beta coefficients and the y-intercept. There are no p-values but there are clearly 95% CI and we can see that they along with the standard errors are very very small, indicating that we can have a lot of confidence that are our estimates represent the true population estimates (which they do!). The in the 'Further Distributional Parameters', we see that the residual standard error is also present. Thus the bayesian model was able to reproduce the fixed effects along with the one random effect for the errors. In comparison to the path analysis model, we see that the estimates are identical!

Model Summary Path Analysis Model (Unstandardized)
Screenshot 2025-06-24 at 12 20 36 AM Screenshot 2025-06-23 at 6 17 33 PM

There are very cool functions that we can use to extract specific information from our Bayesian model. We can use functions like fixef() and VarCorr() to extract the unstandardized estimates. bayes_R2() will produce the coefficient of determination, which is nice (unsure if its for the outcome or for the full model), there is a test for vif through check_collinearity(), which is awesome! Lastly, we can convert our regressions into standardized beta coefficients using standardize_parameters(), and we see they also match that of the path analysis example.

Extracting Info From Model Path Analysis (Standardized )
Screenshot 2025-06-24 at 12 28 04 AM Screenshot 2025-06-23 at 6 16 16 PM

Example 2: Multiple Regression (Multicollinearity)

Generating the data

# Load required packages
library(tidyverse)
library(brms)
library(MASS)    
library(kableExtra)
library(parameters) # standardize_parameters
library(performance) # check_collinearity

# Set seed for reproducibility
set.seed(123)

# Set population parameters
b0 = .5
b1 = -.05
b2 = -.06
b3 = -.04
sig = .12 # more appropriate for logistic model

# Step 1: Generate synthetic data
n <- 5000  # Sample size

# Create a correlation matrix (moderate correlations)
cov_matrix <- matrix(c(
  1.0, 0.75, 0.80,
  0.75, 1.0, 0.83,
  0.80, 0.83, 1.0
), nrow = 3)

# Generate predictors from multivariate normal distribution
predictors <- mvrnorm(n, mu = c(0, 0, 0), Sigma = cov_matrix)

# Create data frame with new variable names
data <- data.frame(
  x1 = predictors[, 1],  # anxiety
  x2 = predictors[, 2],  # depression
  x3 = predictors[, 3]   # stress
)

# Generate a linear predictor with noise
data$y <- b0 + b1 * data$x1 + b2 * data$x2 + b3 * data$x3 + rnorm(n, mean =0, sd = sig)

# Force the outcome to be between 0 and 100
data$y <- pmax(0, pmin(1, data$y ))

Specifying and running the model

# Run the Bayesian Model
mul2_bay <- brm(
  y ~ x1 + x2 + x3,  # Same formula as lm()
  data = data,
  family = gaussian(),  # Continuous outcome (default link = identity)
  chains = 4,
  cores = 4,
  seed = 123
)

Model Output

# Model output summary
summary(mul2_bay)

# Extract the parameters
round(fixef(mul2_bay),2) # fixed effects
round(VarCorr(mul2_bay)$residual__$sd,2) # sigma
round(bayes_R2(mul2_bay), 2) # r^2
check_collinearity(mul2_bay) # Checks multicollinearity
suppressMessages(suppressWarnings(
  standardize_parameters(mul2_bay, method = "refit", verbose = FALSE))) # Standardized betas- removes annoying message

We can skip the model output this time and directly focus on the information extracted from the model. We can see that we have extracted the unstandardized beta coefficients and the residual standard error along with $R^2$. Notice that 1 - $R^2$ is equal to the residual variance form the path analysis path diagram. The interesting part is vif, while the numbers are getting kinda high they are still below 5, thus making them appropriate in their inclusion into the model. Next we see that the standardized betas match that of the path analysis- thus the model produced the results as expected.

Extracting Info From Model Path Analysis (Standardized )
Screenshot 2025-06-24 at 12 57 02 AM Screenshot 2025-06-23 at 6 56 37 PM

Example 3: Introducing a Mediator (No Multicollinearity)

Generating the data

# Load required packages
library(tidyverse)
library(brms)
library(MASS)    
library(kableExtra)
library(tidybayes)  # For spread_draws(), mean_qi()

# Set seed for reproducibility
set.seed(123)

# Set population parameters
b0_Y = .6
b0_M = .4
b1 = -.05 # Keep these the same (beta estimates; they are the total effect)
b2 = -.06
b3 = -.04
a1 = -.07 # pathways a
a2 = -.06
a3 = -.03
b =   0.2 # pathway b (only 1)
e_sig = .08
y_sig = .08 # more appropriate for this type of model
c_prime1 = b1 - (a1 * b)
c_prime2 = b2 - (a2 * b)
c_prime3 = b3 - (a3 * b)

# Step 1: Generate synthetic data
n <- 5000  # Sample size

# Create a correlation matrix (moderate correlations)
cov_matrix <- matrix(c(
  1.0, 0.5, 0.4,
  0.5, 1.0, 0.55,
  0.4, 0.55, 1.0
), nrow = 3)

# Generate predictors from multivariate normal distribution
predictors <- mvrnorm(n, mu = c(0, 0, 0), Sigma = cov_matrix)

# Create data frame with new variable names
data <- data.frame(
  x1 = predictors[, 1],  # anxiety
  x2 = predictors[, 2],  # depression
  x3 = predictors[, 3]   # stress
)

# Generate a Mediator variable
data$m <- b0_M + a1 * data$x1 + a2* data$x2 + a3 * data$x3 + rnorm(n, 0, sd = e_sig)

# Generate a linear predictor with noise + Mediator
data$y <- b0_Y + b1 * data$x1 + b2 * data$x2 + b3 * data$x3 + b * data$m + rnorm(n, 0, sd = y_sig)

# Force the outcome to be between 0 and 1
data$y <- pmax(0, pmin(1, data$y))

Specifying and running the model

bayes_mediation <- brm(
  bf(m ~ x1 + x2 + x3) +  # Path a: for all predictors
    bf(y ~ x1 + x2 + x3 + m) +  # Paths c' for all predictors + mediator
    set_rescor(FALSE),  # No residual correlation; SUPER IMPORTANT!
  data = data,
  family = gaussian(),
  chains = 4,
  cores = 4,
  seed = 123
)

Model Output

# Model output summary
summary(bayes_mediation)

# Extract the parameters
round(fixef(bayes_mediation),2) # fixed effects
round(VarCorr(bayes_mediation)$residual__$sd,2) # sigma
round(bayes_R2(bayes_mediation), 2) # r^2

# Check multicollinearity manually
lm1 <- lm(m ~ x1 + x2 + x3, data = data)
lm2 <- lm(y ~ x1 + x2 + x3 + m, data = data)
check_collinearity(lm1)
check_collinearity(lm2)

# Check the standardized beta coefficients
# Rerun the model fully standardized
data_std <- data %>% mutate(across(c(x1, x2, x3, m, y), scale))
bayes_mediation_std <- suppressMessages(suppressWarnings(
  update(bayes_mediation, newdata = data_std)))
round(fixef(bayes_mediation_std),2)

This time let's look at the model output- we see that this is a mediation analysis and a more complex one since we have three variables (x1, x2, x3) predicting a mediator 'm' and then those same predictors plus 'm' are being used to predict the outcome 'y'. Below we see the unstandardized beta coefficients, which include the relationships between x and m, x and y, and the b pathway which is m and y. Below that we see the residual error distribution for both endogenous variables 'm' and 'y'- extremely interesting is that the standard deviation for both of these variables is exactly as what was programmed! Additionally all of the unstandardized beta coefficients are identical to what was produced by the path analysis version.

Model Output
Screenshot 2025-06-24 at 2 16 30 AM

Okay so we needed to make some changes here since the previous functions we were using no longer work with these more complex models. To check vif, it is now recommended to run multiple regressions for each endogenous variables and check the variance inflation factor through the vif() function. The mediator is getting pretty high but I think that is okay and it makes sense considering that it is a composite of the three predictors. Additionally, to get the standardized beta coefficients, it is know recommended to use the scale() function on all the variables to standardized them. Aftewards, rerun the model using the update() function, this will produced the standardized estimates that we can then extract. We see that also from the bayes_R2() function that $R^2$ were produced for both exogenous variables and matched 1 - residual variance of the standardized path analysis.

Extracting Info From Model + lm() models Path Analysis (Standardized )
Screenshot 2025-06-24 at 2 30 20 AM Screenshot 2025-06-23 at 8 29 13 PM

We are not done just yet- we should also look at the indirect effects possible through the mediation in the model. This next part, of the code to produce it is very tedious in Bayesian. We basically need to use the spread_draws() function to obtain thousands of simulated estimates, those for the slope of a predictor (x1, x2, x3) and the mediator ('m'), which are our pathway a's, and the slope of 'm' with 'y', which is our pathway b. Once these values are drawn we need to multiply them together to produce the indirect effect and then do some additional algebra to calculate the total proportion of mediation. we then use the median_qi() function to produce the means of these calculations plus 95% confidence intervals so we know if they are statistically significant or not. Afterwards we display this information.

Generating Indirect Effects (Tedious)

# Extract relevant posterior draws
draws <- bayes_mediation_std %>%
  spread_draws(b_m_x1, b_m_x2, b_m_x3, b_y_m, b_y_x1, b_y_x2, b_y_x3)

# Compute indirect effects and mediation proportions
indirect_effects <- draws %>%
  mutate(
    indirect_x1 = b_m_x1 * b_y_m,
    indirect_x2 = b_m_x2 * b_y_m,
    indirect_x3 = b_m_x3 * b_y_m,
    
    total_x1 = indirect_x1 + b_y_x1,
    total_x2 = indirect_x2 + b_y_x2,
    total_x3 = indirect_x3 + b_y_x3,
    
    prop_med_x1 = indirect_x1 / total_x1,
    prop_med_x2 = indirect_x2 / total_x2,
    prop_med_x3 = indirect_x3 / total_x3
  )

# Summarize the indirect effects and mediation proportions
indirect_summary <- indirect_effects %>%
  median_qi(indirect_x1, indirect_x2, indirect_x3,
            prop_med_x1, prop_med_x2, prop_med_x3) %>%
  as.data.frame() %>%
  dplyr::select(-.width, -.point, -.interval) %>%
  round(3) %>% c()

# Print results as transposed table
indirect_summary_matrix <- matrix(indirect_summary, nrow = 2,byrow = TRUE)
row.names(indirect_summary_matrix) <- c("Indirect Effect", "Mediation")
colnames(indirect_summary_matrix) <- sub(".*_", "", names(indirect_summary))[1:(length(indirect_summary)/2)]
indirect_summary_matrix

As we can see from below- the indirect effects and the mediation proportion match. Also, these are the standardized indirect effects not the unstandardized ones.

Indirect effects Indirect Effects Path Analysis
Screenshot 2025-06-24 at 3 13 46 AM Screenshot 2025-06-24 at 3 11 58 AM
⚠️ **GitHub.com Fallback** ⚠️