Measurement invariance: comparing groups with multigroup CFA
What this is for
In a Dutch community sample, women report a little more anxiety than men on the DASS-21 (0.16 SD, as we’ll see). Is that a difference in anxiety, or a difference in how the items work for women and men? The scores alone can’t say. Factor analysis II fitted a factor model to one sample. This lesson fits it to two groups at once and asks, one constraint at a time, whether the same model holds in both: measurement invariance. Where it holds, the groups’ factor means can be compared. Where an item breaks it, we find the item and decide what to do about it. We start with a scale where invariance holds, the DASS-21 across women and men, and then turn to one where a single item doesn’t: crying on the CES-D.
Goals
By the end of this lesson you should be able to:
- fit a multigroup CFA in
lavaanand say how the two groups are put on one scale; - climb the ladder (configural, metric, scalar) with scaled difference tests and changes in CFI, and say which comparison each step supports;
- find the constraint that fails with score tests, and fit and report a partial invariance model;
- say what one unequal intercept does to a comparison of sum scores and of factor means;
- say what the choice of estimator for a multigroup fit assumes, and what ordered categories change.
Core ideas
One model, two groups
A multigroup CFA fits the model in several groups at once, with any parameter free in each group or held equal across them (Jöreskog, 1971). With intercepts, the model for item \(i\) in group \(g\) is \(x_i = \nu_{ig} + \lambda_{ig}\eta + \varepsilon_i\). Measurement invariance means that responses depend on the factor and not on the group (Meredith, 1993). It is tested in steps, each adding constraints to the one before (Vandenberg & Lance, 2000; Millsap, 2011; Putnick & Bornstein, 2016):
- configural: the same items load on the same factors in every group, every parameter free;
- metric: the loadings \(\lambda_i\) are equal, so a unit of the factor means the same in each group;
- scalar: the intercepts \(\nu_i\) (for ordered items, the thresholds) are equal too, so respondents at the same factor level have the same expected response.
Meredith adds a fourth step, strict invariance, with equal unique variances as well.
Why these comparisons at these steps? A factor has no unit and no origin of its own. In the configural model each group’s factor is scaled separately, so nothing links them. Once the loadings are equal, the items carry one unit across groups, and group B’s factor variance, and its correlations and regressions, can be estimated relative to group A’s. Once the intercepts are equal too, the items carry one origin, and group B’s factor mean can be estimated relative to A’s; lavaan frees it at that step. Comparing correlations or regressions needs metric invariance; comparing means needs scalar.
In item response language, an unequal loading is non-uniform differential item functioning and an unequal intercept is uniform DIF; Differential item functioning takes up the same question item by item, matching on an observed score instead of a factor.
What an unequal intercept does
Try it: one item that works differently
Four items measure one factor in groups A and B (loadings 0.7, intercepts 0, factor SD 1). Item 4 can behave differently in group B.
With both item-4 controls at 0 the lines coincide, and the sum-score gap (0.45 SD) is the latent 0.5 SD shrunk by unreliability. Set the true gap to 0 and item 4’s intercept to 0.3: the sum scores differ by 0.10 SD between groups that don’t differ at all. A loading difference makes the lines cross, so the item favours one group at low factor levels and the other at high ones.
What does the factor model make of the same item? If we impose scalar invariance anyway, the model has to explain item 4’s higher mean in group B with the one thing it has left free, group B’s factor mean. The shift is spread over every item, and the factor mean absorbs part of it. One response is partial invariance (Byrne, Shavelson & Muthén, 1989): free the offending item’s intercept and keep the others equal, so that the comparison of means rests on the items that do work alike.
Try it: what the scalar constraint does to the factor mean
Now \(k\) items load 0.7 in both groups, and the last item’s intercept is higher in group B by the amount you set. The widget computes the large-sample answer: group B’s estimated factor mean under full and under partial scalar invariance, and how often the test of the scalar step (on \(k - 1\) degrees of freedom) would catch the shifted item.
At the start, the groups don’t differ, yet the scalar model puts group B 0.107 SD above A. With equal loadings the error is the shift divided by \(k\lambda\) (problem 1), here \(0.3/(4 \times 0.7)\). The partial model recovers the truth. With 300 respondents per group, the scalar step fails 97% of the time; halve the shift and the error halves while the chance of catching it falls to 44%. Raise \(k\) to 12 and the error falls to 0.036: more invariant items dilute one shifted one. Move the true mean: the error stays the same size, whatever the groups’ real difference.
Testing the steps
Each step is nested in the one before, so a χ² difference tests it, scaled when the estimator is robust (Satorra & Bentler, 2001). As with any χ² test, a large enough sample rejects a step for differences too small to matter. So changes in fit indices are read alongside: Cheung and Rensvold (2002) proposed treating a drop in CFI of more than 0.01 as a failed step, and Chen (2007) paired it with changes in RMSEA and SRMR; Putnick and Bornstein (2016) survey how these criteria are used and questioned.
When a step fails, which constraint is responsible? A score test (lavTestScore() in lavaan) says, for each equality constraint, how much the χ² would fall if that constraint alone were released. The largest is the first suspect. But a scalar model with many items holds dozens of constraints, and a few small \(p\)-values turn up by chance.
Try it: how many flags by chance?
At 39 constraints and α = 0.01, a flag turns up by chance about a third of the time (0.32); at α = 0.05, 86% of the time. Score tests on one model aren’t independent, so these are rough guides, but they set the baseline for reading the largest test.
Choosing an estimator for multigroup fits
In a multigroup fit those assumptions apply group by group, and three things change. With ordered items each group gets its own thresholds, so every category of every item needs responses in each group; a category that is nearly empty in one group is a problem even when it isn’t in the pooled data (Rutkowski, Svetina & Liaw, 2019, who also show that collapsing categories changes fit). Which parameters must be fixed at each step to keep ordinal models identified and nested is subtler than with continuous items (Wu & Estabrook, 2016). And the difference tests behave differently across estimators: in simulations by Sass, Schmitt and Marsh (2014), their Type I error rates varied across ML, MLR and WLSMV, and changes in fit indices under WLSMV needed caution in misspecified models. With missing ordinal responses, WLSMV with pairwise deletion was the one method in the simulations of Chen, Wu, Garnier-Villarreal, Kite and Jia (2020) whose difference tests did not reasonably control their Type I error rate; the full-information ordinal models recovered loadings best.
With sparse categories, then, one option is MLR for the ladder, items as numbers, with an ordinal fit (on collapsed categories if need be) as a check. The two rest on different assumptions about the responses; where they agree, less rests on the choice.
Simulate
The code simulates 500 respondents in each of two groups answering six items that measure one factor, with group B’s factor mean 0.3 SD higher and item 6’s intercept 0.4 higher. It climbs the ladder with lavaan, runs the score tests on the scalar model, frees the item with the largest one, and compares group B’s estimated factor mean with the truth. The first run installs lavaan, which takes a little while; after that, each run takes a few seconds.
With the seed as written, configural to metric passes (\(p = 0.31\)) and the scalar step fails (Δχ² = 42.3 on 5 df); item 6’s intercept has by far the largest score test (40.5); and group B’s mean comes out at 0.377 under full scalar invariance and 0.319 with item 6 free (truth 0.3). Set shift <- 0 and the scalar step should pass; set n <- 150 and see how often the test still finds item 6 across a few seeds.
With real data
The DASS-21: women and men
Our main example returns to neurodegenerative_huizinga_2019_dass, the Dutch DASS-21 answered by a community sample recruited by Huizinga et al. (2020), with the three-factor model of Factor analysis II (Lovibond & Lovibond, 1995). Its job is to show the ladder where it holds. Download the code to run every chunk.
Code
library(lavaan)
# Each IRW table has a landing page (itemresponsewarehouse.org/tables/<name>/)
# with a CSV download. This link is pinned to one version of the data.
dass_url <- "https://redivis.com/api/v1/tables/datapages.item_response_warehouse:v59_0.neurodegenerative_huizinga_2019_dass/rows?format=csv"
df <- read.csv(dass_url)
df$resp <- suppressWarnings(as.numeric(df$resp)) # missing responses arrive as text
wide <- tapply(df$resp, list(df$id, df$item), function(x) x[1])
dass <- as.data.frame(wide[, paste0("DASS.", 1:21)])
names(dass) <- paste0("d", 1:21)
dass$sex <- df$cov_sex[match(rownames(dass), df$id)]
table(sex = dass$sex) # all 1,461 respondentssex
1 2
676 785
Code
dass <- dass[complete.cases(dass), ] # 215 respondents skipped all 21 items; no one skipped some
table(sex = dass$sex) # the 1,246 who answeredsex
1 2
577 669
Code
# 0 = did not apply to me at all ... 3 = applied to me very much. Every item is
# worded toward more distress, so no keying is needed. Scales from the DASS
# scoring template (Lovibond & Lovibond, 1995).
dep <- paste0("d", c(3, 5, 10, 13, 16, 17, 21))
anx <- paste0("d", c(2, 4, 7, 9, 15, 19, 20))
stress <- paste0("d", c(1, 6, 8, 11, 12, 14, 18))
m3 <- paste("dep =~", paste(dep, collapse = " + "), "\n",
"anx =~", paste(anx, collapse = " + "), "\n",
"str =~", paste(stress, collapse = " + "))The table’s cov_sex codes are unlabelled in the processing script. The paper reports 785 women among its 1,461 participants (its Table 1), and code 2 holds exactly 785 of the table’s 1,461 respondents, so we read 2 as women: a match of counts, not a codebook.
Which estimator? For the single-group structure we used WLSMV: four categories, piled at zero. Group by group, the top category is thin.
Code
# The estimator choice, group by group: with ordered items, every category of
# every item needs responses in each group, since each group gets thresholds.
top <- sapply(split(dass[1:21], dass$sex), function(g) sapply(g, function(x) sum(x == 3)))
table(responses_at_3 = top) # items by how many responses sit in the top categoryresponses_at_3
1 2 3 4 5 6 7 8 11 12
5 6 7 7 3 8 3 1 1 1
Code
colSums(top == 0 | top == 1) # items per group with 0 or 1 responses at 31 2
4 1
Every item’s top category holds between 1 and 12 responses in each group, and five items (four among men, one among women) hold a single one. Thresholds resting on one response per group are fragile, so we climb the ladder with MLR, items as numbers, and fit WLSMV as the check.
Code
# Multigroup CFA by cov_sex, three-factor model, items treated as numbers (MLR).
# Configural: same pattern in both groups. Metric: loadings equal. Scalar: loadings
# and intercepts equal; lavaan then frees group 2's factor means.
cfg <- cfa(m3, data = dass, group = "sex", estimator = "MLR")
met <- cfa(m3, data = dass, group = "sex", estimator = "MLR", group.equal = "loadings")
sca <- cfa(m3, data = dass, group = "sex", estimator = "MLR",
group.equal = c("loadings", "intercepts"))
idx_r <- c("chisq.scaled", "df", "cfi.robust", "rmsea.robust", "srmr")
round(rbind(configural = fitMeasures(cfg, idx_r), metric = fitMeasures(met, idx_r),
scalar = fitMeasures(sca, idx_r)), 3) chisq.scaled df cfi.robust rmsea.robust srmr
configural 874.695 372 0.931 0.062 0.044
metric 881.133 390 0.932 0.060 0.048
scalar 912.661 408 0.931 0.059 0.048
Code
lavTestLRT(cfg, met, sca) # scaled (Satorra-Bentler) differences| Df | AIC | BIC | Chisq | Chisq diff | RMSEA | Df diff | Pr(>Chisq) | |
|---|---|---|---|---|---|---|---|---|
| cfg | 372 | 29902.65 | 30579.51 | 1548.076 | NA | NA | NA | NA |
| met | 390 | 29896.78 | 30481.34 | 1578.205 | 13.50787 | 0.0000000 | 18 | 0.7605629 |
| sca | 408 | 29885.31 | 30377.57 | 1602.735 | 24.58263 | 0.0242018 | 18 | 0.1368512 |
Equal loadings cost almost nothing (a scaled Δχ² of 13.5 on 18 df, \(p = 0.76\)), and neither do equal intercepts (24.6 on 18 df, \(p = 0.14\)). Robust CFI is 0.931 or 0.932 at every step, far inside the 0.01 criterion. The configural fit is modest, but the question at each step is whether the constraints make it worse, and they don’t.
Code
# Which equality constraint strains most? One score test per constraint.
score_tests <- function(fit) {
st <- lavTestScore(fit)$uni
pt <- parTable(fit)
st$constraint <- sapply(st$lhs, function(l) with(pt[pt$plabel == l, ], paste(lhs, op, rhs)))
st[order(-st$X2.scaled), c("constraint", "X2.scaled", "df", "p.value.scaled")]
}
st <- score_tests(sca)
nrow(st)[1] 39
Code
head(st, 3)| constraint | X2.scaled | df | p.value.scaled | |
|---|---|---|---|---|
| 23 | d16 ~1 | 6.921176 | 1 | 0.0085181 |
| 2 | dep =~ d10 | 3.764150 | 1 | 0.0523625 |
| 38 | d14 ~1 | 2.684030 | 1 | 0.1013589 |
The constraint under most strain is item 16’s intercept (“I was unable to become enthusiastic about anything”, in the wording of the English DASS-21; Lovibond & Lovibond, 1995), with a score test of 6.9 on 1 df. Its \(p\)-value is 0.009, and among 39 tests one that small turns up by chance about three times in ten (\(1 - 0.991^{39} \approx 0.30\), the widget’s baseline).
Code
# Group 2's factor means under scalar invariance, in units of group 1's factor SD.
latent_means <- function(fit, factors) {
pe <- parameterEstimates(fit)
mu2 <- pe[pe$op == "~1" & pe$lhs %in% factors & pe$group == 2, c("lhs", "est", "se")]
sd1 <- sqrt(diag(lavInspect(fit, "est")[[1]]$psi))
data.frame(factor = mu2$lhs, mean_diff = round(mu2$est, 3), se = round(mu2$se, 3),
in_group1_SDs = round(mu2$est / sd1[mu2$lhs], 2))
}
latent_means(sca, c("dep", "anx", "str"))| factor | mean_diff | se | in_group1_SDs | |
|---|---|---|---|---|
| dep | dep | 0.036 | 0.024 | 0.09 |
| anx | anx | 0.039 | 0.016 | 0.16 |
| str | str | 0.056 | 0.020 | 0.18 |
With scalar invariance the factor means can be compared: women sit 0.16 SD above men on anxiety, 0.18 SD on stress, and 0.09 SD on depression.
Code
# The same ladder with the items as ordered categories (WLSMV): the thresholds play
# the intercepts' part. (lavaan may warn about the parameters' covariance matrix:
# some top categories hold one response in a group.)
cfg_o <- cfa(m3, data = dass, group = "sex", ordered = TRUE)
met_o <- cfa(m3, data = dass, group = "sex", ordered = TRUE, group.equal = "loadings")
sca_o <- cfa(m3, data = dass, group = "sex", ordered = TRUE,
group.equal = c("loadings", "thresholds", "intercepts"))
idx_o <- c("df", "cfi.scaled", "rmsea.scaled", "srmr")
round(rbind(configural = fitMeasures(cfg_o, idx_o), metric = fitMeasures(met_o, idx_o),
scalar = fitMeasures(sca_o, idx_o)), 3) df cfi.scaled rmsea.scaled srmr
configural 372 0.978 0.050 0.051
metric 390 0.987 0.037 0.053
scalar 429 0.983 0.040 0.051
Code
# Are these three models nested? Count what each one frees beyond the loadings and
# thresholds: the default scalar model frees 21 scale factors and 3 factor means
# that the default metric model fixes, so it is not the metric model plus constraints.
sapply(list(configural = cfg_o, metric = met_o, scalar = sca_o), function(f) {
pt <- parTable(f)
c(free_scale_factors = sum(pt$op == "~*~" & pt$free > 0),
free_factor_means = sum(pt$op == "~1" & pt$lhs %in% c("dep", "anx", "str") & pt$free > 0))
}) configural metric scalar
free_scale_factors 0 0 21
free_factor_means 0 0 3
As ordered categories, every step fits well (CFI 0.978 to 0.987, RMSEA at most 0.050). We read only the indices. lavaan’s default ordinal scalar model frees 21 scale factors and 3 factor means that its metric model fixes, so the two aren’t nested and their difference isn’t a test of the thresholds alone (problem 5). The DASS-21 passes.
The CES-D: where one item breaks
Our failure case is alexandrowicz_2018_cesd: the 20-item CES-D (Radloff, 1977), answered 0 to 3 by a sample of Austrian adults (Alexandrowicz, Jahn & Wancata, 2018). Its job is to show the ladder failing and what to do next.
Code
cesd_url <- "https://redivis.com/api/v1/tables/datapages.item_response_warehouse_4:v8_0.alexandrowicz_2018_cesd/rows?format=csv"
df <- read.csv(cesd_url)
wide <- tapply(df$resp, list(df$id, df$item), function(x) x[1])
items <- c(sprintf("ces_%02d", c(1, 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20)),
sprintf("cesr%02d", c(4, 8, 12, 16)))
cesd <- as.data.frame(wide[, items])
cesd$sex <- df$cov_sex[match(rownames(cesd), df$id)]
table(sex = cesd$sex) # all 514 respondentssex
1 2
251 263
Code
table(items_skipped = rowSums(is.na(cesd[items])))items_skipped
0 1 2
508 5 1
Code
cesd <- cesd[complete.cases(cesd), ]
table(sex = cesd$sex) # complete casessex
1 2
246 262
Code
# 0 = rarely or none of the time ... 3 = most or all of the time. The four positively
# worded items (cesr04, cesr08, cesr12, cesr16) arrive reversed, so higher = more
# depressive symptoms on every item: they correlate positively with the rest.
round(range(cor(cesd[items])[lower.tri(diag(20))]), 2)[1] -0.02 0.62
Code
top <- sapply(split(cesd[items], cesd$sex), function(g) sapply(g, function(x) sum(x == 3)))
colSums(top <= 1) # items per group with 0 or 1 responses in the top category1 2
7 5
Code
# Two factors: the 16 symptom items, and Radloff's positive-affect items.
pos <- sprintf("cesr%02d", c(4, 8, 12, 16))
m2 <- paste("dep =~", paste(setdiff(items, pos), collapse = " + "), "\n",
"pa =~", paste(pos, collapse = " + "))The paper reports 264 women among 518 respondents; the table has 263 respondents coded 2 of 514, so 2 is probably women, though nothing documents it. The four positively worded items arrive reversed: no inter-item correlation is below −0.02. Six respondents skipped an item or two, so listwise deletion costs about 1%. The model has two factors: the 16 symptom items, and the four items of Radloff’s positive-affect subscale, as Alexandrowicz et al. list them (their Table 1). The estimator: the top categories are sparser than the DASS’s (seven items among men and five among women have at most one response at 3), so MLR again, with an ordinal check.
Code
cfg_c <- cfa(m2, data = cesd, group = "sex", estimator = "MLR")
met_c <- cfa(m2, data = cesd, group = "sex", estimator = "MLR", group.equal = "loadings")
sca_c <- cfa(m2, data = cesd, group = "sex", estimator = "MLR",
group.equal = c("loadings", "intercepts"))
round(rbind(configural = fitMeasures(cfg_c, idx_r), metric = fitMeasures(met_c, idx_r),
scalar = fitMeasures(sca_c, idx_r)), 3) chisq.scaled df cfi.robust rmsea.robust srmr
configural 556.332 338 0.902 0.056 0.059
metric 625.794 356 0.873 0.062 0.090
scalar 663.147 374 0.865 0.063 0.092
Code
lavTestLRT(cfg_c, met_c, sca_c)| Df | AIC | BIC | Chisq | Chisq diff | RMSEA | Df diff | Pr(>Chisq) | |
|---|---|---|---|---|---|---|---|---|
| cfg_c | 338 | 15768.11 | 16284.23 | 695.6322 | NA | NA | NA | NA |
| met_c | 356 | 15855.50 | 16295.47 | 819.0141 | 51.30654 | 0.1323582 | 18 | 0.0000478 |
| sca_c | 374 | 15858.21 | 16222.04 | 857.7328 | 39.10169 | 0.0676033 | 18 | 0.0027614 |
The configural model fits modestly by the conventional cutoffs (robust CFI 0.902, below 0.95; RMSEA 0.056, under 0.06). Equal loadings don’t: a scaled Δχ² of 51.3 on 18 df (\(p < .001\)), and CFI drops by 0.029, three times the 0.01 criterion. The scalar step fails as well (39.1 on 18 df).
Code
st_met <- score_tests(met_c) # at the metric step: which loading?
nrow(st_met)[1] 18
Code
head(st_met, 3)| constraint | X2.scaled | df | p.value.scaled | |
|---|---|---|---|---|
| 12 | dep =~ ces_17 | 11.719253 | 1 | 0.0006186 |
| 3 | dep =~ ces_05 | 5.807588 | 1 | 0.0159572 |
| 1 | dep =~ ces_02 | 3.420608 | 1 | 0.0643873 |
The crying item’s loading has the largest score test, 11.7 on 1 df among 18, twice the next. Alexandrowicz et al. (2018) found DIF by gender on the same item with a Rasch model, which is some reassurance that it isn’t an accident of this model. We free it, then look at the intercepts.
Code
# Partial invariance: free the crying item's loading (metric), then its intercept too
# (scalar), keeping every other constraint.
met_p <- cfa(m2, data = cesd, group = "sex", estimator = "MLR", group.equal = "loadings",
group.partial = "dep =~ ces_17")
sca_p1 <- cfa(m2, data = cesd, group = "sex", estimator = "MLR",
group.equal = c("loadings", "intercepts"), group.partial = "dep =~ ces_17")
head(score_tests(sca_p1), 2) # at the scalar step, with the loading already free| constraint | X2.scaled | df | p.value.scaled | |
|---|---|---|---|---|
| 30 | ces_17 ~1 | 7.628321 | 1 | 0.0057459 |
| 27 | ces_13 ~1 | 6.125482 | 1 | 0.0133247 |
Code
sca_p <- cfa(m2, data = cesd, group = "sex", estimator = "MLR",
group.equal = c("loadings", "intercepts"),
group.partial = c("dep =~ ces_17", "ces_17 ~ 1"))
lavTestLRT(cfg_c, met_p)| Df | AIC | BIC | Chisq | Chisq diff | RMSEA | Df diff | Pr(>Chisq) | |
|---|---|---|---|---|---|---|---|---|
| cfg_c | 338 | 15768.11 | 16284.23 | 695.6322 | NA | NA | NA | NA |
| met_p | 355 | 15801.56 | 16245.76 | 763.0753 | 28.05027 | 0.0784414 | 17 | 0.0443543 |
Code
lavTestLRT(met_p, sca_p)| Df | AIC | BIC | Chisq | Chisq diff | RMSEA | Df diff | Pr(>Chisq) | |
|---|---|---|---|---|---|---|---|---|
| met_p | 355 | 15801.56 | 16245.76 | 763.0753 | NA | NA | NA | NA |
| sca_p | 372 | 15793.15 | 16165.43 | 788.6632 | 25.46969 | 0.0443913 | 17 | 0.0846794 |
Code
round(rbind(partial_metric = fitMeasures(met_p, idx_r), partial_scalar = fitMeasures(sca_p, idx_r)), 3) chisq.scaled df cfi.robust rmsea.robust srmr
partial_metric 584.44 355 0.892 0.058 0.077
partial_scalar 610.47 372 0.889 0.057 0.078
With the crying loading free, the metric step is borderline (28.1 on 17 df, \(p = 0.04\); CFI 0.892, a drop of 0.010). At the scalar step the crying intercept is again first (7.6). Freed as well, the partial scalar model holds against the partial metric one (25.5 on 17 df, \(p = 0.08\)).
Code
rbind(full_scalar = latent_means(sca_c, c("dep", "pa")), partial = latent_means(sca_p, c("dep", "pa")))| factor | mean_diff | se | in_group1_SDs | |
|---|---|---|---|---|
| full_scalar.dep | dep | 0.056 | 0.026 | 0.21 |
| full_scalar.pa | pa | -0.045 | 0.047 | -0.10 |
| partial.dep | dep | 0.051 | 0.026 | 0.18 |
| partial.pa | pa | -0.045 | 0.048 | -0.10 |
Code
# The crying item's loading and intercept in each group (partial model):
pe <- parameterEstimates(sca_p)
pe[(pe$op == "=~" & pe$rhs == "ces_17") | (pe$op == "~1" & pe$lhs == "ces_17"),
c("lhs", "op", "rhs", "group", "est", "se")]| lhs | op | rhs | group | est | se | |
|---|---|---|---|---|---|---|
| 13 | dep | =~ | ces_17 | 1 | 0.3248080 | 0.1572090 |
| 56 | ces_17 | ~1 | 1 | 0.0853659 | 0.0204093 | |
| 78 | dep | =~ | ces_17 | 2 | 1.2808958 | 0.2350493 |
| 121 | ces_17 | ~1 | 2 | 0.1829560 | 0.0335570 |
Does freeing the item change the comparison? Under full scalar invariance group 2 sits 0.21 SD above group 1 on the symptom factor; with the crying item free, 0.18 SD. Positive affect is 0.10 SD lower in group 2 either way. The crying item’s loading is 0.33 in group 1 and 1.28 in group 2 (in units of the first item’s loading), and its intercept 0.085 and 0.183: in group 2, crying goes with the other symptoms far more closely, and at the factor’s zero point it is reported more often.
Code
# As a check with an ordinal estimator: the top categories are too sparse to keep, so
# each item is scored as any symptom (1-3) against none (0), and the scalar model is
# fitted with WLSMV. Which constraints strain most?
cesd_bin <- cesd
cesd_bin[items] <- lapply(cesd_bin[items], function(x) as.integer(x > 0))
# With binary items, the theta parameterization fixes each latent response's
# residual variance at 1 in both groups, which keeps the scalar model identified
# (Wu & Estabrook, 2016); the group-2 factor means are free.
sca_b <- cfa(m2, data = cesd_bin, group = "sex", ordered = TRUE, parameterization = "theta",
group.equal = c("loadings", "thresholds", "intercepts"))
st_b <- lavTestScore(sca_b)$uni
pt <- parTable(sca_b)
st_b$constraint <- sapply(st_b$lhs, function(l) with(pt[pt$plabel == l, ], paste(lhs, op, rhs)))
head(st_b[order(-st_b$X2), c("constraint", "X2", "df", "p.value")], 3)| constraint | X2 | df | p.value | |
|---|---|---|---|---|
| 12 | dep =~ ces_17 | 33.69917 | 1 | 0.0000000 |
| 14 | dep =~ ces_19 | 18.22343 | 1 | 0.0000196 |
| 3 | dep =~ ces_05 | 10.92681 | 1 | 0.0009478 |
As a check with an ordinal estimator, on items scored as any symptom against none, the crying loading again has the largest score test (33.7).
What might a report say? One view drops the crying item, so that the scale is invariant as it stands. Another keeps it and frees its parameters, so that the comparison rests on the other 19 items while the item still counts toward each respondent’s score. A third ignores the failure because it is one item in twenty. The first changes the instrument, and its scores no longer match published norms; the third lets one item push the comparison, by 0.03 SD here; the second needs enough invariant items to carry the comparison. Which cost matters depends on what the comparison is for. Reporting it both ways, with the item and the reason named, lets a reader see how much rests on the one item: here the two give 0.21 and 0.18 SD, and the conclusion doesn’t change.
Problems
- The size of the error. Four items with equal loadings \(\lambda\) and unique variances \(\psi\) measure one factor in two groups, with item 4’s intercept higher by \(\delta\) in group B. (a) Show that the gap between the groups’ expected sum scores is \(4\lambda\kappa + \delta\), and express it in SDs of group A’s sum score, as the first widget does. (b) With \(k\) items, show that the scalar model’s population estimate of group B’s mean is \(\kappa + \delta/(k\lambda)\). (Hint: with the covariances fitted exactly, the scalar model’s mean part is a generalized least-squares regression of the item mean differences on \(\lambda\).)
- A different grouping. Split the CES-D respondents at the median of
cov_ageand climb the ladder with the same two-factor model. Does the crying item stand out again? Which constraints do, and what would you do about them? - Significant but small. A scale is compared across two groups of 10,000 respondents each. The scalar step gives a scaled Δχ² of 180 on 20 df (\(p < .001\)) and a drop in CFI of 0.003. Is the scale invariant enough to compare means? What would you look at before deciding, and what would you report?
- Design. You want to compare DASS-21 scores across the education levels in
cov_education. Which level of invariance do you need to compare means, and which to compare the depression–anxiety correlation? Check the group sizes first: which estimator would you use, and what would you do if one item’s intercept failed in one group? - Nested or not? Using the code’s ordinal DASS fits, list what each of the three models fixes and frees (thresholds, scale factors, factor means and variances). Why is the default metric-to-scalar difference not a test of equal thresholds? Build a nested sequence as Wu and Estabrook (2016) propose (the
semToolsfunctionmeasEq.syntax()writes the syntax), test it, and compare the verdict with the MLR ladder. - Challenge (open). With thirty countries, Asparouhov and Muthén (2014) note, the scalar model tends to fit poorly and to throw up many large modification indices. They propose alignment, which estimates the countries’ factor means without requiring exact invariance. What does alignment assume in place of invariance, and how would you decide whether the means it gives are trustworthy enough to rank the countries? I don’t know of a settled answer.
Going further
- Meredith, W. (1993). Measurement invariance, factor analysis and factorial invariance. Psychometrika, 58(4), 525–543. https://doi.org/10.1007/BF02294825. The definitions, from weak to strict.
- Jöreskog, K. G. (1971). Simultaneous factor analysis in several populations. Psychometrika, 36(4), 409–426. https://doi.org/10.1007/BF02291366. The multigroup model and its tests.
- Vandenberg, R. J., & Lance, C. E. (2000). A review and synthesis of the measurement invariance literature. Organizational Research Methods, 3(1), 4–70. https://doi.org/10.1177/109442810031002. The ladder, step by step.
- Putnick, D. L., & Bornstein, M. H. (2016). Measurement invariance conventions and reporting: The state of the art and future directions for psychological research. Developmental Review, 41, 71–90. https://doi.org/10.1016/j.dr.2016.06.004. How invariance is tested and reported in practice, with the criteria for changes in fit.
- Byrne, B. M., Shavelson, R. J., & Muthén, B. (1989). Testing for the equivalence of factor covariance and mean structures: The issue of partial measurement invariance. Psychological Bulletin, 105(3), 456–466. https://doi.org/10.1037/0033-2909.105.3.456. Partial invariance.
- Cheung, G. W., & Rensvold, R. B. (2002). Evaluating goodness-of-fit indexes for testing measurement invariance. Structural Equation Modeling, 9(2), 233–255. https://doi.org/10.1207/s15328007sem0902_5. Where the change-in-CFI criterion comes from.
- Chen, F. F. (2007). Sensitivity of goodness of fit indexes to lack of measurement invariance. Structural Equation Modeling, 14(3), 464–504. https://doi.org/10.1080/10705510701301834. Which index notices which kind of non-invariance.
- Wu, H., & Estabrook, R. (2016). Identification of confirmatory factor analysis models of different levels of invariance for ordered categorical outcomes. Psychometrika, 81(4), 1014–1045. https://doi.org/10.1007/s11336-016-9506-0. Invariance with ordinal items, identified and nested (problem 5).
- Svetina, D., Rutkowski, L., & Rutkowski, D. (2020). Multiple-group invariance with categorical outcomes using updated guidelines: An illustration using Mplus and the lavaan/semTools packages. Structural Equation Modeling, 27(1), 111–130. https://doi.org/10.1080/10705511.2019.1602776. A worked tutorial for the ordinal ladder in R.
- Sass, D. A., Schmitt, T. A., & Marsh, H. W. (2014). Evaluating model fit with ordered categorical data within a measurement invariance framework: A comparison of estimators. Structural Equation Modeling, 21(2), 167–180. https://doi.org/10.1080/10705511.2014.882658. ML, MLR and WLSMV at each step.
- Chen, P.-Y., Wu, W., Garnier-Villarreal, M., Kite, B. A., & Jia, F. (2020). Testing measurement invariance with ordinal missing data: A comparison of estimators and missing data techniques. Multivariate Behavioral Research, 55(1), 87–101. https://doi.org/10.1080/00273171.2019.1608799. Missing ordinal responses in the ladder.
- Nye, C. D., & Drasgow, F. (2011). Effect size indices for analyses of measurement equivalence: Understanding the practical importance of differences between groups. Journal of Applied Psychology, 96(5), 966–980. https://doi.org/10.1037/a0022955. An effect size for non-invariance, for when “significant” isn’t the question (problem 3).
- Millsap, R. E. (2011). Statistical approaches to measurement invariance. Routledge. https://doi.org/10.4324/9780203821961. The full treatment, factor models and IRT together.
- The lavaan tutorial on multiple groups:
group.equal,group.partialand the tests.
Data sources
Data from the Item Response Warehouse (each table name links to its IRW page); references from IRW biblio.
neurodegenerative_huizinga_2019_dass: Huizinga, F., Heutink, J., de Haan, G. A., van der Lijn, I., van der Feen, F. E., Vrijling, A. C., … & Koerts, J. (2020). The development of the Screening of Visual Complaints questionnaire for patients with neurodegenerative disorders: Evaluation of psychometric features in a community sample. PLoS One, 15(4), e0232232. https://doi.org/10.1371/journal.pone.0232232. Licence: CC0 1.0.alexandrowicz_2018_cesd: Alexandrowicz, R.; Jahn, R.; Wancata, J. (2018). Assessing the dimensionality of the CES-D using multi-dimensional multi-level Rasch models. PLOS ONE, 13(5), e0197908. https://doi.org/10.1371/journal.pone.0197908. Licence: CC BY 4.0.
For instructors
- Source: new material. Split from Factor analysis II on 09-25 (Ben, #58), at a colleague’s suggestion that the factor-analysis lesson was too long; multigroup invariance had moved into that lesson from Differential item functioning on 09-24. The DASS example, the CES-D failure case and the first widget come from it; the estimator section is new.
- Timing: about 45 minutes for the core ideas and widgets; the real-data section is a 30-minute lab (the whole script runs in about a minute locally).
- Placement: after Factor analysis II, and before Fairness in testing and Differential item functioning in the first course, which return to invariance with item response models and observed-score matching. Structural equation modeling with lavaan uses it for multigroup SEM.
- Held back: invariance item by item (Differential item functioning); occasions and treatments as groupings (Measurement invariance under treatment and life events); many groups and alignment (problem 6 only); the effect-size view of non-invariance (in Going further).
- Data: the DASS is in the public domain (DASS FAQ, no. 3), so one item is quoted; CES-D items are named by their content, not quoted. Which
cov_sexcode is women rests on counts matching each paper, not on a codebook. The CES-D’s positive-affect items are identified from Alexandrowicz et al. (2018, Table 1). The ladders are fitted with MLR, items as numbers, because some top categories hold one response or none in a group; the ordinal fits are checks. - Solutions: held back in
solutions/measurement-invariance.qmd(not published).