This vignette shows how taxify matches hybrid and aggregate names and
how it attaches traits to them. Both need handling in matching and again
in trait resolution. A hybrid names a cross between two taxa, sometimes
as a nothospecies (Mentha ×piperita) and sometimes as a
formula that spells out both parents
(Salix alba × Salix fragilis). An aggregate names a group
of closely related microspecies under one label
(Rubus fruticosus agg., Galium mollugo s.l.),
one step above the binomial. taxify treats the two with a parallel
fallback: where a hybrid formula has no record of its own, it resolves
through its two parents; where an aggregate has no record of its own, it
falls back to the nominal binomial.
Match the list with
taxify(), which records hybrids inis_hybridandhybrid_typeand aggregates inqualifierandaggregate_fallback.Parse the parents of a hybrid formula with
add_hybrid_info().Choose whether an aggregate keeps its own concept with the
aggregatesargument oftaxify().Attach traits with
add_trait()or anadd_<source>()door, which fall back to a hybrid’s parents and an aggregate’s binomial.
Example
Matching a mixed species list
Botanical nomenclature marks a hybrid with the multiplication sign (×, U+00D7) in one of three positions. A nothogenus places it before the genus name, for an intergeneric cross (between species in two different genera), as in Leyland cypress:
×Cupressocyparis leylandii
A nothospecies places it before the specific epithet, with the same genus on both sides of the cross. Peppermint (Mentha ×piperita, a cross of M. aquatica and M. spicata) is the classic case:
Mentha ×piperita
A hybrid formula names both parent species, joined by the sign:
Salix alba × Salix fragilis
The list below mixes an ordinary species with a nothospecies, a nothogenus, a formula and a second nothospecies, written with the plain “x” most data use in place of the sign, and matches them in a single call:
names <- c(
"Quercus robur",
"Mentha x piperita",
"x Cupressocyparis leylandii",
"Salix alba x Salix fragilis",
"Platanus x hispanica"
)
result <- taxify(names, backbone = "wfo")
result[, c("input_name", "accepted_name", "is_hybrid", "hybrid_type", "match_type")]The expected output looks roughly like this:
| input_name | accepted_name | is_hybrid | hybrid_type | match_type |
|---|---|---|---|---|
| Quercus robur | Quercus robur | FALSE | NA | exact |
| Mentha x piperita | Mentha × piperita | TRUE | nothospecies | exact |
| x Cupressocyparis leylandii | Hesperotropsis leylandii | TRUE | nothogenus | exact |
| Salix alba x Salix fragilis | Salix alba × Salix fragilis | TRUE | formula | hybrid_formula |
| Platanus x hispanica | Platanus × hispanica | TRUE | nothospecies | exact |
The two nothospecies (Mentha, Platanus) matched
because WFO stores them as accepted names with the × in the canonical
name. The nothogenus ×Cupressocyparis matched too: taxify tries
the sign-retained forms, and WFO resolves the cross to its accepted
genus (Hesperotropsis). WFO carries no single record for the
Salix cross, so it resolves through its two parents with
match_type = "hybrid_formula". Both parents resolve against
WFO, so accepted_name holds the accepted-parent cross
“Salix alba × Salix fragilis” and matched_name the
input-parent cross. When a parent cannot be matched, both stay
NA.
is_hybrid is TRUE for all four hybrid
inputs, and hybrid_type names the kind, whether or not the
name matched: both describe the input itself and are set during
cleaning, before any backbone match.
Hybrid parents with add_hybrid_info()
hybrid_type is already in the taxify()
output. add_hybrid_info() goes further for hybrid formulas:
it parses the input_name column for the two parents and
resolves each against the same backbone(s) used for the result. It adds
these columns:
hybrid_parent_1: the first parent binomial (for formulas) or NAhybrid_parent_2: the second parent binomial (for formulas, with abbreviated or omitted genera expanded) or NAhybrid_parent_1_accepted,hybrid_parent_2_accepted: the accepted name each parent resolves to, or NA if it did not matchhybrid_parent_1_id,hybrid_parent_2_id: the backbonetaxon_idof the accepted taxon each parent resolves to, ready forid2name(), or NA if it did not match
For nothogenus and nothospecies names the parent columns are NA, because the name string does not encode the parents: those of Mentha ×piperita (Mentha aquatica and Mentha spicata) appear nowhere in it. Only hybrid formulas carry both parent names.
result |> add_hybrid_info()The new columns for the five-name example:
| input_name | hybrid_type | hybrid_parent_1 | hybrid_parent_1_accepted | hybrid_parent_2 |
|---|---|---|---|---|
| Quercus robur | NA | NA | NA | NA |
| Mentha x piperita | nothospecies | NA | NA | NA |
| x Cupressocyparis leylandii | nothogenus | NA | NA | NA |
| Salix alba x Salix fragilis | formula | Salix alba | Salix alba | Salix fragilis |
| Platanus x hispanica | nothospecies | NA | NA | NA |
Parsing hybrid formulas
Field botanists record hybrid formulas when the parentage of a specimen is known or suspected, so they appear in botanical and horticultural datasets. The notation varies: some records spell out both genera in full, others abbreviate the second genus.
formulas <- c(
"Salix alba x Salix fragilis",
"Quercus pyrenaica x Q. petraea",
"Populus nigra x Populus deltoides",
"Rosa canina x R. gallica"
)
formula_result <- taxify(formulas, backbone = "wfo")
formula_result <- formula_result |> add_hybrid_info()
formula_result[, c("input_name", "hybrid_type",
"hybrid_parent_1", "hybrid_parent_2")]| input_name | hybrid_type | hybrid_parent_1 | hybrid_parent_2 |
|---|---|---|---|
| Salix alba x Salix fragilis | formula | Salix alba | Salix fragilis |
| Quercus pyrenaica x Q. petraea | formula | Quercus pyrenaica | Quercus petraea |
| Populus nigra x Populus deltoides | formula | Populus nigra | Populus deltoides |
| Rosa canina x R. gallica | formula | Rosa canina | Rosa gallica |
The abbreviation “Q.” in the second row was expanded to “Quercus”, and “R.” to “Rosa” in the fourth. The expansion is purely textual: when the second parent’s genus is a single capital letter followed by a period, it is replaced with the first token of the first parent.
Traits for hybrids
Trait enrichment is hybrid-aware. When you attach traits to a result that contains a hybrid formula, each trait is resolved with a ladder:
- the hybrid directly: if the cross resolved to a nothospecies and that name carries the trait, it is used as-is;
- the average of both parents: otherwise the two parents are resolved
and their trait values combined: numeric traits are averaged, and a
categorical trait is taken as the shared value, or reported as
"A x B"when the parents disagree (anadd_<source>()door also warns); - the single available parent: if only one parent carries the trait, its value is used.
This happens automatically inside every
add_<source>() door and inside
add_trait(); no extra call is needed.
# A hybrid formula with no direct trait record inherits from its parents
taxify("Salix alba x Salix fragilis", backbone = "wfo") |>
add_trait("plant_height")If the parents have first been materialized with
add_hybrid_info(), the add_<source>()
doors additionally expose each parent’s own value as
<trait>_parent1 / <trait>_parent2,
so the components of the average sit beside the combined value.
An aggregate species list
An aggregate is marked either agg. (aggregate) or s.l. (sensu lato, “in the broad sense”). Matched against WFO, which carries no aggregate taxa, each aggregate falls back to its binomial and is flagged:
agg_names <- c(
"Rubus fruticosus agg.",
"Taraxacum officinale agg.",
"Galium mollugo s.l.",
"Quercus robur"
)
res <- taxify(agg_names, backbone = "wfo")
res[, c("input_name", "accepted_name", "qualifier",
"aggregate_fallback", "match_type")]| input_name | accepted_name | qualifier | aggregate_fallback | match_type |
|---|---|---|---|---|
| Rubus fruticosus agg. | Rubus fruticosus | agg. | TRUE | exact |
| Taraxacum officinale agg. | Taraxacum officinale | agg. | TRUE | exact |
| Galium mollugo s.l. | Galium mollugo | s.l. | TRUE | exact |
| Quercus robur | Quercus robur | NA | NA | exact |
The three aggregates resolve to their binomials with
aggregate_fallback = TRUE, and the plain species carries
NA in both qualifier columns. Against Euro+Med, where
Rubus fruticosus aggr. is a stored taxon, the same query would
resolve to the aggregate itself with
aggregate_fallback = FALSE.
Traits for aggregates
Trait enrichment is aggregate-aware in the same way. The join resolves an aggregate through the taxonomic hierarchy:
- a species query takes its own value first, and inherits its aggregate’s value where the source records the trait only at the aggregate level;
- an aggregate query takes the aggregate-level value first, and where the source carries none it falls back to the nominal binomial’s value, so the species’ own measurement stands in for the group where no aggregate-level figure exists.
This happens automatically inside every
add_<source>() door and inside
add_trait().
# Rubus fruticosus agg. takes the aggregate's trait where the source records
# one, otherwise the nominal Rubus fruticosus value.
taxify("Rubus fruticosus agg.") |>
add_trait("plant_height")The binomial fallback is on by default. Turning it off, per call or
globally, keeps an aggregate without aggregate-level data as
NA:
taxify("Rubus fruticosus agg.") |>
add_trait("plant_height", aggregate_trait_fallback = FALSE)
options(taxify.aggregate_trait_fallback = FALSE)With options(taxify.trait_provenance = TRUE), each
enrichment adds a <enrichment>_basis column recording
where each value came from: "primary" for a same-level hit,
"aggregate" for a species inheriting its aggregate’s value,
and "binomial" for an aggregate standing in on its
binomial.
How taxify detects hybrids
Detection happens during name cleaning, before any backbone matching.
Each name is tokenized, the hybrid marker is looked for in specific
positions, and the result is classified as nothogenus, nothospecies,
formula, or non-hybrid. is_hybrid (logical) records whether
a marker was found in the original input and is present whether or not
the name matched a backbone record; hybrid_type records the
finer classification ("nothogenus",
"nothospecies", "formula", or
NA).
In real-world data the multiplication sign is frequently replaced by a lowercase or uppercase “x”, since herbarium databases, spreadsheet exports and OCR output rarely preserve the Unicode character. Data arrive in three forms:
the Unicode character itself,
×(common in well-curated databases)a lowercase
xsurrounded by spaces (common in spreadsheets and field data)an uppercase
Xsurrounded by spaces (less common, found in older databases and OCR output)
The Unicode multiplication sign is the character the International Code of Nomenclature prescribes. taxify replaces every occurrence of it with a space-padded “x” and works on the resulting uniform token stream, so the downstream logic handles one representation. A standalone “x” counts as a marker only with whitespace boundaries around it, which keeps “Saxifraga” from triggering detection: its “x” sits inside the word.
When UTF-8 text containing × is read with a Latin-1 or Windows-1252 encoding, the two-byte sequence can come out as “0c3097” or “0c3014”. The name cleaning pipeline repairs both misreadings before hybrid detection runs, so names corrupted this way are still handled.
After detection, the marker is handled by type. For a nothospecies like “Mentha ×piperita”, the cleaned form becomes “Mentha piperita”, and taxify also tries the sign-retained form “Mentha × piperita” (some backbones store the × in the canonical name). For a nothogenus like “×Cupressocyparis leylandii”, taxify tries the sign-retained “× Cupressocyparis leylandii” and the no-space “×Cupressocyparis leylandii” alongside the stripped binomial, so a backbone that keeps the sign still matches.
A hybrid formula is not a single taxon name. taxify first tries the
full parent-expanded formula (“Salix alba × Salix fragilis”) against the
backbone: some backbones store a cross as a name or synonym of the
resulting nothospecies (here Salix × rubens), and when they do,
the formula resolves normally. If the backbone does not store it, taxify
parses the parents. When both parents resolve, matched_name
holds the input-parent formula and accepted_name the
accepted-parent formula; the two differ only when a parent is a synonym,
mirroring an ordinary synonym row. If either parent fails to resolve,
both stay NA, and the row is still flagged
is_hybrid = TRUE with
match_type = "hybrid_formula". That is as far as matching
can go without a full parentage crosswalk. The parents remain
recoverable through add_hybrid_info(), and trait joins fall
back to them automatically.
The same-genus shorthand is recognized too:
"Salix alba × fragilis", with the second parent written as
a bare epithet, parses to parents “Salix alba” and “Salix fragilis”.
What matches and what does not
The three hybrid types have different matching profiles against backbones.
Nothospecies are the best-supported form. WFO and COL both store many nothospecies as accepted names, with the × as part of the canonical name; Mentha ×piperita, Platanus ×hispanica and Narcissus ×medioluteus appear in both. taxify tries the stripped form (“Mentha piperita”) and the form with the × reinserted (“Mentha × piperita”), and at least one of these typically matches.
Nothogenera go through the same sign-aware logic, so coverage follows
the backbone. taxify tries “× Cupressocyparis leylandii”,
“×Cupressocyparis leylandii” and the stripped “Cupressocyparis
leylandii”, so a backbone that stores the sign form (as WFO and COL do)
resolves the cross. When a nothogenus is genuinely absent, the row has
match_type = "none" and accepted_name = NA,
with is_hybrid still TRUE.
Hybrid formulas are tried against the backbone as a whole first. When
that misses, the row is marked
match_type = "hybrid_formula", the ID, rank and
classification columns stay NA, and the cross is named by
its two parents in matched_name and
accepted_name, never collapsed to one parent. To work with
the parents, add_hybrid_info() resolves both against the
same backbone:
info <- result |> add_hybrid_info()
formula_rows <- info[!is.na(info$hybrid_type) & info$hybrid_type == "formula", ]
formula_rows[, c("input_name", "hybrid_parent_1", "hybrid_parent_1_accepted",
"hybrid_parent_2", "hybrid_parent_2_accepted")]Practical notes
WFO has the broadest coverage of plant nothospecies and nothogenera, reflecting its focus on the world flora. COL includes hybrids across all kingdoms, with uneven coverage. GBIF aggregates many sources and includes hybrid names where the contributing checklists provide them. ITIS, NCBI and OTT have minimal hybrid coverage.
Hybrid detection is input-side only. taxify detects hybrids in the
names you supply and does not scan the backbone for hybrid records. If a
backbone stores “Mentha × piperita” as an accepted name, taxify matches
your input against it, but the backbone record’s own hybrid status is
not exposed as a separate field; the is_hybrid column
reflects your input.
The formula parser expects binomials (genus plus epithet) on both sides of the × marker. A formula that includes a subspecies or variety rank (e.g., “Salix alba var. vitellina × Salix fragilis”) is still detected as a formula, but the parent extraction may include the rank and infraspecific epithet in the parent name. This is generally the desired behavior, since the full trinomial identifies the parent more precisely than the binomial alone.
Hybrid names sometimes carry authorship strings (“Mentha ×piperita L.”). The cleaning pipeline strips authorship before matching, so an author string does not interfere with hybrid detection or matching:
# Authorship is stripped; hybrid detection still works
taxify("Mentha x piperita L.", backbone = "wfo")add_hybrid_info() parses the parents from the
input_name column and resolves each against the backbone(s)
used for the result, making one backbone lookup for the set of unique
parents (memoized within a session). The parse itself is pure string
work; the parent resolution reuses the matching engine of
taxify().
How taxify matches aggregates
An aggregate groups several closely related microspecies under one
name. Apomictic complexes are the usual reason: Rubus
fruticosus, Taraxacum officinale and Hieracium
each cover hundreds of near-indistinguishable segregates, and field data
routinely records the aggregate and leaves the microspecies open.
agg. is the explicit marker; s.l. widens a name to
include taxa a narrow reading would split off. Both mean the same thing
for matching: the name refers to the whole group. taxify folds the
spelling variants a source might use (agg.,
aggr., agg, -agg,
coll. sp., sensu lato) to a canonical
agg. or s.l. marker, recorded in the
qualifier column. The opposite marker, s.str.
(sensu stricto, “in the narrow sense”), narrows a name to its
core binomial, so it is recorded in qualifier and matched
as an ordinary name.
A backbone may or may not carry a dedicated taxon for the aggregate.
Only the aggregate-bearing backbones, Euro+Med and WoRMS, store
"<binomial> aggr." as a concept of its own; the
others record the binomial alone, without the group above it. The
aggregates argument sets what happens in each case.
aggregates = "preserve" (the default) keeps the
aggregate as its own concept. It matches the backbone’s aggregate taxon
where one exists and otherwise falls back to the nominal binomial. The
aggregate_fallback column records which happened:
FALSE when the dedicated aggregate taxon was found,
TRUE when it fell back to the binomial, and NA
for non-aggregate names.
aggregates = "collapse" strips the marker up front and
matches the binomial the way any name is matched. The qualifier is still
recorded, but there is no fallback to flag, so
aggregate_fallback stays NA.
Where to go next
- Enrichments for the enrichment join and the provenance columns in general.