library(pacman); p_load(psych, lavaan, dplyr)
FITM <- c("chisq", "df", "nPar", "cfi", "rmsea", "rmsea.ci.lower", "rmsea.ci.upper", "aic", "bic")

psych::describe(data)

Rationale

Karwowski & Milerski (2021) should be applauded for analyzing and providing excellent data for investigating the effect of schooling on measured intelligence. They exploited a Polish educational reform that made education more intensive for younger students and then showed that it seemed they experienced larger gains than the kids one year older who were not exposed.

They did assess measurement invariance, but they only went up to scalar invariance. Scalar invariance is good enough when you’re trying to see if means are broadly comparable, but without invariant residual variances (i.e., strict factorial invariance), you’re in the dark on whether mean changes can be interpreted in common because the model will tend to conflate broad gains of all sorts that are in the residuals with latent gains, and this is all the more problematic if you use a simple model.

Using Karwowski & Milerski’s parcel-based model for g, I will conduct an MGCFA of their Time 1 and Time 2 data to assess whether there was evidence of baseline and endpoint differences, as well as differential gain for the younger, more intensively-schooled cohort. Differential gains will be assessed by comparing the middle schooler group at T1 to the primary schooler group at T2, since those samples will have similar ages and comparable academic experience, excluding supplementary schooling.

Analysis

Baseline Model

  ########################################################################
########## INITIAL TESTS - CFA FOR EACH TIME POINT SEPARATELY ############
  ########################################################################

IQparcT1 <- 'g =~ parc_anf1 + parc_wnf1 + parc_marf1 + parc_rotf1'
IQparcT2 <- 'g =~ parc_anf2 + parc_wnf2 + parc_marf2 + parc_rotf2'

###########################
###### SIMPLE MODEL #######
###########################

fit1 <- sem(IQparcT1, data=data, std.lv = T, missing = "FIML", meanstructure = TRUE)

fit2 <- sem(IQparcT2, data=data, std.lv = T, missing = "FIML", meanstructure = TRUE)

round(cbind(BASE1 = fitMeasures(fit1, FITM),
            BASE2 = fitMeasures(fit2, FITM), 3))
##                BASE1 BASE2  
## chisq             20    22 3
## df                 2     2 3
## npar              12    12 3
## cfi                1     1 3
## rmsea              0     0 3
## rmsea.ci.lower     0     0 3
## rmsea.ci.upper     0     0 3
## aic            27499 27899 3
## bic            27566 27966 3

Within Time 1 and Time 2 Comparisons

#Their models

#############################################
######## CONFIGURAL INVARIANCE MODEL ########
#############################################

fitconf1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = T, group = "Cohort")

fitconf2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = T, group = "Cohort")

############################################
########## METRIC INVARIANCE MODEL #########
############################################

fitmetr1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"))

fitmetr2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"))

#########################################
####### SCALAR INVARIANCE MODEL #########
#########################################

fitsclr1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts"))

fitsclr2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts"))

#My models

#########################################
####### STRICT INVARIANCE MODEL #########
#########################################

fitstrct1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"))

fitstrct2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"))

#########################################
####### LATENT VARIANCE INVARIANCE MODEL #########
#########################################

fitlatv1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"))

fitlatv2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"))

#########################################
####### LATENT MEAN INVARIANCE MODEL #########
#########################################

fitlatm1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances", "means"))

fitlatm2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances", "means"))
round(cbind(CONF1 = fitMeasures(fitconf1, FITM),
            METR1 = fitMeasures(fitmetr1, FITM),
            SCLR1 = fitMeasures(fitsclr1, FITM),
            STRI1 = fitMeasures(fitstrct1, FITM),
            LATV1 = fitMeasures(fitlatv1, FITM),
            LATM1 = fitMeasures(fitlatm1, FITM)),3)
##                    CONF1     METR1     SCLR1     STRI1     LATV1     LATM1
## chisq             20.471    27.987    32.357    46.516    52.854    59.940
## df                 4.000     7.000    10.000    14.000    15.000    16.000
## npar              24.000    21.000    18.000    14.000    13.000    12.000
## cfi                0.992     0.990     0.989     0.984     0.982     0.979
## rmsea              0.065     0.055     0.048     0.049     0.051     0.053
## rmsea.ci.lower     0.039     0.035     0.030     0.034     0.036     0.039
## rmsea.ci.upper     0.094     0.077     0.067     0.065     0.066     0.068
## aic            27483.647 27485.163 27483.533 27489.691 27494.030 27499.115
## bic            27617.596 27602.369 27583.995 27567.828 27566.586 27566.090
round(cbind(CONF2 = fitMeasures(fitconf2, FITM),
            METR2 = fitMeasures(fitmetr2, FITM),
            SCLR2 = fitMeasures(fitsclr2, FITM),
            STRI2 = fitMeasures(fitstrct2, FITM),
            LATV2 = fitMeasures(fitlatv2, FITM),
            LATM2 = fitMeasures(fitlatm2, FITM)),3)
##                    CONF2     METR2     SCLR2     STRI2     LATV2     LATM2
## chisq             22.585    25.971    33.995    48.085    49.253    49.276
## df                 4.000     7.000    10.000    14.000    15.000    16.000
## npar              24.000    21.000    18.000    14.000    13.000    12.000
## cfi                0.994     0.994     0.992     0.989     0.989     0.989
## rmsea              0.069     0.053     0.049     0.050     0.048     0.046
## rmsea.ci.lower     0.043     0.032     0.032     0.035     0.034     0.032
## rmsea.ci.upper     0.098     0.075     0.068     0.066     0.064     0.061
## aic            27896.042 27893.429 27895.453 27901.543 27900.710 27898.734
## bic            28029.991 28010.634 27995.914 27979.680 27973.266 27965.708
"Time 1"
## [1] "Time 1"
1-pchisq(fitMeasures(fitmetr1, "chisq")-fitMeasures(fitconf1, "chisq"), 3)  #Metric
## chisq 
## 0.057
1-pchisq(fitMeasures(fitsclr1, "chisq")-fitMeasures(fitmetr1, "chisq"), 3)  #Scalar
## chisq 
## 0.224
1-pchisq(fitMeasures(fitstrct1, "chisq")-fitMeasures(fitsclr1, "chisq"), 4) #Strict
## chisq 
## 0.007
1-pchisq(fitMeasures(fitlatv1, "chisq")-fitMeasures(fitstrct1, "chisq"), 1) #Latent Variance
## chisq 
## 0.012
1-pchisq(fitMeasures(fitlatm1, "chisq")-fitMeasures(fitlatv1, "chisq"), 1)  #Latent Mean
## chisq 
## 0.008
"Time 2"
## [1] "Time 2"
1-pchisq(fitMeasures(fitmetr2, "chisq")-fitMeasures(fitconf2, "chisq"), 3)  #Metric
## chisq 
## 0.336
1-pchisq(fitMeasures(fitsclr2, "chisq")-fitMeasures(fitmetr2, "chisq"), 3)  #Scalar
## chisq 
## 0.046
1-pchisq(fitMeasures(fitstrct2, "chisq")-fitMeasures(fitsclr2, "chisq"), 4) #Strict
## chisq 
## 0.007
1-pchisq(fitMeasures(fitlatv2, "chisq")-fitMeasures(fitstrct2, "chisq"), 1) #Latent Variance
## chisq 
##  0.28
1-pchisq(fitMeasures(fitlatm2, "chisq")-fitMeasures(fitlatv2, "chisq"), 1)  #Latent Mean
## chisq 
## 0.878
#My models

#########################################
####### STRICT INVARIANCE MODEL #########
#########################################

pfitstrct1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"),
               group.partial = c("parc_rotf1 ~~ parc_rotf1"))

fitlatv1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"),
               group.partial = c("parc_rotf1 ~~ parc_rotf1"))

fitlatm1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "means"),
               group.partial = c("parc_rotf1 ~~ parc_rotf1"))

round(cbind(CONF1  = fitMeasures(fitconf1, FITM),
            METR1  = fitMeasures(fitmetr1, FITM),
            SCLR1  = fitMeasures(fitsclr1, FITM),
            STRI1  = fitMeasures(fitstrct1, FITM),
            PSTRI1 = fitMeasures(pfitstrct1, FITM),
            LATV1  = fitMeasures(fitlatv1, FITM), #Drop Restraint
            LATM1  = fitMeasures(fitlatm1, FITM)), 3) #Drop Restraint
##                    CONF1     METR1     SCLR1     STRI1    PSTRI1     LATV1
## chisq             20.471    27.987    32.357    46.516    37.730    43.845
## df                 4.000     7.000    10.000    14.000    13.000    14.000
## npar              24.000    21.000    18.000    14.000    15.000    14.000
## cfi                0.992     0.990     0.989     0.984     0.988     0.986
## rmsea              0.065     0.055     0.048     0.049     0.044     0.047
## rmsea.ci.lower     0.039     0.035     0.030     0.034     0.028     0.031
## rmsea.ci.upper     0.094     0.077     0.067     0.065     0.061     0.063
## aic            27483.647 27485.163 27483.533 27489.691 27482.905 27487.021
## bic            27617.596 27602.369 27583.995 27567.828 27566.624 27565.158
##                    LATM1
## chisq             44.819
## df                14.000
## npar              14.000
## cfi                0.985
## rmsea              0.047
## rmsea.ci.lower     0.032
## rmsea.ci.upper     0.063
## aic            27487.994
## bic            27566.131
1-pchisq(fitMeasures(pfitstrct1, "chisq") - fitMeasures(fitsclr1, "chisq"), 3)   #Partial Strict
## chisq 
## 0.146
1-pchisq(fitMeasures(fitlatv1, "chisq")   - fitMeasures(pfitstrct1, "chisq"), 1) #Latent Variances
## chisq 
## 0.013
1-pchisq(fitMeasures(fitlatm1, "chisq")   - fitMeasures(pfitstrct1, "chisq"), 1) #Means
## chisq 
## 0.008
summary(pfitstrct1, fit = T, stand = T)
## lavaan 0.6.14 ended normally after 31 iterations
## 
##   Estimator                                         ML
##   Optimization method                           NLMINB
##   Number of model parameters                        25
##   Number of equality constraints                    10
## 
##   Number of observations per group:                   
##     1,00                                           985
##     2,00                                           976
##   Number of missing patterns per group:               
##     1,00                                             1
##     2,00                                             1
## 
## Model Test User Model:
##                                                       
##   Test statistic                                37.730
##   Degrees of freedom                                13
##   P-value (Chi-square)                           0.000
##   Test statistic for each group:
##     1,00                                        19.238
##     2,00                                        18.492
## 
## Model Test Baseline Model:
## 
##   Test statistic                              2075.599
##   Degrees of freedom                                12
##   P-value                                        0.000
## 
## User Model versus Baseline Model:
## 
##   Comparative Fit Index (CFI)                    0.988
##   Tucker-Lewis Index (TLI)                       0.989
##                                                       
##   Robust Comparative Fit Index (CFI)             0.988
##   Robust Tucker-Lewis Index (TLI)                0.989
## 
## Loglikelihood and Information Criteria:
## 
##   Loglikelihood user model (H0)             -13726.453
##   Loglikelihood unrestricted model (H1)             NA
##                                                       
##   Akaike (AIC)                               27482.905
##   Bayesian (BIC)                             27566.624
##   Sample-size adjusted Bayesian (SABIC)      27518.968
## 
## Root Mean Square Error of Approximation:
## 
##   RMSEA                                          0.044
##   90 Percent confidence interval - lower         0.028
##   90 Percent confidence interval - upper         0.061
##   P-value H_0: RMSEA <= 0.050                    0.700
##   P-value H_0: RMSEA >= 0.080                    0.000
##                                                       
##   Robust RMSEA                                   0.044
##   90 Percent confidence interval - lower         0.028
##   90 Percent confidence interval - upper         0.061
##   P-value H_0: Robust RMSEA <= 0.050             0.700
##   P-value H_0: Robust RMSEA >= 0.080             0.000
## 
## Standardized Root Mean Square Residual:
## 
##   SRMR                                           0.029
## 
## Parameter Estimates:
## 
##   Standard errors                             Standard
##   Information                                 Observed
##   Observed information based on                Hessian
## 
## 
## Group 1 [1,00]:
## 
## Latent Variables:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##   g =~                                                                  
##     prc_nf1           1.000                               0.781    0.692
##     prc_wn1 (.p2.)    2.111    0.077   27.436    0.000    1.648    0.787
##     prc_mr1 (.p3.)    1.956    0.076   25.648    0.000    1.527    0.695
##     prc_rt1 (.p4.)    0.626    0.035   17.782    0.000    0.489    0.457
## 
## Intercepts:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##    .prc_nf1 (.10.)    3.157    0.032   97.766    0.000    3.157    2.797
##    .prc_wn1 (.11.)    4.470    0.063   70.860    0.000    4.470    2.135
##    .prc_mr1 (.12.)    4.153    0.063   65.919    0.000    4.153    1.891
##    .prc_rt1 (.13.)    2.592    0.028   92.832    0.000    2.592    2.422
##     g                 0.000                               0.000    0.000
## 
## Variances:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##    .prc_nf1 (.p5.)    0.665    0.029   22.849    0.000    0.665    0.522
##    .prc_wn1 (.p6.)    1.671    0.100   16.778    0.000    1.671    0.381
##    .prc_mr1 (.p7.)    2.490    0.110   22.663    0.000    2.490    0.516
##    .prc_rt1           0.906    0.044   20.548    0.000    0.906    0.791
##     g                 0.610    0.045   13.409    0.000    1.000    1.000
## 
## 
## Group 2 [2,00]:
## 
## Latent Variables:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##   g =~                                                                  
##     prc_nf1           1.000                               0.861    0.726
##     prc_wn1 (.p2.)    2.111    0.077   27.436    0.000    1.818    0.815
##     prc_mr1 (.p3.)    1.956    0.076   25.648    0.000    1.685    0.730
##     prc_rt1 (.p4.)    0.626    0.035   17.782    0.000    0.539    0.456
## 
## Intercepts:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##    .prc_nf1 (.10.)    3.157    0.032   97.766    0.000    3.157    2.662
##    .prc_wn1 (.11.)    4.470    0.063   70.860    0.000    4.470    2.004
##    .prc_mr1 (.12.)    4.153    0.063   65.919    0.000    4.153    1.799
##    .prc_rt1 (.13.)    2.592    0.028   92.832    0.000    2.592    2.192
##     g                 0.110    0.042    2.659    0.008    0.128    0.128
## 
## Variances:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##    .prc_nf1 (.p5.)    0.665    0.029   22.849    0.000    0.665    0.472
##    .prc_wn1 (.p6.)    1.671    0.100   16.778    0.000    1.671    0.336
##    .prc_mr1 (.p7.)    2.490    0.110   22.663    0.000    2.490    0.467
##    .prc_rt1           1.107    0.053   20.960    0.000    1.107    0.792
##     g                 0.742    0.054   13.641    0.000    1.000    1.000
pfitstrct2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"),
               group.partial = c("parc_rotf2 ~~ parc_rotf2", "parc_wnf2 ~~ parc_wnf2"))

fitlatv2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"),
               group.partial = c("parc_rotf2 ~~ parc_rotf2", "parc_wnf2 ~~ parc_wnf2"))

fitlatm2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances", "means"),
               group.partial = c("parc_rotf2 ~~ parc_rotf2", "parc_wnf2 ~~ parc_wnf2"))

round(cbind(CONF2  = fitMeasures(fitconf2, FITM),
            METR2  = fitMeasures(fitmetr2, FITM),
            SCLR2  = fitMeasures(fitsclr2, FITM),
            STRI2  = fitMeasures(fitstrct2, FITM),
            PSTRI2 = fitMeasures(pfitstrct2, FITM),
            LATV2  = fitMeasures(fitlatv2, FITM), 
            LATM2  = fitMeasures(fitlatm2, FITM)),3)
##                    CONF2     METR2     SCLR2     STRI2    PSTRI2     LATV2
## chisq             22.585    25.971    33.995    48.085    38.633    39.482
## df                 4.000     7.000    10.000    14.000    12.000    13.000
## npar              24.000    21.000    18.000    14.000    16.000    15.000
## cfi                0.994     0.994     0.992     0.989     0.991     0.991
## rmsea              0.069     0.053     0.049     0.050     0.048     0.046
## rmsea.ci.lower     0.043     0.032     0.032     0.035     0.031     0.030
## rmsea.ci.upper     0.098     0.075     0.068     0.066     0.065     0.062
## aic            27896.042 27893.429 27895.453 27901.543 27896.091 27894.939
## bic            28029.991 28010.634 27995.914 27979.680 27985.390 27978.657
##                    LATM2
## chisq             39.506
## df                14.000
## npar              14.000
## cfi                0.992
## rmsea              0.043
## rmsea.ci.lower     0.028
## rmsea.ci.upper     0.059
## aic            27892.963
## bic            27971.100
1-pchisq(fitMeasures(pfitstrct2, "chisq") - fitMeasures(fitsclr2, "chisq"), 2) #Partial Strict
## chisq 
## 0.098
1-pchisq(fitMeasures(fitlatv2, "chisq")   - fitMeasures(pfitstrct2, "chisq"), 1) #Latent Variances
## chisq 
## 0.357
1-pchisq(fitMeasures(fitlatm2, "chisq")   - fitMeasures(fitlatv2, "chisq"), 1)  #Means
## chisq 
## 0.877

Simple Development

df <- data[data$Cohort %in% c("1,00", "2,00"), ]
df$parc_anf <- ifelse(df$Cohort == "1,00", df$parc_anf2, df$parc_anf1)
df$parc_wnf <- ifelse(df$Cohort == "1,00", df$parc_wnf2, df$parc_wnf1)
df$parc_marf <- ifelse(df$Cohort == "1,00", df$parc_marf2, df$parc_marf1)
df$parc_rotf <- ifelse(df$Cohort == "1,00", df$parc_rotf2, df$parc_rotf1)
df <- df[, c("Cohort", "parc_anf", "parc_wnf", "parc_marf", "parc_rotf")]

IQparc <- 'g =~ parc_anf + parc_wnf + parc_marf + parc_rotf'

#############################################
######## CONFIGURAL INVARIANCE MODEL ########
#############################################

fitconf <- cfa(IQparc, data=df, missing = "FIML", meanstructure = TRUE, std.lv = T, group = "Cohort")

############################################
########## METRIC INVARIANCE MODEL #########
############################################

fitmetr <- cfa(IQparc, data=df, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"))

pfitmetr <- cfa(IQparc, data=df, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"),
               group.partial = c("g =~ parc_rotf"))

#########################################
####### SCALAR INVARIANCE MODEL #########
#########################################

fitsclr <- cfa(IQparc, data=df, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts"),
               group.partial = c("g =~ parc_rotf"))

#########################################
####### STRICT INVARIANCE MODEL #########
#########################################

fitstrct <- cfa(IQparc, data=df, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"),
               group.partial = c("g =~ parc_rotf"))

#########################################
####### LATENT VARIANCE INVARIANCE MODEL #########
#########################################

fitlatv <- cfa(IQparc, data=df, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"),
               group.partial = c("g =~ parc_rotf"))

#########################################
####### LATENT MEAN INVARIANCE MODEL #########
#########################################

fitlatm <- cfa(IQparc, data=df, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "means"),
               group.partial = c("g =~ parc_rotf"))

round(cbind(CONF  = fitMeasures(fitconf, FITM),
            METR  = fitMeasures(fitmetr, FITM),
            PMETR = fitMeasures(pfitmetr, FITM),
            SCLR  = fitMeasures(fitsclr, FITM),
            STRI  = fitMeasures(fitstrct, FITM),
            LATV  = fitMeasures(fitlatv, FITM), #Drop constraint
            LATM  = fitMeasures(fitlatm, FITM)), 3)
##                     CONF      METR     PMETR      SCLR      STRI      LATV
## chisq             21.428    29.944    22.401    24.828    30.109    36.915
## df                 4.000     7.000     6.000     9.000    13.000    14.000
## npar              24.000    21.000    22.000    19.000    15.000    14.000
## cfi                0.993     0.991     0.994     0.994     0.993     0.991
## rmsea              0.067     0.058     0.053     0.042     0.037     0.041
## rmsea.ci.lower     0.041     0.037     0.031     0.023     0.019     0.025
## rmsea.ci.upper     0.096     0.080     0.077     0.063     0.054     0.057
## aic            27814.662 27817.178 27811.635 27808.062 27805.343 27810.149
## bic            27948.611 27934.383 27934.422 27914.105 27889.061 27888.286
##                     LATM
## chisq             31.403
## df                14.000
## npar              14.000
## cfi                0.993
## rmsea              0.036
## rmsea.ci.lower     0.019
## rmsea.ci.upper     0.052
## aic            27804.637
## bic            27882.774
1-pchisq(fitMeasures(fitmetr, "chisq")  - fitMeasures(fitconf, "chisq"), 3)  #Metric
## chisq 
## 0.036
1-pchisq(fitMeasures(pfitmetr, "chisq") - fitMeasures(fitconf, "chisq"), 2) #Metric
## chisq 
## 0.615
1-pchisq(fitMeasures(fitsclr, "chisq")  - fitMeasures(pfitmetr, "chisq"), 3) #Scalar
## chisq 
## 0.489
1-pchisq(fitMeasures(fitstrct, "chisq") - fitMeasures(fitsclr, "chisq"), 4) #Strict
## chisq 
##  0.26
1-pchisq(fitMeasures(fitlatv, "chisq")  - fitMeasures(fitstrct, "chisq"), 1) #Latent Variance
## chisq 
## 0.009
1-pchisq(fitMeasures(fitlatm, "chisq")  - fitMeasures(fitstrct, "chisq"), 1) #Latent Mean
## chisq 
## 0.255

Discussion

Oddly, mental rotation was influenced by different variables or had different reliability at both timepoints. I do not know why this was. At time two, there were different influences on reasoning too. I do not know why this happened, but partially invariant models had to be fit. There was meager evidence for a difference in intercepts at T2 (p = 0.046), and consistent with that, this had no effect on results, so I did not pursue a partially invariant scalar/strong model for this page. At T1, the latent variance of g differed, and again, I do not know why, but unfortunately, that needed to be released. At T2, the latent variances were equal.

The lack of longitudinal invariance for the exposed primary school cohort suggested that there was likely some effect, but it was expressed through changing the interpretation of indicator variables. Despite this, the levels of the items were still consistently interpreted.

The most surprising result came from the explicit test of whether the latent mean of g differed at T1 or T2. The answers differed: at T1, Group 2 was more intelligent, but at T2, they were equal. Because the differences dissolved, whatever impact intensive schooling might have had - and Karwowski & Milerski maintained it was “not systematic and quite unstable” - may have been general.

But the final analysis assessed whether this was really just the rate of developmental change being greater at younger ages by comparing Primary T2 and Middle T1. The problem with this is the retesting effect, but the assessment’s retest bonus should have at least faded a bit, if not totally, over the school year. With this said, the result was clear: changes in g as a result of more intensive education were not supported The primary school children’s mean for g was not distinguishable from the preexisting differences observed in the cohort that was not exposed to the more intensive primary schooling regime.

There was negative evidence that more intensive education increased the rate of development of g. As noted in the postscript below, verbal results were inconclusive because of poor model fit. Nonverbal results were consistent with the results for g, as the primary schooler T2 mean did not differ from the middle schooler T1 mean (p = 0.171).

References

Karwowski, M., & Milerski, B. (2021). Intensive schooling and cognitive ability: A case of Polish educational reform. Personality and Individual Differences, 183, 111121. https://doi.org/10.1016/j.paid.2021.111121

Data: https://osf.io/hm3df/

Postscript: Nonverbal and Verbal

Note that because these are items, the estimator is wrong. However, DWLS did not seem to want to converge and WLSMV couldn’t handle the missingness well. Nonverbal first, since that was what they listed first, and what seemed to show catch-up gains in their study (see Fig. 2.).

  ########################################################################
########## INITIAL TESTS - CFA FOR EACH TIME POINT SEPARATELY ############
  ########################################################################

IQparcT1 <- 'g =~ mat1ok_f1 + mat2ok_f1 + mat3ok_f1 + mat4ok_f1 + mat5ok_f1 + mat6ok_f1 + 
      mat8ok_f1 + mat9ok_f1 + mat10ok_f1 + mat11ok_f1'
IQparcT2 <- 'g =~ mat1ok_f2 + mat2ok_f2 + mat3ok_f2 + mat4ok_f2 + mat5ok_f2 + mat6ok_f2 + 
      mat8ok_f2 + mat9ok_f2 + mat10ok_f2 + mat11ok_f2'

###########################
###### SIMPLE MODEL #######
###########################

fit1 <- sem(IQparcT1, data=data, std.lv = T, missing = "FIML", meanstructure = TRUE)

fit2 <- sem(IQparcT2, data=data, std.lv = T, missing = "FIML", meanstructure = TRUE)

round(cbind(BASE1 = fitMeasures(fit1, FITM),
            BASE2 = fitMeasures(fit2, FITM), 3))
##                BASE1 BASE2  
## chisq            122   115 3
## df                35    35 3
## npar              30    30 3
## cfi                1     1 3
## rmsea              0     0 3
## rmsea.ci.lower     0     0 3
## rmsea.ci.upper     0     0 3
## aic            19068 18789 3
## bic            19232 18952 3
#Their models

#############################################
######## CONFIGURAL INVARIANCE MODEL ########
#############################################

fitconf1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = T, group = "Cohort")

fitconf2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = T, group = "Cohort")

############################################
########## METRIC INVARIANCE MODEL #########
############################################

fitmetr1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"),
               group.partial = c("g =~ mat4ok_f1"))

fitmetr2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"))

#########################################
####### SCALAR INVARIANCE MODEL #########
#########################################

fitsclr1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts"),
               group.partial = c("g =~ mat4ok_f1"))

fitsclr2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts"))

#My models

#########################################
####### STRICT INVARIANCE MODEL #########
#########################################

fitstrct1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"),
               group.partial = c("g =~ mat4ok_f1"))

fitstrct2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"))

#########################################
####### LATENT VARIANCE INVARIANCE MODEL #########
#########################################

fitlatv1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"),
               group.partial = c("g =~ mat4ok_f1"))

fitlatv2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"))

#########################################
####### LATENT MEAN INVARIANCE MODEL #########
#########################################

fitlatm1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "means"),
               group.partial = c("g =~ mat4ok_f1"))

fitlatm2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances", "means"))

round(cbind(CONF1 = fitMeasures(fitconf1, FITM),
            METR1 = fitMeasures(fitmetr1, FITM),
            SCLR1 = fitMeasures(fitsclr1, FITM),
            STRI1 = fitMeasures(fitstrct1, FITM),
            LATV1 = fitMeasures(fitlatv1, FITM), #Drop constraint
            LATM1 = fitMeasures(fitlatm1, FITM)),3)
##                    CONF1     METR1     SCLR1     STRI1     LATV1     LATM1
## chisq            165.004   176.453   183.180   190.882   193.339   206.423
## df                70.000    78.000    87.000    97.000    98.000    98.000
## npar              60.000    52.000    43.000    33.000    32.000    32.000
## cfi                0.915     0.912     0.914     0.916     0.915     0.904
## rmsea              0.039     0.038     0.035     0.033     0.033     0.035
## rmsea.ci.lower     0.032     0.030     0.028     0.026     0.026     0.029
## rmsea.ci.upper     0.047     0.045     0.043     0.040     0.040     0.042
## aic            19073.523 19068.973 19057.700 19045.402 19045.859 19058.943
## bic            19401.805 19353.484 19292.968 19225.957 19220.942 19234.026
round(cbind(CONF2 = fitMeasures(fitconf2, FITM),
            METR2 = fitMeasures(fitmetr2, FITM),
            SCLR2 = fitMeasures(fitsclr2, FITM),
            STRI2 = fitMeasures(fitstrct2, FITM),
            LATV2 = fitMeasures(fitlatv2, FITM),
            LATM2 = fitMeasures(fitlatm2, FITM)),3)
##                    CONF2     METR2     SCLR2     STRI2     LATV2     LATM2
## chisq            172.186   176.635   186.943   193.128   194.316   194.316
## df                70.000    79.000    88.000    98.000    99.000   100.000
## npar              60.000    51.000    42.000    32.000    31.000    30.000
## cfi                0.930     0.933     0.932     0.935     0.934     0.935
## rmsea              0.042     0.038     0.037     0.034     0.034     0.034
## rmsea.ci.lower     0.034     0.031     0.029     0.027     0.027     0.026
## rmsea.ci.upper     0.050     0.046     0.044     0.041     0.041     0.041
## aic            18827.027 18813.476 18805.785 18791.969 18791.157 18789.158
## bic            19152.334 19089.987 19033.499 18965.466 18959.232 18951.811
"Time 1"
## [1] "Time 1"
1-pchisq(fitMeasures(fitmetr1, "chisq")-fitMeasures(fitconf1, "chisq"), 8)  #Metric
## chisq 
## 0.178
1-pchisq(fitMeasures(fitsclr1, "chisq")-fitMeasures(fitmetr1, "chisq"), 9)  #Scalar
## chisq 
## 0.666
1-pchisq(fitMeasures(fitstrct1, "chisq")-fitMeasures(fitsclr1, "chisq"), 9) #Strict
## chisq 
## 0.564
1-pchisq(fitMeasures(fitlatv1, "chisq")-fitMeasures(fitstrct1, "chisq"), 1) #Latent Variance
## chisq 
## 0.117
1-pchisq(fitMeasures(fitlatm1, "chisq")-fitMeasures(fitstrct1, "chisq"), 1)  #Latent Mean
## chisq 
##     0
"Time 2"
## [1] "Time 2"
1-pchisq(fitMeasures(fitmetr2, "chisq")-fitMeasures(fitconf2, "chisq"), 9)  #Metric
## chisq 
## 0.879
1-pchisq(fitMeasures(fitsclr2, "chisq")-fitMeasures(fitmetr2, "chisq"), 9)  #Scalar
## chisq 
## 0.326
1-pchisq(fitMeasures(fitstrct2, "chisq")-fitMeasures(fitsclr2, "chisq"), 9) #Strict
## chisq 
## 0.721
1-pchisq(fitMeasures(fitlatv2, "chisq")-fitMeasures(fitstrct2, "chisq"), 1) #Latent Variance
## chisq 
## 0.276
1-pchisq(fitMeasures(fitlatm2, "chisq")-fitMeasures(fitlatv2, "chisq"), 1)  #Latent Mean
## chisq 
## 0.981

For the nonverbal results, the difference observed at T1 was real. The difference in the interpretation of the fourth matrices item was substantial, with Group 1 having a loading of 0.584 versus 0.892 for Group 2. This was not enough to explain the observed performance gaps and the latent mean difference ended up being 0.221 d in the strict model. The latent variance model could not be used, since the latent variances were unequal.

The T2 results were fully invariant, so there were real catch-up gains in nonverbal ability. I do not know why, because unfortunately, the way this was measured, “nonverbal intelligence” is full of variance from g. A more complex model is required, but there are not enough indicators in the dataset to do that. A more important issue also affected the verbal model, so I will discuss that later.

Before moving on, I should note that the authors mentioned in their code that all items modeled fit better if treated as categorical, and I think they should be scolded for this. The reason is because they changed the estimator and the interpretation of the items by using the wrong estimator. Do not treat variables that are not actually categorical as if they are categorical. You will produce a misfitted model because you used an inappropriate estimator. This is a much worse mistake than, say, using ML instead of DWLS or WLSMV when there are convergence issues.

On to verbal:

  ########################################################################
########## INITIAL TESTS - CFA FOR EACH TIME POINT SEPARATELY ############
  ########################################################################

IQparcT1 <- 'g =~ an1ok_f1 + an2ok_f1 + an4ok_f1 + wn4ok_f1 + wn8ok_f1 +
              wn10ok_f1 + wn13ok_f1 + wn18ok_f1 + wn20ok_f1 + wn23ok_f1'
IQparcT2 <- 'g =~ an1ok_f2 + an2ok_f2 + an4ok_f2 + wn4ok_f2 + wn8ok_f2 +
              wn10ok_f2 + wn13ok_f2 + wn18ok_f2 + wn20ok_f2 + wn23ok_f2'

###########################
###### SIMPLE MODEL #######
###########################

fit1 <- sem(IQparcT1, data=data, std.lv = T, missing = "FIML", meanstructure = TRUE)

fit2 <- sem(IQparcT2, data=data, std.lv = T, missing = "FIML", meanstructure = TRUE)

round(cbind(BASE1 = fitMeasures(fit1, FITM),
            BASE2 = fitMeasures(fit2, FITM), 3))
##                BASE1 BASE2  
## chisq            175   170 3
## df                35    35 3
## npar              30    30 3
## cfi                1     1 3
## rmsea              0     0 3
## rmsea.ci.lower     0     0 3
## rmsea.ci.upper     0     0 3
## aic            22603 21304 3
## bic            22768 21467 3
#Their models

#############################################
######## CONFIGURAL INVARIANCE MODEL ########
#############################################

fitconf1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = T, group = "Cohort")

fitconf2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = T, group = "Cohort")

############################################
########## METRIC INVARIANCE MODEL #########
############################################

fitmetr1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"))

fitmetr2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings"),
               group.partial = c("g =~ wn23ok_f2"))

#########################################
####### SCALAR INVARIANCE MODEL #########
#########################################

fitsclr1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts"))

fitsclr2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts"),
               group.partial = c("g =~ wn23ok_f2"))

#My models

#########################################
####### STRICT INVARIANCE MODEL #########
#########################################

fitstrct1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"))

fitstrct2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals"),
               group.partial = c("g =~ wn23ok_f2"))

#########################################
####### LATENT VARIANCE INVARIANCE MODEL #########
#########################################

fitlatv1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"))

fitlatv2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances"),
               group.partial = c("g =~ wn23ok_f2"))

#########################################
####### LATENT MEAN INVARIANCE MODEL #########
#########################################

fitlatm1 <- cfa(IQparcT1, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances", "means"))

fitlatm2 <- cfa(IQparcT2, data=data, missing = "FIML", meanstructure = TRUE, std.lv = F, group = "Cohort", 
               group.equal = c("loadings", "intercepts", "residuals", "lv.variances", "means"),
               group.partial = c("g =~ wn23ok_f2"))

round(cbind(CONF1 = fitMeasures(fitconf1, FITM),
            METR1 = fitMeasures(fitmetr1, FITM),
            SCLR1 = fitMeasures(fitsclr1, FITM),
            STRI1 = fitMeasures(fitstrct1, FITM),
            LATV1 = fitMeasures(fitlatv1, FITM), 
            LATM1 = fitMeasures(fitlatm1, FITM)),3)
##                    CONF1     METR1     SCLR1     STRI1     LATV1     LATM1
## chisq            243.292   269.419   293.461   299.487   303.784   306.623
## df                70.000    79.000    88.000    98.000    99.000   100.000
## npar              60.000    51.000    42.000    32.000    31.000    30.000
## cfi                0.892     0.882     0.872     0.875     0.873     0.872
## rmsea              0.053     0.052     0.052     0.048     0.049     0.048
## rmsea.ci.lower     0.046     0.046     0.045     0.042     0.042     0.042
## rmsea.ci.upper     0.060     0.059     0.058     0.055     0.055     0.055
## aic            22600.088 22608.216 22614.258 22600.284 22602.581 22603.420
## bic            22928.370 22887.255 22844.055 22775.367 22772.193 22767.561
round(cbind(CONF2 = fitMeasures(fitconf2, FITM),
            METR2 = fitMeasures(fitmetr2, FITM),
            SCLR2 = fitMeasures(fitsclr2, FITM),
            STRI2 = fitMeasures(fitstrct2, FITM),
            LATV2 = fitMeasures(fitlatv2, FITM),
            LATM2 = fitMeasures(fitlatm2, FITM)),3)
##                    CONF2     METR2     SCLR2     STRI2     LATV2     LATM2
## chisq            218.410   226.658   244.478   255.113   255.267   257.847
## df                70.000    78.000    87.000    97.000    98.000    99.000
## npar              60.000    52.000    43.000    33.000    32.000    31.000
## cfi                0.939     0.938     0.935     0.935     0.935     0.934
## rmsea              0.050     0.048     0.047     0.044     0.044     0.044
## rmsea.ci.lower     0.043     0.041     0.040     0.038     0.037     0.037
## rmsea.ci.upper     0.058     0.055     0.053     0.051     0.050     0.050
## aic            21310.845 21303.092 21302.912 21293.548 21291.702 21292.282
## bic            21636.151 21585.025 21536.049 21472.466 21465.199 21460.357
"Time 1"
## [1] "Time 1"
1-pchisq(fitMeasures(fitmetr1, "chisq")-fitMeasures(fitconf1, "chisq"), 9)  #Metric
## chisq 
## 0.002
1-pchisq(fitMeasures(fitsclr1, "chisq")-fitMeasures(fitmetr1, "chisq"), 9)  #Scalar
## chisq 
## 0.004
1-pchisq(fitMeasures(fitstrct1, "chisq")-fitMeasures(fitsclr1, "chisq"), 9) #Strict
## chisq 
## 0.737
1-pchisq(fitMeasures(fitlatv1, "chisq")-fitMeasures(fitstrct1, "chisq"), 1) #Latent Variance
## chisq 
## 0.038
1-pchisq(fitMeasures(fitlatm1, "chisq")-fitMeasures(fitlatv1, "chisq"), 1)  #Latent Mean
## chisq 
## 0.092
"Time 2"
## [1] "Time 2"
1-pchisq(fitMeasures(fitmetr2, "chisq")-fitMeasures(fitconf2, "chisq"), 8)  #Metric
## chisq 
##  0.41
1-pchisq(fitMeasures(fitsclr2, "chisq")-fitMeasures(fitmetr2, "chisq"), 9)  #Scalar
## chisq 
## 0.037
1-pchisq(fitMeasures(fitstrct2, "chisq")-fitMeasures(fitsclr2, "chisq"), 9) #Strict
## chisq 
## 0.302
1-pchisq(fitMeasures(fitlatv2, "chisq")-fitMeasures(fitstrct2, "chisq"), 1) #Latent Variance
## chisq 
## 0.694
1-pchisq(fitMeasures(fitlatm2, "chisq")-fitMeasures(fitlatv2, "chisq"), 1)  #Latent Mean
## chisq 
## 0.108

The T1 verbal results could not be analyzed credibly because the initial model fit was very poor and too many loadings had to be released to fit a working metric model. The similarity in means at T1 was uninterpretable. The T2 verbal results could be analyzed credibly because the model fit well enough and only a single loading had to be released to make all subsequent levels of measurement invariance fit just fine. Everything just worked.

The net result of these two findings is that the effect of intensive schooling on group differences in verbal intelligence - which, like nonverbal intelligence, is polluted by g variance - is unclear.

sessionInfo()
## R version 4.2.2 (2022-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 19045)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] dplyr_1.1.0   lavaan_0.6-14 psych_2.2.9   pacman_0.5.1 
## 
## loaded via a namespace (and not attached):
##  [1] bslib_0.4.2      compiler_4.2.2   pillar_1.8.1     jquerylib_0.1.4 
##  [5] tools_4.2.2      digest_0.6.31    tibble_3.1.8     jsonlite_1.8.4  
##  [9] evaluate_0.20    lifecycle_1.0.3  nlme_3.1-160     lattice_0.20-45 
## [13] pkgconfig_2.0.3  rlang_1.0.6      cli_3.6.0        rstudioapi_0.14 
## [17] yaml_2.3.7       parallel_4.2.2   pbivnorm_0.6.0   xfun_0.37       
## [21] fastmap_1.1.0    knitr_1.42       generics_0.1.3   sass_0.4.5      
## [25] vctrs_0.5.2      tidyselect_1.2.0 stats4_4.2.2     grid_4.2.2      
## [29] glue_1.6.2       R6_2.5.1         fansi_1.0.4      rmarkdown_2.20  
## [33] magrittr_2.0.3   MASS_7.3-58.1    htmltools_0.5.4  mnormt_2.1.1    
## [37] quadprog_1.5-8   utf8_1.2.3       cachem_1.0.6