State-space modeling in tidyILD with KFAS

Alex Litovchenko

2026-04-17

What is a state-space model?

In a state-space (or dynamic linear) model, you observe a sequence \(y_1,\ldots,y_T\) and posit latent states \(\alpha_t\) that evolve over time and drive the observations. A minimal Gaussian local level model is:

So the “level” of the process drifts slowly; the data are noisy measurements of that level. In tidyILD, ild_kfas(..., state_spec = "local_level") fits this structure (via KFAS) for a single time series per call—one distinct .ild_id after ild_prepare().

When use this instead of mixed-model residual correlation?

Multilevel models (ild_lme(), ild_brms()) are the right tool when you want population inference: fixed and random effects across many persons, within/between decomposition, and residual dynamics (e.g. AR1 or CAR1 on the within-person residuals) as a nuisance correlation structure.

State-space models in this package focus on explicit latent dynamics for one series at a time: estimating a time-varying level (or, in future specs, trend or AR) in state space, with diagnostics built on one-step-ahead innovations from the Kalman filter.

Conceptual contrast:

Neither replaces the other in general—choose based on whether your primary goal is hierarchical population inference or structured univariate latent dynamics for one person (or one series).

Filtered vs smoothed states

In ILD terms:

Formally, after fitting, KFAS runs KFS() to obtain:

In tidyILD, ild_kfas(..., smoother = TRUE) requests smoothing in KFS(); when FALSE, smoothed states may be unavailable or less central. Use ild_plot_filtered_vs_smoothed() to compare the first latent state over time.

Minimal example

If the KFAS package is installed, you can run:

library(tidyILD)
set.seed(1)
d <- ild_simulate(n_id = 1, n_obs_per = 60, seed = 42)
x <- ild_prepare(d, id = "id", time = "time")
x <- ild_center(x, y)
fit <- suppressWarnings(
  ild_kfas(x, outcome = "y", state_spec = "local_level", time_units = "sim_steps")
)
b <- ild_diagnose(fit)
class(b)
#> [1] "ild_diagnostics_bundle" "list"
ild_autoplot(b, section = "residual", type = "acf")

If KFAS is not installed, install it with install.packages("KFAS") and load tidyILD; the same code then runs end-to-end.

What the backend does not yet do

Read this section before relying on KFAS in a paper or preregistration. The normative scope document inst/dev/KFAS_V1_BACKEND.md in the package source has full detail; the points below are the trust boundaries for v1.

What ild_kfas() is:

What it is not (today):

Irregular timing: see vignette("kfas-irregular-timing-spacing", package = "tidyILD")—tidyILD diagnoses spacing; the KFAS wrapper fits under discrete-time choices and does not, by itself, “solve” irregular measurement in a continuous-time sense.

See also