Longitudinal Measurement Invariance

Testing longitudinal measurement invariance with {lavaan} and {semTools}.

.Model
{lavaan}
{semTools}
Author

Michael McCarthy

Published

November 1, 2021

Prerequisites

To access the datasets, help pages, and functions that we will use in this code snippet, load the following packages:

library(lavaan)
library(semTools)

And read in the data:

social_exchanges <- read.csv(here("data", "2021-11-01_social-exchanges.csv"))

The data contains simulated values for several indicators of positive and negative social exchanges, measured on two occasions (w1 and w2). There are three continuous indicators that measure perceived companionship (vst1, vst2, vst3), and three binary indicators that measure unwanted advice (unw1, unw2, unw3). The data and some of the examples come from Longitudinal Structural Equation Modeling: A Comprehensive Introduction by Jason Newsom.

Configural Invariance

Using the lavaan package.

configural_model_lav <- ("
  # Measurement model
  w1comp =~ w1vst1 + w1vst2 + w1vst3
  w2comp =~ w2vst1 + w2vst2 + w2vst3
  
  # Variances and covariances
  w2comp ~~ w1comp
  w1comp ~~ w1comp
  w2comp ~~ w2comp

  w1vst1 ~~ w1vst1
  w1vst2 ~~ w1vst2
  w1vst3 ~~ w1vst3
  w2vst1 ~~ w2vst1
  w2vst2 ~~ w2vst2
  w2vst3 ~~ w2vst3

  w1vst1 ~~ w2vst1
  w1vst2 ~~ w2vst2
  w1vst3 ~~ w2vst3
")

configural_model_lav_fit <- sem(configural_model_lav, data = social_exchanges)

Using the semTools package.

# First, define the configural model, using the repeated measures factors and
# indicators.
configural_model_smt <- ("
  # Measurement model
  w1comp =~ w1vst1 + w1vst2 + w1vst3
  w2comp =~ w2vst1 + w2vst2 + w2vst3
")

# Second, create a named list indicating which factors are actually the same
# latent variable measured repeatedly.
longitudinal_factor_names <- list(
  comp = c("w1comp", "w2comp")
)

# Third, generate the lavaan model syntax using semTools.
configural_model_smt <- measEq.syntax(
  configural.model = configural_model_smt,
  longFacNames = longitudinal_factor_names,
  ID.fac = "std.lv",
  ID.cat = "Wu.Estabrook.2016",
  data = social_exchanges
)
configural_model_smt <- as.character(configural_model_smt)

# Finally, fit the model using lavaan.
configural_model_smt_fit <- sem(configural_model_smt, data = social_exchanges)

Compare lavaan and semTools fit measures.

Configural invariance is met if the model fits well, indicators load on the same factors, and loadings are all of acceptable magnitude. An alternative way of testing longitudinal configural invariance is to fit separate confirmatory factor models at each time point; configural invariance is met if the previously stated criteria hold and the measure has the same factor structure at each time point.

fitMeasures(configural_model_lav_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#>  9.911  5.000  0.078  0.997  0.041
fitMeasures(configural_model_smt_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#>  9.911  5.000  0.078  0.997  0.041

Weak Invariance

Using the lavaan package.

weak_model_lav <- ("
  # Measurement model
  w1comp =~ w1vst1 + a*w1vst2 + b*w1vst3 # Factor loading equality constraint
  w2comp =~ w2vst1 + a*w2vst2 + b*w2vst3 # Factor loading equality constraint

  # Variances and covariances
  w2comp ~~ w1comp
  w1comp ~~ w1comp
  w2comp ~~ w2comp

  w1vst1 ~~ w1vst1
  w1vst2 ~~ w1vst2
  w1vst3 ~~ w1vst3
  w2vst1 ~~ w2vst1
  w2vst2 ~~ w2vst2
  w2vst3 ~~ w2vst3

  w1vst1 ~~ w2vst1
  w1vst2 ~~ w2vst2
  w1vst3 ~~ w2vst3
")

weak_model_lav_fit <- sem(weak_model_lav, social_exchanges)

Using the semTools package.

weak_model_smt <- measEq.syntax(
  configural.model = configural_model_smt,
  longFacNames = longitudinal_factor_names,
  ID.fac = "std.lv",
  ID.cat = "Wu.Estabrook.2016",
  long.equal = c("loadings"),
  data = social_exchanges
)
weak_model_smt <- as.character(weak_model_smt)

weak_model_smt_fit <- sem(weak_model_smt, data = social_exchanges)

Compare lavaan and semTools fit measures.

fitMeasures(weak_model_lav_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#> 12.077  7.000  0.098  0.997  0.036
fitMeasures(weak_model_smt_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#> 12.077  7.000  0.098  0.997  0.036

Test weak invariance.

lavTestLRT(configural_model_lav_fit, weak_model_lav_fit)

Strong Invariance

Using the lavaan package.

Equality tests of factor variances should only be conducted when all factor loadings also are constrained to be equal over time. When all non-referent loadings are set equal in the constrained model, the chi-square is the same regardless of the referent.

strong_model_lav <- ("
  # Measurement model
  w1comp =~ w1vst1 + a*w1vst2 + b*w1vst3 # Factor loading equality constraint
  w2comp =~ w2vst1 + a*w2vst2 + b*w2vst3 # Factor loading equality constraint

  # Variances and covariances
  w2comp ~~ w1comp
  w2comp ~~ v*w2comp # Factor variance equality constraint
  w1comp ~~ v*w1comp # Factor variance equality constraint

  w1vst1 ~~ w2vst1
  w1vst2 ~~ w2vst2
  w1vst3 ~~ w2vst3
")

strong_model_lav_fit <- sem(strong_model_lav, social_exchanges)

Using the semTools package.

# Example 2.2
strong_model_smt <- measEq.syntax(
  configural.model = configural_model_smt,
  longFacNames = longitudinal_factor_names,
  ID.fac = "std.lv",
  ID.cat = "Wu.Estabrook.2016",
  long.equal = c("loadings", "lv.variances"),
  data = social_exchanges
)
strong_model_smt <- as.character(strong_model_smt)

strong_model_smt_fit <- sem(strong_model_smt, social_exchanges)

Compare lavaan and semTools fit measures.

fitMeasures(strong_model_lav_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#> 37.553  8.000  0.000  0.983  0.080
fitMeasures(strong_model_smt_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#> 37.553  8.000  0.000  0.983  0.080

Test strong invariance.

lavTestLRT(configural_model_lav_fit, weak_model_lav_fit, strong_model_lav_fit)

Strict Invariance

Using the lavaan package.

strict_model_lav <- ("
  # Measurement model
  w1comp =~ w1vst1 + a*w1vst2 + b*w1vst3 # Factor loading equality constraint
  w2comp =~ w2vst1 + a*w2vst2 + b*w2vst3 # Factor loading equality constraint

  # Variances & covariances
  w2comp ~~ w1comp

  w1comp ~~ c*w1comp # Factor variance equality constraint
  w2comp ~~ c*w2comp # Factor variance equality constraint

  w1vst1 ~~ w2vst1
  w1vst2 ~~ w2vst2
  w1vst3 ~~ w2vst3

  w1vst1 ~~ d*w1vst1 # Measurement residual equality constraint
  w1vst2 ~~ e*w1vst2 # Measurement residual equality constraint
  w1vst3 ~~ f*w1vst3 # Measurement residual equality constraint

  w2vst1 ~~ d*w2vst1 # Measurement residual equality constraint
  w2vst2 ~~ e*w2vst2 # Measurement residual equality constraint
  w2vst3 ~~ f*w2vst3 # Measurement residual equality constraint
")

strict_model_lav_fit <- sem(strict_model_lav, social_exchanges)

Using the semTools package.

strict_model_smt <- measEq.syntax(
  configural.model = configural_model_smt,
  longFacNames = longitudinal_factor_names,
  ID.fac = "std.lv",
  ID.cat = "Wu.Estabrook.2016",
  long.equal = c("loadings", "lv.variances", "residuals"),
  data = social_exchanges
)
strict_model_smt <- as.character(strict_model_smt)

strict_model_smt_fit <- sem(strict_model_smt, social_exchanges)

Compare lavaan and semTools fit measures.

fitMeasures(strict_model_lav_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#> 78.779 11.000  0.000  0.961  0.104
fitMeasures(strict_model_smt_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#> 78.779 11.000  0.000  0.961  0.104

Test strict invariance.

lavTestLRT(
  configural_model_lav_fit,
  weak_model_lav_fit,
  strong_model_lav_fit,
  strict_model_lav_fit
)

Avatar

Michael McCarthy

Thanks for reading! I’m Michael, the voice behind Tidy Tales. I am an award winning data scientist and R programmer with the skills and experience to help you solve the problems you care about. You can learn more about me, my consulting services, and my other projects on my personal website.

Comments

Session Info

─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.1.1 (2021-08-10)
 os       macOS Mojave 10.14.6
 system   x86_64, darwin17.0
 ui       X11
 language (EN)
 collate  en_CA.UTF-8
 ctype    en_CA.UTF-8
 tz       America/Vancouver
 date     2022-11-23
 pandoc   2.14.0.3 @ /Applications/RStudio.app/Contents/MacOS/pandoc/ (via rmarkdown)
 quarto   1.2.269 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package     * version date (UTC) lib source
 here        * 1.0.1   2020-12-13 [1] CRAN (R 4.1.0)
 lavaan      * 0.6-11  2022-03-31 [1] CRAN (R 4.1.2)
 semTools    * 0.5-5   2021-07-07 [1] CRAN (R 4.1.0)
 sessioninfo * 1.2.2   2021-12-06 [1] CRAN (R 4.1.0)

 [1] /Users/Michael/Library/R/4.1/library
 [2] /Library/Frameworks/R.framework/Versions/4.1/Resources/library

──────────────────────────────────────────────────────────────────────────────

Data

Download the data used in this post.

Reuse

Citation

BibTeX citation:
@online{mccarthy2021,
  author = {Michael McCarthy},
  title = {Longitudinal {Measurement} {Invariance}},
  date = {2021-11-01},
  url = {https://tidytales.ca/snippets/2021-11-01_longitudinal-measurement-invariance},
  langid = {en}
}
For attribution, please cite this work as:
Michael McCarthy. (2021, November 1). Longitudinal Measurement Invariance. https://tidytales.ca/snippets/2021-11-01_longitudinal-measurement-invariance