Multivariate Multilevel Model for Longitudinal Coupling (APIM type)
Overview
The typical multilevel model used to examine intraindividual covariation requires that one of the two variables be treated as the outcome and the other as a predictor. However, there are cases where we would like both variables to be both predictors and outcomes. One case is where we have distinguishable dyads. Other cases include examination of bivariate coupling.
In this tutorial we consider the multivariate multilevel modeling and how it is set up to examine intraindividual coupling. Specifically, we use a dummy-variable “trick” to model relations of two outcome variables simultaneously.
Introduction to the Research Questions, Model, and Data
The Research Questions
For this example, we use the The AMIB Data. Here, our “dyadic data”, rather than being being about two individuals nested in dyads, are about two variables nested within an individual.
Our interest in this example is to model the intraindividual coupling of positive and negative emotion.
We are going to address:
How do the emotions carry-over from day to day? This is a construct called emotional inertia in the literature.
How does positive emotion influence negative emotions? (protective factor) and, vice versa,
How does negative emotion influence positive emotions? (risk factor)
Basically, this is a cross-lag panel model (or APIM-type model) - except that we are working with intensive longitudinal data, which provides opportunity to prioritize examination of within-person (rather than between-person) associations.
The Multivariate Modeling Enterprise
The basic multilevel model is designed as a model with a univariate outcome. So, we “trick” the model/program into thinking that two (or more) variables are one variable. So clever!
The AMIB Data
We use The AMIB Data, a multiple time-scale data set that has been shared for teaching purposes. The data include person-level dispositions, daily diary assessments, and ecological momentary assessments that were obtained after everyday social interactions (event-contingent sampling).
The data are shared with intent to facilitate learning about analysis of intensive longitudinal data (from experience sampling, daily diary, or EMA designs). The data are posted only for teaching purposes and should not be used for research.
Use requires citation and acknowledgement of both this website and the following paper:
We are working from a standard long file to obtain a “double-entry” stacked file with lagged variables, and dummy variables that will be used to “turn on” and “turn off” the double records and invoke parameter estimation for each variable. In the dyadic example, the data were already prepared. Here, we walk through the data preparation process.
#set filepath for data file
filepath <- "https://raw.githubusercontent.com/The-Change-Lab/collaborations/refs/heads/main/AMIB/AMIB_daily.csv"
#read in the .csv file using the url() function
daily <- read.csv(file=url(filepath), header=TRUE)
#subsetting data to variables of interest
daily1 <- daily %>%
select(id, day, posaff, negaff)
#examine first few rows of the data set
names(daily1)## [1] "id" "day" "posaff" "negaff"
Plotting & Restructuring the Data
Plotting the data
Before we begin running our models, it is always a good idea to look at our data.
We look at our two outcomes: posaff and
negaff.
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 1441 2.45 1.04 2.2 2.34 1.04 1 6.9 5.9 0.96 0.77 0.03
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 1441 4.12 1.1 4.2 4.15 1.19 1 7 6 -0.25 -0.33 0.03
#histogram
daily1 %>%
ggplot(aes(x=negaff)) +
geom_histogram(fill="red", color="black") +
labs(x = "Negative Affect")## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
daily1 %>%
ggplot(aes(x=posaff)) +
geom_histogram(fill="royalblue2", color="black") +
labs(x = "Positive Affect")## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Intraindividual plots
#setting up for changing ids
i <- 106
#plot
daily1 %>%
filter(id <= i) %>%
ggplot(aes(x=day, group=id), legend=FALSE) +
#geom_rect(mapping=aes(xmin=day-.5, xmax=day+.5, ymin=0, ymax=10, fill=wrkstrscw), alpha=0.6) +
geom_point(aes(x=day,y = posaff), color="blue", shape=17, size=2) +
geom_line(aes(x=day,y = posaff), color="blue", lty=1, linewidth=1) +
geom_point(aes(x=day,y = negaff), color="red", shape=17, size=2) +
geom_line(aes(x=day,y = negaff), color="red", lty=1, linewidth=1) +
xlab("Day") +
ylab("Affect (Positive = Blue, Negative = Red)") + ylim(1,7) +
scale_x_continuous(breaks=seq(0,8,by=1)) +
facet_wrap( ~ id)Restructuring the Data
Splitting Predictors
First, we split our predictor variables into time-varying and
time-invariant components. We calculate the iMeans as usual using
dplyr functions.
#Calculating iMeans
daily.imeans <- daily1 %>%
dplyr::group_by(id) %>%
dplyr::summarise(imean.posaff= mean(posaff, na.rm=TRUE),
imean.negaff=mean(negaff, na.rm=TRUE))
#Calculating sample-centered versions
#*Note that this is done in a person-level data file*
daily.imeans <- daily.imeans %>%
mutate(imean.posaff.c = scale(imean.posaff, center=TRUE, scale=FALSE),
imean.negaff.c = scale(imean.negaff, center=TRUE, scale=FALSE))
describe(daily.imeans)## vars n mean sd median trimmed mad min max
## id 1 190 318.29 130.44 321.50 318.99 151.23 101.00 532.00
## imean.posaff 2 190 4.10 0.73 4.10 4.10 0.63 2.22 6.04
## imean.negaff 3 190 2.48 0.73 2.41 2.43 0.72 1.11 5.09
## imean.posaff.c 4 190 0.00 0.73 0.00 0.00 0.63 -1.88 1.94
## imean.negaff.c 5 190 0.00 0.73 -0.07 -0.05 0.72 -1.37 2.61
## range skew kurtosis se
## id 431.00 -0.04 -1.09 9.46
## imean.posaff 3.81 0.04 0.08 0.05
## imean.negaff 3.98 0.68 0.45 0.05
## imean.posaff.c 3.81 0.04 0.08 0.05
## imean.negaff.c 3.98 0.68 0.45 0.05
Lagged variables
Second, we make some lag variables. These will serve as predictors.
#make a duplicate data set
daily1.new <- daily1
#make lag variables (original)
daily1.new <- daily1.new %>%
dplyr::group_by(id) %>%
mutate(posaff.lag1 = lag(posaff, 1),
negaff.lag1 = lag(negaff, 1),
#make lag variables (person-centered)
negaff.state.lag1 = lag(negaff.state, 1),
posaff.state.lag1 = lag(posaff.state, 1))
#Checking structure
head(daily1.new, 12)## # A tibble: 12 × 14
## # Groups: id [2]
## id day posaff negaff imean.posaff imean.negaff imean.posaff.c[,1]
## <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 101 0 3.9 3 4.56 1.5 0.460
## 2 101 1 3.8 2.3 4.56 1.5 0.460
## 3 101 2 5.1 1 4.56 1.5 0.460
## 4 101 3 5.6 1.3 4.56 1.5 0.460
## 5 101 4 4.3 1.1 4.56 1.5 0.460
## 6 101 5 3.9 1 4.56 1.5 0.460
## 7 101 6 5.1 1.2 4.56 1.5 0.460
## 8 101 7 4.8 1.1 4.56 1.5 0.460
## 9 102 0 6.3 1.4 5.18 2.22 1.07
## 10 102 1 7 1.6 5.18 2.22 1.07
## 11 102 2 6.1 2.6 5.18 2.22 1.07
## 12 102 3 6.2 2.8 5.18 2.22 1.07
## # ℹ 7 more variables: imean.negaff.c <dbl[,1]>, posaff.state <dbl>,
## # negaff.state <dbl>, posaff.lag1 <dbl>, negaff.lag1 <dbl>,
## # negaff.state.lag1 <dbl>, posaff.state.lag1 <dbl>
Let’s plot the updated data - within-person and between-variables all in one.
For fun, we see if we can also add between-person differences to the plots?
#Person-centered
#setting up for changing ids
i <- 106
#plot
daily1.new %>%
filter(id <= i) %>%
ggplot(aes(x=day, group=id), legend=FALSE) +
geom_rect(mapping=aes(xmin=0, xmax=8, ymin=-3, ymax=0, fill=imean.negaff.c), alpha=0.6) +
geom_rect(mapping=aes(xmin=0, xmax=8, ymin=0, ymax=+3, fill=imean.posaff.c), alpha=0.6) +
guides(fill=FALSE) + #turining off the legend
geom_hline(aes(yintercept=0)) +
geom_point(aes(x=day,y = posaff.state), color="blue", shape=17, size=2) +
geom_line(aes(x=day,y = posaff.state), color="blue", lty=1, linewidth=1) +
geom_point(aes(x=day,y = negaff.state), color="red", shape=17, size=2) +
geom_line(aes(x=day,y = negaff.state), color="red", lty=1, linewidth=1) +
geom_point(aes(x=day,y = posaff.state.lag1), color="blue", shape=17, size=2) +
geom_line(aes(x=day,y = posaff.state.lag1), color="blue", lty=2, linewidth=1) +
geom_point(aes(x=day,y = negaff.state.lag1), color="red", shape=17, size=2) +
geom_line(aes(x=day,y = negaff.state.lag1), color="red", lty=2, linewidth=1) +
xlab("Day") +
ylab("Affect (Positive = Blue, Negative = Red)") + #ylim(1,7) +
scale_x_continuous(breaks=seq(0,8,by=1)) +
facet_wrap( ~ id)The colors and labeling are not great - but the idea is intact. Cool!
NOTICE THAT THERE IS A PROBLEM WITH ID#103.
Missing Data are not being treated correctly! We need to make some more adjustments when we lag the variables. Be careful! (In this case the problem could be corrected by using a full dataset with 190 x 8 = 1520 rows)
Making Double-Entry Data
Ok - now we have all our variables in place and can shape the data
into a double-entry set-up. We make use of the pivot_longer
function in the tidyr package.
## c("id", "day", "posaff", "negaff", "imean.posaff", "imean.negaff",
## "imean.posaff.c", "imean.negaff.c", "posaff.state", "negaff.state",
## "posaff.lag1", "negaff.lag1", "negaff.state.lag1", "posaff.state.lag1"
## )
daily2 <- daily1.new %>%
pivot_longer(cols = c("posaff", "negaff"),
names_to = "variable",
values_to = "affect",
values_drop_na = FALSE)
#look at updated data set
head(daily2, 12)## # A tibble: 12 × 14
## # Groups: id [1]
## id day imean.posaff imean.negaff imean.posaff.c[,1] imean.negaff.c[,1]
## <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 101 0 4.56 1.5 0.460 -0.978
## 2 101 0 4.56 1.5 0.460 -0.978
## 3 101 1 4.56 1.5 0.460 -0.978
## 4 101 1 4.56 1.5 0.460 -0.978
## 5 101 2 4.56 1.5 0.460 -0.978
## 6 101 2 4.56 1.5 0.460 -0.978
## 7 101 3 4.56 1.5 0.460 -0.978
## 8 101 3 4.56 1.5 0.460 -0.978
## 9 101 4 4.56 1.5 0.460 -0.978
## 10 101 4 4.56 1.5 0.460 -0.978
## 11 101 5 4.56 1.5 0.460 -0.978
## 12 101 5 4.56 1.5 0.460 -0.978
## # ℹ 8 more variables: posaff.state <dbl>, negaff.state <dbl>,
## # posaff.lag1 <dbl>, negaff.lag1 <dbl>, negaff.state.lag1 <dbl>,
## # posaff.state.lag1 <dbl>, variable <chr>, affect <dbl>
#adding two dummy indicators for the two variables of interest
daily2 <- daily2 %>%
mutate(pos = ifelse(variable=="posaff", 1, 0),
neg = ifelse(variable=="negaff", 1, 0),
#adding another variable indicator
var = as.numeric(factor(variable,
levels = c("posaff", "negaff"),
labels = c(1, 2))))
#look at updated data set
head(daily2, 12)## # A tibble: 12 × 17
## # Groups: id [1]
## id day imean.posaff imean.negaff imean.posaff.c[,1] imean.negaff.c[,1]
## <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 101 0 4.56 1.5 0.460 -0.978
## 2 101 0 4.56 1.5 0.460 -0.978
## 3 101 1 4.56 1.5 0.460 -0.978
## 4 101 1 4.56 1.5 0.460 -0.978
## 5 101 2 4.56 1.5 0.460 -0.978
## 6 101 2 4.56 1.5 0.460 -0.978
## 7 101 3 4.56 1.5 0.460 -0.978
## 8 101 3 4.56 1.5 0.460 -0.978
## 9 101 4 4.56 1.5 0.460 -0.978
## 10 101 4 4.56 1.5 0.460 -0.978
## 11 101 5 4.56 1.5 0.460 -0.978
## 12 101 5 4.56 1.5 0.460 -0.978
## # ℹ 11 more variables: posaff.state <dbl>, negaff.state <dbl>,
## # posaff.lag1 <dbl>, negaff.lag1 <dbl>, negaff.state.lag1 <dbl>,
## # posaff.state.lag1 <dbl>, variable <chr>, affect <dbl>, pos <dbl>,
## # neg <dbl>, var <dbl>
Yes! Now the data are in the double-entry format we need for modeling.
The Multivariate Multilevel Model.
We are now ready to start running our models!
Unconditional means model
We start with the simple “unconditional means” model.
\[affect_{it} = \beta_{A0i}dummyA_{it} + \beta_{B0i}dummyB_{it} + e_{it}\] (Notice that there is no intercept in the model). \[\beta_{A0i} = \gamma_{A00} + u_{A0i}\] \[\beta_{B0i} = \gamma_{B00} + u_{B0i}\]
with \[e_{it} \sim \mathcal{N}(0,\mathbf{R})\], and \[\mathbf{U_{i}} \sim \mathcal{N}(0,\mathbf{G})\]. where \[\mathbf{R} = \mathbf{I} \left[\begin{array} {r} \sigma^2_{e} \end{array}\right]\] and \[\mathbf{G} = \left[\begin{array} {rr} \sigma^{2}_{uA0} & \sigma_{uA0uB0} \\ \sigma_{uA0uB0} & \sigma^{2}_{uB0} \end{array}\right]\]
Coding the model. We put a “-1” where we would usually put a “1” for the intercept in order to remove the default.
model1.fit <- lme(fixed = affect ~ -1 + pos + neg,
random = ~ -1 + pos + neg|id,
data=daily2,
na.action = na.exclude)
summary(model1.fit)## Linear mixed-effects model fit by REML
## Data: daily2
## AIC BIC logLik
## 7856.977 7892.77 -3922.488
##
## Random effects:
## Formula: ~-1 + pos + neg | id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## pos 0.6638692 pos
## neg 0.6482248 -0.493
## Residual 0.8478374
##
## Fixed effects: affect ~ -1 + pos + neg
## Value Std.Error DF t-value p-value
## pos 4.105112 0.05333648 2691 76.96630 0
## neg 2.463913 0.05230692 2691 47.10492 0
## Correlation:
## pos
## neg -0.403
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -3.75033477 -0.59659553 -0.03672567 0.55217304 3.83689841
##
## Number of Observations: 2882
## Number of Groups: 190
Fixing Residual Structure
In the previous model we only have one time-varying residual - which is implicitly assumed to apply to both positive affect and negative affect. We need to change that. We want the residuals to be structured as
\[\mathbf{R} = \left[\begin{array} {ll} \sigma^2_{eA} & \sigma_{eAeB} \\ \sigma_{eAeB} & \sigma^2_{eB} \end{array}\right]\]
We add two statements to do this.
#adding the proper residual structure
model2.fit <- lme(fixed = affect ~ -1 + pos + neg,
random = ~ -1 + pos + neg | id,
weights=varIdent(form=~1 | var), #to get different for each var
corr=corSymm(form=~1 | id/day), #to allow them to correlate
data=daily2,
na.action = na.exclude)
summary(model2.fit)## Linear mixed-effects model fit by REML
## Data: daily2
## AIC BIC logLik
## 7428.507 7476.231 -3706.254
##
## Random effects:
## Formula: ~-1 + pos + neg | id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## pos 0.6561721 pos
## neg 0.6534680 -0.374
## Residual 0.8806941
##
## Correlation Structure: General
## Formula: ~1 | id/day
## Parameter estimate(s):
## Correlation:
## 1
## 2 -0.536
## Variance function:
## Structure: Different standard deviations per stratum
## Formula: ~1 | var
## Parameter estimates:
## 1 2
## 1.0000000 0.9243557
## Fixed effects: affect ~ -1 + pos + neg
## Value Std.Error DF t-value p-value
## pos 4.109555 0.05322472 2691 77.21140 0
## neg 2.463665 0.05228332 2691 47.12144 0
## Correlation:
## pos
## neg -0.404
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -3.84620289 -0.59782678 -0.04443837 0.54610645 3.94722848
##
## Number of Observations: 2882
## Number of Groups: 190
Ok, that accommodates the possibility of different variances, and for unknown 3rd variable “common cause” (via the correlated residuals).
Auto-Regressions
We add the auto-regression time-varying predictors
posaff.state.lag1 and negaff.state.lag1.
Our equation becomes \[affect_{it} = \\ \beta_{A0i}dummyA_{it} + \beta_{A1i}dummyA_{it}posaff_{it-1} + e_{Ait} + \\ \beta_{B0i}dummyB_{it} + \beta_{B1i}dummyB_{it}negaff_{it-1} + e_{Bit}\] \[\beta_{A0i} = \gamma_{A00} + u_{A0i}\]
\[\beta_{B0i} = \gamma_{B00} + u_{B0i}\] and \[\beta_{A1i} = \gamma_{A10}\] \[\beta_{B1i} = \gamma_{B10}\] (Notice that these autoregression do not yet have random effects on them; no \(u_{A1i}\) or \(u_{B1i}\))
with \[\mathbf{R} = \left[\begin{array} {ll} \sigma^2_{eA} & \sigma_{eAeB} \\ \sigma_{eAeB} & \sigma^2_{eB} \end{array}\right]\] and \[\mathbf{G} = \left[\begin{array} {rr} \sigma^{2}_{uA0} & \sigma_{uA0uB0} \\ \sigma_{uA0uB0} & \sigma^{2}_{uB0} \end{array}\right]\]
Note the use of the : for the “interaction”
terms. We do this becasue we need control of what main effects
and interactions are being added. We are overriding the defaults with
out “trick”.
#adding auto-regression predictor
model3.fit <- lme(fixed = affect ~ -1 +
pos + pos:posaff.state.lag1 +
neg + neg:negaff.state.lag1,
random = ~ -1 + pos + neg | id,
weights=varIdent(form = ~1 | var),
corr=corSymm(form = ~1 | id/day),
data=daily2,
na.action = na.exclude)
summary(model3.fit)## Linear mixed-effects model fit by REML
## Data: daily2
## AIC BIC logLik
## 6319.05 6377.235 -3149.525
##
## Random effects:
## Formula: ~-1 + pos + neg | id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## pos 0.6808490 pos
## neg 0.6594594 -0.313
## Residual 0.8592680
##
## Correlation Structure: General
## Formula: ~1 | id/day
## Parameter estimate(s):
## Correlation:
## 1
## 2 -0.578
## Variance function:
## Structure: Different standard deviations per stratum
## Formula: ~1 | var
## Parameter estimates:
## 1 2
## 1.0000000 0.9249473
## Fixed effects: affect ~ -1 + pos + pos:posaff.state.lag1 + neg + neg:negaff.state.lag1
## Value Std.Error DF t-value p-value
## pos 4.081514 0.05567805 2301 73.30562 0
## neg 2.403529 0.05346759 2301 44.95301 0
## pos:posaff.state.lag1 0.149636 0.02554709 2301 5.85727 0
## neg:negaff.state.lag1 0.138607 0.02572411 2301 5.38821 0
## Correlation:
## pos neg ps:..1
## neg -0.362
## pos:posaff.state.lag1 -0.008 -0.004
## neg:negaff.state.lag1 -0.003 -0.012 0.307
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -3.80189130 -0.56926009 -0.04598771 0.54317530 4.33285365
##
## Number of Observations: 2490
## Number of Groups: 186
Cross Regressions
We add the cross-regression time-varying predictors
negaff.state.lag1 and posaff.state.lag1.
Our equation becomes \[affect_{it} = \\ \beta_{A0i}dummyA_{it} + \beta_{A1i}dummyA_{it}posaff_{it-1} + \beta_{A2i}dummyA_{it}negaff_{it-1}+ e_{Ait} + \\ \beta_{B0i}dummyB_{it} + \beta_{B1i}dummyB_{it}negaff_{it-1} + \beta_{B2i}dummyB_{it}posaff_{it-1} + e_{Bit}\] \[\beta_{A0i} = \gamma_{A00} + u_{A0i}\] \[\beta_{B0i} = \gamma_{B00} + u_{B0i}\] and \[\beta_{A1i} = \gamma_{A10}\] \[\beta_{B1i} = \gamma_{B10}\] and \[\beta_{A2i} = \gamma_{A20}\] \[\beta_{B2i} = \gamma_{B20}\]
#adding cross-regression predictors
model4.fit <- lme(fixed = affect ~ -1 +
pos + pos:posaff.state.lag1 + pos:negaff.state.lag1 +
neg + neg:negaff.state.lag1 + neg:posaff.state.lag1,
random = ~ -1 + pos + neg | id,
weights=varIdent(form = ~1 | var),
corr=corCompSymm(form = ~1 | id/day),
data=daily2,
na.action = na.exclude)
summary(model4.fit)## Linear mixed-effects model fit by REML
## Data: daily2
## AIC BIC logLik
## 6322.196 6392.007 -3149.098
##
## Random effects:
## Formula: ~-1 + pos + neg | id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## pos 0.6813933 pos
## neg 0.6591403 -0.311
## Residual 0.8563014
##
## Correlation Structure: Compound symmetry
## Formula: ~1 | id/day
## Parameter estimate(s):
## Rho
## -0.5781921
## Variance function:
## Structure: Different standard deviations per stratum
## Formula: ~1 | var
## Parameter estimates:
## 1 2
## 1.0000000 0.9274466
## Fixed effects: affect ~ -1 + pos + pos:posaff.state.lag1 + pos:negaff.state.lag1 + neg + neg:negaff.state.lag1 + neg:posaff.state.lag1
## Value Std.Error DF t-value p-value
## pos 4.078292 0.05569204 2299 73.22936 0.0000
## neg 2.404015 0.05345052 2299 44.97646 0.0000
## pos:posaff.state.lag1 0.177320 0.03504720 2299 5.05945 0.0000
## pos:negaff.state.lag1 0.107901 0.03814830 2299 2.82847 0.0047
## negaff.state.lag1:neg 0.102558 0.03538424 2299 2.89840 0.0038
## posaff.state.lag1:neg 0.022531 0.03250774 2299 0.69311 0.4883
## Correlation:
## pos neg ps:p..1 ps:n..1 ng..1:
## neg -0.361
## pos:posaff.state.lag1 -0.020 0.011
## pos:negaff.state.lag1 -0.023 0.013 0.532
## negaff.state.lag1:neg 0.013 -0.022 -0.307 -0.578
## posaff.state.lag1:neg 0.012 -0.019 -0.578 -0.307 0.532
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -3.74585606 -0.56948320 -0.05883887 0.54773629 4.14314587
##
## Number of Observations: 2490
## Number of Groups: 186
Person-Level Predictors
We add the person-level time-invariant predictors
imean.posaff.c and imean.negaff.c.
\[affect_{it} = \beta_{A0i}dummyA_{it} + \beta_{A1i}dummyA_{it}posaff_{it-1} + \beta_{A2i}dummyA_{it}negaff_{it-1}+ e_{Ait} + \\ \beta_{B0i}dummyB_{it} + \beta_{B1i}dummyB_{it}negaff_{it-1} + \beta_{B2i}dummyB_{it}posaff_{it-1} + e_{Bit}\] \[\beta_{A0i} = \gamma_{A00} + \gamma_{A01}imeanposaff_{i} + \gamma_{A02}imeannegaff_{i} + u_{A0i}\] \[\beta_{B0i} = \gamma_{B00} + \gamma_{B01}imeannegaff_{i} + \gamma_{B02}imeanposaff_{i} + u_{B0i}\] and \[\beta_{A1i} = \gamma_{A10}\] \[\beta_{B1i} = \gamma_{B10}\] and \[\beta_{A2i} = \gamma_{A20}\] \[\beta_{B2i} = \gamma_{B20}\]
#adding main effects for time-invariant predictors
model5.fit <- lme(fixed = affect ~ -1 +
pos + pos:imean.posaff.c + pos:imean.negaff.c +
pos:posaff.state.lag1 + pos:negaff.state.lag1 +
neg + neg:imean.negaff.c + neg:imean.posaff.c +
neg:negaff.state.lag1 + neg:posaff.state.lag1,
random = ~ -1 + pos + neg | id,
weights=varIdent(form = ~1 | var),
corr=corSymm(form = ~1 | id/day),
data=daily2,
na.action = na.exclude)
summary(model5.fit)## Linear mixed-effects model fit by REML
## Data: daily2
## AIC BIC logLik
## 5355.654 5448.71 -2661.827
##
## Random effects:
## Formula: ~-1 + pos + neg | id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## pos 1.136189e-05 pos
## neg 2.478979e-05 0
## Residual 8.017601e-01
##
## Correlation Structure: General
## Formula: ~1 | id/day
## Parameter estimate(s):
## Correlation:
## 1
## 2 -0.575
## Variance function:
## Structure: Different standard deviations per stratum
## Formula: ~1 | var
## Parameter estimates:
## 1 2
## 1.0000000 0.9279277
## Fixed effects: affect ~ -1 + pos + pos:imean.posaff.c + pos:imean.negaff.c + pos:posaff.state.lag1 + pos:negaff.state.lag1 + neg + neg:imean.negaff.c + neg:imean.posaff.c + neg:negaff.state.lag1 + neg:posaff.state.lag1
## Value Std.Error DF t-value p-value
## pos 4.066356 0.02278661 2295 178.45372 0.0000
## neg 2.436755 0.02114433 2295 115.24390 0.0000
## pos:imean.posaff.c 1.032514 0.03398790 2295 30.37887 0.0000
## pos:imean.negaff.c 0.033348 0.03487640 2295 0.95617 0.3391
## pos:posaff.state.lag1 0.175271 0.03257307 2295 5.38085 0.0000
## pos:negaff.state.lag1 0.109973 0.03544511 2295 3.10263 0.0019
## imean.negaff.c:neg 1.012772 0.03236277 2295 31.29435 0.0000
## imean.posaff.c:neg 0.015787 0.03153832 2295 0.50055 0.6167
## negaff.state.lag1:neg 0.095372 0.03289050 2295 2.89968 0.0038
## posaff.state.lag1:neg 0.020261 0.03022546 2295 0.67032 0.5027
## Correlation:
## pos neg ps:mn.p. ps:mn.n. ps:p..1 ps:n..1 imn.n.:
## neg -0.575
## pos:imean.posaff.c -0.005 0.003
## pos:imean.negaff.c 0.045 -0.026 0.398
## pos:posaff.state.lag1 -0.044 0.025 0.022 0.009
## pos:negaff.state.lag1 -0.049 0.028 0.011 0.006 0.533
## imean.negaff.c:neg -0.026 0.045 -0.229 -0.575 -0.005 -0.004
## imean.posaff.c:neg 0.003 -0.005 -0.575 -0.229 -0.013 -0.006 0.398
## negaff.state.lag1:neg 0.028 -0.049 -0.006 -0.004 -0.307 -0.575 0.006
## posaff.state.lag1:neg 0.025 -0.044 -0.013 -0.005 -0.575 -0.307 0.009
## imn.p.: ng..1:
## neg
## pos:imean.posaff.c
## pos:imean.negaff.c
## pos:posaff.state.lag1
## pos:negaff.state.lag1
## imean.negaff.c:neg
## imean.posaff.c:neg
## negaff.state.lag1:neg 0.011
## posaff.state.lag1:neg 0.022 0.533
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -4.97737894 -0.58357800 -0.05172056 0.54842112 4.11842550
##
## Number of Observations: 2490
## Number of Groups: 186
Wait a second! - this model has imean.posaff predicting
posaff with \(\gamma_{A01}\) = 1.01, and
imean.negaff predicting negaff with \(\gamma_{B01}\) = 1.0. That is a “duh!”. The
model doesn’t make sense! Do we actually need the between-person
variables at all?
Note the difference here between the standard cross-lag regression model (which is based on between-person associations) and this model (which is based on within-person associations).
Random Effects
We can push one more step though - and add some more random effects (between-person differences). How far we can push I’m not sure, but lets try! Can we include all the random effects for
and \[\beta_{A1i} = \gamma_{A10} + u_{A1i}\] \[\beta_{B1i} = \gamma_{B10} + u_{B1i}\] and \[\beta_{A2i} = \gamma_{A20} + u_{A2i}\] \[\beta_{B2i} = \gamma_{B20} + u_{B2i}\]
In reality, we build the model up slowly - increasing complexity one random effect at a time. However, here I just jump as far as we could go (and then regret it :-).
#adding additional random effects
model6.fit <- lme(fixed = affect ~ -1 +
pos + pos:posaff.state.lag1 + pos:negaff.state.lag1 +
neg + neg:negaff.state.lag1 + neg:posaff.state.lag1,
random = ~ -1 +
pos + pos:posaff.state.lag1 + pos:negaff.state.lag1 +
neg + neg:negaff.state.lag1 + neg:posaff.state.lag1 | id,
weights=varIdent(form = ~1 | var),
corr=corCompSymm(form = ~1 | id/day),
data=daily2,
na.action = na.exclude,
control = lmeControl(maxIter=500, opt = "optim")) #added in attempt to obtain convergence
summary(model6.fit)## Linear mixed-effects model fit by REML
## Data: daily2
## AIC BIC logLik
## 6332.808 6507.336 -3136.404
##
## Random effects:
## Formula: ~-1 + pos + pos:posaff.state.lag1 + pos:negaff.state.lag1 + neg + neg:negaff.state.lag1 + neg:posaff.state.lag1 | id
## Structure: General positive-definite, Log-Cholesky parametrization
## StdDev Corr
## pos 0.6824439 pos neg ps:p..1 ps:n..1 ng..1:
## neg 0.6625179 -0.306
## pos:posaff.state.lag1 0.1236745 0.071 -0.092
## pos:negaff.state.lag1 0.2419485 -0.010 -0.178 0.840
## negaff.state.lag1:neg 0.1943738 -0.102 -0.035 -0.771 -0.914
## posaff.state.lag1:neg 0.2016603 -0.487 0.028 -0.477 -0.470 0.726
## Residual 0.8369836
##
## Correlation Structure: Compound symmetry
## Formula: ~1 | id/day
## Parameter estimate(s):
## Rho
## -0.5738982
## Variance function:
## Structure: Different standard deviations per stratum
## Formula: ~1 | var
## Parameter estimates:
## 1 2
## 1.0000000 0.9235012
## Fixed effects: affect ~ -1 + pos + pos:posaff.state.lag1 + pos:negaff.state.lag1 + neg + neg:negaff.state.lag1 + neg:posaff.state.lag1
## Value Std.Error DF t-value p-value
## pos 4.078768 0.05554350 2299 73.43376 0.0000
## neg 2.402182 0.05344376 2299 44.94785 0.0000
## pos:posaff.state.lag1 0.172519 0.03665855 2299 4.70609 0.0000
## pos:negaff.state.lag1 0.116137 0.04375927 2299 2.65400 0.0080
## negaff.state.lag1:neg 0.081494 0.03928389 2299 2.07448 0.0381
## posaff.state.lag1:neg 0.002962 0.03642181 2299 0.08134 0.9352
## Correlation:
## pos neg ps:p..1 ps:n..1 ng..1:
## neg -0.354
## pos:posaff.state.lag1 -0.003 -0.011
## pos:negaff.state.lag1 -0.025 -0.054 0.552
## negaff.state.lag1:neg -0.021 -0.032 -0.359 -0.650
## posaff.state.lag1:neg -0.169 -0.007 -0.558 -0.336 0.559
##
## Standardized Within-Group Residuals:
## Min Q1 Med Q3 Max
## -3.46829629 -0.56399714 -0.05553655 0.54784299 3.90461447
##
## Number of Observations: 2490
## Number of Groups: 186
#intervals(model6.fit)
#Error in intervals.lme(model6.fit) :
#cannot get confidence intervals on var-cov components: Non-positive definite approximate variance-covarianceWell, we get estimates - but there is a problem with the model. We cannot get confidence intervals. So, we could try to find justification for fitting a simpler set of random effects; or we can try to do all of this in a Bayesian framework. Look for that in a future tutorial.
Conclusion
This tutorial is meant to illustrate how one can use the multivariate multilevel model to examine intraindividual coupling. We have provided a “seat-of-the-pants” example with the main intent being to demonstrate that the model is possible and the steps for preparing the data and articulating the model. When using the model for a real empirical inquiry - please be careful!
Cheers!
Citations
Pinheiro, J. C., Bates, D., & R Core Team. (2025). nlme: Linear and Nonlinear Mixed Effects Models (Version 3.1-167). https://doi.org/10.1007/b98882
Ram, N., Conroy, D. E., Pincus, A. L., Hyde, A. L., & Molloy, L. E. (2012). Tethering theory to method: Using measures of intraindividual variability to operationalize individuals’ dynamic characteristics. In G. Hancock & J. Harring (Eds.), Advances in longitudinal methods in the social and behavioral sciences (pp. 81-110). New York: Information Age.
R Core Team. (2024). R: A Language and Environment for Statistical Computing. Foundation for Statistical Computing. https://www.R-project.org/
Revelle, W. (2024). psych: Procedures for Psychological, Psychometric, and Personality Research. Northwestern University. https://CRAN.R-project.org/package=psych
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag. https://ggplot2.tidyverse.org/
Wickham, H., François, R., Henry, L., Müller, K., & Vaughan, D. (2023). dplyr: A Grammar of Data Manipulation (Version 1.1.4). https://CRAN.R-project.org/package=dplyr
Wickham, H., Vaughan, D., & Girlich, M. (2024). tidyr: Tidy Messy Data (Version 1.3.1). https://CRAN.R-project.org/package=tidyr