
Cheap diagonal cross-check for the paired phylogenetic decomposition (large T)
Source:R/extract-two-psi-cross-check.R
compare_indep_vs_two_psi.RdRefits the user's data with phylo_indep + indep – a stacked
multivariate generalisation of T univariate phylogenetic GLMMs in
the spirit of Williams et al. (2025) Eq. 3 – using the same engine,
family, link, and grouping as the supplied two-psi fit. Compares the
per-trait diagonals of the joint two-psi fit's implied
\(\boldsymbol\Sigma_{\mathrm{phy}}\) (and \(\boldsymbol\Sigma_{\mathrm{non}}\))
against the marginal-only baseline. Tests only the diagonals
(cheap); does not test the off-diagonal cross-trait covariances.
Arguments
- fit_two_psi
A joint two-psi fit returned by
gllvmTMB(), e.g. one produced bygllvmTMB(value ~ 0 + trait + phylo_latent(species, d = K_phy, unique = TRUE) + indep(0 + trait | species), ...). The cross-check refits the same data and family withphylo_dep + depand compares.- threshold
Numeric (default
0.10): relative-disagreement threshold (Frobenius RMSE / Frobenius magnitude of the unstructured estimate) above which a component is flagged. The default is not calibrated: it flags most datasets simulated from the paired model itself at realistic species counts, so a flag marks disagreement only.- phylo_vcv, phylo_tree
Optional. The phylogenetic correlation matrix or
ape::phylotree, only needed iffit_two_psiwas produced by an older package version that did not store the phylogeny on the fit. Default is to recover them from the fit (fit_two_psi$phylo_vcv/fit_two_psi$phylo_tree).
Value
A list with components:
jointJoint two-psi fit's implied
diag(Sigma_phy)anddiag(Sigma_non)(named numeric vectors of length T).indepThe marginal-only baseline's
diag(Sigma_phy)anddiag(Sigma_non)(same shape; may containNULLif alt fit failed).agreementData frame with rows for
Sigma_phy_diagandSigma_non_diag, columnsrmse,indep_mag,rel_disagreement, andflag.flag,threshold,alt_fitAs in
compare_dep_vs_two_psi().lrtOne-row data frame with the same columns as in
compare_dep_vs_two_psi(), but the direction is reversed: thephylo_indep + indeprefit is the paired fit with its shared loadings set to zero, so the paired fit is the larger model.chi2is twice the log-likelihood gain of the paired fit over the refit anddfits extra free parameters. A smallp_valuemeans the shared loadings improve the fit. The chi-square reference is approximate, because the loadings are not identified under the refit.boundaryAs in
compare_dep_vs_two_psi().
Details
Use this diagnostic when T is large (T \(\geq\) 30) and the
unstructured fit needed by compare_dep_vs_two_psi() is intractable.
Otherwise prefer compare_dep_vs_two_psi() (gold standard).
Cross-check intent: the per-trait diagonals are the simplest summary
the joint two-psi fit and the marginal baseline both target. A flag
means the two fits disagree beyond threshold; it does not show that
the paired model is wrong. The baseline carries its own sampling noise
and the 0.10 default is not calibrated (on the issue 1571
simulations from the paired model, this check also flagged, with
rel_disagreement of 0.24 and 0.16 for the two components at seed 18).
References
Williams, M. J., McGillycuddy, M., Drobniak, S. M., Bolker, B. M., Warton, D. I., & Nakagawa, S. (2025). Fast phylogenetic generalised linear mixed-effects modelling using the glmmTMB R package. bioRxiv 2025.12.20.695312. doi:10.64898/2025.12.20.695312
See also
compare_dep_vs_two_psi() (canonical, full Sigma);
extract_Sigma().
Examples
set.seed(18)
## A balanced phylogeny of 64 species, built in base R: six rounds of
## evenly spaced splits. Two species' phylogenetic correlation is the
## share of the tree's depth they spent as one lineage (5/6 for sister
## species, 0 for species on opposite sides of the root).
n_sp <- 64
Cphy <- matrix(0, n_sp, n_sp)
for (level in 1:6) {
clade <- (seq_len(n_sp) - 1) %/% 2^(6 - level)
Cphy <- Cphy + outer(clade, clade, "==") / 6
}
sp <- paste0("sp", seq_len(n_sp))
dimnames(Cphy) <- list(sp, sp)
## Three traits. A species' value for each trait is the sum of a shared
## phylogenetic factor (with a trait-specific loading), a trait-specific
## phylogenetic effect and a non-phylogenetic species effect. Three
## individuals are measured per species, so these species effects can be
## told apart from measurement noise.
L <- t(chol(Cphy)) # turns independent draws into tree-correlated ones
phy <- outer(drop(L %*% rnorm(n_sp)), c(0.8, 0.6, -0.5)) +
L %*% matrix(rnorm(n_sp * 3, sd = 0.6), n_sp, 3)
non <- matrix(rnorm(n_sp * 3, sd = 0.5), n_sp, 3)
df <- expand.grid(trait = c("t1", "t2", "t3"), individual = 1:3,
species = sp)
idx <- cbind(match(df$species, sp), as.integer(df$trait))
df$value <- phy[idx] + non[idx] + rnorm(nrow(df), sd = 0.5)
## The paired ("two-psi") fit: the phylogenetic covariance is one shared
## factor plus trait-specific phylogenetic variances; the
## non-phylogenetic species variances are independent across traits.
fit <- gllvmTMB(
value ~ 0 + trait +
phylo_latent(species, d = 1, unique = TRUE, vcv = Cphy) +
indep(0 + trait | species),
data = df, trait = "trait", unit = "species", cluster = "species"
)
## Refit with per-trait variances only (phylo_indep + indep) and compare
## the two fits' per-trait phylogenetic and non-phylogenetic variances.
## This cheap check is meant for fits with many traits, where the
## unrestricted refit of compare_dep_vs_two_psi() is too slow; three
## traits keep the example fast.
check <- compare_indep_vs_two_psi(fit)
check$alt_fit$opt$convergence # 0 means the refit converged
#> [1] 0
## rel_disagreement is the gap between the two fits relative to the size
## of the per-trait estimates; flag marks gaps above `threshold`
## (0.10 by default).
check$agreement
#> component rmse indep_mag rel_disagreement flag
#> 1 Sigma_phy_diag 0.14510863 0.6001295 0.2417955 TRUE
#> 2 Sigma_non_diag 0.05749739 0.3689156 0.1558551 TRUE
check$flag
#> [1] TRUE
## Both components are flagged here, even though the data were simulated
## from the paired model itself: with 64 species each trait's
## phylogenetic and non-phylogenetic variances are only loosely
## determined, and the paired fit (which also uses the covariances among
## traits) and this per-trait refit land some way apart. A flag means the
## two fits disagree; on its own it does not show that the paired model
## is wrong. The phylogenetic variances of the two fits:
rbind(two_psi = check$joint$Sigma_phy_diag,
per_trait = check$indep$Sigma_phy_diag)
#> t1 t2 t3
#> two_psi 0.7537365 0.6644219 0.7495611
#> per_trait 0.5599785 0.6467232 0.5904570
## Likelihood-ratio test: does the paired fit's shared factor improve on
## the per-trait refit?
check$lrt
#> loglik_joint loglik_alt chi2 df p_value
#> 1 -641.5846 -646.0889 9.008496 3 0.02917814