Skip to contents

offline taxonomic name resolution

CRAN status CRAN downloads Monthly downloads R-CMD-check Codecov test coverage License: MIT

Overview

Hand taxify a column of messy species names. It cleans them, matches them against a Darwin Core backbone on your disk, resolves synonyms to accepted names, and returns one standardized data.frame. Every step runs locally against a versioned snapshot, so there are no API calls, no rate limits, and the same input gives the same output on any machine. A list of thousands resolves in seconds, with the matching engine written in C through the vectra columnar engine.

Installation

Or the development version from GitHub:

install.packages("pak")
pak::pak("gcol33/taxify")          # vectra is installed automatically

Usage

library(taxify)

# the first call installs the default backbone set (COL + GBIF + ITIS, ~4 GB)
taxify(c(
  "Quercus robur",
  "Pinus abies",        # synonym, resolved to Picea abies
  "Quercus robus",      # typo, fuzzy-corrected to Q. robur
  "Taraxacum officinale"
))

You get one row per input name on a fixed schema: the matched and accepted names with their IDs and authorship, rank, family, genus, epithet, synonym / hybrid / ambiguity flags, the match type, the fuzzy distance, a coarse kingdom and taxon-group label, and the backbone and version used. summary() prints how the batch resolved.

result <- taxify(c("Quercus robur", "Pinus abies", "Quercus robus", "Taraxacum officinale"))
summary(result)
#> -- taxify results ----------------------------------------------------
#>   backbone: COL  |  4 names submitted
#>
#>   matched         4  (exact: 2, case-insensitive: 0, fuzzy: 2, abbrev: 0)
#>   --------------------------------------------------------------
#>   taxon groups: vascular plant: 4

Backbones

taxify ships 19 backbones as compressed .vtr files, pre-built by the companion taxifydb package and downloaded once. Pass several and they form a fallback chain, where a name unmatched by the first cascades to the next. The chain is staged by match quality: every backbone is asked for an exact match before any backbone is asked for a fuzzy one, so a near neighbour in an early backbone does not settle a name a later backbone holds exactly.

# COL first (all kingdoms), then GBIF for whatever COL leaves open
taxify(c("Quercus robur", "Panthera leo", "Amanita muscaria"), backbone = c("col", "gbif"))

Pass no backbone and every installed backbone forms one chain in a fixed priority order: the COL syntheses (COL Extended Release, then COL), then the domain authorities (marine, plants, fungi, algae, fishes, reptiles, mammals, birds, prokaryotes), then the broad aggregators GBIF, ITIS, NCBI, and OTT.

Backbone Scope Names Download
WFO Vascular plants 1.6M 761 MB
COL All kingdoms 5.3M 2.0 GB
COL Extended Release All kingdoms 7.9M 1.6 GB
GBIF All kingdoms 6.4M 1.6 GB
ITIS US focus, freshwater/marine 993k 205 MB
NCBI All life 2.8M 531 MB
OTT All life (synthetic) 3.7M 763 MB
WoRMS Marine/aquatic 1.6M 312 MB
Euro+Med European/Mediterranean plants 147k 35 MB
Species Fungorum Fungi 315k 71 MB
AlgaeBase Algae 172k 36 MB
FishBase Fishes 103k 19 MB
SeaLifeBase Non-fish marine/aquatic 134k 29 MB
Reptile Database Reptiles 50k 10 MB
LCVP Vascular plants 1.3M 252 MB
WCVP Vascular plants 1.4M 309 MB
Mammal Diversity Database Mammals 62k 11 MB
AviList Birds 41k 8 MB
LPSN Prokaryotes (Bacteria/Archaea) 45k 12 MB

list_backbones() returns this table live, with the installed and version status of each. taxify_databases() adds the enrichment layers alongside it.

Matching

Input names are normalized first, so the fuzzy pass runs only on names that genuinely differ from the backbone:

"Quercus robur L."            ->  "Quercus robur"        # authorship stripped
"Pinus cf. sylvestris"        ->  "Pinus sylvestris"     # qualifier removed
"Nothofagus x alpina"         ->  "Nothofagus × alpina"  # hybrid sign normalized (x -> ×)
"Betula pendula (Roth) Doll"  ->  "Betula pendula"       # parenthesized author stripped

Fuzzy matching takes Damerau-Levenshtein, Levenshtein, or Jaro-Winkler with a distance threshold, and runs genus-blocked, so a typo competes against names in its own genus.

taxify and WorldFlora both read the same WFO snapshot, which isolates the two matching implementations on identical data. The corpus is 1,000 accepted binomials drawn from the backbone with a fixed seed; the fuzzy corpus is those names with one substituted character in each epithet, so every one has to resolve by distance.

taxify WorldFlora
Backbone load 4.9 s 20.1 s (CSV into RAM)
Exact match, 1,000 names 2.2 s 17.1 s
Fuzzy match, 1,000 names 18.8 s 4,192 s (70 min)
Fuzzy match, 5,000 names 26.6 s not measured
Peak R heap, fuzzy 1,000 678 MB 4.0 GB

scripts/benchmark-worldflora.R produces these numbers and scripts/benchmark-worldflora-results.json records the run, including package versions and the backbone snapshot. Both packages were measured back to back on one machine (Windows 11, R 4.6.0, taxify 0.3.21, WorldFlora 1.14.5) that was carrying other work at the time, so the ratios are the reliable figures.

Beyond matching

taxify() resolves a name to its accepted name. The same local backbone file answers the related lookups, with nothing else to download:

synonyms("Picea abies")                     # every synonym of an accepted name
children("Quercus")                         # accepted species in a genus
downstream("Fagaceae", downto = "genus")    # all genera under a family
upstream("Quercus robur", to = "family")    # the family a species sits in
class2tree(species)                         # a lineage as a Newick / ape phylo tree
lowest_common(species)                      # the deepest shared rank (the MRCA)
parse_name("Quercus robur (L.) H.Karst.")   # genus / epithet / author, no lookup
id2name("2878688", backbone = "gbif")       # GBIF usage key -> name + classification
comm2sci("pedunculate oak")                 # common name -> scientific
sci2comm("Quercus robur")                   # scientific -> common names
reconcile(old_species_list)                 # how a checklist maps onto the backbone
taxify_lock(result)                         # freeze the backbone + enrichment versions
cite(result)                                # citations for every source used

Traits and status

108 enrichment layers join published trait and status data to a result through the backbone-resolved accepted name, so synonyms in either dataset land on the same key.

taxify(plant_names) |>
  add_iucn() |>                  # IUCN Red List
  add_griis("AT") |>             # GRIIS invasive status
  add_zanne() |>                 # Zanne et al. woodiness
  add_eive()                     # EIVE indicator values

taxify(fish_names) |>
  add_fishbase() |>              # FishBase morphology and ecology
  add_fishmorph()                # FISHMORPH functional traits

taxify(plant_names) |>
  add_trait("seed_mass")         # every source that carries it, harmonized to mg

Sources span all kingdoms: IUCN, GRIIS, GBIF common names, WCVP, EIVE, Diaz et al., LEDA, GIFT, FungalTraits, FUNGuild, AlgaeTraits, EltonTraits, AVONET, PanTHERIA, AmphiBIO, FISHMORPH, FishBase, AnAge, GloNAF, LepTraits, AnimalTraits, and regional plant-trait sets for France (Baseflor), Britain (Ecoflora), and Germany (FloraWeb), among others. list_enrichments() returns the full set in R, list_traits() browses the cross-source trait vocabulary behind add_trait(), and the enrichments vignette lists every source with its reference and license.

add_data() joins your own table the same way, auto-detecting the species column and matching it through the backbones used in the original call. It reads data.frames, CSV, CSV.GZ, XLSX, SQLite, and .vtr.

result |> add_data("TRY_traits.csv")
result |> add_data("TRY_traits.csv", cols = c("LeafArea", "SLA", "PlantHeight"))

Checking a list

inspect() returns only the names that look wrong, each labelled with what stands out and the name to use instead: typos, retired synonyms, made-up genera, near-duplicate spellings, and the lone animal in a list of plants. Each label is ranked by whether it needs a decision, a second look, or optional cleanup.

inspect(field_names)                   # offline register and list checks
inspect(field_names, backbones = TRUE) # also typos, synonyms, ambiguity

For a regional field list, region steers fuzzy correction toward species that occur where you work, so a misspelling resolves to the plant that grows there. Pass a region name, a TDWG code, or coordinates.

taxify(field_names, region = "Belgium")
taxify(field_names, coords = c(4.35, 50.85))

Support

“Software is like sex: it’s better when it’s free.” — Linus Torvalds

I’m a PhD student who builds R packages in my free time because I believe good tools should be free and open. I started these projects for my own work and figured others might find them useful too.

If this package saved you some time, buying me a coffee is a nice way to say thanks. It helps with my coffee addiction.

Buy Me A Coffee

License

MIT (see the LICENSE file)

Citation

@software{taxify,
  author = {Colling, Gilles},
  title  = {taxify: Offline Taxonomic Name Matching Against Darwin Core Backbones},
  year   = {2026},
  url    = {https://github.com/gcol33/taxify}
}

Cite the backbones and enrichment layers you actually used with cite(result), which pulls each source’s own reference from the manifest.