--- title: "Hybrids and aggregates" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Hybrids and aggregates} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- 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](taxify.html). 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_()` function, which fall back to a hybrid's parents and an aggregate's binomial. ``` r 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](#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: ``` r 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 #> 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: ``` r 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: ``` r 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: ``` r 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 #> 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: ``` r 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`: ``` r 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_()` 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_()` function also adds each parent's value as `_parent1` and `_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: ``` r 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_()` function adds a `_basis` column that records whether each value is the name's own, an aggregate's, or a binomial's. ## Where next - [How names are matched](fuzzy-matching.html): the other match stages. - [Attaching published data](enrichments.html): `add_trait()` and the `add_()` functions in full.