Cross-Sectional and Intensive Longitudinal APIM and DIM
2026-07-12
For readers who want the model code and reporting tables without the full tutorial:
interdepinterdep prepares dyadic data for multilevel models (Küng, 2026). It:
Install the development version from GitHub with pak:
interdep deliberately returns explicit, self-documenting names. After data preparation, this tutorial drops the generated .i_ prefix. For longitudinal predictors it otherwise retains the package’s component order (for example, cwp_actor and cbp_partner) and shortens within_dyad_deviation to dev. The remaining aliases are:
General and cross-sectional aliases
Canonical interdep name |
Tutorial alias | Interpretation |
|---|---|---|
.i_is_female_x_male_female |
is_female |
female member of a female-male dyad |
.i_is_female_x_male_male |
is_male |
male member of a female-male dyad |
.i_diff_female_x_male_arbitrary |
idiff |
female-male exchangeability contrast |
.i_diff_assumed_exchangeable_arbitrary |
idiff |
role-free exchangeability contrast |
.i_communication_actor |
communication_actor |
actor’s communication score |
.i_communication_partner |
communication_partner |
partner’s communication score |
.i_communication_dyad_mean_gmc |
communication_dyad_mean_gmc |
grand-mean-centered dyad mean |
.i_communication_within_dyad_deviation |
communication_dyad_dev |
member’s within-dyad deviation |
To keep these aliases consistent, this tutorial uses rename_interdep_for_tutorial(), defined in 00_R_Functions/PrepareData.R. The helper renames only generated columns that are present:
The mapping is cosmetic: values and statistical meanings are unchanged. Apply it only to the final model-ready output; continue using canonical names while calling interdep preparation functions.
Some functions used in this presentation are sourced from the files available in the folder 00_R_Functions.
library(tidyverse)
library(interdep)
library(brms)
library(glmmTMB)
library(easystats)
library(DiagrammeR)
library(DHARMa)
library(MASS)
library(purrr)
source(file.path('00_R_Functions', 'ReportModels.R'))
source(file.path('00_R_Functions', 'PrettyTables.R'))
source(file.path('00_R_Functions', 'PrepareData.R'))
print_apim_preview <- function(df) {
if (knitr::is_latex_output()) {
preview_cols <- c(
"userID", "coupleID", "gender", "is_male", "is_female",
"satisfaction", "communication_actor", "communication_partner"
)
df <- df |>
dplyr::select(dplyr::any_of(preview_cols))
}
print_df(head(df))
}
options(
brms.backend = 'cmdstanr',
brms.file_refit = 'on_change'
) (e.g., Kenny et al., 2006; Kenny & Cook, 1999; Kenny & Kashy, 2011)
| userID | coupleID | gender | communication | satisfaction | .i_composition | .i_composition_role | is_female | is_male | communication_actor | communication_partner |
|---|---|---|---|---|---|---|---|---|---|---|
| 1_1 | 1 | female | 4.851107 | 4.841332 | female_x_male | female_x_male_female | 1 | 0 | 4.851107 | 6.004533 |
| 1_2 | 1 | male | 6.004533 | 5.577165 | female_x_male | female_x_male_male | 0 | 1 | 6.004533 | 4.851107 |
| 2_1 | 2 | female | 5.881321 | 7.248741 | female_x_male | female_x_male_female | 1 | 0 | 5.881321 | 6.433723 |
| 2_2 | 2 | male | 6.433723 | 6.960293 | female_x_male | female_x_male_male | 0 | 1 | 6.433723 | 5.881321 |
| 3_1 | 3 | female | 4.283971 | 6.928699 | female_x_male | female_x_male_female | 1 | 0 | 4.283971 | 5.516060 |
| 3_2 | 3 | male | 5.516060 | 6.077688 | female_x_male | female_x_male_male | 0 | 1 | 5.516060 | 4.283971 |
For cross-sectional APIMs, communication_actor and communication_partner contain the original communication scores. The earlier version of this tutorial grand-mean centered both columns manually. Removing that centering does not change either slope or fitted value, but the role-specific intercepts now refer to communication = 0 rather than average communication.
glmmTMB models residual variance/dispersion via dispformula, but residual covariance is handled through random-effect covariance structures.
For Gaussian mixed models, dispformula = ~ 0 fixes the residual variance to a tiny value. This pushes the Gaussian residual variance into the random-effect block.
In this Gaussian dyadic model, the us(0 + is_male + is_female | coupleID) block therefore represents the 2x2 residual variance-covariance structure between partners.
For non-Gaussian models, this is not an ordinary residual covariance on the response scale. The same random-effect trick adds latent Gaussian heterogeneity/correlation on the linear predictor scale.
Do not use dispformula = ~ 0 as a general non-Gaussian residual-covariance trick. For families with an estimable dispersion parameter, dispformula models that family-specific parameter via a log link; for families without one, such as Poisson or binomial, it is ignored. Partner-specific dispersion, e.g. dispformula = ~ 0 + is_male + is_female, is only appropriate when the family has a dispersion parameter and this is substantively intended.
model_dist_apim_glmmtmb <- glmmTMB(
satisfaction ~
# Remove global intercept, introduce male and female intercepts.
0 + is_male + is_female +
# Actor effect for male and female
communication_actor:is_male +
communication_actor:is_female +
# Partner effect for male and female
communication_partner:is_male +
communication_partner:is_female +
# Modelling residual non-independence with an unstructured 2x2 covariance.
us(0 + is_male + is_female | coupleID)
# Gaussian response:
# dispformula = ~ 0 fixes residual variance to a tiny value,
# pushing the Gaussian residual variance into this random-effect block.
, dispformula = ~ 0
# Non-Gaussian responses:
# dispformula is family-specific dispersion, or ignored for families
# without a dispersion parameter. Do not use ~0 as a residual trick.
# If dispersion exists and should differ by partner, use e.g.:
# dispformula = ~ 0 + is_male + is_female
, data = df_apim
, family = gaussian
)Note that ‘unstructured’ is the default in glmmTMB, so instead of
us(0 + is_male + is_female | coupleID)
we could also remove us and simply write
(0 + is_male + is_female | coupleID).
An unstructured covariance matrix gives is_male and is_female their own variance and freely estimates their correlation. With more than two members (e.g., triads), it would estimate a separate correlation for every pair.
For two partners, the covariance matrix is:
\[ \boldsymbol{\Sigma} = \begin{pmatrix} \sigma^2_{female} & \rho\,\sigma_{female}\sigma_{male} \\ \rho\,\sigma_{female}\sigma_{male} & \sigma^2_{male} \end{pmatrix}. \]
Family: gaussian ( identity )
Formula: satisfaction ~ 0 + is_male + is_female + communication_actor:is_male +
communication_actor:is_female + communication_partner:is_male +
communication_partner:is_female + us(0 + is_male + is_female | coupleID)
Dispersion: ~0
Data: df_apim
AIC BIC logLik -2*log(L) df.resid
3776.7 3821.7 -1879.3 3758.7 1091
Random effects:
Conditional model:
Groups Name Variance Std.Dev. Corr
coupleID is_male 2.559 1.600
is_female 1.682 1.297 0.51
Number of obs: 1100, groups: coupleID, 550
Conditional model:
Estimate Std. Error z value Pr(>|z|)
is_male -5.46305 0.28124 -19.42 < 2e-16 ***
is_female -3.91806 0.22802 -17.18 < 2e-16 ***
is_male:communication_actor 1.84363 0.04931 37.39 < 2e-16 ***
is_female:communication_actor 1.59248 0.04112 38.72 < 2e-16 ***
is_male:communication_partner 0.16765 0.05072 3.31 0.000949 ***
is_female:communication_partner 0.31084 0.03997 7.78 7.48e-15 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Usually, simulation-based tools like DHARMa (Hartig, 2026) are great for residual diagnostics in complex (generalized) MLMs.
However, when we remove the native residuals dispformula = ~ 0 the residual is, in practice, set to a tiny constant. This confuses model diagnostic packages!
Therefore, residuals need to be manually simulated and compared.
In brms, we can use brms-provided residual covariance structures directly. We do not have to set residuals internally to zero.
formula_dist_apim_b <- bf(
satisfaction ~
# Remove global intercept, introduce male and female intercepts.
0 + is_male + is_female +
# Actor effect for male and female
communication_actor:is_male +
communication_actor:is_female +
# Partner effect for male and female
communication_partner:is_male +
communication_partner:is_female +
# Gender indexes the two within-couple residual positions.
unstr(time = gender, gr = coupleID),
# Allowing two distinct residual variances for males vs females:
sigma = ~ 0 + is_male + is_female
)
priors_dist_apim_b <- c(
# prior(normal(2, 3), class = "Intercept"),
prior(normal(0, 5), class = "b"),
prior(student_t(3, 0, 1.5), class = "b", dpar = "sigma"),
prior(lkj(2), class = "cortime")
)
model_dist_apim_b <- brm(
formula = formula_dist_apim_b,
data = df_apim,
family = gaussian(link = identity),
prior = priors_dist_apim_b,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'example1_dist_apim_b') # Cache the model
) Family: gaussian
Links: mu = identity; sigma = log
Formula: satisfaction ~ 0 + is_male + is_female + communication_actor:is_male + communication_actor:is_female + communication_partner:is_male + communication_partner:is_female + unstr(time = gender, gr = coupleID)
sigma ~ 0 + is_male + is_female
Data: df_apim (Number of observations: 1100)
Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
total post-warmup draws = 4000
Correlation Structures:
Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
cortime(female,male) 0.51 0.03 0.44 0.57 1.00 2895 2470
Regression Coefficients:
Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
is_male -5.44 0.28 -5.99 -4.88 1.00 2458 2617
is_female -3.90 0.23 -4.37 -3.47 1.00 2401 2641
is_male:communication_actor 1.84 0.05 1.75 1.94 1.00 2678 2997
is_female:communication_actor 1.59 0.04 1.51 1.67 1.00 2371 2549
is_male:communication_partner 0.17 0.05 0.07 0.26 1.00 2555 2726
is_female:communication_partner 0.31 0.04 0.23 0.39 1.00 2398 2724
sigma_is_male 0.47 0.03 0.42 0.53 1.00 3555 2827
sigma_is_female 0.26 0.03 0.21 0.33 1.00 3317 2788
Draws were sampled using sample(hmc). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).For families that have no residuals, this does not work and we need to model the non-independence in the random effects:
formula_dist_apim_c <- bf(
as.integer(satisfaction + 6) ~
# Remove global intercept, introduce male and female intercepts.
0 + is_male + is_female +
# Actor effect for male and female
communication_actor:is_male +
communication_actor:is_female +
# Partner effect for male and female
communication_partner:is_male +
communication_partner:is_female +
# Modelling residual correlation as random effects, because we cannot specify
# a sigma parameter, as this distribution has no residuals.
(0 + is_male + is_female | coupleID)
# sigma ~ 0 + is_male + is_female
)
priors_dist_apim_c <- c(
prior(normal(0, 5), class = "b"),
prior(student_t(3, 0, 1.5), class = "sd"),
prior(lkj(2), class = "cor")
)
model_dist_apim_c <- brm(
formula = formula_dist_apim_c,
data = df_apim,
family = poisson(),
prior = priors_dist_apim_c,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'example1_dist_apim_c') # Cache the model
) Family: poisson
Links: mu = log
Formula: as.integer(satisfaction + 6) ~ 0 + is_male + is_female + communication_actor:is_male + communication_actor:is_female + communication_partner:is_male + communication_partner:is_female + (0 + is_male + is_female | coupleID)
Data: df_apim (Number of observations: 1100)
Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
total post-warmup draws = 4000
Multilevel Hyperparameters:
~coupleID (Number of levels: 550)
Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sd(is_male) 0.01 0.01 0.00 0.04 1.00 2950 1558
sd(is_female) 0.01 0.01 0.00 0.03 1.00 3336 1803
cor(is_male,is_female) 0.02 0.44 -0.80 0.81 1.00 6459 2872
Regression Coefficients:
Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
is_male 1.29 0.06 1.18 1.41 1.00 4332 2701
is_female 1.52 0.06 1.40 1.63 1.00 4999 2882
is_male:communication_actor 0.18 0.01 0.16 0.20 1.00 5510 3239
is_female:communication_actor 0.14 0.01 0.12 0.16 1.00 5619 2960
is_male:communication_partner 0.02 0.01 -0.00 0.03 1.00 5119 3116
is_female:communication_partner 0.03 0.01 0.01 0.05 1.00 6257 2679
Draws were sampled using sample(hmc). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).model_glmmTMB_dist_apim_c <- glmmTMB(
as.integer(satisfaction + 6) ~
# Remove global intercept, introduce male and female intercepts.
0 + is_male + is_female +
# Actor effect for male and female
communication_actor:is_male +
communication_actor:is_female +
# Partner effect for male and female
communication_partner:is_male +
communication_partner:is_female +
# Modelling residual correlation as random effects, because we cannot specify
# a sigma parameter, as this distribution has no residuals.
(0 + is_male + is_female | coupleID)
, data = df_apim
, family = poisson()
) Family: poisson ( log )
Formula: as.integer(satisfaction + 6) ~ 0 + is_male + is_female + communication_actor:is_male +
communication_actor:is_female + communication_partner:is_male +
communication_partner:is_female + (0 + is_male + is_female | coupleID)
Data: df_apim
AIC BIC logLik -2*log(L) df.resid
NA NA NA NA 1091
Random effects:
Conditional model:
Groups Name Variance Std.Dev. Corr
coupleID is_male 1.331e-10 1.154e-05
is_female 2.502e-12 1.582e-06 -1.00
Number of obs: 1100, groups: coupleID, 550
Conditional model:
Estimate Std. Error z value Pr(>|z|)
is_male 1.294677 0.057667 22.451 < 2e-16 ***
is_female 1.516475 0.055264 27.441 < 2e-16 ***
is_male:communication_actor 0.180139 0.009598 18.768 < 2e-16 ***
is_female:communication_actor 0.143973 0.009580 15.028 < 2e-16 ***
is_male:communication_partner 0.015476 0.010003 1.547 0.12183
is_female:communication_partner 0.028344 0.009180 3.088 0.00202 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Check Rhats and Effective Sample Sizes (ESS_tail and ESS_bulk) directly from the brms summary. Additionally you can (using Model B as an example):
In case of non-convergence, bad residuals or low predictive accuracy, the model is likely miss-specified.
You can try different families that match your data by using the family argument. For instance, you can easily conduct an ordinal regression by simply setting family = cumulative() and adjusting your priors. For dichotomous outcomes you can use family = bernoulli(). brms supports a variety of families and link functions for these families. In general, you should use the default link function (check ?brms::family.brmsfit()).
Note that for some of the later models in this tutorial, some ESS values are too low. In this case, usually we would increase iterations (and warmup), and/or adapt_delta (see brms documentation).
For Gaussian outcomes, residual covariance has a direct response-scale interpretation:
For non-Gaussian outcomes, the same syntax usually does not estimate an ordinary residual covariance.
Instead, random-effect covariance terms represent latent Gaussian heterogeneity on the link scale.
For count outcomes, latent partner covariance is often useful and can be estimable:
For dichotomous outcomes, the same idea is much harder:
0 or 1 per occasionBinary dyadic covariance models can converge mechanically while still being unreliable.
Watch for:
-1 or 1Practical default:
| Est. | 95% CI | Rhat | Bulk_ESS | Tail_ESS | |
|---|---|---|---|---|---|
| Fixed Effects | |||||
| is_male | -5.44 | [-5.99, -4.88] | 1.003 | 2458 | 2617 |
| is_female | -3.90 | [-4.37, -3.47] | 1.001 | 2401 | 2641 |
| is_male:communication_actor | 1.84 | [ 1.75, 1.94] | 1.000 | 2678 | 2997 |
| is_female:communication_actor | 1.59 | [ 1.51, 1.67] | 1.001 | 2371 | 2549 |
| is_male:communication_partner | 0.17 | [ 0.07, 0.26] | 1.001 | 2555 | 2726 |
| is_female:communication_partner | 0.31 | [ 0.23, 0.39] | 1.000 | 2398 | 2724 |
| Residual Structure | |||||
| cortime(female,male) | 0.51 | [0.44, 0.57] | 1.000 | 2895 | 2470 |
| sigma_is_male | 1.60 | [1.51, 1.71] | 1.000 | 3555 | 2827 |
| sigma_is_female | 1.30 | [1.23, 1.39] | 1.001 | 3317 | 2788 |
NOTE: brms estimates sigma on the log scale. In the tables, sigmas are exponentiated to show residual SDs on the response scale, like glmmTMB output.
| userID | coupleID | diaryday | gender | closeness | provided_support |
|---|---|---|---|---|---|
| 1_1 | 1 | 0 | female | 5.659395 | 4.733482 |
| 1_1 | 1 | 1 | female | 7.249842 | 5.019950 |
| 1_1 | 1 | 2 | female | 6.040587 | 5.365179 |
| 1_1 | 1 | 3 | female | 6.898253 | 5.148704 |
| 1_1 | 1 | 4 | female | 5.350049 | 3.702039 |
| 1_1 | 1 | 5 | female | 6.191282 | 4.844602 |
df_long_apim <- prepare_interdep_data(
data = df_long,
group = coupleID,
member = userID,
role = gender,
time = diaryday,
predictors = provided_support,
model_type = "apim"
) %>%
rename_interdep_for_tutorial(
# Keep interdep's cwp_actor/cbp_partner component ordering.
predictor = "provided_support"
) %>%
mutate(
diaryday_c = diaryday - mean(diaryday)
)| userID | coupleID | diaryday | gender | closeness | provided_support | .i_composition | .i_composition_role | is_female | is_male | provided_support_cwp | provided_support_cbp | provided_support_cwp_actor | provided_support_cwp_partner | provided_support_cbp_actor | provided_support_cbp_partner | diaryday_c |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31_1 | 31 | 0 | female | 5.47 | 6.02 | female_x_male | female_x_male_female | 1 | 0 | 0.41 | 0.65 | 0.41 | 1.54 | 0.65 | 0.58 | -27 |
| 31_1 | 31 | 1 | female | 7.69 | 6.13 | female_x_male | female_x_male_female | 1 | 0 | 0.51 | 0.65 | 0.51 | 1.4 | 0.65 | 0.58 | -26 |
| 31_1 | 31 | 2 | female | 5.83 | 6.1 | female_x_male | female_x_male_female | 1 | 0 | 0.49 | 0.65 | 0.49 | 0.79 | 0.65 | 0.58 | -25 |
| 31_1 | 31 | 3 | female | 6.55 | 6.99 | female_x_male | female_x_male_female | 1 | 0 | 1.38 | 0.65 | 1.38 | -0.15 | 0.65 | 0.58 | -24 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 31_2 | 31 | 0 | male | 5.19 | 7.09 | female_x_male | female_x_male_male | 0 | 1 | 1.54 | 0.58 | 1.54 | 0.41 | 0.58 | 0.65 | -27 |
| 31_2 | 31 | 1 | male | 6.96 | 6.95 | female_x_male | female_x_male_male | 0 | 1 | 1.4 | 0.58 | 1.4 | 0.51 | 0.58 | 0.65 | -26 |
| 31_2 | 31 | 2 | male | 6.9 | 6.34 | female_x_male | female_x_male_male | 0 | 1 | 0.79 | 0.58 | 0.79 | 0.49 | 0.58 | 0.65 | -25 |
| 31_2 | 31 | 3 | male | 7.59 | 5.39 | female_x_male | female_x_male_male | 0 | 1 | -0.15 | 0.58 | -0.15 | 1.38 | 0.58 | 0.65 | -24 |
With repeated measures we need to center variables in order to not conflate levels of analysis:
Although the data contain couple, person, and day structure, the APIM is written at the person-day level after decomposing predictors into between-person and within-person components. Dyad-level dependence is still modelled explicitly through partner-specific random effects and residual covariance structures.
prepare_interdep_data() performs this predictor decomposition before creating the actor and partner columns. The centered diaryday_c variable above is a separate time transformation for the models, not part of the dyadic predictor construction.
nlme model of this data for reference (simplified random effects structure to achieve convergence).
This package is old, but still often used and cited. So here, we briefly show it to demonstrate that we can achieve the same model with much more modern and powerful methods.
library(nlme)
model1 <- lme(
fixed =
# Intercepts
closeness ~ 0 + is_male + is_female +
# Time Slopes
is_male:diaryday_c +
is_female:diaryday_c +
# Between-Person APIM
is_male:provided_support_cbp_actor +
is_male:provided_support_cbp_partner +
is_female:provided_support_cbp_actor +
is_female:provided_support_cbp_partner +
# Within-Person APIM
is_male:provided_support_cwp_actor +
is_male:provided_support_cwp_partner +
is_female:provided_support_cwp_actor +
is_female:provided_support_cwp_partner,
random =
~ 0 + is_male + is_female
+ is_male:diaryday_c + is_female:diaryday_c
# in nlme we need to remove many random slopes to achieve convergence
# alternatively, we could try to remove correlations between the slopes.
#+ is_male:provided_support_cwp_actor
#+ is_male:provided_support_cwp_partner
#+ is_female:provided_support_cwp_actor
#+ is_female:provided_support_cwp_partner
| coupleID,
weights = varIdent(form = ~ 1 | gender), # heterogeneous residual variances
corr = corCompSymm(form = ~ 1 | coupleID/diaryday), # compound symmetry (partner's values on each day)
data = df_long_apim,
control = list(maxIter=1000)
)
summary(model1)model_dist_apim_long_glmmtmb <- glmmTMB(
closeness ~ 0 + is_male + is_female +
# Time Slopes
is_male:diaryday_c +
is_female:diaryday_c +
# Between-Person APIM
is_male:provided_support_cbp_actor +
is_male:provided_support_cbp_partner +
is_female:provided_support_cbp_actor +
is_female:provided_support_cbp_partner +
# Within-Person APIM
is_male:provided_support_cwp_actor +
is_male:provided_support_cwp_partner +
is_female:provided_support_cwp_actor +
is_female:provided_support_cwp_partner +
# Accounting for non-independence between partners' means and trajectories
# and effect sensitivities via random effects:
(0 + is_male + is_female
+ is_male:diaryday_c + is_female:diaryday_c
| coupleID) +
# Accounting for daily non-independence
us(0 + is_male + is_female | coupleID:diaryday)
# heteroscedastic residuals
, dispformula = ~ 0
, data = df_long_apim
) Family: gaussian ( identity )
Formula: closeness ~ 0 + is_male + is_female + is_male:diaryday_c + is_female:diaryday_c +
is_male:provided_support_cbp_actor + is_male:provided_support_cbp_partner +
is_female:provided_support_cbp_actor + is_female:provided_support_cbp_partner +
is_male:provided_support_cwp_actor + is_male:provided_support_cwp_partner +
is_female:provided_support_cwp_actor + is_female:provided_support_cwp_partner +
(0 + is_male + is_female + is_male:diaryday_c + is_female:diaryday_c |
coupleID) + us(0 + is_male + is_female | coupleID:diaryday)
Dispersion: ~0
Data: df_long_apim
AIC BIC logLik -2*log(L) df.resid
30001.8 30184.4 -14975.9 29951.8 10975
Random effects:
Conditional model:
Groups Name Variance Std.Dev. Corr
coupleID is_male 0.6813649 0.82545
is_female 0.7929829 0.89050 0.19
is_male:diaryday_c 0.0002637 0.01624 0.55 0.03
is_female:diaryday_c 0.0002571 0.01603 0.21 0.54 -0.01
coupleID:diaryday is_male 1.2583395 1.12176
is_female 0.5206603 0.72157 0.04
Number of obs: 11000, groups: coupleID, 100; coupleID:diaryday, 5500
Conditional model:
Estimate Std. Error z value Pr(>|z|)
is_male 4.486375 0.085960 52.19 < 2e-16 ***
is_female 5.736720 0.091763 62.52 < 2e-16 ***
is_male:diaryday_c -0.003258 0.001883 -1.73 0.08355 .
is_female:diaryday_c 0.010606 0.001717 6.18 6.46e-10 ***
is_male:provided_support_cbp_actor 1.128018 0.101922 11.07 < 2e-16 ***
is_male:provided_support_cbp_partner 0.298902 0.098817 3.02 0.00249 **
is_female:provided_support_cbp_actor 1.380753 0.105607 13.07 < 2e-16 ***
is_female:provided_support_cbp_partner 0.587254 0.108927 5.39 7.00e-08 ***
is_male:provided_support_cwp_actor 0.184479 0.020564 8.97 < 2e-16 ***
is_male:provided_support_cwp_partner 0.124548 0.020571 6.05 1.41e-09 ***
is_female:provided_support_cwp_actor 0.419162 0.013277 31.57 < 2e-16 ***
is_female:provided_support_cwp_partner 0.170127 0.013270 12.82 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Let’s compare the random effects outputs to see if the models are truly equivalent.
Residual variance in nlme must first be constructed from the output by taking the residual SD and multiplying it by the parameter estimate under the variance function for each partner.
StdDev Corr
is_male 0.83645552 is_mal is_fml is_m:_
is_female 0.90199634 0.189
is_male:diaryday_c 0.01634796 0.544 0.033
is_female:diaryday_c 0.01612665 0.204 0.534 -0.012
Residual 0.72170702
Correlation Structure: Compound symmetry
Formula: ~1 | coupleID/diaryday
Parameter estimate(s):
Rho
0.04094751
Variance function:
Structure: Different standard deviations per stratum
Formula: ~1 | gender
Parameter estimates:
1 2
1.000000 1.554608
Manually computed by multiplying the residual SD by the multipliers:
Residual SDs by partner:
Partner 1: 0.721707
Partner 2: 1.121972
Random effects:
Conditional model:
Groups Name Variance Std.Dev. Corr
coupleID is_male 0.6814498 0.82550
is_female 0.7930407 0.89053 0.19
is_male:diaryday_c 0.0002637 0.01624 0.55 0.03
is_female:diaryday_c 0.0002571 0.01603 0.21 0.54 -0.01
coupleID:diaryday is_male 1.2583552 1.12176
is_female 0.5206608 0.72157 0.04
Equivalent brms model.
In contrast to glmmTMB, brms provides native variance-covariance structures to model residuals directly. Therefore, we do NOT set the residuals to zero!
formula_dist_apim_long_simple <- bf(
closeness ~ 0 + is_male + is_female +
# Time Slopes
is_male:diaryday_c +
is_female:diaryday_c +
# Between-Person APIM
is_male:provided_support_cbp_actor +
is_male:provided_support_cbp_partner +
is_female:provided_support_cbp_actor +
is_female:provided_support_cbp_partner +
# Within-Person APIM
is_male:provided_support_cwp_actor +
is_male:provided_support_cwp_partner +
is_female:provided_support_cwp_actor +
is_female:provided_support_cwp_partner +
# Accounting for non-independence between partners' means and trajectories
# and effect sensitivities via random effects:
(0 + is_male + is_female
+ is_male:diaryday_c + is_female:diaryday_c
| coupleID) +
# Gender indexes the two partner residual positions within each couple-day.
unstr(time = gender, gr = coupleID:diaryday)
# heteroscedastic residuals
, sigma ~ 0 + is_male + is_female
)
# brms' defaults for group-level SDs and correlations are sensible weak defaults
# (half Student-t priors for SDs and LKJ priors for correlation matrices). Here we
# set weakly regularizing priors explicitly so the modelling assumptions are visible.
priors_dist_apim_long_simple <- c(
prior(normal(4, 2), class = "b", coef = "is_male"), # male intercept
prior(normal(4, 2), class = "b", coef = "is_female"), # female intercept
prior(normal(0, 2), class = "b"), # other beta coefficients
prior(exponential(1), class = "sd"),
prior(lkj(2), class = "cor"),
prior(normal(0, 0.7), class = "b", dpar = "sigma"),
prior(lkj(2), class = "cortime")
)
model_dist_apim_long_simple <- brm(
formula = formula_dist_apim_long_simple,
data = df_long_apim,
family = gaussian(link = identity), # student() is also often a nice robust option.
prior = priors_dist_apim_long_simple,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'example1_dist_apim_long_simpel') # Cache the model
)| Est. | 95% CI | Rhat | Bulk_ESS | Tail_ESS | |
|---|---|---|---|---|---|
| Fixed Effects | |||||
| is_male | 4.49 | [ 4.32, 4.65] | 1.007 | 324 | 794 |
| is_female | 5.74 | [ 5.56, 5.91] | 1.009 | 355 | 656 |
| is_male:diaryday_c | 0.00 | [-0.01, 0.00] | 1.007 | 1072 | 2113 |
| is_female:diaryday_c | 0.01 | [ 0.01, 0.01] | 1.002 | 880 | 1778 |
| is_male:provided_support_cbp_actor | 1.14 | [ 0.91, 1.34] | 1.006 | 489 | 791 |
| is_male:provided_support_cbp_partner | 0.30 | [ 0.10, 0.51] | 1.004 | 553 | 716 |
| is_female:provided_support_cbp_actor | 1.38 | [ 1.18, 1.58] | 1.005 | 507 | 1077 |
| is_female:provided_support_cbp_partner | 0.60 | [ 0.36, 0.81] | 1.005 | 440 | 851 |
| is_male:provided_support_cwp_actor | 0.18 | [ 0.14, 0.23] | 1.000 | 5328 | 3057 |
| is_male:provided_support_cwp_partner | 0.12 | [ 0.08, 0.16] | 1.003 | 7228 | 3389 |
| is_female:provided_support_cwp_actor | 0.42 | [ 0.39, 0.45] | 1.002 | 5990 | 3068 |
| is_female:provided_support_cwp_partner | 0.17 | [ 0.14, 0.20] | 1.001 | 5829 | 3105 |
| Random Effects | |||||
| sd(is_male) | 0.84 | [ 0.73, 0.97] | 1.010 | 520 | 1077 |
| sd(is_female) | 0.92 | [ 0.80, 1.06] | 1.006 | 556 | 998 |
| sd(is_male:diaryday_c) | 0.02 | [ 0.01, 0.02] | 1.001 | 1744 | 2637 |
| sd(is_female:diaryday_c) | 0.02 | [ 0.01, 0.02] | 1.001 | 1358 | 2043 |
| cor(is_male,is_female) | 0.18 | [-0.05, 0.35] | 1.010 | 321 | 688 |
| cor(is_male,is_male:diaryday_c) | 0.51 | [ 0.32, 0.67] | 1.004 | 1528 | 1804 |
| cor(is_female,is_male:diaryday_c) | 0.03 | [-0.21, 0.24] | 1.002 | 1315 | 2204 |
| cor(is_male,is_female:diaryday_c) | 0.19 | [-0.01, 0.38] | 1.003 | 753 | 1691 |
| cor(is_female,is_female:diaryday_c) | 0.51 | [ 0.33, 0.65] | 1.002 | 1073 | 1832 |
| cor(is_male:diaryday_c,is_female:diaryday_c) | -0.02 | [-0.25, 0.22] | 1.008 | 867 | 1748 |
| cortime(female,male) | 0.04 | [ 0.01, 0.07] | 1.000 | 6904 | 3067 |
| Residual Structure | |||||
| sigma_is_male | 1.12 | [ 1.10, 1.14] | 1.001 | 6996 | 2514 |
| sigma_is_female | 0.72 | [ 0.71, 0.74] | 0.999 | 6880 | 2868 |
Note: Sigma rows are shown on the response scale; they were exponentiated from brms’ log-sigma parameterization.
In this model, same-day partner interdependence is handled by unstr(time = gender, gr = coupleID:diaryday). In current univariate brms models, we cannot combine this residual correlation structure with ar() in the same formula.
glmmTMB currently also does not allow partner interdependence across time, but I have a fork of glmmTMB with added support and am currently working on integrating this with upstream glmmTMB.
A dynamic MLM can account for temporal dependence by including lagged within-person centered outcomes as predictors (include both actor inertia and partner spillover).
But:
We add all random slopes and their correlations. This is neither converging in nlme nor glmmTMB.
formula_dist_apim_long_complex <- bf(
closeness ~
# Intercepts
0 + is_male + is_female +
# Time Slopes
is_male:diaryday_c +
is_female:diaryday_c +
# Between-Person APIM
is_male:provided_support_cbp_actor +
is_male:provided_support_cbp_partner +
is_female:provided_support_cbp_actor +
is_female:provided_support_cbp_partner +
# Within-Person APIM
is_male:provided_support_cwp_actor +
is_male:provided_support_cwp_partner +
is_female:provided_support_cwp_actor +
is_female:provided_support_cwp_partner +
# Accounting for non-independence between partners' means and trajectories
# and effect sensitivities via random effects:
(0 + is_male + is_female +
is_male:diaryday_c + is_female:diaryday_c +
is_male:provided_support_cwp_actor +
is_male:provided_support_cwp_partner +
is_female:provided_support_cwp_actor +
is_female:provided_support_cwp_partner
| coupleID ) +
# Accounting for daily non-independence
unstr(time = gender, gr = coupleID:diaryday),
# No more AR1. You could also test MA or ARMA.
sigma ~ 0 + is_male + is_female # heteroscedastic residuals
)
priors_dist_apim_long_complex <- c(
prior(normal(4, 2), class = "b", coef = "is_male"),
prior(normal(4, 2), class = "b", coef = "is_female"),
prior(normal(0, 2), class = "b"),
prior(exponential(1), class = "sd"),
prior(lkj(2), class = "cor"),
prior(normal(0, 0.7), class = "b", dpar = "sigma"),
prior(lkj(2), class = "cortime")
)
model_dist_apim_long_complex <- brm(
formula = formula_dist_apim_long_complex,
data = df_long_apim,
family = gaussian(link = identity),
prior = priors_dist_apim_long_complex,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'example1_dist_apim_long_complex') # Cache the model
)| Est. | 95% CI | Rhat | Bulk_ESS | Tail_ESS | |
|---|---|---|---|---|---|
| Fixed Effects | |||||
| is_male | 4.49 | [ 4.31, 4.67] | 1.006 | 334 | 722 |
| is_female | 5.73 | [ 5.54, 5.92] | 1.006 | 271 | 770 |
| is_male:diaryday_c | 0.00 | [-0.01, 0.00] | 1.002 | 1249 | 1965 |
| is_female:diaryday_c | 0.01 | [ 0.01, 0.01] | 1.005 | 840 | 2033 |
| is_male:provided_support_cbp_actor | 1.12 | [ 0.92, 1.32] | 1.004 | 688 | 1529 |
| is_male:provided_support_cbp_partner | 0.31 | [ 0.10, 0.51] | 1.002 | 656 | 1267 |
| is_female:provided_support_cbp_actor | 1.37 | [ 1.16, 1.59] | 1.002 | 491 | 987 |
| is_female:provided_support_cbp_partner | 0.60 | [ 0.35, 0.82] | 1.003 | 432 | 740 |
| is_male:provided_support_cwp_actor | 0.18 | [ 0.14, 0.23] | 1.000 | 5755 | 3046 |
| is_male:provided_support_cwp_partner | 0.13 | [ 0.08, 0.18] | 1.000 | 6028 | 3158 |
| is_female:provided_support_cwp_actor | 0.42 | [ 0.39, 0.45] | 1.000 | 5549 | 3243 |
| is_female:provided_support_cwp_partner | 0.17 | [ 0.14, 0.20] | 1.000 | 5548 | 3014 |
| Random Effects | |||||
| sd(is_male) | 0.85 | [ 0.74, 0.98] | 1.002 | 857 | 1449 |
| sd(is_female) | 0.91 | [ 0.79, 1.05] | 1.006 | 515 | 1265 |
| sd(is_male:diaryday_c) | 0.02 | [ 0.01, 0.02] | 1.000 | 1973 | 2634 |
| sd(is_female:diaryday_c) | 0.02 | [ 0.01, 0.02] | 1.001 | 1361 | 2175 |
| sd(is_male:provided_support_cwp_actor) | 0.09 | [ 0.02, 0.15] | 1.002 | 1162 | 1085 |
| sd(is_male:provided_support_cwp_partner) | 0.14 | [ 0.06, 0.20] | 1.000 | 1012 | 947 |
| sd(is_female:provided_support_cwp_actor) | 0.08 | [ 0.02, 0.12] | 1.004 | 923 | 905 |
| sd(is_female:provided_support_cwp_partner) | 0.06 | [ 0.01, 0.10] | 1.001 | 1002 | 1720 |
| cor(is_male,is_female) | 0.15 | [-0.04, 0.34] | 1.022 | 350 | 819 |
| cor(is_male,is_male:diaryday_c) | 0.48 | [ 0.28, 0.63] | 1.001 | 2211 | 2867 |
| cor(is_female,is_male:diaryday_c) | 0.00 | [-0.20, 0.22] | 1.003 | 1801 | 3156 |
| cor(is_male,is_female:diaryday_c) | 0.17 | [-0.03, 0.36] | 1.005 | 940 | 2321 |
| cor(is_female,is_female:diaryday_c) | 0.49 | [ 0.30, 0.64] | 1.008 | 1172 | 2078 |
| cor(is_male:diaryday_c,is_female:diaryday_c) | -0.03 | [-0.26, 0.19] | 1.003 | 1108 | 1730 |
| cor(is_male,is_male:provided_support_cwp_actor) | 0.37 | [-0.06, 0.70] | 1.001 | 4857 | 2732 |
| cor(is_female,is_male:provided_support_cwp_actor) | 0.19 | [-0.22, 0.57] | 1.003 | 5018 | 2783 |
| cor(is_male:diaryday_c,is_male:provided_support_cwp_actor) | 0.33 | [-0.13, 0.68] | 1.001 | 5222 | 3270 |
| cor(is_female:diaryday_c,is_male:provided_support_cwp_actor) | 0.27 | [-0.17, 0.64] | 1.000 | 4899 | 2758 |
| cor(is_male,is_male:provided_support_cwp_partner) | 0.06 | [-0.26, 0.38] | 1.000 | 5278 | 3217 |
| cor(is_female,is_male:provided_support_cwp_partner) | 0.10 | [-0.22, 0.43] | 1.001 | 4991 | 2571 |
| cor(is_male:diaryday_c,is_male:provided_support_cwp_partner) | 0.04 | [-0.31, 0.40] | 1.000 | 4298 | 2455 |
| cor(is_female:diaryday_c,is_male:provided_support_cwp_partner) | 0.11 | [-0.22, 0.45] | 1.000 | 4766 | 2466 |
| cor(is_male:provided_support_cwp_actor,is_male:provided_support_cwp_partner) | 0.24 | [-0.27, 0.68] | 1.005 | 1124 | 2137 |
| cor(is_male,is_female:provided_support_cwp_actor) | -0.06 | [-0.44, 0.29] | 1.001 | 4620 | 2478 |
| cor(is_female,is_female:provided_support_cwp_actor) | 0.21 | [-0.17, 0.55] | 1.000 | 4730 | 2276 |
| cor(is_male:diaryday_c,is_female:provided_support_cwp_actor) | -0.14 | [-0.51, 0.25] | 1.001 | 4056 | 3026 |
| cor(is_female:diaryday_c,is_female:provided_support_cwp_actor) | -0.07 | [-0.43, 0.30] | 1.000 | 5099 | 3132 |
| cor(is_male:provided_support_cwp_actor,is_female:provided_support_cwp_actor) | -0.17 | [-0.61, 0.37] | 1.002 | 1540 | 2245 |
| cor(is_male:provided_support_cwp_partner,is_female:provided_support_cwp_actor) | -0.10 | [-0.56, 0.40] | 1.001 | 2566 | 2969 |
| cor(is_male,is_female:provided_support_cwp_partner) | 0.27 | [-0.21, 0.63] | 1.000 | 5732 | 2872 |
| cor(is_female,is_female:provided_support_cwp_partner) | 0.07 | [-0.37, 0.48] | 1.000 | 5615 | 2735 |
| cor(is_male:diaryday_c,is_female:provided_support_cwp_partner) | 0.12 | [-0.34, 0.53] | 1.000 | 5813 | 3459 |
| cor(is_female:diaryday_c,is_female:provided_support_cwp_partner) | 0.15 | [-0.31, 0.56] | 1.002 | 5869 | 2966 |
| cor(is_male:provided_support_cwp_actor,is_female:provided_support_cwp_partner) | 0.16 | [-0.39, 0.64] | 1.001 | 2540 | 3043 |
| cor(is_male:provided_support_cwp_partner,is_female:provided_support_cwp_partner) | 0.06 | [-0.48, 0.57] | 1.002 | 3413 | 3154 |
| cor(is_female:provided_support_cwp_actor,is_female:provided_support_cwp_partner) | 0.11 | [-0.44, 0.60] | 1.001 | 3345 | 3240 |
| cortime(female,male) | 0.04 | [ 0.01, 0.07] | 1.002 | 8477 | 2860 |
| Residual Structure | |||||
| sigma_is_male | 1.11 | [ 1.09, 1.14] | 1.002 | 6681 | 2536 |
| sigma_is_female | 0.72 | [ 0.70, 0.73] | 1.001 | 6554 | 2959 |
In complex models like this, we want to make sure we are not overfitting.
Conduct regular model convergence checks (inspecting chains, Rhats, ESS, Multicollinearity, Residual diagnostics etc.). If these are bad (but good for a simpler model), it could be a sign of overfitting.
Leave-one-out cross-validation is a powerful tool for investigating overfitting. It tests out of sample prediction and thus punishes overfitting.
All Pareto k estimates are good (k < 0.7).
All Pareto k estimates are good (k < 0.7).
High Pareto-k values indicate influential observations and/or unreliable importance-sampling approximations. They are not automatically evidence of overfitting, but they should be checked; loo(model, moment_match = TRUE) is often recommended when loo flags problematic values.
The absolute ELPD values are not interpreted in isolation, but differences in ELPD can be used to compare models.
elpd_diff se_diff
model_dist_apim_long_complex 0.0 0.0
model_dist_apim_long_simple -15.4 6.7
No obvious overfitting is detected here. The more complex model appears more predictive, but ELPD differences should be interpreted relative to their standard errors; rules such as 2x SE are rough heuristics, not hard decision thresholds.
If there was no obvious difference, we could follow our theory and the philosophy of the maximal random effects structure (Barr et al., 2013). Alternatively, we could follow the philosophy of parsimony, especially in the territory of such complex models.
(continued on next page)
One optional summary is the report package’s comparison output, including its approximate p-value; this should be treated as an aid to interpretation rather than a definitive model-selection test.
The difference in predictive accuracy, as indexed by Expected Log Predictive Density (ELPD-LOO), suggests that ‘model_dist_apim_long_complex’ is the best model (ELPD = -14608.23), followed by ‘model_dist_apim_long_simple’ (diff-ELPD = -15.44 +- 6.68, p = 0.021).
In this instance, the maximal model (no AR) seems to be the preferred one.
In the case of overfitting, we could try to improve the model by subsequently excluding the smallest random effects (or correlations between them) and compare various models. For more guidance on random effects check out del Rosario & West (2025).
| Group | Parameter | True (Simulated) | Estimate | 95% CI | Coverage | Abs. Error | Recovery |
|---|---|---|---|---|---|---|---|
| Fixed | Intercept (male) | 4.5659893 | 4.4890395 | [4.305, 4.668] | Yes | 0.077 | Covered |
| Fixed | Intercept (female) | 5.7823087 | 5.7316403 | [5.537, 5.919] | Yes | 0.051 | Covered |
| Fixed | Time slope (male) | -0.0050000 | -0.0029689 | [-0.007, 0.001] | Yes | 0.002 | Covered |
| Fixed | Time slope (female) | 0.0100000 | 0.0104637 | [0.007, 0.014] | Yes | 0.000 | Covered |
| Between-person | Actor BP (male) | 1.1855503 | 1.1204791 | [0.917, 1.322] | Yes | 0.065 | Covered |
| Between-person | Partner BP (male) | 0.2707719 | 0.3078625 | [0.104, 0.511] | Yes | 0.037 | Covered |
| Between-person | Actor BP (female) | 1.4298780 | 1.3697538 | [1.156, 1.594] | Yes | 0.060 | Covered |
| Between-person | Partner BP (female) | 0.5291829 | 0.5924361 | [0.351, 0.817] | Yes | 0.063 | Covered |
| Within-person | Actor WP (male) | 0.2000000 | 0.1812234 | [0.135, 0.227] | Yes | 0.019 | Covered |
| Within-person | Partner WP (male) | 0.1000000 | 0.1270781 | [0.079, 0.176] | Yes | 0.027 | Covered |
| Within-person | Actor WP (female) | 0.4000000 | 0.4205947 | [0.390, 0.451] | Yes | 0.021 | Covered |
| Within-person | Partner WP (female) | 0.2000000 | 0.1713545 | [0.143, 0.200] | Yes | 0.029 | Covered |
| Random effects (SD) | Couple intercept SD (male) | 0.7919045 | 0.8495693 | [0.740, 0.984] | Yes | 0.058 | Covered |
| Random effects (SD) | Couple intercept SD (female) | 0.8979616 | 0.9154796 | [0.793, 1.055] | Yes | 0.018 | Covered |
| Random effects (SD) | Time slope SD (male) | 0.0149848 | 0.0167471 | [0.014, 0.020] | Yes | 0.002 | Covered |
| Random effects (SD) | Time slope SD (female) | 0.0150338 | 0.0164231 | [0.014, 0.019] | Yes | 0.001 | Covered |
| Random effects (SD) | Actor WP slope SD (male) | 0.1006336 | 0.0899573 | [0.021, 0.153] | Yes | 0.011 | Covered |
| Random effects (SD) | Partner WP slope SD (male) | 0.0806085 | 0.1344609 | [0.055, 0.201] | Yes | 0.054 | Covered |
| Random effects (SD) | Actor WP slope SD (female) | 0.0955935 | 0.0759606 | [0.019, 0.120] | Yes | 0.020 | Covered |
| Random effects (SD) | Partner WP slope SD (female) | 0.0678586 | 0.0542037 | [0.005, 0.102] | Yes | 0.014 | Covered |
| Random effects (cor) | cor(Intercept (male), Intercept (female)) | 0.1803231 | 0.1521969 | [-0.042, 0.335] | Yes | 0.028 | Covered |
| Random effects (cor) | cor(Intercept (male), Time slope (male)) | 0.5083773 | 0.4708544 | [0.283, 0.632] | Yes | 0.038 | Covered |
| Random effects (cor) | cor(Intercept (male), Time slope (female)) | 0.1921701 | 0.1708417 | [-0.033, 0.365] | Yes | 0.021 | Covered |
| Random effects (cor) | cor(Intercept (male), Actor WP slope (male)) | 0.2262539 | 0.3543439 | [-0.059, 0.696] | Yes | 0.128 | Covered |
| Random effects (cor) | cor(Intercept (male), Partner WP slope (male)) | 0.0953425 | 0.0605446 | [-0.260, 0.378] | Yes | 0.035 | Covered |
| Random effects (cor) | cor(Intercept (male), Actor WP slope (female)) | 0.0398182 | -0.0614619 | [-0.436, 0.294] | Yes | 0.101 | Covered |
| Random effects (cor) | cor(Intercept (male), Partner WP slope (female)) | 0.3479821 | 0.2520320 | [-0.206, 0.629] | Yes | 0.096 | Covered |
| Random effects (cor) | cor(Intercept (female), Time slope (male)) | 0.0526005 | 0.0061449 | [-0.201, 0.221] | Yes | 0.046 | Covered |
| Random effects (cor) | cor(Intercept (female), Time slope (female)) | 0.6138595 | 0.4831640 | [0.303, 0.636] | Yes | 0.131 | Covered |
| Random effects (cor) | cor(Intercept (female), Actor WP slope (male)) | 0.1319990 | 0.1866849 | [-0.225, 0.569] | Yes | 0.055 | Covered |
| Random effects (cor) | cor(Intercept (female), Partner WP slope (male)) | -0.0241164 | 0.1010175 | [-0.221, 0.429] | Yes | 0.125 | Covered |
| Random effects (cor) | cor(Intercept (female), Actor WP slope (female)) | 0.1385319 | 0.2046311 | [-0.172, 0.549] | Yes | 0.066 | Covered |
| Random effects (cor) | cor(Intercept (female), Partner WP slope (female)) | -0.0263927 | 0.0667585 | [-0.371, 0.485] | Yes | 0.093 | Covered |
| Random effects (cor) | cor(Time slope (male), Time slope (female)) | 0.1339757 | -0.0304431 | [-0.257, 0.190] | Yes | 0.164 | Covered |
| Random effects (cor) | cor(Time slope (male), Actor WP slope (male)) | 0.2400640 | 0.3130248 | [-0.133, 0.682] | Yes | 0.073 | Covered |
| Random effects (cor) | cor(Time slope (male), Partner WP slope (male)) | 0.0918211 | 0.0427317 | [-0.313, 0.397] | Yes | 0.049 | Covered |
| Random effects (cor) | cor(Time slope (male), Actor WP slope (female)) | 0.0024579 | -0.1337158 | [-0.512, 0.253] | Yes | 0.136 | Covered |
| Random effects (cor) | cor(Time slope (male), Partner WP slope (female)) | 0.2635152 | 0.1105994 | [-0.343, 0.527] | Yes | 0.153 | Covered |
| Random effects (cor) | cor(Time slope (female), Actor WP slope (male)) | 0.0711586 | 0.2634154 | [-0.166, 0.641] | Yes | 0.192 | Covered |
| Random effects (cor) | cor(Time slope (female), Partner WP slope (male)) | 0.0441926 | 0.1093859 | [-0.225, 0.448] | Yes | 0.065 | Covered |
| Random effects (cor) | cor(Time slope (female), Actor WP slope (female)) | 0.1380171 | -0.0687735 | [-0.428, 0.300] | Yes | 0.207 | Covered |
| Random effects (cor) | cor(Time slope (female), Partner WP slope (female)) | 0.0279326 | 0.1470529 | [-0.313, 0.558] | Yes | 0.119 | Covered |
| Random effects (cor) | cor(Actor WP slope (male), Partner WP slope (male)) | 0.1128802 | 0.2345585 | [-0.266, 0.682] | Yes | 0.122 | Covered |
| Random effects (cor) | cor(Actor WP slope (male), Actor WP slope (female)) | 0.0478755 | -0.1584750 | [-0.610, 0.370] | Yes | 0.206 | Covered |
| Random effects (cor) | cor(Actor WP slope (male), Partner WP slope (female)) | 0.1135981 | 0.1524683 | [-0.386, 0.639] | Yes | 0.039 | Covered |
| Random effects (cor) | cor(Partner WP slope (male), Actor WP slope (female)) | -0.0107839 | -0.0987572 | [-0.562, 0.404] | Yes | 0.088 | Covered |
| Random effects (cor) | cor(Partner WP slope (male), Partner WP slope (female)) | 0.1384787 | 0.0602770 | [-0.484, 0.573] | Yes | 0.078 | Covered |
| Random effects (cor) | cor(Actor WP slope (female), Partner WP slope (female)) | 0.1494376 | 0.1005532 | [-0.442, 0.599] | Yes | 0.049 | Covered |
| Residuals | Same-day residual correlation | 0.0366176 | 0.0413748 | [0.015, 0.068] | Yes | 0.005 | Covered |
| Residuals | Residual SD (male) (exp) | 1.1210517 | 1.1147509 | [1.093, 1.136] | Yes | 0.006 | Covered |
| Residuals | Residual SD (female) (exp) | 0.7173353 | 0.7181891 | [0.705, 0.732] | Yes | 0.001 | Covered |
Partners are exchangeable, i.e., not systematically different.
But they should still be allowed to vary within each couple, while being correlated.
For illustration, we now prepare the same female-male dyads under an exchangeability constraint. This is a modelling constraint/test, not a claim that the data-generating dyads are truly exchangeable. Starting again from the raw data makes that changed assumption explicit.
df_exchangeable <- prepare_interdep_data(
data = df,
group = coupleID,
member = userID,
role = gender,
predictors = communication,
model_type = c("apim", "dim"),
include_compositions = "female-male",
set_exchangeable_compositions = "female-male",
seed = 123
) %>%
rename_interdep_for_tutorial(
predictor = "communication"
)| userID | coupleID | gender | communication | satisfaction | .i_composition | .i_composition_role | .i_is_female_x_male | idiff | communication_actor | communication_partner | communication_dyad_mean_gmc | communication_dyad_dev |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1_1 | 1 | female | 4.851107 | 4.841332 | female_x_male | female_x_male | 1 | -1 | 4.851107 | 6.004533 | 0.4213527 | -0.5767130 |
| 1_2 | 1 | male | 6.004533 | 5.577165 | female_x_male | female_x_male | 1 | 1 | 6.004533 | 4.851107 | 0.4213527 | 0.5767130 |
| 2_1 | 2 | female | 5.881321 | 7.248741 | female_x_male | female_x_male | 1 | -1 | 5.881321 | 6.433723 | 1.1510550 | -0.2762013 |
| 2_2 | 2 | male | 6.433723 | 6.960293 | female_x_male | female_x_male | 1 | 1 | 6.433723 | 5.881321 | 1.1510550 | 0.2762013 |
| 3_1 | 3 | female | 4.283971 | 6.928699 | female_x_male | female_x_male | 1 | -1 | 4.283971 | 5.516060 | -0.1064514 | -0.6160447 |
| 3_2 | 3 | male | 5.516060 | 6.077688 | female_x_male | female_x_male | 1 | 1 | 5.516060 | 4.283971 | -0.1064514 | 0.6160447 |
For the fixed effects, we can pool the intercepts is_male and is_female by simply reintroducing a single shared intercept 1.
For the actor and partner effects, we can remove the interactions to get a pooled estimate.
For the residual, we want to constrain both partners’ variances to be equal. Often, this is attempted via: (1 | coupleID).
dispformula = ~ 0, the shared random intercept would imply a correlation near 1, which is too restrictive.One solution in glmmTMB is the following:
model_exch_apim_glmmtmb <- glmmTMB(
satisfaction ~
# Pooled single intercept
1 +
# Pooled actor and partner effects
communication_actor + communication_partner +
# Independent common and member-deviation terms parameterize a homogeneous
# two-member covariance using interdep's arbitrary contrast.
(1 | coupleID) + (0 + idiff | coupleID)
# Since we model residuals via a cs random effects structure, glmmTMB requires us to remove an
# additional independent residual
, dispformula = ~ 0
, data = df_exchangeable
)This successfully returns a single pooled residual variance, and a correlation between partners:
Family: gaussian ( identity )
Formula: satisfaction ~ 1 + communication_actor + communication_partner +
(1 | coupleID) + (0 + idiff | coupleID)
Dispersion: ~0
Data: df_exchangeable
AIC BIC logLik -2*log(L) df.resid
4026.6 4051.7 -2008.3 4016.6 1095
Random effects:
Conditional model:
Groups Name Variance Std.Dev.
coupleID (Intercept) 1.6146 1.2707
coupleID.1 idiff 0.7881 0.8877
Number of obs: 1100, groups: coupleID, 550
Conditional model:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -4.70971 0.22332 -21.09 < 2e-16 ***
communication_actor 1.71990 0.03177 54.13 < 2e-16 ***
communication_partner 0.24137 0.03177 7.60 3.04e-14 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1The gender-coded cross-sectional version does not transfer directly to ILD.
For repeated measures, we instead index the homogeneous two-partner residual structure at the couple-day level, as shown later.
In brms, this issue can be solved more easily with a native residual correlation structure and one pooled residual variance:
formula_exch_apim <- bf(
satisfaction ~ 1 +
communication_actor + communication_partner +
# To achieve a homogeneous two-partner covariance structure we use an
# unstructured residual correlation over the two member positions.
unstr(time = idiff, gr = coupleID)
# ... but now, we do not allow individual variances anymore.
# We want one pooled residual variance. So we do not specify a sigma formula.
# This implies:
# , sigma = ~ 1
)
priors_exch_apim <- c(
prior(normal(2, 10), class = "Intercept"),
prior(normal(0, 5), class = "b"),
prior(student_t(3, 0, 1.5), class = "sigma"),
prior(lkj(2), class = "cortime")
)
model_exch_apim <- brm(
formula = formula_exch_apim,
data = df_exchangeable,
family = gaussian(link = identity),
prior = priors_exch_apim,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'example1_ind_apim') # Cache the model
)| Est. | 95% CI | Rhat | Bulk_ESS | Tail_ESS | |
|---|---|---|---|---|---|
| Fixed Effects | |||||
| Intercept | -4.71 | [-5.13, -4.26] | 1.004 | 4681 | 2769 |
| communication_actor | 1.72 | [ 1.66, 1.78] | 1.002 | 4425 | 2895 |
| communication_partner | 0.24 | [ 0.18, 0.30] | 1.000 | 4971 | 3242 |
| Residual Structure | |||||
| cortime(-1,1) | 0.34 | [0.26, 0.42] | 1.000 | 4665 | 3140 |
| sigma | 1.55 | [1.49, 1.63] | 1.001 | 4114 | 3191 |
Leave-one-out (loo) cross-validation compares out-of-sample predictive accuracy. It can be meaningful for non-nested models when they are fit to the same outcome and observations, and when the pointwise log-likelihoods are comparable.
| Model | elpd_diff | se_diff |
|---|---|---|
| model_dist_apim | 0.0 | 0.0 |
| model_exch_apim | -172.9 | 16.5 |
The difference in predictive accuracy, as indexed by Expected Log Predictive Density (ELPD-LOO), suggests that ‘model_dist_apim_b’ is the best model (ELPD = -1806.42), followed by ‘model_exch_apim’ (diff-ELPD = -172.85 +- 16.53, p < .001).
See: documentation of “report”
The same prepare_interdep_data() call generated the DIM columns directly from the raw long-format observations.
| userID | coupleID | satisfaction | communication | communication_dyad_mean_gmc | communication_dyad_dev | idiff |
|---|---|---|---|---|---|---|
| 1_1 | 1 | 4.841332 | 4.851107 | 0.4213527 | -0.5767130 | -1 |
| 1_2 | 1 | 5.577165 | 6.004533 | 0.4213527 | 0.5767130 | 1 |
| 2_1 | 2 | 7.248741 | 5.881321 | 1.1510550 | -0.2762013 | -1 |
| 2_2 | 2 | 6.960293 | 6.433723 | 1.1510550 | 0.2762013 | 1 |
| 3_1 | 3 | 6.928699 | 4.283971 | -0.1064514 | -0.6160447 | -1 |
| 3_2 | 3 | 6.077688 | 5.516060 | -0.1064514 | 0.6160447 | 1 |
Decompose communication variance into:
Same assumption about exchangeability as in the APIM
communication_dyad_mean_gmc is the grand-mean-centered dyad mean, while communication_dyad_dev is each member’s deviation from that dyad mean.
| userID | coupleID | satisfaction | communication_dyad_mean_gmc | communication_dyad_dev | idiff |
|---|---|---|---|---|---|
| 1_1 | 1 | 4.841332 | 0.4213527 | -0.5767130 | -1 |
| 1_2 | 1 | 5.577165 | 0.4213527 | 0.5767130 | 1 |
| 2_1 | 2 | 7.248741 | 1.1510550 | -0.2762013 | -1 |
| 2_2 | 2 | 6.960293 | 1.1510550 | 0.2762013 | 1 |
| 3_1 | 3 | 6.928699 | -0.1064514 | -0.6160447 | -1 |
| 3_2 | 3 | 6.077688 | -0.1064514 | 0.6160447 | 1 |
formula_exch_dim <- bf(
satisfaction ~ 1 +
communication_dyad_mean_gmc + communication_dyad_dev +
unstr(time = idiff, gr = coupleID)
# sigma ~ 1 is implied.
)
priors_exch_dim <- c(
prior(normal(2, 10), class = "Intercept"),
prior(normal(0, 5), class = "b"),
prior(student_t(3, 0, 1.5), class = "sigma"),
prior(lkj(2), class = "cortime")
)
model_exch_dim <- brm(
formula = formula_exch_dim,
data = df_exchangeable,
family = gaussian(link = identity),
prior = priors_exch_dim,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'example1_ind_dim') # Cache the model
)| Est. | 95% CI | Rhat | Bulk_ESS | Tail_ESS | |
|---|---|---|---|---|---|
| Fixed Effects | |||||
| Intercept | 5.11 | [5.00, 5.22] | 1.001 | 4390 | 2769 |
| communication_dyad_mean_gmc | 1.96 | [1.87, 2.05] | 1.003 | 4658 | 3120 |
| communication_dyad_dev | 1.48 | [1.39, 1.57] | 1.002 | 4285 | 2998 |
| Residual Structure | |||||
| cortime(-1,1) | 0.34 | [0.27, 0.42] | 1.001 | 4164 | 2873 |
| sigma | 1.55 | [1.49, 1.62] | 1.000 | 4560 | 3396 |
The APIM uses raw actor and partner predictors, whereas the generated DIM dyad mean is grand-mean centered. The two fitted models therefore use different origins: their slope and covariance parameters are directly comparable, but their printed intercepts are not. For the equations below, we shift the APIM intercept to average communication:
\[ b_{0,\,APIM(GMC)} = b_{0,\,APIM(raw)} + \bar{x} (b_{actor} + b_{partner}). \]
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | -4.71 | [-5.13, -4.26] |
| communication_actor | 1.72 | [ 1.66, 1.78] |
| communication_partner | 0.24 | [ 0.18, 0.30] |
| Residual Structure | ||
| cortime(-1,1) | 0.34 | [0.26, 0.42] |
| sigma | 1.55 | [1.49, 1.63] |
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | 5.11 | [5.00, 5.22] |
| communication_dyad_mean_gmc | 1.96 | [1.87, 2.05] |
| communication_dyad_dev | 1.48 | [1.39, 1.57] |
| Residual Structure | ||
| cortime(-1,1) | 0.34 | [0.27, 0.42] |
| sigma | 1.55 | [1.49, 1.62] |
\[b_{actor} + b_{partner} = b_{cbc}\] \[b_{actor} - b_{partner} = b_{cwc}\]
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | -4.71 | [-5.13, -4.26] |
| communication_actor | 1.72 | [ 1.66, 1.78] |
| communication_partner | 0.24 | [ 0.18, 0.30] |
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | 5.11 | [5.00, 5.22] |
| communication_dyad_mean_gmc | 1.96 | [1.87, 2.05] |
| communication_dyad_dev | 1.48 | [1.39, 1.57] |
Top sliders: grand-mean-centered Communication for Actor and Partner (APIM).
Bottom sliders: reparameterized communication — Centered Between-Couple (xcbc) and Centered Within-Couple (xcwc) (DIM).
\[b_{cbc} = b_{actor} + b_{partner}\]
\[b_{cwc} = b_{actor} - b_{partner}\]
\[b_{actor} = \frac{b_{cbc} + b_{cwc}}{2}\]
\[b_{partner} = \frac{b_{cbc} - b_{cwc}}{2}\]
Example: using APIM coefficients to obtain DIM coefficients and computing Credible Intervals of DIM coefficients.
Estimate Est.Error CI.Lower CI.Upper
1 1.96 0.04 1.87 2.04
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | -4.71 | [-5.13, -4.26] |
| communication_actor | 1.72 | [ 1.66, 1.78] |
| communication_partner | 0.24 | [ 0.18, 0.30] |
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | 5.11 | [5.00, 5.22] |
| communication_dyad_mean_gmc | 1.96 | [1.87, 2.05] |
| communication_dyad_dev | 1.48 | [1.39, 1.57] |
In the distinguishable case, the Dyadic Score Model (DSM) (e.g., Stadler et al., 2023) is equivalent to the APIM! (Bolger et al., 2025; Iida et al., 2018)
We start again from the raw longitudinal data and omit role, which tells interdep to treat the two members as exchangeable. Requesting both model types creates the APIM and DIM predictors in the same model-ready object.
df_long_dim <- prepare_interdep_data(
data = df_long,
group = coupleID,
member = userID,
time = diaryday,
predictors = provided_support,
model_type = c("apim", "dim"),
seed = 123
) %>%
rename_interdep_for_tutorial(
# Keep interdep's cwp_actor/cbp_partner component ordering.
predictor = "provided_support"
) %>%
mutate(
diaryday_c = diaryday - mean(diaryday)
)| userID | coupleID | diaryday | gender | closeness | provided_support | .i_composition | .i_composition_role | .i_is_assumed_exchangeable | idiff | provided_support_cwp | provided_support_cbp | provided_support_cwp_actor | provided_support_cwp_partner | provided_support_cbp_actor | provided_support_cbp_partner | provided_support_cwp_dyad_mean | provided_support_cwp_dev | provided_support_cbp_dyad_mean | provided_support_cbp_dev | diaryday_c |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31_1 | 31 | 0 | female | 5.47 | 6.02 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 0.41 | 0.65 | 0.41 | 1.54 | 0.65 | 0.58 | 0.97 | -0.57 | 0.61 | 0.04 | -27 |
| 31_1 | 31 | 1 | female | 7.69 | 6.13 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 0.51 | 0.65 | 0.51 | 1.4 | 0.65 | 0.58 | 0.96 | -0.44 | 0.61 | 0.04 | -26 |
| 31_1 | 31 | 2 | female | 5.83 | 6.1 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 0.49 | 0.65 | 0.49 | 0.79 | 0.65 | 0.58 | 0.64 | -0.15 | 0.61 | 0.04 | -25 |
| 31_1 | 31 | 3 | female | 6.55 | 6.99 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 1.38 | 0.65 | 1.38 | -0.15 | 0.65 | 0.58 | 0.61 | 0.77 | 0.61 | 0.04 | -24 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 31_2 | 31 | 0 | male | 5.19 | 7.09 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | 1.54 | 0.58 | 1.54 | 0.41 | 0.58 | 0.65 | 0.97 | 0.57 | 0.61 | -0.04 | -27 |
| 31_2 | 31 | 1 | male | 6.96 | 6.95 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | 1.4 | 0.58 | 1.4 | 0.51 | 0.58 | 0.65 | 0.96 | 0.44 | 0.61 | -0.04 | -26 |
| 31_2 | 31 | 2 | male | 6.9 | 6.34 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | 0.79 | 0.58 | 0.79 | 0.49 | 0.58 | 0.65 | 0.64 | 0.15 | 0.61 | -0.04 | -25 |
| 31_2 | 31 | 3 | male | 7.59 | 5.39 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | -0.15 | 0.58 | -0.15 | 1.38 | 0.58 | 0.65 | 0.61 | -0.77 | 0.61 | -0.04 | -24 |
The generated provided_support_cwp_dyad_mean and provided_support_cwp_dev columns form the within-person DIM. The corresponding provided_support_cbp_dyad_mean and provided_support_cbp_dev columns form the between-person DIM.
idiff ApproachThe idiff alias refers to interdep’s generated arbitrary -1/+1 member contrast. The fixed seed makes that arbitrary assignment reproducible.
idiff Approach| userID | coupleID | diaryday | gender | closeness | provided_support | .i_composition | .i_composition_role | .i_is_assumed_exchangeable | idiff | provided_support_cwp | provided_support_cbp | provided_support_cwp_actor | provided_support_cwp_partner | provided_support_cbp_actor | provided_support_cbp_partner | provided_support_cwp_dyad_mean | provided_support_cwp_dev | provided_support_cbp_dyad_mean | provided_support_cbp_dev | diaryday_c |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31_1 | 31 | 0 | female | 5.47 | 6.02 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 0.41 | 0.65 | 0.41 | 1.54 | 0.65 | 0.58 | 0.97 | -0.57 | 0.61 | 0.04 | -27 |
| 31_1 | 31 | 1 | female | 7.69 | 6.13 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 0.51 | 0.65 | 0.51 | 1.4 | 0.65 | 0.58 | 0.96 | -0.44 | 0.61 | 0.04 | -26 |
| 31_1 | 31 | 2 | female | 5.83 | 6.1 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 0.49 | 0.65 | 0.49 | 0.79 | 0.65 | 0.58 | 0.64 | -0.15 | 0.61 | 0.04 | -25 |
| 31_1 | 31 | 3 | female | 6.55 | 6.99 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 1.38 | 0.65 | 1.38 | -0.15 | 0.65 | 0.58 | 0.61 | 0.77 | 0.61 | 0.04 | -24 |
| 31_1 | 31 | 4 | female | 7.03 | 6.7 | assumed_exchangeable | assumed_exchangeable | 1 | -1 | 1.08 | 0.65 | 1.08 | -0.36 | 0.65 | 0.58 | 0.36 | 0.72 | 0.61 | 0.04 | -23 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 31_2 | 31 | 0 | male | 5.19 | 7.09 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | 1.54 | 0.58 | 1.54 | 0.41 | 0.58 | 0.65 | 0.97 | 0.57 | 0.61 | -0.04 | -27 |
| 31_2 | 31 | 1 | male | 6.96 | 6.95 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | 1.4 | 0.58 | 1.4 | 0.51 | 0.58 | 0.65 | 0.96 | 0.44 | 0.61 | -0.04 | -26 |
| 31_2 | 31 | 2 | male | 6.9 | 6.34 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | 0.79 | 0.58 | 0.79 | 0.49 | 0.58 | 0.65 | 0.64 | 0.15 | 0.61 | -0.04 | -25 |
| 31_2 | 31 | 3 | male | 7.59 | 5.39 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | -0.15 | 0.58 | -0.15 | 1.38 | 0.58 | 0.65 | 0.61 | -0.77 | 0.61 | -0.04 | -24 |
| 31_2 | 31 | 4 | male | 7.55 | 5.19 | assumed_exchangeable | assumed_exchangeable | 1 | 1 | -0.36 | 0.58 | -0.36 | 1.08 | 0.58 | 0.65 | 0.36 | -0.72 | 0.61 | -0.04 | -23 |
model_exch_long_apim_glmmtmb <- glmmTMB(
closeness ~ 1 +
diaryday_c +
# Within-person APIM
provided_support_cwp_actor +
provided_support_cwp_partner +
# Equivalent to within-person DIM:
# provided_support_cwp_dyad_mean +
# provided_support_cwp_dev +
# Between-person APIM
provided_support_cbp_actor +
provided_support_cbp_partner +
# Equivalent to between-person DIM
# provided_support_cbp_dev +
# provided_support_cbp_dyad_mean +
# Dyad-Level intercept and slopes for time-varying predictors
(1 + diaryday_c + provided_support_cwp_actor + provided_support_cwp_partner | coupleID) +
# Both partners' deviations from these dyad-level means and slopes.
# Note: separate random-effects block to make them uncorrelated with dyad-level REs.
(0 + idiff +
I(idiff * diaryday_c) +
I(idiff * provided_support_cwp_actor) +
I(idiff * provided_support_cwp_partner) | coupleID) +
# After accounting for stable differences between partners, they may still be
# non-independent each day due to external occurrences and daily exchange processes
# not captured in the fixed effects. Thus, residuals are not yet independent.
# Homogeneous same-day residual covariance between partners:
(1 | coupleID:diaryday) + (0 + idiff | coupleID:diaryday)
# We remove the additional independent residual variance
# (only if the family has a native residual!)
, dispformula = ~ 0
, data = df_long_dim
)In this example, the glmmTMB model does not converge properly. The random-effects structure is too complex for the information available in these data, so we would have to simplify it. Instead, we use brms below.
In brms, the same model can be estimated as:
Unlike the distinguishable longitudinal model, this exchangeable specification keeps one pooled residual sigma (sigma ~ 1 is implied). Its residual SD prior therefore targets class = "sigma" directly.
formula_exch_long_apim <- bf(
closeness ~ 1 +
diaryday_c +
# Within-person APIM
provided_support_cwp_actor +
provided_support_cwp_partner +
# Between-person APIM
provided_support_cbp_actor +
provided_support_cbp_partner +
# Dyad-Level intercept and slopes for time-varying predictors
(1 + diaryday_c + provided_support_cwp_actor + provided_support_cwp_partner | coupleID) +
# Both partners' deviations from these dyad-level means and slopes.
# Note: separate random-effects block to make them uncorrelated with dyad-level REs.
(0 + idiff +
I(idiff * diaryday_c) +
I(idiff * provided_support_cwp_actor) +
I(idiff * provided_support_cwp_partner) | coupleID) +
# Modelling Level 1 residuals in brms:
unstr(time = idiff, gr = coupleID:diaryday)
# Again, no need to model heterogeneous residual variances (sigma)
# Implied: sigma = ~ 1
)
priors_exch_long_apim <- c(
prior(normal(4, 2), class = "Intercept"),
prior(normal(0, 2), class = "b"),
prior(exponential(1), class = "sd"),
prior(lkj(2), class = "cor"),
prior(student_t(3, 0, 1.5), class = "sigma"),
prior(lkj(2), class = "cortime")
)
model_exch_long_apim <- brm(
formula = formula_exch_long_apim,
data = df_long_dim,
family = gaussian(link = identity),
prior = priors_exch_long_apim,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'model_apim_ind_long_apim')
)The equivalent DIM specification:
# DIM Version
formula_exch_long_dim <- bf(
closeness ~ 1 +
diaryday_c +
provided_support_cwp_dyad_mean +
provided_support_cwp_dev +
provided_support_cbp_dyad_mean +
provided_support_cbp_dev +
(1 + diaryday_c + provided_support_cwp_actor + provided_support_cwp_partner | coupleID) +
(0 + idiff +
I(idiff * diaryday_c) +
I(idiff * provided_support_cwp_actor) +
I(idiff * provided_support_cwp_partner) | coupleID) +
unstr(time = idiff, gr = coupleID:diaryday)
)
priors_exch_long_dim <- c(
prior(normal(4, 2), class = "Intercept"),
prior(normal(0, 2), class = "b"),
prior(exponential(1), class = "sd"),
prior(lkj(2), class = "cor"),
prior(student_t(3, 0, 1.5), class = "sigma"),
prior(lkj(2), class = "cortime")
)
model_exch_long_dim <- brm(
formula = formula_exch_long_dim,
data = df_long_dim,
family = gaussian(link = identity),
prior = priors_exch_long_dim,
chains = 4,
cores = 4,
iter = 2000,
warmup = 1000,
seed = 123,
file = file.path('brms_cache', 'model_apim_ind_long_dim')
)idiffFor appropriate exchangeable random effects, we can use mean-deviation idiff coding (del Rosario & West, 2025; Kenny & Ackerman, 2023):
(1 | coupleID)Exchangeability is retained: if partners are flipped, the result is the same.
(del Rosario & West (2025) provide practical guidance on how to reduce the random effects structure in case of non-convergence.)
idiff ComponentsFor two partners coded with idiff = +1 and idiff = -1, the glmmTMB structure (1 | coupleID:diaryday) + (0 + idiff | coupleID:diaryday) uses independent common and member-deviation components to imply a homogeneous two-partner residual covariance.
This parameterization assumes exactly two rows per couple-day, independent common/couple-mean and idiff deviation random-effect blocks, and dispformula = ~ 0.
The model code uses these common and deviation components directly, so no manually constructed partner-position indicators are required.
It also does not need back-transformation.
Comparing APIM (left) and DIM (right) output of fixed effects.
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | 5.10 | [4.97, 5.24] |
| diaryday_c | 0.00 | [0.00, 0.01] |
| provided_support_cwp_actor | 0.30 | [0.27, 0.33] |
| provided_support_cwp_partner | 0.15 | [0.12, 0.18] |
| provided_support_cbp_actor | 1.32 | [1.14, 1.48] |
| provided_support_cbp_partner | 0.38 | [0.21, 0.56] |
| Est. | 95% CI | |
|---|---|---|
| Fixed Effects | ||
| Intercept | 5.11 | [4.98, 5.23] |
| diaryday_c | 0.00 | [0.00, 0.01] |
| provided_support_cwp_dyad_mean | 0.45 | [0.41, 0.49] |
| provided_support_cwp_dev | 0.15 | [0.11, 0.19] |
| provided_support_cbp_dyad_mean | 1.70 | [1.55, 1.86] |
| provided_support_cbp_dev | 0.93 | [0.61, 1.21] |
APIM Results in Detail:
| Est. | 95% CI | Rhat | Bulk_ESS | Tail_ESS | |
|---|---|---|---|---|---|
| Fixed Effects | |||||
| Intercept | 5.10 | [4.97, 5.24] | 1.017 | 175 | 225 |
| diaryday_c | 0.00 | [0.00, 0.01] | 1.005 | 463 | 1553 |
| provided_support_cwp_actor | 0.30 | [0.27, 0.33] | 1.001 | 2845 | 2572 |
| provided_support_cwp_partner | 0.15 | [0.12, 0.18] | 1.000 | 3249 | 2819 |
| provided_support_cbp_actor | 1.32 | [1.14, 1.48] | 1.017 | 171 | 286 |
| provided_support_cbp_partner | 0.38 | [0.21, 0.56] | 1.005 | 212 | 492 |
| Random Effects | |||||
| sd(Intercept) | 0.67 | [ 0.58, 0.77] | 1.022 | 389 | 702 |
| sd(diaryday_c) | 0.01 | [ 0.01, 0.01] | 1.001 | 1581 | 2780 |
| sd(provided_support_cwp_actor) | 0.05 | [ 0.01, 0.09] | 1.004 | 990 | 953 |
| sd(provided_support_cwp_partner) | 0.08 | [ 0.04, 0.12] | 1.002 | 1158 | 1197 |
| sd(idiff) | 0.84 | [ 0.74, 0.97] | 1.049 | 218 | 539 |
| sd(IidiffMUdiaryday_c) | 0.01 | [ 0.01, 0.02] | 1.010 | 1152 | 1947 |
| sd(IidiffMUprovided_support_cwp_actor) | 0.14 | [ 0.11, 0.18] | 1.004 | 1154 | 2284 |
| sd(IidiffMUprovided_support_cwp_partner) | 0.07 | [ 0.01, 0.11] | 1.004 | 754 | 627 |
| cor(Intercept,diaryday_c) | 0.56 | [ 0.38, 0.70] | 1.004 | 1129 | 1909 |
| cor(Intercept,provided_support_cwp_actor) | 0.49 | [ 0.02, 0.82] | 1.002 | 2958 | 2405 |
| cor(diaryday_c,provided_support_cwp_actor) | 0.37 | [-0.10, 0.76] | 1.002 | 2789 | 2668 |
| cor(Intercept,provided_support_cwp_partner) | 0.21 | [-0.12, 0.53] | 1.001 | 2870 | 2182 |
| cor(diaryday_c,provided_support_cwp_partner) | 0.19 | [-0.19, 0.54] | 1.003 | 2961 | 2724 |
| cor(provided_support_cwp_actor,provided_support_cwp_partner) | 0.35 | [-0.27, 0.80] | 1.002 | 687 | 1343 |
| cor(idiff,IidiffMUdiaryday_c) | 0.60 | [ 0.44, 0.73] | 1.014 | 788 | 1455 |
| cor(idiff,IidiffMUprovided_support_cwp_actor) | 0.66 | [ 0.46, 0.81] | 1.004 | 1299 | 1965 |
| cor(IidiffMUdiaryday_c,IidiffMUprovided_support_cwp_actor) | 0.42 | [ 0.15, 0.63] | 1.008 | 1619 | 2359 |
| cor(idiff,IidiffMUprovided_support_cwp_partner) | 0.12 | [-0.29, 0.53] | 1.000 | 2816 | 1776 |
| cor(IidiffMUdiaryday_c,IidiffMUprovided_support_cwp_partner) | 0.11 | [-0.32, 0.55] | 1.003 | 3148 | 1966 |
| cor(IidiffMUprovided_support_cwp_actor,IidiffMUprovided_support_cwp_partner) | 0.34 | [-0.17, 0.76] | 1.000 | 1647 | 2123 |
| cortime(-1,1) | 0.04 | [ 0.01, 0.07] | 1.001 | 4908 | 2263 |
| Residual Structure | |||||
| sigma | 0.94 | [ 0.92, 0.95] | 1.000 | 6231 | 2668 |
We can rotate the random effects structure back to obtain the full APIM Random effects variance-covariance matrix, based on (Kenny & Ackerman, 2023). Just as we would in a SEM model with equality constraints!
For example, the within-partner variance of the random intercept is:
\[ \operatorname{Var}(u_{\text{common intercept}}) + \operatorname{Var}(u_{\text{idiff intercept}}) \]
and the cross-partner covariance of both partners’ random intercepts is:
\[ \operatorname{Var}(u_{\text{common intercept}}) - \operatorname{Var}(u_{\text{idiff intercept}}) \]
In the model output, the common-intercept variance is labelled Intercept, and the deviation-intercept variance is labelled idiff. This rotation only holds when the common/couple-mean and idiff deviation random effects are uncorrelated. This is why we put them in separate random-effects blocks.
| Intercept | diaryday_c | provided_support_cwp_actor | provided_support_cwp_partner | |
|---|---|---|---|---|
| Intercept | 1.1802004 | 0.0112332 | 0.0955767 | 0.0179927 |
| diaryday_c | 0.0112332 | 0.0003159 | 0.0010075 | 0.0002727 |
| provided_support_cwp_actor | 0.0955767 | 0.0010075 | 0.0238464 | 0.0042531 |
| provided_support_cwp_partner | 0.0179927 | 0.0002727 | 0.0042531 | 0.0118398 |
| Intercept | diaryday_c | provided_support_cwp_actor | provided_support_cwp_partner | |
|---|---|---|---|---|
| Intercept | -0.2635988 | -0.0024112 | -0.0630951 | 0.0048753 |
| diaryday_c | -0.0024112 | -0.0000434 | -0.0005769 | 0.0000857 |
| provided_support_cwp_actor | -0.0630951 | -0.0005769 | -0.0179709 | -0.0015373 |
| provided_support_cwp_partner | 0.0048753 | 0.0000857 | -0.0015373 | 0.0024947 |
| PartnerA_Intercept | PartnerA_diaryday_c | PartnerA_provided_support_cwp_actor | PartnerA_provided_support_cwp_partner | PartnerB_Intercept | PartnerB_diaryday_c | PartnerB_provided_support_cwp_actor | PartnerB_provided_support_cwp_partner | |
|---|---|---|---|---|---|---|---|---|
| PartnerA_Intercept | 1.180 | 0.011 | 0.096 | 0.018 | -0.264 | -0.002 | -0.063 | 0.005 |
| PartnerA_diaryday_c | 0.011 | 0.000 | 0.001 | 0.000 | -0.002 | 0.000 | -0.001 | 0.000 |
| PartnerA_provided_support_cwp_actor | 0.096 | 0.001 | 0.024 | 0.004 | -0.063 | -0.001 | -0.018 | -0.002 |
| PartnerA_provided_support_cwp_partner | 0.018 | 0.000 | 0.004 | 0.012 | 0.005 | 0.000 | -0.002 | 0.002 |
| PartnerB_Intercept | -0.264 | -0.002 | -0.063 | 0.005 | 1.180 | 0.011 | 0.096 | 0.018 |
| PartnerB_diaryday_c | -0.002 | 0.000 | -0.001 | 0.000 | 0.011 | 0.000 | 0.001 | 0.000 |
| PartnerB_provided_support_cwp_actor | -0.063 | -0.001 | -0.018 | -0.002 | 0.096 | 0.001 | 0.024 | 0.004 |
| PartnerB_provided_support_cwp_partner | 0.005 | 0.000 | -0.002 | 0.002 | 0.018 | 0.000 | 0.004 | 0.012 |
The helper function summarize_exchangeable_apim_brms() is defined in 00_R_Functions/ReportModels.R. It rotates every posterior draw, so the interval estimates for the transformed SDs, correlations, covariances, and residual quantities are computed on the transformed scale.
For glmmTMB, the same back-transformation is algebraically possible, but interval estimation is less direct because there are no posterior draws. One would usually transform parametric bootstrap draws or asymptotic draws from the fitted parameter covariance matrix.
| parameter | estimate | est_error | q_low | q_high |
|---|---|---|---|---|
| PartnerA_Intercept | 1.085 | 0.056 | 0.987 | 1.206 |
| PartnerA_diaryday_c | 0.018 | 0.001 | 0.016 | 0.020 |
| PartnerA_provided_support_cwp_actor | 0.153 | 0.018 | 0.119 | 0.191 |
| PartnerA_provided_support_cwp_partner | 0.107 | 0.020 | 0.066 | 0.144 |
| PartnerB_Intercept | 1.085 | 0.056 | 0.987 | 1.206 |
| PartnerB_diaryday_c | 0.018 | 0.001 | 0.016 | 0.020 |
| PartnerB_provided_support_cwp_actor | 0.153 | 0.018 | 0.119 | 0.191 |
| PartnerB_provided_support_cwp_partner | 0.107 | 0.020 | 0.066 | 0.144 |
| PartnerA_Intercept | PartnerA_diaryday_c | PartnerA_provided_support_cwp_actor | PartnerA_provided_support_cwp_partner | PartnerB_Intercept | PartnerB_diaryday_c | PartnerB_provided_support_cwp_actor | PartnerB_provided_support_cwp_partner | |
|---|---|---|---|---|---|---|---|---|
| PartnerA_Intercept | 1.000 | 0.582 | 0.570 | 0.152 | -0.223 | -0.125 | -0.376 | 0.041 |
| PartnerA_diaryday_c | 0.582 | 1.000 | 0.367 | 0.141 | -0.125 | -0.137 | -0.210 | 0.044 |
| PartnerA_provided_support_cwp_actor | 0.570 | 0.367 | 1.000 | 0.253 | -0.376 | -0.210 | -0.754 | -0.091 |
| PartnerA_provided_support_cwp_partner | 0.152 | 0.141 | 0.253 | 1.000 | 0.041 | 0.044 | -0.091 | 0.211 |
| PartnerB_Intercept | -0.223 | -0.125 | -0.376 | 0.041 | 1.000 | 0.582 | 0.570 | 0.152 |
| PartnerB_diaryday_c | -0.125 | -0.137 | -0.210 | 0.044 | 0.582 | 1.000 | 0.367 | 0.141 |
| PartnerB_provided_support_cwp_actor | -0.376 | -0.210 | -0.754 | -0.091 | 0.570 | 0.367 | 1.000 | 0.253 |
| PartnerB_provided_support_cwp_partner | 0.041 | 0.044 | -0.091 | 0.211 | 0.152 | 0.141 | 0.253 | 1.000 |
section_runs <- rle(a$reporting_summary$section)
section_ends <- cumsum(section_runs$lengths)
section_starts <- section_ends - section_runs$lengths + 1
section_labels <- c(
"Fixed effects" = "Fixed Effects",
"Back-transformed random-effect SDs" = "Back-Transformed SDs",
"Back-transformed random-effect correlations" = "Back-Transformed Correlations",
"Residual structure" = "Residual Structure"
)
reporting_rows_to_pack <- Map(c, section_starts, section_ends)
names(reporting_rows_to_pack) <- unname(section_labels[section_runs$values])
print_df(
a$reporting_summary %>%
mutate(across(where(is.numeric), ~ round(.x, 3))),
rows_to_pack = reporting_rows_to_pack,
scroll_height = '50vh'
)| section | parameter | estimate | est_error | q_low | q_high |
|---|---|---|---|---|---|
| Fixed Effects | |||||
| Fixed effects | Intercept | 5.102 | 0.070 | 4.966 | 5.240 |
| Fixed effects | diaryday_c | 0.004 | 0.001 | 0.001 | 0.006 |
| Fixed effects | provided_support_cwp_actor | 0.300 | 0.014 | 0.273 | 0.328 |
| Fixed effects | provided_support_cwp_partner | 0.149 | 0.015 | 0.120 | 0.177 |
| Fixed effects | provided_support_cbp_actor | 1.316 | 0.088 | 1.140 | 1.483 |
| Fixed effects | provided_support_cbp_partner | 0.382 | 0.086 | 0.212 | 0.557 |
| Back-Transformed SDs | |||||
| Back-transformed random-effect SDs | PartnerA_Intercept | 1.085 | 0.056 | 0.987 | 1.206 |
| Back-transformed random-effect SDs | PartnerA_diaryday_c | 0.018 | 0.001 | 0.016 | 0.020 |
| Back-transformed random-effect SDs | PartnerA_provided_support_cwp_actor | 0.153 | 0.018 | 0.119 | 0.191 |
| Back-transformed random-effect SDs | PartnerA_provided_support_cwp_partner | 0.107 | 0.020 | 0.066 | 0.144 |
| Back-transformed random-effect SDs | PartnerB_Intercept | 1.085 | 0.056 | 0.987 | 1.206 |
| Back-transformed random-effect SDs | PartnerB_diaryday_c | 0.018 | 0.001 | 0.016 | 0.020 |
| Back-transformed random-effect SDs | PartnerB_provided_support_cwp_actor | 0.153 | 0.018 | 0.119 | 0.191 |
| Back-transformed random-effect SDs | PartnerB_provided_support_cwp_partner | 0.107 | 0.020 | 0.066 | 0.144 |
| Back-Transformed Correlations | |||||
| Back-transformed random-effect correlations | PartnerA_diaryday_c with PartnerA_Intercept | 0.581 | 0.056 | 0.470 | 0.681 |
| Back-transformed random-effect correlations | PartnerA_provided_support_cwp_actor with PartnerA_Intercept | 0.572 | 0.083 | 0.403 | 0.723 |
| Back-transformed random-effect correlations | PartnerA_provided_support_cwp_actor with PartnerA_diaryday_c | 0.369 | 0.101 | 0.169 | 0.558 |
| Back-transformed random-effect correlations | PartnerA_provided_support_cwp_partner with PartnerA_Intercept | 0.157 | 0.117 | -0.069 | 0.382 |
| Back-transformed random-effect correlations | PartnerA_provided_support_cwp_partner with PartnerA_diaryday_c | 0.146 | 0.129 | -0.114 | 0.400 |
| Back-transformed random-effect correlations | PartnerA_provided_support_cwp_partner with PartnerA_provided_support_cwp_actor | 0.266 | 0.152 | -0.028 | 0.558 |
| Back-transformed random-effect correlations | PartnerB_Intercept with PartnerA_Intercept | -0.221 | 0.095 | -0.402 | -0.039 |
| Back-transformed random-effect correlations | PartnerB_Intercept with PartnerA_diaryday_c | -0.124 | 0.084 | -0.288 | 0.040 |
| Back-transformed random-effect correlations | PartnerB_Intercept with PartnerA_provided_support_cwp_actor | -0.377 | 0.103 | -0.572 | -0.163 |
| Back-transformed random-effect correlations | PartnerB_Intercept with PartnerA_provided_support_cwp_partner | 0.043 | 0.125 | -0.199 | 0.281 |
| Back-transformed random-effect correlations | PartnerB_diaryday_c with PartnerA_Intercept | -0.124 | 0.084 | -0.288 | 0.040 |
| Back-transformed random-effect correlations | PartnerB_diaryday_c with PartnerA_diaryday_c | -0.136 | 0.118 | -0.362 | 0.094 |
| Back-transformed random-effect correlations | PartnerB_diaryday_c with PartnerA_provided_support_cwp_actor | -0.211 | 0.111 | -0.415 | 0.019 |
| Back-transformed random-effect correlations | PartnerB_diaryday_c with PartnerA_provided_support_cwp_partner | 0.046 | 0.138 | -0.231 | 0.310 |
| Back-transformed random-effect correlations | PartnerB_diaryday_c with PartnerB_Intercept | 0.581 | 0.056 | 0.470 | 0.681 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_actor with PartnerA_Intercept | -0.377 | 0.103 | -0.572 | -0.163 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_actor with PartnerA_diaryday_c | -0.211 | 0.111 | -0.415 | 0.019 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_actor with PartnerA_provided_support_cwp_actor | -0.755 | 0.151 | -0.983 | -0.412 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_actor with PartnerA_provided_support_cwp_partner | -0.096 | 0.160 | -0.412 | 0.215 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_actor with PartnerB_Intercept | 0.572 | 0.083 | 0.403 | 0.723 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_actor with PartnerB_diaryday_c | 0.369 | 0.101 | 0.169 | 0.558 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_partner with PartnerA_Intercept | 0.043 | 0.125 | -0.199 | 0.281 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_partner with PartnerA_diaryday_c | 0.046 | 0.138 | -0.231 | 0.310 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_partner with PartnerA_provided_support_cwp_actor | -0.096 | 0.160 | -0.412 | 0.215 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_partner with PartnerA_provided_support_cwp_partner | 0.230 | 0.394 | -0.570 | 0.960 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_partner with PartnerB_Intercept | 0.157 | 0.117 | -0.069 | 0.382 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_partner with PartnerB_diaryday_c | 0.146 | 0.129 | -0.114 | 0.400 |
| Back-transformed random-effect correlations | PartnerB_provided_support_cwp_partner with PartnerB_provided_support_cwp_actor | 0.266 | 0.152 | -0.028 | 0.558 |
| Residual Structure | |||||
| Residual structure | residual_variance | 0.878 | 0.012 | 0.854 | 0.903 |
| Residual structure | sigma | 0.937 | 0.007 | 0.924 | 0.950 |
Dyadic Multilevel Modelling • brms/cmdstanr • Pascal Küng