Dispatches to pigauto's phylogenetic baseline machinery and returns imputed latent-scale means and standard errors for every species.
Usage
fit_baseline(
data,
tree,
splits = NULL,
model = "BM",
graph = NULL,
multi_obs_aggregation = c("hard", "soft"),
lambda_mode = c("estimate", "fixed_1", "cv", "bayes"),
lambda_fixed = NULL,
em_iterations = 0L,
em_tol = 0.001,
em_offdiag = FALSE,
joint_solver = c("inhouse", "rphylopars"),
predict_method = c("auto", "exact", "per_column"),
joint_refine_iter = 0L,
predict_route = NULL,
seed = NULL
)Arguments
- data
object of class
"pigauto_data".- tree
object of class
"phylo".- splits
list (output of
make_missing_splits) orNULL.- model
character. Evolutionary model:
"BM"(default) or"OU".- graph
optional list returned by
build_phylo_graph. When supplied,graph$D(cophenetic distances) is reused for label propagation andgraph$R_phy(phylogenetic correlation matrix) is reused for BM imputation, avoiding duplicate \(O(n^2)\) allocations. WhenNULL(default), both matrices are computed here.- multi_obs_aggregation
character. How to aggregate multiple observations per species before the Level-C joint baseline:
"hard"(default) thresholds binary proportions at 0.5 and uses argmax for categorical, matching Phase 10 behaviour."soft"preserves species-level proportions and dispatches the truncated-Gaussian soft E-step (estep_liability_binary_soft) so that intermediate class frequencies contribute fractional liability evidence. Only relevant for multi-obs data with binary or categorical traits when the Level-C joint baseline is active.- lambda_mode
character. Pagel-lambda mode for the CONTINUOUS-FAMILY baseline (continuous, count, proportion, zi_count magnitude columns; NOT ordinal, which the threshold-joint path treats as a liability column via
estep_liability_ordinal()– see “Per-type lambda dispatch” in Details)."estimate"(default, S4) fits each continuous-family column's own Pagel's lambda via profile REML; when the joint MVN or threshold-joint baseline fires, this now runs INSIDE that joint fit (R/joint_mvn_solver.R's per-columnlambda_cols) rather than being discarded in favour of a separate per-column re-fit."fixed_1"preserves the classic Brownian correlation matrix (lambda = 1 everywhere)."cv"and"bayes"have no joint analogue and force the per-column BM path for continuous-family columns (as before); binary/ordinal/categorical/ zi_gate columns are unaffected bylambda_modein every case and keep the threshold-joint / OVR-categorical baseline at lambda = 1. Covariate caveat: whendata$covariatesis supplied, the per-column path switches tobm_impute_col_with_cov(), which accepts a numeric lambda or"estimate"but not"cv"/"bayes"; those two modes are silently ignored (fit at lambda = 1) for BM-eligible columns and a warning is emitted.- lambda_fixed
optional named numeric vector (names = latent column names, i.e.
colnames(data$X_scaled)) giving a FIXED lambda per continuous-family column, overridinglambda_modeentirely for those columns (spec 4.5: reproduce a previous fit's exact per-trait lambda at predict time without re-estimating – typically supply that fit's own$lambda_per_trait). Columns not present inlambda_fixedkeep their lambda = 1 default.NULL(default) means every continuous-family column followslambda_modenormally.- em_iterations
integer. Number of Phase 6 EM iterations for the threshold-joint baseline (binary + ordinal + OVR categorical). Default
0Ldisables the EM loop and preserves v0.9.1 output byte-for-byte. When>= 1, the BM rate \(\Sigma\) learned by the in-house joint solver (R/joint_mvn_solver.R) at iteration \(k\) is fed back as the per-trait prior SD at iteration \(k+1\), up toem_iterationstimes or untilem_tolconvergence.em_iterations = 1Lis a degenerate single-pass run and produces the same baseline output as0L;>= 2Lis needed for actual iteration. Only affects the threshold-joint path (continuous-only traits pass through the existing joint MVN path unchanged).- em_tol
numeric. Relative-Frobenius convergence tolerance for the Phase 6 / 7 EM loop. Early-stops when \(||\Sigma_k - \Sigma_{k-1}||_F / ||\Sigma_{k-1}||_F < \)
em_tol. Default1e-3.- em_offdiag
logical. Phase 7 opt-in: when
TRUEANDem_iterations >= 2L, each liability cell's prior at iteration \(k+1\) is the conditional-MVN \((\mu, sd)\) given the posterior liability of other traits at iteration \(k\), using the full off- diagonal entries of \(\Sigma\). Binary + ordinal only (OVR categorical stays on Phase 6 diagonal). DefaultFALSEpreserves Phase 6 behaviour.- joint_solver
character. Which solver estimates the joint Sigma / posterior for the joint MVN, threshold-joint, and OVR categorical baselines.
"inhouse"(default) uses the single-pass in-house solver (R/joint_mvn_solver.R); underlambda_mode = "fixed_1"it is byte-identical to prior releases, butlambda_mode = "estimate"(the current default) is new behaviour, not a byte-compatibility guarantee."rphylopars"delegates toRphylopars::phylopars(); under"estimate"this calls it withmodel = "lambda"(substantially slower thanmodel = "BM"), with automatic fallback to"inhouse"on failure or implausible output (a plausibility guard that also fires underlambda_mode = "fixed_1"– see NEWS). Only affects the joint MVN / threshold-joint / OVR categorical paths above; ignored when those paths don't fire.lambda_modedoes NOT disable the continuous-only joint MVN path: bothfit_joint_mvn_baseline()andfit_joint_threshold_baseline()accept alambda_mode/lambda_fixedargument and estimate lambda inside the joint fit vialambda_cols– see “Per-type lambda dispatch” in Details. Only"cv"/"bayes"force continuous-family columns off the joint path entirely (no joint analogue for those two modes).- predict_method
character. Prediction route for the in-house joint solver.
"auto"(default, S5b/S5c) fits the baseline once with the"exact"route and once with the"per_column"route, then for each TRAIT (eachtrait_mapentry; a categorical trait's K latent columns and a zi_count trait's gate + magnitude columns are chosen together) picks whichever route has the lower loss on that trait's cells: mean squared error on the z-scored latent scale for continuous/count/proportion/ordinal/zi_count magnitude, mean log-loss ofplogis(mu)against the observed 0/1 truth for binary/zi_count gate, and mean multinomial log-loss for categorical. Which cells the choice sees: a trait's held-out validation rows (species in multi-observation data) are split into two halves (seeded viaseed, or the ambient RNG state whenseed = NULL) when the trait has at least 38 of them and the two candidate fits predict it differently. The route choice sees only half A; half B is reserved sofit_pigauto()/impute()calibrate the GNN gate and compute conformal scores on rows that never informed the choice. Below 38 rows each half would keep fewer than the 19 a 95\ split-conformal interval needs, so the same rows both choose the route and calibrate; when both fits agree, no choice is made and all rows calibrate. With fewer than 5 validation cells, or on a genuine tie, a trait keeps"exact", as it does whensplitsisNULL(no validation cells at all makes every trait"exact"). The chosen route per trait is returned as$predict_method_by_trait."exact"uses the full cross-trait conditional mean and variance ofvec(L) ~ MVN(0, Sigma %x% R(lambda_block))in the sparse precision form (Hadfield & Nakagawa, 2010), with each column centred at its own GLS phylogenetic mean atlambda_blockbefore the solve (mean-model consistency; seedocs/dev-log/exact-default/). Discrete liability columns (binary, zi gate, ordinal-via-OVR synthetic columns) sharelambda_blockunder"exact"rather than staying fixed at lambda = 1, so the whole joint fit uses one internally-consistentR(lambda_block). Falls back to"per_column"above roughly 20000 unknown cells (roughly 4000 species at 5 traits), on a singular/unusable Sigma, or when fewer than 2 joint columns or no Henderson sparse precision are available; the fallback prints a one-timemessage()(not a warning) per R session whenpredict_methodwas left at its default, or awarning()every time when"exact"was requested explicitly."per_column"retains the original per-column conditional prediction route (each column's own posterior, no cross-trait borrowing in the prediction step;seis that column's own conditional SE). Neither option changes covariance estimation or the"rphylopars"solver.- joint_refine_iter
integer, default
0L. Enables cross-trait refinement of the joint baseline's cell imputations using the estimated Sigma (the in-house solver'smax_iterEM cell-refinement;R/joint_mvn_solver.R).0Lpreserves current behaviour byte-for-byte. The refinement is guarded: the Sigma step must shrink each iteration, or the loop rolls back to the last good iterate and sets$diverged. Has no effect on any trait predicted by the"exact"route: the exact conditional solve returns its analytic posterior directly and never enters this EM loop. Under the defaultpredict_method = "auto",joint_refine_itertherefore only ever applies to traits that"auto"routes to"per_column"(see$predict_method_by_traitbelow).- predict_route
optional named character vector (names = trait names, i.e.
vapply(data$trait_map, function(tm) tm$name, character(1)); values"exact"or"per_column"). Forces the prediction route for each named trait, bypassing the"auto"validation-loss comparison entirely (and overridingpredict_method, if a value other than"auto"was also supplied). Traits not named default to"exact". A name that matches no trait indata$trait_mapis ignored, with a warning (once per call). Used internally to replay a validation-split"auto"choice onto a production refit that has no validation cells (e.g.fit_pigauto()'sbaseline_full, fit withsplits = NULL) so that refit reuses the SAME per-trait route rather than re-deciding with zero validation evidence.NULL(default) disables forcing;predict_methodgoverns normally.- seed
optional integer. Seeds the deterministic half-A (route choice) / half-B (reserved for
fit_pigauto()/impute()'s gate calibration and conformal scoring) split of each trait's validation cells underpredict_method = "auto"with real validation cells (see “"auto"” above).NULL(default) leaves the split to whatever the ambient RNG state is at call time, matching this package's other optionalseedarguments. Ignored whenpredict_methodis"exact"/"per_column", whenpredict_routeis supplied, or when there are no validation cells.
Value
A list with:
- mu
Numeric matrix (n_species x p_latent), baseline means in latent scale.
- se
Numeric matrix (n_species x p_latent), standard errors.
- path
Named character vector, one entry per
trait_mapentry (name = trait/group name), recording which dispatch branch produced that trait's baseline:"joint_mvn"(Phase 2 continuous-only joint),"threshold_joint"(Phase 3/5 binary + ordinal + continuous-family liability joint),"ovr_categorical"(Phase 6 one-vs-rest K-fits),"per_column_bm"(per-column Brownian-motion conditional MVN, including the lambda-aware path and the per-trait ordinal fallback that beats threshold-joint on held-out val MSE),"multi_proportion_bm"(per-componentper_column_bmon the CLR columns of a multi_proportion group – same kernel as"per_column_bm", labelled separately because multi_proportion is architecturally distinct),"label_propagation"(phylogenetic label propagation for binary/categorical/ordinal, and the per-trait ordinal fallback that beats the other two candidates),"zi_gate_lp"(label propagation for a zero-inflated count's gate column), or"zi_mag_constant"(global mean/sd fallback for a zero-inflated count's magnitude column when fewer than 5 non-zero observations exist). Forzi_counttraits (2 latent columns: gate then magnitude),pathreports the GATE column's dispatch – the magnitude column can independently land on"joint_mvn","threshold_joint","per_column_bm", or"zi_mag_constant"and is not separately surfaced here.- lambda_per_trait
Named numeric vector (length
p_latent, names =colnames(data$X_scaled)), the lambda actually used for each latent column: 1 for every column not eligible for lambda estimation (all discrete columns; continuous-family columns underlambda_mode %in% c("cv", "bayes")), else the estimated or fixed value.- lambda_block
Numeric scalar, the shared lambda used internally by whichever joint fit ran for its Sigma M-step / opt-in exact conditional / opt-in EM refine (see
R/joint_mvn_solver.R's$lambda_block);NAwhen no joint fit ran.- lambda_mode
Character, echoes the resolved
lambda_modeargument.- predict_method_used
Character scalar,
"exact","per_column", or"auto". Under a concretepredict_method("exact"/"per_column"), aggregated across every joint fit that ran:"exact"only if every joint fit achieved exact;"per_column"if any fell back, if none ran (a pure per-column baseline), or if every joint fit usedjoint_solver = "rphylopars"(no exact/per_column dichotomy there). Underpredict_method = "auto"(or a forcedpredict_route), always"auto"; see$predict_method_by_traitfor the resolved per-trait routes.- predict_method_by_trait
Named character vector (names = trait names), one entry per
trait_mapentry, giving the route ("exact"or"per_column") actually used for that trait's cells. Under a concretepredict_method, every entry equalspredict_method_used. Under"auto"(or a forcedpredict_route), the per-trait validation choice (seepredict_method's"auto"entry above).
Details
When splits is supplied the val and test cells are masked to
NA before fitting, so the baseline is evaluated under the same
conditions as fit_pigauto.
Continuous-family columns use Brownian-motion conditional MVN baselines on the phylogenetic correlation matrix, either independently or through the joint MVN path when the data and optional dependencies support it. Binary, ordinal, categorical, and zero-inflated gate columns use the appropriate label-propagation or threshold/liability baseline candidates, with per-column fallbacks when a joint path is not available.
Covariates and the joint baseline (P1-8): data$covariates
is only used by the per-column BM path (bm_impute_col_with_cov()).
The joint MVN and threshold-joint (Rphylopars) baselines do not accept a
covariate design matrix, so when a joint path is selected (BM-eligible
columns >= 2, or binary/ordinal cols present, with Rphylopars available)
any supplied covariates are ignored for the BASELINE and a warning is
emitted; covariates still reach the GNN correction via
fit_pigauto regardless of which baseline path fires.
Per-type lambda dispatch (arc/lambda-per-type; corrected in S4/S6,
feat/joint-lambda-default): lambda_mode only ever governs the
baseline for CONTINUOUS-FAMILY columns (continuous, count, proportion,
zi_count magnitude) – NOT ordinal. Binary, ordinal, categorical, and
zero-inflated gate columns always stay at lambda = 1 in EVERY path
(threshold-joint, OVR-categorical, label propagation, and the per-trait
ordinal path-selection fallback below), regardless of lambda_mode
– there is no discrete-trait analogue of Pagel's lambda, and previously
forcing these columns onto label propagation any time lambda_mode
!= "fixed_1" cost 19pp of Trophic.Level accuracy on AVONET (0.789 ->
0.600; see docs/dev-log/2026-08-16-external-comparison-results.md).
When lambda_mode != "fixed_1" and the threshold-joint baseline
fires for a dataset with binary/ordinal AND continuous-family columns,
the joint liability fit's own continuous-column OUTPUT is now USED
(each continuous-family column gets its own lambda_k via the
joint solver's lambda_cols), rather than being discarded for a
separate lambda-aware per-column BM re-fit as in the pre-S4 design.
Ordinal liability columns inside that same joint fit, and the
BM-via-MVN / K-class-OVR-LP alternatives the "Per-trait ordinal path
selection" block below compares against it, all stay at lambda = 1.
Examples
# \donttest{
data(avonet300, tree300, package = "pigauto")
tree <- ape::keep.tip(tree300, tree300$tip.label[seq_len(30L)])
traits <- avonet300[match(tree$tip.label, avonet300$Species_Key),
c("Mass", "Wing.Length"), drop = FALSE]
rownames(traits) <- tree$tip.label
pd <- preprocess_traits(traits, tree)
splits <- make_missing_splits(pd$X_scaled, trait_map = pd$trait_map)
bl <- fit_baseline(pd, tree, splits)
# }
