Author: tidyILD authors
Date: 2026-04-17

# Temporal dynamics: choosing a model for ILD

#### tidyILD authors

#### 2026-04-17

This article is a decision guide for temporal structure: lags in the mean, residual autocorrelation, time-varying effects, and latent dynamics. It does not replace statistical theory or domain expertise; it maps scientific questions to tidyILD entry points you already have.

## Three axes before you fit anything

1. Estimand — Associational lag effect? Stability of the outcome (lagged outcome in the mean)? Causal effect under a clear identification strategy (see MSM vignettes)? Latent smooth trajectory?
2. Spacing — Occasions are regular-ish or irregular-ish? See `ild_spacing_class()`, `ild_spacing()`, and `vignette("ild-decomposition-and-spacing", package = "tidyILD")`.
3. Where dynamics live — In the fixed-effects mean (e.g. `y ~ x_lag1`), in residual correlation (AR1/CAR1 on within-person residuals), in a smooth over calendar/study time (`ild_tvem()`), or in a latent state (`ild_kfas()`, `ild_ctsem()`).

## Decision flow (conceptual)

The following diagram is a navigation aid (rendered on sites that support Mermaid; otherwise read the labels as a checklist). Source in Mermaid syntax:

    flowchart TD
      q1[Estimand clear?]
      spacing[Check ild_spacing_class]
      meanLag[Lags in mean: ild_lag ild_panel_lag_prepare ild_crosslag]
      residAR[Residual AR: ild_lme ar1=TRUE]
      tvem[Effect varies over time: ild_tvem]
      ct[Continuous-time latent: ild_ctsem]
      kfas[Discrete-time latent level: ild_kfas]
      q1 --> spacing
      spacing --> meanLag
      spacing --> residAR
      meanLag --> residAR
      spacing --> tvem
      spacing --> ct
      spacing --> kfas

- Irregular spacing makes index lags (row order) easy to misinterpret as equal time. Prefer `gap_aware` or `time_window` in `ild_lag()`, or align to a grid, or use continuous-time models when the estimand requires it (`vignette("kfas-irregular-timing-spacing", package = "tidyILD")`).
- Residual ACF after a lag-mean model may indicate unmodeled autocorrelation; compare to `ild_lme(..., ar1 = TRUE)` (nlme path with AR1 or CAR1 chosen from spacing). See `vignette("temporal-dynamics-model-choice", package = "tidyILD")` guardrails in `ild_diagnose()`.

## Feature map

Question | tidyILD tools | Backend / notes | Not a substitute for
--- | --- | --- | ---
Lagged predictor → outcome | `ild_lag()`, `ild_crosslag()`, `ild_panel_lag_prepare()` | `ild_lme` / `ild_brms` | Full panel VAR / DSEM (multivariate lag system)
Lagged outcome (stability) | Same lag helpers; model `y ~ y_lag1 + ...` carefully | Mixed model | Dynamic structural equation modeling software if that is the estimand
Residual serial correlation | `ild_lme(..., ar1 = TRUE)` | nlme AR1 or CAR1 | Does not add lagged mean structure by itself
Effect changes over study time | `ild_tvem()`, `ild_tvem_plot()` | mgcv GAM | Random slopes over time per person (consider `ild_brms` recipes)
Discrete-time latent level | `ild_kfas()` | KFAS | Pooled multilevel latent model across many IDs
Continuous-time latent dynamics | `ild_ctsem()` | ctsem / Stan | Quick lag regression on irregular data without CT assumptions
Compare a few fitted models | `ild_compare_fits()` | AIC/BIC where defined | Likelihood-ratio tests unless models are nested and comparable
Multivariate lags / feedback (joint system) | Same lag helpers; `ild_crosslag()` is one equation at a time | Export preprocessed data; see `vignette("ild-specialist-backends", package = "tidyILD")` | dynamite, lavaan DSEM, multivariate brms / ctsem
High-dimensional time-varying predictors (p >> n) | Unpenalized `ild_lme` / `lme4` is not designed for this | Same vignette: hand off to penalized longitudinal tools | PGEE and related methods

## Minimal examples

    library(tidyILD)
    set.seed(1)
    d <- ild_simulate(n_id = 12, n_obs_per = 10, seed = 1)
    x <- ild_prepare(d, id = "id", time = "time")
    out <- ild_crosslag(x, y, y, lag = 1L, ar1 = FALSE, warn_no_ar1 = FALSE)
    #> boundary (singular) fit: see help('isSingular')
    out$lag_term[, c("term", "estimate", "std_error")]
    #> # A tibble: 1 × 3
    #>   term   estimate std_error
    #>   <chr>     <dbl>     <dbl>
    #> 1 y_lag1    0.778    0.0607

    x2 <- ild_center(x, y)
    fit_ar <- tryCatch(
      ild_lme(y ~ y_bp + y_wp, data = x2, ar1 = TRUE, warn_no_ar1 = FALSE, warn_uncentered = FALSE),
      error = function(e) NULL
    )
    if (!is.null(fit_ar)) {
      print(fit_ar)
    } else {
      "nlme fit skipped on this platform"
    }
    #> [1] "nlme fit skipped on this platform"

    x3 <- ild_simulate(n_id = 10, n_obs_per = 15, seed = 2)
    x3$x <- rnorm(nrow(x3))
    x3 <- ild_prepare(x3, id = "id", time = "time")
    tv <- ild_tvem(x3, "y", "x", k = 5, re_id = TRUE)
    summary(tv)
    #>
    #> Family: gaussian
    #> Link function: identity
    #>
    #> Formula:
    #> y ~ s(.ild_time_num, k = 5) + s(.ild_time_num, by = x, k = 5) +
    #>     s(.ild_id, bs = "re")
    #>
    #> Parametric coefficients:
    #>             Estimate Std. Error t value Pr(>|t|)
    #> (Intercept)   0.9312     0.3205   2.905  0.00429 **
    #> ---
    #> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    #>
    #> Approximate significance of smooth terms:
    #>                      edf Ref.df     F p-value
    #> s(.ild_time_num)   2.514  3.025 23.05  <2e-16 ***
    #> s(.ild_time_num):x 2.000  2.000  0.94   0.393
    #> s(.ild_id)         8.820  9.000 49.59  <2e-16 ***
    #> ---
    #> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    #>
    #> R-sq.(adj) =  0.779   Deviance explained = 79.9%
    #> GCV = 0.33544  Scale est. = 0.30339   n = 150

## Further reading

- `vignette("tidyILD-workflow", package = "tidyILD")`
- `vignette("ild-specialist-backends", package = "tidyILD")` (multivariate / high-p handoffs)
- `vignette("kfas-choosing-backend", package = "tidyILD")`
- `vignette("brms-dynamics-recipes", package = "tidyILD")` (Bayesian templates)
- `vignette("heterogeneity-interpretation", package = "tidyILD")` (person-specific slopes vs dynamics)

    #> R version 4.5.3 (2026-03-11)
    #> Platform: aarch64-apple-darwin20
    #> Running under: macOS Sequoia 15.6
    #>
    #> Matrix products: default
    #> BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib
    #> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
    #>
    #> locale:
    #> [1] C/en_US/en_US/C/en_US/en_US
    #>
    #> time zone: America/New_York
    #> tzcode source: internal
    #>
    #> attached base packages:
    #> [1] stats     graphics  grDevices utils     datasets  methods   base
    #>
    #> other attached packages:
    #> [1] ggplot2_4.0.0 dplyr_1.1.4   tidyILD_0.4.0
    #>
    #> loaded via a namespace (and not attached):
    #>  [1] sandwich_3.1-1     sass_0.4.10        utf8_1.2.6         generics_0.1.4
    #>  [5] anytime_0.3.12     lattice_0.22-9     lme4_1.1-37        digest_0.6.37
    #>  [9] magrittr_2.0.4     evaluate_1.0.5     grid_4.5.3         timechange_0.3.0
    #> [13] RColorBrewer_1.1-3 fastmap_1.2.0      jsonlite_2.0.0     Matrix_1.7-4
    #> [17] mgcv_1.9-4         scales_1.4.0       jquerylib_0.1.4    reformulas_0.4.1
    #> [21] Rdpack_2.6.4       cli_3.6.5          rlang_1.1.6        rbibutils_2.3
    #> [25] KFAS_1.6.0         splines_4.5.3      withr_3.0.2        cachem_1.1.0
    #> [29] yaml_2.3.10        tools_4.5.3        nloptr_2.2.1       minqa_1.2.8
    #> [33] tsibble_1.2.0      boot_1.3-32        vctrs_0.6.5        R6_2.6.1
    #> [37] clubSandwich_0.6.1 zoo_1.8-14         lifecycle_1.0.4    lubridate_1.9.4
    #> [41] MASS_7.3-65        pkgconfig_2.0.3    pillar_1.11.1      bslib_0.9.0
    #> [45] gtable_0.3.6       glue_1.8.0         Rcpp_1.1.0         xfun_0.53
    #> [49] tibble_3.3.0       tidyselect_1.2.1   knitr_1.50         farver_2.1.2
    #> [53] htmltools_0.5.8.1  nlme_3.1-168       rmarkdown_2.29     labeling_0.4.3
    #> [57] compiler_4.5.3     S7_0.2.0