Teaching Myself: Bayesian Path Modeling in blavaan and brms

In previous blog posts, I looked into Bayesian linear regression - with a single predictor, multiple predictors, and a single and multiple outcome variables. blavaan and brms also allow to specify path models (i.e., structural equation models without latent variables) that allow to investigate more complex relationships between variables.
r
Bayes
Path model
brms
blavaan
Author

Olaf Borghi

Published

Tuesday, April 16, 2024

# use groundhog to make code maximally reproducible
if (!require("groundhog", quietly = TRUE)) {
  install.packages("groundhog")
}
## Attached: 'Groundhog' (Version: 3.1.2)
## Tips and troubleshooting: https://groundhogR.com
library("groundhog")

# use groundhog to install and load packages - we may not use all of them and some consider it bad practise
# to still load them, but all of them are nice to have and could be useful
pkgs <- c("here",         # System path management
          "tidyverse",    # ggplot, dplyr, %>%, and friends
          "easystats",    # bayestestR, performance, effectsize and friends
          "modelsummary", # Data and model summaries with tables and plots
          "psych",        # Descriptive statistics and other tools
          "brms",         # Bayesian modeling through Stan
          "blavaan",      # Bayesian SEM
          "lavaan",       # Frequentist SEM
          "semPlot",      # Plots for SEM
          "tidybayes",    # Integration of Bayesian models in tidy + ggplot workflow
          "broom",        # Convert model objects to data frames
          "broom.mixed",  # Convert brms model objects to data frames
          "tinytable",    # Lightweight package to create tables
          "hrbrthemes",   # Additional ggplot themes
          "extrafont",    # Additional fonts for plots etc
          "ggdist",       # Special geoms for posterior distributions
          "ggrepel",      # Automatically position labels
          "gghalves",     # Special half geoms
          "patchwork"     # Combine ggplot objects
          )

groundhog.library(pkgs, "2024-03-01") # change to a recent date
## here() starts at C:/Users/Olaf/Documents/olafborghi.github.io/blog/2024/04/16
## Warning: package 'ggplot2' was built under R version 4.3.3
## ── Attaching core tidyverse packages ────────────────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.0     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.1
## ✔ purrr     1.0.2
## ── Conflicts ──────────────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
## Warning: package 'easystats' was built under R version 4.3.3
## # Attaching packages: easystats 0.7.0 (red = needs update)
## ✔ bayestestR  0.13.2   ✔ correlation 0.8.4 
## ✖ datawizard  0.9.1    ✖ effectsize  0.8.6 
## ✖ insight     0.19.8   ✔ modelbased  0.8.7 
## ✖ performance 0.10.9   ✖ parameters  0.21.5
## ✔ report      0.5.8    ✖ see         0.8.2 
## 
## Restart the R-Session and update packages with `easystats::easystats_update()`.
## 
## Version 2.0.0 of `modelsummary`, to be released soon, will introduce a breaking
##   change: The default table-drawing package will be `tinytable` instead of
##   `kableExtra`. All currently supported table-drawing packages will continue to be
##   supported for the foreseeable future, including `kableExtra`, `gt`, `huxtable`,
##   `flextable, and `DT`.
##   
##   You can always call the `config_modelsummary()` function to change the default
##   table-drawing package in persistent fashion. To try `tinytable` now:
##   
##   config_modelsummary(factory_default = 'tinytable')
##   
##   To set the default back to `kableExtra`:
##   
##   config_modelsummary(factory_default = 'kableExtra')
## 
## Attaching package: 'modelsummary'
## 
## The following object is masked from 'package:parameters':
## 
##     supported_models
## 
## The following object is masked from 'package:insight':
## 
##     supported_models
## 
## 
## Attaching package: 'psych'
## 
## The following object is masked from 'package:modelsummary':
## 
##     SD
## 
## The following object is masked from 'package:effectsize':
## 
##     phi
## 
## The following object is masked from 'package:datawizard':
## 
##     rescale
## 
## The following objects are masked from 'package:ggplot2':
## 
##     %+%, alpha
## 
## Loading required package: Rcpp
## Loading 'brms' package (version 2.20.4). Useful instructions
## can be found by typing help('brms'). A more detailed introduction
## to the package is available through vignette('brms_overview').
## 
## Attaching package: 'brms'
## 
## The following object is masked from 'package:psych':
## 
##     cs
## 
## The following object is masked from 'package:stats':
## 
##     ar
## Warning: package 'blavaan' was built under R version 4.3.3
## This is blavaan 0.5-3
## On multicore systems, we suggest use of future::plan("multicore") or
##   future::plan("multisession") for faster post-MCMC computations.
## This is lavaan 0.6-17
## lavaan is FREE software! Please report any bugs.
## 
## Attaching package: 'lavaan'
## 
## The following object is masked from 'package:psych':
## 
##     cor2cov
## 
## 
## Attaching package: 'tidybayes'
## 
## The following objects are masked from 'package:brms':
## 
##     dstudent_t, pstudent_t, qstudent_t, rstudent_t
## 
## The following object is masked from 'package:parameters':
## 
##     parameters
## 
## The following object is masked from 'package:bayestestR':
## 
##     hdi
## Warning: package 'broom.mixed' was built under R version 4.3.3
## NOTE: Either Arial Narrow or Roboto Condensed fonts are required to use these themes.
##       Please use hrbrthemes::import_roboto_condensed() to install Roboto Condensed and
##       if Arial Narrow is not on your system, please see https://bit.ly/arialnarrow
## Registering fonts with R
## 
## Attaching package: 'ggdist'
## 
## The following objects are masked from 'package:brms':
## 
##     dstudent_t, pstudent_t, qstudent_t, rstudent_t
## 
## The following object is masked from 'package:bayestestR':
## 
##     hdi
## Warning: package 'ggrepel' was built under R version 4.3.3
## Warning: package 'gghalves' was built under R version 4.3.3
## Warning: package 'patchwork' was built under R version 4.3.3
## [36mSuccessfully attached 'here_1.0.1'[0m
## [36mSuccessfully attached 'tidyverse_2.0.0'[0m
## [36mSuccessfully attached 'easystats_0.7.0'[0m
## [36mSuccessfully attached 'modelsummary_1.4.5'[0m
## [36mSuccessfully attached 'psych_2.4.1'[0m
## [36mSuccessfully attached 'brms_2.20.4'[0m
## [36mSuccessfully attached 'blavaan_0.5-3'[0m
## [36mSuccessfully attached 'lavaan_0.6-17'[0m
## [36mSuccessfully attached 'semPlot_1.1.6'[0m
## [36mSuccessfully attached 'tidybayes_3.0.6'[0m
## [36mSuccessfully attached 'broom_1.0.5'[0m
## [36mSuccessfully attached 'broom.mixed_0.2.9.4'[0m
## [36mSuccessfully attached 'tinytable_0.0.5'[0m
## [36mSuccessfully attached 'hrbrthemes_0.8.0'[0m
## [36mSuccessfully attached 'extrafont_0.19'[0m
## [36mSuccessfully attached 'ggdist_3.3.1'[0m
## [36mSuccessfully attached 'ggrepel_0.9.5'[0m
## [36mSuccessfully attached 'gghalves_0.1.4'[0m
## [36mSuccessfully attached 'patchwork_1.2.0'[0m

Teaching Myself: Bayesian Path Modeling

Using the same dataset as in previous blog posts, this time I will look into path models within a Bayesian statistical framework. Path models allow to directly check complex models, including associations between multiple predictors, multiple outcomes, and indirect associations.

I will not touch upon the topics of causality etc. here. However, I want to point out that before running a path model, one should carefully think about the relationships of interest. In addition, analyses of indirect effects are often called “mediation analysis”. Some use mediation in a merely causal context, and as I don’t touch causality here, I will stick to the term “indirect association”. Path models on cross-sectional data are also criticized from time to time, so here a few suggested reads before you think of doing this in your own research:

So beware: This blog post is mainly about messing around with some data and trying out different functions!

Specifically, we want to investigate whether there is an indirect association between Age and Conservatism, Dogmatism, and Religiosity via five cognitive factors Caution, Temporal Discounting, Perceptual Processing Time, Speed of Evidence Accumulation, and Strategic Information Processing (Self-regulation ontology; see Eisenberg et al., 2018; 2019)

Meet the data

I will again use open data from Zmigrod et al. (2021). This dataset includes cognitive and personality factors derived from exploratory factor analysis (EFA) of a large number of behavioral cognitive tasks and personality questionnaires from the Self-Regulation Ontology and in addition to that, several measures of ideology.

Some preparations and explorations

Exploratory factor analysis

I will directly re-use some of the code that was shared together with the manuscript (Zmigrod et al., 2021; see above) to obtain three ideology factors using EFA on the scores of several questionnaires.

# create df with relevant data for EFA 
efa_data <- data.frame(
        secs_social = data$SocialConservatism, 
        secs_econ = data$EconConservatism,
        nation = data$Nationalism, 
        auth = data$Authoritarianism,
        SJ = data$SJ, 
        sdo = data$SDO,
        patriot = data$Patriotism,
        progrSum = data$IngroupAttitude, #
        dogma = data$Dogmatism,
        IH1 = -1*data$IH1, # IH factors reversed because conceptually inverse of dogmatism
        IH2 = -1*data$IH2,
        IH3 = -1*data$IH3,
        IH4 = -1*data$IH4,
        rel_atten = data$ReligiousAttendance,
        rel_pray = data$ReligiousPrayer,
        rel_imp = data$ReligionImportance
      )

# run EFA 
scree(efa_data) # scree plot

fa.parallel(efa_data, fm = "ml") # parallel analysis

## Parallel analysis suggests that the number of factors =  3  and the number of components =  3
Ideol_EFA <- fa(efa_data, 
                nfactors=3, 
                rotate="oblimin")     # EFA oblimin for 3 factors
## Loading required namespace: GPArotation

print.psych(Ideol_EFA)
## Factor Analysis using method =  minres
## Call: fa(r = efa_data, nfactors = 3, rotate = "oblimin")
## Standardized loadings (pattern matrix) based upon correlation matrix
##               MR1   MR2   MR3   h2    u2 com
## secs_social  0.72 -0.06  0.32 0.76 0.245 1.4
## secs_econ    0.75 -0.08 -0.06 0.51 0.488 1.0
## nation       0.79  0.18 -0.02 0.70 0.299 1.1
## auth         0.44  0.11  0.10 0.28 0.725 1.2
## SJ           0.73 -0.02 -0.08 0.49 0.507 1.0
## sdo          0.59  0.25 -0.13 0.44 0.564 1.5
## patriot      0.76 -0.14  0.06 0.59 0.414 1.1
## progrSum     0.53  0.14 -0.02 0.33 0.668 1.1
## dogma        0.23  0.75  0.05 0.71 0.294 1.2
## IH1         -0.17  0.54  0.13 0.29 0.705 1.3
## IH2          0.01  0.62  0.08 0.40 0.604 1.0
## IH3         -0.12  0.83 -0.07 0.66 0.342 1.1
## IH4          0.07  0.63  0.06 0.44 0.561 1.0
## rel_atten    0.07  0.04  0.72 0.56 0.440 1.0
## rel_pray     0.02 -0.02  0.88 0.78 0.215 1.0
## rel_imp     -0.04  0.03  0.99 0.95 0.047 1.0
## 
##                        MR1  MR2  MR3
## SS loadings           3.84 2.54 2.50
## Proportion Var        0.24 0.16 0.16
## Cumulative Var        0.24 0.40 0.56
## Proportion Explained  0.43 0.29 0.28
## Cumulative Proportion 0.43 0.72 1.00
## 
##  With factor correlations of 
##      MR1  MR2  MR3
## MR1 1.00 0.23 0.34
## MR2 0.23 1.00 0.08
## MR3 0.34 0.08 1.00
## 
## Mean item complexity =  1.1
## Test of the hypothesis that 3 factors are sufficient.
## 
## df null model =  120  with the objective function =  8.8 with Chi Square =  2876
## df of  the model are 75  and the objective function was  0.92 
## 
## The root mean square of the residuals (RMSR) is  0.04 
## The df corrected root mean square of the residuals is  0.05 
## 
## The harmonic n.obs is  331 with the empirical chi square  117  with prob <  0.0013 
## The total n.obs was  334  with Likelihood Chi Square =  300  with prob <  1.1e-28 
## 
## Tucker Lewis Index of factoring reliability =  0.868
## RMSEA index =  0.095  and the 90 % confidence intervals are  0.084 0.106
## BIC =  -136
## Fit based upon off diagonal values = 0.99
## Measures of factor score adequacy             
##                                                    MR1  MR2  MR3
## Correlation of (regression) scores with factors   0.95 0.93 0.98
## Multiple R square of scores with factors          0.91 0.86 0.97
## Minimum correlation of possible factor scores     0.82 0.73 0.93

Alright, just as in the manuscript the EFA indicates three factors, the first appears to be a factor of political conservatism, as there are high loadings of social and economic conservatism, nationalism, religiosity on this factor. The second appears to be a factor of dogmatism, with high loadings of the dogmatism and intellectual humility measure. The third appears to be a religiosity factor.

If you want to explore EFA a bit more, here again a suggested read Factor Analysis with the psych package by Micheal Clark. For now we don’t really care that much about the details, all we want are the three factor scores so that we can explore a few path models.

# obtain factor loadings and add them to data
factor_scores <- as.data.frame(Ideol_EFA$scores)

data <- add_column(
  data, 
  conservatism_factor = factor_scores$MR1, 
  dogmatism_factor = factor_scores$MR2, 
  religiosity_factor = factor_scores$MR3  
  )

Standardizing the data

Again, I will standardize the data. The scales are arbitrary anyway in this case, and this simplifies specifying the priors in this very basic example.

df <- data %>%
  select(1, 3:11, 59:61) %>%
  rename(ID = SubjectN) %>% 
  mutate(Edu = as.factor(Edu),
         Gender = as.factor(Gender),
         ID = as.factor(ID)) %>% 
  mutate(across(where(is.numeric), scale))

str(df)
## tibble [334 × 13] (S3: tbl_df/tbl/data.frame)
##  $ ID                 : Factor w/ 334 levels "1","2","3","4",..: 1 2 3 4 5 6 7 8 9 10 ...
##  $ Age                : num [1:334, 1] -1.776 -0.479 -1.068 -0.715 -0.951 ...
##   ..- attr(*, "scaled:center")= num 37.1
##   ..- attr(*, "scaled:scale")= num 8.49
##  $ Edu                : Factor w/ 7 levels "1","2","3","4",..: 4 3 3 1 5 7 5 5 4 4 ...
##  $ Gender             : Factor w/ 2 levels "0","1": 1 1 1 1 2 1 2 2 2 2 ...
##  $ Income             : num [1:334, 1] 0.259 0.259 -0.666 -0.666 1.184 ...
##   ..- attr(*, "scaled:center")= num 2.72
##   ..- attr(*, "scaled:scale")= num 1.08
##  $ SpeededIP          : num [1:334, 1] -1.844 -2.424 -1.974 -1.407 -0.603 ...
##   ..- attr(*, "scaled:center")= num 0.00825
##   ..- attr(*, "scaled:scale")= num 0.969
##  $ StrategicIP        : num [1:334, 1] -1.469 -1.599 0.751 -1.028 -0.522 ...
##   ..- attr(*, "scaled:center")= num -0.0448
##   ..- attr(*, "scaled:scale")= num 1.04
##  $ Percep             : num [1:334, 1] -0.0923 -0.6635 1.0988 -0.6263 0.2969 ...
##   ..- attr(*, "scaled:center")= num 0.0484
##   ..- attr(*, "scaled:scale")= num 1.02
##  $ Caution            : num [1:334, 1] 0.194 -0.592 -1.213 -0.057 1.131 ...
##   ..- attr(*, "scaled:center")= num 0.0285
##   ..- attr(*, "scaled:scale")= num 1.03
##  $ Discount           : num [1:334, 1] 0.492 1.608 1.569 1.196 -0.186 ...
##   ..- attr(*, "scaled:center")= num -0.0189
##   ..- attr(*, "scaled:scale")= num 1.03
##  $ conservatism_factor: num [1:334, 1] 0.359 1.793 -0.143 0.491 0.307 ...
##   ..- attr(*, "scaled:center")= num -0.0198
##   ..- attr(*, "scaled:scale")= num 0.937
##  $ dogmatism_factor   : num [1:334, 1] 2.025 0.584 -1.742 1.494 -0.424 ...
##   ..- attr(*, "scaled:center")= num -0.0148
##   ..- attr(*, "scaled:scale")= num 0.927
##  $ religiosity_factor : num [1:334, 1] -0.72 1.81 -0.582 0.061 1.824 ...
##   ..- attr(*, "scaled:center")= num 0.00144
##   ..- attr(*, "scaled:scale")= num 0.986

Descriptive analyses

I recently learned about the modelsummary package, and it looks very very nice. So let’s try to create a descriptive table using the package.

df %>% 
  select(Age, Income, SpeededIP, StrategicIP, Percep, Caution, Discount, conservatism_factor, dogmatism_factor, religiosity_factor) %>% 
  datasummary_skim(.)
Unique Missing Pct. Mean SD Min Median Max
Age 42 1 −0.0 1.0 −1.8 −0.1 3.1
Income 6 2 −0.0 1.0 −1.6 0.3 2.1
SpeededIP 321 0 0.0 1.0 −2.4 −0.0 2.7
StrategicIP 325 0 −0.0 1.0 −2.4 −0.1 2.6
Percep 315 0 −0.0 1.0 −2.6 −0.0 3.1
Caution 316 0 −0.0 1.0 −2.4 0.0 2.6
Discount 319 0 0.0 1.0 −2.4 0.1 1.9
conservatism_factor 317 5 −0.0 1.0 −2.0 −0.0 2.6
dogmatism_factor 317 5 0.0 1.0 −2.1 −0.1 4.6
religiosity_factor 317 5 −0.0 1.0 −0.9 −0.6 2.8

Ok, this is actually super super cool! We can also summarize the categorical variables. By default datasummary_skim will only summarize numerical variables (above I selected a few specific variables of interest on purpose). We can also tell it to just summarize categorical variables:

datasummary_skim(df, type = "categorical")
## Warning: These variables were omitted because they include more than 50 levels: ID.
N %
Edu 1 2 0.6
2 56 16.8
3 69 20.7
4 48 14.4
5 125 37.4
6 24 7.2
7 8 2.4
Gender 0 163 48.8
1 167 50.0

Again, this is so so cool! I am a big fan of the package already! Let’s see if we can find some more use for it a bit later.

Ready for the Path Models?

Using lavaan for frequentist path models

Path models can also be seen as structural equation models with only observed and no latent variables. They do not model measurement error in a way as latent variable models do, but are thus usually also a bit less complex. We will first run a simple path model in lavaan, and then try to replicate the findings in brms and blavaan. Also, for now we will not control for demographic variables.

Let’s specify our lavaan model.

model1 <- 
' 
# direct associations (outcome ~ direct predictor)
    conservatism_factor ~ c1*Age
    dogmatism_factor ~ c2*Age
    religiosity_factor ~ c3*Age
  
# indirect associations (indirect predictor ~ direct predictor)
    SpeededIP ~ a1*Age
    StrategicIP ~ a2*Age
    Percep ~ a3*Age
    Caution ~ a4*Age
    Discount ~ a5*Age
    
# indirect associations (outcome ~ indirect predictor)
    conservatism_factor ~ b11*SpeededIP + b12*StrategicIP + b13*Percep + b14*Caution + b15*Discount
    dogmatism_factor ~ b21*SpeededIP + b22*StrategicIP + b23*Percep + b24*Caution + b25*Discount
    religiosity_factor ~ b31*SpeededIP + b32*StrategicIP + b33*Percep + b34*Caution + b35*Discount
    
# residual covariances

# mediator residual covariance
    SpeededIP ~~ StrategicIP + Percep + Caution + Discount
    StrategicIP ~~ Percep + Caution + Discount
    Percep ~~ Caution + Discount
    Caution ~~ Discount

# dependent residual covariance
    conservatism_factor ~~ dogmatism_factor + religiosity_factor
    dogmatism_factor ~~ religiosity_factor
    
# effect decomposition
# x = age, m1 = SpeededIP, m2 = StrategicIP, m3 = Percep, m4 = Caution, m5 = Discount
  
# y1 = conservatism_factor;  
    ind_x_m1_y1 := a1*b11
    ind_x_m2_y1 := a2*b12
    ind_x_m3_y1 := a3*b13
    ind_x_m4_y1 := a4*b14
    ind_x_m5_y1 := a4*b15
    ind_x_y1 := ind_x_m1_y1 + ind_x_m2_y1 + ind_x_m3_y1 + ind_x_m4_y1 + ind_x_m5_y1
    tot_x_y1 := ind_x_y1 + c1
    
# y2 = dogmatism_factor
    ind_x_m1_y2 := a1*b21
    ind_x_m2_y2 := a2*b22
    ind_x_m3_y2 := a3*b23
    ind_x_m4_y2 := a4*b24
    ind_x_m5_y2 := a4*b25
    ind_x_y2 := ind_x_m1_y2 + ind_x_m2_y2 + ind_x_m3_y2 + ind_x_m4_y2 + ind_x_m5_y2
    tot_x_y2 := ind_x_y2 + c2
  
# y3 = religiosity_factor
    ind_x_m1_y3 := a1*b31
    ind_x_m2_y3 := a2*b32
    ind_x_m3_y3 := a3*b33
    ind_x_m4_y3 := a4*b34
    ind_x_m5_y3 := a4*b35
    ind_x_y3 := ind_x_m1_y3 + ind_x_m2_y3 + ind_x_m3_y3 + ind_x_m4_y3 + ind_x_m5_y3
    tot_x_y3 := ind_x_y3 + c3
'

Let’s fit and run this path model in lavaan.

fit1 <- sem(model = model1, data  = df,
            std.ov = TRUE, mimic = "Mplus", 
            estimator = "ML", missing = "fiml") 
## Warning in lav_data_full(data = data, group = group, cluster = cluster, : lavaan WARNING: 4 cases were deleted due to missing values in 
##        exogenous variable(s), while fixed.x = TRUE.
parest1 <- parameterestimates(fit1)
parest1 %>% 
  slice(55:75) %>% 
  select(label, est, se, pvalue, ci.lower, ci.upper) %>% 
  tt(.)
tinytable_b7by6g5zkz7c537w6gxr
label est se pvalue ci.lower ci.upper
ind_x_m1_y1 -0.01473 0.0169 3.85e-01 -0.04794 0.01847
ind_x_m2_y1 0.02956 0.0137 3.15e-02 0.00262 0.05650
ind_x_m3_y1 0.00765 0.0119 5.21e-01 -0.01572 0.03103
ind_x_m4_y1 0.04944 0.0178 5.36e-03 0.01464 0.08423
ind_x_m5_y1 0.03731 0.0166 2.46e-02 0.00478 0.06985
ind_x_y1 0.10923 0.0369 3.10e-03 0.03685 0.18161
tot_x_y1 0.27269 0.0585 3.16e-06 0.15801 0.38738
ind_x_m1_y2 -0.04499 0.0197 2.21e-02 -0.08351 -0.00647
ind_x_m2_y2 0.01060 0.0100 2.91e-01 -0.00907 0.03027
ind_x_m3_y2 -0.00642 0.0126 6.12e-01 -0.03122 0.01837
ind_x_m4_y2 0.01187 0.0161 4.61e-01 -0.01970 0.04343
ind_x_m5_y2 0.01522 0.0162 3.48e-01 -0.01654 0.04698
ind_x_y2 -0.01373 0.0347 6.92e-01 -0.08168 0.05423
tot_x_y2 -0.07333 0.0595 2.18e-01 -0.19000 0.04333
ind_x_m1_y3 -0.00921 0.0177 6.03e-01 -0.04395 0.02552
ind_x_m2_y3 0.02789 0.0136 4.04e-02 0.00122 0.05456
ind_x_m3_y3 0.01286 0.0128 3.16e-01 -0.01228 0.03800
ind_x_m4_y3 0.02405 0.0165 1.44e-01 -0.00819 0.05630
ind_x_m5_y3 0.00584 0.0158 7.12e-01 -0.02515 0.03683
ind_x_y3 0.06143 0.0350 7.94e-02 -0.00720 0.13006
tot_x_y3 0.13190 0.0593 2.62e-02 0.01562 0.24817

Let’s remind ourselves of what is what:

x = age, m1 = SpeededIP, m2 = StrategicIP, m3 = Percep, m4 = Caution, m5 = Discount, y1 = conservatism, y2 = dogmatism, y3 = religiosity.

The model indicates

  • Significant, but very small positive indirect associations between Age and Conservatism via the cognitive factors StrategicIP, Caution, and Discount.

  • No significant indirect associations between Age, Dogmatism and Religiosity via cognitive factors.

We can also check a bit more information using summary.

summary(fit1)
## lavaan 0.6.17 ended normally after 18 iterations
## 
##   Estimator                                         ML
##   Optimization method                           NLMINB
##   Number of model parameters                        52
## 
##                                                   Used       Total
##   Number of observations                           330         334
##   Number of missing patterns                         2            
## 
## Model Test User Model:
##                                                       
##   Test statistic                                 0.000
##   Degrees of freedom                                 0
## 
## Parameter Estimates:
## 
##   Standard errors                             Standard
##   Information                                 Observed
##   Observed information based on                Hessian
## 
## Regressions:
##                         Estimate  Std.Err  z-value  P(>|z|)
##   conservatism_factor ~                                    
##     Age       (c1)         0.163    0.059    2.786    0.005
##   dogmatism_factor ~                                       
##     Age       (c2)        -0.060    0.063   -0.953    0.341
##   religiosity_factor ~                                     
##     Age       (c3)         0.070    0.062    1.138    0.255
##   SpeededIP ~                                              
##     Age       (a1)         0.287    0.053    5.436    0.000
##   StrategicIP ~                                            
##     Age       (a2)        -0.151    0.054   -2.779    0.005
##   Percep ~                                                 
##     Age       (a3)         0.216    0.054    4.024    0.000
##   Caution ~                                                
##     Age       (a4)         0.274    0.053    5.169    0.000
##   Discount ~                                               
##     Age       (a5)        -0.072    0.055   -1.305    0.192
##   conservatism_factor ~                                    
##     SpeeddIP (b11)        -0.051    0.058   -0.881    0.378
##     StrtgcIP (b12)        -0.195    0.058   -3.394    0.001
##     Percep   (b13)         0.035    0.054    0.650    0.516
##     Caution  (b14)         0.181    0.055    3.305    0.001
##     Discount (b15)         0.136    0.055    2.496    0.013
##   dogmatism_factor ~                                       
##     SpeeddIP (b21)        -0.157    0.062   -2.524    0.012
##     StrtgcIP (b22)        -0.070    0.061   -1.142    0.253
##     Percep   (b23)        -0.030    0.058   -0.512    0.609
##     Caution  (b24)         0.043    0.058    0.744    0.457
##     Discount (b25)         0.056    0.058    0.955    0.339
##   religiosity_factor ~                                     
##     SpeeddIP (b31)        -0.032    0.062   -0.522    0.602
##     StrtgcIP (b32)        -0.184    0.061   -3.035    0.002
##     Percep   (b33)         0.059    0.057    1.035    0.300
##     Caution  (b34)         0.088    0.058    1.525    0.127
##     Discount (b35)         0.021    0.058    0.370    0.711
## 
## Covariances:
##                          Estimate  Std.Err  z-value  P(>|z|)
##  .SpeededIP ~~                                              
##    .StrategicIP             0.340    0.055    6.163    0.000
##    .Percep                 -0.177    0.052   -3.393    0.001
##    .Caution                 0.109    0.051    2.146    0.032
##    .Discount               -0.167    0.053   -3.129    0.002
##  .StrategicIP ~~                                            
##    .Percep                 -0.089    0.053   -1.668    0.095
##    .Caution                 0.051    0.052    0.985    0.325
##    .Discount               -0.292    0.056   -5.180    0.000
##  .Percep ~~                                                 
##    .Caution                 0.080    0.052    1.542    0.123
##    .Discount                0.035    0.053    0.652    0.514
##  .Caution ~~                                                
##    .Discount               -0.032    0.053   -0.608    0.543
##  .conservatism_factor ~~                                    
##    .dogmatism_fctr          0.235    0.052    4.540    0.000
##    .religisty_fctr          0.270    0.052    5.198    0.000
##  .dogmatism_factor ~~                                       
##    .religisty_fctr          0.074    0.053    1.391    0.164
## 
## Intercepts:
##                    Estimate  Std.Err  z-value  P(>|z|)
##    .consrvtsm_fctr   -0.006    0.052   -0.111    0.912
##    .dogmatism_fctr   -0.000    0.055   -0.001    0.999
##    .religisty_fctr   -0.002    0.054   -0.038    0.970
##    .SpeededIP         0.000    0.053    0.000    1.000
##    .StrategicIP      -0.000    0.054   -0.000    1.000
##    .Percep            0.000    0.054    0.000    1.000
##    .Caution           0.000    0.053    0.000    1.000
##    .Discount         -0.000    0.055   -0.000    1.000
## 
## Variances:
##                    Estimate  Std.Err  z-value  P(>|z|)
##    .consrvtsm_fctr    0.833    0.067   12.530    0.000
##    .dogmatism_fctr    0.947    0.076   12.530    0.000
##    .religisty_fctr    0.928    0.074   12.530    0.000
##    .SpeededIP         0.915    0.071   12.845    0.000
##    .StrategicIP       0.974    0.076   12.845    0.000
##    .Percep            0.950    0.074   12.845    0.000
##    .Caution           0.922    0.072   12.845    0.000
##    .Discount          0.992    0.077   12.845    0.000
## 
## Defined Parameters:
##                    Estimate  Std.Err  z-value  P(>|z|)
##     ind_x_m1_y1      -0.015    0.017   -0.870    0.385
##     ind_x_m2_y1       0.030    0.014    2.150    0.032
##     ind_x_m3_y1       0.008    0.012    0.642    0.521
##     ind_x_m4_y1       0.049    0.018    2.785    0.005
##     ind_x_m5_y1       0.037    0.017    2.248    0.025
##     ind_x_y1          0.109    0.037    2.958    0.003
##     tot_x_y1          0.273    0.059    4.660    0.000
##     ind_x_m1_y2      -0.045    0.020   -2.289    0.022
##     ind_x_m2_y2       0.011    0.010    1.057    0.291
##     ind_x_m3_y2      -0.006    0.013   -0.508    0.612
##     ind_x_m4_y2       0.012    0.016    0.737    0.461
##     ind_x_m5_y2       0.015    0.016    0.939    0.348
##     ind_x_y2         -0.014    0.035   -0.396    0.692
##     tot_x_y2         -0.073    0.060   -1.232    0.218
##     ind_x_m1_y3      -0.009    0.018   -0.520    0.603
##     ind_x_m2_y3       0.028    0.014    2.050    0.040
##     ind_x_m3_y3       0.013    0.013    1.003    0.316
##     ind_x_m4_y3       0.024    0.016    1.462    0.144
##     ind_x_m5_y3       0.006    0.016    0.369    0.712
##     ind_x_y3          0.061    0.035    1.754    0.079
##     tot_x_y3          0.132    0.059    2.223    0.026

Also note: Our model perfectly fits the data, but this is because the model is just-identified (i.e., there are 0 degrees of freedom): Every pair of variable is connected - our model is just a different way to describe the full data. This does not mean that the model syntax is incorrect - this is often the case in models with just observed variables and indirect associations.

There is much more in this output that can be of interest. But as we want to focus on the Bayesian analyses here, let’s move on for now.

blavaan: The Bayesian counterpart of lavaan

Using very similar syntax as lavaan, blavaan allows us to fit Bayesian structural equation models (or path models as here).

Let’s check the default priors of blavaan.

dpriors()
##                nu             alpha            lambda              beta             theta 
##    "normal(0,32)"    "normal(0,10)"    "normal(0,10)"    "normal(0,10)" "gamma(1,.5)[sd]" 
##               psi               rho             ibpsi               tau 
## "gamma(1,.5)[sd]"       "beta(1,1)" "wishart(3,iden)"   "normal(0,1.5)"

Contrary to brms, in many cases we can use these default priors. We can modify the prior distributions, and in particular, we want to specify the prior of the regressions \(\beta\) as \(N(0,1)\), as we will later use this prior in brms. It should not matter to much, especilly for the other parameters, but this is also good to try out on how to specify priors.

mydp <- dpriors(beta="normal(0,1)")
mydp
##                nu             alpha            lambda              beta             theta 
##    "normal(0,32)"    "normal(0,10)"    "normal(0,10)"     "normal(0,1)" "gamma(1,.5)[sd]" 
##               psi               rho             ibpsi               tau 
## "gamma(1,.5)[sd]"       "beta(1,1)" "wishart(3,iden)"   "normal(0,1.5)"

Let’s try to fit the same model as previously.

fit2 <- bsem(model1, dp = mydp, data = df)
## Warning in lav_data_full(data = data, group = group, cluster = cluster, : lavaan WARNING: 4 cases were deleted due to missing values in 
##        exogenous variable(s), while fixed.x = TRUE.
## Computing post-estimation metrics (including lvs if requested)...
## Warning in tmpmat[lower.tri(tmpmat)] <- lavpartable$est[covpars]: number of items to
## replace is not a multiple of replacement length
summary(fit2)
## blavaan 0.5.3 ended normally after 1000 iterations
## 
##   Estimator                                      BAYES
##   Optimization method                             MCMC
##   Number of model parameters                        52
## 
##                                                   Used       Total
##   Number of observations                           330         334
##   Number of missing patterns                         2            
## 
##   Statistic                                 MargLogLik         PPP
##   Value                                      -3662.530       0.518
## 
## Parameter Estimates:
## 
## 
## Regressions:
##                         Estimate  Post.SD pi.lower pi.upper     Rhat    Prior       
##   conservatism_factor ~                                                             
##     Age       (c1)         0.162    0.059    0.048    0.279    1.000     normal(0,1)
##   dogmatism_factor ~                                                                
##     Age       (c2)        -0.061    0.065   -0.189    0.065    0.999     normal(0,1)
##   religiosity_factor ~                                                              
##     Age       (c3)         0.069    0.063   -0.054    0.191    1.000     normal(0,1)
##   SpeededIP ~                                                                       
##     Age       (a1)         0.284    0.051    0.183    0.389    0.999     normal(0,1)
##   StrategicIP ~                                                                     
##     Age       (a2)        -0.151    0.055   -0.261   -0.045    1.000     normal(0,1)
##   Percep ~                                                                          
##     Age       (a3)         0.211    0.052    0.108    0.315    1.000     normal(0,1)
##   Caution ~                                                                         
##     Age       (a4)         0.272    0.054    0.168    0.375    1.000     normal(0,1)
##   Discount ~                                                                        
##     Age       (a5)        -0.071    0.056   -0.177    0.039    0.999     normal(0,1)
##   conservatism_factor ~                                                             
##     SpeeddIP (b11)        -0.052    0.059   -0.167    0.065    1.000     normal(0,1)
##     StrtgcIP (b12)        -0.197    0.059   -0.313   -0.083    1.001     normal(0,1)
##     Percep   (b13)         0.036    0.056   -0.073    0.149    1.000     normal(0,1)
##     Caution  (b14)         0.182    0.056    0.075    0.289    1.000     normal(0,1)
##     Discount (b15)         0.135    0.054    0.030    0.246    1.000     normal(0,1)
##   dogmatism_factor ~                                                                
##     SpeeddIP (b21)        -0.158    0.066   -0.289   -0.028    1.000     normal(0,1)
##     StrtgcIP (b22)        -0.072    0.063   -0.193    0.050    1.000     normal(0,1)
##     Percep   (b23)        -0.030    0.060   -0.148    0.089    1.000     normal(0,1)
##     Caution  (b24)         0.046    0.057   -0.067    0.153    1.000     normal(0,1)
##     Discount (b25)         0.054    0.058   -0.057    0.167    1.000     normal(0,1)
##   religiosity_factor ~                                                              
##     SpeeddIP (b31)        -0.031    0.062   -0.151    0.094    1.000     normal(0,1)
##     StrtgcIP (b32)        -0.184    0.063   -0.308   -0.061    0.999     normal(0,1)
##     Percep   (b33)         0.059    0.060   -0.061    0.176    1.000     normal(0,1)
##     Caution  (b34)         0.087    0.059   -0.025    0.204    0.999     normal(0,1)
##     Discount (b35)         0.021    0.058   -0.095    0.131    1.000     normal(0,1)
## 
## Covariances:
##                          Estimate  Post.SD pi.lower pi.upper     Rhat    Prior       
##  .SpeededIP ~~                                                                       
##    .StrategicIP             0.341    0.056    0.238    0.456    1.000     lkj_corr(1)
##    .Percep                 -0.176    0.051   -0.279   -0.081    0.999     lkj_corr(1)
##    .Caution                 0.110    0.052    0.011    0.215    0.999     lkj_corr(1)
##    .Discount               -0.167    0.054   -0.276   -0.063    1.000     lkj_corr(1)
##  .StrategicIP ~~                                                                     
##    .Percep                 -0.087    0.054   -0.196    0.017    1.000     lkj_corr(1)
##    .Caution                 0.050    0.054   -0.056    0.156    1.000     lkj_corr(1)
##    .Discount               -0.296    0.057   -0.416   -0.187    1.000     lkj_corr(1)
##  .Percep ~~                                                                          
##    .Caution                 0.079    0.053   -0.023    0.182    0.999     lkj_corr(1)
##    .Discount                0.035    0.055   -0.074    0.143    0.999     lkj_corr(1)
##  .Caution ~~                                                                         
##    .Discount               -0.033    0.053   -0.135    0.069    0.999     lkj_corr(1)
##  .conservatism_factor ~~                                                             
##    .dogmatism_fctr          0.242    0.056    0.132    0.349    1.001     lkj_corr(1)
##    .religisty_fctr          0.275    0.054    0.177    0.385    1.002     lkj_corr(1)
##  .dogmatism_factor ~~                                                                
##    .religisty_fctr          0.075    0.056   -0.035    0.187    1.001     lkj_corr(1)
## 
## Intercepts:
##                    Estimate  Post.SD pi.lower pi.upper     Rhat    Prior       
##    .consrvtsm_fctr   -0.003    0.054   -0.107    0.103    0.999    normal(0,10)
##    .dogmatism_fctr    0.004    0.057   -0.105    0.116    0.999    normal(0,10)
##    .religisty_fctr   -0.011    0.056   -0.122    0.094    1.000    normal(0,10)
##    .SpeededIP         0.003    0.053   -0.099    0.109    1.000    normal(0,10)
##    .StrategicIP       0.013    0.055   -0.096    0.119    1.000    normal(0,10)
##    .Percep           -0.023    0.052   -0.124    0.083    1.000    normal(0,10)
##    .Caution           0.001    0.053   -0.103    0.103    1.000    normal(0,10)
##    .Discount         -0.000    0.055   -0.111    0.109    1.000    normal(0,10)
## 
## Variances:
##                    Estimate  Post.SD pi.lower pi.upper     Rhat    Prior       
##    .consrvtsm_fctr    0.867    0.071    0.737    1.020    1.000 gamma(1,.5)[sd]
##    .dogmatism_fctr    0.987    0.080    0.841    1.155    1.001 gamma(1,.5)[sd]
##    .religisty_fctr    0.943    0.074    0.807    1.098    1.000 gamma(1,.5)[sd]
##    .SpeededIP         0.926    0.073    0.798    1.080    0.999 gamma(1,.5)[sd]
##    .StrategicIP       0.995    0.080    0.853    1.163    1.000 gamma(1,.5)[sd]
##    .Percep            0.941    0.075    0.802    1.097    0.999 gamma(1,.5)[sd]
##    .Caution           0.953    0.074    0.821    1.114    0.999 gamma(1,.5)[sd]
##    .Discount          1.026    0.082    0.875    1.198    1.000 gamma(1,.5)[sd]
## 
## Defined Parameters:
##                    Estimate  Post.SD pi.lower pi.upper     Rhat    Prior       
##     ind_x_m1_y1      -0.015    0.017   -0.049    0.019                         
##     ind_x_m2_y1       0.030    0.014    0.001    0.058                         
##     ind_x_m3_y1       0.008    0.012   -0.017    0.032                         
##     ind_x_m4_y1       0.049    0.018    0.014    0.085                         
##     ind_x_m5_y1       0.037    0.017    0.004    0.070                         
##     ind_x_y1          0.109    0.038    0.034    0.183                         
##     tot_x_y1          0.271    0.059    0.155    0.387                         
##     ind_x_m1_y2      -0.045    0.021   -0.085   -0.004                         
##     ind_x_m2_y2       0.011    0.011   -0.011    0.032                         
##     ind_x_m3_y2      -0.006    0.013   -0.032    0.019                         
##     ind_x_m4_y2       0.012    0.016   -0.019    0.044                         
##     ind_x_m5_y2       0.015    0.017   -0.018    0.047                         
##     ind_x_y2         -0.013    0.036   -0.084    0.058                         
##     tot_x_y2         -0.074    0.062   -0.195    0.047                         
##     ind_x_m1_y3      -0.009    0.018   -0.043    0.026                         
##     ind_x_m2_y3       0.028    0.014   -0.001    0.056                         
##     ind_x_m3_y3       0.012    0.013   -0.014    0.038                         
##     ind_x_m4_y3       0.024    0.017   -0.010    0.058                         
##     ind_x_m5_y3       0.006    0.016   -0.026    0.037                         
##     ind_x_y3          0.061    0.036   -0.009    0.131                         
##     tot_x_y3          0.130    0.060    0.012    0.248

The results are very similar.

Path models in brms

We can do something very similar also using brms! brms is a very flexible and powerful packages that is very flexible with modeling approaches. I think blavaan is the more straight-forward choice as soon as you want to specifiy latent variable models, but for models with just observed variables brms may even be the better pick, even in the case of path models. Let’s try it out and compare the results.

Selecting priors

I already scaled all variables of interest. This will make it a bit simpler to select priors. I will use weakly informative priors for slope and intercept parameters again (see https://github.com/stan-dev/stan/wiki/Prior-Choice-Recommendations). I do this merely to keep things simple and straightforward here - usually a few more thoughts should be placed in specifying your prior. The selected prior is \(N(0,1)\). Let’s plot to prior to see what kind of an effect it may have on our parameters.


colors <- ipsum_pal()(1)

prior(normal(0, 1)) %>% 
  parse_dist() %>% 
  
  ggplot(aes(xdist = .dist_obj, y = prior)) + 
  stat_halfeye(.width = 0.683, p_limits = c(.001, .999), 
               slab_fill = colors[1], slab_alpha = 0.8) +
  scale_y_discrete(NULL, breaks = NULL, expand = expansion(add = 0.1)) +
  labs(title = "Prior N(0,1)", x = "Parameter Space", y = "") +
  coord_cartesian(xlim = c(-4, 4)) +
  scale_x_continuous(breaks = seq(-4, 4, by = 1)) +
  theme_ipsum() +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank()) 

Setting up the model

In brms we have to specify our model a bit different than previously. However, in principle, we are doing nothing else than fitting a multivariate linear regression once again. The way we set up this model should thus remind both of what we specified in the previous blog post, and of the lavaan syntax from above.


model2 <- 
  # direct and indirect paths to ideological factors
  bf(conservatism_factor ~ Age + SpeededIP + StrategicIP + Percep + Caution + Discount) + 
  bf(dogmatism_factor ~ Age + SpeededIP + StrategicIP + Percep + Caution + Discount) + 
  bf(religiosity_factor ~ Age + SpeededIP + StrategicIP + Percep + Caution + Discount) +
  # paths from age to indirect predictors (cognitive factors)
  bf(SpeededIP ~ Age) + 
  bf(StrategicIP ~ Age) + 
  bf(Percep ~ Age) + 
  bf(Caution ~ Age) + 
  bf(Discount ~ Age)

Let’s check how our priors are set by default in brms.

get_prior(model2 + set_rescor(FALSE), data = df)
## Warning: Rows containing NAs were excluded from the model.
##                    prior     class        coef group               resp dpar nlpar lb ub
##                   (flat)         b                                                      
##                   (flat) Intercept                                                      
##                   (flat)         b                              Caution                 
##                   (flat)         b         Age                  Caution                 
##     student_t(3, 0, 2.5) Intercept                              Caution                 
##     student_t(3, 0, 2.5)     sigma                              Caution             0   
##                   (flat)         b                   conservatismfactor                 
##                   (flat)         b         Age       conservatismfactor                 
##                   (flat)         b     Caution       conservatismfactor                 
##                   (flat)         b    Discount       conservatismfactor                 
##                   (flat)         b      Percep       conservatismfactor                 
##                   (flat)         b   SpeededIP       conservatismfactor                 
##                   (flat)         b StrategicIP       conservatismfactor                 
##     student_t(3, 0, 2.5) Intercept                   conservatismfactor                 
##     student_t(3, 0, 2.5)     sigma                   conservatismfactor             0   
##                   (flat)         b                             Discount                 
##                   (flat)         b         Age                 Discount                 
##   student_t(3, 0.1, 2.5) Intercept                             Discount                 
##     student_t(3, 0, 2.5)     sigma                             Discount             0   
##                   (flat)         b                      dogmatismfactor                 
##                   (flat)         b         Age          dogmatismfactor                 
##                   (flat)         b     Caution          dogmatismfactor                 
##                   (flat)         b    Discount          dogmatismfactor                 
##                   (flat)         b      Percep          dogmatismfactor                 
##                   (flat)         b   SpeededIP          dogmatismfactor                 
##                   (flat)         b StrategicIP          dogmatismfactor                 
##     student_t(3, 0, 2.5) Intercept                      dogmatismfactor                 
##     student_t(3, 0, 2.5)     sigma                      dogmatismfactor             0   
##                   (flat)         b                               Percep                 
##                   (flat)         b         Age                   Percep                 
##  student_t(3, -0.1, 2.5) Intercept                               Percep                 
##     student_t(3, 0, 2.5)     sigma                               Percep             0   
##                   (flat)         b                    religiosityfactor                 
##                   (flat)         b         Age        religiosityfactor                 
##                   (flat)         b     Caution        religiosityfactor                 
##                   (flat)         b    Discount        religiosityfactor                 
##                   (flat)         b      Percep        religiosityfactor                 
##                   (flat)         b   SpeededIP        religiosityfactor                 
##                   (flat)         b StrategicIP        religiosityfactor                 
##  student_t(3, -0.6, 2.5) Intercept                    religiosityfactor                 
##     student_t(3, 0, 2.5)     sigma                    religiosityfactor             0   
##                   (flat)         b                            SpeededIP                 
##                   (flat)         b         Age                SpeededIP                 
##     student_t(3, 0, 2.5) Intercept                            SpeededIP                 
##     student_t(3, 0, 2.5)     sigma                            SpeededIP             0   
##                   (flat)         b                          StrategicIP                 
##                   (flat)         b         Age              StrategicIP                 
##     student_t(3, 0, 2.5) Intercept                          StrategicIP                 
##     student_t(3, 0, 2.5)     sigma                          StrategicIP             0   
##        source
##       default
##       default
##       default
##  (vectorized)
##       default
##       default
##       default
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##       default
##       default
##       default
##  (vectorized)
##       default
##       default
##       default
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##       default
##       default
##       default
##  (vectorized)
##       default
##       default
##       default
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##  (vectorized)
##       default
##       default
##       default
##  (vectorized)
##       default
##       default
##       default
##  (vectorized)
##       default
##       default

By default brms sets non-informative flat priors for our slope parameters. We thus have to change this, and can do so directly when fitting the model.

Fitting and summarising the first model

m3 <- brm(model2 + set_rescor(FALSE), 
          data = df, 
          prior = c(prior(normal(0, 1), class = "Intercept"), 
                    prior(normal(0, 1), class = "b")),
          seed = bayes_seed,
          file = "02_fits/fit_m3")

We get a warning message:

Warning: Rows containing NAs were excluded from the model.

We are not happy with this, but for now we will deal with it later.

Check the results. As usual, we can also use summary(m1.1) or print(m1.1) to do so.

summary(m3, prob = 0.95)  # prob = 0.95 is the default
##  Family: MV(gaussian, gaussian, gaussian, gaussian, gaussian, gaussian, gaussian, gaussian) 
##   Links: mu = identity; sigma = identity
##          mu = identity; sigma = identity
##          mu = identity; sigma = identity
##          mu = identity; sigma = identity
##          mu = identity; sigma = identity
##          mu = identity; sigma = identity
##          mu = identity; sigma = identity
##          mu = identity; sigma = identity 
## Formula: conservatism_factor ~ Age + SpeededIP + StrategicIP + Percep + Caution + Discount 
##          dogmatism_factor ~ Age + SpeededIP + StrategicIP + Percep + Caution + Discount 
##          religiosity_factor ~ Age + SpeededIP + StrategicIP + Percep + Caution + Discount 
##          SpeededIP ~ Age 
##          StrategicIP ~ Age 
##          Percep ~ Age 
##          Caution ~ Age 
##          Discount ~ Age 
##    Data: df (Number of observations: 314) 
##   Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
##          total post-warmup draws = 4000
## 
## Population-Level Effects: 
##                                Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
## conservatismfactor_Intercept      -0.00      0.05    -0.11     0.11 1.00     6337
## dogmatismfactor_Intercept          0.00      0.06    -0.11     0.12 1.00     6525
## religiosityfactor_Intercept       -0.01      0.05    -0.11     0.09 1.00     6099
## SpeededIP_Intercept                0.01      0.05    -0.09     0.12 1.00     6149
## StrategicIP_Intercept              0.01      0.06    -0.10     0.13 1.00     5718
## Percep_Intercept                  -0.03      0.05    -0.13     0.08 1.00     6003
## Caution_Intercept                  0.03      0.05    -0.08     0.14 1.00     5574
## Discount_Intercept                 0.01      0.06    -0.10     0.12 1.00     5602
## conservatismfactor_Age             0.16      0.06     0.05     0.29 1.00     4368
## conservatismfactor_SpeededIP      -0.05      0.06    -0.17     0.06 1.00     4502
## conservatismfactor_StrategicIP    -0.20      0.06    -0.32    -0.08 1.00     5018
## conservatismfactor_Percep          0.04      0.06    -0.08     0.15 1.00     5278
## conservatismfactor_Caution         0.18      0.06     0.07     0.29 1.00     5297
## conservatismfactor_Discount        0.14      0.06     0.02     0.25 1.00     5710
## dogmatismfactor_Age               -0.06      0.06    -0.18     0.06 1.00     3949
## dogmatismfactor_SpeededIP         -0.16      0.06    -0.28    -0.03 1.00     4213
## dogmatismfactor_StrategicIP       -0.07      0.06    -0.19     0.05 1.00     4326
## dogmatismfactor_Percep            -0.03      0.06    -0.15     0.09 1.00     4723
## dogmatismfactor_Caution            0.04      0.06    -0.07     0.16 1.00     5841
## dogmatismfactor_Discount           0.05      0.06    -0.07     0.18 1.00     5344
## religiosityfactor_Age              0.07      0.06    -0.05     0.19 1.00     4160
## religiosityfactor_SpeededIP       -0.03      0.06    -0.15     0.09 1.00     3946
## religiosityfactor_StrategicIP     -0.18      0.06    -0.31    -0.06 1.00     4377
## religiosityfactor_Percep           0.06      0.06    -0.06     0.18 1.00     4796
## religiosityfactor_Caution          0.08      0.06    -0.03     0.20 1.00     5670
## religiosityfactor_Discount         0.02      0.06    -0.10     0.14 1.00     4846
## SpeededIP_Age                      0.28      0.05     0.18     0.39 1.00     5559
## StrategicIP_Age                   -0.14      0.06    -0.26    -0.03 1.00     7492
## Percep_Age                         0.21      0.05     0.11     0.32 1.00     5614
## Caution_Age                        0.30      0.06     0.18     0.41 1.00     6587
## Discount_Age                      -0.08      0.06    -0.19     0.03 1.00     6507
##                                Tail_ESS
## conservatismfactor_Intercept       2623
## dogmatismfactor_Intercept          2985
## religiosityfactor_Intercept        2855
## SpeededIP_Intercept                3067
## StrategicIP_Intercept              2994
## Percep_Intercept                   3038
## Caution_Intercept                  3158
## Discount_Intercept                 3195
## conservatismfactor_Age             3269
## conservatismfactor_SpeededIP       3487
## conservatismfactor_StrategicIP     3386
## conservatismfactor_Percep          3219
## conservatismfactor_Caution         3159
## conservatismfactor_Discount        3473
## dogmatismfactor_Age                3397
## dogmatismfactor_SpeededIP          3290
## dogmatismfactor_StrategicIP        3359
## dogmatismfactor_Percep             3091
## dogmatismfactor_Caution            3100
## dogmatismfactor_Discount           3030
## religiosityfactor_Age              2947
## religiosityfactor_SpeededIP        3058
## religiosityfactor_StrategicIP      3375
## religiosityfactor_Percep           3125
## religiosityfactor_Caution          2985
## religiosityfactor_Discount         3287
## SpeededIP_Age                      3144
## StrategicIP_Age                    2951
## Percep_Age                         3037
## Caution_Age                        2954
## Discount_Age                       3069
## 
## Family Specific Parameters: 
##                          Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
## sigma_conservatismfactor     0.93      0.04     0.86     1.00 1.00     4946     3050
## sigma_dogmatismfactor        0.99      0.04     0.91     1.07 1.00     5632     2831
## sigma_religiosityfactor      0.97      0.04     0.90     1.05 1.00     6108     2799
## sigma_SpeededIP              0.97      0.04     0.89     1.04 1.00     7028     3235
## sigma_StrategicIP            0.99      0.04     0.92     1.08 1.00     6009     3152
## sigma_Percep                 0.96      0.04     0.89     1.04 1.00     5479     3026
## sigma_Caution                0.97      0.04     0.89     1.05 1.00     5749     3318
## sigma_Discount               1.00      0.04     0.93     1.09 1.00     6932     2907
## 
## Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
## and Tail_ESS are effective sample size measures, and Rhat is the potential
## scale reduction factor on split chains (at convergence, Rhat = 1).

This time, we need some trickery to compute the indirect effects. Let’s remind ourselves how we did this previously - what I write out below, is what we specified in the lavaan syntax.

The indirect effect in general is calculated by the multiplication of the path from the direct predictor to the indirect predictor (a)

  • In this case, this is the path from Age to SpeededIP, StrategicIP, Percep, Caution, Discount respectively

with the path from the indirect predictor to the outcome (b)

  • In this case, this is the path from SpeededIP, StrategicIP, Percep, Caution, Discount to conservatism_factor, dogmatism_factor, or religiosity_factor

And the direct paths (c) are the paths from the direct predictor to the outcome

  • In this case, this is the path from Age to conservatism_factor, dogmatism_factor, or religiosity_factor

The overall indirect effect for one outcome is nothing but the sum of all indirect effects to that outcome.

The total effects are nothing but the sum of the indirect and direct effects.

In brms we get the posterior of all of those effects. We can use the posterior draws to calculate these effects - though some manual work is needed. Let’s first extract the draws.

m3_draws <- as_draws_df(m3)
head(m3_draws)
## # A draws_df: 6 iterations, 1 chains, and 41 variables
##   b_conservatismfactor_Intercept b_dogmatismfactor_Intercept
## 1                          0.034                      0.0476
## 2                         -0.073                      0.0472
## 3                          0.067                     -0.0393
## 4                         -0.057                      0.0448
## 5                          0.011                     -0.0044
## 6                         -0.015                      0.0076
##   b_religiosityfactor_Intercept b_SpeededIP_Intercept b_StrategicIP_Intercept
## 1                         0.089                0.0604                  0.0062
## 2                        -0.102                0.0793                 -0.0450
## 3                         0.061               -0.0360                  0.0651
## 4                        -0.091                0.0584                 -0.0283
## 5                         0.042                0.0086                  0.0081
## 6                        -0.064                0.0171                  0.0299
##   b_Percep_Intercept b_Caution_Intercept b_Discount_Intercept
## 1             -0.041              0.0104               -0.032
## 2             -0.090              0.0472               -0.047
## 3              0.026              0.0121                0.065
## 4             -0.091              0.0820               -0.047
## 5             -0.029              0.0161               -0.045
## 6             -0.024             -0.0012                0.057
## # ... with 33 more variables
## # ... hidden reserved variables {'.chain', '.iteration', '.draw'}

Now let’s try to calculate the indirect effects.

m3_draws <- m3_draws %>% 
  mutate(
    # Age -> Conservatism
    ind_Age_Spe_Con = b_SpeededIP_Age * b_conservatismfactor_SpeededIP,
    ind_Age_Str_Con = b_StrategicIP_Age * b_conservatismfactor_StrategicIP,
    ind_Age_Per_Con = b_Percep_Age * b_conservatismfactor_Percep,
    ind_Age_Cau_Con = b_Caution_Age * b_conservatismfactor_Caution,
    ind_Age_Dis_Con = b_Discount_Age * b_conservatismfactor_Discount,
    ind_Age_Con = ind_Age_Spe_Con + ind_Age_Str_Con + ind_Age_Per_Con + ind_Age_Cau_Con + ind_Age_Dis_Con,
    tot_Age_Con = ind_Age_Con + b_conservatismfactor_Age,
    
    # Age -> Dogmatism
    ind_Age_Spe_Dog = b_SpeededIP_Age * b_dogmatismfactor_SpeededIP,
    ind_Age_Str_Dog = b_StrategicIP_Age * b_dogmatismfactor_StrategicIP,
    ind_Age_Per_Dog = b_Percep_Age * b_dogmatismfactor_Percep,
    ind_Age_Cau_Dog = b_Caution_Age * b_dogmatismfactor_Caution,
    ind_Age_Dis_Dog = b_Discount_Age * b_dogmatismfactor_Discount,
    ind_Age_Dog = ind_Age_Spe_Dog + ind_Age_Str_Dog + ind_Age_Per_Dog + ind_Age_Cau_Dog + ind_Age_Dis_Dog,
    tot_Age_Dog = ind_Age_Dog + b_dogmatismfactor_Age,
    
    # Age -> Religiosity
    ind_Age_Spe_Rel = b_SpeededIP_Age * b_religiosityfactor_SpeededIP,
    ind_Age_Str_Rel = b_StrategicIP_Age * b_religiosityfactor_StrategicIP,
    ind_Age_Per_Rel = b_Percep_Age * b_religiosityfactor_Percep,
    ind_Age_Cau_Rel = b_Caution_Age * b_religiosityfactor_Caution,
    ind_Age_Dis_Rel = b_Discount_Age * b_religiosityfactor_Discount,
    ind_Age_Rel = ind_Age_Spe_Rel + ind_Age_Str_Rel + ind_Age_Per_Rel + ind_Age_Cau_Rel + ind_Age_Dis_Rel,
    tot_Age_Rel = ind_Age_Rel + b_religiosityfactor_Age
    )

Use the describe_posterior function to get some summary statistics of what we calculated.

m3_draws %>% 
  select(45:65) %>% 
  describe_posterior(., 
                     centrality = "median",
                     dispersion = FALSE,
                     ci = 0.95,
                     ci_method = "hdi",
                     rope_range = c(-0.02, 0.02),
                     rope_ci = 1) 
## Warning: Dropping 'draws_df' class as required metadata was removed.
## Summary of Posterior Distribution
## 
## Parameter       |    Median |         95% CI |     pd |          ROPE | % in ROPE
## ---------------------------------------------------------------------------------
## ind_Age_Spe_Con |     -0.01 | [-0.05,  0.02] | 80.58% | [-0.02, 0.02] |    61.68%
## ind_Age_Str_Con |      0.03 | [ 0.00,  0.06] | 99.48% | [-0.02, 0.02] |    30.18%
## ind_Age_Per_Con |  7.16e-03 | [-0.02,  0.03] | 73.52% | [-0.02, 0.02] |    83.62%
## ind_Age_Cau_Con |      0.05 | [ 0.02,  0.09] | 99.90% | [-0.02, 0.02] |     3.15%
## ind_Age_Dis_Con | -9.59e-03 | [-0.03,  0.00] | 91.27% | [-0.02, 0.02] |    83.33%
## ind_Age_Con     |      0.06 | [ 0.00,  0.13] | 97.20% | [-0.02, 0.02] |     8.88%
## tot_Age_Con     |      0.23 | [ 0.12,  0.34] |   100% | [-0.02, 0.02] |        0%
## ind_Age_Spe_Dog |     -0.04 | [-0.09, -0.01] | 99.25% | [-0.02, 0.02] |    10.08%
## ind_Age_Str_Dog |  9.17e-03 | [-0.01,  0.03] | 87.12% | [-0.02, 0.02] |    83.33%
## ind_Age_Per_Dog | -5.80e-03 | [-0.04,  0.02] | 69.17% | [-0.02, 0.02] |    82.83%
## ind_Age_Cau_Dog |      0.01 | [-0.02,  0.05] | 77.25% | [-0.02, 0.02] |    63.70%
## ind_Age_Dis_Dog | -3.16e-03 | [-0.02,  0.01] | 76.20% | [-0.02, 0.02] |    96.75%
## ind_Age_Dog     |     -0.03 | [-0.10,  0.03] | 83.00% | [-0.02, 0.02] |    30.88%
## tot_Age_Dog     |     -0.09 | [-0.20,  0.01] | 95.38% | [-0.02, 0.02] |     7.45%
## ind_Age_Spe_Rel | -8.77e-03 | [-0.04,  0.03] | 70.28% | [-0.02, 0.02] |    69.05%
## ind_Age_Str_Rel |      0.02 | [ 0.00,  0.05] | 99.40% | [-0.02, 0.02] |    35.43%
## ind_Age_Per_Rel |      0.01 | [-0.01,  0.04] | 84.92% | [-0.02, 0.02] |    72.17%
## ind_Age_Cau_Rel |      0.02 | [-0.01,  0.06] | 93.10% | [-0.02, 0.02] |    39.05%
## ind_Age_Dis_Rel | -9.98e-04 | [-0.02,  0.01] | 62.18% | [-0.02, 0.02] |    99.08%
## ind_Age_Rel     |      0.05 | [-0.01,  0.12] | 94.35% | [-0.02, 0.02] |    13.50%
## tot_Age_Rel     |      0.12 | [ 0.02,  0.23] | 98.88% | [-0.02, 0.02] |     2.50%

Alrighty, let’s compare to what we obtained previously in lavaan

parest1 %>% 
  slice(55:75) %>% 
  select(label, est, pvalue, ci.lower, ci.upper) %>% 
  mutate(across(where(is.numeric), ~round(.x, 2))) %>% 
  tt(.)
tinytable_ns9inqvd7d69qhj3dmr4
label est pvalue ci.lower ci.upper
ind_x_m1_y1 -0.01 0.38 -0.05 0.02
ind_x_m2_y1 0.03 0.03 0.00 0.06
ind_x_m3_y1 0.01 0.52 -0.02 0.03
ind_x_m4_y1 0.05 0.01 0.01 0.08
ind_x_m5_y1 0.04 0.02 0.00 0.07
ind_x_y1 0.11 0.00 0.04 0.18
tot_x_y1 0.27 0.00 0.16 0.39
ind_x_m1_y2 -0.04 0.02 -0.08 -0.01
ind_x_m2_y2 0.01 0.29 -0.01 0.03
ind_x_m3_y2 -0.01 0.61 -0.03 0.02
ind_x_m4_y2 0.01 0.46 -0.02 0.04
ind_x_m5_y2 0.02 0.35 -0.02 0.05
ind_x_y2 -0.01 0.69 -0.08 0.05
tot_x_y2 -0.07 0.22 -0.19 0.04
ind_x_m1_y3 -0.01 0.60 -0.04 0.03
ind_x_m2_y3 0.03 0.04 0.00 0.05
ind_x_m3_y3 0.01 0.32 -0.01 0.04
ind_x_m4_y3 0.02 0.14 -0.01 0.06
ind_x_m5_y3 0.01 0.71 -0.03 0.04
ind_x_y3 0.06 0.08 -0.01 0.13
tot_x_y3 0.13 0.03 0.02 0.25

The results that we got are very very similar and lead to the same interpretation.

However, before we interpret the estimates at all, it is best practice to check if our model converged properly. Let’s follow the steps of the WAMB checklist this time!

Resources

I am not a statistician, so please take all of what I wrote and coded here with a lot of caution. However, there are many great resources out there if you want to delve deeper into the topic - I am just getting started with this, but here are a few potentially helpful links.

And there are of course many many more resources, this is just to get started with some.