This vignette collects the questions early users most often hit when running pigauto on their own data. Each section follows the same template:
If you hit something that isn’t here and feels surprising, please open an issue: most of the items below were added because real users tripped on them.
impute() stopped because my input is fully
observed”Symptom. You call impute(df, tree) on a
fully-observed dataset and it stops before training.
Why this happens. impute() fills
NA cells in rows matched to the tree. It stops before
fitting only when the entire matched target table has no missing cells;
one complete column is not enough to trigger the stop if another matched
target cell is missing. For predictive assessment, hide known values
before fitting and compare predictions with the held-out truth, or use
cross_validate().
Diagnose.
library(pigauto)
data(avonet300, tree300)
df <- avonet300
rownames(df) <- df$Species_Key
df$Species_Key <- NULL
sum(is.na(df)) # if 0, there's nothing for impute() to doFix. Mask some cells before calling
impute(), then evaluate predictions only on the held-out
cells:
library(pigauto)
data(avonet300, tree300)
df <- avonet300
rownames(df) <- df$Species_Key
df$Species_Key <- NULL
set.seed(1L)
n_hide <- min(30L, sum(!is.na(df$Mass)))
if (n_hide == 0L) stop("No observed Mass values are available to hide.")
hide <- sample(which(!is.na(df$Mass)), n_hide)
df_obs <- df
df_obs$Mass[hide] <- NA # hide up to 30 mass values
result <- impute(df_obs, tree300)
result$completed$Mass[hide] # pigauto's imputations
df$Mass[hide] # held-out truth, for comparison
sum(result$imputed_mask[, "Mass"]) # number of values hidden aboveFor your own data with real NAs, the mask marks cells
targeted for imputation; a marked cell can still be NA if
no prediction was available. Extract by trait to preserve mixed column
types:
mask <- result$imputed_mask
imputed_values <- lapply(colnames(mask), function(nm) {
result$completed[[nm]][mask[, nm]]
})
names(imputed_values) <- colnames(mask)Check the selected values for remaining NAs. To keep
only successful fills for one trait, add
!is.na(result$completed[[nm]]) to that trait’s mask. The
full-cell prediction in result$prediction$imputed is for
diagnostics, not a replacement for the completed data.
See also. ?impute (“What gets imputed
(read this first)”), issue #67.
Symptom. You impute an ordinal trait and the
prediction is the majority class for every species. For example, on
avonet300$Migration (K = 3 ordinal: Resident / Partial /
Full), 300/0/0.
Why this happens. Two things compound:
NAs,
impute() stops before fitting; use
cross_validate() or mask known values for assessment (see
Pitfall 1). A complete Migration column alone does not
cause the stop.n_imputations = 1L,
pool_method = "median"), a small ordinal trait whose
marginal distribution is heavily skewed (AVONET Migration
is ~78 % Resident / 14 % Partial / 8 % Full at n = 300) can have its
calibrated gate snap to a corner that returns the majority class for
every species.Diagnose.
library(pigauto)
data(avonet300, tree300)
df <- avonet300; rownames(df) <- df$Species_Key; df$Species_Key <- NULL
table(df$Migration) # check the marginal distribution
hide <- sample(which(!is.na(df$Migration)), 30L)
df$Migration[hide] <- NA
result <- impute(df, tree300, verbose = FALSE)
table(result$completed$Migration[hide])Fix. Use cross_validate() to assess
model-level fit settings on your own preprocessed trait table. It does
not compare the number of imputations or pooling rule. To compare those
impute() choices, score each result against the same
prespecified, hidden measurements, as in the example below. First
prepare the input for the model-level cross-validation check:
A closed gate is a calibration outcome on the validation data; it
does not by itself establish high phylogenetic signal or explain why the
GNN failed to help. Check validation size and compare candidate
predictions under the same held-out design. For an imbalanced ordinal
trait, more draws with pool_method = "mode" is one
candidate to evaluate, not a resolution or package-wide
recommendation.
library(pigauto)
data(avonet300, tree300)
df <- avonet300; rownames(df) <- df$Species_Key; df$Species_Key <- NULL
set.seed(1L)
n_hide <- min(30L, sum(!is.na(df$Migration)))
if (n_hide == 0L) stop("No observed Migration values are available to hide.")
hide <- sample(which(!is.na(df$Migration)), n_hide)
df_obs <- df
df_obs$Migration[hide] <- NA
# Default settings: prone to majority-class collapse on imbalanced K = 3
result <- impute(df_obs, tree300, verbose = FALSE)
table(result$completed$Migration[hide], df$Migration[hide])
# Candidate for K = 3 ordinal: more draws + mode pooling
result_mode <- impute(df_obs, tree300, n_imputations = 20L,
pool_method = "mode", verbose = FALSE)
table(result_mode$completed$Migration[hide], df$Migration[hide])See also. ?impute (“Imbalanced K-class
traits”) and issue #68. The
mode-pooling comparison was a three-seed study on an AVONET subset; its
result is specific to that design.
This section applies when you fit with gnn = TRUE. With
the default gnn = FALSE no GNN is trained, so
r_cal_gnn is 0 by construction and predictions come from
the calibrated baseline.
Symptom. You expected the GNN to improve
predictions, but inspecting the fitted model shows the calibrated gate
is fully or near-fully closed (r_cal_gnn ≈ 0): the GNN
contributes little or nothing to the calibrated prediction.
Why this happens. This is a possible calibration
outcome, not by itself a sign of high phylogenetic signal. With
safety_floor = FALSE, the mean candidate is absent; with
safety_floor = TRUE, calibration can also choose the grand
mean. After training, pigauto compares the baseline and GNN predictions
on held-out validation cells. When safety_floor = TRUE, it
also compares a grand-mean prediction. These candidates receive
nonnegative weights that add to one. If adding the GNN does not improve
the best available combination of the baseline and, when enabled, the
grand mean, its weight can be r_cal_gnn = 0. The grand mean
is not a candidate when safety_floor = FALSE. The result
depends on the validation data and metric; inspect validation size and
compare the baseline, GNN, and any mean candidate before interpreting
it.
Diagnose.
This diagnostic fits a GNN, so the optional torch runtime must be
installed and available. The explicit gnn = TRUE argument
matters: without it, the default fit trains no network and its GNN gate
is zero by construction.
library(pigauto)
data(avonet300, tree300)
df <- avonet300; rownames(df) <- df$Species_Key; df$Species_Key <- NULL
set.seed(1L)
hide <- sample(which(!is.na(df$Migration)), 30L)
df_obs <- df
df_obs$Migration[hide] <- NA
fit <- impute(df_obs, tree300, gnn = TRUE, verbose = FALSE)$fit
# Per-latent-column calibrated gates:
fit$r_cal_bm # r assigned to the BM baseline
fit$r_cal_gnn # r assigned to the GNN delta
fit$r_cal_mean # r assigned to the grand meanA row where r_cal_gnn is small (< 0.1) means the gate
has effectively closed for that latent column.
Fix. First treat the closed gate as the calibration result for this validation split, not as evidence about signal strength. Compare held-out predictions from the baseline and GNN under the same data split; check that the validation set is large enough for the comparison. If you suspect the GNN should help (for example, you added covariates or expect cross-trait structure), also check that those inputs vary and are available:
For the paired comparison, run cross_validate() twice on
the same prepared data, with the same seeds value and
gnn = FALSE and gnn = TRUE. This reuses the
same held-out folds for both fits. The GNN guide’s
comparison example shows the setup, code, and how to compare the
returned metrics.
?fit_pigauto “Calibration at small n”).See also. ?fit_pigauto
(phylo_signal_gate, “Safety floor”) and the validation results, which report
tested settings and their limits.
With multiple rows per species, the baseline aggregates observations
and covariates to species level. With gnn = FALSE, it
cannot use row-specific covariate differences to refine predictions
within a species. Joint baseline routes can omit covariates altogether;
check the selected route and warnings. The GNN can use row-level
covariates, but fitted predictions can vary with the seed. Compare
several seeds on the same held-out cells to assess stability; a fixed
seed makes a run repeatable, not necessarily representative.
For an untransformed continuous trait, a global per-trait conformal score gives the same interval width for all rows in a fit. It does not adapt the width to how many observations a species has. Transformations and other scoring methods can produce a different pattern. Held-out coverage is useful to report, but coverage in one dataset does not establish conditional coverage for each species.
Symptom. You aren’t sure whether phylogenetic information is useful for your dataset.
Why this matters. A lambda estimate describes fitted phylogenetic structure; it does not determine predictive performance on a new dataset. Use held-out evaluation rather than a generic threshold to compare a phylogenetic workflow with an appropriate alternative.
Diagnose. The fitted object stores the per-trait λ
values used by phylo_signal_gate (they are computed only
when the gate is switched on, which is no longer the default):
library(pigauto)
data(avonet300, tree300)
df <- avonet300; rownames(df) <- df$Species_Key; df$Species_Key <- NULL
set.seed(1L)
hide <- sample(which(!is.na(df$Mass)), 30L)
df_obs <- df
df_obs$Mass[hide] <- NA
fit <- impute(df_obs, tree300, phylo_signal_gate = TRUE)$fit
fit$phylo_signal_per_traitThe output reports λ on the observed cells where it can be estimated. Discrete traits use the package’s internal continuous proxy on the latent/liability scale.
Fix. Use the lambda estimate as a diagnostic, then
run cross_validate() on a preprocessed input and compare
candidate workflows under the same held-out design. If phylogenetic
structure is weak, consider whether a non-phylogenetic analysis is more
appropriate for your scientific question:
library(pigauto)
data(avonet300, tree300)
df <- avonet300; rownames(df) <- df$Species_Key; df$Species_Key <- NULL
pd <- preprocess_traits(df, tree300)
cv <- cross_validate(pd, tree300)See also. ?fit_pigauto
(phylo_signal_gate) and the validation results for tested
settings and limitations. The historical
Pagel’s λ signal-sweep results report one design with 300 species,
30% missingness, and three replicates; they do not establish general
expected performance.
Symptom. A masked log-transformed continuous trait (body mass, seed mass, fish weight) predicts a value 50–100× larger than anything observed. On AVONET, the canonical case is the cassowary: truth ≈ 35 kg, predicted up to ~540 kg.
Why this happens. A single prediction can be far
beyond the observed range because a modest extrapolation on the
transformed latent scale becomes large after inverse transformation.
Log-transformed continuous traits use exp();
log1p-transformed count and positive zi_count magnitudes
use expm1(). A single impute() prediction is
not an MC-dropout draw. Stochastic tail variation can arise when you
request multiple imputations with
draws_method = "mc_dropout"; a small number of draws can
then leave the pooled value sensitive to a rare tail draw.
Diagnose.
This diagnostic assumes you have already created df and
result by running impute(); here,
df is the original trait table and result is
the returned pigauto_result.
# After running impute(), check whether any imputation exceeds the
# observed maximum by an unrealistic factor:
predicted_mass <- result$completed$Mass[result$imputed_mask[, "Mass"]]
obs_max <- max(df$Mass, na.rm = TRUE)
sum(predicted_mass > 5 * obs_max)A non-zero count is a signal of tail extrapolation.
Fix. clamp_outliers = TRUE caps
post-back-transform predictions for log-transformed continuous, count,
and zi_count magnitude traits at
obs_max * clamp_factor (default 5). This is opt-in because
a cap changes imputed values and can hide genuinely extreme species.
Check units and species-name matching first. Assess the prediction rule
by hiding known values and comparing predictions with their truth. Use a
cap only when the chosen limit is defensible for the trait and study
system, and report the selected multiplier.
library(pigauto)
data(avonet300, tree300)
df <- avonet300; rownames(df) <- df$Species_Key; df$Species_Key <- NULL
set.seed(1L)
n_hide <- min(30L, sum(!is.na(df$Mass)))
if (n_hide == 0L) stop("No observed Mass values are available to hide.")
hide <- sample(which(!is.na(df$Mass)), n_hide)
df_obs <- df
df_obs$Mass[hide] <- NA
result <- impute(df_obs, tree300,
clamp_outliers = TRUE,
clamp_factor = 5,
verbose = FALSE)See also. ?impute
(clamp_outliers, clamp_factor arguments). The
Mass diagnosis and clamp comparison were historical analyses on specific
data and settings; they do not establish a general prediction rule.