7.3 Invariance Over Time#
Measurement invariance across time for quantitative item responses#
# General imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Rpy2 imports
from rpy2 import robjects as ro
from rpy2.robjects import pandas2ri, numpy2ri
from rpy2.robjects.packages import importr
# Automatic conversion of arrays and dataframes
pandas2ri.activate()
numpy2ri.activate()
# Set random seed for reproducibility
ro.r('set.seed(123)')
# Ipython extenrsion for magix plotting
%load_ext rpy2.ipython
# R imports
importr('base')
importr('lavaan')
importr('semTools')
importr('MPsychoR')
importr('lordif')
importr('Hmisc')
c:\Users\maku1542\AppData\Local\miniconda3\envs\psy126\Lib\site-packages\rpy2\robjects\packages.py:367: UserWarning: The symbol 'quartz' is not in this R namespace/package.
warnings.warn(
rpy2.robjects.packages.Package as a <module 'Hmisc'>
CFA can also be extended to longitudinal settings, where indicators are presented to the same individual at multiple points in time \(t\). Across the time points, we can consider the same measurement invariance principles as in multigroup CFA (configural, weak, and strong invariance). However, in longitudinal CFA we need to account for correlated residuals (i.e. autoregressive effects) since we cannot assume that independence holds across time points.
Load, prepare and inspect the dataset#
ro.r('data("SDOwave")')
# Convert to Python
SDOwave = pandas2ri.rpy2py(ro.globalenv['SDOwave'])
print(SDOwave.head())
I1.1996 I2.1996 I3.1996 I4.1996 I1.1997 I2.1997 I3.1997 I4.1997 \
1 2.0 2.0 4.0 4.0 1.0 1.0 4.0 1.0
2 1.0 1.0 2.0 2.0 1.0 1.0 2.0 3.0
3 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
4 2.0 2.0 3.0 3.0 2.0 2.0 2.0 2.0
5 1.0 1.0 6.0 4.0 7.0 1.0 1.0 1.0
I1.1998 I2.1998 I3.1998 I4.1998 I1.1999 I2.1999 I3.1999 I4.1999 \
1 2.0 1.0 4.0 2.0 2.0 3.0 3.0 1.0
2 4.0 2.0 5.0 4.0 3.0 2.0 4.0 4.0
3 1.0 1.0 2.0 1.0 1.0 1.0 6.0 1.0
4 2.0 2.0 2.0 2.0 2.0 1.0 1.0 1.0
5 1.0 1.0 4.0 1.0 4.0 1.0 3.0 2.0
I1.2000 I2.2000 I3.2000 I4.2000
1 2.0 1.0 5.0 3.0
2 1.0 1.0 3.0 3.0
3 1.0 1.0 5.0 1.0
4 1.0 1.0 1.0 1.0
5 1.0 1.0 3.0 2.0
The dataset#
To illustrate longitudinal CFA, we use a dataset on social dominance orientation (SDO; Sidanius and Pratto, 2001). SDO is assessed on the same individuals from 1996 to 2000 (Sidanius et al., 2010), involving the following four items, scored on a 7-point scale:
It is probably a good thing that certain groups are at the top and other groups are at the bottom (I1).
Inferior groups should stay in their place (I2).
We should do what we can to equalize conditions for different groups (I3, reversed).
Increased social equality is beneficial to society (I4, reversed).
For the moment, let us consider a simple latent variable structure where all four items load on a general SDO dimension. We pick 2 years only: 1996 vs. 1998.
The most relevant model within this context is the strong/scale invariance model, which allows us to compare the latent means across the two time points. We restrict the factor loadings as well as the corresponding intercepts of each item to be equal in both measurement occasions (strong/scale invariance). Also, we need to allow for residual covariances (using the ~~
symbol in the syntax) for each item across time points. With this we account for autoregressive effects, which has to be done when modeling longitudinal data.
Fit the models#
We will start with the scale invariance model and work our way down to lower levels of measurement invariance. (We exclude the strict invariance model here).
We will start with the scale invariance model and work our way down to lower levels of measurement invariance. (We exclude the strict invariance model here).
Scale Invariance#
# Specify the model
ro.r("""model_scale <- '
SDO1996 =~ 1*I1.1996 + a2*I2.1996 + a3*I3.1996 + a4*I4.1996
SDO1998 =~ 1*I1.1998 + a2*I2.1998 + a3*I3.1998 + a4*I4.1998
SDO1996 ~~ SDO1998
I1.1996 ~ int1*1; I1.1998 ~ int1*1
I2.1996 ~ int2*1; I2.1998 ~ int2*1
I3.1996 ~ int3*1; I3.1998 ~ int3*1
I4.1996 ~ int4*1; I4.1998 ~ int4*1
I1.1996 ~~ I1.1998
I2.1996 ~~ I2.1998
I3.1996 ~~ I3.1998
I4.1996 ~~ I4.1998
SDO1996 ~ 0*1
SDO1998 ~ 1'
""")
# Fit the model
ro.r('fit_scale <- lavaan::cfa(model_scale, data = SDOwave, estimator = "MLR")')
# Print the model output with fit measures and standardized estimates
summary_fit_scale = ro.r('summary(fit_scale, fit.measures=TRUE, standardized=TRUE)')
print(summary_fit_scale)
lavaan 0.6-19 ended normally after 45 iterations
Estimator ML
Optimization method NLMINB
Number of model parameters 30
Number of equality constraints 7
Number of observations 612
Model Test User Model:
Standard Scaled
Test Statistic 315.748 203.519
Degrees of freedom 21 21
P-value (Chi-square) 0.000 0.000
Scaling correction factor 1.551
Yuan-Bentler correction (Mplus variant)
Model Test Baseline Model:
Test statistic 1993.536 1098.021
Degrees of freedom 28 28
P-value 0.000 0.000
Scaling correction factor 1.816
User Model versus Baseline Model:
Comparative Fit Index (CFI) 0.850 0.829
Tucker-Lewis Index (TLI) 0.800 0.773
Robust Comparative Fit Index (CFI) 0.854
Robust Tucker-Lewis Index (TLI) 0.806
Loglikelihood and Information Criteria:
Loglikelihood user model (H0) -7384.052 -7384.052
Scaling correction factor 1.434
for the MLR correction
Loglikelihood unrestricted model (H1) -7226.178 -7226.178
Scaling correction factor 1.718
for the MLR correction
Akaike (AIC) 14814.104 14814.104
Bayesian (BIC) 14915.689 14915.689
Sample-size adjusted Bayesian (SABIC) 14842.669 14842.669
Root Mean Square Error of Approximation:
RMSEA 0.151 0.119
90 Percent confidence interval - lower 0.137 0.107
90 Percent confidence interval - upper 0.166 0.131
P-value H_0: RMSEA <= 0.050 0.000 0.000
P-value H_0: RMSEA >= 0.080 1.000 1.000
Robust RMSEA 0.148
90 Percent confidence interval - lower 0.130
90 Percent confidence interval - upper 0.167
P-value H_0: Robust RMSEA <= 0.050 0.000
P-value H_0: Robust RMSEA >= 0.080 1.000
Standardized Root Mean Square Residual:
SRMR 0.096 0.096
Parameter Estimates:
Standard errors Sandwich
Information bread Observed
Observed information based on Hessian
Latent Variables:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
SDO1996 =~
I1.1996 1.000 0.434 0.319
I2.1996 (a2) 0.790 0.101 7.842 0.000 0.343 0.302
I3.1996 (a3) 2.949 0.326 9.042 0.000 1.281 0.845
I4.1996 (a4) 2.965 0.353 8.389 0.000 1.288 0.952
SDO1998 =~
I1.1998 1.000 0.407 0.326
I2.1998 (a2) 0.790 0.101 7.842 0.000 0.322 0.308
I3.1998 (a3) 2.949 0.326 9.042 0.000 1.200 0.813
I4.1998 (a4) 2.965 0.353 8.389 0.000 1.207 0.930
Covariances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
SDO1996 ~~
SDO1998 0.098 0.023 4.275 0.000 0.552 0.552
.I1.1996 ~~
.I1.1998 0.425 0.075 5.697 0.000 0.425 0.280
.I2.1996 ~~
.I2.1998 0.258 0.072 3.594 0.000 0.258 0.240
.I3.1996 ~~
.I3.1998 0.220 0.076 2.911 0.004 0.220 0.316
.I4.1996 ~~
.I4.1998 -0.109 0.052 -2.078 0.038 -0.109 -0.555
Intercepts:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.I1.1996 (int1) 2.064 0.045 45.732 0.000 2.064 1.516
.I1.1998 (int1) 2.064 0.045 45.732 0.000 2.064 1.654
.I2.1996 (int2) 1.633 0.039 42.279 0.000 1.633 1.438
.I2.1998 (int2) 1.633 0.039 42.279 0.000 1.633 1.562
.I3.1996 (int3) 2.765 0.060 46.233 0.000 2.765 1.825
.I3.1998 (int3) 2.765 0.060 46.233 0.000 2.765 1.874
.I4.1996 (int4) 2.428 0.054 44.827 0.000 2.428 1.795
.I4.1998 (int4) 2.428 0.054 44.827 0.000 2.428 1.872
SDO1996 0.000 0.000 0.000
SDO1998 -0.047 0.019 -2.432 0.015 -0.115 -0.115
Variances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.I1.1996 1.664 0.126 13.170 0.000 1.664 0.898
.I2.1996 1.172 0.131 8.960 0.000 1.172 0.909
.I3.1996 0.656 0.103 6.377 0.000 0.656 0.286
.I4.1996 0.170 0.079 2.145 0.032 0.170 0.093
.I1.1998 1.391 0.104 13.370 0.000 1.391 0.894
.I2.1998 0.989 0.144 6.874 0.000 0.989 0.905
.I3.1998 0.739 0.102 7.275 0.000 0.739 0.339
.I4.1998 0.226 0.082 2.756 0.006 0.226 0.134
SDO1996 0.189 0.043 4.363 0.000 1.000 1.000
SDO1998 0.166 0.035 4.756 0.000 1.000 1.000
The parameters of main interest are the latent variable means (however, this has nothing to do with measurement invariance but can be used to compare latent score bewteen time points):
print(ro.r("parameterEstimates(fit_scale)[22:23,]"))
lhs op rhs label est se z pvalue ci.lower ci.upper
22 SDO1996 ~1 0.000 0.000 NA NA 0.000 0.000
23 SDO1998 ~1 -0.047 0.019 -2.432 0.015 -0.085 -0.009
We see that the mean for 1996 was fixed to 0 and is therefore used as the reference year. The second line suggests that there is a significant decrease in SDO from 1996 to 1998.
Note that a weak/metric invariance version of this model can be fitted by restricting both
latent means to 0 and freeing up the intercepts. For a configural invariance version,
the loadings need to be freed up as well (first two syntax lines). For a strict invariance version, intercepts, loadings and residuals need to be fixed.
If we do not want to do this by hand, the longInvariance
function from semTools
can be used
to establish such a generic sequence (similar to the measurementInvariance
function used above).
Using the anova
function, the models can be again tested against each other.
Let us fit a weak/metric invariance model.
Metric Invariance#
Adapt the code from the scale model to fit a metric model by freeing up the intercepts and restrict both latent variables to zero. Name your model model.metric
.
# Specify the model
ro.r("""model_metric <- '
SDO1996 =~ 1*I1.1996 + a2*I2.1996 + a3*I3.1996 + a4*I4.1996
SDO1998 =~ 1*I1.1998 + a2*I2.1998 + a3*I3.1998 + a4*I4.1998
SDO1996 ~~ SDO1998
I1.1996 ~ 1; I1.1998 ~ 1
I2.1996 ~ 1; I2.1998 ~ 1
I3.1996 ~ 1; I3.1998 ~ 1
I4.1996 ~ 1; I4.1998 ~ 1
I1.1996 ~~ I1.1998
I2.1996 ~~ I2.1998
I3.1996 ~~ I3.1998
I4.1996 ~~ I4.1998
SDO1996 ~ 0*1
SDO1998 ~ 0*1'
""")
# Fit the model
ro.r('fit_metric <- lavaan::cfa(model_metric, data = SDOwave, estimator = "MLR")')
# Print the model output with fit measures and standardized estimates
summary_fit_metric = ro.r('summary(fit_metric, fit.measures=TRUE, standardized=TRUE)')
print(summary_fit_metric)
lavaan 0.6-19 ended normally after 43 iterations
Estimator ML
Optimization method NLMINB
Number of model parameters 29
Number of equality constraints 3
Number of observations 612
Model Test User Model:
Standard Scaled
Test Statistic 310.762 189.360
Degrees of freedom 18 18
P-value (Chi-square) 0.000 0.000
Scaling correction factor 1.641
Yuan-Bentler correction (Mplus variant)
Model Test Baseline Model:
Test statistic 1993.536 1098.021
Degrees of freedom 28 28
P-value 0.000 0.000
Scaling correction factor 1.816
User Model versus Baseline Model:
Comparative Fit Index (CFI) 0.851 0.840
Tucker-Lewis Index (TLI) 0.768 0.751
Robust Comparative Fit Index (CFI) 0.855
Robust Tucker-Lewis Index (TLI) 0.775
Loglikelihood and Information Criteria:
Loglikelihood user model (H0) -7381.559 -7381.559
Scaling correction factor 1.588
for the MLR correction
Loglikelihood unrestricted model (H1) -7226.178 -7226.178
Scaling correction factor 1.718
for the MLR correction
Akaike (AIC) 14815.117 14815.117
Bayesian (BIC) 14929.952 14929.952
Sample-size adjusted Bayesian (SABIC) 14847.408 14847.408
Root Mean Square Error of Approximation:
RMSEA 0.163 0.125
90 Percent confidence interval - lower 0.147 0.112
90 Percent confidence interval - upper 0.179 0.137
P-value H_0: RMSEA <= 0.050 0.000 0.000
P-value H_0: RMSEA >= 0.080 1.000 1.000
Robust RMSEA 0.160
90 Percent confidence interval - lower 0.140
90 Percent confidence interval - upper 0.181
P-value H_0: Robust RMSEA <= 0.050 0.000
P-value H_0: Robust RMSEA >= 0.080 1.000
Standardized Root Mean Square Residual:
SRMR 0.096 0.096
Parameter Estimates:
Standard errors Sandwich
Information bread Observed
Observed information based on Hessian
Latent Variables:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
SDO1996 =~
I1.1996 1.000 0.437 0.321
I2.1996 (a2) 0.781 0.101 7.726 0.000 0.341 0.301
I3.1996 (a3) 2.946 0.326 9.044 0.000 1.287 0.848
I4.1996 (a4) 2.938 0.348 8.447 0.000 1.284 0.950
SDO1998 =~
I1.1998 1.000 0.410 0.328
I2.1998 (a2) 0.781 0.101 7.726 0.000 0.320 0.306
I3.1998 (a3) 2.946 0.326 9.044 0.000 1.206 0.816
I4.1998 (a4) 2.938 0.348 8.447 0.000 1.203 0.928
Covariances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
SDO1996 ~~
SDO1998 0.099 0.023 4.305 0.000 0.553 0.553
.I1.1996 ~~
.I1.1998 0.425 0.075 5.700 0.000 0.425 0.279
.I2.1996 ~~
.I2.1998 0.259 0.072 3.618 0.000 0.259 0.241
.I3.1996 ~~
.I3.1998 0.217 0.076 2.871 0.004 0.217 0.316
.I4.1996 ~~
.I4.1998 -0.104 0.052 -1.991 0.046 -0.104 -0.507
Intercepts:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.I1.1996 2.042 0.056 36.593 0.000 2.042 1.500
.I1.1998 2.034 0.050 40.688 0.000 2.034 1.630
.I2.1996 1.673 0.047 35.833 0.000 1.673 1.475
.I2.1998 1.564 0.042 37.580 0.000 1.564 1.498
.I3.1996 2.742 0.061 45.150 0.000 2.742 1.807
.I3.1998 2.655 0.061 43.803 0.000 2.655 1.797
.I4.1996 2.441 0.055 44.387 0.000 2.441 1.806
.I4.1998 2.273 0.052 43.523 0.000 2.273 1.753
SDO1996 0.000 0.000 0.000
SDO1998 0.000 0.000 0.000
Variances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.I1.1996 1.663 0.127 13.068 0.000 1.663 0.897
.I2.1996 1.170 0.130 9.031 0.000 1.170 0.910
.I3.1996 0.646 0.105 6.166 0.000 0.646 0.280
.I4.1996 0.180 0.079 2.273 0.023 0.180 0.098
.I1.1998 1.390 0.104 13.420 0.000 1.390 0.892
.I2.1998 0.987 0.144 6.837 0.000 0.987 0.906
.I3.1998 0.729 0.100 7.310 0.000 0.729 0.334
.I4.1998 0.234 0.081 2.896 0.004 0.234 0.139
SDO1996 0.191 0.044 4.372 0.000 1.000 1.000
SDO1998 0.168 0.035 4.802 0.000 1.000 1.000
Note that the values in the intercepts
section are now varying.
Again, we can extract latent variable means (however, this has again nothing to do with measurement invariance):
print(ro.r("parameterEstimates(fit_metric)[22:23,]"))
lhs op rhs label est se z pvalue ci.lower ci.upper
22 SDO1996 ~1 0 0 NA NA 0 0
23 SDO1998 ~1 0 0 NA NA 0 0
Let us continue with a configural variance model.
Configural Invariance#
Adapt the code from the metric model to fit a configural model by freeing up the loadings as well (first two syntax lines). Name your model model.configural
.
# Specify the model
ro.r("""model_configural <- '
SDO1996 =~ 1*I1.1996 + I2.1996 + I3.1996 + I4.1996
SDO1998 =~ 1*I1.1998 + I2.1998 + I3.1998 + I4.1998
SDO1996 ~~ SDO1998
I1.1996 ~ 1; I1.1998 ~ 1
I2.1996 ~ 1; I2.1998 ~ 1
I3.1996 ~ 1; I3.1998 ~ 1
I4.1996 ~ 1; I4.1998 ~ 1
I1.1996 ~~ I1.1998
I2.1996 ~~ I2.1998
I3.1996 ~~ I3.1998
I4.1996 ~~ I4.1998
SDO1996 ~ 0*1
SDO1998 ~ 0*1'
""")
# Fit the model
ro.r('fit_configural <- lavaan::cfa(model_configural, data = SDOwave, estimator = "MLR")')
# Print the model output with fit measures and standardized estimates
summary_fit_configural = ro.r('summary(fit_configural, fit.measures=TRUE, standardized=TRUE)')
print(summary_fit_configural)
lavaan 0.6-19 ended normally after 61 iterations
Estimator ML
Optimization method NLMINB
Number of model parameters 29
Number of observations 612
Model Test User Model:
Standard Scaled
Test Statistic 301.687 175.637
Degrees of freedom 15 15
P-value (Chi-square) 0.000 0.000
Scaling correction factor 1.718
Yuan-Bentler correction (Mplus variant)
Model Test Baseline Model:
Test statistic 1993.536 1098.021
Degrees of freedom 28 28
P-value 0.000 0.000
Scaling correction factor 1.816
User Model versus Baseline Model:
Comparative Fit Index (CFI) 0.854 0.850
Tucker-Lewis Index (TLI) 0.728 0.720
Robust Comparative Fit Index (CFI) 0.858
Robust Tucker-Lewis Index (TLI) 0.735
Loglikelihood and Information Criteria:
Loglikelihood user model (H0) -7377.021 -7377.021
Scaling correction factor 1.719
for the MLR correction
Loglikelihood unrestricted model (H1) -7226.178 -7226.178
Scaling correction factor 1.718
for the MLR correction
Akaike (AIC) 14812.043 14812.043
Bayesian (BIC) 14940.128 14940.128
Sample-size adjusted Bayesian (SABIC) 14848.059 14848.059
Root Mean Square Error of Approximation:
RMSEA 0.177 0.132
90 Percent confidence interval - lower 0.160 0.119
90 Percent confidence interval - upper 0.194 0.146
P-value H_0: RMSEA <= 0.050 0.000 0.000
P-value H_0: RMSEA >= 0.080 1.000 1.000
Robust RMSEA 0.173
90 Percent confidence interval - lower 0.151
90 Percent confidence interval - upper 0.197
P-value H_0: Robust RMSEA <= 0.050 0.000
P-value H_0: Robust RMSEA >= 0.080 1.000
Standardized Root Mean Square Residual:
SRMR 0.092 0.092
Parameter Estimates:
Standard errors Sandwich
Information bread Observed
Observed information based on Hessian
Latent Variables:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
SDO1996 =~
I1.1996 1.000 0.493 0.358
I2.1996 0.814 0.088 9.287 0.000 0.401 0.349
I3.1996 2.562 0.314 8.164 0.000 1.264 0.842
I4.1996 2.622 0.339 7.734 0.000 1.293 0.952
SDO1998 =~
I1.1998 1.000 0.363 0.294
I2.1998 0.729 0.177 4.124 0.000 0.265 0.258
I3.1998 3.452 0.561 6.154 0.000 1.254 0.837
I4.1998 3.242 0.568 5.708 0.000 1.178 0.912
Covariances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
SDO1996 ~~
SDO1998 0.099 0.023 4.318 0.000 0.553 0.553
.I1.1996 ~~
.I1.1998 0.423 0.074 5.745 0.000 0.423 0.279
.I2.1996 ~~
.I2.1998 0.257 0.071 3.628 0.000 0.257 0.240
.I3.1996 ~~
.I3.1998 0.210 0.075 2.813 0.005 0.210 0.316
.I4.1996 ~~
.I4.1998 -0.095 0.052 -1.842 0.065 -0.095 -0.436
Intercepts:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.I1.1996 2.042 0.056 36.593 0.000 2.042 1.483
.I1.1998 2.034 0.050 40.688 0.000 2.034 1.649
.I2.1996 1.673 0.047 35.833 0.000 1.673 1.455
.I2.1998 1.564 0.042 37.580 0.000 1.564 1.520
.I3.1996 2.742 0.061 45.150 0.000 2.742 1.827
.I3.1998 2.655 0.061 43.803 0.000 2.655 1.771
.I4.1996 2.441 0.055 44.387 0.000 2.441 1.798
.I4.1998 2.273 0.052 43.523 0.000 2.273 1.761
SDO1996 0.000 0.000 0.000
SDO1998 0.000 0.000 0.000
Variances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.I1.1996 1.652 0.128 12.945 0.000 1.652 0.872
.I2.1996 1.161 0.130 8.930 0.000 1.161 0.878
.I3.1996 0.655 0.097 6.771 0.000 0.655 0.291
.I4.1996 0.171 0.078 2.210 0.027 0.171 0.093
.I1.1998 1.390 0.103 13.489 0.000 1.390 0.913
.I2.1998 0.988 0.144 6.846 0.000 0.988 0.934
.I3.1998 0.675 0.114 5.925 0.000 0.675 0.300
.I4.1998 0.279 0.097 2.866 0.004 0.279 0.167
SDO1996 0.243 0.058 4.205 0.000 1.000 1.000
SDO1998 0.132 0.041 3.257 0.001 1.000 1.000
Model Comparison#
To assess which model provides a better fit (and therefore to assess which level of measurement invariance holds) we can use the anova()
function.
Let us first compare the configural model and the metric model.
print(ro.r("anova(fit_configural, fit_metric)"))
Scaled Chi-Squared Difference Test (method = "satorra.bentler.2001")
lavaan->lavTestLRT():
lavaan NOTE: The "Chisq" column contains standard test statistics, not the
robust test that should be reported per model. A robust difference test is
a function of two standard (not robust) statistics.
Df AIC BIC Chisq Chisq diff Df diff Pr(>Chisq)
fit_configural 15 14812 14940 301.69
fit_metric 18 14815 14930 310.76 7.2119 3 0.06544 .
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
We see that the metric model fits worse than the configural model. This is no suprise as we include more restrictions in the metric model. However, the fit is not significantly worse (p = .0654) and the BIC even favors the metric model. From this we may conclude that metric invariance holds but there is a huge BUT (see below).
Next, lets compare the metric model and the scale model.
print(ro.r("anova(fit_metric, fit_scale)"))
Scaled Chi-Squared Difference Test (method = "satorra.bentler.2001")
lavaan->lavTestLRT():
lavaan NOTE: The "Chisq" column contains standard test statistics, not the
robust test that should be reported per model. A robust difference test is
a function of two standard (not robust) statistics.
Df AIC BIC Chisq Chisq diff Df diff Pr(>Chisq)
fit_metric 18 14815 14930 310.76
fit_scale 21 14814 14916 315.75 4.9207 3 0.1777
From this comparison we can conclude that scale invariance holds as the scale model is not significantly worse than the metric model.
Now to the BUT: You can see in the model output of the configural model that the fit is quite bad (e.g. RMSEA = 0.177 (> .08)). This is not too surprising since research has shown that there are two subdimensions of SDO (Ho et al., 2015): anti-egalitarianism and dominance. According to the theory, the first two items are supposed to load on the dominance dimension (SDO-D), whereas the remaining two items load on anti-egalitarianism (SDO-E).
Therefore, we should be cautious with interpreting the comparisons above and first make sure that we find a good fitting model to our data.