Semtools longitudinal partial invariance syntax

85 views
Skip to first unread message

Alexander Miles

unread,
Sep 2, 2024, 9:43:48 PMSep 2
to lav...@googlegroups.com
Hello, everyone!

I am trying to use the long.partial argument in semTools::measEq.syntax(), to free individual intercepts in my strong longitudinal invariance model. 
Below, I have a reproducible example with a simulated dataset of 2 time points and 2 factors. 
I use, in my example, long.partial = "x2_t2 ~1", though this fails to free the intercept at x2_t2 (as indicated by the modification indices at the end of the example).

Is there a way to use  semTools::measEq.syntax() to free individual intercepts for longitudinal partial invariance testing? 

Thanks!
Alex

library(dplyr)
#> This is lavaan 0.6-18
library(MASS)
library(semTools)
#> This is semTools 0.5-6

set.seed(42)
n <- 500

# 2 factors with 3 indicators each
lambda <- matrix(c(0.8, 0.7, 0.6,  # factor 1
                   0.8, 0.7, 0.6),  # factor 2
                 nrow = 3, byrow = TRUE)

intercepts <- rep(0, 3)
residuals <- diag(c(0.36, 0.49, 0.64))

factor_means <- c(0, 0.2)
factor_covariances <- diag(c(1, 1))

# Simulate 2 time points
simulate_time_point <- function() {
  factors <- mvrnorm(n, mu = factor_means, Sigma = factor_covariances) # Generate factor scores
  observed <- factors %*% t(lambda) + mvrnorm(n, mu = intercepts, Sigma = residuals) # Compute observed variables
  colnames(observed) <- paste0("x", 1:3)
  return(as.data.frame(observed))
}

data_t1 <- simulate_time_point()
data_t2 <- simulate_time_point()

# Combine
colnames(data_t1) <- paste0("x", 1:3, "_t1")
colnames(data_t2) <- paste0("x", 1:3, "_t2")

longitudinal_data <- cbind(data_t1, data_t2)
longitudinal_data$ID <- 1:n

# Building the model
long_config_model <- '
  eta1 =~ x1_t1 + x2_t1 + x3_t1
  eta2 =~ x1_t2 + x2_t2 + x3_t2

'

longitudinal_factor_names <- list(
  eta = c("eta1", "eta2")
)

config_model_smt <- semTools::measEq.syntax(
  configural.model = long_config_model,
  longFacNames = longitudinal_factor_names,
  ID.fac = "fixed.factor",
  ID.cat = "Wu.Estabrook.2016",
  data = longitudinal_data)

config_model_smt <- as.character(config_model_smt)

strong_model_smt <- semTools::measEq.syntax(
  configural.model = config_model_smt,
  longFacNames = longitudinal_factor_names,
  ID.fac = "std.lv",
  ID.cat = "Wu.Estabrook.2016",
  long.equal = c("loadings", "intercepts"),
  data = longitudinal_data
)
strong_model_smt <- as.character(strong_model_smt)

partial_strong_model_smt <- semTools::measEq.syntax(
  configural.model = config_model_smt,
  longFacNames = longitudinal_factor_names,
  ID.fac = "std.lv",
  ID.cat = "Wu.Estabrook.2016",
  long.equal = c("loadings", "intercepts"),
  long.partial = "x2_t2 ~1",
  data = longitudinal_data
)

partial_strong_model_smt <- as.character(partial_strong_model_smt)

config_fit_smt <-
  lavaan::cfa(
    config_model_smt,
    data = longitudinal_data,
)

strong_fit_smt <-
  lavaan::cfa(
    strong_model_smt,
    data = longitudinal_data,
  )

partial_strong_fit_smt <-
  lavaan::cfa(
    partial_strong_model_smt,
    data = longitudinal_data,
)

strong_fit_smt
#> lavaan 0.6-18 ended normally after 25 iterations
#>
#>   Estimator                                         ML
#>   Optimization method                           NLMINB
#>   Number of model parameters                        24
#>   Number of equality constraints                     6
#>
#>   Number of observations                           500
#>
#> Model Test User Model:
#>                                                      
#>   Test statistic                                 5.121
#>   Degrees of freedom                                 9
#>   P-value (Chi-square)                           0.824
partial_strong_fit_smt
#> lavaan 0.6-18 ended normally after 25 iterations
#>
#>   Estimator                                         ML
#>   Optimization method                           NLMINB
#>   Number of model parameters                        24
#>   Number of equality constraints                     6
#>
#>   Number of observations                           500
#>
#> Model Test User Model:
#>                                                      
#>   Test statistic                                 5.121
#>   Degrees of freedom                                 9
#>   P-value (Chi-square)                           0.824



cat(partial_strong_model_smt)
#> ## LOADINGS:
#>
#> eta1 =~ NA*x1_t1 + lambda.1_1*x1_t1
#> eta1 =~ NA*x2_t1 + lambda.2_1*x2_t1
#> eta1 =~ NA*x3_t1 + lambda.3_1*x3_t1
#> eta2 =~ NA*x1_t2 + lambda.1_1*x1_t2
#> eta2 =~ NA*x2_t2 + lambda.2_1*x2_t2
#> eta2 =~ NA*x3_t2 + lambda.3_1*x3_t2
#>
#> ## INTERCEPTS:
#>
#> x1_t1 ~ NA*1 + nu.1*1
#> x2_t1 ~ NA*1 + nu.2*1
#> x3_t1 ~ NA*1 + nu.3*1
#> x1_t2 ~ NA*1 + nu.1*1
#> x2_t2 ~ NA*1 + nu.2*1
#> x3_t2 ~ NA*1 + nu.3*1
#>
#> ## UNIQUE-FACTOR VARIANCES:
#>
#> x1_t1 ~~ NA*x1_t1 + theta.1_1*x1_t1
#> x2_t1 ~~ NA*x2_t1 + theta.2_2*x2_t1
#> x3_t1 ~~ NA*x3_t1 + theta.3_3*x3_t1
#> x1_t2 ~~ NA*x1_t2 + theta.4_4*x1_t2
#> x2_t2 ~~ NA*x2_t2 + theta.5_5*x2_t2
#> x3_t2 ~~ NA*x3_t2 + theta.6_6*x3_t2
#>
#> ## UNIQUE-FACTOR COVARIANCES:
#>
#> x1_t1 ~~ NA*x1_t2 + theta.4_1*x1_t2
#> x2_t1 ~~ NA*x2_t2 + theta.5_2*x2_t2
#> x3_t1 ~~ NA*x3_t2 + theta.6_3*x3_t2
#>
#> ## LATENT MEANS/INTERCEPTS:
#>
#> eta1 ~ 0*1 + alpha.1*1
#> eta2 ~ NA*1 + alpha.2*1
#>
#> ## COMMON-FACTOR VARIANCES:
#>
#> eta1 ~~ 1*eta1 + psi.1_1*eta1
#> eta2 ~~ NA*eta2 + psi.2_2*eta2
#>
#> ## COMMON-FACTOR COVARIANCES:
#>
#> eta1 ~~ NA*eta2 + psi.2_1*eta2


modindices(partial_strong_fit_smt,
           sort. = TRUE,
           free.remove = FALSE) %>%
           subset(op == "~1") %>%
           head(20)
#> Warning: lavaan->modindices():  
#>    the modindices() function ignores equality constraints; use lavTestScore()
#>    to assess the impact of releasing one or multiple constraints.
#>      lhs op rhs    mi    epc sepc.lv sepc.all sepc.nox
#> 11 x2_t2 ~1     0.234 -0.018  -0.018   -0.015   -0.015
#> 8  x2_t1 ~1     0.227  0.018   0.018    0.015    0.015
#> 9  x3_t1 ~1     0.219 -0.019  -0.019   -0.015   -0.015
#> 12 x3_t2 ~1     0.208  0.018   0.018    0.014    0.014
#> 10 x1_t2 ~1     0.002  0.002   0.002    0.001    0.001
#> 7  x1_t1 ~1     0.002 -0.002  -0.002   -0.001   -0.001
#> 23  eta2 ~1     0.000  0.000   0.000    0.000    0.000


Terrence Jorgensen

unread,
Sep 12, 2024, 5:15:04 AMSep 12
to lavaan
Hi, thanks for the reprex, that's a big help.

in my example, long.partial = "x2_t2 ~1" fails to free the intercept at x2_t2
 
That's because you don't refer to a repeatedly measured indicator, only to one instance (i.e., a variable in your data set).  You are specifying longitudinal_factor_names, but you need to refer to the name of the longitudinal indicator "x2".  
You can find the help-page Details section on Repeated Measures informative about this.

If you do NOT override the measEq.syntax() output with the as.character() output, then you can see the default longIndNames= that get generated automatically.  (Also, you could continue to use your original model specification: long_config_model)

partial_strong_model_smt <- semTools::measEq.syntax(

    configural.model = long_config_model,
    longFacNames = longitudinal_factor_names,
    ID.fac = "std.lv",
    # ID.cat = "Wu.Estabrook.2016", # already the default

    long.equal = c("loadings", "intercepts"),
    long.partial = "x2_t2 ~1",
    data = longitudinal_data
)
partial_strong_model <- as.character(partial_strong_model_smt)
partial_strong_model_smt@call$longIndNames
$._eta_.ind.1
   eta1    eta2
"x1_t1" "x1_t2"

$._eta_.ind.2
   eta1    eta2
"x2_t1" "x2_t2"

$._eta_.ind.3
   eta1    eta2
"x3_t1" "x3_t2" 


So the correct way to free the second repeatedly measured indicator's intercepts across occasions is to refer to that variable name (in bold above)

partial_strong_model_smt <- semTools::measEq.syntax(

    configural.model = long_config_model,
    longFacNames = longitudinal_factor_names,
    ID.fac = "std.lv",

    long.equal = c("loadings", "intercepts"),
    long.partial = "._eta_.ind.2~1",
    data = longitudinal_data
)
partial_strong_model <- as.character(partial_strong_model_smt)
cat(partial_strong_model, sep = "\n")

...
## INTERCEPTS:


x1_t1 ~ NA*1 + nu.1*1
x2_t1 ~ NA*1 + nu.2*1 # now they are

x3_t1 ~ NA*1 + nu.3*1
x1_t2 ~ NA*1 + nu.1*1
x2_t2 ~ NA*1 + nu.5*1 # different

x3_t2 ~ NA*1 + nu.3*1



Or you could explicitly create your own names, so you don't have to look them up in a previous specification:

longitudinal_indicator_names <- list(x1 = c(eta1 = "x1_t1", eta2 = "x1_t2"),
                                     x2 = c(eta1 = "x2_t1", eta2 = "x2_t2"),
                                     x3 = c(eta1 = "x3_t1", eta2 = "x3_t2"))
partial_strong_model_smt <- semTools::measEq.syntax(
    configural.model = long_config_model,
    longFacNames = longitudinal_factor_names,
    longIndNames = longitudinal_indicator_names,
    ID.fac = "std.lv",

    long.equal = c("loadings", "intercepts"),
    long.partial = "x2~1",
    data = longitudinal_data
)

Terrence D. Jorgensen    (he, him, his)
Assistant Professor, Methods and Statistics
Research Institute for Child Development and Education, the University of Amsterdam
http://www.uva.nl/profile/t.d.jorgensen


Alexander Miles

unread,
Sep 19, 2024, 2:15:34 AMSep 19
to lavaan
Hi Terrence,

Thank you for the reply! I have a second question, if you'd be willing to answer: If I had, say, 4 different time points (x1 = c(eta1 = "x1_t1", eta2 = "x1_t2", eta3 = "x1_t3", eta4 = "x1_t4"),  would it be possible to constrain an indicator's intercept only for one time point? Something like, x1_t1 = x1_t2 = x1_t3, but now the intercept x1_t4 is different.

Thank you again,
Alex

Terrence Jorgensen

unread,
Sep 19, 2024, 3:55:19 AMSep 19
to lavaan
would it be possible to constrain an indicator's intercept only for one time point? 

Not using the long.partial= or group.partial= arguments.  But you can use the update(change.syntax=) argument to update any individual parameter's label/value.  See the class?measEq.syntax help-page description of that argument, and find an example in this post:


(also check the reply immediately following, regarding changing the label in addition to the value)

Alexander Miles

unread,
Sep 19, 2024, 10:41:31 PMSep 19
to lavaan
Thank you, Terrence! I've got it working now. 

For future reference (and anyone who stumbles across this):  
partial_strong_model_smt_2 <-
  update(partial_strong_model_smt,
         change.syntax = c("x2_t4 ~ NA*1 + nu.2 * 1",
                           "x2_t4 ~ NA*1 + nu.2_4 * 1"))
Reply all
Reply to author
Forward
0 new messages