rr-micer-lavaansem

Moderated APIM with lavaan on multiply imputed data


My goal is to conduct a moderated Actor-Partner Interdependence Model (APIM; Cook & Kenny, 2005) with distinguishable dyads using lavaan syntax on multiply imputed data (generated by the mice package). I have found numerous online examples of mediated APIMs and APIMs with moderated mediation, but none with moderation. I used this tutorial on lavaan APIMs to generate a base/no-moderation model, but I am having trouble properly incorporating the moderator.

In my reprex, I have mother-father dyads that each have two variables and a moderator (all continuous). I want the father moderator to moderate the effect of the father's actor and partner paths and the mother moderator to moderate the effect of the mother's actor and partner's paths (see my ideal figure at the bottom of this post). I've seen how to add a covariate to an APIM, but I am lost on how to properly incorporate two moderators.

My current strategy is to create the interaction effect within the dataset after multiple imputation and then incorporate that directly into the lavaan syntax, rather than trying to rely on the * or : operators. However, it is clear from visualizing with semPaths() that my lavaan syntax is not set up correctly. Any advice on how to correctly incorporate the two moderators in the lavaan syntax would be thoroughly appreciated.

library(mice)
library(lavaan)
library(tidyverse)
library(faux)
library(missMethods)
library(semTools)
library(semPlot)
options(scipen = 9999)
set.seed(1234)

# generate data
df <- faux::rnorm_multi(n = 100, vars = 6, mu = 3, sd = 1, 
                        varnames = c("mom_var1", "mom_var2", "mom_mod",
                                     "dad_var1", "dad_var2", "dad_mod")) %>%
  mutate(id = rep(1:100)) %>% select(id, everything()) %>%
  missMethods::delete_MCAR(cols = c("mom_var1", "mom_var2", "mom_mod",
                                    "dad_var1", "dad_var2", "dad_mod"), p = .15)

# mean center the moderators
center_scale <- function(x) {
  as.numeric(scale(x, center = TRUE, scale = FALSE))
}

df <- df %>%
  mutate(mom_mod = center_scale(mom_mod)) %>%
  mutate(dad_mod = center_scale(dad_mod))

# impute
imp <- mice::mice(df, m = 5, maxit = 10, seed = 1234, printFlag = FALSE)

# create interaction after imputation
impute_dfs <- mice::complete(imp, action = "long", include = TRUE) %>%
  mutate(mom_int = mom_var1*mom_mod) %>%
  mutate(dad_int = dad_var1*dad_mod)

# turn it back into a mids object
imp <- as.mids(impute_dfs)

# base model
apim_no_mod <- "
                  dad_var2  ~ a1*dad_var1  # Dad actor effect
                  mom_var2  ~ a2*mom_var1  # Mom actor effect
                  dad_var2  ~ p12*mom_var1  # Mom to dad partner effect
                  mom_var2  ~ p21*dad_var1  # Dad to mom partner effect
                  dad_var1 ~ mx1*1 # Mean for X for dads
                  mom_var1 ~ mx2*1 # Mean for X for moms
                  dad_var2 ~ iy1*1  # Intercept for Y for moms
                  mom_var2 ~ iy2*1 # Intercept for Y for moms
                  dad_var1 ~~ vx1*dad_var1  # Variance for X for dads
                  mom_var1 ~~ vx2*mom_var1  # Variance for X for moms
                  dad_var2 ~~ ve1*dad_var2   # Error variance for Y for dads
                  mom_var2 ~~ ve2*mom_var2    # Error variance for Y for moms
                  mom_var1 ~~ cx*dad_var1 # Covariance of X between dads and moms
                  mom_var2 ~~ cy*dad_var2  # Covariance of errors between dads and moms
"

# trying but failing to incorporate the two interaction effects
apim_with_two_mods <- "
                  dad_var1 ~ cx1*dad_mod
                  mom_var1 ~ cx2*mom_mod
                  dad_var2  ~ a1*dad_var1 +  p12*mom_var1 + cy1*dad_mod + cy3*dad_int
                  mom_var2  ~ a2*mom_var1 +  p21*dad_var1 + cy2*mom_mod + cy4*mom_int
                  
                  mom_mod ~ mc1*1
                  dad_mod ~ mc2*1
                  mom_int ~ mc3*1
                  dad_int ~ mc4*1
                  
                  dad_var1 ~ ix1*1
                  mom_var1 ~ ix2*1
                  dad_var2 ~ iy1*1
                  mom_var2 ~ iy2*1
                  
                  mom_mod ~~ vv1*mom_mod
                  dad_mod ~~ vv2*dad_mod
                  mom_int ~~ vv3*mom_int
                  dad_int ~~ vv4*dad_int
                  
                  dad_var1 ~~ vex1*dad_var1
                  mom_var1 ~~ vex2*mom_var1
                  dad_var2 ~~ vye1*dad_var2
                  mom_var2 ~~ vye2*mom_var2
                  mom_var1 ~~ cx*dad_var1
                  mom_var2 ~~ cy*dad_var2
                  
                  covXd := cx1-cx2
                  covyd := cy1-cy2
                  covXa := (cx1+cx2)/2
                  covya := (cy1+cy2)/2
"

myapim <- apim_with_two_mods
sem_model <- sem.mi(model = myapim, fixed.x = FALSE, data = imp)
summary(sem_model)

# Plot model see what it looks like 
## *I know there are issues with using this code, but
## *I just want to visualize the paths, not the numbers specifically
# https://stackoverflow.com/questions/75573417/plotting-a-sem-actor-partner-interdependence-model-after-runmi
df <- df %>% mutate(mom_int = mom_var1*mom_mod) %>% mutate(dad_int = dad_var1*dad_mod)
other_model <- sem(myapim, fixed.x = FALSE, data = df)
baseplot <- semPlot::semPlotModel(other_model)
desired_output <- data.frame(standardizedsolution(sem_model))
desired_output <- desired_output %>% filter(op != ":=")
baseplot@Pars$est <- desired_output$est.std
semPaths(baseplot, nCharNodes = 0, what = "est",
         whatLabels = "invisible", fade = FALSE, layout = "tree2", 
         rotation = 2, style = "ram", intercepts = FALSE, residuals = FALSE, 
         optimizeLatRes = TRUE, curve = 3.1, nDigits = 3, sizeMan = 12, sizeMan2 = 10, 
         label.cex = 1.2, edge.label.position = 0.45, edge.label.cex = 1.5)

enter image description here

At least in theory, this is how I would like my final model to look like:

enter image description here

EDIT (4/20/23)

I may have found an answer to this, but I won't post this as an answer until I'm sure it works. Similar to how this Shiny app adds covariate, we could just add four covariates (the two moderators and the two manually-created interaction effects).

better_apim <- '

                        mom_var2  ~ a1*mom_var1
                        dad_var2  ~ a2*dad_var1
                        mom_var2  ~ p12*dad_var1
                        dad_var2  ~ p21*mom_var1
                        mom_var1 ~ mx1*1
                        dad_var1 ~ mx2*1
                        mom_var2 ~ my1*1
                        dad_var2 ~ my2*1
                        mom_var1 ~~ vx1*mom_var1
                        dad_var1 ~~ vx2*dad_var1
                        mom_var2 ~~ vy1*mom_var2
                        dad_var2 ~~ vy2*dad_var2
                        dad_var1 ~~ cx*mom_var1
                        dad_var2 ~~ cy*mom_var2 
 
                        mom_var1 ~~ cx1b1*mom_int
                        dad_var1 ~~ cx2b1*mom_int
                        mom_var1~~ cx1b2*dad_int
                        dad_var1 ~~ cx2b2*dad_int
                        
                        mom_var1~~ cx1b3*mom_mod
                        dad_var1 ~~ cx2b3*mom_mod
                        mom_var1~~ cx1b4*dad_mod
                        dad_var1 ~~ cx2b4*dad_mod
                        
                        mom_int ~~ b1b2*dad_int 
                        mom_int ~~ b1b3*mom_mod 
                        dad_int ~~ b2b3*mom_mod 
                        
                        mom_int ~~ b1b4*dad_mod
                        dad_int ~~ b2b4*dad_mod 
                         
                        mom_var2 ~ mom_int1*mom_int 
                        dad_var2 ~ mom_int2*mom_int 
                        mom_int ~~ vmom_int*mom_int 
                        mom_int ~ mmom_int*1 
                        
                        mom_var2 ~ dad_int1*dad_int 
                        dad_var2 ~ dad_int2*dad_int 
                        dad_int ~~ vdad_int*dad_int 
                        dad_int ~ mdad_int*1 
                          
                        mom_var2 ~ mom_mod1*mom_mod 
                        dad_var2 ~ mom_mod2*mom_mod 
                        mom_mod ~~ vmom_mod*mom_mod 
                        mom_mod ~ mmom_mod*1 
                        
                        mom_var2 ~ mom_mod1*dad_mod
                        dad_var2 ~ mom_mod2*dad_mod 
                        dad_mod ~~ vmom_mod*dad_mod 
                        dad_mod ~ mdad_mod*1 
                        
                        a_diff := a1 - a2
                        p_diff := p12 - p21
                        k1 := p12/a1
                        k2 := p21/a2
                        k_diff := k1 - k2
                        i_diff := my1 - my2
                        a_ave := (a1 + a2)/2
                        p_ave := (p12 + p21)/2
                        i_ave := (my1 +my2)/2
                        sum1 := (p12 + a1)/2
                        sum2 := (p21 + a2)/2
                        cont1 := a1 - p12
                        cont2 := a2 - p21
                        
'

Solution

  • I believe I have identified a partial solution. The below code does not calculate k-ratios and such, but it does provide a proper moderated APIM. First, it is very important to mean-center your moderators, which I neglected to do in my original question. I have also commented out the partner interaction effect (e.g., I excluded dad_int when predicting mom_var2), but this can be included depending on your hypothesis.

    library(mice); library(lavaan); library(tidyverse); library(faux); library(missMethods)
    set.seed(1234)
    
    ## prepwork
    # generate data
    df <- faux::rnorm_multi(n = 100, vars = 6, mu = 3, sd = 1, 
                            varnames = c("mom_var1", "mom_var2", "mom_mod", "dad_var1", "dad_var2", "dad_mod")) %>%
      mutate(id = rep(1:100)) %>% select(id, everything()) %>%
      missMethods::delete_MCAR(cols = c("mom_var1", "mom_var2", "mom_mod", "dad_var1", "dad_var2", "dad_mod"), p = .15) %>%
      # mean center the moderators 
      mutate(mom_mod = as.numeric(scale(mom_mod, center = TRUE, scale = FALSE))) %>%
      mutate(dad_mod = as.numeric(scale(dad_mod, center = TRUE, scale = FALSE))) %>%
      # create interaction term
      mutate(mom_int = mom_var1*mom_mod) %>%
      mutate(dad_int = dad_var1*dad_mod)
    
    # impute
    imp <- mice::mice(df, m = 5, maxit = 10, seed = 1234, printFlag = FALSE)
    
    myapim <- "
      mom_var2 ~ mom_var1 + mom_mod + mom_int + dad_var1 + dad_mod #+ dad_int
      dad_var2 ~ dad_var1 + dad_mod + dad_int + mom_var1 + mom_mod #+ mom_int 
      mom_var1 ~~ mom_var1
      dad_var1 ~~ dad_var1
      mom_var2 ~~ mom_var2
      dad_var2 ~~ dad_var2
      dad_var1 ~~ mom_var1
      dad_var2 ~~ mom_var2 
      mom_mod ~~ dad_mod
      mom_int ~~ dad_int
      mom_mod ~~ dad_int
      dad_mod ~~ mom_int
      "
    
    sem_model <- sem.mi(model = myapim, fixed.x = FALSE, data = imp)
    summary(sem_model)