Institute for Mental and Organisational Health, FHNW
06 June, 2025
{lavaan}{lavaan}-Syntax:
=~ denotes a latent variable measured by indicators.lavaan::cfa() (you can also use lavaan::sem()).summary(){lavaan} (first MV per CF).When using maximum likelihood (ML) estimation, parameters are found by minimizing
\[F_{ML}=ln|S| - ln|\Sigma| + trace(S\Sigma^{-1})-p\]
\(\rightarrow\) Perfect fitting model, \(S\) and \(\Sigma\) are identical, so \(F_{ML}\) is 0, as \(S\Sigma^{-1}=I\) and thus \(trace(S\Sigma^{-1})=p\).
Absolute fit indices, e.g.:
Incremental fit indices, e.g. (reduction in misfit compared to baseline, usually zero covariances):
\(\rightarrow\) Report multiple indices (F. Chen et al., 2008; Hu & Bentler, 1999; Moshagen & Auerswald, 2018).
\(\rightarrow\) Don’t overrely on cut-off values (F. Chen et al., 2008; Kenny et al., 2015; Lai & Green, 2016).
\(\rightarrow\) RMSEA and TLI good measures of fit and fitting propensity according to (Bader & Moshagen, 2025), but see also (Bonifay et al., 2025).
Practical exoperience has made us feel that a value of the RMSEA of about 0.05 or less would indicate a close fit of the model in relation to the degrees of freedom.
Browne & Cudeck (1993), p.144
In our experience, models with overall fit indices of less than .90 can usually be improved substantially.
Bentler & Bonett (1980), p. 600
\(\rightarrow\) But many simulation studies since (see previous slide).
{lavaan}cfa()summary()lavResiduals()modindices()More {lavaan} syntax:
# add a cross-loading
model <- '
CF1 =~ MV1 + MV2 + MV3
CF2 =~ MV4 + MV5 + MV6 + MV2
'
# add a cov between residuals of MV3 and MV6
model <- '
CF1 =~ MV1 + MV2 + MV3
CF2 =~ MV4 + MV5 + MV6
MV3 ~~ MV6
'
# constrain or fix parameters
model <- '
CF1 =~ a*MV1 + a*MV2 + a*MV3
CF2 =~ MV4 + MV5 + MV6
CF1 ~~ 0*CF2
'{lavaan} output.model <- '
CF1 =~ MV1 + MV2 + MV3
CF2 =~ MV4 + MV5 + MV6
'
# non-normal data:
# robust standard errors and scaled test statistic
fit <- lavaan::cfa(model, data = your_data,
se = "robust", test = "Satorra-Bentler")
# categorical data
fit <- lavaan::cfa(model, data = your_data,
ordered = TRUE)
# small datasets
fit <- lavaan::cfa(model, data = your_data,
bounds = "standard")
summary(fit, fit.measures = TRUE, standardized = TRUE){mice} and {lavaan.mi})model <- '
CF1 =~ MV1 + MV2 + MV3
CF2 =~ MV4 + MV5 + MV6
'
# FIML
fit <- lavaan::cfa(model, data = your_data,
missing = "ml") # or "fiml" or "direct"
# Multiple imputation with {mice} (simplified)
data_imp <- mice(data = your_data %>%
mutate(across(MV1:MV6, ordered)),
method = "polr")
data_imp <- map(seq_len(5),
~complete(data_imp, action = .x, inc = FALSE))
# fit with {lavaan.mi}
fit_imp <- cfa.mi(model, data = data_imp, ordered = TRUE){lavaan}cfa() and arguments group and group.equal specified.lavTestLRT()fitMeasures())semTools::measEq.syntax() can help generating model syntax)model <- '
CF1 =~ MV1 + MV2 + MV3
CF2 =~ MV4 + MV5 + MV6
'
# configural invariance
fit_conf <- cfa(model, data = your_data, group = "group_var")
# weak invariance
fit_weak <- cfa(model, data = your_data, group = "group_var",
group.equal = "loadings")
# strong invariance
fit_strong <- cfa(model, data = your_data, group = "group_var",
group.equal = c("intercepts", "loadings"))
# model comparison tests
lavTestLRT(fit_conf, gad7_2f_wfit_weakeak, fit_strong)
# obtain fit measures of interest
fitMeasures(fit_conf)[c("cfi", "rmsea", "srmr")]Model specification:
Input data:
Model estimation:
Model evaluation:
Substantive conclusions:

Introduction to Factor Analysis - GSP 2025