# Within-between decomposition and handling irregular spacing

#### tidyILD authors

#### 2026-04-17

## Within-between decomposition

In intensive longitudinal data, predictors often vary both between persons (e.g. some people are higher on average) and within person (e.g. momentary fluctuations). Using the raw variable in a multilevel model conflates these two sources. tidyILD’s `ild_center()` makes the decomposition explicit.

- Between-person (BP): person mean of the variable.
- Within-person (WP): deviation from the person mean (variable minus BP).

Use the WP component at level 1 (within-person effect) and the BP component at level 2 or in cross-level interactions to avoid ecological fallacy and conflation bias.

    library(tidyILD)
    d <- ild_simulate(n_id = 5, n_obs_per = 8, seed = 1)
    x <- ild_prepare(d, id = "id", time = "time")
    x <- ild_center(x, y)
    # New columns: y_bp (person mean), y_wp (deviation from person mean)
    head(x[, c("id", "y", "y_bp", "y_wp")])
    #> # A tibble: 6 × 4
    #>      id       y   y_bp    y_wp
    #>   <int>   <dbl>  <dbl>   <dbl>
    #> 1     1 -1.04   -0.154 -0.882
    #> 2     1 -0.283  -0.154 -0.129
    #> 3     1 -0.0573 -0.154  0.0969
    #> 4     1 -0.0386 -0.154  0.116
    #> 5     1 -0.379  -0.154 -0.225
    #> 6     1  0.629  -0.154  0.784

## Irregular spacing and lags

ILD often has irregular time spacing (e.g. EMA prompts at random times). Using `dplyr::lag()` assumes equal spacing and can produce misaligned lags. Use `ild_lag()` with `mode = "gap_aware"` so that lags are set to NA when the time distance to the previous observation exceeds a threshold.

    d <- ild_simulate(n_id = 3, n_obs_per = 6, irregular = TRUE, seed = 2)
    x <- ild_prepare(d, id = "id", time = "time")
    x <- ild_lag(x, y, mode = "gap_aware", max_gap = 4000)
    # Compare: .ild_dt (interval) and y_lag1 (NA after large gaps)
    x[, c(".ild_id", ".ild_dt", "y", "y_lag1")]
    #> # A tibble: 18 × 4
    #>    .ild_id .ild_dt      y y_lag1
    #>      <int>   <dbl>  <dbl>  <dbl>
    #>  1       1     NA  -0.335 NA
    #>  2       1   3843. -0.552 -0.335
    #>  3       1   3445.  0.955 -0.552
    #>  4       1   3114. -1.01   0.955
    #>  5       1   4531.  0.715 NA
    #>  6       1   3600.  0.394  0.715
    #>  7       2     NA   0.924 NA
    #>  8       2   4000.  0.745 NA
    #>  9       2   3161.  1.66   0.745
    #> 10       2   3698.  0.119  1.66
    #> 11       2   3603.  1.61   0.119
    #> 12       2   3223.  1.89   1.61
    #> 13       3     NA   0.993 NA
    #> 14       3   2904. -0.155  0.993
    #> 15       3   3869.  1.42  -0.155
    #> 16       3   4138.  0.995 NA
    #> 17       3   3747.  1.79   0.995
    #> 18       3   2699.  1.62   1.79

## Spacing classification

`ild_spacing_class()` returns “regular-ish” or “irregular-ish” based on the variability of intervals and the proportion of large gaps. The rule is overridable. This classification can inform the choice of correlation structure in `ild_lme()` (AR1 for regular-ish, CAR1 for irregular-ish).

    ild_summary(x)$spacing
    #> $median_dt
    #> [1] 3603.228
    #>
    #> $iqr_dt
    #> [1] 663.6274
    #>
    #> $n_intervals
    #> [1] 15
    #>
    #> $pct_gap
    #> [1] NA
    #>
    #> $by_id
    #> # A tibble: 3 × 5
    #>      id median_dt iqr_dt n_intervals pct_gap
    #>   <int>     <dbl>  <dbl>       <int>   <dbl>
    #> 1     1     3600.   398.           5      NA
    #> 2     2     3603.   475.           5      NA
    #> 3     3     3747.   965.           5      NA
    ild_spacing_class(x)
    #> [1] "regular-ish"

If your data start as a tsibble (`tbl_ts`), see `vignette("tsibble-interoperability", package = "tidyILD")` for how tidyILD records key/index and interval provenance and how to round-trip with `ild_as_tsibble()`.