Latent growth models with missing data

Scope

A latent growth curve model describes change across repeated measurements with latent intercept and slope factors. From semTests’ point of view, it is a continuous SEM. The usual robust p-values therefore work, including with full-information maximum likelihood (FIML) when a few repeated measurements have gone missing.

We will fit a linear trajectory, check its overall fit, and then ask whether a more flexible shape fits noticeably better.

library("semTests")
library("lavaan")

growth_data <- Demo.growth

A reproducible missing-data mechanism

Demo.growth is complete. To exercise FIML, missingness in the later waves t3 and t4 depends on the first wave t1. This gives the example a reproducible MAR-like mechanism and says nothing about a real missingness process.

set.seed(20260718)
driver <- as.numeric(scale(growth_data$t1))
growth_data$t3[runif(nrow(growth_data)) < plogis(qlogis(.20) + .8 * driver)] <- NA
growth_data$t4[runif(nrow(growth_data)) < plogis(qlogis(.20) + .8 * driver)] <- NA
colMeans(is.na(growth_data[paste0("t", 1:4)]))
#>    t1    t2    t3    t4 
#> 0.000 0.000 0.195 0.190

Linear growth under FIML

The linear model fixes the slope loadings to 0, 1, 2, 3:

linear <- growth(
  "i =~ 1*t1 + 1*t2 + 1*t3 + 1*t4
   s =~ 0*t1 + 1*t2 + 2*t3 + 3*t4",
  growth_data,
  missing = "fiml", estimator = "MLR"
)

pvalues(linear, c("SB", "SS", "ALL", "PEBA4"))
#>      sb_ml      ss_ml     all_ml   peba4_ml 
#> 0.09706262 0.09900832 0.09857646 0.09741806 
#> estimator: ML (MLR) (FIML) | data: continuous | information: observed | df: 5 | FIML convention: observed

FIML uses the standard likelihood-ratio statistic and the observed-information convention by default. The footer records both choices, so they do not vanish when the result is copied into another script.

Is the growth linear?

A latent-basis model frees the last two slope loadings and lets the data choose the shape of change. Linear growth fixes those loadings to 2 and 3, which makes it a special case of the latent-basis model. The linear model is the more constrained fit, so it goes first:

basis <- growth(
  "i =~ 1*t1 + 1*t2 + 1*t3 + 1*t4
   s =~ 0*t1 + 1*t2 + t3 + t4",
  growth_data,
  missing = "fiml", estimator = "MLR"
)

pvalues_nested(linear, basis, tests = c("SB", "SS", "ALL", "PEBA2"))
#>     sb_ml     ss_ml    all_ml  peba2_ml 
#> 0.5701799 0.5691468 0.5692181 0.5699399 
#> estimator: ML (MLR) (FIML) | data: continuous | information: observed | df: 2 | FIML convention: observed | nested (method 2000, A.method delta)

A small p-value would be evidence that the linear shape is too rigid. A large one means this comparison found no clear reason to let the trajectory bend. The two freed loadings give a two-degree-of-freedom test, so we use PEBA2. The number of blocks cannot exceed the test degrees of freedom.

The missing-data details are collected in vignette("fiml-missing-data", package = "semTests").