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)
At least in theory, this is how I would like my final model to look like:
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
'
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)