Moderation (interaction) in lavaan

148 views
Skip to first unread message

Martin Lacher

unread,
Sep 29, 2022, 5:54:21 PM9/29/22
to lavaan
Sorry for my maybe lame question, I googled a lot but didn't find a solution:

I would like to do a regression with a interaction term in lavaan, but I can't find out, how to do that. In lm I simply would run a model as "posttest ~ pretest + predictor*moderator" but in lavaan I understand the * works differently. In several post I read, I could just use the semicolon instead, but then I get a "lavaan ERROR: missing observed variables in dataset: predictor:mediator".

So please, where can I find good information on how to do this properly in lavaan? This is just a simplified example, in the end I would like to run a 2-level regression with a categorical moderator (group) on level 1, but if I can't even do the very simple model...

Thanks a lot in advance for your support and for all your great work out there! ;)

Cheers, Martin

Jeremy Miles

unread,
Sep 29, 2022, 8:17:51 PM9/29/22
to lav...@googlegroups.com
If your model is just a regression, you create the moderator outside the model statement.

Something like:

my_data <- 
  my_data %>%
  dplyr::mutate(m_x_p = moderator * predictor)

Then your lavaan model looks like:

posttest ~ pretest + predictor + moderator + m_x_p

If your moderator (or predictor) is categorical there is also the option of multiple groups, which allows you to relax some assumptions.

Jeremy



--
You received this message because you are subscribed to the Google Groups "lavaan" group.
To unsubscribe from this group and stop receiving emails from it, send an email to lavaan+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/lavaan/d928e968-e3ae-4bee-a239-de7fffebf71an%40googlegroups.com.

Martin Lacher

unread,
Sep 30, 2022, 4:47:52 AM9/30/22
to lavaan
Thanks a lot, Jeremy, for the quick reply. That's kind of the "SPSS"-method, not very elegant, but should work, indeed. As for the multigroup: I actually already tried this, but wasn't sure about the interpretation of the result. In one group the investigated predictor was signicifant, in the other group it wasn't - is this enough to say, that the group affilation is a moderator of the prediction?

Thanks, Martin

Christian Arnold

unread,
Sep 30, 2022, 5:30:53 AM9/30/22
to lav...@googlegroups.com
Hi Martin,

no, this is not sufficient. There are several ways to test the moderator effect. Two examples:

library(lavaan)

# Method 1

HS.model <- "
f1 =~ x1 + x2 + x3
f2 =~ x4 + x5 + x6
f2 ~ c(bg1, bg2) * f1
mod := bg2 - bg1
"

fit <- sem(HS.model, data = HolzingerSwineford1939, group = "school")

pe  <- parameterEstimates(fit)
pe[pe$op == "~",]
pe[pe$op == ":=",]


# Method 2

HS.model.uncon <- "
f1 =~ x1 + x2 + x3
f2 =~ x4 + x5 + x6
f2 ~ c(bg1, bg2) * f1
"

HS.model.con <- "
f1 =~ x1 + x2 + x3
f2 =~ x4 + x5 + x6
f2 ~ c(bg1, bg2) * f1
bg1 == bg2
"

fit.uncon <- sem(HS.model.uncon, data = HolzingerSwineford1939, group = "school")
fit.con   <- sem(HS.model.con, data = HolzingerSwineford1939, group = "school")

lavTestLRT(fit.uncon, fit.con)


In the first case, the difference of the effects is tested. In the second case, the model is compared to a nested model where it is assumed that the effects do not differ. If you prefer resampling techniques, you can use bootstrapping for method 1. For method 2, permutation tests should be applicable (in the latter case I am not 100% sure).

Best

Christian


Von: lav...@googlegroups.com <lav...@googlegroups.com> im Auftrag von Martin Lacher <lacher...@gmail.com>
Gesendet: Freitag, 30. September 2022 10:47
An: lavaan <lav...@googlegroups.com>
Betreff: Re: Moderation (interaction) in lavaan
 

Martin Lacher

unread,
Oct 1, 2022, 4:49:26 PM10/1/22
to lavaan
Dear Christian,

Thanks a lot for this detailed answer. I tried to implement it with my 2-level model, but I get an error message "Fehler in ov.names.l[[g]] : Indizierung außerhalb der Grenzen" (I assume you speak German).

This is my code:

dta.ATI <- fscores.I0_I1 |>
  dplyr::select("Pre_z", "Post","Lehrperson","SR3.theta","math.fscores","Gruppe") |>
  na.omit()
dta.ATI$Gruppe <- droplevels(dta.ATI$Gruppe)
dta.ATI$Gruppe <- recode(dta.ATI$Gruppe,"'I0'=0; 'I1'=1") #Dummykodierung mit Zahlen
dta.ATI <-scale_ignore(dta.ATI,ignore=c("Lehrperson","Gruppe")) #Alle Variablen skalieren und zentrieren, damit lavaan sauber durchläuft

lavmodeldef <- "
              level: 1
                Post ~ Pre_z + c(bg1,bg2)*SR3.theta

              level: 2
                Post ~ Pre_z


  mod := bg2 - bg1
"
#this is your model as proposed:
# f1 =~ x1 + x2 + x3
# f2 =~ x4 + x5 + x6
# f2 ~ c(bg1, bg2) * f1
# mod := bg2 - bg1

fit <- sem(lavmodeldef, data = dta.ATI, cluster = "Lehrperson", estimator="MLR",group="Gruppe")


pe  <- parameterEstimates(fit)
pe[pe$op == "~",]
pe[pe$op == ":=",]

----END OF CODE----

Here is the structure of dta.ATI:

> str(dta.ATI)
'data.frame':    170 obs. of  6 variables:
 $ Pre_z       : num  1.204 1.05 0.876 0.364 -0.834 ...
 $ Post        : num  -0.0232 0.4186 0.8905 0.6055 -1.2467 ...
 $ Lehrperson  : Factor w/ 17 levels "1980","2690",..: 15 15 15 15 15 15 15 15 15 15 ...
 $ SR3.theta   : num  2.027 0.364 2.027 2.027 -0.868 ...
 $ math.fscores: num  0.919 0.593 0.593 0.593 -2.017 ...
 $ Gruppe      : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...

What am I'm doing wrong? Before I had to add "group: 1" and "group :2" lines to the model avoid this error, but now this also leads to an error.

Thanks a lot in advance, Martin

Martin Lacher

unread,
Oct 1, 2022, 5:39:15 PM10/1/22
to lavaan
I think I could solve the problem myself. This code seems to work (I'm glad if somebody can verify it):

dta.ATI <- fscores.I0_I1 |>
  dplyr::select("Pre_z", "Post","Lehrperson","SR3.theta","math.fscores","Gruppe") |>
  na.omit()
dta.ATI$Gruppe <- droplevels(dta.ATI$Gruppe)
dta.ATI$Gruppe <- recode(dta.ATI$Gruppe,"'I0'=0; 'I1'=1") #Dummykodierung mit Zahlen
dta.ATI <-scale_ignore(dta.ATI,ignore=c("Lehrperson","Gruppe")) #Alle Variablen skalieren und zentrieren, damit lavaan sauber durchläuft

#Version 1, difference of bg2 and bg1

lavmodeldef <- "
  Group: 1
              level: 1
                Post ~ Pre_z + bg1*SR3.theta +math.fscores

              level: 2
                Post ~ Pre_z + math.fscores
  Group: 2
              level: 1
                Post ~ Pre_z + bg2*SR3.theta + math.fscores

              level: 2
                Post ~ Pre_z + math.fscores


  mod := bg2 - bg1
"
fit <- sem(lavmodeldef, data = dta.ATI, cluster = "Lehrperson", estimator="MLR",group="Gruppe")

pe  <- parameterEstimates(fit)
pe[pe$op == "~",]
pe[pe$op == ":=",]

#Version 2, nested models

lavmodeldef.uncon <- "
  Group: 1
              level: 1
                Post ~ Pre_z + bg1*SR3.theta +math.fscores

              level: 2
                Post ~ Pre_z + math.fscores
  Group: 2
              level: 1
                Post ~ Pre_z + bg2*SR3.theta + math.fscores

              level: 2
                Post ~ Pre_z + math.fscores
"

lavmodeldef.con <- "
  Group: 1
              level: 1
                Post ~ Pre_z + bg1*SR3.theta +math.fscores

              level: 2
                Post ~ Pre_z + math.fscores
  Group: 2
              level: 1
                Post ~ Pre_z + bg2*SR3.theta + math.fscores

              level: 2
                Post ~ Pre_z + math.fscores
  bg1 == bg2
"

fit.uncon <- sem(lavmodeldef.uncon, data = dta.ATI, cluster = "Lehrperson", estimator="MLR",group="Gruppe")
fit.con   <- sem(lavmodeldef.con, data = dta.ATI, cluster = "Lehrperson", estimator="MLR",group="Gruppe")

lavTestLRT(fit.uncon, fit.con)

---END OF CODE----

unfortunately, for me, both versions have p-values of around .99, so no significant difference (this interpretation is correct, isn't it?)

Thanks all the same, maybe the code is useful for somebody.

Kind regards, Martin
Reply all
Reply to author
Forward
0 new messages