Skip to contents

Formula-LHS marker that lets gllvmTMB() accept a wide data frame (one row per unit, one column per trait) without making the user pivot first to one row per (unit, trait) observation.

Usage

traits(...)

Arguments

...

Column-selection expression(s) passed verbatim to tidyr::pivot_longer(cols = ...). Bare names or any tidyselect verb (all_of(), starts_with(), matches(), etc.) are accepted.

Value

A formula marker; never evaluated as a function call. The parser recognises traits(...) on the LHS of a gllvmTMB() formula and dispatches to the wide-format pivot pre-pass.

Details

The package teaches two shapes, long or wide data-frame:

  • long: gllvmTMB(value ~ ..., data = df_long, ...) – one row per (unit, trait) observation.

  • wide data frame: gllvmTMB(traits(t1, t2, ...) ~ ..., data = df_wide, ...) – one row per unit, one column per trait, with compact formula syntax.

The soft-deprecated gllvmTMB_wide(Y, ...) wrapper remains exported for legacy matrix-wrapper workflows, but new examples should use traits(...) through gllvmTMB().

Both taught shapes reach the same stacked-trait model after internal stacking; the user picks whichever shape matches their data on disk.

Because the LHS already names the response traits, the RHS can use a compact wide shorthand. 1 expands to the trait-specific intercepts 0 + trait; ordinary predictors such as env_temp expand to (0 + trait):env_temp; and latent(1 | individual) expands to the long covariance syntax latent(0 + trait | individual). Ordinary latent() includes its \(\boldsymbol\Psi\) companion by default. The same 1 | group shorthand is recognised for indep(), dep(), bar-style phylo_indep() / phylo_dep(), and the spatial_*() keywords. Species-axis phylogenetic keywords such as phylo_latent(species, d = K) already name their phylogenetic axis and pass through unchanged. Ordinary random-intercept terms such as (1 | batch) also pass through unchanged.

gllvmTMB(
  traits(sleep, mass, lifespan, brain) ~ 1 + env_temp +
    latent(1 | individual, d = 2),
  data   = wide_df,
  unit   = "individual",
  family = gaussian()
)

Internally traits() is implemented as a tidyr::pivot_longer() pre-pass: the wide data is pivoted to long format with trait as a factor column (levels in the order the user supplied to traits()) and .y_wide_ as the response column; the LHS of the formula is rewritten from traits(...) to .y_wide_; and the compact RHS is expanded to the trait-stacked long syntax before dispatch. The explicit long RHS remains accepted, so existing calls that already write 0 + trait and latent(0 + trait | group) keep working.

Tidyselect verbs are supported because traits() forwards its arguments to tidyr::pivot_longer(cols = ...): traits(all_of(cols)), traits(starts_with("sp")), traits(matches("^y[0-9]+$")), traits(any_of(c("a", "b"))), and bare names all work.

Cells with NA responses are, by default, dropped via pivot_longer(values_drop_na = TRUE) – the canonical complete-case behaviour. Passing missing = miss_control(response = "include") to gllvmTMB() instead keeps every (unit, trait) cell and masks the NA cells out of the likelihood (the observed-response mask), preserving per-cell identity and original-row accounting in fit$missing_data. Users who want strict listwise drop should pre-filter the wide data before calling.

For a mixed-family wide fit, pass a named family = list(...) whose names exactly match the response columns selected by traits(). The internal stacking step creates the corresponding per-row family selector; long data may instead continue to provide an explicit family_var column. Per-row weight vectors of length nrow(data) are replicated across traits. For per-cell weight matrices, pivot to long format and pass a weights column aligned with (unit, trait) rows. The legacy matrix wrapper gllvmTMB_wide() still accepts matrix weights for migration code.

See also

gllvmTMB() for model fitting. The legacy matrix wrapper gllvmTMB_wide(Y, ...) is soft-deprecated in 0.2.0; wide-data examples now use the traits(...) LHS through gllvmTMB().

Examples

## Wide format: one column per trait; traits() stacks them internally.
## The LHS *is* the trait spec, so no `trait =` argument is needed.
## traits() uses the tidyr package to stack the columns.
if (requireNamespace("tidyr", quietly = TRUE)) {
  set.seed(1)
  n <- 60
  z <- rnorm(n)  # one latent axis shared by the three traits
  df_wide <- data.frame(
    unit = factor(paste0("u", seq_len(n))),
    t1 = 0.9 * z + rnorm(n, sd = 0.5),
    t2 = 0.7 * z + rnorm(n, sd = 0.5),
    t3 = -0.6 * z + rnorm(n, sd = 0.5)
  )
  fit <- gllvmTMB(
    traits(t1, t2, t3) ~ 1 + latent(1 | unit, d = 1, unique = TRUE),
    data = df_wide, unit = "unit"
  )
  print(extract_Sigma(fit))
}
#> ℹ Auto-suppressing `sigma_eps`: `indep(0 + trait | unit)` is at the per-row
#>   level, so it already absorbs the observation residual.
#> • Fixed at 0.000819 (~1/1000 of sd(y)) to keep the Gaussian density
#>   well-defined; the row-level residual variance is fully captured by the
#>   per-row diagonal term.
#> $Sigma
#>            t1         t2         t3
#> t1  0.7571792  0.4339882 -0.4176667
#> t2  0.4339882  0.6229700 -0.3367003
#> t3 -0.4176667 -0.3367003  0.5863013
#> 
#> $R
#>            t1         t2         t3
#> t1  1.0000000  0.6318951 -0.6268593
#> t2  0.6318951  1.0000000 -0.5571209
#> t3 -0.6268593 -0.5571209  1.0000000
#> 
#> $level
#> [1] "unit"
#> 
#> $part
#> [1] "total"
#> 
#> $note
#> character(0)
#>