An equality constraint specifies that the unstandardized estimates for two parameters in a model must be equal. In essence, instead of fitting two free parameters, the program fits only one. Why might this be useful? Constraints are primarily useful to test specific hypotheses.
Let’s consider a couple of examples:
This is a path analysis in which we analyze men and women separately (dividing into subsamples) and examine whether an association parameter is equal between sexes.
grViz("
digraph sexdiff {
# a 'graph' statement
graph [overlap = true, fontsize = 12]
node [shape = box, fontname = Arial]
vgames_M; aggress_M; vgames_F; aggress_F;
{ rank = same; vgames_M; aggress_M }
{ rank = same; vgames_F; aggress_F }
# edges
vgames_M:s -> aggress_M:s [dir='both', label='a'];
vgames_F:s -> aggress_F:s [dir='both', label='b'];
}")
In other words, does \(a\) equal \(b\)? From a parameter estimation standpoint, if \(a\) and \(b\) are free, we are estimating two parameters. If we enforce equality, then we are estimating only one: \(a := b\). Therefore, in general, models with equality constraints are nested within models that lack such constraints. This holds true because a free model can capture the case when \(a\) and \(b\) are equal, even if both are free parameters.
This is a multiple-group analysis in which we have a covariance matrix for each country in three separate samples (each with \(N = 1000\)). The goal of the analysis is to check whether the factor loading for a specific item (“I worry a lot”) is equivalent across countries. One could imagine also testing a broader family of models in which we equate all loadings across countries to examine measurement invariance, discussed below.
In a four-factor model, we might be interested in whether the association of Anger and Sadness is equal in magnitude to the association of Happiness and Wellbeing? Remember that we constrain unstandardized estimates, so for interpretability, we might apply a unit variance identification (UVI) strategy so that the variances of each factor are on the same scale. Therefore, the covariances would also scale similarly.
There are many canonical examples of how constraints are used in the SEM literature. For example, in formal tests of measurement invariance, factor loadings are often equated to enforce similar associations between a factor and its indicators in different groups.
However, constraints are a more general tool for you as a researcher to test a hypothesis about the components of a SEM. Like modification indices, constraints should be well-founded conceptually, but are open to abuse or confusion. It is up to you as a scientist to decide whether testing a nested model in which some parameters are equated is a meaningful alternative account of the data. Simply observing that two parameters estimates are around 0.8 should not motivate you to test a model in which they are equated.
Comparing equality-constrained versus free (nested) models is a test of approximate equivalence. That is, the free model will, by definition, fit better because it has more free parameters. However, does if fit substantially (or statistically significantly) better? If the nested model comparison is not significantly different, it suggests that the model does not fit substantially worse with the addition of equality constraints. In general, the principle of parsimony should tilt our preference toward models that fit the data well using fewer parameters. Thus, if the constrained model is not inferior, it is often preferred.
The most common approach to examining the effect of constraints on fit is nested model comparison using a likelihood ratio test (LRT). This is also sometimes called a chi-square difference test; these tests are usually interchangeale. As a reminder, the model chi-square difference test is based on the difference of the model \(\chi^2\) for the constrained versus free models and the difference in degrees of freedom (constrained - free). The difference in \(\chi^2\) is itself \(\chi^2\) distributed with degrees of freedom equal to the difference between models.
If the LRT/\(\chi^2\) difference is significant (often \(p < .05\)), it indicates that the more constrained model fits significantly worse than the free model. But as we’ve discussed elsewhere, this test can become overpowered in large samples, so examining parameter differences in the free and constrained model is essential to get a sense of the practical differences. For example, if two factor loadings are 0.9 and 0.8 in the free case, but 0.86 in the constrained case, what would we do if the LRT had \(p = .02\)? How bad are residuals?
Note that the effect of constraints (or dropping paths, for that matter) can also be quantified using a Wald W statistic. This estimates the expected increase in the model \(\chi^2\) if the parameter constraints were to be introduced to a free model. This is asymptotically equivalent (i.e., as sample size increases) to the model chi-square difference test and the LRT under most scenarios. The practical advantage of a Wald test is that one can test the effect of constraints on a model without needing to refit the model or specify new syntax.
Likewise, the modification index for a given path estimates the change in model \(\chi^2\) if the path were included. It is based on the Lagrange multiplier test (LMT), which (in this context) is a test of whether a candidate omitted parameter is equal to zero. Remember that if we don’t specify a parameter in the model (e.g., a cross-loading), it is assumed to be zero. Although the LMT is usually computed on individual parameters (e.g., the table of modification indices), one can compute an LMT on a set of tests such as the hypothesis that all five potential cross-loadings are zero. See lavTestScore
for details.
If you’re curious about the relationships among these parameter equivalent testing approaches, see this: https://stats.idre.ucla.edu/other/mult-pkg/faq/general/faqhow-are-the-likelihood-ratio-wald-and-lagrange-multiplier-score-tests-different-andor-similar/.
Let’s return to our example from last week with Fear of Global Warming and Concern for Animal Welfare as examples that predict individual charitable contributions to environmental groups. For the moment, I’ve left out the donations part to focus only on the measurement model. What if we wanted to test the hypothesis that all of the indicators of FoGWar are equally associated with the construct? (As we’ll see below, this is called tau-equivalence).
We will rig things a bit to have the factor loadings of the 4 indicators vary: .6, .7, .8, and .7.
Raw correlation matrix:
#this is just the syntax to simulate the data
demo.model <- '
l_FoGWar =~ .6*FoGWar1 + .7*FoGWar2 + .8*FoGWar3 + .7*FoGWar4 #definition of factor FoGWar with loadings on 4 items
l_CoAWe =~ .75*CoAWe5 + .85*CoAWe6 + .8*CoAWe7 + .8*CoAWe8 #definition of factor CoAWe with loadings on 4 items
l_FoGWar ~~ 0.4*l_CoAWe
#error variance in inverse proportion to factor loadings
FoGWar1 ~~ (1-.6^2)*FoGWar1
FoGWar2 ~~ (1-.7^2)*FoGWar2
FoGWar3 ~~ (1-.8^2)*FoGWar3
FoGWar4 ~~ (1-.7^2)*FoGWar4
CoAWe5 ~~ (1-.75^2)*CoAWe5
CoAWe6 ~~ (1-.85^2)*CoAWe6
CoAWe7 ~~ (1-.8^2)*CoAWe7
CoAWe8 ~~ (1-.8^2)*CoAWe8
'
# generate data; note, standardized lv is default
simData <- lavaan::simulateData(demo.model, sample.nobs=500)
round(cor(simData), 2)
## FoGWar1 FoGWar2 FoGWar3 FoGWar4 CoAWe5 CoAWe6 CoAWe7 CoAWe8
## FoGWar1 1.00 0.46 0.53 0.45 0.23 0.22 0.12 0.19
## FoGWar2 0.46 1.00 0.56 0.52 0.23 0.25 0.18 0.18
## FoGWar3 0.53 0.56 1.00 0.62 0.25 0.23 0.18 0.22
## FoGWar4 0.45 0.52 0.62 1.00 0.28 0.29 0.22 0.27
## CoAWe5 0.23 0.23 0.25 0.28 1.00 0.64 0.60 0.58
## CoAWe6 0.22 0.25 0.23 0.29 0.64 1.00 0.68 0.64
## CoAWe7 0.12 0.18 0.18 0.22 0.60 0.68 1.00 0.65
## CoAWe8 0.19 0.18 0.22 0.27 0.58 0.64 0.65 1.00
msem.syntax <- '
l_FoGWar =~ f1*FoGWar1 + f2*FoGWar2 + f3*FoGWar3 + f4*FoGWar4 #definition of factor FoGWar with loadings on 4 items
l_CoAWe =~ CoAWe5 + CoAWe6 + CoAWe7 + CoAWe8 #definition of factor CoAWe with loadings on 4 items
l_FoGWar ~~ l_CoAWe
'
msem <- sem(msem.syntax, simData, ) #standardize factor variances to avoid first loading at 1.0.
#summary(msem, fit.measures=TRUE)
fitMeasures(msem)[c("chisq", "df", "pvalue", "rmsea", "srmr", "cfi")]
## chisq df pvalue rmsea srmr cfi
## 27.2557 19.0000 0.0988 0.0295 0.0290 0.9952
semPaths_default(msem)
inspect(msem)
## $lambda
## l_FGWr l_CoAW
## FoGWar1 0 0
## FoGWar2 1 0
## FoGWar3 2 0
## FoGWar4 3 0
## CoAWe5 0 0
## CoAWe6 0 4
## CoAWe7 0 5
## CoAWe8 0 6
##
## $theta
## FoGWr1 FoGWr2 FoGWr3 FoGWr4 CoAWe5 CoAWe6 CoAWe7 CoAWe8
## FoGWar1 8
## FoGWar2 0 9
## FoGWar3 0 0 10
## FoGWar4 0 0 0 11
## CoAWe5 0 0 0 0 12
## CoAWe6 0 0 0 0 0 13
## CoAWe7 0 0 0 0 0 0 14
## CoAWe8 0 0 0 0 0 0 0 15
##
## $psi
## l_FGWr l_CoAW
## l_FoGWar 16
## l_CoAWe 7 17
This is implemented by the lavTestWald
function in lavaan
:
con <- '
f1==f2
f1==f3
f1==f4
'
lavTestWald(msem, constraints = con)
## Warning in lav_partable_constraints_ceq(partable = partable, con = LIST, :
## lavaan WARNING: non-free parameter(s) in equality constraint(s): f1
## $stat
## [1] 9.65
##
## $df
## [1] 3
##
## $p.value
## [1] 0.0218
##
## $se
## [1] "standard"
Note that the constraints must be unique to have the Wald test be estimable. Thus, after we specify f1==f2
and f1==f3
, this implies that f2==f3
(transitivity) and we should not include that in the syntax.
Here, we find a significant p-value, suggesting that the fit is significantly worse if we force the loadings of FoGWar indicators to be equal.
The anova
function in lavaan
will compute the LRT for nested models. It requires the researcher to fit two nested models, then compare them. Here, we can fit the equality constrained model:
msem_eq.syntax <- '
l_FoGWar =~ f1*FoGWar1 + f1*FoGWar2 + f1*FoGWar3 + f1*FoGWar4 #equate loadings by using the same parameter label
l_CoAWe =~ CoAWe5 + CoAWe6 + CoAWe7 + CoAWe8
l_FoGWar ~~ l_CoAWe
'
msem_eq <- sem(msem_eq.syntax, simData, ) #standardize factor variances to avoid first loading at 1.0.
#summary(msem_eq, fit.measures=TRUE)
fitMeasures(msem_eq)[c("chisq", "df", "pvalue", "rmsea", "srmr", "cfi")]
## chisq df pvalue rmsea srmr cfi
## 39.5229 22.0000 0.0123 0.0399 0.0426 0.9897
semPaths_default(msem_eq)
inspect(msem_eq)
## $lambda
## l_FGWr l_CoAW
## FoGWar1 0 0
## FoGWar2 0 0
## FoGWar3 0 0
## FoGWar4 0 0
## CoAWe5 0 0
## CoAWe6 0 1
## CoAWe7 0 2
## CoAWe8 0 3
##
## $theta
## FoGWr1 FoGWr2 FoGWr3 FoGWr4 CoAWe5 CoAWe6 CoAWe7 CoAWe8
## FoGWar1 5
## FoGWar2 0 6
## FoGWar3 0 0 7
## FoGWar4 0 0 0 8
## CoAWe5 0 0 0 0 9
## CoAWe6 0 0 0 0 0 10
## CoAWe7 0 0 0 0 0 0 11
## CoAWe8 0 0 0 0 0 0 0 12
##
## $psi
## l_FGWr l_CoAW
## l_FoGWar 13
## l_CoAWe 4 14
By using the same parameter label f1
, lavaan
constrains those parameters to be equal. This is a convenient way to specify equality constraints.
anova(msem, msem_eq)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
msem | 19 | 9760 | 9831 | 27.3 | NA | NA | NA |
msem_eq | 22 | 9766 | 9825 | 39.5 | 12.3 | 3 | 0.007 |
We see the same effect for the LRT as the Wald test (no surprise there), suggesting worse fit under constrained loadings.
In measurement models, there is a useful set of constraints one can apply to understand the similarity of associations between indicators and latent variables. In a typical CFA, we estimate factor loadings for each indicator (excepting the reference if we use the unit loading identification strategy). As we’ve discussed, the standardized loadings provide an interpretable measure of how associated each indicator (item) is with the factor. Items with lower factor loadings reflect the latent construct to a lesser degree.
What if, however, we wish to test the hypothesis that all items measure the construct equally well? Likewise, what if we also wish to know if all items have equal precision in their measurement (i.e., proportion of item variance explained by the factor)? These are hypotheses that can be tested using constraints.
The first type of factor model is called congeneric. This is what we’ve come to expect of vanilla CFA: we specify that certain items load onto certain factors. Factor loadings and error variances (disturbances) are freely estimated for each indicator (excepting the reference indicator under ULI). A congeneric model has the following properties:
Factor loadings are freely estimated for each indicator
Disturbances (aka measurement error, error variance) are freely estimated for each indicator
Disturbances are independent (conditional independence assumption)
Indicators load onto only one latent variable (no cross-loadings)
Under these assumptions, the observed correlations among indicators reflect the influence of the latent variables, but we do not assume that a) the proportion of true score (factor score) variance is equal across indicators, or that b) that the latent construct scales with item responses equally.
Congeneric models already have certain constraints on structure, namely that certain items load onto certain factors according to our hypotheses. Unlike EFA, where all items technically load onto all factors, we typically fix many elements in \(\lambda\) to zero to enforce a hypothesis about latent structure. If the congeneric model fails, we would not usually move on to test more restrictive measurement models. But if the congeneric model fits well, we might consider additional measurement tests.
First, we will consider what’s called tau-equivalence, defined as:
Loadings on a given factor are equal
Error variances are free
Note we inherit the other features of the congeneric model, namely independent errors and no cross-loadings.
If loadings are equal, then each indicator has an equivalent association to the underlying construct. Therefore, a one-unit change in the underlying factor is associated with the same increase in the response to each indicator.
Note that measures of internal consistency are implicitly based on a tau-equivalent model. This is part of why coefficient alpha is technically a lower bound on the test’s reliability. If we do not have a tau-equivalent factor model, coefficient alpha will underestimate the reliability of the scale. For details, see Graham 2006 Educational and Psychological Measurement.
If we meet the assumptions of a tau-equivalent model, we might consider a further restriction called parallel structure.
Loadings on a given factor are equal
Error variances for indicators of a factor are equal
The additional requirement of equal error variances (disturbances) tests a model where each indicator has the same level of precision. This structure has two major implications. First, the amount of item variation attributable to the true score (in a CTT sense) is equal for all items. Second, the expected value of each of the items is the same – i.e., the items are fungible. A parallel indicators model relates in part to the practice of developing parallel forms of a test where we wish to use a subsets of items from a broader pool to assess the same construct. To be confident that we are indeed measuring the same construct, we need to satisfy the parallel constraint step. That is, the model with parallel indicators should not fit worse than a tau-equivalent model, which should not fit worse than the congeneric model.
Going back to our FoGWar and CoAWe example, we can examine whether we can satisfy congeneric, tau-equivalent, and/or parallel structure. These structural tests should be applied sequentially. For example, if we do not have tau equivalence, we cannot have parallel structure. But, we can evaluate these structural tests for each factor individually – one may be parallel, the other may be only congeneric.
Though we didn’t use the nomenclature, our initial two-factor CFA model with freed loadings is the congeneric case. As a reminder:
summary(msem, standardized=TRUE)
## lavaan (0.5-23.1097) converged normally after 26 iterations
##
## Number of observations 500
##
## Estimator ML
## Minimum Function Test Statistic 27.256
## Degrees of freedom 19
## P-value (Chi-square) 0.099
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar =~
## FoGWar1 (f1) 1.000 0.665 0.636
## FoGWar2 (f2) 1.075 0.087 12.417 0.000 0.715 0.699
## FoGWar3 (f3) 1.268 0.094 13.559 0.000 0.843 0.816
## FoGWar4 (f4) 1.123 0.086 13.059 0.000 0.747 0.754
## l_CoAWe =~
## CoAWe5 1.000 0.737 0.750
## CoAWe6 1.163 0.064 18.221 0.000 0.857 0.842
## CoAWe7 1.086 0.062 17.637 0.000 0.801 0.811
## CoAWe8 1.059 0.062 16.950 0.000 0.780 0.779
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar ~~
## l_CoAWe 0.184 0.030 6.161 0.000 0.375 0.375
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .FoGWar1 0.650 0.048 13.616 0.000 0.650 0.595
## .FoGWar2 0.536 0.042 12.676 0.000 0.536 0.511
## .FoGWar3 0.358 0.039 9.256 0.000 0.358 0.335
## .FoGWar4 0.424 0.037 11.399 0.000 0.424 0.432
## .CoAWe5 0.422 0.033 12.920 0.000 0.422 0.437
## .CoAWe6 0.302 0.029 10.276 0.000 0.302 0.291
## .CoAWe7 0.333 0.029 11.424 0.000 0.333 0.342
## .CoAWe8 0.395 0.032 12.324 0.000 0.395 0.393
## l_FoGWar 0.442 0.061 7.298 0.000 1.000 1.000
## l_CoAWe 0.543 0.058 9.364 0.000 1.000 1.000
Note that the loadings for FoGWar are similar, but perhaps not equivalent. They range from .64 to .82. The question of tau equivalence is whether the loadings can be constrained to equality without reducing fit substantially. As we saw above, by using the same parameter loading for a set of indicators, lavaan
will enforce an equality constraint.
Above, we tested a model in which we equated FoGWar loadings. As a reminder:
summary(msem_eq, standardized=TRUE)
## lavaan (0.5-23.1097) converged normally after 21 iterations
##
## Number of observations 500
##
## Estimator ML
## Minimum Function Test Statistic 39.523
## Degrees of freedom 22
## P-value (Chi-square) 0.012
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar =~
## FoGWar1 (f1) 1.000 0.750 0.687
## FoGWar2 (f1) 1.000 0.750 0.722
## FoGWar3 (f1) 1.000 0.750 0.759
## FoGWar4 (f1) 1.000 0.750 0.755
## l_CoAWe =~
## CoAWe5 1.000 0.737 0.750
## CoAWe6 1.163 0.064 18.230 0.000 0.858 0.842
## CoAWe7 1.086 0.062 17.635 0.000 0.800 0.811
## CoAWe8 1.058 0.062 16.950 0.000 0.780 0.779
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar ~~
## l_CoAWe 0.210 0.032 6.637 0.000 0.380 0.380
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .FoGWar1 0.628 0.047 13.323 0.000 0.628 0.528
## .FoGWar2 0.517 0.040 12.775 0.000 0.517 0.479
## .FoGWar3 0.414 0.035 12.009 0.000 0.414 0.424
## .FoGWar4 0.423 0.035 12.088 0.000 0.423 0.429
## .CoAWe5 0.422 0.033 12.921 0.000 0.422 0.437
## .CoAWe6 0.302 0.029 10.265 0.000 0.302 0.291
## .CoAWe7 0.334 0.029 11.435 0.000 0.334 0.343
## .CoAWe8 0.395 0.032 12.330 0.000 0.395 0.393
## l_FoGWar 0.562 0.043 12.951 0.000 1.000 1.000
## l_CoAWe 0.544 0.058 9.366 0.000 1.000 1.000
anova(msem, msem_eq)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
msem | 19 | 9760 | 9831 | 27.3 | NA | NA | NA |
msem_eq | 22 | 9766 | 9825 | 39.5 | 12.3 | 3 | 0.007 |
The LRT suggested that model fit worsened when FoGWar loadings were equated.
What about testing tau-equivalence for CoAWe?
msem_tau_coawe.syntax <- '
l_FoGWar =~ FoGWar1 + FoGWar2 + FoGWar3 + FoGWar4 #equate loadings by using the same parameter label
l_CoAWe =~ f1*CoAWe5 + f1*CoAWe6 + f1*CoAWe7 + f1*CoAWe8
l_FoGWar ~~ l_CoAWe
'
msem_tau_coawe <- sem(msem_tau_coawe.syntax, simData, )
summary(msem_tau_coawe, fit.measures=TRUE, standardized=TRUE)
## lavaan (0.5-23.1097) converged normally after 24 iterations
##
## Number of observations 500
##
## Estimator ML
## Minimum Function Test Statistic 35.358
## Degrees of freedom 22
## P-value (Chi-square) 0.036
##
## Model test baseline model:
##
## Minimum Function Test Statistic 1734.552
## Degrees of freedom 28
## P-value 0.000
##
## User model versus baseline model:
##
## Comparative Fit Index (CFI) 0.992
## Tucker-Lewis Index (TLI) 0.990
##
## Loglikelihood and Information Criteria:
##
## Loglikelihood user model (H0) -4866.928
## Loglikelihood unrestricted model (H1) -4849.249
##
## Number of free parameters 14
## Akaike (AIC) 9761.856
## Bayesian (BIC) 9820.861
## Sample-size adjusted Bayesian (BIC) 9776.424
##
## Root Mean Square Error of Approximation:
##
## RMSEA 0.035
## 90 Percent Confidence Interval 0.009 0.055
## P-value RMSEA <= 0.05 0.880
##
## Standardized Root Mean Square Residual:
##
## SRMR 0.038
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar =~
## FoGWar1 1.000 0.665 0.636
## FoGWar2 1.075 0.087 12.415 0.000 0.715 0.699
## FoGWar3 1.268 0.094 13.561 0.000 0.844 0.816
## FoGWar4 1.123 0.086 13.059 0.000 0.747 0.754
## l_CoAWe =~
## CoAWe5 (f1) 1.000 0.797 0.780
## CoAWe6 (f1) 1.000 0.797 0.811
## CoAWe7 (f1) 1.000 0.797 0.810
## CoAWe8 (f1) 1.000 0.797 0.788
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar ~~
## l_CoAWe 0.199 0.031 6.355 0.000 0.376 0.376
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .FoGWar1 0.650 0.048 13.616 0.000 0.650 0.595
## .FoGWar2 0.536 0.042 12.681 0.000 0.536 0.512
## .FoGWar3 0.358 0.039 9.249 0.000 0.358 0.334
## .FoGWar4 0.425 0.037 11.400 0.000 0.425 0.432
## .CoAWe5 0.408 0.032 12.715 0.000 0.408 0.391
## .CoAWe6 0.329 0.028 11.958 0.000 0.329 0.342
## .CoAWe7 0.334 0.028 12.008 0.000 0.334 0.345
## .CoAWe8 0.387 0.031 12.543 0.000 0.387 0.379
## l_FoGWar 0.442 0.061 7.298 0.000 1.000 1.000
## l_CoAWe 0.635 0.046 13.804 0.000 1.000 1.000
Let’s compare to the congeneric model using an LRT. The tau equivalent model is nested in the congeneric.
anova(msem, msem_tau_coawe)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
msem | 19 | 9760 | 9831 | 27.3 | NA | NA | NA |
msem_tau_coawe | 22 | 9762 | 9821 | 35.4 | 8.1 | 3 | 0.044 |
aictab(list(congeneric=msem, tau=msem_tau_coawe))
Modnames | K | AICc | Delta_AICc | ModelLik | AICcWt | LL | Cum.Wt |
---|---|---|---|---|---|---|---|
congeneric | 17 | 9761 | 0.0 | 1.000 | 0.7 | -4863 | 0.7 |
tau | 14 | 9763 | 1.7 | 0.428 | 0.3 | -4867 | 1.0 |
fitMeasures(msem_tau_coawe)[c("chisq", "df", "pvalue", "rmsea", "srmr", "cfi")]
## chisq df pvalue rmsea srmr cfi
## 35.3582 22.0000 0.0355 0.0348 0.0378 0.9922
Here, we see something perhaps less definitive: the LRT is barely significant (in our sample of 500), suggesting a failure of tau equivalence, but the AICc indicates that the tau equivalent model is nearly as good (difference of 1.7 points), and it is more parsimonious. We also find that the global fit measures are all acceptable. This highlights the problem with relying on NHST alone to judge constrained models. One should make an informed judgment based on a) LRT, b) changes in global fit measures (esp. change in CFI), c) change in residuals under the constrained solution, and d) interpretability.
With regard to change in residuals, we can examine this in a few ways. I suggest looking at the absolute change in the residuals in correlational units.
abs(resid(msem, type="cor")$cor - resid(msem_tau_coawe, type="cor")$cor)
## FoGWr1 FoGWr2 FoGWr3 FoGWr4 CoAWe5 CoAWe6 CoAWe7 CoAWe8
## FoGWar1 0.000
## FoGWar2 0.000 0.000
## FoGWar3 0.000 0.000 0.000
## FoGWar4 0.000 0.000 0.000 0.000
## CoAWe5 0.007 0.008 0.010 0.009 0.000
## CoAWe6 0.007 0.008 0.009 0.008 0.001 0.000
## CoAWe7 0.000 0.000 0.000 0.000 0.023 0.026 0.000
## CoAWe8 0.003 0.003 0.003 0.003 0.030 0.016 0.006 0.000
The reduction in fit for bivariate correlations falls in the .03 range for most indicators.
In this case, I would at least consider the parallel model.
In the parallel model, we add constraints on disturbances for the indicators (error variance). Let’s apply these constraints to the tau-equivalent CoAWe model, leaving FoGWar as congeneric. Remember that we only consider the parallel model if the tau-equivalent step fits well.
msem_parallel_coawe.syntax <- '
l_FoGWar =~ FoGWar1 + FoGWar2 + FoGWar3 + FoGWar4 #equate loadings by using the same parameter label
l_CoAWe =~ f1*CoAWe5 + f1*CoAWe6 + f1*CoAWe7 + f1*CoAWe8
l_FoGWar ~~ l_CoAWe
#equate residual variances
CoAWe5 ~~ rv1*CoAWe5
CoAWe6 ~~ rv1*CoAWe6
CoAWe7 ~~ rv1*CoAWe7
CoAWe8 ~~ rv1*CoAWe8
'
msem_parallel_coawe <- sem(msem_parallel_coawe.syntax, simData, )
summary(msem_parallel_coawe, fit.measures=TRUE, standardized=TRUE)
## lavaan (0.5-23.1097) converged normally after 21 iterations
##
## Number of observations 500
##
## Estimator ML
## Minimum Function Test Statistic 40.234
## Degrees of freedom 25
## P-value (Chi-square) 0.028
##
## Model test baseline model:
##
## Minimum Function Test Statistic 1734.552
## Degrees of freedom 28
## P-value 0.000
##
## User model versus baseline model:
##
## Comparative Fit Index (CFI) 0.991
## Tucker-Lewis Index (TLI) 0.990
##
## Loglikelihood and Information Criteria:
##
## Loglikelihood user model (H0) -4869.366
## Loglikelihood unrestricted model (H1) -4849.249
##
## Number of free parameters 11
## Akaike (AIC) 9760.732
## Bayesian (BIC) 9807.093
## Sample-size adjusted Bayesian (BIC) 9772.178
##
## Root Mean Square Error of Approximation:
##
## RMSEA 0.035
## 90 Percent Confidence Interval 0.012 0.054
## P-value RMSEA <= 0.05 0.896
##
## Standardized Root Mean Square Residual:
##
## SRMR 0.034
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar =~
## FoGWar1 1.000 0.665 0.636
## FoGWar2 1.075 0.087 12.416 0.000 0.715 0.699
## FoGWar3 1.268 0.093 13.566 0.000 0.844 0.816
## FoGWar4 1.123 0.086 13.063 0.000 0.747 0.754
## l_CoAWe =~
## CoAWe5 (f1) 1.000 0.793 0.795
## CoAWe6 (f1) 1.000 0.793 0.795
## CoAWe7 (f1) 1.000 0.793 0.795
## CoAWe8 (f1) 1.000 0.793 0.795
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## l_FoGWar ~~
## l_CoAWe 0.200 0.031 6.386 0.000 0.378 0.378
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .CoAWe5 (rv1) 0.366 0.013 27.386 0.000 0.366 0.368
## .CoAWe6 (rv1) 0.366 0.013 27.386 0.000 0.366 0.368
## .CoAWe7 (rv1) 0.366 0.013 27.386 0.000 0.366 0.368
## .CoAWe8 (rv1) 0.366 0.013 27.386 0.000 0.366 0.368
## .FoGWar1 0.650 0.048 13.616 0.000 0.650 0.595
## .FoGWar2 0.536 0.042 12.686 0.000 0.536 0.512
## .FoGWar3 0.357 0.039 9.250 0.000 0.357 0.334
## .FoGWar4 0.425 0.037 11.402 0.000 0.425 0.432
## l_FoGWar 0.443 0.061 7.300 0.000 1.000 1.000
## l_CoAWe 0.629 0.046 13.768 0.000 1.000 1.000
The f1
parameter label equates CoAWe factor loadings, whereas the rv1
parameter label equates error variances. These parameter labels are arbitrary and chosen by you. I try to use a shorthand that is easy (for me) to remember.
Let’s compare the congeneric, tau-equivalent, and parallel models using LRT:
anova(msem, msem_tau_coawe, msem_parallel_coawe)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
msem | 19 | 9760 | 9831 | 27.3 | NA | NA | NA |
msem_tau_coawe | 22 | 9762 | 9821 | 35.4 | 8.10 | 3 | 0.044 |
msem_parallel_coawe | 25 | 9761 | 9807 | 40.2 | 4.88 | 3 | 0.181 |
The parallel model does not fit significantly worse than the tau-equivalent model despite having three fewer parameters.
And now AICc and global fit:
aictab(list(congeneric=msem, tau=msem_tau_coawe, parallel=msem_parallel_coawe))
Modnames | K | AICc | Delta_AICc | ModelLik | AICcWt | LL | Cum.Wt | |
---|---|---|---|---|---|---|---|---|
1 | congeneric | 17 | 9761 | 0.00 | 1.000 | 0.433 | -4863 | 0.433 |
3 | parallel | 11 | 9761 | 0.25 | 0.883 | 0.382 | -4869 | 0.815 |
2 | tau | 14 | 9763 | 1.70 | 0.428 | 0.185 | -4867 | 1.000 |
fitMeasures(msem_parallel_coawe)[c("chisq", "df", "pvalue", "rmsea", "srmr", "cfi")]
## chisq df pvalue rmsea srmr cfi
## 40.2340 25.0000 0.0276 0.0349 0.0343 0.9911
AIC also suggests that the models all fit the data similarly. The parallel model is actually quite close to the congeneric case in the fit:parsimony tradeoff that AICc represents (remember its ‘efficiency’ property).
So far, we have primarily considered constraints in the context of a single sample, enforcing equality on different parameters in the model. Oftentimes, however, we are faced with questions in which we a) have multiple distinct groups or samples, or b) hypothesize that a categorical moderator may affect some of the parameters in our model. In either case, this is typically a multiple-groups problem in which we are interested in understanding how well a given SEM fits independent samples of data.
In the simplest case, we could simply run the SEM in each sample and compare the results qualitatively. An equivalent approach is to fit both groups simultaneously in a multiple-groups model, but allow every parameter to be free between groups.
Let’s consider the example from the lavaan
tutorial: http://lavaan.ugent.be/tutorial/groups.html. Here, we have data from intelligence tests conducted in two separate schools, Pasteur and Grant-White:
data(HolzingerSwineford1939)
describeBy(HolzingerSwineford1939, HolzingerSwineford1939$school)
##
## Descriptive statistics by group
## group: Grant-White
## vars n mean sd median trimmed mad min max range
## id 1 145 275.26 43.34 275.00 275.12 54.86 201.00 351.00 150.00
## sex 2 145 1.50 0.50 2.00 1.50 0.00 1.00 2.00 1.00
## ageyr 3 145 12.72 0.97 13.00 12.67 1.48 11.00 16.00 5.00
## agemo 4 145 5.34 3.48 5.00 5.31 4.45 0.00 11.00 11.00
## school* 5 145 1.00 0.00 1.00 1.00 0.00 1.00 1.00 0.00
## grade 6 144 7.45 0.50 7.00 7.44 0.00 7.00 8.00 1.00
## x1 7 145 4.93 1.15 5.00 4.96 1.24 1.83 8.50 6.67
## x2 8 145 6.20 1.11 6.25 6.14 1.11 2.25 9.25 7.00
## x3 9 145 2.00 1.04 1.88 1.92 1.11 0.38 4.50 4.12
## x4 10 145 3.32 1.13 3.00 3.27 0.99 0.33 6.33 6.00
## x5 11 145 4.71 1.16 4.75 4.78 1.11 1.00 7.00 6.00
## x6 12 145 2.47 1.14 2.29 2.38 1.06 0.29 5.86 5.57
## x7 13 145 3.92 1.03 3.87 3.90 1.10 1.30 6.48 5.17
## x8 14 145 5.49 1.05 5.50 5.45 0.89 3.05 10.00 6.95
## x9 15 145 5.33 1.03 5.31 5.33 1.15 3.11 9.25 6.14
## skew kurtosis se
## id 0.02 -1.21 3.60
## sex -0.01 -2.01 0.04
## ageyr 0.70 0.63 0.08
## agemo 0.06 -1.23 0.29
## school* NaN NaN 0.00
## grade 0.19 -1.98 0.04
## x1 -0.12 -0.13 0.10
## x2 0.23 0.75 0.09
## x3 0.61 -0.51 0.09
## x4 0.40 0.16 0.09
## x5 -0.54 0.13 0.10
## x6 0.71 0.14 0.09
## x7 0.16 -0.42 0.09
## x8 0.68 2.09 0.09
## x9 0.20 0.41 0.09
## --------------------------------------------------------
## group: Pasteur
## vars n mean sd median trimmed mad min max range
## id 1 156 84.81 48.91 85.50 84.90 62.27 1.00 168.00 167.00
## sex 2 156 1.53 0.50 2.00 1.53 0.00 1.00 2.00 1.00
## ageyr 3 156 13.25 1.06 13.00 13.15 1.48 12.00 16.00 4.00
## agemo 4 156 5.40 3.44 5.00 5.33 4.45 0.00 11.00 11.00
## school* 5 156 2.00 0.00 2.00 2.00 0.00 2.00 2.00 0.00
## grade 6 156 7.50 0.50 7.50 7.50 0.74 7.00 8.00 1.00
## x1 7 156 4.94 1.19 5.00 4.97 1.24 0.67 7.50 6.83
## x2 8 156 5.98 1.23 5.75 5.89 1.11 3.50 9.25 5.75
## x3 9 156 2.49 1.16 2.38 2.48 1.30 0.25 4.50 4.25
## x4 10 156 2.82 1.15 2.67 2.80 0.99 0.00 6.00 6.00
## x5 11 156 4.00 1.31 4.00 4.02 1.48 1.00 6.75 5.75
## x6 12 156 1.92 0.99 1.86 1.85 1.06 0.14 6.14 6.00
## x7 13 156 4.43 1.09 4.37 4.40 1.19 2.04 7.43 5.39
## x8 14 156 5.56 0.98 5.47 5.53 0.93 3.50 8.30 4.80
## x9 15 156 5.42 0.99 5.42 5.40 0.95 2.78 8.61 5.83
## skew kurtosis se
## id -0.02 -1.23 3.92
## sex -0.10 -2.00 0.04
## ageyr 0.68 -0.21 0.09
## agemo 0.11 -1.23 0.28
## school* NaN NaN 0.00
## grade 0.00 -2.01 0.04
## x1 -0.37 0.63 0.09
## x2 0.68 0.17 0.10
## x3 0.15 -1.10 0.09
## x4 0.22 -0.13 0.09
## x5 -0.13 -0.88 0.10
## x6 0.99 1.87 0.08
## x7 0.30 -0.45 0.09
## x8 0.35 0.01 0.08
## x9 0.22 0.11 0.08
The variables x1-x3
are indicators of visual processing, x4-x6
reflect textual processing, and x7-x9
measure processing speed.
Here is the fit of a three-factor model to each group.
HS.model <- ' visual =~ x1 + x2 + x3
textual =~ x4 + x5 + x6
speed =~ x7 + x8 + x9 '
fit_pasteur <- cfa(HS.model, data = filter(HolzingerSwineford1939, school=="Pasteur"))
summary(fit_pasteur)
## lavaan (0.5-23.1097) converged normally after 38 iterations
##
## Number of observations 156
##
## Estimator ML
## Minimum Function Test Statistic 64.309
## Degrees of freedom 24
## P-value (Chi-square) 0.000
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 0.394 0.122 3.220 0.001
## x3 0.570 0.140 4.076 0.000
## textual =~
## x4 1.000
## x5 1.183 0.102 11.613 0.000
## x6 0.875 0.077 11.421 0.000
## speed =~
## x7 1.000
## x8 1.125 0.277 4.057 0.000
## x9 0.922 0.225 4.104 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.479 0.106 4.531 0.000
## speed 0.185 0.077 2.397 0.017
## textual ~~
## speed 0.182 0.069 2.628 0.009
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.298 0.232 1.286 0.198
## .x2 1.334 0.158 8.464 0.000
## .x3 0.989 0.136 7.271 0.000
## .x4 0.425 0.069 6.138 0.000
## .x5 0.456 0.086 5.292 0.000
## .x6 0.290 0.050 5.780 0.000
## .x7 0.820 0.125 6.580 0.000
## .x8 0.510 0.116 4.406 0.000
## .x9 0.680 0.104 6.516 0.000
## visual 1.097 0.276 3.967 0.000
## textual 0.894 0.150 5.963 0.000
## speed 0.350 0.126 2.778 0.005
semPaths_default(fit_pasteur)
### Grant-White
fit_grantwhite <- cfa(HS.model, data = filter(HolzingerSwineford1939, school=="Grant-White"))
summary(fit_grantwhite)
## lavaan (0.5-23.1097) converged normally after 34 iterations
##
## Number of observations 145
##
## Estimator ML
## Minimum Function Test Statistic 51.542
## Degrees of freedom 24
## P-value (Chi-square) 0.001
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 0.736 0.155 4.760 0.000
## x3 0.925 0.166 5.584 0.000
## textual =~
## x4 1.000
## x5 0.990 0.087 11.418 0.000
## x6 0.963 0.085 11.377 0.000
## speed =~
## x7 1.000
## x8 1.226 0.187 6.569 0.000
## x9 1.058 0.165 6.429 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.408 0.098 4.153 0.000
## speed 0.276 0.076 3.639 0.000
## textual ~~
## speed 0.222 0.073 3.022 0.003
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.715 0.126 5.675 0.000
## .x2 0.899 0.123 7.339 0.000
## .x3 0.557 0.103 5.409 0.000
## .x4 0.315 0.065 4.870 0.000
## .x5 0.419 0.072 5.812 0.000
## .x6 0.406 0.069 5.880 0.000
## .x7 0.600 0.091 6.584 0.000
## .x8 0.401 0.094 4.248 0.000
## .x9 0.535 0.089 6.010 0.000
## visual 0.604 0.160 3.762 0.000
## textual 0.942 0.152 6.177 0.000
## speed 0.461 0.118 3.910 0.000
We can fit the groups in a single model in which both contribute to the sample likelihood. Recall that the sample likelihood reflect the probability of the observed data given the model and parameters. If we consider all free parameters for one group as a vector, \(\boldsymbol{\theta}_1\) and free parameters for the other group as a vector, \(\boldsymbol{\theta}_2\), then the total number of free parameters, \(q\) is simply \(\boldsymbol{\theta}_1 + \boldsymbol{\theta}_2\). In this way, each sample contributes independently to the sample likelihood function insofar as the parameters are completely separated by group.
We can fit multiple groups in SEM using the group
argument in lavaan
. Let’s add a grouping structure to the intelligence testing example:
fit_combined <- cfa(HS.model, data = HolzingerSwineford1939, group="school", meanstructure=FALSE) #lavaan will include means/intercepts by default in multiple-group analysis
summary(fit_combined, fit.measures=TRUE, standardized=TRUE)
## lavaan (0.5-23.1097) converged normally after 57 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 115.851
## Degrees of freedom 48
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 64.309
## Grant-White 51.542
##
## Model test baseline model:
##
## Minimum Function Test Statistic 957.769
## Degrees of freedom 72
## P-value 0.000
##
## User model versus baseline model:
##
## Comparative Fit Index (CFI) 0.923
## Tucker-Lewis Index (TLI) 0.885
##
## Loglikelihood and Information Criteria:
##
## Loglikelihood user model (H0) -3682.198
## Loglikelihood unrestricted model (H1) -3624.272
##
## Number of free parameters 42
## Akaike (AIC) 7448.395
## Bayesian (BIC) 7604.094
## Sample-size adjusted Bayesian (BIC) 7470.894
##
## Root Mean Square Error of Approximation:
##
## RMSEA 0.097
## 90 Percent Confidence Interval 0.075 0.120
## P-value RMSEA <= 0.05 0.001
##
## Standardized Root Mean Square Residual:
##
## SRMR 0.074
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual =~
## x1 1.000 1.047 0.887
## x2 0.394 0.122 3.220 0.001 0.412 0.336
## x3 0.570 0.140 4.076 0.000 0.597 0.515
## textual =~
## x4 1.000 0.946 0.823
## x5 1.183 0.102 11.613 0.000 1.119 0.856
## x6 0.875 0.077 11.421 0.000 0.827 0.838
## speed =~
## x7 1.000 0.591 0.547
## x8 1.125 0.277 4.057 0.000 0.665 0.682
## x9 0.922 0.225 4.104 0.000 0.545 0.551
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual ~~
## textual 0.479 0.106 4.531 0.000 0.484 0.484
## speed 0.185 0.077 2.397 0.017 0.299 0.299
## textual ~~
## speed 0.182 0.069 2.628 0.009 0.325 0.325
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 0.298 0.232 1.286 0.198 0.298 0.214
## .x2 1.334 0.158 8.464 0.000 1.334 0.887
## .x3 0.989 0.136 7.271 0.000 0.989 0.735
## .x4 0.425 0.069 6.138 0.000 0.425 0.322
## .x5 0.456 0.086 5.292 0.000 0.456 0.267
## .x6 0.290 0.050 5.780 0.000 0.290 0.297
## .x7 0.820 0.125 6.580 0.000 0.820 0.701
## .x8 0.510 0.116 4.406 0.000 0.510 0.535
## .x9 0.680 0.104 6.516 0.000 0.680 0.696
## visual 1.097 0.276 3.967 0.000 1.000 1.000
## textual 0.894 0.150 5.963 0.000 1.000 1.000
## speed 0.350 0.126 2.778 0.005 1.000 1.000
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual =~
## x1 1.000 0.777 0.677
## x2 0.736 0.155 4.760 0.000 0.572 0.517
## x3 0.925 0.166 5.583 0.000 0.719 0.694
## textual =~
## x4 1.000 0.971 0.866
## x5 0.990 0.087 11.418 0.000 0.961 0.829
## x6 0.963 0.085 11.377 0.000 0.935 0.826
## speed =~
## x7 1.000 0.679 0.659
## x8 1.226 0.187 6.569 0.000 0.833 0.796
## x9 1.058 0.165 6.429 0.000 0.719 0.701
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual ~~
## textual 0.408 0.098 4.153 0.000 0.541 0.541
## speed 0.276 0.076 3.639 0.000 0.523 0.523
## textual ~~
## speed 0.222 0.073 3.022 0.003 0.336 0.336
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 0.715 0.126 5.676 0.000 0.715 0.542
## .x2 0.899 0.123 7.339 0.000 0.899 0.733
## .x3 0.557 0.103 5.409 0.000 0.557 0.519
## .x4 0.315 0.065 4.870 0.000 0.315 0.251
## .x5 0.419 0.072 5.812 0.000 0.419 0.312
## .x6 0.406 0.069 5.880 0.000 0.406 0.317
## .x7 0.600 0.091 6.584 0.000 0.600 0.566
## .x8 0.401 0.094 4.249 0.000 0.401 0.367
## .x9 0.535 0.089 6.010 0.000 0.535 0.509
## visual 0.604 0.160 3.762 0.000 1.000 1.000
## textual 0.942 0.152 6.177 0.000 1.000 1.000
## speed 0.461 0.118 3.910 0.000 1.000 1.000
We are now provided with parameter estimates for each group. We also have the \(\chi^2\) contribution of each group to the total model \(\chi^2\). Notice that the \(\chi^2\) for each group is identical to the \(\chi^2\) obtained when we fit the samples independently. Thus, the jumping off point for most multiple-group analyses is a free model in which all parameters are estimated uniquely in each group.
This model specification essentially looks like this:
The free model is not very parsimonious, however, nor does it usually address our substantive research questions. In fact, we are often interested in testing the hypothesis that the latent structure (or at least part of it) is equivalent across groups. If we translate a measure of personality from English to Dutch, it would be ideal if the latent structure was the same in both populations. This would let us compare results across nations without concern that the tests are measuring different constructs in each nation. Likewise, if we wish to compare rates of change in aggressive behaviors between prison and psychiatric settings, we wish to test a hypothesis about relative equality in change parameters across settings.
inspect(fit_combined)
## $Pasteur
## $Pasteur$lambda
## visual textul speed
## x1 0 0 0
## x2 1 0 0
## x3 2 0 0
## x4 0 0 0
## x5 0 3 0
## x6 0 4 0
## x7 0 0 0
## x8 0 0 5
## x9 0 0 6
##
## $Pasteur$theta
## x1 x2 x3 x4 x5 x6 x7 x8 x9
## x1 7
## x2 0 8
## x3 0 0 9
## x4 0 0 0 10
## x5 0 0 0 0 11
## x6 0 0 0 0 0 12
## x7 0 0 0 0 0 0 13
## x8 0 0 0 0 0 0 0 14
## x9 0 0 0 0 0 0 0 0 15
##
## $Pasteur$psi
## visual textul speed
## visual 16
## textual 19 17
## speed 20 21 18
##
##
## $`Grant-White`
## $`Grant-White`$lambda
## visual textul speed
## x1 0 0 0
## x2 22 0 0
## x3 23 0 0
## x4 0 0 0
## x5 0 24 0
## x6 0 25 0
## x7 0 0 0
## x8 0 0 26
## x9 0 0 27
##
## $`Grant-White`$theta
## x1 x2 x3 x4 x5 x6 x7 x8 x9
## x1 28
## x2 0 29
## x3 0 0 30
## x4 0 0 0 31
## x5 0 0 0 0 32
## x6 0 0 0 0 0 33
## x7 0 0 0 0 0 0 34
## x8 0 0 0 0 0 0 0 35
## x9 0 0 0 0 0 0 0 0 36
##
## $`Grant-White`$psi
## visual textul speed
## visual 37
## textual 40 38
## speed 41 42 39
#semPaths_default(fit_combined)
Thus, we can apply the same general idea of applying equality constraints between groups as we did in the single sample case above. For multiple group analyses, lavaan uses a vector of parameter loadings to indicate what equality constraints to apply in each group. In the two-school example, what if we wish to equate factor loadings on the visual
factor across groups? We can add a two-element parameter label vector in front of each loading (e.g., c(f1, f1)*x1
).
HS.model_floadings_vis <- ' visual =~ c(f1,f1)*x1 + c(f2,f2)*x2 + c(f3,f3)*x3
textual =~ x4 + x5 + x6
speed =~ x7 + x8 + x9 '
floadings_vis <- cfa(HS.model_floadings_vis, data = HolzingerSwineford1939, group="school", )
summary(floadings_vis)
## lavaan (0.5-23.1097) converged normally after 50 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 118.761
## Degrees of freedom 50
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 66.242
## Grant-White 52.519
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 (f1) 1.000
## x2 (f2) 0.602 0.101 5.980 0.000
## x3 (f3) 0.788 0.109 7.255 0.000
## textual =~
## x4 1.000
## x5 1.190 0.103 11.570 0.000
## x6 0.878 0.077 11.370 0.000
## speed =~
## x7 1.000
## x8 1.138 0.281 4.053 0.000
## x9 0.952 0.232 4.100 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.396 0.096 4.105 0.000
## speed 0.171 0.071 2.412 0.016
## textual ~~
## speed 0.178 0.068 2.618 0.009
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 4.941 0.093 52.980 0.000
## .x2 5.984 0.100 60.116 0.000
## .x3 2.487 0.094 26.474 0.000
## .x4 2.823 0.092 30.689 0.000
## .x5 3.995 0.105 38.183 0.000
## .x6 1.922 0.079 24.321 0.000
## .x7 4.432 0.087 51.181 0.000
## .x8 5.563 0.078 71.214 0.000
## .x9 5.418 0.079 68.440 0.000
## visual 0.000
## textual 0.000
## speed 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.556 0.138 4.026 0.000
## .x2 1.256 0.155 8.103 0.000
## .x3 0.879 0.129 6.842 0.000
## .x4 0.431 0.070 6.174 0.000
## .x5 0.449 0.086 5.192 0.000
## .x6 0.289 0.050 5.743 0.000
## .x7 0.831 0.124 6.696 0.000
## .x8 0.513 0.115 4.459 0.000
## .x9 0.670 0.105 6.394 0.000
## visual 0.801 0.171 4.693 0.000
## textual 0.889 0.150 5.932 0.000
## speed 0.339 0.123 2.745 0.006
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 (f1) 1.000
## x2 (f2) 0.602 0.101 5.980 0.000
## x3 (f3) 0.788 0.109 7.255 0.000
## textual =~
## x4 1.000
## x5 0.990 0.087 11.424 0.000
## x6 0.963 0.085 11.374 0.000
## speed =~
## x7 1.000
## x8 1.228 0.188 6.546 0.000
## x9 1.076 0.168 6.420 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.447 0.102 4.408 0.000
## speed 0.310 0.080 3.877 0.000
## textual ~~
## speed 0.222 0.073 3.044 0.002
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 4.930 0.097 50.782 0.000
## .x2 6.200 0.091 68.357 0.000
## .x3 1.996 0.085 23.448 0.000
## .x4 3.317 0.093 35.625 0.000
## .x5 4.712 0.096 48.986 0.000
## .x6 2.469 0.094 26.277 0.000
## .x7 3.921 0.086 45.819 0.000
## .x8 5.488 0.087 63.174 0.000
## .x9 5.327 0.085 62.571 0.000
## visual 0.000
## textual 0.000
## speed 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.647 0.126 5.125 0.000
## .x2 0.933 0.121 7.731 0.000
## .x3 0.604 0.096 6.276 0.000
## .x4 0.315 0.065 4.869 0.000
## .x5 0.418 0.072 5.807 0.000
## .x6 0.407 0.069 5.889 0.000
## .x7 0.607 0.091 6.645 0.000
## .x8 0.409 0.094 4.361 0.000
## .x9 0.524 0.089 5.910 0.000
## visual 0.719 0.160 4.483 0.000
## textual 0.942 0.152 6.178 0.000
## speed 0.454 0.117 3.879 0.000
For a good summary of how to present measurement invariance tests, see: http://www.lesahoffman.com/CLP948/CLP948_Example07a_CFA_MG_Invariance.pdf
The code is in Mplus, but the description is general.
The measurementInvariance
function from the semTools
package provides a convenient way to test different steps of measurement invariance, ranging from configural through strict invariance. It applies the appropriate constraints for measurement invariance tests:
Integrating (blurring?) over a huge swath of literature, I would recommend satisfying at least strong invariance if you want to compare groups on factor means, variance, or covariances. There is less general support for requiring strict invariance, since that essentially forces the uniqueness of each item (i.e., variance not attributable to the factor) to be the same across groups. This also implies equal reliability across groups, assuming equal factor variances. There is not a huge conceptual gain in demonstrating strict MI for most applications, in my opinion.
mlist <- measurementInvariance(HS.model, data = HolzingerSwineford1939, strict=TRUE, std.lv=TRUE, group = "school")
##
## Measurement invariance models:
##
## Model 1 : fit.configural
## Model 2 : fit.loadings
## Model 3 : fit.intercepts
## Model 4 : fit.residuals
## Model 5 : fit.means
##
## Chi Square Difference Test
##
## Df AIC BIC Chisq Chisq diff Df diff Pr(>Chisq)
## fit.configural 48 7484 7707 116
## fit.loadings 54 7481 7681 124 8.2 6 0.224
## fit.intercepts 60 7509 7687 164 40.1 6 4.4e-07 ***
## fit.residuals 69 7508 7653 182 17.4 9 0.043 *
## fit.means 72 7542 7675 221 39.8 3 1.2e-08 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
##
## Fit measures:
##
## cfi rmsea cfi.delta rmsea.delta
## fit.configural 0.923 0.097 NA NA
## fit.loadings 0.921 0.093 0.002 0.004
## fit.intercepts 0.882 0.107 0.038 0.015
## fit.residuals 0.873 0.104 0.009 0.003
## fit.means 0.831 0.117 0.042 0.013
Although the measurementInvariance
function is great, it is often useful to have more control over what is equated among groups while avoiding the verbose syntax of needing to label every parameter constraint. Lavaan provides the group.equal
argument, which can accept any of the following: “loadings”, “intercepts”, “means”, “thresholds”, “regressions”, “residuals”, “residual.covariances”, “lv.variances”, or “lv.covariances”. See ?lavOptions
for details.
For the weak invariance model, we would use group.equal="loadings"
. Note that we have to be careful about the std.lv
argument when fitting multiple groups. There are two ways to identify the weak invariance model:
Note that passing std.lv=TRUE
to sem
will setup the UVI properly in a single-group analysis. But under multiple groups, at present it will set factor variances to 1 in all groups, which does not satisfy 2c above. Thus, I would suggest not using std.lv=TRUE
and not taking the UVI approach. Instead, the ULI (reference indicators) is easy to setup. I include syntax below for UVI but you’ll see it is a bit tricky.
Small sidebar: the measurementInvariance
function above does handle the UVI/ULI challenge in invariance testing internally, so you can use TRUE
or FALSE
above and get equivalent results (as you should).
mconfigural <- sem(HS.model, data = HolzingerSwineford1939, std.lv=FALSE, group = "school")
#ULI approach (reference indicator)
mweak <- sem(HS.model, data = HolzingerSwineford1939, std.lv=FALSE, group = "school", group.equal="loadings")
#Manual UVI approach since std.lv=TRUE gives improper (nonequivalent) results
mmod_uvi <-'
visual =~ NA*x1 + x2 + x3
textual =~ NA*x4 + x5 + x6
speed =~ NA*x7 + x8 + x9
#at present, lavaan does not allow parameter labels to be combined with constraints, such as c(1, v1)
visual ~~ c(v1, v2)*visual
textual ~~ c(t1, t2)*textual
speed ~~ c(s1, s2)*speed
#thus, use constraints to enforce the structure of fixing variances to 1.0 in one group, free in others
v1==1
t1==1
s1==1
'
mweak_uvi <- sem(mmod_uvi, data = HolzingerSwineford1939, std.lv=FALSE, group = "school", group.equal="loadings", meanstructure=TRUE)
summary(mweak_uvi)
## lavaan (0.5-23.1097) converged normally after 48 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 124.044
## Degrees of freedom 54
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 68.825
## Grant-White 55.219
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 (.p1.) 0.897 0.095 9.427 0.000
## x2 (.p2.) 0.537 0.085 6.329 0.000
## x3 (.p3.) 0.704 0.085 8.241 0.000
## textual =~
## x4 (.p4.) 0.956 0.072 13.302 0.000
## x5 (.p5.) 1.035 0.078 13.308 0.000
## x6 (.p6.) 0.871 0.064 13.685 0.000
## speed =~
## x7 (.p7.) 0.552 0.070 7.841 0.000
## x8 (.p8.) 0.663 0.075 8.892 0.000
## x9 (.p9.) 0.573 0.069 8.298 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.485 0.087 5.601 0.000
## speed 0.340 0.114 2.994 0.003
## textual ~~
## speed 0.333 0.100 3.342 0.001
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 4.941 0.093 52.991 0.000
## .x2 5.984 0.100 60.096 0.000
## .x3 2.487 0.094 26.465 0.000
## .x4 2.823 0.093 30.371 0.000
## .x5 3.995 0.101 39.715 0.000
## .x6 1.922 0.081 23.711 0.000
## .x7 4.432 0.086 51.540 0.000
## .x8 5.563 0.078 71.088 0.000
## .x9 5.418 0.079 68.153 0.000
## visual 0.000
## textual 0.000
## speed 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## visual (v1) 1.000 NA
## textual (t1) 1.000 NA
## speed (s1) 1.000 0.000 761628491.287 0.000
## .x1 0.551 0.137 4.010 0.000
## .x2 1.258 0.155 8.117 0.000
## .x3 0.882 0.128 6.884 0.000
## .x4 0.434 0.070 6.238 0.000
## .x5 0.508 0.082 6.229 0.000
## .x6 0.266 0.050 5.294 0.000
## .x7 0.849 0.114 7.468 0.000
## .x8 0.515 0.095 5.409 0.000
## .x9 0.658 0.096 6.865 0.000
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 (.p1.) 0.897 0.095 9.427 0.000
## x2 (.p2.) 0.537 0.085 6.329 0.000
## x3 (.p3.) 0.704 0.085 8.241 0.000
## textual =~
## x4 (.p4.) 0.956 0.072 13.302 0.000
## x5 (.p5.) 1.035 0.078 13.308 0.000
## x6 (.p6.) 0.871 0.064 13.685 0.000
## speed =~
## x7 (.p7.) 0.552 0.070 7.841 0.000
## x8 (.p8.) 0.663 0.075 8.892 0.000
## x9 (.p9.) 0.573 0.069 8.298 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.510 0.125 4.088 0.000
## speed 0.633 0.167 3.789 0.000
## textual ~~
## speed 0.428 0.140 3.071 0.002
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 4.930 0.097 50.763 0.000
## .x2 6.200 0.091 68.379 0.000
## .x3 1.996 0.085 23.455 0.000
## .x4 3.317 0.092 35.950 0.000
## .x5 4.712 0.100 47.173 0.000
## .x6 2.469 0.091 27.248 0.000
## .x7 3.921 0.086 45.555 0.000
## .x8 5.488 0.087 63.257 0.000
## .x9 5.327 0.085 62.786 0.000
## visual 0.000
## textual 0.000
## speed 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## visual (v2) 0.897 0.225 3.990 0.000
## textual (t2) 0.992 0.186 5.336 0.000
## speed (s2) 1.559 0.379 4.116 0.000
## .x1 0.645 0.127 5.084 0.000
## .x2 0.933 0.121 7.732 0.000
## .x3 0.605 0.096 6.282 0.000
## .x4 0.329 0.062 5.279 0.000
## .x5 0.384 0.073 5.270 0.000
## .x6 0.437 0.067 6.576 0.000
## .x7 0.599 0.090 6.651 0.000
## .x8 0.406 0.089 4.541 0.000
## .x9 0.532 0.086 6.202 0.000
##
## Constraints:
## |Slack|
## v1 - (1) 0.000
## t1 - (1) 0.000
## s1 - (1) 0.000
semTools::net(mweak, mweak_uvi)
##
## If cell [R, C] is TRUE, the model in row R is nested within column C.
##
## If cell [R, C] is TRUE and the models have the same degrees of freedom,
## they are equivalent models. See Bentler & Satorra (2010) for details.
##
## If cell [R, C] is NA, then the model in column C did not converge when
## fit to the implied means and covariance matrix from the model in row R.
##
## The hidden diagonal is TRUE because any model is equivalent to itself.
## The upper triangle is hidden because for models with the same degrees
## of freedom, cell [C, R] == cell [R, C]. For all models with different
## degrees of freedom, the upper diagonal is all FALSE because models with
## fewer degrees of freedom (i.e., more parameters) cannot be nested
## within models with more degrees of freedom (i.e., fewer parameters).
##
## mweak mweak_uvi
## mweak (df = 54)
## mweak_uvi (df = 54) TRUE
The UVI/ULI identification steps are the same as in the weak invariance model.
mstrong <- sem(HS.model, data = HolzingerSwineford1939, std.lv=FALSE, group = "school", group.equal=c("loadings", "intercepts"), meanstructure=TRUE)
summary(mstrong) #useful to double check which parameters are equated.
## lavaan (0.5-23.1097) converged normally after 60 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 164.103
## Degrees of freedom 60
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 90.210
## Grant-White 73.892
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 (.p2.) 0.576 0.101 5.713 0.000
## x3 (.p3.) 0.798 0.112 7.146 0.000
## textual =~
## x4 1.000
## x5 (.p5.) 1.120 0.066 16.965 0.000
## x6 (.p6.) 0.932 0.056 16.608 0.000
## speed =~
## x7 1.000
## x8 (.p8.) 1.130 0.145 7.786 0.000
## x9 (.p9.) 1.009 0.132 7.667 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.410 0.095 4.293 0.000
## speed 0.178 0.066 2.687 0.007
## textual ~~
## speed 0.180 0.062 2.900 0.004
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.25.) 5.001 0.090 55.760 0.000
## .x2 (.26.) 6.151 0.077 79.905 0.000
## .x3 (.27.) 2.271 0.083 27.387 0.000
## .x4 (.28.) 2.778 0.087 31.953 0.000
## .x5 (.29.) 4.035 0.096 41.858 0.000
## .x6 (.30.) 1.926 0.079 24.426 0.000
## .x7 (.31.) 4.242 0.073 57.975 0.000
## .x8 (.32.) 5.630 0.072 78.531 0.000
## .x9 (.33.) 5.465 0.069 79.016 0.000
## visual 0.000
## textual 0.000
## speed 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.555 0.139 3.983 0.000
## .x2 1.296 0.158 8.186 0.000
## .x3 0.944 0.136 6.929 0.000
## .x4 0.445 0.069 6.430 0.000
## .x5 0.502 0.082 6.136 0.000
## .x6 0.263 0.050 5.264 0.000
## .x7 0.888 0.120 7.416 0.000
## .x8 0.541 0.095 5.706 0.000
## .x9 0.654 0.096 6.805 0.000
## visual 0.796 0.172 4.641 0.000
## textual 0.879 0.131 6.694 0.000
## speed 0.322 0.082 3.914 0.000
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 (.p2.) 0.576 0.101 5.713 0.000
## x3 (.p3.) 0.798 0.112 7.146 0.000
## textual =~
## x4 1.000
## x5 (.p5.) 1.120 0.066 16.965 0.000
## x6 (.p6.) 0.932 0.056 16.608 0.000
## speed =~
## x7 1.000
## x8 (.p8.) 1.130 0.145 7.786 0.000
## x9 (.p9.) 1.009 0.132 7.667 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.427 0.097 4.417 0.000
## speed 0.329 0.082 4.006 0.000
## textual ~~
## speed 0.236 0.073 3.224 0.001
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.25.) 5.001 0.090 55.760 0.000
## .x2 (.26.) 6.151 0.077 79.905 0.000
## .x3 (.27.) 2.271 0.083 27.387 0.000
## .x4 (.28.) 2.778 0.087 31.953 0.000
## .x5 (.29.) 4.035 0.096 41.858 0.000
## .x6 (.30.) 1.926 0.079 24.426 0.000
## .x7 (.31.) 4.242 0.073 57.975 0.000
## .x8 (.32.) 5.630 0.072 78.531 0.000
## .x9 (.33.) 5.465 0.069 79.016 0.000
## visual -0.148 0.122 -1.211 0.226
## textual 0.576 0.117 4.918 0.000
## speed -0.177 0.090 -1.968 0.049
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.654 0.128 5.094 0.000
## .x2 0.964 0.123 7.812 0.000
## .x3 0.641 0.101 6.316 0.000
## .x4 0.343 0.062 5.534 0.000
## .x5 0.376 0.073 5.133 0.000
## .x6 0.437 0.067 6.559 0.000
## .x7 0.625 0.095 6.574 0.000
## .x8 0.434 0.088 4.914 0.000
## .x9 0.522 0.086 6.102 0.000
## visual 0.708 0.160 4.417 0.000
## textual 0.870 0.131 6.659 0.000
## speed 0.505 0.115 4.379 0.000
anova(mweak, mstrong)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
mweak | 54 | 7481 | 7681 | 124 | NA | NA | NA |
mstrong | 60 | 7509 | 7687 | 164 | 40.1 | 6 | 0 |
Consistent with the above output of measurementInvariance
, the LRT indicates that constraining the intercepts to equality worsens fit. Likewise, the AIC is 28 points worse, a large reduction in relative fit. What might be the problem? By constraining the loadings and intercepts to equality, we are implying that at a given level of the factor, the expected value of each indicator shoud be equal between groups. This is true because we have equated the strength of association between the factor and the indicators via the loadings.
But if the strong invariance model is worse than the weak MI model, it implies that the expected value of at least one indicator at a given level of the factor differs between groups. In other words, the average level of an indicator is higher or lower in one group even after accounting for the part explained by the factor.
When mean structure is considered in the common factor model, the predicted values for a given indicator, \(\textbf{y}_i\), are:
\[ \textbf{y}_i = \nu_i + \boldsymbol{\Lambda}_y \boldsymbol{\eta}_i + \boldsymbol{\varepsilon}_i \]
Note that we don’t have an any covariates predicting factor scores or indicators. These would be require an extension of the model. And more matrices!
But the main idea here is that we now have an intercept \(\nu_i\) for the item \(\textbf{y}_i\). We often add mean structure to a SEM for two reasons:
Circling back to our failure to corroborate strong invariance, we might wish to look at which constraints on the intercepts are leading to the greatest reduction in fit. Whereas the modificationIndices
in lavaan
gives information on change in model \(\chi^2\) if we add additional parameters, we need to use the lavTestScore
function to examine how model \(\chi^2\) would improve if we freed (removed) these constraints. Note that this function is still based on the Lagrange multiplier test, as in standard modification indices.
#what is the effect of freeing constraints of the strong relative to the weak?
lavTestScore(mstrong, epc=TRUE)
## $test
##
## total score test:
##
## test X2 df p.value
## 1 score 47 15 0
##
## $uni
##
## univariate score tests:
##
## lhs op rhs X2 df p.value
## 1 .p2. == .p38. 0.306 1 0.580
## 2 .p3. == .p39. 1.636 1 0.201
## 3 .p5. == .p41. 2.744 1 0.098
## 4 .p6. == .p42. 2.627 1 0.105
## 5 .p8. == .p44. 0.027 1 0.871
## 6 .p9. == .p45. 0.004 1 0.952
## 7 .p25. == .p61. 5.847 1 0.016
## 8 .p26. == .p62. 6.863 1 0.009
## 9 .p27. == .p63. 19.193 1 0.000
## 10 .p28. == .p64. 2.139 1 0.144
## 11 .p29. == .p65. 1.563 1 0.211
## 12 .p30. == .p66. 0.032 1 0.857
## 13 .p31. == .p67. 15.021 1 0.000
## 14 .p32. == .p68. 4.710 1 0.030
## 15 .p33. == .p69. 1.498 1 0.221
##
## $epc
##
## expected parameter changes (epc) and expected parameter values (epv):
##
## lhs op rhs group free label plabel est epc epv
## 1 visual =~ x1 1 0 .p1. NA NA NA
## 2 visual =~ x2 1 1 .p2. .p2. 0.576 -0.136 0.440
## 3 visual =~ x3 1 2 .p3. .p3. 0.798 -0.216 0.582
## 4 textual =~ x4 1 0 .p4. NA NA NA
## 5 textual =~ x5 1 3 .p5. .p5. 1.120 0.074 1.194
## 6 textual =~ x6 1 4 .p6. .p6. 0.932 -0.049 0.883
## 7 speed =~ x7 1 0 .p7. NA NA NA
## 8 speed =~ x8 1 5 .p8. .p8. 1.130 0.038 1.168
## 9 speed =~ x9 1 6 .p9. .p9. 1.009 0.006 1.015
## 10 x1 ~~ x1 1 7 .p10. 0.555 -0.151 0.405
## 11 x2 ~~ x2 1 8 .p11. 1.296 0.030 1.327
## 12 x3 ~~ x3 1 9 .p12. 0.944 0.089 1.034
## 13 x4 ~~ x4 1 10 .p13. 0.445 -0.003 0.443
## 14 x5 ~~ x5 1 11 .p14. 0.502 -0.041 0.461
## 15 x6 ~~ x6 1 12 .p15. 0.263 0.027 0.291
## 16 x7 ~~ x7 1 13 .p16. 0.888 0.006 0.894
## 17 x8 ~~ x8 1 14 .p17. 0.541 -0.010 0.531
## 18 x9 ~~ x9 1 15 .p18. 0.654 0.005 0.658
## 19 visual ~~ visual 1 16 .p19. 0.796 0.198 0.994
## 20 textual ~~ textual 1 17 .p20. 0.879 0.000 0.879
## 21 speed ~~ speed 1 18 .p21. 0.322 -0.010 0.312
## 22 visual ~~ textual 1 19 .p22. 0.410 0.037 0.447
## 23 visual ~~ speed 1 20 .p23. 0.178 0.013 0.191
## 24 textual ~~ speed 1 21 .p24. 0.180 -0.003 0.177
## 25 x1 ~1 1 22 .p25. .p25. 5.001 -0.060 4.941
## 26 x2 ~1 1 23 .p26. .p26. 6.151 -0.167 5.984
## 27 x3 ~1 1 24 .p27. .p27. 2.271 0.216 2.487
## 28 x4 ~1 1 25 .p28. .p28. 2.778 0.045 2.823
## 29 x5 ~1 1 26 .p29. .p29. 4.035 -0.040 3.995
## 30 x6 ~1 1 27 .p30. .p30. 1.926 -0.003 1.922
## 31 x7 ~1 1 28 .p31. .p31. 4.242 0.190 4.432
## 32 x8 ~1 1 29 .p32. .p32. 5.630 -0.067 5.563
## 33 x9 ~1 1 30 .p33. .p33. 5.465 -0.048 5.418
## 34 visual ~1 1 0 .p34. NA NA NA
## 35 textual ~1 1 0 .p35. NA NA NA
## 36 speed ~1 1 0 .p36. NA NA NA
## 37 visual =~ x1 2 0 .p37. NA NA NA
## 38 visual =~ x2 2 31 .p2. .p38. 0.576 0.119 0.695
## 39 visual =~ x3 2 32 .p3. .p39. 0.798 0.065 0.863
## 40 textual =~ x4 2 0 .p40. NA NA NA
## 41 textual =~ x5 2 33 .p5. .p41. 1.120 -0.142 0.978
## 42 textual =~ x6 2 34 .p6. .p42. 0.932 0.031 0.963
## 43 speed =~ x7 2 0 .p43. NA NA NA
## 44 speed =~ x8 2 35 .p8. .p44. 1.130 0.118 1.247
## 45 speed =~ x9 2 36 .p9. .p45. 1.009 0.097 1.106
## 46 x1 ~~ x1 2 37 .p46. 0.654 0.048 0.701
## 47 x2 ~~ x2 2 38 .p47. 0.964 -0.023 0.941
## 48 x3 ~~ x3 2 39 .p48. 0.641 -0.013 0.628
## 49 x4 ~~ x4 2 40 .p49. 0.343 -0.027 0.316
## 50 x5 ~~ x5 2 41 .p50. 0.376 0.061 0.437
## 51 x6 ~~ x6 2 42 .p51. 0.437 -0.026 0.411
## 52 x7 ~~ x7 2 43 .p52. 0.625 0.027 0.652
## 53 x8 ~~ x8 2 44 .p53. 0.434 -0.019 0.415
## 54 x9 ~~ x9 2 45 .p54. 0.522 -0.006 0.516
## 55 visual ~~ visual 2 46 .p55. 0.708 -0.085 0.623
## 56 textual ~~ textual 2 47 .p56. 0.870 0.073 0.943
## 57 speed ~~ speed 2 48 .p57. 0.505 -0.075 0.430
## 58 visual ~~ textual 2 49 .p58. 0.427 -0.004 0.423
## 59 visual ~~ speed 2 50 .p59. 0.329 -0.042 0.286
## 60 textual ~~ speed 2 51 .p60. 0.236 -0.008 0.228
## 61 x1 ~1 2 52 .p25. .p61. 5.001 0.061 5.062
## 62 x2 ~1 2 53 .p26. .p62. 6.151 0.143 6.294
## 63 x3 ~1 2 54 .p27. .p63. 2.271 -0.160 2.111
## 64 x4 ~1 2 55 .p28. .p64. 2.778 -0.056 2.722
## 65 x5 ~1 2 56 .p29. .p65. 4.035 0.092 4.127
## 66 x6 ~1 2 57 .p30. .p66. 1.926 -0.030 1.896
## 67 x7 ~1 2 58 .p31. .p67. 4.242 -0.145 4.097
## 68 x8 ~1 2 59 .p32. .p68. 5.630 0.078 5.708
## 69 x9 ~1 2 60 .p33. .p69. 5.465 0.057 5.522
## 70 visual ~1 2 61 .p70. -0.148 0.015 -0.133
## 71 textual ~1 2 62 .p71. 0.576 0.019 0.596
## 72 speed ~1 2 63 .p72. -0.177 0.001 -0.176
The epc=TRUE
argument requests that lavaan compute the “expected parameter change” that would result from freeing the constraint. This also gives us a readout of the parameter labels used by lavaan to set constraints. You can also examine parTable
to understand the parameter labels. Note the addition of the group
column.
parTable(mstrong)
id | lhs | op | rhs | user | block | group | free | ustart | exo | label | plabel | start | est | se |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | visual | =~ | x1 | 1 | 1 | 1 | 0 | 1 | 0 | .p1. | 1.000 | 1.000 | 0.000 | |
2 | visual | =~ | x2 | 1 | 1 | 1 | 1 | NA | 0 | .p2. | .p2. | 0.769 | 0.576 | 0.101 |
3 | visual | =~ | x3 | 1 | 1 | 1 | 2 | NA | 0 | .p3. | .p3. | 1.186 | 0.798 | 0.112 |
4 | textual | =~ | x4 | 1 | 1 | 1 | 0 | 1 | 0 | .p4. | 1.000 | 1.000 | 0.000 | |
5 | textual | =~ | x5 | 1 | 1 | 1 | 3 | NA | 0 | .p5. | .p5. | 1.237 | 1.120 | 0.066 |
6 | textual | =~ | x6 | 1 | 1 | 1 | 4 | NA | 0 | .p6. | .p6. | 0.865 | 0.932 | 0.056 |
7 | speed | =~ | x7 | 1 | 1 | 1 | 0 | 1 | 0 | .p7. | 1.000 | 1.000 | 0.000 | |
8 | speed | =~ | x8 | 1 | 1 | 1 | 5 | NA | 0 | .p8. | .p8. | 1.227 | 1.130 | 0.145 |
9 | speed | =~ | x9 | 1 | 1 | 1 | 6 | NA | 0 | .p9. | .p9. | 0.827 | 1.009 | 0.132 |
10 | x1 | ~~ | x1 | 0 | 1 | 1 | 7 | NA | 0 | .p10. | 0.698 | 0.555 | 0.139 | |
11 | x2 | ~~ | x2 | 0 | 1 | 1 | 8 | NA | 0 | .p11. | 0.752 | 1.296 | 0.158 | |
12 | x3 | ~~ | x3 | 0 | 1 | 1 | 9 | NA | 0 | .p12. | 0.673 | 0.944 | 0.136 | |
13 | x4 | ~~ | x4 | 0 | 1 | 1 | 10 | NA | 0 | .p13. | 0.660 | 0.445 | 0.069 | |
14 | x5 | ~~ | x5 | 0 | 1 | 1 | 11 | NA | 0 | .p14. | 0.854 | 0.502 | 0.082 | |
15 | x6 | ~~ | x6 | 0 | 1 | 1 | 12 | NA | 0 | .p15. | 0.487 | 0.263 | 0.050 | |
16 | x7 | ~~ | x7 | 0 | 1 | 1 | 13 | NA | 0 | .p16. | 0.585 | 0.888 | 0.120 | |
17 | x8 | ~~ | x8 | 0 | 1 | 1 | 14 | NA | 0 | .p17. | 0.476 | 0.541 | 0.095 | |
18 | x9 | ~~ | x9 | 0 | 1 | 1 | 15 | NA | 0 | .p18. | 0.489 | 0.654 | 0.096 | |
19 | visual | ~~ | visual | 0 | 1 | 1 | 16 | NA | 0 | .p19. | 0.050 | 0.796 | 0.172 | |
20 | textual | ~~ | textual | 0 | 1 | 1 | 17 | NA | 0 | .p20. | 0.050 | 0.879 | 0.131 | |
21 | speed | ~~ | speed | 0 | 1 | 1 | 18 | NA | 0 | .p21. | 0.050 | 0.322 | 0.082 | |
22 | visual | ~~ | textual | 0 | 1 | 1 | 19 | NA | 0 | .p22. | 0.000 | 0.410 | 0.095 | |
23 | visual | ~~ | speed | 0 | 1 | 1 | 20 | NA | 0 | .p23. | 0.000 | 0.178 | 0.066 | |
24 | textual | ~~ | speed | 0 | 1 | 1 | 21 | NA | 0 | .p24. | 0.000 | 0.180 | 0.062 | |
25 | x1 | ~1 | 0 | 1 | 1 | 22 | NA | 0 | .p25. | .p25. | 4.941 | 5.001 | 0.090 | |
26 | x2 | ~1 | 0 | 1 | 1 | 23 | NA | 0 | .p26. | .p26. | 5.984 | 6.151 | 0.077 | |
27 | x3 | ~1 | 0 | 1 | 1 | 24 | NA | 0 | .p27. | .p27. | 2.487 | 2.271 | 0.083 | |
28 | x4 | ~1 | 0 | 1 | 1 | 25 | NA | 0 | .p28. | .p28. | 2.823 | 2.778 | 0.087 | |
29 | x5 | ~1 | 0 | 1 | 1 | 26 | NA | 0 | .p29. | .p29. | 3.995 | 4.035 | 0.096 | |
30 | x6 | ~1 | 0 | 1 | 1 | 27 | NA | 0 | .p30. | .p30. | 1.922 | 1.926 | 0.079 | |
31 | x7 | ~1 | 0 | 1 | 1 | 28 | NA | 0 | .p31. | .p31. | 4.432 | 4.242 | 0.073 | |
32 | x8 | ~1 | 0 | 1 | 1 | 29 | NA | 0 | .p32. | .p32. | 5.563 | 5.630 | 0.072 | |
33 | x9 | ~1 | 0 | 1 | 1 | 30 | NA | 0 | .p33. | .p33. | 5.418 | 5.465 | 0.069 | |
34 | visual | ~1 | 0 | 1 | 1 | 0 | 0 | 0 | .p34. | 0.000 | 0.000 | 0.000 | ||
35 | textual | ~1 | 0 | 1 | 1 | 0 | 0 | 0 | .p35. | 0.000 | 0.000 | 0.000 | ||
36 | speed | ~1 | 0 | 1 | 1 | 0 | 0 | 0 | .p36. | 0.000 | 0.000 | 0.000 | ||
37 | visual | =~ | x1 | 1 | 2 | 2 | 0 | 1 | 0 | .p37. | 1.000 | 1.000 | 0.000 | |
38 | visual | =~ | x2 | 1 | 2 | 2 | 31 | NA | 0 | .p2. | .p38. | 0.896 | 0.576 | 0.101 |
39 | visual | =~ | x3 | 1 | 2 | 2 | 32 | NA | 0 | .p3. | .p39. | 1.155 | 0.798 | 0.112 |
40 | textual | =~ | x4 | 1 | 2 | 2 | 0 | 1 | 0 | .p40. | 1.000 | 1.000 | 0.000 | |
41 | textual | =~ | x5 | 1 | 2 | 2 | 33 | NA | 0 | .p5. | .p41. | 0.991 | 1.120 | 0.066 |
42 | textual | =~ | x6 | 1 | 2 | 2 | 34 | NA | 0 | .p6. | .p42. | 0.962 | 0.932 | 0.056 |
43 | speed | =~ | x7 | 1 | 2 | 2 | 0 | 1 | 0 | .p43. | 1.000 | 1.000 | 0.000 | |
44 | speed | =~ | x8 | 1 | 2 | 2 | 35 | NA | 0 | .p8. | .p44. | 1.282 | 1.130 | 0.145 |
45 | speed | =~ | x9 | 1 | 2 | 2 | 36 | NA | 0 | .p9. | .p45. | 0.895 | 1.009 | 0.132 |
46 | x1 | ~~ | x1 | 0 | 2 | 2 | 37 | NA | 0 | .p46. | 0.659 | 0.654 | 0.128 | |
47 | x2 | ~~ | x2 | 0 | 2 | 2 | 38 | NA | 0 | .p47. | 0.613 | 0.964 | 0.123 | |
48 | x3 | ~~ | x3 | 0 | 2 | 2 | 39 | NA | 0 | .p48. | 0.537 | 0.641 | 0.101 | |
49 | x4 | ~~ | x4 | 0 | 2 | 2 | 40 | NA | 0 | .p49. | 0.629 | 0.343 | 0.062 | |
50 | x5 | ~~ | x5 | 0 | 2 | 2 | 41 | NA | 0 | .p50. | 0.671 | 0.376 | 0.073 | |
51 | x6 | ~~ | x6 | 0 | 2 | 2 | 42 | NA | 0 | .p51. | 0.640 | 0.437 | 0.067 | |
52 | x7 | ~~ | x7 | 0 | 2 | 2 | 43 | NA | 0 | .p52. | 0.531 | 0.625 | 0.095 | |
53 | x8 | ~~ | x8 | 0 | 2 | 2 | 44 | NA | 0 | .p53. | 0.547 | 0.434 | 0.088 | |
54 | x9 | ~~ | x9 | 0 | 2 | 2 | 45 | NA | 0 | .p54. | 0.526 | 0.522 | 0.086 | |
55 | visual | ~~ | visual | 0 | 2 | 2 | 46 | NA | 0 | .p55. | 0.050 | 0.708 | 0.160 | |
56 | textual | ~~ | textual | 0 | 2 | 2 | 47 | NA | 0 | .p56. | 0.050 | 0.870 | 0.131 | |
57 | speed | ~~ | speed | 0 | 2 | 2 | 48 | NA | 0 | .p57. | 0.050 | 0.505 | 0.115 | |
58 | visual | ~~ | textual | 0 | 2 | 2 | 49 | NA | 0 | .p58. | 0.000 | 0.427 | 0.097 | |
59 | visual | ~~ | speed | 0 | 2 | 2 | 50 | NA | 0 | .p59. | 0.000 | 0.329 | 0.082 | |
60 | textual | ~~ | speed | 0 | 2 | 2 | 51 | NA | 0 | .p60. | 0.000 | 0.236 | 0.073 | |
61 | x1 | ~1 | 0 | 2 | 2 | 52 | NA | 0 | .p25. | .p61. | 4.930 | 5.001 | 0.090 | |
62 | x2 | ~1 | 0 | 2 | 2 | 53 | NA | 0 | .p26. | .p62. | 6.200 | 6.151 | 0.077 | |
63 | x3 | ~1 | 0 | 2 | 2 | 54 | NA | 0 | .p27. | .p63. | 1.996 | 2.271 | 0.083 | |
64 | x4 | ~1 | 0 | 2 | 2 | 55 | NA | 0 | .p28. | .p64. | 3.317 | 2.778 | 0.087 | |
65 | x5 | ~1 | 0 | 2 | 2 | 56 | NA | 0 | .p29. | .p65. | 4.712 | 4.035 | 0.096 | |
66 | x6 | ~1 | 0 | 2 | 2 | 57 | NA | 0 | .p30. | .p66. | 2.469 | 1.926 | 0.079 | |
67 | x7 | ~1 | 0 | 2 | 2 | 58 | NA | 0 | .p31. | .p67. | 3.921 | 4.242 | 0.073 | |
68 | x8 | ~1 | 0 | 2 | 2 | 59 | NA | 0 | .p32. | .p68. | 5.488 | 5.630 | 0.072 | |
69 | x9 | ~1 | 0 | 2 | 2 | 60 | NA | 0 | .p33. | .p69. | 5.327 | 5.465 | 0.069 | |
70 | visual | ~1 | 0 | 2 | 2 | 61 | NA | 0 | .p70. | 0.000 | -0.148 | 0.122 | ||
71 | textual | ~1 | 0 | 2 | 2 | 62 | NA | 0 | .p71. | 0.000 | 0.576 | 0.117 | ||
72 | speed | ~1 | 0 | 2 | 2 | 63 | NA | 0 | .p72. | 0.000 | -0.177 | 0.090 | ||
73 | .p2. | == | .p38. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
74 | .p3. | == | .p39. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
75 | .p5. | == | .p41. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
76 | .p6. | == | .p42. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
77 | .p8. | == | .p44. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
78 | .p9. | == | .p45. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
79 | .p25. | == | .p61. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
80 | .p26. | == | .p62. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
81 | .p27. | == | .p63. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
82 | .p28. | == | .p64. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
83 | .p29. | == | .p65. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
84 | .p30. | == | .p66. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
85 | .p31. | == | .p67. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
86 | .p32. | == | .p68. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 | ||
87 | .p33. | == | .p69. | 2 | 0 | 0 | 0 | NA | 0 | 0.000 | 0.000 | 0.000 |
Examining the output of lavTestScore
, we see that two constraints are associated with the lion’s share of decrement in fit – 34 of 51 points. These are:
.p27. == .p63.
\(\chi^2\) = 19.24
.p31. == .p67.
\(\chi^2\) = 15.01
From the output, we see that p27 and p63 represent intercept equality for x3
, whereas p31 and p67 represent intercept equality for x7
. Freeing these between groups would result in partial strong invariance in which 7 of 9 indicators have equal loadings and intercepts. This yields a model with one item having differential item functioning (DIF) for the visual
and speed
factor. Partial invariance is often seen as acceptable (in the sense of permitting group comparisons on factor means and variances/covariances) if only a handful of equality constraints are not satisfied. Here, because we have only three indicators per factor, noninvariance of one indicator may make comparing factor means/variances more dubious than if we had 10 indicators per factor.
Setting aside this complexity, if we wished to allow the intercepts for x1
and x3
to vary by group, while otherwise enforcing strong invariance, we could use the group.partial
argument, which frees selected parameters while equating the remainder:
mpartial_strong <- sem(HS.model, data = HolzingerSwineford1939, std.lv=FALSE, group = "school", group.equal=c("loadings", "intercepts"), group.partial=c("x3~1", "x7~1"), meanstructure=TRUE)
summary(mpartial_strong)
## lavaan (0.5-23.1097) converged normally after 61 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 129.422
## Degrees of freedom 58
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 71.170
## Grant-White 58.253
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 (.p2.) 0.606 0.101 5.988 0.000
## x3 (.p3.) 0.791 0.109 7.259 0.000
## textual =~
## x4 1.000
## x5 (.p5.) 1.120 0.066 16.960 0.000
## x6 (.p6.) 0.932 0.056 16.606 0.000
## speed =~
## x7 1.000
## x8 (.p8.) 1.200 0.155 7.741 0.000
## x9 (.p9.) 1.041 0.136 7.635 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.404 0.095 4.247 0.000
## speed 0.168 0.064 2.647 0.008
## textual ~~
## speed 0.172 0.060 2.882 0.004
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.25.) 4.914 0.092 53.538 0.000
## .x2 (.26.) 6.087 0.079 76.999 0.000
## .x3 2.487 0.094 26.474 0.000
## .x4 (.28.) 2.778 0.087 31.953 0.000
## .x5 (.29.) 4.035 0.096 41.861 0.000
## .x6 (.30.) 1.926 0.079 24.425 0.000
## .x7 4.432 0.086 51.533 0.000
## .x8 (.32.) 5.569 0.074 75.328 0.000
## .x9 (.33.) 5.409 0.070 77.182 0.000
## visual 0.000
## textual 0.000
## speed 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.560 0.137 4.086 0.000
## .x2 1.267 0.156 8.105 0.000
## .x3 0.879 0.128 6.850 0.000
## .x4 0.446 0.069 6.432 0.000
## .x5 0.502 0.082 6.132 0.000
## .x6 0.263 0.050 5.258 0.000
## .x7 0.850 0.114 7.471 0.000
## .x8 0.516 0.095 5.429 0.000
## .x9 0.656 0.096 6.852 0.000
## visual 0.796 0.170 4.691 0.000
## textual 0.879 0.131 6.693 0.000
## speed 0.304 0.078 3.918 0.000
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 (.p2.) 0.606 0.101 5.988 0.000
## x3 (.p3.) 0.791 0.109 7.259 0.000
## textual =~
## x4 1.000
## x5 (.p5.) 1.120 0.066 16.960 0.000
## x6 (.p6.) 0.932 0.056 16.606 0.000
## speed =~
## x7 1.000
## x8 (.p8.) 1.200 0.155 7.741 0.000
## x9 (.p9.) 1.041 0.136 7.635 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.426 0.097 4.412 0.000
## speed 0.312 0.079 3.955 0.000
## textual ~~
## speed 0.223 0.071 3.163 0.002
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.25.) 4.914 0.092 53.538 0.000
## .x2 (.26.) 6.087 0.079 76.999 0.000
## .x3 1.955 0.108 18.170 0.000
## .x4 (.28.) 2.778 0.087 31.953 0.000
## .x5 (.29.) 4.035 0.096 41.861 0.000
## .x6 (.30.) 1.926 0.079 24.425 0.000
## .x7 3.992 0.094 42.478 0.000
## .x8 (.32.) 5.569 0.074 75.328 0.000
## .x9 (.33.) 5.409 0.070 77.182 0.000
## visual 0.051 0.129 0.393 0.695
## textual 0.576 0.117 4.918 0.000
## speed -0.071 0.089 -0.800 0.424
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.651 0.127 5.138 0.000
## .x2 0.939 0.122 7.721 0.000
## .x3 0.603 0.096 6.248 0.000
## .x4 0.343 0.062 5.532 0.000
## .x5 0.377 0.073 5.136 0.000
## .x6 0.437 0.067 6.556 0.000
## .x7 0.599 0.090 6.655 0.000
## .x8 0.407 0.089 4.570 0.000
## .x9 0.531 0.086 6.186 0.000
## visual 0.715 0.160 4.473 0.000
## textual 0.870 0.131 6.659 0.000
## speed 0.475 0.109 4.344 0.000
anova(mweak, mpartial_strong)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
mweak | 54 | 7481 | 7681 | 124 | NA | NA | NA |
mpartial_strong | 58 | 7478 | 7663 | 129 | 5.38 | 4 | 0.251 |
We now see that our partial strong invariance model does not fit worse than the weak model. Examining the parameter estimates, we also see that the mean of x3
and x7
is higher in the Pasteur school compared ot Grant-White. One might then infer that there are school-specific differences in these cognitive test scores that are not attributable to differences in the corresponding latent factors, visual and speed, respectively.
We might then entertain a strict variant. Once we allow partial noninvariance in a testing step, we should typically carry this freedom forward to the next step. Here, we should probably retain noninvariance of intercepts for x3
and x7
when we test the (partial) strict invariance model.
mstrict <- cfa(HS.model, data = HolzingerSwineford1939, std.lv=FALSE, group = "school", group.equal=c("loadings", "intercepts", "residuals"), group.partial=c("x3~1", "x7~1"))
summary(mstrict)
## lavaan (0.5-23.1097) converged normally after 59 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 147.260
## Degrees of freedom 67
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 79.438
## Grant-White 67.822
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 (.p2.) 0.624 0.104 5.986 0.000
## x3 (.p3.) 0.825 0.113 7.288 0.000
## textual =~
## x4 1.000
## x5 (.p5.) 1.125 0.066 17.130 0.000
## x6 (.p6.) 0.934 0.056 16.749 0.000
## speed =~
## x7 1.000
## x8 (.p8.) 1.194 0.162 7.355 0.000
## x9 (.p9.) 1.068 0.146 7.308 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.363 0.094 3.875 0.000
## speed 0.165 0.063 2.627 0.009
## textual ~~
## speed 0.168 0.060 2.807 0.005
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.25.) 4.910 0.093 52.673 0.000
## .x2 (.26.) 6.072 0.079 76.983 0.000
## .x3 2.487 0.090 27.772 0.000
## .x4 (.28.) 2.784 0.086 32.195 0.000
## .x5 (.29.) 4.029 0.096 41.811 0.000
## .x6 (.30.) 1.927 0.081 23.746 0.000
## .x7 4.432 0.082 53.865 0.000
## .x8 (.32.) 5.568 0.073 75.821 0.000
## .x9 (.33.) 5.411 0.071 76.698 0.000
## visual 0.000
## textual 0.000
## speed 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.10.) 0.636 0.101 6.303 0.000
## .x2 (.11.) 1.101 0.100 10.973 0.000
## .x3 (.12.) 0.724 0.085 8.529 0.000
## .x4 (.13.) 0.383 0.047 8.096 0.000
## .x5 (.14.) 0.435 0.057 7.613 0.000
## .x6 (.15.) 0.353 0.042 8.336 0.000
## .x7 (.16.) 0.738 0.076 9.696 0.000
## .x8 (.17.) 0.479 0.073 6.592 0.000
## .x9 (.18.) 0.580 0.069 8.386 0.000
## visual 0.776 0.164 4.737 0.000
## textual 0.893 0.131 6.826 0.000
## speed 0.318 0.080 3.990 0.000
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## visual =~
## x1 1.000
## x2 (.p2.) 0.624 0.104 5.986 0.000
## x3 (.p3.) 0.825 0.113 7.288 0.000
## textual =~
## x4 1.000
## x5 (.p5.) 1.125 0.066 17.130 0.000
## x6 (.p6.) 0.934 0.056 16.749 0.000
## speed =~
## x7 1.000
## x8 (.p8.) 1.194 0.162 7.355 0.000
## x9 (.p9.) 1.068 0.146 7.308 0.000
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## visual ~~
## textual 0.421 0.095 4.444 0.000
## speed 0.315 0.078 4.029 0.000
## textual ~~
## speed 0.223 0.071 3.148 0.002
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.25.) 4.910 0.093 52.673 0.000
## .x2 (.26.) 6.072 0.079 76.983 0.000
## .x3 1.951 0.114 17.044 0.000
## .x4 (.28.) 2.784 0.086 32.195 0.000
## .x5 (.29.) 4.029 0.096 41.811 0.000
## .x6 (.30.) 1.927 0.081 23.746 0.000
## .x7 3.992 0.099 40.135 0.000
## .x8 (.32.) 5.568 0.073 75.821 0.000
## .x9 (.33.) 5.411 0.071 76.698 0.000
## visual 0.054 0.128 0.423 0.672
## textual 0.575 0.118 4.888 0.000
## speed -0.071 0.089 -0.805 0.421
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## .x1 (.10.) 0.636 0.101 6.303 0.000
## .x2 (.11.) 1.101 0.100 10.973 0.000
## .x3 (.12.) 0.724 0.085 8.529 0.000
## .x4 (.13.) 0.383 0.047 8.096 0.000
## .x5 (.14.) 0.435 0.057 7.613 0.000
## .x6 (.15.) 0.353 0.042 8.336 0.000
## .x7 (.16.) 0.738 0.076 9.696 0.000
## .x8 (.17.) 0.479 0.073 6.592 0.000
## .x9 (.18.) 0.580 0.069 8.386 0.000
## visual 0.664 0.150 4.436 0.000
## textual 0.876 0.132 6.620 0.000
## speed 0.446 0.109 4.095 0.000
anova(mpartial_strong, mstrict)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
mpartial_strong | 58 | 7478 | 7663 | 129 | NA | NA | NA |
mstrict | 67 | 7478 | 7630 | 147 | 17.8 | 9 | 0.037 |
We do not see a substantial decrement in fit for adding strict invariance: the LRT is significant (bad), but the AIC is essentially the same. We might examine change in CFI (recall: reductions of .01 or more should be cause for concern) as well.
In all of these models, variance and covariances among latent variables are freely estimated among groups. One can also consider structural invariance in which variances, covariances, and/or structural regressions among variables can be equated among groups. For example, one might wish to know whether the association between neuroticism and extraversion is similar in college students compared to Wall Street executives.
Structural invariance is more about testing a substantively interesting hypothesis than verifying that we can compare the latent variables in the first place. Thus, measurement invariance is about examining potential problems in the comparability of the factors across groups. If we satisfy at least strong MI we could look at structural invariance. But if structural invariance fails, it doesn’t mean anything bad in a psychometric sense, more than the groups are legitimately different in their mean/covariance structure at the factor level.
If we want to equate factor covariances in a model that at least satisfied strong measurement invariance, we can add group.equal="lv.covariances"
. Although lavaan will implement this for us, note models that equate both variances and covariances between groups are likely to be more interpretable. What does it mean to have identical factor covariance across groups when the variances are different? Thus, I would advocate also adding "lv.variances"
in general.
mstructural <- cfa(HS.model, data = HolzingerSwineford1939, std.lv=FALSE, group = "school", group.equal=c("loadings", "intercepts", "residuals", "lv.variances", "lv.covariances"), group.partial=c("x3~1", "x7~1"))
summary(mstructural, standardized=TRUE)
## lavaan (0.5-23.1097) converged normally after 59 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 153.258
## Degrees of freedom 73
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 81.641
## Grant-White 71.617
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual =~
## x1 1.000 0.869 0.745
## x2 (.p2.) 0.599 0.102 5.847 0.000 0.521 0.443
## x3 (.p3.) 0.789 0.111 7.079 0.000 0.685 0.622
## textual =~
## x4 1.000 0.941 0.835
## x5 (.p5.) 1.124 0.066 17.112 0.000 1.057 0.848
## x6 (.p6.) 0.935 0.056 16.768 0.000 0.879 0.829
## speed =~
## x7 1.000 0.618 0.585
## x8 (.p8.) 1.186 0.162 7.303 0.000 0.733 0.726
## x9 (.p9.) 1.066 0.146 7.280 0.000 0.659 0.655
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual ~~
## textual (.22.) 0.403 0.070 5.751 0.000 0.493 0.493
## speed (.23.) 0.244 0.054 4.528 0.000 0.454 0.454
## textual ~~
## speed (.24.) 0.196 0.048 4.049 0.000 0.337 0.337
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.25.) 4.912 0.092 53.595 0.000 4.912 4.215
## .x2 (.26.) 6.074 0.077 78.473 0.000 6.074 5.167
## .x3 2.487 0.088 28.188 0.000 2.487 2.257
## .x4 (.28.) 2.784 0.086 32.312 0.000 2.784 2.472
## .x5 (.29.) 4.029 0.096 41.986 0.000 4.029 3.231
## .x6 (.30.) 1.927 0.081 23.820 0.000 1.927 1.817
## .x7 4.432 0.085 52.360 0.000 4.432 4.192
## .x8 (.32.) 5.568 0.077 72.219 0.000 5.568 5.513
## .x9 (.33.) 5.411 0.074 73.380 0.000 5.411 5.375
## visual 0.000 0.000 0.000
## textual 0.000 0.000 0.000
## speed 0.000 0.000 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.10.) 0.603 0.105 5.732 0.000 0.603 0.444
## .x2 (.11.) 1.111 0.101 11.020 0.000 1.111 0.804
## .x3 (.12.) 0.745 0.086 8.701 0.000 0.745 0.614
## .x4 (.13.) 0.383 0.047 8.095 0.000 0.383 0.302
## .x5 (.14.) 0.438 0.057 7.650 0.000 0.438 0.281
## .x6 (.15.) 0.352 0.042 8.305 0.000 0.352 0.313
## .x7 (.16.) 0.735 0.076 9.625 0.000 0.735 0.658
## .x8 (.17.) 0.482 0.073 6.574 0.000 0.482 0.473
## .x9 (.18.) 0.579 0.070 8.300 0.000 0.579 0.571
## visual (.19.) 0.754 0.136 5.548 0.000 1.000 1.000
## textual (.20.) 0.885 0.103 8.621 0.000 1.000 1.000
## speed (.21.) 0.383 0.083 4.587 0.000 1.000 1.000
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual =~
## x1 1.000 0.869 0.745
## x2 (.p2.) 0.599 0.102 5.847 0.000 0.521 0.443
## x3 (.p3.) 0.789 0.111 7.079 0.000 0.685 0.622
## textual =~
## x4 1.000 0.941 0.835
## x5 (.p5.) 1.124 0.066 17.112 0.000 1.057 0.848
## x6 (.p6.) 0.935 0.056 16.768 0.000 0.879 0.829
## speed =~
## x7 1.000 0.618 0.585
## x8 (.p8.) 1.186 0.162 7.303 0.000 0.733 0.726
## x9 (.p9.) 1.066 0.146 7.280 0.000 0.659 0.655
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual ~~
## textual (.22.) 0.403 0.070 5.751 0.000 0.493 0.493
## speed (.23.) 0.244 0.054 4.528 0.000 0.454 0.454
## textual ~~
## speed (.24.) 0.196 0.048 4.049 0.000 0.337 0.337
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.25.) 4.912 0.092 53.595 0.000 4.912 4.215
## .x2 (.26.) 6.074 0.077 78.473 0.000 6.074 5.167
## .x3 1.957 0.111 17.606 0.000 1.957 1.776
## .x4 (.28.) 2.784 0.086 32.312 0.000 2.784 2.472
## .x5 (.29.) 4.029 0.096 41.986 0.000 4.029 3.231
## .x6 (.30.) 1.927 0.081 23.820 0.000 1.927 1.817
## .x7 3.993 0.102 39.290 0.000 3.993 3.776
## .x8 (.32.) 5.568 0.077 72.219 0.000 5.568 5.513
## .x9 (.33.) 5.411 0.074 73.380 0.000 5.411 5.375
## visual 0.049 0.129 0.381 0.703 0.057 0.057
## textual 0.575 0.118 4.885 0.000 0.611 0.611
## speed -0.072 0.089 -0.809 0.418 -0.116 -0.116
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.10.) 0.603 0.105 5.732 0.000 0.603 0.444
## .x2 (.11.) 1.111 0.101 11.020 0.000 1.111 0.804
## .x3 (.12.) 0.745 0.086 8.701 0.000 0.745 0.614
## .x4 (.13.) 0.383 0.047 8.095 0.000 0.383 0.302
## .x5 (.14.) 0.438 0.057 7.650 0.000 0.438 0.281
## .x6 (.15.) 0.352 0.042 8.305 0.000 0.352 0.313
## .x7 (.16.) 0.735 0.076 9.625 0.000 0.735 0.658
## .x8 (.17.) 0.482 0.073 6.574 0.000 0.482 0.473
## .x9 (.18.) 0.579 0.070 8.300 0.000 0.579 0.571
## visual (.19.) 0.754 0.136 5.548 0.000 1.000 1.000
## textual (.20.) 0.885 0.103 8.621 0.000 1.000 1.000
## speed (.21.) 0.383 0.083 4.587 0.000 1.000 1.000
anova(mstrict, mstructural)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
mstrict | 67 | 7478 | 7630 | 147 | NA | NA | NA |
mstructural | 73 | 7472 | 7602 | 153 | 6 | 6 | 0.423 |
Here, we see no reduction in fit for the structurally invariant model. Given that this model also satisfies at least partial strong MI, this bolsters our confidence that the factor covariance structure among cognitive tests is essentially equivalent between schools.
Finally, as part of structural invariance, we might also be interested in mean differences between schools. This is group.equal="means"
.
mmeandiff <- cfa(HS.model, data = HolzingerSwineford1939, std.lv=FALSE, group = "school", meanstructure=TRUE, group.equal=c("loadings", "intercepts", "residuals", "lv.variances", "lv.covariances", "means"), group.partial=c("x3~1", "x7~1"))
summary(mmeandiff, standardized=TRUE)
## lavaan (0.5-23.1097) converged normally after 56 iterations
##
## Number of observations per group
## Pasteur 156
## Grant-White 145
##
## Estimator ML
## Minimum Function Test Statistic 181.981
## Degrees of freedom 76
## P-value (Chi-square) 0.000
##
## Chi-square for each group:
##
## Pasteur 96.004
## Grant-White 85.977
##
## Parameter Estimates:
##
## Information Expected
## Standard Errors Standard
##
##
## Group 1 [Pasteur]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual =~
## x1 1.000 0.854 0.733
## x2 (.p2.) 0.622 0.105 5.926 0.000 0.532 0.452
## x3 (.p3.) 0.814 0.115 7.098 0.000 0.696 0.631
## textual =~
## x4 1.000 0.989 0.851
## x5 (.p5.) 1.114 0.065 17.017 0.000 1.101 0.855
## x6 (.p6.) 0.928 0.055 16.729 0.000 0.918 0.839
## speed =~
## x7 1.000 0.620 0.586
## x8 (.p8.) 1.197 0.164 7.276 0.000 0.742 0.734
## x9 (.p9.) 1.054 0.145 7.269 0.000 0.653 0.649
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual ~~
## textual (.22.) 0.407 0.072 5.627 0.000 0.482 0.482
## speed (.23.) 0.236 0.053 4.448 0.000 0.446 0.446
## textual ~~
## speed (.24.) 0.187 0.050 3.762 0.000 0.305 0.305
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.25.) 4.936 0.067 73.473 0.000 4.936 4.235
## .x2 (.26.) 6.088 0.068 89.855 0.000 6.088 5.179
## .x3 2.529 0.083 30.585 0.000 2.529 2.293
## .x4 (.28.) 3.061 0.067 45.694 0.000 3.061 2.634
## .x5 (.29.) 4.341 0.074 58.452 0.000 4.341 3.369
## .x6 (.30.) 2.186 0.063 34.667 0.000 2.186 1.998
## .x7 4.425 0.080 55.451 0.000 4.425 4.185
## .x8 (.32.) 5.527 0.058 94.854 0.000 5.527 5.467
## .x9 (.33.) 5.374 0.058 92.546 0.000 5.374 5.334
## visual 0.000 0.000 0.000
## textual 0.000 0.000 0.000
## speed 0.000 0.000 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.10.) 0.628 0.104 6.048 0.000 0.628 0.463
## .x2 (.11.) 1.099 0.101 10.930 0.000 1.099 0.795
## .x3 (.12.) 0.733 0.086 8.500 0.000 0.733 0.602
## .x4 (.13.) 0.373 0.048 7.831 0.000 0.373 0.276
## .x5 (.14.) 0.447 0.058 7.674 0.000 0.447 0.269
## .x6 (.15.) 0.354 0.043 8.253 0.000 0.354 0.296
## .x7 (.16.) 0.734 0.076 9.592 0.000 0.734 0.656
## .x8 (.17.) 0.472 0.075 6.321 0.000 0.472 0.462
## .x9 (.18.) 0.588 0.070 8.403 0.000 0.588 0.579
## visual (.19.) 0.730 0.133 5.470 0.000 1.000 1.000
## textual (.20.) 0.978 0.112 8.729 0.000 1.000 1.000
## speed (.21.) 0.384 0.084 4.591 0.000 1.000 1.000
##
##
## Group 2 [Grant-White]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual =~
## x1 1.000 0.854 0.733
## x2 (.p2.) 0.622 0.105 5.926 0.000 0.532 0.452
## x3 (.p3.) 0.814 0.115 7.098 0.000 0.696 0.631
## textual =~
## x4 1.000 0.989 0.851
## x5 (.p5.) 1.114 0.065 17.017 0.000 1.101 0.855
## x6 (.p6.) 0.928 0.055 16.729 0.000 0.918 0.839
## speed =~
## x7 1.000 0.620 0.586
## x8 (.p8.) 1.197 0.164 7.276 0.000 0.742 0.734
## x9 (.p9.) 1.054 0.145 7.269 0.000 0.653 0.649
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## visual ~~
## textual (.22.) 0.407 0.072 5.627 0.000 0.482 0.482
## speed (.23.) 0.236 0.053 4.448 0.000 0.446 0.446
## textual ~~
## speed (.24.) 0.187 0.050 3.762 0.000 0.305 0.305
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.25.) 4.936 0.067 73.473 0.000 4.936 4.235
## .x2 (.26.) 6.088 0.068 89.855 0.000 6.088 5.179
## .x3 1.951 0.085 22.865 0.000 1.951 1.769
## .x4 (.28.) 3.061 0.067 45.694 0.000 3.061 2.634
## .x5 (.29.) 4.341 0.074 58.452 0.000 4.341 3.369
## .x6 (.30.) 2.186 0.063 34.667 0.000 2.186 1.998
## .x7 3.929 0.082 47.704 0.000 3.929 3.716
## .x8 (.32.) 5.527 0.058 94.854 0.000 5.527 5.467
## .x9 (.33.) 5.374 0.058 92.546 0.000 5.374 5.334
## visual 0.000 0.000 0.000
## textual 0.000 0.000 0.000
## speed 0.000 0.000 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .x1 (.10.) 0.628 0.104 6.048 0.000 0.628 0.463
## .x2 (.11.) 1.099 0.101 10.930 0.000 1.099 0.795
## .x3 (.12.) 0.733 0.086 8.500 0.000 0.733 0.602
## .x4 (.13.) 0.373 0.048 7.831 0.000 0.373 0.276
## .x5 (.14.) 0.447 0.058 7.674 0.000 0.447 0.269
## .x6 (.15.) 0.354 0.043 8.253 0.000 0.354 0.296
## .x7 (.16.) 0.734 0.076 9.592 0.000 0.734 0.656
## .x8 (.17.) 0.472 0.075 6.321 0.000 0.472 0.462
## .x9 (.18.) 0.588 0.070 8.403 0.000 0.588 0.579
## visual (.19.) 0.730 0.133 5.470 0.000 1.000 1.000
## textual (.20.) 0.978 0.112 8.729 0.000 1.000 1.000
## speed (.21.) 0.384 0.084 4.591 0.000 1.000 1.000
anova(mstructural, mmeandiff)
Df | AIC | BIC | Chisq | Chisq diff | Df diff | Pr(>Chisq) | |
---|---|---|---|---|---|---|---|
mstructural | 73 | 7472 | 7602 | 153 | NA | NA | NA |
mmeandiff | 76 | 7495 | 7613 | 182 | 28.7 | 3 | 0 |
It does not appear that we can equate the means between groups: significant LRT and drop in AIC of 23 points. Rather, examining the structurally invariant model with equal factor variances/covariances, we see that the mean of the textual factor in the Grant-White school is higher about .6 SD shift, whereas the other factors (visual and speed) are similar between schools.