Skip to contents

Aligns species in the trait data frame to the tree, detects or accepts trait types (continuous, binary, categorical, ordinal, count, proportion, zi_count), and encodes each trait into a continuous latent matrix.

Usage

preprocess_traits(
  traits,
  tree,
  species_col = NULL,
  trait_types = NULL,
  multi_proportion_groups = NULL,
  log_cols = NULL,
  log_transform = TRUE,
  center = TRUE,
  scale = TRUE,
  covariates = NULL,
  ordinal_levels = NULL
)

# S3 method for class 'pigauto_data'
print(x, ...)

Arguments

traits

data.frame with species as row names (one row per species), or with a species column identified by species_col (potentially multiple rows per species). Columns may be numeric, integer, factor, ordered, character, or logical.

tree

object of class "phylo".

species_col

character. Name of the column in traits that identifies species. When supplied, traits may have multiple rows per species. The column is removed from trait columns before encoding. Default NULL uses row names (one row per species).

trait_types

named character vector overriding auto-detection, e.g. c(Mass = "continuous", Diet = "categorical"). Valid types: "continuous", "binary", "categorical", "ordinal", "count", "proportion", "zi_count". Proportion and zi_count are override-only (not auto-detected). Unspecified traits are auto-detected. Note that "multi_proportion" is NOT set here — use multi_proportion_groups instead.

multi_proportion_groups

named list declaring compositional (multi-proportion) trait groups. Each element is a character vector of column names whose row-wise values sum to 1 (e.g. list(diet = c("plants", "insects", "fish"))). The group name becomes a single trait in the output, encoded via centred log-ratio (CLR) + per-component z-score. Group names must NOT match any column in traits. Default NULL.

log_cols

character vector of continuous trait names to log-transform. Default NULL means auto-detect (log if all observed values are positive). Set to character(0) to disable.

log_transform

logical. Legacy parameter: if TRUE and log_cols is NULL, log-transform all continuous traits with all-positive values. Overridden by log_cols when both are supplied. Traits already on a log scale should not be logged again: set log_transform = FALSE or leave them out of log_cols (see impute).

center

logical. Subtract column means for continuous traits, log1p counts, ordinal scores, logit proportions, nonzero zero-inflated-count magnitudes, and each compositional CLR component. Binary/categorical indicators and the zero-inflation gate are unchanged. Default TRUE.

scale

logical. Divide those same transformed values by their column SDs; binary/categorical indicators and the zero-inflation gate are unchanged. Default TRUE.

covariates

data.frame or numeric matrix of environmental covariates. Covariates are conditioners: they inform imputation but are not themselves imputed, so they must be fully observed (no NAs — if a variable has missing values, put it in traits instead). Rows are aligned to the same species / observation order as X_scaled after traits are matched to the tree. Single-obs: rownames should match tree$tip.label (they are reordered to tip order). Multi-obs: include the same species_col and keep observations in the same order within each species in both frames. Repeated species labels alone do not identify individual observations: matching takes the next unused covariate row for each species. Align by an explicit observation ID before calling if within-species rows were shuffled independently. Unnamed covariates are assumed to share the input-row order of traits and receive the same drop / reorder. Matching nrow alone is not enough — unmatched species names are an error.

Numeric / integer columns

z-scored automatically.

Factor / ordered columns

one-hot encoded (K binary columns per factor with K levels). Column names become "var.level".

Character / logical columns

coerced to factor, then one-hot.

Default NULL (no covariates).

ordinal_levels

named list giving the scientific order of ordinal levels, e.g. list(status = c("LC", "NT", "VU", "EN", "CR")). Each named column is coerced to an ordered factor with exactly those levels before type detection, so no trait_types entry is needed: the coerced column detects as "ordinal", or as "binary" (order preserved) when exactly two levels are declared — the same detection an ordered() column with those levels receives. Observed values absent from the declared levels are an error. Same semantics as the ordinal_levels argument of read_traits. Default NULL.

x

A preprocessed trait-data object.

...

Ignored. Included for compatibility with the base print() generic.

Value

A list of class "pigauto_data" with components:

X_scaled

Numeric matrix (n_obs x p_latent), latent encoding. When species_col is NULL, n_obs = n_species.

X_raw

Numeric matrix of continuous traits after optional log but before z-scoring (for backward compatibility).

X_original

Original data.frame (aligned to tree, before encoding).

means

Named numeric vector of scalar means used for centering continuous, count, ordinal, proportion, and zero-inflated-count magnitude traits. Multi-proportion component means are vectors stored in the corresponding trait_map entry.

sds

Named numeric vector of scalar SDs for those same traits. Multi-proportion component SDs are vectors stored in the corresponding trait_map entry.

species_names

Character vector of unique species matching tree$tip.label order (length = n_species).

obs_species

Character vector of species labels per observation (length = n_obs). When multi-obs, can have duplicates. NULL when species_col is NULL.

obs_to_species

Integer vector (length = n_obs) mapping each observation to its species index in species_names. NULL when species_col is NULL.

n_species

Integer, number of unique species.

n_obs

Integer, number of observations (= n_species when single-obs).

multi_obs

Logical, TRUE whenever species_col is supplied, including with exactly one observation per species.

trait_names

Character vector of original trait names.

latent_names

Character vector of latent column names.

trait_map

List of trait descriptors (see Details).

p_latent

Integer, total number of latent columns.

log_transform

Logical, legacy field (TRUE if any continuous trait was log-transformed).

The print method returns x invisibly.

Details

When each species has one observation (the default), output rows match tree$tip.label order. When species_col is supplied, multiple observations per species are supported: the output matrix has one row per observation, plus an obs_to_species mapping for the GNN (which operates at species level).

Automatic type detection (when trait_types = NULL) follows the R class of each column — no user input is required for most data:

R classpigauto type
numeric (non-integer)continuous
integer, all nonnegativecount
integer, any negative valuecontinuous
factor with 2 levelsbinary
factor (unordered) with >2 levelscategorical
ordered with 2 levelsbinary
ordered with >2 levelsordinal
characterconverted to factor, then binary or categorical
logicalbinary (FALSE = 0, TRUE = 1)

Whole-number measurements can still be scientifically continuous. Use trait_types = c(measurement = "continuous") to override count detection.

Two types require an explicit override because they cannot be distinguished from their R class alone:

"proportion"

A numeric column bounded 0–1 looks identical to continuous. Declare it explicitly: trait_types = c(SurvivalRate = "proportion"). Encoded via qlogis(clamp(x, 0.001, 0.999)).

"zi_count"

An integer column with excess zeros looks identical to count. Declare it explicitly: trait_types = c(Parasites = "zi_count"). Encoded as a binary zero/non-zero gate plus log1p-z magnitude.

Practical examples of type assignment:

  • Body mass (numeric, all positive) → continuous, auto-log-transformed.

  • Clutch size (integer) → count.

  • Migratory (factor with levels "Yes"/"No") → binary.

  • Diet (factor with >2 levels) → categorical.

  • IUCN status (ordered factor, LC < NT < VU < EN < CR) → ordinal. If left as an unordered factor, it becomes categorical — both are valid depending on the question.

  • Parasite load (integer with many zeros) → needs trait_types = c(Parasites = "zi_count").

  • Survival rate (numeric, values in 0 to 1) → needs trait_types = c(Survival = "proportion").

Species and composition alignment: trait rows whose species are absent from the tree are dropped with a warning. Tree tips without trait rows remain in the aligned data with all trait values missing; no shared species is an error. For a multi_proportion_groups composition, a row is used only when every component is present, so a partly missing composition is treated as missing as a whole. Complete compositions must be non-negative and sum to 1 within 1 percent; zero components are replaced by a small positive value for the log-ratio transform.

Latent encoding per type:

continuous

optional log(), then z-score (1 latent column)

binary

0/1 encoding (1 latent column)

count

log1p(), then z-score (1 latent column)

ordinal

integer coding (0 to K-1), then z-score (1 latent column)

categorical

one-hot encoding (K latent columns)

proportion

qlogis(clamp(x, 0.001, 0.999)), then z-score (1 latent column)

zi_count

gate (0/1) + log1p-z of non-zeros (2 latent columns)

multi_proportion

centred log-ratio (CLR) + per-component z-score (K latent columns per group). Rows must sum to 1. Declared via the multi_proportion_groups argument, not trait_types.

Printing

Reports species and trait counts, and observation counts plus the observation-per-species range and median in multi-observation mode. With trait_map, it reports type counts and the latent-column count. Without trait_map, it reports column names and log-transform status. The latent-cell missing fraction is printed only when greater than zero and can differ from original-cell missingness. Covariates are stored in x$covariates, not displayed by this method. The dots argument is unused.

Examples

# Single-obs per species (backward compatible)
data(avonet300, tree300, package = "pigauto")
traits <- avonet300
rownames(traits) <- traits$Species_Key
traits$Species_Key <- NULL
pd <- preprocess_traits(traits, tree300)
dim(pd$X_scaled)   # 300 x p_latent
#> [1] 300  14

# Multi-obs per species (via species_col)
pd2 <- preprocess_traits(avonet300, tree300, species_col = "Species_Key")
pd2$n_obs      # number of observations
#> [1] 300
pd2$n_species  # number of unique species
#> [1] 300