Hybrids and aggregates

This article shows how taxify matches the names of hybrids and aggregates, and how it attaches trait values to them when the published datasets hold none for the name itself. A hybrid formula with no record of its own resolves through its two parents; an aggregate with no record of its own falls back to its binomial. Installation is covered in Get started. The examples match against World Flora Online (WFO) and Euro+Med PlantBase.

  1. Match the list with taxify(), which records hybrids in is_hybrid and hybrid_type and aggregates in qualifier and aggregate_fallback.

  2. Parse the parents of a hybrid formula with add_hybrid_info().

  3. Choose whether an aggregate keeps its own concept with the aggregates argument.

  4. Attach traits with add_trait() or an add_<source>() function, which fall back to a hybrid’s parents and an aggregate’s binomial.

library(taxify)

Hybrid names

Botanical names mark a hybrid with the multiplication sign (×) in one of three places:

  • before the genus, for a cross between two genera (a nothogenus): ×Cupressocyparis leylandii, the Leyland cypress;

  • before the epithet, for a cross within one genus (a nothospecies): Mentha ×piperita, peppermint;

  • between two parent names (a hybrid formula): Salix alba × Salix fragilis.

Most data write a plain x in place of the sign. A hybrid name is recognised during cleaning, before any backbone is read, and a formula is first matched as a whole before its parents are resolved one by one (both described in How hybrids are detected). The list below has one name of each kind, a second nothospecies, a nothospecies with an author, and a species that is no hybrid:

hybrids <- c(
  "Quercus robur", "Mentha x piperita", "x Cupressocyparis leylandii",
  "Salix alba x Salix fragilis", "Platanus x hispanica",
  "Mentha x piperita L."
)
res <- taxify(hybrids, backbone = "wfo", verbose = FALSE)
res[, c("input_name", "accepted_name", "hybrid_type", "match_type")]
#>                    input_name               accepted_name  hybrid_type
#> 1               Quercus robur               Quercus robur         <NA>
#> 2           Mentha x piperita           Mentha × piperita nothospecies
#> 3 x Cupressocyparis leylandii  × Hesperotropsis leylandii   nothogenus
#> 4 Salix alba x Salix fragilis Salix alba × Salix fragilis      formula
#> 5        Platanus x hispanica          Platanus hispanica nothospecies
#> 6        Mentha x piperita L.           Mentha × piperita nothospecies
#>       match_type
#> 1          exact
#> 2          exact
#> 3          exact
#> 4 hybrid_formula
#> 5          exact
#> 6          exact
#> Sources: WFO 2026.06 | cite() for full citations

is_hybrid (not shown) is TRUE on the five hybrid rows, and hybrid_type names the kind. Both describe the input and are set during cleaning, so they are filled whether or not the name matched.

WFO places the Leyland cypress in a different nothogenus. WFO holds no record for the Salix cross, so the row resolves through its two parents, with match_type = "hybrid_formula": accepted_name holds the cross of the accepted parent names, and matched_name the cross of the parents as written. The two differ only when a parent is a synonym. The ID, rank and classification columns of a formula row stay NA, and when a parent does not match, accepted_name stays NA as well.

Hybrid parents

A formula row has no backbone record of its own, so its parents are what can be looked up. add_hybrid_info() parses the two parents of each hybrid formula and resolves them against the backbones of the result. It adds the parents as written, their accepted names and their backbone IDs:

info <- add_hybrid_info(res)
info[info$hybrid_type %in% "formula",
     c("hybrid_parent_1", "hybrid_parent_1_accepted",
       "hybrid_parent_2", "hybrid_parent_2_accepted")]
#>   hybrid_parent_1 hybrid_parent_1_accepted hybrid_parent_2
#> 4      Salix alba               Salix alba  Salix fragilis
#>   hybrid_parent_2_accepted
#> 4           Salix fragilis
#> Sources: WFO 2026.06 | cite() for full citations

The parent columns are NA for a nothospecies or a nothogenus, whose name does not say which species were crossed: the parents of Mentha ×piperita, M. aquatica and M. spicata, appear nowhere in it.

Formulas are written in several ways. An abbreviated genus in the second parent is expanded from the first parent, and a bare epithet is read as a species of the first parent’s genus:

formulas <- c(
  "Quercus pyrenaica x Q. petraea", "Populus nigra x Populus deltoides",
  "Rosa canina x R. gallica", "Salix alba x fragilis"
)
fo <- add_hybrid_info(taxify(formulas, backbone = "wfo", verbose = FALSE))
fo[, c("input_name", "hybrid_parent_1", "hybrid_parent_2")]
#>                          input_name   hybrid_parent_1   hybrid_parent_2
#> 1    Quercus pyrenaica x Q. petraea Quercus pyrenaica   Quercus petraea
#> 2 Populus nigra x Populus deltoides     Populus nigra Populus deltoides
#> 3          Rosa canina x R. gallica       Rosa canina      Rosa gallica
#> 4             Salix alba x fragilis        Salix alba    Salix fragilis
#> Sources: WFO | cite() for full citations

How hybrids are detected

Detection runs during cleaning, before any backbone is read. The sign × is replaced with a space-padded x, and an x counts as a hybrid marker only when it stands alone between spaces, so the x inside Saxifraga is never one. A sign misread as × or Ã\u0097, which happens when text in UTF-8 is read as Latin-1, is repaired first.

A nothospecies or nothogenus is matched with the sign removed and with the sign kept in each of its spellings (Mentha × piperita, ×Cupressocyparis), because backbones differ in how they store it. A hybrid formula is first matched as a whole, since some backbones store a cross as a synonym of the nothospecies it produces; only when that fails are the parents resolved one by one.

The parser expects a binomial on each side of the sign. A formula with a subspecies or variety on one side is still detected, and the rank and infraspecific epithet stay part of that parent’s name.

Aggregates

An aggregate groups closely related species under one name, as in apomictic groups such as Rubus fruticosus and Taraxacum officinale, where field data often record the group and leave the species open. It is written with agg. or s.l. (sensu lato, in the broad sense). taxify reads the variants a source may use (aggr., agg, -agg, coll. sp., sensu lato) as agg. or s.l. and records the marker in qualifier. The opposite marker s.str. (sensu stricto, in the narrow sense) is recorded in qualifier and the name is matched as written.

Some backbones hold an aggregate as a taxon of its own, others hold only the binomial. By default (aggregates = "preserve") an aggregate is matched as a taxon where the backbone has one, and falls back to its binomial otherwise. WFO holds no aggregates:

aggs <- c(
  "Rubus fruticosus agg.", "Taraxacum officinale agg.",
  "Galium mollugo s.l.", "Quercus robur"
)
wfo <- taxify(aggs, backbone = "wfo", verbose = FALSE)
wfo[, c("input_name", "accepted_name", "qualifier", "aggregate_fallback")]
#>                  input_name        accepted_name qualifier aggregate_fallback
#> 1     Rubus fruticosus agg.     Rubus fruticosus      agg.               TRUE
#> 2 Taraxacum officinale agg. Taraxacum officinale      agg.               TRUE
#> 3       Galium mollugo s.l.       Galium mollugo      s.l.               TRUE
#> 4             Quercus robur        Quercus robur      <NA>                 NA
#> Sources: WFO 2026.06 | cite() for full citations

The three aggregates resolve to their binomials, and aggregate_fallback = TRUE records that the group itself was not found.

Euro+Med PlantBase holds aggregates as taxa:

em <- taxify(aggs, backbone = "euromed", verbose = FALSE)
em[, c("input_name", "accepted_name", "aggregate_fallback")]
#>                  input_name             accepted_name aggregate_fallback
#> 1     Rubus fruticosus agg.            Rubus plicatus               TRUE
#> 2 Taraxacum officinale agg. Taraxacum sect. Taraxacum              FALSE
#> 3       Galium mollugo s.l.            Galium mollugo               TRUE
#> 4             Quercus robur             Quercus robur                 NA
#> Sources: EUROMED 2026.08 | cite() for full citations

Taraxacum officinale agg. matches the Euro+Med aggregate, which Euro+Med treats as a synonym of Taraxacum sect. Taraxacum, so aggregate_fallback is FALSE. Euro+Med treats the binomial Rubus fruticosus as a synonym of Rubus plicatus, and the row follows it.

aggregates = "collapse" removes the marker before matching, so the binomial is matched like any other name and aggregate_fallback stays NA:

col <- taxify(aggs, backbone = "euromed", aggregates = "collapse",
              verbose = FALSE)
col[, c("input_name", "accepted_name", "aggregate_fallback")]
#>                  input_name        accepted_name aggregate_fallback
#> 1     Rubus fruticosus agg.       Rubus plicatus                 NA
#> 2 Taraxacum officinale agg. Taraxacum officinale                 NA
#> 3       Galium mollugo s.l.       Galium mollugo                 NA
#> 4             Quercus robur        Quercus robur                 NA
#> Sources: EUROMED 2026.08 | cite() for full citations

Traits for hybrids and aggregates

Published datasets rarely hold a value for a hybrid formula or an aggregate. add_trait() and every add_<source>() function fill those rows from related names.

For a hybrid formula, a value recorded for the hybrid itself is used first. Without one, the values of the two parents are combined: numbers are averaged, and a category is kept when the parents agree and written as "A x B" when they differ, with a warning. When only one parent has a value, that value is used. After add_hybrid_info(), an add_<source>() function also adds each parent’s value as <trait>_parent1 and <trait>_parent2.

For an aggregate, the value recorded for the aggregate is used first, and the binomial’s value otherwise. A species without a value of its own takes its aggregate’s value where the dataset records the trait only for the group.

The next chunk attaches plant height to two willows and their cross. mode = "wide" keeps one column per dataset, so the cross can be compared with its parents row by row:

salix <- c("Salix alba", "Salix fragilis", "Salix alba x Salix fragilis")
tr <- taxify(salix, backbone = "wfo", verbose = FALSE) |>
  add_trait("plant_height", mode = "wide")
#> add_trait('plant_height'): gift 3, diaz 3, austraits 2, bien 3, brot 0 (of 3 resolved names).
tr[, c("input_name", "plant_height_gift", "plant_height_diaz",
       "plant_height_austraits")]
#>                    input_name plant_height_gift plant_height_diaz
#> 1                  Salix alba              30.0          17.36306
#> 2              Salix fragilis              25.0          14.94508
#> 3 Salix alba x Salix fragilis              27.5          16.15407
#>   plant_height_austraits
#> 1                     20
#> 2                     NA
#> 3                     20
#> Sources: WFO 2026.06, GIFT (Weigelt et al. 2020) 2026.08, Diaz et al. 2022 2026.08, AusTraits (Falster et al. 2021) 2026.08, BIEN (Maitner et al. 2018) 2026.08 | cite() for full citations

The cross has no value of its own in any of these datasets. In GIFT and in the data of Díaz et al. its height is the mean of the two parents; AusTraits has a height for Salix alba only, and the cross takes it.

aggregate_trait_fallback = FALSE, or options(taxify.aggregate_trait_fallback = FALSE) for a session, leaves such rows NA. With options(taxify.trait_provenance = TRUE), an add_<source>() function adds a <source>_basis column that records whether each value is the name’s own, an aggregate’s, or a binomial’s.

Where next