1 Longitudinal Invariance
2 Libraries
3 Data
db_invariance <-
db_long %>%
select(id_estudiante, ola, starts_with(c("perc", "pref"))) %>%
pivot_wider(
id_cols = id_estudiante,
names_from = ola,
names_glue = "{.value}{ola}",
values_from = c(
perc_effort, perc_talent,
perc_rich_parents, perc_contact,
pref_effort, pref_talent,
pref_rich_parents, pref_contact
)
) %>%
na.omit() %>%
rename_with(~ str_replace_all(., "_", ""))
names(db_invariance)
names(db_long)
4 Models
4.1 Configural
# Set up model CFA
configural_model_smt <- ('
percmerit1 =~ perceffort1 + perctalent1
percmerit2 =~ perceffort2 + perctalent2
percnmerit1 =~ percrichparents1 + perccontact1
percnmerit2 =~ percrichparents2 + perccontact2
prefmerit1 =~ prefeffort1 + preftalent1
prefmerit2 =~ prefeffort2 + preftalent2
prefnmerit1 =~ prefrichparents1 + prefcontact1
prefnmerit2 =~ prefrichparents2 + prefcontact2')
#Configural Model
model_configural <- ('
# Medición en T1
percmerit1 =~ perceffort1 + perctalent1
percnmerit1 =~ percrichparents1 + perccontact1
prefmerit1 =~ prefeffort1 + preftalent1
prefnmerit1 =~ prefrichparents1 + prefcontact1
# Medición en T2
percmerit2 =~ perceffort2 + perctalent2
percnmerit2 =~ percrichparents2 + perccontact2
prefmerit2 =~ prefeffort2 + preftalent2
prefnmerit2 =~ prefrichparents2 + prefcontact2
')
fit_configural <- cfa(model_configural, data = db_invariance, std.lv = TRUE, meanstructure = TRUE)
#Usar la opción std.lv = TRUE en modelos de ecuaciones estructurales o análisis factorial confirmatorio implica cambiar la forma en que identificamos los factores latentes. En lugar de fijar una de las cargas factoriales a 1 —lo que equivale a usar ese ítem como “regla” para escalar el factor—, se opta por fijar la varianza de la variable latente en 1 y permitir que todas las cargas se estimen libremente. Esta decisión tiene varias ventajas importantes, especialmente cuando se trabaja con modelos longitudinales o con análisis de invarianza factorial.
#Primero, usar std.lv = TRUE garantiza que todas las cargas sean comparables entre sí, ya que ninguna está restringida arbitrariamente. Esto es relevante en análisis donde queremos examinar si la estructura factorial se mantiene estable en el tiempo o entre grupos, pues evita dar un peso privilegiado a un ítem específico. Segundo, facilita la interpretación de los parámetros, ya que al fijar la varianza de los factores a 1, cada carga expresa directamente la relación estándar (es decir, la contribución en unidades de desviación estándar del ítem al factor), sin depender de la escala arbitraria inducida por un ítem marcador.
#Además, cuando se busca evaluar la invarianza métrica, escalar o estricta, std.lv = TRUE permite aplicar restricciones de igualdad en las cargas e interceptos de forma simétrica y clara. Por contraste, si se usa el método de identificación por marcador (una carga fija), entonces la carga fijada no se puede restringir entre grupos o tiempos, lo que limita la capacidad para probar igualdad completa. Por esta razón, los procedimientos automatizados como measEq.syntax() del paquete semTools usan por defecto este tipo de identificación, justamente para garantizar flexibilidad y simetría en los modelos de invarianza.
#Finalmente, desde una perspectiva estadística, la identificación mediante varianza del factor fija (std.lv) es más coherente cuando los factores representan construcciones latentes homogéneas. A diferencia de fijar una carga (lo cual implica que ese ítem define el factor), fijar la varianza del factor implica que el constructo se mide en una escala fija y abstracta, lo que es más neutral teóricamente. Esto es especialmente importante si el ítem marcador cambia de comportamiento o fiabilidad entre grupos o tiempos: al no depender de un ítem particular, evitamos distorsiones en la escala del factor.
#En resumen, std.lv = TRUE ofrece ventajas tanto prácticas como teóricas: evita sesgos en la identificación, permite comparaciones limpias entre grupos o momentos, y proporciona estimaciones más estables y fáciles de interpretar en modelos complejos, como los de invarianza longitudinal.
summary(fit_configural, fit.measures = TRUE, standardized = TRUE)
#To see the internal representation of the model (this helps understand the model definition)
inspect(fit_configural,what="list")
4.2 Metric (Weak)
# Equal Loadings
model_metric <- ('
percmerit1 =~ v1*perceffort1 + v2*perctalent1
percmerit2 =~ v1*perceffort2 + v2*perctalent2
percnmerit1 =~ v3*percrichparents1 + v4*perccontact1
percnmerit2 =~ v3*percrichparents2 + v4*perccontact2
prefmerit1 =~ v5*prefeffort1 + v6*preftalent1
prefmerit2 =~ v5*prefeffort2 + v6*preftalent2
prefnmerit1 =~ v7*prefrichparents1 + v8*prefcontact1
prefnmerit2 =~ v7*prefrichparents2 + v8*prefcontact2
')
fit_metric <- cfa(model_metric, data = db_invariance, std.lv = TRUE, meanstructure = TRUE)
summary(fit_metric, fit.measures = TRUE, standardized = TRUE)
4.3 Scalar (Strong)
# Equal Loadings and intercepts
model_scalar <- ('
# Igualdad de cargas
percmerit1 =~ v1*perceffort1 + v2*perctalent1
percmerit2 =~ v1*perceffort2 + v2*perctalent2
percnmerit1 =~ v3*percrichparents1 + v4*perccontact1
percnmerit2 =~ v3*percrichparents2 + v4*perccontact2
prefmerit1 =~ v5*prefeffort1 + v6*preftalent1
prefmerit2 =~ v5*prefeffort2 + v6*preftalent2
prefnmerit1 =~ v7*prefrichparents1 + v8*prefcontact1
prefnmerit2 =~ v7*prefrichparents2 + v8*prefcontact2
# Igualdad de interceptos
perceffort1 ~ int1*1
perceffort2 ~ int1*1
perctalent1 ~ int2*1
perctalent2 ~ int2*1
percrichparents1 ~ int3*1
percrichparents2 ~ int3*1
perccontact1 ~ int4*1
perccontact2 ~ int4*1
prefeffort1 ~ int5*1
prefeffort2 ~ int5*1
preftalent1 ~ int6*1
preftalent2 ~ int6*1
prefrichparents1 ~ int7*1
prefrichparents2 ~ int7*1
prefcontact1 ~ int8*1
prefcontact2 ~ int8*1
')
fit_scalar <- cfa(model_scalar, data = db_invariance, std.lv = TRUE, meanstructure = TRUE)
summary(fit_scalar, fit.measures = TRUE, standardized = TRUE)
4.4 Strict
# Equal Loadings, intercepts and variance
model_strict <- paste0(model_scalar, '
perceffort1 ~~ e1*perceffort1
perceffort2 ~~ e1*perceffort2
perctalent1 ~~ e2*perctalent1
perctalent2 ~~ e2*perctalent2
percrichparents1 ~~ e3*percrichparents1
percrichparents2 ~~ e3*percrichparents2
perccontact1 ~~ e4*perccontact1
perccontact2 ~~ e4*perccontact2
prefeffort1 ~~ e5*prefeffort1
prefeffort2 ~~ e5*prefeffort2
preftalent1 ~~ e6*preftalent1
preftalent2 ~~ e6*preftalent2
prefrichparents1 ~~ e7*prefrichparents1
prefrichparents2 ~~ e7*prefrichparents2
prefcontact1 ~~ e8*prefcontact1
prefcontact2 ~~ e8*prefcontact2
')
fit_strict <- cfa(model_strict, data = db_invariance, std.lv = TRUE, meanstructure = TRUE)
summary(fit_strict, fit.measures = TRUE, standardized = TRUE)
5 Comparative Table
# Compare ajust
tab01 <- lavTestLRT(fit_configural, fit_metric, fit_scalar, fit_strict) %>%
dplyr::as_tibble() %>%
dplyr::select("Chisq", "Df", "chisq_diff" = `Chisq diff`, "df_diff" = `Df diff`, "pvalue" = `Pr(>Chisq)`) %>%
dplyr::mutate(
stars = gtools::stars.pval(pvalue),
chisqt = paste0(round(Chisq, 2), " (", Df, ") "),
decision = ifelse(pvalue > 0.05, yes = "Accept", no = "Reject"),
model = c("Configural", "Weak", "Strong", "Strict")
)
fit.meas <- dplyr::bind_rows(
lavaan::fitmeasures(fit_configural, output = "matrix")[c("chisq", "df", "cfi", "rmsea", "rmsea.ci.lower", "rmsea.ci.upper"), ],
lavaan::fitmeasures(fit_metric, output = "matrix")[c("chisq", "df", "cfi", "rmsea", "rmsea.ci.lower", "rmsea.ci.upper"), ],
lavaan::fitmeasures(fit_scalar, output = "matrix")[c("chisq", "df", "cfi", "rmsea", "rmsea.ci.lower", "rmsea.ci.upper"), ],
lavaan::fitmeasures(fit_strict, output = "matrix")[c("chisq", "df", "cfi", "rmsea", "rmsea.ci.lower", "rmsea.ci.upper"), ]
)
fit.meas <- fit.meas %>%
dplyr::mutate(
diff.chi2 = chisq - lag(chisq, default = dplyr::first(chisq)),
diff.df = df - lag(df, default = dplyr::first(df)),
diff.cfi = cfi - lag(cfi, default = dplyr::first(cfi)),
diff.rmsea = rmsea - lag(rmsea, default = dplyr::first(rmsea))
) %>%
round(3) %>%
dplyr::mutate(rmsea.ci = paste0(rmsea, " \n ", "(", rmsea.ci.lower, "-", rmsea.ci.upper, ")"))
tab.inv <- dplyr::bind_cols(tab01, fit.meas) %>%
dplyr::select(model, chisqt, cfi, rmsea.ci, diff.chi2, diff.df, diff.cfi, diff.rmsea, stars, decision) %>%
dplyr::mutate(diff.chi2 = paste0(diff.chi2, " (", diff.df, ") ", stars)) %>%
dplyr::select(model, chisqt, cfi, rmsea.ci, diff.chi2, diff.cfi, diff.rmsea, decision)
# clean values
tab.inv[tab.inv == c("0 (0) ")] <- NA
tab.inv[tab.inv == c(0)] <- NA
col.nam <- c(
"Model", "χ^2 (df)", "CFI", "RMSEA (90 CI)",
"Δ χ^2 (Δ df)", "Δ CFI", "Δ RMSEA", "Decision"
)
footnote <- paste0("N = ", fit_configural@Data@nobs[[1]])
tab.inv %>%
kableExtra::kable(
format = "html",
align = "c",
booktabs = T,
escape = F,
caption = NULL,
col.names = col.nam
) %>%
kableExtra::kable_styling(
full_width = T,
latex_options = "hold_position",
bootstrap_options = c("striped", "bordered", "condensed"),
font_size = 23
) %>%
kableExtra::column_spec(c(1, 8), width = "3.5cm") %>%
kableExtra::column_spec(2:7, width = "4cm") %>%
kableExtra::column_spec(4, width = "5cm")
Model | χ^2 (df) | CFI | RMSEA (90 CI) | Δ χ^2 (Δ df) | Δ CFI | Δ RMSEA | Decision |
---|---|---|---|---|---|---|---|
Configural | 207.84 (76) | 0.930 | 0.054 (0.045-0.063) | ||||
Weak | 220.88 (84) | 0.927 | 0.053 (0.044-0.061) | 13.043 (8) | -0.003 | -0.002 | Accept |
Strong | 234.36 (92) | 0.924 | 0.051 (0.043-0.059) | 13.479 (8) . | -0.003 | -0.001 | Accept |
Strict | 245.88 (100) | 0.922 | 0.05 (0.042-0.058) | 11.52 (8) | -0.002 | -0.001 | Accept |