Structural Equation Modelling

This document will define the model of resource access and its impact on eating behaviours, exploring both SES and Social Resources as mediators for eating behaviours in the Covid-19 pandemic.

library(lavaan)
library(MASS)
library(semPlot)
library(dagitty)
library(tidyverse)
library(haven)
wave1 <- read_sav("covid-19_wave1_survey_cls.sav") |> mutate(wave = 1)
wave2 <- read_sav("covid-19_wave2_survey_cls.sav") |> mutate(wave = 2)
wave3 <- read_sav("covid-19_wave3_survey_cls.sav") |> mutate(wave = 3)

long_data1 <- bind_rows(wave1, wave2, wave3)


#Reverse coding SOCPROV_3 
long_data1$CW1_SOCPROV_3 <- 4 - long_data1$CW1_SOCPROV_3
long_data1$CW2_SOCPROV_3 <- 4 - long_data1$CW2_SOCPROV_3
long_data1$CW3_SOCPROV_3 <- 4 - long_data1$CW3_SOCPROV_3

#converting labelled variables to numeric 
long_data1 <- long_data1 |> mutate(across(where(is.labelled), as.numeric))

#converting CW1_EXCISESP to a consistent scale
long_data1 <- long_data1 |>
  mutate(CW1_EXCISESP_scaled = scales::rescale(CW1_EXCISESP, to = c(0, 7)))

#removing unit errors or misunderstandings from CW1_FRTVEGSP
long_data1 <- long_data1 |>
  mutate(
    CW1_FRTVEGSP_trim = ifelse(CW1_FRTVEGSP > 20, NA, CW1_FRTVEGSP)
  )

#reverse coding CW1_FOODAFFORD
long_data1 <- long_data1 |>
  mutate(
    ses_proxy = case_when(
      CW1_FOODAFFORD == 1 ~ 4,
      CW1_FOODAFFORD == 2 ~ 3,
      CW1_FOODAFFORD == 3 ~ 2,
      CW1_FOODAFFORD == 4 ~ 1,
      TRUE ~ NA_real_
    )
  )
model_sem <- '
  # Measurement models
  socialprov_t1 =~ CW1_SOCPROV_1 + CW1_SOCPROV_2 + CW1_SOCPROV_3
  socialprov_t2 =~ CW2_SOCPROV_1 + CW2_SOCPROV_2 + CW2_SOCPROV_3
  socialprov_t3 =~ CW3_SOCPROV_1 + CW3_SOCPROV_2 + CW3_SOCPROV_3

  # Longitudinal autoregressive effects
  socialprov_t2 ~ socialprov_t1 + ses_proxy
  socialprov_t3 ~ socialprov_t2 + ses_proxy

  CW2_FRTVEGSP ~ CW1_FRTVEGSP_trim + socialprov_t1 + ses_proxy
  CW3_FRTVEGSP ~ CW2_FRTVEGSP + socialprov_t2 + ses_proxy

  CW2_EXCISESP ~ CW1_EXCISESP_scaled + socialprov_t1 + ses_proxy
  CW3_EXCISESP ~ CW2_EXCISESP + socialprov_t2 + ses_proxy

'
fit_sem <- lavaan::sem(
  model_sem,
  data = long_data1,
  missing = "fiml",
  fixed.x = FALSE 
)

summary(fit_sem, fit.measures = TRUE, standardized = TRUE)
## lavaan 0.6-19 ended normally after 162 iterations
## 
##   Estimator                                         ML
##   Optimization method                           NLMINB
##   Number of model parameters                        63
## 
##                                                   Used       Total
##   Number of observations                         58862       67518
##   Number of missing patterns                        59            
## 
## Model Test User Model:
##                                                       
##   Test statistic                               292.404
##   Degrees of freedom                                89
##   P-value (Chi-square)                           0.000
## 
## Model Test Baseline Model:
## 
##   Test statistic                             17288.816
##   Degrees of freedom                               117
##   P-value                                        0.000
## 
## User Model versus Baseline Model:
## 
##   Comparative Fit Index (CFI)                    0.988
##   Tucker-Lewis Index (TLI)                       0.984
##                                                       
##   Robust Comparative Fit Index (CFI)                NA
##   Robust Tucker-Lewis Index (TLI)                   NA
## 
## Loglikelihood and Information Criteria:
## 
##   Loglikelihood user model (H0)            -294136.523
##   Loglikelihood unrestricted model (H1)    -293990.321
##                                                       
##   Akaike (AIC)                              588399.046
##   Bayesian (BIC)                            588964.972
##   Sample-size adjusted Bayesian (SABIC)     588764.757
## 
## Root Mean Square Error of Approximation:
## 
##   RMSEA                                          0.006
##   90 Percent confidence interval - lower         0.005
##   90 Percent confidence interval - upper         0.007
##   P-value H_0: RMSEA <= 0.050                    1.000
##   P-value H_0: RMSEA >= 0.080                    0.000
##                                                       
##   Robust RMSEA                                      NA
##   90 Percent confidence interval - lower            NA
##   90 Percent confidence interval - upper            NA
##   P-value H_0: Robust RMSEA <= 0.050                NA
##   P-value H_0: Robust RMSEA >= 0.080                NA
## 
## Standardized Root Mean Square Residual:
## 
##   SRMR                                           0.075
## 
## Parameter Estimates:
## 
##   Standard errors                             Standard
##   Information                                 Observed
##   Observed information based on                Hessian
## 
## Latent Variables:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##   socialprov_t1 =~                                                      
##     CW1_SOCPROV_1     1.000                               0.259    0.618
##     CW1_SOCPROV_2     1.332    0.044   29.997    0.000    0.345    0.764
##     CW1_SOCPROV_3     0.911    0.028   32.135    0.000    0.236    0.564
##   socialprov_t2 =~                                                      
##     CW2_SOCPROV_1     1.000                               0.308    0.649
##     CW2_SOCPROV_2     1.270    0.029   43.851    0.000    0.392    0.802
##     CW2_SOCPROV_3     0.889    0.020   45.525    0.000    0.274    0.555
##   socialprov_t3 =~                                                      
##     CW3_SOCPROV_1     1.000                               0.319    0.682
##     CW3_SOCPROV_2     1.204    0.027   44.362    0.000    0.384    0.790
##     CW3_SOCPROV_3     0.920    0.021   44.527    0.000    0.293    0.598
## 
## Regressions:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##   socialprov_t2 ~                                                       
##     socialprov_t1     0.000   94.099    0.000    1.000    0.000    0.000
##     ses_proxy         0.187       NA                      0.606    0.269
##   socialprov_t3 ~                                                       
##     socialprov_t2    -0.001   31.234   -0.000    1.000   -0.001   -0.001
##     ses_proxy         0.000   96.302    0.000    1.000    0.000    0.000
##   CW2_FRTVEGSP ~                                                        
##     CW1_FRTVEGSP_t    0.624   27.310    0.023    0.982    0.624    0.522
##     socialprov_t1     0.000  405.887    0.000    1.000    0.000    0.000
##     ses_proxy        -2.259   18.290   -0.124    0.902   -2.259   -0.446
##   CW3_FRTVEGSP ~                                                        
##     CW2_FRTVEGSP      0.000       NA                      0.000    0.000
##     socialprov_t2    -0.000       NA                     -0.000   -0.000
##     ses_proxy        -0.000       NA                     -0.000   -0.000
##   CW2_EXCISESP ~                                                        
##     CW1_EXCISESP_s    1.281   73.780    0.017    0.986    1.281    0.506
##     socialprov_t1     0.000  492.839    0.000    1.000    0.000    0.000
##     ses_proxy        -1.645    5.917   -0.278    0.781   -1.645   -0.335
##   CW3_EXCISESP ~                                                        
##     CW2_EXCISESP      0.000  108.914    0.000    1.000    0.000    0.000
##     socialprov_t2     0.000  807.808    0.000    1.000    0.000    0.000
##     ses_proxy        -0.000  596.483   -0.000    1.000   -0.000   -0.000
## 
## Covariances:
##                        Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##  .socialprov_t3 ~~                                                          
##    .CW3_FRTVEGSP         -0.059       NA                     -0.185   -0.077
##    .CW3_EXCISESP         -0.075    0.077   -0.977    0.329   -0.235   -0.105
##  .CW3_FRTVEGSP ~~                                                           
##    .CW3_EXCISESP          0.745    0.061   12.248    0.000    0.745    0.140
##   ses_proxy ~~                                                              
##     CW1_FRTVEGSP_t        0.106    0.007   15.348    0.000    0.106    0.127
##     CW1_EXCISESP_s        0.037    0.003   11.958    0.000    0.037    0.097
##   CW1_FRTVEGSP_trim ~~                                                      
##     CW1_EXCISESP_s        0.325    0.014   24.054    0.000    0.325    0.201
## 
## Intercepts:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##    .CW1_SOCPROV_1     1.184    0.005  231.634    0.000    1.184    2.825
##    .CW1_SOCPROV_2     1.191    0.006  215.860    0.000    1.191    2.639
##    .CW1_SOCPROV_3     1.145    0.005  223.296    0.000    1.145    2.741
##    .CW2_SOCPROV_1     0.542       NA                      0.542    1.142
##    .CW2_SOCPROV_2     0.326       NA                      0.326    0.667
##    .CW2_SOCPROV_3     0.581       NA                      0.581    1.178
##    .CW3_SOCPROV_1     1.242  362.392    0.003    0.997    1.242    2.655
##    .CW3_SOCPROV_2     1.221  436.446    0.003    0.998    1.221    2.511
##    .CW3_SOCPROV_3     1.212  333.283    0.004    0.997    1.212    2.470
##    .CW2_FRTVEGSP      9.980       NA                      9.980    4.443
##    .CW3_FRTVEGSP      3.938       NA                      3.938    1.647
##    .CW2_EXCISESP      7.657       NA                      7.657    3.510
##    .CW3_EXCISESP      3.056 2283.456    0.001    0.999    3.056    1.370
##     ses_proxy         3.770    0.003 1077.686    0.000    3.770    8.497
##     CW1_FRTVEGSP_t    3.839    0.015  251.231    0.000    3.839    2.041
##     CW1_EXCISESP_s    1.199    0.007  173.212    0.000    1.199    1.391
## 
## Variances:
##                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
##    .CW1_SOCPROV_1     0.108    0.003   39.094    0.000    0.108    0.618
##    .CW1_SOCPROV_2     0.085    0.004   21.625    0.000    0.085    0.416
##    .CW1_SOCPROV_3     0.119    0.003   44.438    0.000    0.119    0.681
##    .CW2_SOCPROV_1     0.130    0.003   50.356    0.000    0.130    0.578
##    .CW2_SOCPROV_2     0.085    0.003   25.535    0.000    0.085    0.357
##    .CW2_SOCPROV_3     0.169    0.003   62.552    0.000    0.169    0.692
##    .CW3_SOCPROV_1     0.117    0.003   44.150    0.000    0.117    0.535
##    .CW3_SOCPROV_2     0.089    0.003   27.679    0.000    0.089    0.376
##    .CW3_SOCPROV_3     0.155    0.003   54.327    0.000    0.155    0.642
##    .CW2_FRTVEGSP      2.963  129.843    0.023    0.982    2.963    0.587
##    .CW3_FRTVEGSP      5.714       NA                      5.714    1.000
##    .CW2_EXCISESP      3.162  139.749    0.023    0.982    3.162    0.665
##    .CW3_EXCISESP      4.978    0.164   30.436    0.000    4.978    1.000
##     socialprov_t1     0.067    0.003   21.321    0.000    1.000    1.000
##    .socialprov_t2     0.088       NA                      0.928    0.928
##    .socialprov_t3     0.102    0.008   13.242    0.000    1.000    1.000
##     ses_proxy         0.197    0.002   89.680    0.000    0.197    1.000
##     CW1_FRTVEGSP_t    3.537    0.041   86.899    0.000    3.537    1.000
##     CW1_EXCISESP_s    0.743    0.008   88.022    0.000    0.743    1.000
semPaths(
  object = fit_sem,             # your lavaan fit object
  what = "std",                 # standardized estimates
  whatLabels = "std",           # display standardized values
  style = "lisrel",             # cleaner layout
  layout = "tree",              # top-down structure
  edge.label.cex = 0.9,         # size of the path coefficients
  sizeMan = 5,                  # size of manifest variables
  sizeLat = 7,                  # size of latent variables
  edge.color = "blue",         # path color
  residuals = FALSE,            # hide residuals for clarity
  intercepts = FALSE,           # hide intercepts
  nCharNodes = 0,               # show full variable names
  optimizeLatRes = TRUE,        # avoid overlap
  rotation = 2,                 # flip layout if needed
  title = FALSE,                # remove default title
  asize = 1.5                   # arrow size
)

library(dagitty)

dag <- dagitty("
dag {
  ses_proxy -> eating_t1
  ses_proxy -> eating_t2
  ses_proxy -> eating_t3
  ses_proxy -> exercise_t1
  ses_proxy -> exercise_t2
  ses_proxy -> exercise_t3

  socialprov_t1 -> eating_t1
  socialprov_t1 -> exercise_t1
  socialprov_t2 -> eating_t2
  socialprov_t2 -> exercise_t2
  socialprov_t3 -> eating_t3
  socialprov_t3 -> exercise_t3

  socialprov_t1 -> socialprov_t2
  socialprov_t2 -> socialprov_t3

  eating_t1 -> eating_t2
  eating_t2 -> eating_t3

  exercise_t1 -> exercise_t2
  exercise_t2 -> exercise_t3

}
")

# plot
plot(dag)
## Plot coordinates for graph not supplied! Generating coordinates, see ?coordinates for how to set your own.

library(DiagrammeR)
## Warning: package 'DiagrammeR' was built under R version 4.4.3
grViz("
digraph SEM {
  
  # node styles
  node [shape = box, style = filled, color = lightgray, fontname = Helvetica]
  
  # SES and socialprov nodes
  ses [label = 'SES (ses_proxy)', shape = ellipse, color = lightblue]
  sp1 [label = 'Social Provision T1', shape = ellipse, color = lightgreen]
  sp2 [label = 'Social Provision T2', shape = ellipse, color = lightgreen]
  sp3 [label = 'Social Provision T3', shape = ellipse, color = lightgreen]
  
  # Eating and exercise observed variables
  eat1 [label = 'Eating T1']
  ex1  [label = 'Exercise T1']
  eat2 [label = 'Eating T2']
  ex2  [label = 'Exercise T2']
  eat3 [label = 'Eating T3']
  ex3  [label = 'Exercise T3']

  # Paths T1
  sp1 -> eat1
  ses -> eat1
  sp1 -> ex1
  ses -> ex1

  # Paths T2
  sp2 -> eat2
  ses -> eat2
  sp2 -> ex2
  ses -> ex2

  # Paths T3
  sp3 -> eat3
  ses -> eat3
  sp3 -> ex3
  ses -> ex3

  # autoregressive paths
  eat1 -> eat2
  eat2 -> eat3
  ex1  -> ex2
  ex2  -> ex3

  # socialprov continuity
  sp1 -> sp2
  sp2 -> sp3
}
")