Skip to contents

This chapter is under construction.

library(alda)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(tidyr)
library(lavaan)
#> This is lavaan 0.6-18
#> lavaan is FREE software! Please report any bugs.
library(tidySEM)
#> Loading required package: OpenMx
#> To take full advantage of multiple cores, use:
#>   mxOption(key='Number of Threads', value=parallel::detectCores()) #now
#>   Sys.setenv(OMP_NUM_THREADS=parallel::detectCores()) #before library(OpenMx)
#> Registered S3 method overwritten by 'tidySEM':
#>   method          from  
#>   predict.MxModel OpenMx

Note: Covariance Structure Analysis is just another name for structural equation modelling (SEM).

8.1 The General Covariance Structure Model

8.2 The Basics of Latent Growth Modeling

Table 8.1, page 282:

alcohol_use_2_wide <- alcohol_use_2 |>
  pivot_wider(names_from = time, values_from = c(alcohol_use, peer_pressure))

alcohol_use_2_wide
#> # A tibble: 1,122 × 8
#>    id    female alcohol_use_0 alcohol_use_1 alcohol_use_2 peer_pressure_0
#>    <fct>  <dbl>         <dbl>         <dbl>         <dbl>           <dbl>
#>  1 1          0         0.693         0.288         0.511            0   
#>  2 2          0         0             0             0                0   
#>  3 3          0         0             0             0                0   
#>  4 4          0         0             0.511         0.511            1.10
#>  5 5          0         0.288         0             0.847            0   
#>  6 6          0         0             0             0                0   
#>  7 7          0         0.288         0.288         0                0   
#>  8 8          0         0             0             0                0   
#>  9 9          0         0             0.511         0                0   
#> 10 10         0         0.511         0.693         1.30             0   
#> # ℹ 1,112 more rows
#> # ℹ 2 more variables: peer_pressure_1 <dbl>, peer_pressure_2 <dbl>

# Means
alcohol_use_2_wide |>
  summarise(across(female:peer_pressure_2, mean)) |>
  glimpse()
#> Rows: 1
#> Columns: 7
#> $ female          <dbl> 0.6122995
#> $ alcohol_use_0   <dbl> 0.2250666
#> $ alcohol_use_1   <dbl> 0.2541351
#> $ alcohol_use_2   <dbl> 0.287923
#> $ peer_pressure_0 <dbl> 0.1771944
#> $ peer_pressure_1 <dbl> 0.2904569
#> $ peer_pressure_2 <dbl> 0.3470381

# Covariances
cov(select(alcohol_use_2_wide, -c(id, female)))
#>                 alcohol_use_0 alcohol_use_1 alcohol_use_2 peer_pressure_0
#> alcohol_use_0      0.13558718    0.07775260    0.06526470      0.06586967
#> alcohol_use_1      0.07775260    0.15528121    0.08186386      0.04479710
#> alcohol_use_2      0.06526470    0.08186386    0.18075945      0.03988182
#> peer_pressure_0    0.06586967    0.04479710    0.03988182      0.17399159
#> peer_pressure_1    0.06404875    0.09647876    0.06580980      0.07158186
#> peer_pressure_2    0.06008199    0.07433086    0.13197010      0.07071309
#>                 peer_pressure_1 peer_pressure_2
#> alcohol_use_0        0.06404875      0.06008199
#> alcohol_use_1        0.09647876      0.07433086
#> alcohol_use_2        0.06580980      0.13197010
#> peer_pressure_0      0.07158186      0.07071309
#> peer_pressure_1      0.26190160      0.11180554
#> peer_pressure_2      0.11180554      0.28901177

Table 8.2, page 289, Model A:

# Model A: Unconditional model
model_A <- ("
  # Intercept and slope with fixed coefficients
  intercept =~ 1*alcohol_use_0 + 1*alcohol_use_1 + 1*alcohol_use_2
  slope     =~ 0*alcohol_use_0 + .75*alcohol_use_1 + 1.75*alcohol_use_2
")

model_A_fit <- growth(
  model_A, data = alcohol_use_2_wide, estimator = "ml", mimic = "Mplus"
)

summary(model_A_fit)
#> lavaan 0.6-18 ended normally after 32 iterations
#> 
#>   Estimator                                         ML
#>   Optimization method                           NLMINB
#>   Number of model parameters                         8
#> 
#>   Number of observations                          1122
#>   Number of missing patterns                         1
#> 
#> Model Test User Model:
#>                                                       
#>   Test statistic                                 0.048
#>   Degrees of freedom                                 1
#>   P-value (Chi-square)                           0.826
#> 
#> Parameter Estimates:
#> 
#>   Standard errors                             Standard
#>   Information                                 Observed
#>   Observed information based on                Hessian
#> 
#> Latent Variables:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>   intercept =~                                        
#>     alcohol_use_0     1.000                           
#>     alcohol_use_1     1.000                           
#>     alcohol_use_2     1.000                           
#>   slope =~                                            
#>     alcohol_use_0     0.000                           
#>     alcohol_use_1     0.750                           
#>     alcohol_use_2     1.750                           
#> 
#> Covariances:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>   intercept ~~                                        
#>     slope            -0.012    0.005   -2.727    0.006
#> 
#> Intercepts:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>     intercept         0.226    0.011   21.106    0.000
#>     slope             0.036    0.007    4.898    0.000
#> 
#> Variances:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>    .alcohol_use_0     0.048    0.006    7.550    0.000
#>    .alcohol_use_1     0.076    0.004   17.051    0.000
#>    .alcohol_use_2     0.077    0.010    7.756    0.000
#>     intercept         0.087    0.007   12.253    0.000
#>     slope             0.020    0.005    3.795    0.000
fitMeasures(model_A_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#>  0.048  1.000  0.826  1.000  0.000

Figure 8.2, Model A:

lay <- get_layout(
  NA, "intercept", NA,  "slope", NA,
  "alcohol_use_0", NA,  "alcohol_use_1", NA, "alcohol_use_2",
  rows = 2
)

graph_sem(model_A_fit, layout = lay)

Table 8.2, page 289, Model B:

# Model B: Adding female as a time-invariant predictor
model_B <- ("
  # Intercept and slope with fixed coefficients
  intercept =~ 1*alcohol_use_0 + 1*alcohol_use_1 + 1*alcohol_use_2
  slope =~ 0*alcohol_use_0 + .75*alcohol_use_1 + 1.75*alcohol_use_2
  
  # Regressions
  intercept ~ female
  slope ~ female
")

model_B_fit <- growth(
  model_B, data = alcohol_use_2_wide, estimator = "ml", mimic = "Mplus"
)

summary(model_B_fit)
#> lavaan 0.6-18 ended normally after 33 iterations
#> 
#>   Estimator                                         ML
#>   Optimization method                           NLMINB
#>   Number of model parameters                        10
#> 
#>   Number of observations                          1122
#>   Number of missing patterns                         1
#> 
#> Model Test User Model:
#>                                                       
#>   Test statistic                                 1.545
#>   Degrees of freedom                                 2
#>   P-value (Chi-square)                           0.462
#> 
#> Parameter Estimates:
#> 
#>   Standard errors                             Standard
#>   Information                                 Observed
#>   Observed information based on                Hessian
#> 
#> Latent Variables:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>   intercept =~                                        
#>     alcohol_use_0     1.000                           
#>     alcohol_use_1     1.000                           
#>     alcohol_use_2     1.000                           
#>   slope =~                                            
#>     alcohol_use_0     0.000                           
#>     alcohol_use_1     0.750                           
#>     alcohol_use_2     1.750                           
#> 
#> Regressions:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>   intercept ~                                         
#>     female           -0.042    0.022   -1.912    0.056
#>   slope ~                                             
#>     female            0.008    0.015    0.522    0.602
#> 
#> Covariances:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>  .intercept ~~                                        
#>    .slope            -0.012    0.005   -2.661    0.008
#> 
#> Intercepts:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>    .intercept         0.251    0.017   14.653    0.000
#>    .slope             0.031    0.012    2.640    0.008
#> 
#> Variances:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>    .alcohol_use_0     0.049    0.006    7.616    0.000
#>    .alcohol_use_1     0.075    0.004   17.036    0.000
#>    .alcohol_use_2     0.077    0.010    7.789    0.000
#>    .intercept         0.086    0.007   12.191    0.000
#>    .slope             0.019    0.005    3.740    0.000
fitMeasures(model_B_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#>  1.545  2.000  0.462  1.000  0.000

Comparison with baseline model for Model B:

# Baseline for Model B (not shown in table)
model_B_baseline <- ("
  # Intercept and slope with fixed coefficients
  intercept =~ 1*alcohol_use_0 + 1*alcohol_use_1 + 1*alcohol_use_2
  slope =~ 0*alcohol_use_0 + .75*alcohol_use_1 + 1.75*alcohol_use_2
  
  # Regressions
  intercept ~ 0*female
  slope ~ 0*female
  
  alcohol_use_0 ~ 0*1
  alcohol_use_1 ~ 0*1
  alcohol_use_2 ~ 0*1
")

model_B_baseline_fit <- growth(
  model_B_baseline, data = alcohol_use_2_wide, estimator = "ml", mimic = "Mplus"
)

anova(model_B_baseline_fit, model_B_fit)
#> 
#> Chi-Squared Difference Test
#> 
#>                      Df    AIC    BIC  Chisq Chisq diff    RMSEA Df diff
#> model_B_fit           2 2577.9 2628.1 1.5447                            
#> model_B_baseline_fit  4 2577.7 2617.9 5.3665     3.8218 0.028493       2
#>                      Pr(>Chisq)
#> model_B_fit                    
#> model_B_baseline_fit     0.1479

Figure 8.2, Model B:

lay <- get_layout(
  NA, NA, "female", NA, NA,
  NA, "intercept", NA,  "slope", NA,
  "alcohol_use_0", NA,  "alcohol_use_1", NA, "alcohol_use_2",
  rows = 3
)

graph_sem(model_B_fit, layout = lay)

Table 8.2, page 289, Model C:

# Model C: Model B but with slope fixed to zero
model_C <- ("
  # Intercept and slope with fixed coefficients
  intercept =~ 1*alcohol_use_0 + 1*alcohol_use_1 + 1*alcohol_use_2
  slope =~ 0*alcohol_use_0 + .75*alcohol_use_1 + 1.75*alcohol_use_2
  
  # Regressions
  intercept ~ female
  slope ~ 0*female
")

model_C_fit <- growth(
  model_C, data = alcohol_use_2_wide, estimator = "ml", mimic = "Mplus"
)

summary(model_C_fit)
#> lavaan 0.6-18 ended normally after 32 iterations
#> 
#>   Estimator                                         ML
#>   Optimization method                           NLMINB
#>   Number of model parameters                         9
#> 
#>   Number of observations                          1122
#>   Number of missing patterns                         1
#> 
#> Model Test User Model:
#>                                                       
#>   Test statistic                                 1.817
#>   Degrees of freedom                                 3
#>   P-value (Chi-square)                           0.611
#> 
#> Parameter Estimates:
#> 
#>   Standard errors                             Standard
#>   Information                                 Observed
#>   Observed information based on                Hessian
#> 
#> Latent Variables:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>   intercept =~                                        
#>     alcohol_use_0     1.000                           
#>     alcohol_use_1     1.000                           
#>     alcohol_use_2     1.000                           
#>   slope =~                                            
#>     alcohol_use_0     0.000                           
#>     alcohol_use_1     0.750                           
#>     alcohol_use_2     1.750                           
#> 
#> Regressions:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>   intercept ~                                         
#>     female           -0.037    0.019   -1.885    0.059
#>   slope ~                                             
#>     female            0.000                           
#> 
#> Covariances:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>  .intercept ~~                                        
#>    .slope            -0.012    0.005   -2.667    0.008
#> 
#> Intercepts:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>    .intercept         0.248    0.016   15.525    0.000
#>    .slope             0.036    0.007    4.898    0.000
#> 
#> Variances:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>    .alcohol_use_0     0.049    0.006    7.609    0.000
#>    .alcohol_use_1     0.075    0.004   17.036    0.000
#>    .alcohol_use_2     0.077    0.010    7.801    0.000
#>    .intercept         0.086    0.007   12.194    0.000
#>    .slope             0.019    0.005    3.739    0.000
fitMeasures(model_C_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#>  1.817  3.000  0.611  1.000  0.000

Figure 8.2, Model C (same as Model B but with slope ~ 0*female):

graph_sem(model_C_fit, layout = lay)

Table 8.2, page 289, Model D:

# Model D: Adding peer_pressure as a time-varying predictor
model_D <- ("
  # Intercept and slope with fixed coefficients
  alc_intercept =~ 1*alcohol_use_0 + 1*alcohol_use_1 + 1*alcohol_use_2
  alc_slope =~ 0*alcohol_use_0 + .75*alcohol_use_1 + 1.75*alcohol_use_2
  
  peer_intercept =~ 1*peer_pressure_0 + 1*peer_pressure_1 + 1*peer_pressure_2
  peer_slope =~ 0*peer_pressure_0 + .75*peer_pressure_1 + 1.75*peer_pressure_2
  
  # Regressions
  alc_intercept ~ start(.8)*peer_intercept + start(.08)*peer_slope
  alc_slope ~ start(-.1)*peer_intercept + start(.6)*peer_slope
  
  # Time-varying covariances
  alcohol_use_0 ~~ peer_pressure_0
  alcohol_use_1 ~~ peer_pressure_1
  alcohol_use_2 ~~ peer_pressure_2
  
  # Fix intercepts to zero
  alcohol_use_0 ~ 0*1
  alcohol_use_1 ~ 0*1
  alcohol_use_2 ~ 0*1
  peer_pressure_0 ~ 0*1
  peer_pressure_1 ~ 0*1
  peer_pressure_2 ~ 0*1

")

model_D_fit <- growth(
  model_D, data = alcohol_use_2_wide, estimator = "ml", mimic = "Mplus"
)

summary(model_D_fit)
#> lavaan 0.6-18 ended normally after 72 iterations
#> 
#>   Estimator                                         ML
#>   Optimization method                           NLMINB
#>   Number of model parameters                        23
#> 
#>   Number of observations                          1122
#>   Number of missing patterns                         1
#> 
#> Model Test User Model:
#>                                                       
#>   Test statistic                                11.557
#>   Degrees of freedom                                 4
#>   P-value (Chi-square)                           0.021
#> 
#> Parameter Estimates:
#> 
#>   Standard errors                             Standard
#>   Information                                 Observed
#>   Observed information based on                Hessian
#> 
#> Latent Variables:
#>                     Estimate  Std.Err  z-value  P(>|z|)
#>   alc_intercept =~                                     
#>     alcohol_use_0      1.000                           
#>     alcohol_use_1      1.000                           
#>     alcohol_use_2      1.000                           
#>   alc_slope =~                                         
#>     alcohol_use_0      0.000                           
#>     alcohol_use_1      0.750                           
#>     alcohol_use_2      1.750                           
#>   peer_intercept =~                                    
#>     peer_pressur_0     1.000                           
#>     peer_pressur_1     1.000                           
#>     peer_pressur_2     1.000                           
#>   peer_slope =~                                        
#>     peer_pressur_0     0.000                           
#>     peer_pressur_1     0.750                           
#>     peer_pressur_2     1.750                           
#> 
#> Regressions:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>   alc_intercept ~                                     
#>     peer_intercept    0.799    0.103    7.781    0.000
#>     peer_slope        0.080    0.184    0.438    0.661
#>   alc_slope ~                                         
#>     peer_intercept   -0.143    0.076   -1.884    0.060
#>     peer_slope        0.577    0.193    2.990    0.003
#> 
#> Covariances:
#>                     Estimate  Std.Err  z-value  P(>|z|)
#>  .alcohol_use_0 ~~                                     
#>    .peer_pressur_0     0.011    0.006    1.773    0.076
#>  .alcohol_use_1 ~~                                     
#>    .peer_pressur_1     0.034    0.005    7.324    0.000
#>  .alcohol_use_2 ~~                                     
#>    .peer_pressur_2     0.037    0.010    3.663    0.000
#>   peer_intercept ~~                                    
#>     peer_slope         0.001    0.007    0.166    0.868
#>  .alc_intercept ~~                                     
#>    .alc_slope         -0.006    0.005   -1.249    0.212
#> 
#> Intercepts:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>    .alcohol_use_0     0.000                           
#>    .alcohol_use_1     0.000                           
#>    .alcohol_use_2     0.000                           
#>    .peer_pressur_0    0.000                           
#>    .peer_pressur_1    0.000                           
#>    .peer_pressur_2    0.000                           
#>    .alc_intercept     0.067    0.016    4.252    0.000
#>    .alc_slope         0.008    0.015    0.564    0.573
#>     peer_intercept    0.188    0.012   15.743    0.000
#>     peer_slope        0.096    0.010    9.922    0.000
#> 
#> Variances:
#>                    Estimate  Std.Err  z-value  P(>|z|)
#>    .alcohol_use_0     0.048    0.006    7.553    0.000
#>    .alcohol_use_1     0.076    0.004   17.165    0.000
#>    .alcohol_use_2     0.076    0.010    7.819    0.000
#>    .peer_pressur_0    0.106    0.011    9.790    0.000
#>    .peer_pressur_1    0.171    0.009   19.713    0.000
#>    .peer_pressur_2    0.129    0.018    7.325    0.000
#>    .alc_intercept     0.042    0.007    5.649    0.000
#>    .alc_slope         0.009    0.005    1.697    0.090
#>     peer_intercept    0.070    0.010    6.729    0.000
#>     peer_slope        0.028    0.009    3.214    0.001
fitMeasures(model_D_fit, c("chisq", "df", "pvalue", "cfi", "rmsea"))
#>  chisq     df pvalue    cfi  rmsea 
#> 11.557  4.000  0.021  0.996  0.041

Comparison with baseline model for Model D:

# Baseline for Model D (not shown in table)
model_D_baseline <- ("
  # Intercepts and slopes with fixed coefficients
  alc_intercept =~ 1*alcohol_use_0 + 1*alcohol_use_1 + 1*alcohol_use_2
  alc_slope =~ 0*alcohol_use_0 + .75*alcohol_use_1 + 1.75*alcohol_use_2
  
  peer_intercept =~ 1*peer_pressure_0 + 1*peer_pressure_1 + 1*peer_pressure_2
  peer_slope =~ 0*peer_pressure_0 + .75*peer_pressure_1 + 1.75*peer_pressure_2
  
  # Regressions
  alc_intercept ~ 0*peer_intercept + 0*peer_slope
  alc_slope ~ 0*peer_intercept + 0*peer_slope
  
  # Time-varying covariances
  alcohol_use_0 ~~ peer_pressure_0
  alcohol_use_1 ~~ peer_pressure_1
  alcohol_use_2 ~~ peer_pressure_2
  
  alcohol_use_0 ~ 0*1
  alcohol_use_1 ~ 0*1
  alcohol_use_2 ~ 0*1
  peer_pressure_0 ~ 0*1
  peer_pressure_1 ~ 0*1
  peer_pressure_2 ~ 0*1
")

model_D_baseline_fit <- growth(
  model_D_baseline, data = alcohol_use_2_wide, estimator = "ml", mimic = "Mplus"
)

anova(model_D_baseline_fit, model_D_fit)
#> 
#> Chi-Squared Difference Test
#> 
#>                      Df    AIC    BIC   Chisq Chisq diff   RMSEA Df diff
#> model_D_fit           4 6120.5 6236.1  11.557                           
#> model_D_baseline_fit  8 6443.6 6539.1 342.648     331.09 0.26997       4
#>                      Pr(>Chisq)    
#> model_D_fit                        
#> model_D_baseline_fit  < 2.2e-16 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Figure 8.2, Model D:

lay <- get_layout(
  "peer_pressure_0", NA,  "peer_pressure_1", NA, "peer_pressure_2",
  NA, "peer_intercept", NA,  "peer_slope", NA,
  NA, "alc_intercept", NA,  "alc_slope", NA,
  "alcohol_use_0", NA,  "alcohol_use_1", NA, "alcohol_use_2",
  rows = 4
)

graph_sem(model_D_fit, layout = lay)

8.3 Cross-Domain Analysis of Change

8.4 Extensions of Latent Growth Modeling