This document will outline the code used to define a cross-lagged panel model which will allow for more fine grained analysis of the effect of social resources on eating behaviours.
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_clpm <- '
# Measurement model: latent social provisions at each wave
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
# Autoregressive effects
socialprov_t2 ~ socialprov_t1
socialprov_t3 ~ socialprov_t2
eating_t2 ~ eating_t1
eating_t3 ~ eating_t2
exercise_t2 ~ exercise_t1
exercise_t3 ~ exercise_t2
# Cross-lagged effects
eating_t2 ~ socialprov_t1 + ses_proxy
eating_t3 ~ socialprov_t2 + ses_proxy
exercise_t2 ~ socialprov_t1 + ses_proxy
exercise_t3 ~ socialprov_t2 + ses_proxy
# Covariances at each wave
eating_t1 ~~ exercise_t1
eating_t2 ~~ exercise_t2
eating_t3 ~~ exercise_t3
# SES covariances
ses_proxy ~~ socialprov_t1
# Define observed variables (rename for clarity)
eating_t1 =~ 1*CW1_FRTVEGSP_trim
eating_t2 =~ 1*CW2_FRTVEGSP
eating_t3 =~ 1*CW3_FRTVEGSP
exercise_t1 =~ 1*CW1_EXCISESP_scaled
exercise_t2 =~ 1*CW2_EXCISESP
exercise_t3 =~ 1*CW3_EXCISESP
# Variances (optional, helps with identifiability)
socialprov_t1 ~~ socialprov_t1
socialprov_t2 ~~ socialprov_t2
socialprov_t3 ~~ socialprov_t3
ses_proxy ~~ ses_proxy
eating_t1 ~~ eating_t1
eating_t2 ~~ eating_t2
eating_t3 ~~ eating_t3
exercise_t1 ~~ exercise_t1
exercise_t2 ~~ exercise_t2
exercise_t3 ~~ exercise_t3
'
fit_clpm <- lavaan::sem(model_clpm, data = long_data1, missing = "ML", fixed.x = FALSE)
summary(fit_clpm, fit.measures = TRUE, standardized = TRUE)
## lavaan 0.6-19 ended normally after 166 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 392.752
## Degrees of freedom 89
## P-value (Chi-square) 0.000
##
## Model Test Baseline Model:
##
## Test statistic 18226.076
## Degrees of freedom 120
## P-value 0.000
##
## User Model versus Baseline Model:
##
## Comparative Fit Index (CFI) 0.983
## Tucker-Lewis Index (TLI) 0.977
##
## Robust Comparative Fit Index (CFI) NA
## Robust Tucker-Lewis Index (TLI) NA
##
## Loglikelihood and Information Criteria:
##
## Loglikelihood user model (H0) -294186.697
## Loglikelihood unrestricted model (H1) -293990.321
##
## Akaike (AIC) 588499.394
## Bayesian (BIC) 589065.320
## Sample-size adjusted Bayesian (SABIC) 588865.105
##
## Root Mean Square Error of Approximation:
##
## RMSEA 0.008
## 90 Percent confidence interval - lower 0.007
## 90 Percent confidence interval - upper 0.008
## 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.107
##
## 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.263 0.628
## CW1_SOCPROV_2 1.276 0.040 31.912 0.000 0.335 0.745
## CW1_SOCPROV_3 0.906 0.028 32.111 0.000 0.238 0.571
## 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
## eating_t1 =~
## CW1_FRTVEGSP_t 1.000 1.881 1.000
## eating_t2 =~
## CW2_FRTVEGSP 1.000 2.246 1.000
## eating_t3 =~
## CW3_FRTVEGSP 1.000 2.390 1.000
## exercise_t1 =~
## CW1_EXCISESP_s 1.000 0.862 1.000
## exercise_t2 =~
## CW2_EXCISESP 1.000 2.181 1.000
## exercise_t3 =~
## CW3_EXCISESP 1.000 2.231 1.000
##
## Regressions:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## socialprov_t2 ~
## socialprov_t1 -1.047 NA -0.892 -0.892
## socialprov_t3 ~
## socialprov_t2 -0.000 NA -0.000 -0.000
## eating_t2 ~
## eating_t1 -0.425 68.027 -0.006 0.995 -0.356 -0.356
## eating_t3 ~
## eating_t2 -0.075 NA -0.070 -0.070
## exercise_t2 ~
## exercise_t1 -0.139 379.725 -0.000 1.000 -0.055 -0.055
## exercise_t3 ~
## exercise_t2 -0.309 1.675 -0.184 0.854 -0.302 -0.302
## eating_t2 ~
## socialprov_t1 0.670 20.650 0.032 0.974 0.078 0.078
## ses_proxy -0.098 85.717 -0.001 0.999 -0.044 -0.019
## eating_t3 ~
## socialprov_t2 0.000 NA 0.000 0.000
## ses_proxy -0.004 NA -0.001 -0.001
## exercise_t2 ~
## socialprov_t1 0.666 71.703 0.009 0.993 0.080 0.080
## ses_proxy -0.071 NA -0.033 -0.015
## exercise_t3 ~
## socialprov_t2 -0.000 NA -0.000 -0.000
## ses_proxy -0.013 134.910 -0.000 1.000 -0.006 -0.003
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## eating_t1 ~~
## exercise_t1 0.325 0.014 24.048 0.000 0.200 0.200
## .eating_t2 ~~
## .exercise_t2 0.707 52.846 0.013 0.989 0.156 0.156
## .eating_t3 ~~
## .exercise_t3 0.727 NA 0.143 0.143
## socialprov_t1 ~~
## ses_proxy -0.020 0.002 -11.785 0.000 -0.074 -0.167
## eating_t1 -0.046 0.008 -5.941 0.000 -0.093 -0.093
## exercise_t1 -0.014 0.003 -4.175 0.000 -0.064 -0.064
## .socialprov_t3 ~~
## .eating_t3 -0.059 NA -0.077 -0.077
## .exercise_t3 -0.075 NA -0.111 -0.111
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .CW1_SOCPROV_1 1.177 0.005 230.795 0.000 1.177 2.815
## .CW1_SOCPROV_2 1.182 0.005 215.031 0.000 1.182 2.627
## .CW1_SOCPROV_3 1.139 0.005 222.488 0.000 1.139 2.731
## .CW2_SOCPROV_1 1.247 0.004 284.387 0.000 1.247 2.627
## .CW2_SOCPROV_2 1.221 0.005 270.602 0.000 1.221 2.500
## .CW2_SOCPROV_3 1.208 0.005 264.725 0.000 1.208 2.447
## .CW3_SOCPROV_1 1.242 0.005 257.082 0.000 1.242 2.654
## .CW3_SOCPROV_2 1.221 0.005 243.069 0.000 1.221 2.510
## .CW3_SOCPROV_3 1.212 0.005 239.110 0.000 1.212 2.469
## .CW1_FRTVEGSP_t 3.841 0.015 251.263 0.000 3.841 2.042
## .CW2_FRTVEGSP 4.226 323.156 0.013 0.990 4.226 1.881
## .CW3_FRTVEGSP 3.924 NA 3.924 1.642
## .CW1_EXCISESP_s 1.199 0.007 173.236 0.000 1.199 1.391
## .CW2_EXCISESP 3.260 NA 3.260 1.495
## .CW3_EXCISESP 3.023 509.136 0.006 0.995 3.023 1.355
## ses_proxy 3.770 0.003 1077.682 0.000 3.770 8.497
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## socialprov_t1 0.069 0.003 22.233 0.000 1.000 1.000
## .socialprov_t2 0.019 NA 0.204 0.204
## .socialprov_t3 0.102 NA 1.000 1.000
## ses_proxy 0.197 0.002 89.681 0.000 0.197 1.000
## eating_t1 3.536 0.041 86.920 0.000 1.000 1.000
## .eating_t2 4.345 202.661 0.021 0.983 0.861 0.861
## .eating_t3 5.686 NA 0.995 0.995
## exercise_t1 0.743 0.008 88.031 0.000 1.000 1.000
## .exercise_t2 4.707 78.008 0.060 0.952 0.989 0.989
## .exercise_t3 4.525 NA 0.909 0.909
## .CW1_SOCPROV_1 0.106 0.003 39.090 0.000 0.106 0.606
## .CW1_SOCPROV_2 0.090 0.004 25.227 0.000 0.090 0.446
## .CW1_SOCPROV_3 0.117 0.003 44.570 0.000 0.117 0.674
## .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
## .CW1_FRTVEGSP_t 0.000 0.000 0.000
## .CW2_FRTVEGSP 0.000 0.000 0.000
## .CW3_FRTVEGSP 0.000 0.000 0.000
## .CW1_EXCISESP_s 0.000 0.000 0.000
## .CW2_EXCISESP 0.000 0.000 0.000
## .CW3_EXCISESP 0.000 0.000 0.000
library(semPlot)
library(semPlot)
# Custom label names for clarity (optional)
node_labels <- c(
"SocialProv_T1", "SocialProv_T2", "SocialProv_T3",
"Eating_T1", "Eating_T2", "Eating_T3",
"Exercise_T1", "Exercise_T2", "Exercise_T3",
"SES"
)
# Plot with modifications
semPaths(
object = fit_clpm,
what = "std", # show standardized estimates
layout = "tree", # vertical ordering by time
style = "lisrel", # cleaner SEM look
edge.label.cex = 0.8,
fade = FALSE,
curve = 1.5, # less curvature for readability
sizeMan = 6,
sizeLat = 8,
nCharNodes = 0,
residuals = FALSE, # HIDE residual variances
intercepts = FALSE, # HIDE means/intercepts
nodeLabels = node_labels, # custom labels
reorder = TRUE, # arrange left to right
exoCov = TRUE # show covariances with SES
)