
Canonical likelihood-based cross-check for the paired phylogenetic decomposition
Source:R/extract-two-psi-cross-check.R
compare_dep_vs_two_psi.RdRefits the user's data with full unstructured T x T phylogenetic and
non-phylogenetic trait covariances (phylo_dep + dep) using the same
engine, family, link, and unit / cluster grouping as the supplied
two-psi fit. Compares the joint two-psi fit's implied
\(\boldsymbol\Sigma_{\mathrm{phy}} = \boldsymbol\Lambda_{\mathrm{phy}}\boldsymbol\Lambda_{\mathrm{phy}}^\top + \boldsymbol\Psi_{\mathrm{phy}}\)
(and the analogous \(\boldsymbol\Sigma_{\mathrm{non}}\)) against
the unstructured baseline. Per-component RMSE plus a flag field
(TRUE when any component disagrees beyond threshold). A flag means
the two fits disagree; it does not show that the paired two-psi model
is wrong or not identified.
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:
jointThe two-psi fit's implied
Sigma_phyandSigma_non(T x T matrices).depThe unstructured
phylo_dep + depbaseline's impliedSigma_phyandSigma_non(T x T matrices). May containNULLentries if the alt fit failed.agreementData frame with rows for
Sigma_phyandSigma_non, columnsrmse(Frobenius RMSE between joint and dep),dep_mag(Frobenius magnitude of the dep estimate),rel_disagreement(rmse / dep_mag), andflag(rel_disagreement > threshold, orNAwhen the alt refit failed so the component could not be tested).flagLogical:
TRUEif any component is flagged,FALSEif all tested components agreed,NAif nothing could be tested (distinguishes agreement from a failed refit).thresholdThe threshold used.
lrtOne-row data frame: the likelihood-ratio test of the paired fit against the unstructured refit, which contains it. Columns
loglik_joint,loglik_alt,chi2(twice the log-likelihood gain of the refit),df(the refit's extra free parameters) andp_value(upper tail of a chi-square withdfdegrees of freedom). AllNAwhen the refit failed, whenfit_two_psihas no non-phylogenetic term (the refit always adds one, so the fits are not nested) or when a log-likelihood is unavailable. The chi-square reference is approximate because the paired model sits on a reduced-rank and variance boundary of the unstructured one; usep_valueas a guide, after checkingalt_fit$opt$convergence.rel_disagreementandflagstay as descriptive summaries; the likelihood ratio is the comparison that accounts for the refit's extra flexibility.boundaryList with character vectors
jointandalt: the boundary flags of the paired fit and of the refit (fromgllvmTMB_diagnose(), for example"near_zero_sd_B"). A variance estimated at zero can still reportpdHess = TRUE. When either vector is non-empty the chi-square reference of thelrtis unreliable, because the null value lies on the edge of the parameter space; readp_valueas a rough guide only. The function also prints a one-line note in that case.alt_fitThe refit
gllvmTMB_multiobject (orNULLon failure), retained so users can inspect convergence and pull other extractor outputs.
Details
Conceptual basis: Williams et al. (2025) bioRxiv 2025.12.20.695312
Eq. 3, generalised across traits. The unstructured fit has
\(T(T+1)/2\) free parameters per tier and is the strongest available
benchmark for the implied total covariance. Agreement between the two
fits is reassuring. Disagreement is a prompt to look further, not a
verdict: the more flexible baseline estimates many more parameters
(for example non-phylogenetic off-diagonals whose true value may be 0)
and carries its own sampling noise at realistic species counts. In
simulations from the paired model itself (T = 3, d = 1; see issue 1571),
the default threshold flagged nearly every dataset: 24/24 seeds at 64
species, 4/4 at 128 and 3/4 at 256, with the paired fit usually closer
to the true non-phylogenetic Sigma than the baseline. The 0.10
default of threshold is therefore not calibrated; treat a flag as
a comparison to inspect, for example by checking the boundary and
convergence diagnostics of both fits.
The lrt component is the likelihood-based comparison: because the paired
fit is nested in the unstructured refit, it reports their
likelihood-ratio test, which accounts for the refit's many extra
parameters. rel_disagreement and flag are kept as descriptive
summaries only.
The cross-check separates three questions:
- Level 1: can we fit?
Both fits converged.
- Level 2: total Sigma_phy and Sigma_non agree?
This diagnostic answers Level 2 directly. The two estimators target the SAME total covariance with different parameterisations, so agreement is what is compared (see the caveat above).
- Level 3: split into Lambda Lambda^T + Psi?
Diagnostic does not test Level 3 directly; Level 2 agreement is the pre-requisite for Level 3 to be meaningful.
Computational scope: tractable for T <= ~30. For larger T, prefer
compare_indep_vs_two_psi().
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
Hadfield, J. D. & Nakagawa, S. (2010). General quantitative genetic methods for comparative biology: phylogenies, taxonomies and multi- trait models for continuous and categorical characters. Journal of Evolutionary Biology 23, 494-508. doi:10.1111/j.1420-9101.2009.01915.x
Meyer, K. & Kirkpatrick, M. (2008). Perils of parsimony: properties of reduced-rank estimates of genetic covariance matrices. Genetics 180, 1153-1166. doi:10.1534/genetics.108.090159
Felsenstein, J. (2005). Using the quantitative genetic threshold model for inferences between and within species. Philosophical Transactions of the Royal Society B 360, 1427-1434. doi:10.1098/rstb.2005.1669
Felsenstein, J. (2012). A comparative method for both discrete and continuous characters using the threshold model. American Naturalist 179, 145-156. doi:10.1086/663681
See also
compare_indep_vs_two_psi() for the cheap diagonal fallback
when T is large; extract_Sigma() for the underlying covariance
extractor.
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 unrestricted phylogenetic and non-phylogenetic covariances
## (phylo_dep + dep) and compare the two fits' covariance matrices.
check <- compare_dep_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 unrestricted estimate; flag marks gaps above `threshold`
## (0.10 by default).
check$agreement
#> component rmse dep_mag rel_disagreement flag
#> 1 Sigma_phy 0.1810554 0.3928673 0.4608563 TRUE
#> 2 Sigma_non 0.0933282 0.2285131 0.4084152 TRUE
check$flag
#> [1] TRUE
## Both components are flagged here, even though the data were simulated
## from the paired model itself. The unrestricted refit gains little
## log-likelihood for its three extra parameters, yet it splits the trait
## covariance differently between the phylogenetic and non-phylogenetic
## parts: with 64 species the data only loosely determine that split.
## So a flag means the two fits disagree; on its own it does not show
## that the paired model is wrong.
c(two_psi = logLik(fit), unrestricted = logLik(check$alt_fit))
#> two_psi unrestricted
#> -641.5846 -639.7612
## The likelihood-ratio test of the two fits (the unrestricted refit
## contains the paired fit); this accounts for its extra parameters.
check$lrt
#> loglik_joint loglik_alt chi2 df p_value
#> 1 -641.5846 -639.7612 3.646863 3 0.302208
check$joint$Sigma_phy # paired fit
#> t1 t2 t3
#> t1 0.7537365 0.2841576 -0.4519319
#> t2 0.2841576 0.6644219 -0.2607492
#> t3 -0.4519319 -0.2607492 0.7495611
check$dep$Sigma_phy # unrestricted refit
#> t1 t2 t3
#> t1 0.66630576 0.03233858 -0.2236047
#> t2 0.03233858 0.69424559 -0.1763252
#> t3 -0.22360468 -0.17632525 0.5467096