2. From calls to quantitative traits
Source:vignettes/quantitative-traits.Rmd
quantitative-traits.RmdVignette 1 produced one call per curated GIFT for one genome. This one turns those calls into numbers you can compare between genomes — and shows why a number of that kind is meaningless until you say what it was counted over.
We reuse the illustrative gut anaerobe from vignette 1.
gut_anaerobe <- data.frame(
gene_id = paste0("g", sprintf("%04d", 1:33)),
namespace = c(rep("CAZY", 3), rep("KO", 30)),
accession = c(
"GH11", "GH39", "CE1",
"K10543", "K10544", "K10545", "K01805", "K00854",
"K00764", "K01945", "K00601", "K01952", "K01587", "K01756", "K00602",
"K03412", "K00575", "K03407", "K03408", "K03776", "K03413", "K03410",
"K03427", "K01153", "K01154",
"K00812", "K01915", "K00600", "K00826", "K00259",
"K02945", "K02863", "K01873"
),
stringsAsFactors = FALSE
)
result <- evaluate_gifts(gut_anaerobe)
sum(result$gifts$complete)
#> [1] 10The problem: five is not a result
This genome supports ten GIFTs. Is that a lot?
The question has no answer until you say ten out of what. Out of the catalogue curated today? Out of all metabolic ones? Out of the amino-acid biosynthetic capabilities, of which this genome was tested for none? And when the next curation campaign adds forty GIFTs, does “ten” still mean the same thing?
gifter’s answer is that every quantitative trait names its reference frame, and carries that name in the output beside the number.
Step 1: define a frame
reference_frame() builds one from curated metadata — the
GIFT type, the metabolic mode, the registered facet vocabulary, the
derived profile view. You never write a list of GIFT identifiers
yourself; that would move biological content out of the database, where
it can be validated and versioned, and into your script, where it
cannot.
Common questions already have named, versioned presets. Inspect them before writing a custom filter:
presets <- list_reference_frames()
presets[, c("frame_id", "bounded", "member_count")]
#> # A tibble: 19 × 3
#> frame_id bounded member_count
#> <chr> <lgl> <int>
#> 1 amino_acid_autonomy TRUE 22
#> 2 aromatic_catabolism FALSE 12
#> 3 biomass_essential_anabolism TRUE 44
#> 4 carbohydrate_degradation FALSE 18
#> 5 carbon_acquisition FALSE 36
#> 6 chemical_detoxification FALSE 5
#> 7 cofactor_autonomy TRUE 15
#> 8 extracellular_public_goods FALSE 3
#> 9 fermentation_products FALSE 11
#> 10 host_glycan_foraging FALSE 7
#> 11 nitrogen_acquisition FALSE 20
#> 12 nucleotide_autonomy TRUE 4
#> 13 nutrient_uptake FALSE 3
#> 14 plant_fibre_utilisation FALSE 13
#> 15 released_bioactive_products FALSE 5
#> 16 shikimate_derived_biosynthesis FALSE 10
#> 17 short_chain_fatty_acids FALSE 4
#> 18 sulfur_acquisition FALSE 4
#> 19 vitamin_biosynthesis FALSE 20
carbohydrate <- reference_frame(preset = "carbohydrate_degradation")
carbohydrate
#> <gifter_frame> Carbohydrate degradation
#> preset: carbohydrate_degradation
#> GIFTs: 18
#> bounded: FALSE
#> database version: 2026.39.1Choose a preset from your research question
The searchable Frames view in the Atlas is the quickest chooser: search for a substrate, process or metric, then narrow the cards to genome, community, network or bounded-coverage analyses. The full registry is reproduced below so that the choice remains visible in this tutorial too. “Current GIFTs” describes this database release, not a fixed list stored in the preset.
| Research question | Preset | Use it for | Current GIFTs | Recommended starting metrics | Denominator |
|---|---|---|---|---|---|
| Amino-acid autonomy | amino_acid_autonomy | Biomass-essential anabolic GIFTs in the amino-acid substrate class. | 22 | community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction | Bounded: coverage fractions are valid |
| Aromatic catabolism | aromatic_catabolism | GIFTs that activate, cleave or funnel aromatic rings toward central metabolism. | 12 | community: community_richness, provider_count; genome: gift_richness; network: distributed_chain_links | Open: use counts, breadth or distribution |
| Biomass-essential anabolic GIFTs | biomass_essential_anabolism | Anabolic GIFTs whose declared output is a curated biomass-essential anchor. | 44 | community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction | Bounded: coverage fractions are valid |
| Carbohydrate degradation | carbohydrate_degradation | Catabolic GIFTs acting on polysaccharides, neutral monosaccharides, amino sugars or uronates. | 18 | community: abundance_coverage, community_richness, provider_count; genome: breadth_substrate_class, gift_richness | Open: use counts, breadth or distribution |
| Carbon acquisition | carbon_acquisition | GIFTs that convert an acquired substrate into central carbon metabolism. | 36 | community: community_richness, provider_count; genome: breadth_substrate_class, gift_richness | Open: use counts, breadth or distribution |
| Chemical detoxification | chemical_detoxification | Defense GIFTs implementing a complete curated chemical-detoxification mechanism. | 5 | community: community_richness, provider_count; genome: gift_richness | Open: use counts, breadth or distribution |
| Cofactor autonomy | cofactor_autonomy | Biomass-essential anabolic GIFTs in the cofactor substrate class. | 15 | community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction | Bounded: coverage fractions are valid |
| Extracellular processing outputs | extracellular_public_goods | Metabolic GIFTs derived as consuming and releasing extracellular anchors. | 3 | community: abundance_coverage, provider_count; genome: gift_richness; network: handoff_edges | Open: use counts, breadth or distribution |
| Fermentation products | fermentation_products | GIFTs that form a curated terminal fermentation product. | 11 | community: community_richness, provider_count; genome: breadth_substrate_class, gift_richness | Open: use counts, breadth or distribution |
| Host glycan foraging | host_glycan_foraging | GIFTs acting on host-derived glycans or the sugars released from them. | 7 | community: abundance_coverage, provider_count; genome: gift_richness | Open: use counts, breadth or distribution |
| Nitrogen acquisition | nitrogen_acquisition | GIFTs whose curated role makes nitrogen available from inorganic or organic sources. | 20 | community: community_richness, provider_count; genome: breadth_substrate_class, gift_richness | Open: use counts, breadth or distribution |
| Nucleotide autonomy | nucleotide_autonomy | Biomass-essential anabolic GIFTs in the nucleotide substrate class. | 4 | community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction | Bounded: coverage fractions are valid |
| Nutrient uptake | nutrient_uptake | Metabolic GIFTs derived as moving an extracellular anchor to a cytoplasmic anchor. | 3 | community: abundance_coverage, provider_count; genome: gift_richness; network: distributed_chain_links | Open: use counts, breadth or distribution |
| Plant fibre utilisation | plant_fibre_utilisation | GIFTs curated with a fibre-degradation role, including polymer processing, uptake and intracellular catabolism of released sugars. | 13 | community: community_richness, provider_count; genome: gift_richness; network: distributed_chain_links | Open: use counts, breadth or distribution |
| Released bioactive products | released_bioactive_products | GIFTs whose chemistry forms a product curated as potentially bioactive when released. | 5 | community: abundance_coverage, provider_count; genome: gift_richness | Open: use counts, breadth or distribution |
| Shikimate-derived biosynthesis | shikimate_derived_biosynthesis | Biosynthetic GIFTs whose product carbon skeleton is derived from the shikimate pathway. | 10 | community: community_richness, provider_count; genome: gift_richness | Open: use counts, breadth or distribution |
| Short-chain fatty acids | short_chain_fatty_acids | GIFTs whose curated substrate or product class is a short-chain fatty acid. | 4 | community: abundance_coverage, community_richness, provider_count; genome: gift_richness | Open: use counts, breadth or distribution |
| Sulfur acquisition | sulfur_acquisition | GIFTs whose curated role makes sulfur available from inorganic or organic sources. | 4 | community: community_richness, provider_count; genome: gift_richness | Open: use counts, breadth or distribution |
| Vitamin biosynthesis | vitamin_biosynthesis | GIFTs participating in curated vitamin and cofactor biosynthetic capabilities. | 20 | community: community_richness, provider_count; genome: gift_richness | Open: use counts, breadth or distribution |
Choose the frame from the biological question first, then use its scope-specific metric recommendations:
- for one genome, start with
gift_richnessor the recommended breadth metric; - for a community, use
community_richnessto count represented capabilities andprovider_countfor redundancy, keeping abundance separate; - use a network metric only when the question depends on compatible, extracellular GIFT boundaries;
- use
supported_fractionorcommunity_coverageonly where the table marks the frame bounded, and always reportassessable_fractionalongside a genome fraction.
Each preset carries its description, interpretation limits and
recommended genome, community or network metrics. Its members are still
resolved from the current release’s metadata: the registry stores a
query, never a list of GIFT identifiers. An open preset cannot be
promoted to bounded = TRUE at runtime.
For a question that has no preset, build a custom frame directly:
reference_frame(type = "metabolic")
#> <gifter_frame> GIFTs where type = metabolic
#> GIFTs: 143
#> bounded: FALSE
#> database version: 2026.39.1
reference_frame(mode = "catabolic")
#> <gifter_frame> GIFTs where mode = catabolic
#> GIFTs: 69
#> bounded: FALSE
#> database version: 2026.39.1
reference_frame(facet = "substrate_class", value = "polysaccharide")
#> <gifter_frame> GIFTs where facet = substrate_class, value = polysaccharide
#> GIFTs: 9
#> bounded: FALSE
#> database version: 2026.39.1Filters combine with AND:
reference_frame(mode = "catabolic", resource_strategy = "uptake")
#> <gifter_frame> GIFTs where mode = catabolic, resource_strategy = uptake
#> GIFTs: 0
#> bounded: FALSE
#> database version: 2026.39.1Note that mode partitions this database more finely than
gift_type does: most current GIFTs are metabolic, while
mode separates those capabilities into anabolic, catabolic, transport
and interconversion questions. The current counts are returned by
list_gifts() rather than fixed in the tutorial.
Step 2: compute traits
traits <- genome_traits(result, genome_id = "MAG_001")
traits
#> <gifter_traits> MAG_001
#> metrics: 274 rows across 14 reference frames
#> database version: 2026.39.1
#>
#> all curated GIFTs 10 / 163 supported
#> defense GIFTs 1 / 7 supported
#> metabolic GIFTs 8 / 143 supported
#> regulatory GIFTs 1 / 4 supported
#> structural GIFTs 0 / 9 supported
#> anabolic GIFTs 5 / 67 supported
#> catabolic GIFTs 2 / 69 supported
#> interconversion GIFTs 0 / 4 supported
#> transport GIFTs 1 / 3 supported
#> GIFTs with resource strategy private 6 / 130 supported
#> GIFTs with resource strategy public_good 1 / 3 supported
#> GIFTs with resource strategy unresolved 0 / 7 supported
#> GIFTs with resource strategy uptake 1 / 3 supported
#> biomass-essential anabolic GIFTs 5 / 44 supportedWith no frames supplied you get a default set that partitions the catalogue by type, by mode, and by resource strategy. The result is a long-form table:
names(traits$metrics)
#> [1] "target_type" "target_id" "metric_id"
#> [4] "value" "unit" "numerator"
#> [7] "denominator" "assessable" "reference_frame"
#> [10] "database_version" "derivation_method"Every row carries its numerator, its denominator, how many members of the frame were assessable, the frame itself, and the database version. That is what lets a number be taken apart six months later.
richness <- traits$metrics[traits$metrics$metric_id == "gift_richness", ]
richness[, c("reference_frame", "value", "assessable")]
#> # A tibble: 14 × 3
#> reference_frame value assessable
#> <chr> <dbl> <int>
#> 1 all curated GIFTs 10 163
#> 2 defense GIFTs 1 7
#> 3 metabolic GIFTs 8 143
#> 4 regulatory GIFTs 1 4
#> 5 structural GIFTs 0 9
#> 6 anabolic GIFTs 5 67
#> 7 catabolic GIFTs 2 69
#> 8 interconversion GIFTs 0 4
#> 9 transport GIFTs 1 3
#> 10 GIFTs with resource strategy private 6 130
#> 11 GIFTs with resource strategy public_good 1 3
#> 12 GIFTs with resource strategy unresolved 0 7
#> 13 GIFTs with resource strategy uptake 1 3
#> 14 biomass-essential anabolic GIFTs 5 44Step 3: ask why a number is what it is
Every metric keeps the GIFTs that produced it.
trace <- traits$trace[
traits$trace$metric_id == "gift_richness" &
traits$trace$reference_frame == "catabolic GIFTs",
]
trace[, c("metric_id", "gift_id")]
#> # A tibble: 2 × 2
#> metric_id gift_id
#> <chr> <chr>
#> 1 gift_richness xylan_degradation
#> 2 gift_richness xylose_degradation_isomeraseThis is the point of the long-form shape. A trait you cannot decompose is a trait you cannot defend.
Step 4: breadth is not richness
Counting GIFTs overcounts wherever the ontology happens to be finely subdivided. Breadth counts distinct classifications instead:
breadth <- traits$metrics[
startsWith(traits$metrics$metric_id, "breadth_") &
traits$metrics$reference_frame == "all curated GIFTs",
]
breadth[, c("metric_id", "value", "denominator")]
#> # A tibble: 13 × 3
#> metric_id value denominator
#> <chr> <dbl> <int>
#> 1 breadth_biosynthetic_family 0 1
#> 2 breadth_challenge_class 0 4
#> 3 breadth_defense_class 1 3
#> 4 breadth_input_resource_origin 3 7
#> 5 breadth_metabolic_cycle 0 2
#> 6 breadth_network_position 2 4
#> 7 breadth_output_resource_origin 2 6
#> 8 breadth_physiological_role 4 15
#> 9 breadth_regulatory_class 1 2
#> 10 breadth_resource_strategy 3 4
#> 11 breadth_structural_class 0 3
#> 12 breadth_substrate_class 3 19
#> 13 breadth_substrate_tier 3 3breadth_substrate_class counts how many curated
substrate classes this genome can act on, out of the classes present in
the frame. Two GIFTs acting on the same class contribute one class, not
two.
Step 5: fractions, and when gifter refuses to give you one
Here is a metric that is not in the output above:
"supported_fraction" %in% richness$metric_id
#> [1] FALSEgifter will not tell you what fraction of the metabolic catalogue this genome supports. Supporting eight curated metabolic GIFTs does not mean a genome lacks every other metabolic capability; the open catalogue will grow. A fraction of an open, growing set reads as “the share of microbial function this organism carries”, which is not what it measures.
A fraction is offered only for a frame you explicitly declare bounded — a claim that curation intends to cover that set completely.
autonomy <- reference_frame(preset = "biomass_essential_anabolism")
autonomy
#> <gifter_frame> Biomass-essential anabolic GIFTs
#> preset: biomass_essential_anabolism
#> GIFTs: 44
#> bounded: TRUE
#> database version: 2026.39.1
autonomy_traits <- genome_traits(
result, frames = list(autonomy), genome_id = "MAG_001"
)
autonomy_traits$metrics[
autonomy_traits$metrics$metric_id %in%
c("gift_richness", "supported_fraction"),
c("metric_id", "value", "numerator", "denominator")
]
#> # A tibble: 2 × 4
#> metric_id value numerator denominator
#> <chr> <dbl> <int> <int>
#> 1 gift_richness 5 5 NA
#> 2 supported_fraction 0.114 5 44Over that frame the fraction is meaningful: it is genomic coverage of the curated biomass-essential anabolic capabilities. It still does not mean the organism grows without supplementation — it means the genes for those capabilities are or are not there.
Step 6: MAG completeness, and the honesty of a denominator
Everything above assumed that a negative call means absence. For a closed isolate genome that is reasonable. For a 55%-complete metagenome-assembled genome it is not: the organism was never fully observed, so it has not been shown to lack anything.
By default gifter makes the optimistic assumption and tells you it did:
assessable <- traits$metrics[
traits$metrics$metric_id == "assessable_fraction" &
traits$metrics$reference_frame == "all curated GIFTs",
]
assessable[, c("value", "numerator", "denominator")]
#> # A tibble: 1 × 3
#> value numerator denominator
#> <dbl> <int> <int>
#> 1 1 163 163Supply a completeness estimate and a threshold, and negative calls on
a genome below that threshold leave every denominator. Both may be
stated as proportions or as the percentages CheckM and BUSCO report — a
proportion cannot exceed 1, so gifter reads the scale from the values
themselves, and quality = c(MAG_001 = 55), threshold = 90
is the same analysis as the call below:
fragmented <- genome_traits(
result, frames = list(autonomy), genome_id = "MAG_001",
quality = c(MAG_001 = 0.55), policy = "completeness", threshold = 0.9
)
#> Warning: Less than half of the reference frame was assessable for 1 of 1
#> frames, including "Biomass-essential anabolic GIFTs". Proportions over them
#> rest on very few GIFTs; read them beside assessable_fraction.
fragmented$metrics[
fragmented$metrics$metric_id %in%
c("gift_richness", "supported_fraction", "assessable_fraction"),
c("metric_id", "value", "numerator", "denominator")
]
#> # A tibble: 3 × 4
#> metric_id value numerator denominator
#> <chr> <dbl> <int> <int>
#> 1 gift_richness 5 5 NA
#> 2 assessable_fraction 0.114 5 44
#> 3 supported_fraction 1 5 5Read that carefully, because it is the most misusable output in the
package. supported_fraction went from 0.125 to
1.0, and the genome did not gain a single capability:
gift_richness is 5 in both runs. All that happened is that
the 35 negative calls left the denominator, because a 55%-complete
genome has not been shown to lack them. The number now means “of the
five capabilities we could assess, five are supported”, which is nearly
content-free. assessable_fraction is what says so — 5 of 40
— and the warning gifter emitted points at exactly that.
The honest reading of a fragmented genome is
gift_richness beside assessable_fraction, not
the fraction on its own.
If the frame contains no supported GIFTs on a genome
below the quality threshold, none of its members is assessable under
this policy. gifter still reports gift_richness = 0 and
assessable_fraction = 0, but it emits no
supported_fraction row: there is no denominator.
An absent row is not a measured zero or an NA value to
include in an average.
Two guarantees hold whatever policy you choose:
- no policy can make an unsupported GIFT supported. Genome quality changes how absence is read and nothing else. The complete calls are identical in both runs above.
- there is no default threshold. How complete a genome must be before its silence is informative is your declared choice, and a package default would be read as a recommendation.
The incomplete-genome case study measures how calls and assessable denominators change when genes are removed from reference genomes.
What these numbers may not say
- richness counts encoded capabilities in the current gifter ontology within a stated frame. It is not a measure of biological complexity or of metabolic versatility in any particular environment.
- a supported fraction is genomic coverage, never growth independence, activity, flux or phenotype.
- nothing here has become more certain than the calls underneath it. A
trait built on an
ambiguouscall inherits that ambiguity.