Skip to contents

screen_gllvmTMB() is a pre-fit check for candidate responses. It answers a small question before you fit a stacked-trait GLLVM:

Are these traits, items, or indicators risky to put into this exact model?

It does not decide which responses to use, remove indicators, choose a latent rank, prove identifiability, or guarantee convergence. It uses the same wide-to-long stacking route as gllvmTMB(), then reports data and formula conditions that are easy to miss before fitting. It is not a complete dry run of every parser or engine check.

The current screen covers one binomial() family using the logit, probit, or cloglog link. Trait-level support works for Bernoulli and multi-trial binomial responses; pairwise duplicate/complement screening is Bernoulli-only. Other families return NOT_CHECKED rather than pretending they were screened.

The screen has three separate jobs:

  1. Marginal response support: invalid values, unusable denominators, constants, rare outcomes, and strong imbalance.
  2. Response-pair redundancy: exact or near duplicates, complements, and unusually strong pairwise association on jointly observed units.
  3. Formula shape: fixed-effect rank, grouping-level support, and whether the requested latent dimension is smaller than the raw number of traits.

Formal complete or quasi-complete separation depends on the response–predictor design. Rare or constant outcomes can motivate a formal separation check, but duplicate or complementary response columns are redundancy conditions, not classical separation. A clean screen_gllvmTMB() result therefore does not prove that separation cannot occur.

Setup

The example uses a systematic-map style data set. Each row is a review. Each binary column records whether a review has a topic, region, taxonomic group, or outcome indicator. In this setting the word trait means “response column”; scientifically, it may be a content item or coding indicator.

For a binary joint species distribution model (JSDM), read review as the site, plot, survey, or other sampling unit and read each indicator column as one species’ presence-absence response. The screen asks whether those response columns are well supported for the latent block you are about to fit.

n <- 40
reviews <- data.frame(
  review = factor(seq_len(n)),
  outcome_richness = rep(c(0, 1), length.out = n),
  outcome_diversity = rep(c(0, 1), length.out = n),
  outcome_no_change = 1 - rep(c(0, 1), length.out = n),
  region_north_america = c(rep(1, 37), rep(0, 3)),
  level_ecosystem = 0
)

This candidate set contains three common problems.

outcome_diversity duplicates outcome_richness. outcome_no_change is its exact complement. region_north_america is near-universal, with only three minority outcomes. level_ecosystem is constant.

Wide-data screen

Most systematic-map data start wide: one row per review and one column per indicator. Use the same traits(...) left-hand side that you would use in a wide gllvmTMB() call.

Here latent(1 | review, d = 2) requests two ordinary unit-tier latent variables for the review or site units. The screen warns when d is not smaller than the raw number of traits. It reports pair redundancy separately and does not estimate an effective rank after collapsing duplicate responses. This page does not screen phylogenetic, spatial, or meta-analytic covariance tiers.

The thresholds are advisory and configurable. This example stores the defaults explicitly so the active rules remain visible.

ctl <- screen_control()
data.frame(
  rule = c(
    "minority count: stronger warning",
    "minority count: warning",
    "prevalence: stronger information flag",
    "prevalence: information flag",
    "pair discordance: stronger warning",
    "pair discordance: warning",
    "pair Hamming rate: warning",
    "absolute phi/Jaccard: stronger warning",
    "absolute phi/Jaccard: warning"
  ),
  default = c(
    paste0("< ", ctl$rare_strong_n),
    paste0("< ", ctl$rare_warn_n),
    paste(ctl$prevalence_strong, collapse = " to "),
    paste(ctl$prevalence_warn, collapse = " to "),
    paste0("< ", ctl$discordant_strong_n),
    paste0("< ", ctl$discordant_warn_n),
    paste0("<= ", ctl$hamming_rate_warn),
    paste0(">= ", ctl$phi_strong),
    paste0(">= ", ctl$phi_warn)
  )
)
#>                                     rule      default
#> 1       minority count: stronger warning          < 5
#> 2                minority count: warning         < 10
#> 3  prevalence: stronger information flag 0.02 to 0.98
#> 4           prevalence: information flag 0.05 to 0.95
#> 5     pair discordance: stronger warning          < 5
#> 6              pair discordance: warning         < 10
#> 7             pair Hamming rate: warning      <= 0.01
#> 8 absolute phi/Jaccard: stronger warning      >= 0.95
#> 9          absolute phi/Jaccard: warning       >= 0.9

Prevalence outside the displayed interval is flagged; it is not a deletion cutoff. Minority counts take priority, which prevents a large data set with a low prevalence from receiving the same warning as a tiny data set with only a few minority outcomes.

scr <- screen_gllvmTMB(
  traits(
    outcome_richness,
    outcome_diversity,
    outcome_no_change,
    region_north_america,
    level_ecosystem
  ) ~ 1 + latent(1 | review, d = 2),
  data = reviews,
  unit = "review",
  family = binomial("probit"),
  control = ctl
)
#> Warning: ! Ordinary `latent()` now includes a per-trait Psi by default (Sigma = Lambda
#>   Lambda^T + Psi).
#>  This changed in gllvmTMB 0.2.0; earlier `latent()` was loadings-only (Lambda
#>   Lambda^T).
#> → Pass `latent(..., unique = FALSE)` for the old rotation-invariant
#>   loadings-only fit.

scr
#> gllvmTMB pre-fit response screen
#>   FAIL 4 | WARN 1 | PASS 13
#>   Inspect 5 recommendations.
#>  status                    action                                 trait
#>    WARN                   inspect                  region_north_america
#>    FAIL exclude_from_latent_block                       level_ecosystem
#>    FAIL        collapse_or_recode  outcome_richness / outcome_diversity
#>    FAIL        collapse_or_recode  outcome_richness / outcome_no_change
#>    FAIL        collapse_or_recode outcome_diversity / outcome_no_change
#>                                             evidence
#>    the minority outcome has very few observed trials
#>                all observed outcomes are on one side
#>   the two traits are exact duplicates on paired rows
#>  the two traits are exact complements on paired rows
#>  the two traits are exact complements on paired rows

The summary counts checks, not traits: FAIL 4 combines one trait-level and three pair-level failures, WARN 1 is one trait warning, and PASS 13 includes trait, pair, and formula-shape checks. Statuses are scope-specific. A pair-level PASS cannot cancel a trait-level FAIL, and a trait-level PASS cannot cancel an exact redundant-pair finding.

  • PASS means no condition was flagged at the current thresholds; it is not a guarantee of a safe fitted model.
  • WARN and INFO are advisory. severity grades the evidence within that status, so WARN with severity = "strong" is not contradictory.
  • FAIL means invalid binomial input, no usable denominator, a constant response that is uninformative for this latent block, or exact pair redundancy on the observed overlap.
  • NOT_CHECKED means the requested screen is unavailable for that row or family.

The trait table reports the denominators behind each warning. A 5% prevalence with 20 Bernoulli trials is not the same evidence as a 5% prevalence with 100000 trials. n_valid counts usable rows, while total_trials includes the multi-trial denominator.

trait_screen <- screen_table(scr, "traits")
trait_screen[, c(
  "trait", "status", "severity", "n_valid", "total_trials",
  "prevalence", "minority_count", "action"
)]
#>                  trait status severity n_valid total_trials prevalence
#> 1     outcome_richness   PASS     none      40           40      0.500
#> 2    outcome_diversity   PASS     none      40           40      0.500
#> 3    outcome_no_change   PASS     none      40           40      0.500
#> 4 region_north_america   WARN   strong      40           40      0.925
#> 5      level_ecosystem   FAIL constant      40           40      0.000
#>   minority_count                    action
#> 1             20                      keep
#> 2             20                      keep
#> 3             20                      keep
#> 4              3                   inspect
#> 5              0 exclude_from_latent_block

The pair table catches exact duplicates, exact complements, and near-duplicate Bernoulli indicators on jointly observed review rows. n_pair is the overlap denominator; discordant_n counts different outcomes, and hamming_rate is that count divided by n_pair. “Exact” therefore means exact among the paired rows shown, not necessarily across rows where one response is missing.

pairs <- screen_table(scr, "pairs")
pairs[, c(
  "trait_i", "trait_j", "status", "severity",
  "n_pair", "discordant_n", "hamming_rate", "action"
)]
#>                 trait_i              trait_j status   severity n_pair
#> 1      outcome_richness    outcome_diversity   FAIL  duplicate     40
#> 2      outcome_richness    outcome_no_change   FAIL complement     40
#> 3      outcome_richness region_north_america   PASS       none     40
#> 4      outcome_richness      level_ecosystem   PASS       none     40
#> 5     outcome_diversity    outcome_no_change   FAIL complement     40
#> 6     outcome_diversity region_north_america   PASS       none     40
#> 7     outcome_diversity      level_ecosystem   PASS       none     40
#> 8     outcome_no_change region_north_america   PASS       none     40
#> 9     outcome_no_change      level_ecosystem   PASS       none     40
#> 10 region_north_america      level_ecosystem   PASS       none     40
#>    discordant_n hamming_rate             action
#> 1             0        0.000 collapse_or_recode
#> 2            40        1.000 collapse_or_recode
#> 3            21        0.525               keep
#> 4            20        0.500               keep
#> 5            40        1.000 collapse_or_recode
#> 6            21        0.525               keep
#> 7            20        0.500               keep
#> 8            19        0.475               keep
#> 9            20        0.500               keep
#> 10           37        0.925               keep

Recommendations are written as actions to inspect, not automatic decisions. For example, a near-universal indicator may be scientifically central. In that case, report why it stays in the first latent block and run a sensitivity model. If it is only a coding artefact, recode it, collapse it with a related item, or keep it descriptive instead of making it define a latent variable.

recommendations <- screen_table(scr, "recommendations")
recommendations[, c("scope", "status", "action", "trait", "evidence")]
#>   scope status                    action                                 trait
#> 1 trait   WARN                   inspect                  region_north_america
#> 2 trait   FAIL exclude_from_latent_block                       level_ecosystem
#> 3  pair   FAIL        collapse_or_recode  outcome_richness / outcome_diversity
#> 4  pair   FAIL        collapse_or_recode  outcome_richness / outcome_no_change
#> 5  pair   FAIL        collapse_or_recode outcome_diversity / outcome_no_change
#>                                              evidence
#> 1   the minority outcome has very few observed trials
#> 2               all observed outcomes are on one side
#> 3  the two traits are exact duplicates on paired rows
#> 4 the two traits are exact complements on paired rows
#> 5 the two traits are exact complements on paired rows

The formula and grouping checks are separate tables. Here all 40 units have both outcome sides across the screened block, and the requested rank is smaller than the raw trait count.

screen_table(scr, "design")[, c(
  "component", "status", "value", "threshold", "message"
)]
#>           component status value threshold
#> 1 fixed_effect_rank   PASS     5         5
#> 2       unit_levels   PASS    40         2
#> 3     latent_rank_1   PASS     2         5
#>                                                      message
#> 1                   fixed-effect design has full column rank
#> 2                      unit grouping has at least two levels
#> 3 requested latent rank is smaller than the number of traits

as.data.frame(table(
  status = screen_table(scr, "units")$status
))
#>   status Freq
#> 1   PASS   40

Long-data screen

The same screen works after the indicators have already been stacked long. The long form is useful when your workflow stores one row per (review, indicator) cell.

long <- stats::reshape(
  reviews,
  varying = list(names(reviews)[names(reviews) != "review"]),
  v.names = "present",
  timevar = "trait",
  times = names(reviews)[names(reviews) != "review"],
  idvar = "review",
  direction = "long"
)
long$trait <- factor(long$trait, levels = names(reviews)[names(reviews) != "review"])
row.names(long) <- NULL

scr_long <- screen_gllvmTMB(
  present ~ 0 + trait + latent(0 + trait | review, d = 2),
  data = long,
  unit = "review",
  trait = "trait",
  family = binomial("probit"),
  control = ctl
)

long_traits <- screen_table(scr_long, "traits")
long_traits[, c(
  "trait", "status", "n_valid", "prevalence", "minority_count"
)]
#>                  trait status n_valid prevalence minority_count
#> 1     outcome_richness   PASS      40      0.500             20
#> 2    outcome_diversity   PASS      40      0.500             20
#> 3    outcome_no_change   PASS      40      0.500             20
#> 4 region_north_america   WARN      40      0.925              3
#> 5      level_ecosystem   FAIL      40      0.000              0

identical(
  trait_screen[, c("trait", "status", "n_success", "n_failure")],
  long_traits[, c("trait", "status", "n_success", "n_failure")]
)
#> [1] TRUE

The printed comparison verifies that the wide and canonical long calls produce the same trait-support summaries in this example. It does not claim that every future table is identical under every formula. Use whichever data shape matches your workflow.

Apply the screen before fitting

The constant response cannot inform this latent block, and the duplicate and complementary outcomes should not enter the same block unchanged. The next candidate keeps one representative response, removes the constant, and retains the near-universal response because we decide it is scientifically important. That retained warning must be documented and revisited in a sensitivity fit.

candidate <- reviews[c(
  "review",
  "outcome_richness",
  "region_north_america"
)]

scr_candidate <- screen_gllvmTMB(
  traits(outcome_richness, region_north_america) ~
    1 + latent(1 | review, d = 1),
  data = candidate,
  unit = "review",
  family = binomial("probit"),
  control = ctl
)

screen_table(scr_candidate, "recommendations")[, c(
  "scope", "status", "action", "trait", "evidence"
)]
#>   scope status  action                trait
#> 1 trait   WARN inspect region_north_america
#>                                            evidence
#> 1 the minority outcome has very few observed trials

This rerun contains only the deliberately retained rarity warning. If the rare response is not scientifically essential, keep it descriptive or fit a sensitivity model without it. The screen supports that decision; it does not make the decision.

What to do with warnings

Read FAIL according to its scope. Invalid values or denominators require data repair. A constant response is valid binomial data but has no minority support for the proposed latent block; exclude it from that block or collect informative variation. Exact duplicate or complementary pairs should be selected, combined, or recoded before fitting the same latent block unchanged. Always check the reported n_pair first, especially when responses have different missingness.

WARN means limited marginal support or unusually strong pairwise redundancy or association. Inspect the coding, missingness, overlap denominator, and scientific purpose. Do not lower the latent rank and assume the problem is gone; a near-constant binary item can dominate a one-axis model too.

INFO means the screen saw an imbalanced condition but enough denominator support that a hard warning would overstate the evidence. This distinction is why the table reports minority counts.

After fitting, still run check_gllvmTMB(). Pre-fit screening and fitted-model diagnostics answer different questions. screen_gllvmTMB() catches candidate indicator risks before fitting; check_gllvmTMB() catches large fitted loadings, weak axes, optimizer status, and uncertainty-health symptoms after fitting. The full sequence is shown in Can I trust this fit?.

Evidence base

The defaults are evidence-informed heuristics, not universal deletion rules. Complete and quasi-complete separation is a well-studied maximum-likelihood failure mode (Albert and Anderson 1984; Mansournia et al. 2018), but its presence depends on the response–predictor design. Minority-count rules such as “10 events per variable” are context-dependent heuristics rather than hard laws (Peduzzi et al. 1996; Vittinghoff and McCulloch 2007). This screen reports conditions that deserve inspection; it does not replace a formal separation analysis.

Rare species are not automatically discarded in JSDMs. Shared community structure can make sparse responses scientifically valuable, while very sparse or constant responses can still make species-specific parameters hard to estimate (Ovaskainen and Soininen 2011; Warton et al. 2015; Norberg et al. 2019). Documented prevalence thresholds are analysis choices, not general rules. Retain an important rare species only with a stated reason and a sensitivity analysis.

Psychometric item analysis uses the same principle: item support, redundancy, response frequencies, and interpretability are evidence for review rather than automatic deletion. mirt::itemstats() is a useful precedent for inspecting items before fitting an IRT model (Chalmers 2012). screen_gllvmTMB() likewise reports response risks without silently changing the formula.

Post-fit loading salience, rotation, and latent-rank interpretation belong after fitting. They are not substitutes for this pre-fit response screen.

Next decision

Fit only after every WARN row has a documented keep, recode, combine, or omit decision. Then run Can I trust this fit?; a clean pre-fit screen does not make the fitted optimiser, Hessian, or weak-axis checks pass automatically. If a scientifically essential response remains constant or has too little denominator support, report that limitation and defer its species-specific effect rather than forcing it into the model.

References

  • Albert A, Anderson JA (1984). On the existence of maximum likelihood estimates in logistic regression models. Biometrika 71:1–10.
  • Peduzzi P, Concato J, Kemper E, Holford TR, Feinstein AR (1996). A simulation study of the number of events per variable in logistic regression analysis. Journal of Clinical Epidemiology 49:1373–1379.
  • Vittinghoff E, McCulloch CE (2007). Relaxing the rule of ten events per variable in logistic and Cox regression. American Journal of Epidemiology 165:710–718.
  • Mansournia MA, Geroldinger A, Greenland S, Heinze G (2018). Separation in logistic regression: causes, consequences, and control. American Journal of Epidemiology 187:864–870.
  • Chalmers RP (2012). mirt: A multidimensional item response theory package for the R environment. Journal of Statistical Software 48(6):1–29.
  • Chalmers RP. mirt::itemstats() documentation: generic item summary statistics without prior IRT fitting. https://rdrr.io/cran/mirt/man/itemstats.html
  • Norberg A, Abrego N, Blanchet FG, Adler FR, Anderson BJ, Anttila J, Araujo MB, Dallas T, Dunson D, Elith J, Foster SD, Fox R, Franklin J, Godsoe W, Guisan A, O’Hara B, Hill NA, Holt RD, Hui FKC, Husby M, Kalas JA, Lehikoinen A, Luoto M, Mod HK, Newell G, Renner I, Roslin T, Soininen J, Thuiller W, Vanhatalo J, Warton D, White M, Zimmermann NE, Gravel D, Ovaskainen O (2019). A comprehensive evaluation of predictive performance of 33 species distribution models at species and community levels. Ecological Monographs 89:e01370. https://doi.org/10.1002/ecm.1370.
  • Ovaskainen O, Soininen J (2011). Making more out of sparse data: hierarchical modeling of species communities. Ecology 92:289–295. https://doi.org/10.1890/10-1251.1.
  • Warton DI, Blanchet FG, O’Hara RB, Ovaskainen O, Taskinen S, Walker SC, Hui FKC (2015). So many variables: joint modeling in community ecology. Trends in Ecology & Evolution 30:766–779. https://doi.org/10.1016/j.tree.2015.09.007.