The Cortisol Data
Growth Models: Simple to Complex
Overview
This script shares The Cortisol Data, an N = 34, T = 9 time points data set we have used to illustrate a variety of growth modeling and mixture modeling methods. We describe the data and then walk through R-based implementations of the models covered in:
articulate developmental change: Matching theory to method.
International Journal of Behavioral Development, 31(4), 303-316.
https://doi.org/10.1177/0165025407077751
.
The data are shared with intent that others may find them useful for learning about growth modeling or developing new methods for analysis of change.
New publications based on these data require citation and acknowledgement of the full set of papers that have used the data, inlcuding the above paper, and …
identifying differences in longitudinal change among unobserved groups.
International Journal of Behavioral Development, 33(6), 565-576.
https://doi.org/10.1177/0165025409343765
Ram, N., Grimm, K., Gatzke-Kopp, L. & Molenaar, P.C.M. (2011).
Longitudinal mixture models and the identification of archetypes:
Action-adventure, mystery, science fiction, fantasy, or romance? In B.
Laursen, T. Little, & N. Card (Eds.) Handbook of Developmental Research
Methods (pp. 481-500). New York: Guilford.
Grimm, K.J., Steele, J.S., Ram, N., & Nesselroade, J.R. (2013).
Exploratory latent growth models in the structural equation modeling
framework. Structural Equation Modeling, 20(4), 568-591.
https://doi.org/10.1080/10705511.2013.824775
.
Nilam & Kevin
Outline
This script covers …
- Loading the Cortisol Data
- Reshaping the Cortisol Data
- Fitting the series of growth models from Ram & Grimm (2007):
- Linear
- Quadratic
- Latent Basis
- Exponential
- Multiphase
Preliminaries
Loading libraries used in this script.
library(psych) # data descriptives
library(nlme) # Linear & Nonlinear Mixed Effects Models
library(lme4) # Linear Mixed-Effects Models using 'Eigen' and S4
library(lmerTest) # Model p-values
library(tidyverse) # ggplot, stringr, dplyr, readr, forcats, tidyr, purrLoading the public data
#set filepath for data file
filepath <- "https://raw.githubusercontent.com/The-Change-Lab/collaborations/main/TheCortisolData/TheCortisolData.csv"
#read in the .csv file using the url() function
cortisol_wide <- read.csv(file=url(filepath), header=TRUE)Looking at the top few rows of the wide data.
## id cort_0 cort_1 cort_2 cort_3 cort_4 cort_5 cort_6 cort_7 cort_8
## 1 1 4.2 4.1 9.7 14.0 19.0 18.0 20.0 23.0 24.0
## 2 2 5.5 5.6 14.0 16.0 19.0 17.0 18.0 20.0 19.0
## 3 3 4.0 3.8 7.5 12.0 14.0 13.0 9.1 8.2 7.9
## 4 4 6.1 5.6 14.0 20.0 26.0 23.0 26.0 25.0 26.0
## 5 5 4.6 4.4 7.2 12.3 15.8 16.1 17.0 17.8 19.1
## 6 6 6.8 9.5 14.2 19.6 19.0 13.9 13.4 12.5 11.7
Reshaping the data
Two main data schema are used to accommodate repeated measures data - “Wide Format” and “Long Format”. Different functions work with different kinds of data input. We already have the wide format data. We make a set of long format data.
Reshape from wide to long
#reshaping wide to long
cortisol_long <- reshape(data=cortisol_wide,
timevar=c("time"),
idvar="id",
varying=c("cort_0","cort_1","cort_2","cort_3",
"cort_4","cort_5","cort_6","cort_7","cort_8"),
direction="long", sep="_")
#sorting for easy viewing
#order by id and time
cortisol_long <- cortisol_long[order(cortisol_long$id,cortisol_long$time), ]To match the scaling of time used in some of the papers we add an additional time variable that runs from 0 to 1.
Looking at the top few rows of the long data.
## id time cort timescaled
## 1.0 1 0 4.2 0.000
## 1.1 1 1 4.1 0.125
## 1.2 1 2 9.7 0.250
## 1.3 1 3 14.0 0.375
## 1.4 1 4 19.0 0.500
## 1.5 1 5 18.0 0.625
## 1.6 1 6 20.0 0.750
## 1.7 1 7 23.0 0.875
## 1.8 1 8 24.0 1.000
## 2.0 2 0 5.5 0.000
## 2.1 2 1 5.6 0.125
## 2.2 2 2 14.0 0.250
## 2.3 2 3 16.0 0.375
## 2.4 2 4 19.0 0.500
## 2.5 2 5 17.0 0.625
## 2.6 2 6 18.0 0.750
## 2.7 2 7 20.0 0.875
## 2.8 2 8 19.0 1.000
Fitting the series of growth models from Ram & Grimm (2007)
Linear Growth Model
#linear model
cort_linear <- nlme(cort ~ g0 + g1*timescaled,
fixed = g0 + g1 ~ 1,
random = g0 + g1 ~ 1,
group = ~id,
start = c(g0=8.0, g1=10.9),
data = cortisol_long,
na.action = "na.exclude")## Warning in nlme.formula(cort ~ g0 + g1 * timescaled, fixed = g0 + g1 ~ 1, :
## Iteration 1, LME step: nlminb() did not converge (code = 1). Do increase
## 'msMaxIter'!
## Nonlinear mixed-effects model fit by maximum likelihood
## Model: cort ~ g0 + g1 * timescaled
## Data: cortisol_long
## AIC BIC logLik
## 1837.972 1860.313 -912.9859
##
## Random effects:
## Formula: list(g0 ~ 1, g1 ~ 1)
## Level: id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## g0 0.2031221 g0
## g1 4.4910644 0.999
## Residual 4.3824247
##
## Fixed effects: g0 + g1 ~ 1
## Value Std.Error DF t-value p-value
## g0 8.000588 0.4647811 271 17.213670 0
## g1 10.881176 1.0970972 271 9.918152 0
## Correlation:
## g0
## g1 -0.542
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -1.8427253 -0.7510109 -0.1820519 0.5969253 2.9508200
##
## Number of Observations: 306
## Number of Groups: 34
## id = pdLogChol(list(g0 ~ 1,g1 ~ 1))
## Variance StdDev Corr
## g0 0.04125857 0.2031221 g0
## g1 20.16965946 4.4910644 0.999
## Residual 19.20564640 4.3824247
Plotting the predicted trajectories
#obtaining predicted scores for individuals
cortisol_long$pred_linear <- predict(cort_linear)
#obtaining predicted scores for prototype
cortisol_long$proto_linear <- predict(cort_linear, level=0)
#plotting predicted trajectories
#intraindividual change trajetories
cortisol_long %>%
ggplot( aes(x = time, y = pred_linear, group = id)) +
geom_line(color="black") +
geom_line(aes(x = time, y = proto_linear),
color="red", linewidth = 1.5) +
xlab("Time") +
ylab("Cortisol") + ylim(0,30) +
scale_x_continuous(breaks=seq(0,8, by = 1)) +
theme_minimal()Note that the model has some convergence issues, and the solution has hit a parameter boundary. In this model, and some of the models that follow, the correlation between the random effects in intercept and slope is questionable.
Quadratic Growth Model
#creating the quadratic time variable
cortisol_long$timesq <- cortisol_long$timescaled^2
cort_quad <- lmer(cort ~ 1 + timescaled + timesq +
(1 + timescaled + timesq | id),
data = cortisol_long,
na.action = "na.exclude")
summary(cort_quad)## Linear mixed model fit by REML. t-tests use Satterthwaite's method [
## lmerModLmerTest]
## Formula: cort ~ 1 + timescaled + timesq + (1 + timescaled + timesq | id)
## Data: cortisol_long
##
## REML criterion at convergence: 1649.9
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -2.24859 -0.66952 0.03488 0.52129 2.55586
##
## Random effects:
## Groups Name Variance Std.Dev. Corr
## id (Intercept) 1.308 1.143
## timescaled 28.449 5.334 0.63
## timesq 26.582 5.156 -0.97 -0.44
## Residual 9.353 3.058
## Number of obs: 306, groups: id, 34
##
## Fixed effects:
## Estimate Std. Error df t value Pr(>|t|)
## (Intercept) 3.6009 0.4692 51.9152 7.674 4.22e-10 ***
## timescaled 41.0508 2.1882 62.2810 18.760 < 2e-16 ***
## timesq -30.1696 2.1071 51.2034 -14.318 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Correlation of Fixed Effects:
## (Intr) tmscld
## timescaled -0.552
## timesq 0.369 -0.870
cort_quad <- nlme(cort ~ g0 + g1*timescaled + g2*timesq,
fixed = g0 + g1 + g2 ~ 1,
random = g0 + g1 + g2 ~ 1,
group = ~id,
start = c(g0=3.6, g1=41.1, g2=-30.2),
data = cortisol_long,
na.action = "na.exclude")## Warning in nlme.formula(cort ~ g0 + g1 * timescaled + g2 * timesq, fixed = g0 +
## : Iteration 1, LME step: nlminb() did not converge (code = 1). Do increase
## 'msMaxIter'!
## Nonlinear mixed-effects model fit by maximum likelihood
## Model: cort ~ g0 + g1 * timescaled + g2 * timesq
## Data: cortisol_long
## AIC BIC logLik
## 1675.114 1712.35 -827.5571
##
## Random effects:
## Formula: list(g0 ~ 1, g1 ~ 1, g2 ~ 1)
## Level: id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## g0 1.116417 g0 g1
## g1 5.246087 0.627
## g2 5.033705 -0.964 -0.436
## Residual 3.051628
##
## Fixed effects: g0 + g1 + g2 ~ 1
## Value Std.Error DF t-value p-value
## g0 3.60086 0.4687746 270 7.681422 0
## g1 41.05077 2.1888421 270 18.754561 0
## g2 -30.16959 2.1050257 270 -14.332174 0
## Correlation:
## g0 g1
## g1 -0.560
## g2 0.381 -0.873
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -2.24520649 -0.67204216 0.03085329 0.53148561 2.57505404
##
## Number of Observations: 306
## Number of Groups: 34
## id = pdLogChol(list(g0 ~ 1,g1 ~ 1,g2 ~ 1))
## Variance StdDev Corr
## g0 1.246386 1.116417 g0 g1
## g1 27.521432 5.246087 0.627
## g2 25.338182 5.033705 -0.964 -0.436
## Residual 9.312435 3.051628
Plotting the predicted trajectories
#obtaining predicted scores for individuals
cortisol_long$pred_quad <- predict(cort_quad)
#obtaining predicted scores for prototype
cortisol_long$proto_quad <- predict(cort_quad, level=0)
#plotting predicted trajectories
#intraindividual change trajetories
cortisol_long %>%
ggplot(aes(x = time, y = pred_quad, group = id)) +
geom_line(color="black") +
geom_line(aes(x = time, y = proto_quad),
color = "red", linewidth = 1.5) +
xlab("Time") +
ylab("Cortisol") + ylim(0,30) +
scale_x_continuous(breaks=seq(0,8, by = 1)) +
theme_minimal()Latent Basis Growth Model
#creating time-dummy variables
cortisol_long$time0 <- ifelse(cortisol_long$time == 0, 1, 0)
cortisol_long$time1 <- ifelse(cortisol_long$time == 1, 1, 0)
cortisol_long$time2 <- ifelse(cortisol_long$time == 2, 1, 0)
cortisol_long$time3 <- ifelse(cortisol_long$time == 3, 1, 0)
cortisol_long$time4 <- ifelse(cortisol_long$time == 4, 1, 0)
cortisol_long$time5 <- ifelse(cortisol_long$time == 5, 1, 0)
cortisol_long$time6 <- ifelse(cortisol_long$time == 6, 1, 0)
cortisol_long$time7 <- ifelse(cortisol_long$time == 7, 1, 0)
cortisol_long$time8 <- ifelse(cortisol_long$time == 8, 1, 0)
#latent basis model
cort_latentb <- nlme(cort ~ time0*(g0 + g1*0) +
time1*(g0 + g1*A_1) +
time2*(g0 + g1*A_2) +
time3*(g0 + g1*A_3) +
time4*(g0 + g1*A_4) +
time5*(g0 + g1*A_5) +
time6*(g0 + g1*A_6) +
time7*(g0 + g1*A_7) +
time8*(g0 + g1*1),
fixed = g0 + g1 + A_1 + A_2 + A_3 + A_4 + A_5 + A_6 + A_7 ~ 1,
random = g0 + g1 ~ 1,
groups =~ id,
start = c(g0=5.5, g1=11.5,
A_1=.3, A_2=.4, A_3=.5, A_4=.6,
A_5=.7, A_6=.8, A_7=.9),
data = cortisol_long,
na.action = na.exclude)
summary(cort_latentb)## Nonlinear mixed-effects model fit by maximum likelihood
## Model: cort ~ time0 * (g0 + g1 * 0) + time1 * (g0 + g1 * A_1) + time2 * (g0 + g1 * A_2) + time3 * (g0 + g1 * A_3) + time4 * (g0 + g1 * A_4) + time5 * (g0 + g1 * A_5) + time6 * (g0 + g1 * A_6) + time7 * (g0 + g1 * A_7) + time8 * (g0 + g1 * 1)
## Data: cortisol_long
## AIC BIC logLik
## 1569.164 1617.571 -771.5821
##
## Random effects:
## Formula: list(g0 ~ 1, g1 ~ 1)
## Level: id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## g0 1.698578 g0
## g1 2.735103 0.001
## Residual 2.529224
##
## Fixed effects: g0 + g1 + A_1 + A_2 + A_3 + A_4 + A_5 + A_6 + A_7 ~ 1
## Value Std.Error DF t-value p-value
## g0 5.342011 0.5207451 264 10.258399 0.0000
## g1 10.447225 0.7660285 264 13.638168 0.0000
## A_1 -0.041829 0.0582607 264 -0.717961 0.4734
## A_2 0.522586 0.0500425 264 10.442847 0.0000
## A_3 1.118113 0.0611387 264 18.288132 0.0000
## A_4 1.336652 0.0689221 264 19.393645 0.0000
## A_5 1.081029 0.0599665 264 18.027218 0.0000
## A_6 0.951968 0.0562971 264 16.909722 0.0000
## A_7 1.008691 0.0578269 264 17.443266 0.0000
## Correlation:
## g0 g1 A_1 A_2 A_3 A_4 A_5 A_6
## g1 -0.455
## A_1 -0.581 0.404
## A_2 -0.314 -0.023 0.262
## A_3 0.056 -0.446 -0.066 0.287
## A_4 0.151 -0.534 -0.150 0.261 0.589
## A_5 0.037 -0.427 -0.050 0.291 0.540 0.577
## A_6 -0.034 -0.355 0.013 0.306 0.501 0.526 0.495
## A_7 -0.002 -0.388 -0.015 0.300 0.519 0.550 0.512 0.481
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -2.4004317 -0.6084323 -0.1077279 0.5806294 2.8252173
##
## Number of Observations: 306
## Number of Groups: 34
## id = pdLogChol(list(g0 ~ 1,g1 ~ 1))
## Variance StdDev Corr
## g0 2.885168 1.698578 g0
## g1 7.480787 2.735103 0.001
## Residual 6.396974 2.529224
Plotting the predicted trajectories
#obtaining predicted scores for individuals
cortisol_long$pred_latentb <- predict(cort_latentb)
#obtaining predicted scores for prototype
cortisol_long$proto_latentb <- predict(cort_latentb, level=0)
#plotting predicted trajectories
#intraindividual change trajetories
cortisol_long %>%
ggplot(aes(x = time, y = pred_latentb, group = id)) +
geom_line(color="black") +
geom_line(aes(x = time, y = proto_latentb),
color = "red", linewidth = 1.5) +
xlab("Time") +
ylab("Cortisol") + ylim(0,30) +
scale_x_continuous(breaks=seq(0,8, by = 1)) +
theme_minimal()Exponential Growth Model
#exponential model
cort_expo <- nlme(cort ~ g0 + g1*(exp(-1*alpha*timescaled)),
fixed = g0 + g1 + alpha ~ 1,
random = g0 + g1 ~ 1,
group = ~id,
start = c(g0=17, g1=-14, alpha=0.5),
data = cortisol_long,
na.action = "na.exclude")## Warning in nlme.formula(cort ~ g0 + g1 * (exp(-1 * alpha * timescaled)), :
## Iteration 4, LME step: nlminb() did not converge (code = 1). Do increase
## 'msMaxIter'!
## Nonlinear mixed-effects model fit by maximum likelihood
## Model: cort ~ g0 + g1 * (exp(-1 * alpha * timescaled))
## Data: cortisol_long
## AIC BIC logLik
## 1730.236 1756.302 -858.1182
##
## Random effects:
## Formula: list(g0 ~ 1, g1 ~ 1)
## Level: id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## g0 3.672342 g0
## g1 3.988619 -0.999
## Residual 3.594282
##
## Fixed effects: g0 + g1 + alpha ~ 1
## Value Std.Error DF t-value p-value
## g0 17.203250 0.7689413 270 22.372645 0
## g1 -13.735218 0.9554844 270 -14.375136 0
## alpha 4.108994 0.4851947 270 8.468752 0
## Correlation:
## g0 g1
## g1 -0.788
## alpha -0.442 0.077
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -2.06657120 -0.69580376 -0.03326631 0.60565773 2.73037715
##
## Number of Observations: 306
## Number of Groups: 34
## id = pdLogChol(list(g0 ~ 1,g1 ~ 1))
## Variance StdDev Corr
## g0 13.48610 3.672342 g0
## g1 15.90908 3.988619 -0.999
## Residual 12.91886 3.594282
Plotting the predicted trajectories
#obtaining predicted scores for individuals
cortisol_long$pred_expo <- predict(cort_expo)
#obtaining predicted scores for prototype
cortisol_long$proto_expo <- predict(cort_expo, level=0)
#plotting predicted trajectories
#intraindividual change trajetories
cortisol_long %>%
ggplot(aes(x = time, y = pred_expo, group = id)) +
geom_line(color="black") +
geom_line(aes(x = time, y = proto_expo),
color="red", linewidth = 1.5) +
xlab("Time") +
ylab("Cortisol") + ylim(0,30) +
scale_x_continuous(breaks=seq(0,8, by = 1)) +
theme_minimal()Multiphase Growth Model
#multiphase model
cort_multi <- nlme(cort ~ time0*(g0 + g1*0 + g2*0) +
time1*(g0 + g1*0 + g2*0) +
time2*(g0 + g1*A_2 + g2*0) +
time3*(g0 + g1*A_3 + g2*0) +
time4*(g0 + g1*1 + g2*0) +
time5*(g0 + g1*1 + g2*A_5) +
time6*(g0 + g1*1 + g2*A_6) +
time7*(g0 + g1*1 + g2*A_7) +
time8*(g0 + g1*1 + g2*1),
fixed = g0 + g1 + g2 + A_2 + A_3 + A_5 + A_6 + A_7 ~ 1,
random = g0 + g1 + g2 ~ 1,
groups =~ id,
start = c(g0=15, g1=10, g2=-4,
A_2=.4, A_3=.5, A_5=.7,
A_6=.8, A_7=.9),
data = cortisol_long,
na.action = na.exclude)
summary(cort_multi)## Nonlinear mixed-effects model fit by maximum likelihood
## Model: cort ~ time0 * (g0 + g1 * 0 + g2 * 0) + time1 * (g0 + g1 * 0 + g2 * 0) + time2 * (g0 + g1 * A_2 + g2 * 0) + time3 * (g0 + g1 * A_3 + g2 * 0) + time4 * (g0 + g1 * 1 + g2 * 0) + time5 * (g0 + g1 * 1 + g2 * A_5) + time6 * (g0 + g1 * 1 + g2 * A_6) + time7 * (g0 + g1 * 1 + g2 * A_7) + time8 * (g0 + g1 * 1 + g2 * 1)
## Data: cortisol_long
## AIC BIC logLik
## 1412.881 1468.735 -691.4407
##
## Random effects:
## Formula: list(g0 ~ 1, g1 ~ 1, g2 ~ 1)
## Level: id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## g0 2.161867 g0 g1
## g1 3.580496 -0.315
## g2 4.241375 -0.041 -0.286
## Residual 1.535205
##
## Fixed effects: g0 + g1 + g2 + A_2 + A_3 + A_5 + A_6 + A_7 ~ 1
## Value Std.Error DF t-value p-value
## g0 5.118082 0.4201167 265 12.18252 0
## g1 14.248891 0.6998856 265 20.35889 0
## g2 -3.797260 0.8089645 265 -4.69398 0
## A_2 0.410745 0.0210662 265 19.49783 0
## A_3 0.860230 0.0238885 265 36.01023 0
## A_5 0.715784 0.0598000 265 11.96963 0
## A_6 1.046663 0.0683974 265 15.30267 0
## A_7 1.042029 0.0682431 265 15.26937 0
## Correlation:
## g0 g1 g2 A_2 A_3 A_5 A_6
## g1 -0.372
## g2 -0.033 -0.352
## A_2 -0.158 -0.032 0.111
## A_3 -0.033 -0.214 0.205 0.250
## A_5 0.000 0.059 0.073 -0.056 -0.104
## A_6 0.000 -0.008 0.165 0.007 0.014 0.393
## A_7 0.000 -0.007 0.164 0.006 0.012 0.392 0.504
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -3.40843238 -0.50779682 -0.01780372 0.49095891 3.70249846
##
## Number of Observations: 306
## Number of Groups: 34
## id = pdLogChol(list(g0 ~ 1,g1 ~ 1,g2 ~ 1))
## Variance StdDev Corr
## g0 4.673669 2.161867 g0 g1
## g1 12.819948 3.580496 -0.315
## g2 17.989264 4.241375 -0.041 -0.286
## Residual 2.356854 1.535205
Plotting the predicted trajectories
#obtaining predicted scores for individuals
cortisol_long$pred_multi <- predict(cort_multi)
#obtaining predicted scores for prototype
cortisol_long$proto_multi <- predict(cort_multi, level=0)
#plotting predicted trajectories
#intraindividual change trajetories
cortisol_long %>%
ggplot(aes(x = time, y = pred_multi, group = id)) +
geom_line(color="black") +
geom_line(aes(x = time, y = proto_multi),
color="red", linewidth = 1.5) +
xlab("Time") +
ylab("Cortisol") + ylim(0,30) +
scale_x_continuous(breaks=seq(0,8, by = 1)) +
theme_minimal()These were the 5 models covered in Ram & Grimm (2007). In sharing the data, we encourage the community to discover even more interesting models.
Thanks for playing!
Nilam & Kevin