Temporal dynamics: choosing a model for ILD

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.6.0 (2026-04-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] ggplot2_4.0.3  dplyr_1.2.1    tidyILD_0.4.1  rmarkdown_2.31
#> 
#> loaded via a namespace (and not attached):
#>  [1] sandwich_3.1-1     utf8_1.2.6         sass_0.4.10        generics_0.1.4    
#>  [5] anytime_0.3.13     lattice_0.22-9     lme4_2.0-1         digest_0.6.39     
#>  [9] magrittr_2.0.5     timechange_0.4.0   evaluate_1.0.5     grid_4.6.0        
#> [13] RColorBrewer_1.1-3 fastmap_1.2.0      jsonlite_2.0.0     Matrix_1.7-5      
#> [17] mgcv_1.9-4         scales_1.4.0       jquerylib_0.1.4    reformulas_0.4.4  
#> [21] Rdpack_2.6.6       cli_3.6.6          rlang_1.2.0        rbibutils_2.4.1   
#> [25] KFAS_1.6.0         splines_4.6.0      withr_3.0.2        cachem_1.1.0      
#> [29] yaml_2.3.12        otel_0.2.0         tools_4.6.0        nloptr_2.2.1      
#> [33] minqa_1.2.8        tsibble_1.2.0      boot_1.3-32        clubSandwich_0.7.0
#> [37] buildtools_1.0.0   vctrs_0.7.3        R6_2.6.1           zoo_1.8-15        
#> [41] lubridate_1.9.5    lifecycle_1.0.5    MASS_7.3-65        pkgconfig_2.0.3   
#> [45] pillar_1.11.1      bslib_0.11.0       gtable_0.3.6       glue_1.8.1        
#> [49] Rcpp_1.1.1-1.1     xfun_0.58          tibble_3.3.1       tidyselect_1.2.1  
#> [53] sys_3.4.3          knitr_1.51         farver_2.1.2       htmltools_0.5.9   
#> [57] nlme_3.1-169       labeling_0.4.3     maketools_1.3.2    compiler_4.6.0    
#> [61] S7_0.2.2