--- title: "Measurement Invariance with nomologR" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Measurement Invariance with nomologR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} library(nomologR) knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE, fig.width = 7, fig.height = 4.5 ) ``` # What invariance is for Measurement invariance asks whether a construct is measured comparably across groups or occasions. If it is not, a difference in observed scores can reflect how an item functions in each group rather than a difference in the construct itself. `nomologR` treats invariance as **graded evidence** rather than a sequence of automatic PASS/FAIL decisions. Every fitted level retains: - the generated `semTools::measEq.syntax()` object and the exact lavaan syntax; - the fitted lavaan object; - absolute fit and change in fit; - nested-model comparison evidence where available; - localized equality-constraint diagnostics; - researcher-specified partial-invariance provenance. # A teaching example with a known answer `nomo_demo_network` contains 800 responses collected in two administration groups, `online` and `paper`. Its population model builds in two features: 1. the latent **Agency mean is .25 SD higher** in the `paper` group (a real construct difference); and 2. the **intercept of item `ag3` is .50 higher** in the `paper` group (item bias: `ag3` is endorsed more on paper at the same level of Agency). All loadings are equal across groups. ```{r observed-means} aggregate( cbind(ag1, ag2, ag3, ag4) ~ group, data = nomo_demo_network, FUN = mean ) ``` Every item mean is higher in the `paper` group, but `ag3` is higher by much more. Observed means alone cannot separate the real latent difference from the item-level bias; that is the job of the invariance sequence. # Continuous indicators ```{r continuous} agency_model <- "Agency =~ ag1 + ag2 + ag3 + ag4" inv <- nomo_invariance( agency_model, data = nomo_demo_network, group = "group" ) inv ``` The conventional continuous sequence is: ```text configural -> metric -> scalar -> strict ``` ```{r fit-table} fit <- nomo_table(inv, "fit") fit[, c("level", "constraints", "cfi", "rmsea", "srmr", "delta_cfi", "delta_rmsea", "lrt_p")] ``` ```{r fit-values, include=FALSE} scalar_row <- fit[fit$level == "scalar", ] ``` The configural and metric models fit closely, consistent with equal loadings. Adding intercept equality at the scalar level changes CFI by `r sprintf("%.3f", scalar_row$delta_cfi)` and RMSEA by `r sprintf("%.3f", scalar_row$delta_rmsea)`, and the likelihood-ratio test is very small (p = `r format.pval(scalar_row$lrt_p, digits = 2)`). No single delta-CFI, delta-RMSEA, delta-SRMR, or chi-square rule is treated as universally decisive; the evidence is read together, in context. ```{r change-plot} plot(inv, type = "change") ``` # Localizing strain Global change in fit says *that* a set of constraints is strained. Score diagnostics help locate *where*. ```{r local-strain} strain <- nomo_table(inv, "local_strain") strain[, c("level", "constraint_display", "score_x2", "df", "p_value")] ``` ```{r local-plot} plot(inv, type = "local_strain") ``` ```{r strain-values, include=FALSE} scalar_strain <- strain[strain$level == "scalar", ] ``` The largest scalar-level diagnostic is **`r scalar_strain$constraint_display[[1]]`** (score chi-square `r sprintf("%.1f", scalar_strain$score_x2[[1]])`), which matches the item bias built into the data. Two cautions matter for real research: - Score diagnostics **never authorize an automatic parameter release**. With many constraints, some diagnostics will look notable by chance; look at the smaller metric-level values above for an example. - In real data you do not know the population model. A release chosen only because its diagnostic was largest is a post-hoc decision and should be reported as one. # Researcher-controlled partial invariance ```{r partial} release <- nomo_partial( level = "scalar", syntax = "ag3 ~ 1", rationale = paste( "The ag3 wording was expected to read differently on paper forms;", "this expectation was documented before the analysis." ) ) inv_partial <- nomo_invariance( agency_model, data = nomo_demo_network, group = "group", partial = release ) nomo_table(inv_partial, "partial") fit_partial <- nomo_table(inv_partial, "fit") fit_partial[, c("level", "partial_requested", "cfi", "rmsea", "delta_cfi", "lrt_p")] ``` With the `ag3` intercept released, the remaining intercept equalities are consistent with the data, and the latent mean difference can be interpreted using the three invariant intercepts. The release is carried forward to the strict level so that a parameter freed at one level is not silently constrained again later. `nomologR` records what was released and why. It does not search until a model meets a preferred cutoff. # Ordered indicators Ordered indicators require identification-aware sequences rather than merely substituting WLSMV for ML. For four-or-more-category ordered indicators: ```text configural -> thresholds -> metric -> scalar -> strict ``` For three-category indicators, threshold equality is part of the metric step because it is needed for identification. For binary indicators, threshold, loading, and intercept restrictions cannot be treated as independent sequential tests under the implemented Wu–Estabrook approach, so the sequence begins: ```text configural -> strong -> strict ``` where `strong` imposes thresholds, loadings, and intercepts together. The example below uses four five-category items from `nomo_demo_ordinal`. The two "forms" are assigned by alternating rows purely to demonstrate the ordered workflow; no non-invariance was built into these groups. ```{r ordered} ordinal_items <- nomo_demo_ordinal[, c("a1", "a2", "a3", "a4")] ordinal_items$form <- rep(c("form_1", "form_2"), length.out = nrow(ordinal_items)) inv_ord <- nomo_invariance( "A =~ a1 + a2 + a3 + a4", data = ordinal_items, group = "form", ordered = c("a1", "a2", "a3", "a4") ) nomo_table(inv_ord, "categories") fit_ord <- nomo_table(inv_ord, "fit") fit_ord[, c("level", "constraints", "cfi", "rmsea", "delta_cfi")] ``` # Tables and figures ```{r tables} plot(inv, type = "fit") ``` `nomo_table()` returns ordinary tibbles (`"fit"`, `"categories"`, `"partial"`, `"local_strain"`, `"decision_log"`), and plots are ordinary ggplot objects, so researchers keep direct control over reporting. The same evidence flows into `nomo_report()` when invariance is part of a guided workflow. # Research basis Multiple-group factor analysis originates with Jöreskog (1971), and the configural–metric–scalar–strict hierarchy with Meredith (1993) and Vandenberg and Lance (2000). A decrease in CFI of .01 became a widely used rule after Cheung and Rensvold (2002); later work showed that sensitivity of change-in-fit indices depends on sample size and model context (Chen, 2007; Putnick & Bornstein, 2016), which is why `nomologR` reports several indices without a universal cutoff. Ordered-indicator sequences follow Wu and Estabrook (2016) as implemented in `semTools` (see also Svetina et al., 2020). Partial invariance follows Byrne, Shavelson, and Muthén (1989). Full references are in `?nomo_invariance` and the [research basis](research-basis.html) article.