Author: Alex Litovchenko
Date: 2026-04-17

# Irregular measurement and latent state tracking

#### Alex Litovchenko

#### 2026-04-17

Important: This vignette does not claim that KFAS in tidyILD “fixes” or removes the challenges of irregular measurement. tidyILD helps you measure and report timing structure (intervals, gaps, spacing class). `ild_kfas()` fits a discrete-time state-space model along observation order; it is not a drop-in substitute for continuous-time latent models (e.g. ctsem-like workflows). High irregularity can trigger a guardrail; continuous-time solutions are a later tier.

## Motivation

Intensive longitudinal data (EMA, diaries, wearable prompts) often have irregular observation times: gaps, bursts, and varying intervals between measurements. tidyILD makes that structure visible and actionable before you choose a model:

- `ild_prepare()` computes per-row intervals (e.g. `.ild_dt`) and respects `gap_threshold` for episode boundaries.
- `ild_summary()` and `ild_spacing()` / `ild_spacing_class()` summarize interval variability and classify spacing as regular-ish vs irregular-ish (see `vignette("ild-decomposition-and-spacing", package = "tidyILD")`).
- `ild_design_check()` aggregates spacing and design summaries for reporting.

Those tools support mixed models (e.g. choosing AR1 vs CAR1 for residual correlation in `ild_lme()`) and transparent reporting of how timing was handled. KFAS in tidyILD adds another branch: latent state tracking for a single series—but the same spacing diagnostics still describe whether the timing pattern matches the assumptions of the state-space fit you use.

## What `ild_kfas()` assumes today

The KFAS backend in tidyILD fits models on a discrete time index: observations are ordered after `ild_prepare()`, and the state evolves one step per row for that series. It does not implement a continuous-time Kalman filter with unequal physical intervals built into the transition matrix (that design space points elsewhere, e.g. specialized continuous-time software—see `inst/dev/KFAS_V1_BACKEND.md`).

So:

- Irregular calendar timing is still reflected in ILD metadata (intervals, spacing class, gaps).
- The state-space fit itself is discrete-time along the observation order unless and until the backend gains explicit continuous-time support.

Use spacing outputs to document irregularity and to interpret results cautiously when intervals vary wildly: a local-level model on the observation index is not the same as a model that correctly weights by elapsed time in continuous time.

## `irregular_time` and guardrails

`ild_kfas(..., irregular_time = TRUE)` suppresses the default warning when spacing looks highly irregular relative to a discrete-time local-level assumption. It does not turn the model into a continuous-time model; it acknowledges that you have read the spacing diagnostics and accept the discrete-time framing (or a deliberate approximation).

After fitting, `ild_diagnose()` may attach KFAS-specific guardrails, including `GR_KFAS_HIGH_IRREGULARITY_FOR_DISCRETE_TIME` when the design is irregular-ish but the fit remains discrete-time. That rule is a design-space bridge: it connects spacing classification to state-space workflow without claiming to fix misspecification.

Browse rule definitions with `guardrail_registry()` and inspect triggered rows in `ild_diagnose(fit)$guardrails`.

## Workflow sketch

    library(tidyILD)
    set.seed(3)
    d <- ild_simulate(n_id = 1, n_obs_per = 50, irregular = TRUE, seed = 11)
    x <- ild_prepare(d, id = "id", time = "time", gap_threshold = 7200)
    ild_summary(x)$summary
    #> # A tibble: 1 × 7
    #>    n_id n_obs time_min time_max prop_gap median_dt_sec iqr_dt_sec
    #>   <int> <int>    <dbl>    <dbl>    <dbl>         <dbl>      <dbl>
    #> 1     1    50        0  176801.        0         3561.       579.
    ild_spacing_class(x)
    #> [1] "regular-ish"

If KFAS is installed, you can fit with explicit acknowledgment of irregular spacing:

    fit <- suppressWarnings(
      ild_kfas(
        x,
        outcome = "y",
        state_spec = "local_level",
        time_units = "seconds",
        irregular_time = TRUE
      )
    )
    diag <- ild_diagnose(fit)
    # When spacing is irregular-ish and IQR/median interval ratio > 0.75, expect
    # GR_KFAS_HIGH_IRREGULARITY_FOR_DISCRETE_TIME among triggered guardrails:
    diag$guardrails[, c("rule_id", "message")]
    #> # A tibble: 0 × 2
    #> # ℹ 2 variables: rule_id <chr>, message <chr>

If KFAS is not installed, install the KFAS package to run the chunk above.

## Where to go next

- `vignette("kfas-state-space-modeling", package = "tidyILD")` — state-space concepts and filtered vs smoothed states.
- `vignette("kfas-choosing-backend", package = "tidyILD")` — when to use lme/nlme, brms, or KFAS.