Skip to content

Commit 1b5dabb

Browse files
committed
add measurement invariance example
1 parent 1932efe commit 1b5dabb

File tree

5 files changed

+2226
-0
lines changed

5 files changed

+2226
-0
lines changed

index.Rmd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ knitr::opts_chunk$set(echo = TRUE)
1111

1212
Unless otherwise noted, reports are by Clay Ford.
1313

14+
- [Measurement Invariance Demo](measurement_invariance_demo.html) (2025-06-26)
15+
Example from Chapter 4 of _Latent Variable Modeling Using R_ (Beaujean 2014), Latent Variable Models with Multiple Groups. The example examines if the structure of the WISC-III scale is the same in children with and without manic symptoms. [Book code](https://blogs.baylor.edu/rlatentvariable/sample-page/r-syntax/#Chapter_4_Latent_Variable_Models_with_Multiple_Groups)
16+
1417
- [Extracting data from UpSet Plot](extract_data_upset_plot.html) (2025-04-10)
1518
How to extract specific interactions from a data frame identified in an UpSet plot.
1619

index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,13 @@ <h3>Reports generated for various consultations for the UVA
358358
StatLab.</h3>
359359
<p>Unless otherwise noted, reports are by Clay Ford.</p>
360360
<ul>
361+
<li><p><a href="measurement_invariance_demo.html">Measurement Invariance
362+
Demo</a> (2025-06-26)<br />
363+
Example from Chapter 4 of <em>Latent Variable Modeling Using R</em>
364+
(Beaujean 2014), Latent Variable Models with Multiple Groups. The
365+
example examines if the structure of the WISC-III scale is the same in
366+
children with and without manic symptoms. <a href="https://blogs.baylor.edu/rlatentvariable/sample-page/r-syntax/#Chapter_4_Latent_Variable_Models_with_Multiple_Groups">Book
367+
code</a></p></li>
361368
<li><p><a href="extract_data_upset_plot.html">Extracting data from UpSet
362369
Plot</a> (2025-04-10)<br />
363370
How to extract specific interactions from a data frame identified in an

measurement_invariance_demo.Rmd

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
---
2+
title: "Example of Measurement Invariance"
3+
author: "Clay Ford"
4+
date: "2025-06-26"
5+
output: html_document
6+
---
7+
8+
```{r setup, include=FALSE}
9+
knitr::opts_chunk$set(echo = TRUE)
10+
```
11+
12+
This example comes from Chapter 4 of Beaujean (2014), Latent Variable Models with Multiple Groups. The example examines if the structure of the WISC-III scale is the same in children with and without manic symptoms. [Source](https://blogs.baylor.edu/rlatentvariable/sample-page/r-syntax/#Chapter_4_Latent_Variable_Models_with_Multiple_Groups)
13+
14+
Load the package we need.
15+
16+
```{r}
17+
library(lavaan)
18+
```
19+
20+
21+
## Read in data
22+
23+
The data is manually entered as covariance matrices. This is not normally how you would enter data in R. Notice we enter two separate covariance matrices: one for children with manic symptoms and those without.
24+
25+
```{r}
26+
# variable names
27+
wisc3.names <- c("Info", "Sim", "Vocab","Comp",
28+
"PicComp", "PicArr", "BlkDsgn", "ObjAsmb")
29+
30+
# covariance for group with manic symptoms
31+
manic.cov <- c(9.364, 7.777, 12.461, 6.422, 8.756, 10.112,
32+
5.669, 7.445, 6.797, 8.123, 3.048, 4.922,
33+
4.513, 4.116, 6.200, 3.505, 4.880, 4.899,
34+
5.178, 5.114, 15.603, 3.690, 5.440, 5.220,
35+
3.151, 3.587, 6.219, 11.223, 3.640, 4.641,
36+
4.877, 3.568, 3.819, 5.811, 6.501, 9.797)
37+
38+
# lavaan function to create a covariance matrix
39+
manic.cov <- lav_matrix_lower2full(manic.cov)
40+
41+
# means of the eight variables
42+
manic.means <- c(10.09, 12.07, 10.25, 9.96, 10.90, 11.24, 10.30, 10.44)
43+
44+
# label the covariances and means
45+
colnames(manic.cov) <- rownames(manic.cov) <- wisc3.names
46+
names(manic.means) <- wisc3.names
47+
48+
# preview the covariance matrix
49+
manic.cov
50+
```
51+
52+
53+
Now do the same for the children without manic symptoms.
54+
55+
```{r}
56+
# covariance for group without manic symptoms
57+
norming.cov <- c(9.610, 5.844, 8.410, 6.324, 6.264, 9.000,
58+
4.405, 4.457, 5.046, 8.410, 4.464, 4.547,
59+
4.512, 3.712, 10.240, 3.478, 2.967, 2.970,
60+
2.871, 3.802, 10.890, 5.270, 4.930, 4.080,
61+
3.254, 5.222, 3.590, 11.560, 4.297, 4.594,
62+
4.356, 3.158, 4.963, 3.594, 6.620, 10.890)
63+
norming.cov <- lav_matrix_lower2full(norming.cov)
64+
65+
# means
66+
norming.means <- c(10.10, 10.30, 9.80, 10.10, 10.10, 10.10, 9.90, 10.20)
67+
68+
# label the covariances and means
69+
colnames(norming.cov) <- rownames(norming.cov) <- wisc3.names
70+
names(norming.means) <- wisc3.names
71+
72+
# preview the covariance matrix
73+
norming.cov
74+
```
75+
76+
Finally, combine the covariance matrices, sample sizes, and means into single list objects.
77+
78+
```{r}
79+
combined.cov <- list(manic = manic.cov, norming = norming.cov)
80+
combined.n <- list(manic = 81, norming = 200)
81+
combined.means <- list(manic = manic.means, norming = norming.means)
82+
```
83+
84+
Now ready to specify the CFA model.
85+
86+
## CFA Model
87+
88+
The model below says there are two factors: Verbal-Comprehension (VC) and Visual-Spatial (VS). The model hypothesizes these two factors are influencing four variables each. The model also says we want to estimate the covariance between the two factors (last line).
89+
90+
```{r}
91+
wisc3.model <-'
92+
VC =~ Info + Sim + Vocab + Comp
93+
VS =~ PicComp + PicArr + BlkDsgn + ObjAsmb
94+
VC ~~ VS
95+
'
96+
```
97+
98+
We'll also define a vector of fit indices so we can easily request them after fitting a model.
99+
100+
```{r}
101+
# specify fit indices of interest
102+
fit.indices <- c("chisq", "df", "cfi", "rmsea", "srmr", "mfi")
103+
```
104+
105+
## Configural Invariance
106+
107+
This fits the same model to both groups and allows all parameters to be freely estimated. In other words, we're doing a CFA with each group. Notice in the summary output each group's parameters are different.
108+
109+
```{r}
110+
configural.fit <- cfa(wisc3.model,
111+
sample.cov = combined.cov,
112+
sample.nobs = combined.n,
113+
sample.mean = combined.means)
114+
summary(configural.fit)
115+
```
116+
117+
Fit indices look good.
118+
119+
- CFI = comparative fit index (0.95 or greater)
120+
- RMSEA = root mean square error of approximation (0.06 or lower)
121+
- SRMR = standardized root mean square residual (0.08 or below)
122+
- MFI = McDonald's Fit Index (0.95 or greater)
123+
124+
```{r}
125+
fitMeasures(configural.fit, fit.indices)
126+
```
127+
128+
And the residuals are small. Notice everything is smaller than 2. That's good.
129+
130+
```{r}
131+
residuals(configural.fit, type = "normalized")
132+
```
133+
134+
This is a good fitting model for _each group_.
135+
136+
## Weak Invariance
137+
138+
Now constrain loadings to be equal between groups. Same code as above with one additional argument: `group.equal = "loadings"`. Notice in the summary output that all the loadings (listed under Latent Variables) are equal in both groups.
139+
140+
```{r}
141+
weak.fit <- cfa(wisc3.model,
142+
sample.cov = combined.cov,
143+
sample.nobs = combined.n,
144+
sample.mean = combined.means,
145+
group.equal = "loadings")
146+
summary(weak.fit)
147+
```
148+
149+
According to fit measures this is also a good model.
150+
151+
```{r}
152+
fitMeasures(weak.fit, fit.indices)
153+
```
154+
155+
And the residuals look good as well, though they are getting bigger.
156+
157+
```{r}
158+
residuals(weak.fit, type = "normalized")
159+
```
160+
Finally, we could formally compare the models using a Chi-squared difference test (aka, Log-likelihood ratio test). We can do this with the `lavTestLRT()` function. The null hypothesis is no difference between the models. A small p-value provides evidence against this hypothesis and suggests a preference for the more complex model (ie, the model with fewer degrees of freedom). Below there is some evidence against the weak invariance model (p = 0.049), however AIC and BIC metrics suggest otherwise. Lower AIC/BIC values are better and I don't see any reason to reject the weak invariance model.
161+
162+
```{r}
163+
lavTestLRT(configural.fit, weak.fit)
164+
```
165+
166+
167+
This is also a good model. It appears the manifest (observed) variables for each group are influenced in the same way by the two factors.
168+
169+
## Strong Invariance
170+
171+
Now constrain both loadings and intercepts to be equal. Notice the `group.equal` argument now has a vector: `c("loadings", "intercepts")`. Notice in the summary output that all the loadings (Latent Variables) and Intercepts are equal in both groups.
172+
173+
```{r}
174+
strong.fit <- cfa(wisc3.model,
175+
sample.cov = combined.cov,
176+
sample.nobs = combined.n,
177+
sample.mean = combined.means,
178+
group.equal = c("loadings", "intercepts"))
179+
summary(strong.fit)
180+
```
181+
182+
The fit measures for this model are not so good.
183+
184+
```{r}
185+
fitMeasures(strong.fit, fit.indices)
186+
```
187+
188+
But the residuals are mostly OK. The only point of strain is the residual for the "Sim" mean (2.45) in the manic group.
189+
190+
```{r}
191+
residuals(strong.fit, type = "normalized")
192+
```
193+
194+
The Chi-squared difference test suggests we reject the strong invariance model for the weak invariance model based on the small p-value.
195+
196+
```{r}
197+
lavTestLRT(weak.fit, strong.fit)
198+
```
199+
200+
For sake of completeness, let's move to the next model.
201+
202+
## Strict Invariance
203+
204+
Now constrain loadings, intercepts and variances to be equal. Notice the `group.equal` argument has the vector: `c("loadings", "intercepts", "residuals")`. Notice in the summary output that all the loadings (Latent Variables), intercepts and variances are equal in both groups.
205+
206+
207+
```{r}
208+
strict.fit <- cfa(wisc3.model,
209+
sample.cov = combined.cov,
210+
sample.nobs = combined.n,
211+
sample.mean = combined.means,
212+
group.equal = c("loadings", "intercepts", "residuals"))
213+
summary(strict.fit)
214+
```
215+
216+
Fit measures are not too good. The CFI is too low and RMSEA is too high.
217+
218+
```{r}
219+
fitmeasures(strict.fit, fit.indices)
220+
```
221+
222+
However, the residuals look good for the most part. The only point of strain is the residual for the "PicCmp" variance (-2.829) in the manic group.
223+
224+
```{r}
225+
residuals(strict.fit, type = "normalized")
226+
```
227+
228+
The Chi-squared test favors the strong invariance model based on AIC and p-value, but the BIC actually points to the strict fit.
229+
230+
```{r}
231+
lavTestLRT(strong.fit, strict.fit)
232+
```
233+
234+
235+
236+
## Partial Strict Invariance
237+
238+
If we want, we can remove constraints for certain parameters using the `group.partial` argument. For example, to allow the variance for PicCmp to be estimated separately between the groups, add the line `group.partial = "PicComp ~~ PicComp"`. In the summary, notice under Variances that "PicComp" gets a separate estimate in each group.
239+
240+
```{r}
241+
strict.fit2 <- cfa(wisc3.model,
242+
sample.cov = combined.cov,
243+
sample.nobs = combined.n,
244+
sample.mean = combined.means,
245+
group.equal = c("loadings", "intercepts", "residuals"),
246+
group.partial = "PicComp ~~ PicComp")
247+
summary(strict.fit2)
248+
```
249+
250+
The fit measures are not great...
251+
252+
```{r}
253+
fitmeasures(strict.fit2, fit.indices)
254+
```
255+
256+
But the residuals look pretty good.
257+
258+
```{r}
259+
residuals(strict.fit2, type = "normalized")
260+
```
261+
262+
Obviously one has to wonder if we would arrive at the same conclusions using a new sample of data. We don't want to build a model that's too specific to our sample. That's would be overfitting.
263+
264+
## References
265+
266+
- Beaujean, A. A. (2014) _Latent Variable Modeling Using R_. Routledge.
267+
- Beaujean, A. A., Freeman, M. J., Youngstrom, E., & Carlson, G. (2012). The structure of cognitive abilities in youths with manic symptoms: a factorial invariance study. Assessment, 19(4), 462–471. <https://doi.org/10.1177/1073191111399037>
268+
- R Core Team (2025). _R: A Language and Environment for Statistical Computing_. R Foundation for Statistical Computing, Vienna, Austria. <https://www.R-project.org/>.
269+
- Rosseel, Y. (2012). lavaan: An R Package for Structural Equation Modeling. _Journal of Statistical Software_, 48(2), 1-36. <https://doi.org/10.18637/jss.v048.i02>

measurement_invariance_demo.html

Lines changed: 1449 additions & 0 deletions
Large diffs are not rendered by default.

normality.html

Lines changed: 498 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)