---
title: "Getting started with taxify"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Getting started with taxify}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
eval = FALSE
)
```
```{r va-engine, echo = FALSE, results = "asis", eval = TRUE}
cat('
')
```
## What taxify solves
Almost every biodiversity dataset starts as a column of names. Before any
analysis, those strings have to resolve to one accepted name per taxon, and they
rarely line up on their own: authorship, field qualifiers, capitalization,
historical synonyms, hybrids, and plain typos all keep two records of the same
species apart. A bare `merge()` on raw strings silently drops every row that
disagrees, so the matching has to come first.
`taxify()` takes a character vector and returns one standardized table: each name
cleaned, matched against a backbone you keep on disk, synonyms resolved to the
accepted name. Matching runs in C through the [vectra](https://github.com/gcol33/vectra)
engine, so there are no web services and no rate limits, and the same input gives
the same output on any machine. Fifteen Darwin Core backbones are available
(WFO, COL, GBIF, ITIS, NCBI, OTT, WoRMS, Euro+Med, Species Fungorum, AlgaeBase,
FishBase, SeaLifeBase, Reptile Database, LCVP, WCVP), all queried offline.
```{r}
library(taxify)
```
The first `taxify()` call downloads a backbone once (WFO is about 150 MB) and
caches it under `taxify_data_dir()`. After that, nothing touches the network.
## One call
Hand `taxify()` a vector of names. The list below is deliberately small and
deliberately messy: every entry takes a different route to its accepted name.
```{r}
field_names <- c(
"Quercus robur L.", # authorship to strip
"Quercus robus", # typo
"cf. Betula pendula", # field qualifier
"FAGUS SYLVATICA", # caps
"Quercus pedunculata", # historical synonym of Q. robur
"Q. petraea", # abbreviated genus
"Pinus abies", # synonym of Picea abies (a different genus)
"Festuca rubrra", # typo
"Fallopia japonica", # synonym of Reynoutria japonica (invasive)
"Taraxacum officinale"
)
res <- taxify(field_names)
res[, c("input_name", "accepted_name", "family",
"is_synonym", "match_type", "fuzzy_dist")]
```
```
#> input_name accepted_name family is_synonym match_type fuzzy_dist
#> 1 Quercus robur L. Quercus robur Fagaceae FALSE exact NA
#> 2 Quercus robus Quercus robur Fagaceae FALSE fuzzy 0.077
#> 3 cf. Betula pendula Betula pendula Betulaceae FALSE exact NA
#> 4 FAGUS SYLVATICA Fagus sylvatica Fagaceae FALSE exact_ci NA
#> 5 Quercus pedunculata Quercus robur Fagaceae TRUE exact NA
#> 6 Q. petraea Quercus petraea Fagaceae FALSE abbrev NA
#> 7 Pinus abies Picea abies Pinaceae TRUE exact NA
#> 8 Festuca rubrra Festuca rubra Poaceae FALSE fuzzy 0.071
#> 9 Fallopia japonica Reynoutria japonica Polygonaceae TRUE exact NA
#> 10 Taraxacum officinale Taraxacum officinale Asteraceae FALSE exact NA
```
Ten names, ten rows, every match readable. Each row also carries genus,
authorship, taxon and accepted IDs, a hybrid flag and type, the backbone, and the
exact backbone version (the full table is wider, the same shape for any input).
Each name reaches its accepted name a different way. The animation below walks one
name at a time through the pipeline: the clean step strips authorship, a
qualifier, or case; the match step is exact, case-folded, fuzzy, or abbreviated;
the resolve step follows a synonym to the current name.
```{r pipe-anim, echo = FALSE, results = "asis", eval = TRUE}
body <- "
var s=VA.setup('pp-cv'); if(!s)return; var x=s.ctx,W=s.w,H=s.h,C=VA.C;
var N=NM.length;
var labs=['input','clean','match','resolve','accepted'];
var SX=[64,232,420,602,786];
var yTrack=176;
var PER_NAME=4.2,PERIOD=N*PER_NAME;
function chip(cx,cy,txt,col,w){x.font=VA.F(12);var tw=x.measureText(txt).width;var hw=(w||Math.max(70,tw+16))/2;hw=Math.min(hw,(W-12)/2);cx=VA.clamp(cx,hw+6,W-hw-6);x.save();VA.glow(x,col,9);x.fillStyle=C.bg;x.strokeStyle=col;x.lineWidth=2;x.beginPath();x.rect(cx-hw,cy-15,hw*2,30);x.fill();x.stroke();VA.noglow(x);x.fillStyle=C.ink;x.textAlign='center';x.textBaseline='middle';x.fillText(txt,cx,cy);x.restore();x.textBaseline='alphabetic';}
function station(i,active,col,sub){var cx=SX[i];x.strokeStyle=active?col:C.off;x.lineWidth=active?2:1;if(active)VA.glow(x,col,8);x.beginPath();x.arc(cx,yTrack,11,0,6.2832);x.stroke();VA.noglow(x);if(active){x.fillStyle=col;x.beginPath();x.arc(cx,yTrack,4,0,6.2832);x.fill();}x.fillStyle=active?C.ink:C.mut;x.font=VA.F(12,active);x.textAlign='center';x.fillText(labs[i],cx,yTrack+34);if(sub){x.fillStyle=col;x.font=VA.F(11);x.fillText(sub,cx,yTrack+50);}}
function draw(tc){VA.bg(x,W,H);
VA.glow(x,C.cyan,7);x.fillStyle=C.cyan;x.textAlign='left';x.font=VA.F(15,true);x.fillText('ONE NAME THROUGH THE PIPELINE',16,28);VA.noglow(x);
var gi=Math.min(N-1,Math.floor(tc/PER_NAME));var lt=(tc-gi*PER_NAME)/PER_NAME;
var isSyn=SYN[gi]===1;var isNone=MT[gi]==='none';
x.strokeStyle=C.grid;x.lineWidth=2;x.beginPath();x.moveTo(SX[0],yTrack);x.lineTo(SX[4],yTrack);x.stroke();
var seg=VA.clamp(lt/0.5,0,1)*4;var here=Math.floor(seg+0.0001);
for(var i=0;i<5;i++){
var reached=i<=here;
var col=C.cyan;var sub=null;
if(i===1){col=C.amber;sub=reached?CLN[gi]:null;}
if(i===2){col=isNone?C.red:C.green;sub=reached?MAT[gi]:null;}
if(i===3){col=C.purple;sub=reached?(isSyn?'synonym':'already accepted'):null;if(!isSyn)col=C.off;}
if(i===4){col=isNone?C.red:C.green;}
station(i,reached,col,sub);
}
var a=Math.min(3,Math.floor(seg)),f=VA.clamp(seg-a,0,1);
var px=VA.lerp(SX[a],SX[a+1],VA.easeOut(f));
if(lt<0.5) chip(px,yTrack-46,NM[gi],isNone?C.red:C.cyan);
x.fillStyle=C.mut;x.font=VA.F(12);x.textAlign='center';x.fillText('input',SX[0],yTrack-86);
x.fillStyle=isNone?C.red:C.green;x.font=VA.F(12,true);x.fillText(isNone?'unmatched':'accepted name',SX[4],yTrack-86);
if(lt>=0.5){var aa=VA.clamp((lt-0.5)/0.2,0,1);x.globalAlpha=aa;chip(SX[4],yTrack-46,ACC[gi],isNone?C.red:C.green);x.globalAlpha=1;}
var dotw=12;for(var k=0;k\n",
"\n"))
```
`Quercus robur L.` loses its authorship before matching. `cf. Betula pendula`
loses the qualifier. `FAGUS SYLVATICA` matches after case folding (`exact_ci`).
`Q. petraea` resolves on the genus initial plus epithet (`abbrev`). The three
synonyms (`Quercus pedunculata`, `Pinus abies`, `Fallopia japonica`) resolve to
their accepted names, the last being the current name for a well-known invader.
The two typos go to the fuzzy pass, which is the next thing worth seeing.
## Why a typo barely costs anything
The fuzzy pass never scores a name against the whole backbone. It blocks on genus
first, so `Quercus robus` is compared only against the other *Quercus* names. A
one-letter slip is found in a handful of comparisons rather than across every name
on disk.
```{r block-anim, echo = FALSE, results = "asis", eval = TRUE}
body <- "
var s=VA.setup('bl-cv'); if(!s)return; var x=s.ctx,W=s.w,H=s.h,C=VA.C;
var GX=16,GY=64,COLS=44,ROWS=22,TW=13,TH=15,GW=COLS*TW,GH=ROWS*TH;
var bc0=8,bc1=15,br0=7,br1=13;
var winC=12,winR=10;
var GGX=GX+GW+44,GGW=64,GGY=64,GGH=GH;
var T1=4.8,H1=2.0,T2=5.9,H2=3.4,CYCLE=T1+H1+T2+H2;
function inBlock(c,r){return c>=bc0&&c<=bc1&&r>=br0&&r<=br1;}
function tile(c,r,col,a){x.globalAlpha=a;x.fillStyle=col;x.fillRect(GX+c*TW,GY+r*TH,TW-1.4,TH-1.4);x.globalAlpha=1;}
function frame(){VA.glow(x,C.cyan,6);x.strokeStyle=C.cyan;x.strokeRect(GX,GY,GW,GH);VA.noglow(x);}
function blockOutline(){VA.glow(x,C.green,10);x.strokeStyle=C.green;x.lineWidth=2;x.strokeRect(GX+bc0*TW-1,GY+br0*TH-1,(bc1-bc0+1)*TW,(br1-br0+1)*TH);x.lineWidth=1;VA.noglow(x);}
function gauge(frac,col,ghost,pct){x.strokeStyle=C.mut;x.strokeRect(GGX,GGY,GGW,GGH);var h=Math.max(frac>0?2:0,frac*GGH);VA.glow(x,col,8);x.fillStyle=col;x.fillRect(GGX+1,GGY+GGH-h,GGW-2,h);VA.noglow(x);if(ghost){x.strokeStyle=C.red;x.setLineDash([5,4]);x.lineWidth=1.5;x.beginPath();x.moveTo(GGX-4,GGY+2);x.lineTo(GGX+GGW+4,GGY+2);x.stroke();x.setLineDash([]);x.lineWidth=1;x.fillStyle=C.red;x.font=VA.F(11);x.textAlign='left';x.fillText('whole backbone',GGX-6,GGY-10);}x.fillStyle=C.ink;x.font=VA.F(14,true);x.textAlign='center';x.fillText(pct,GGX+GGW/2,GGY+GGH+20);x.fillStyle=C.mut;x.font=VA.F(11);x.fillText('names scored',GGX+GGW/2,GGY+GGH+36);}
function header(t,col){x.textAlign='left';VA.glow(x,C.cyan,7);x.fillStyle=C.cyan;x.font=VA.F(15,true);x.fillText('FUZZY MATCH :: Quercus robus',16,26);VA.noglow(x);x.fillStyle=col;x.font=VA.F(13);x.fillText(t,16,46);}
function draw(tc){VA.bg(x,W,H);
var c,r;
if(tc0.7){VA.glow(x,'#fff',10);x.fillStyle='#fff';x.fillRect(GX+winC*TW,GY+winR*TH,TW-1.4,TH-1.4);VA.noglow(x);}
gauge(0.06,C.green,true,'one genus');
header('genus-blocked: scored only against other Quercus',C.green);
} else {
for(r=0;r '+QACC+' (1 edit, dist '+QD+')',GX,GY+GH+24);
gauge(0.06,C.green,true,'one genus');
header('match found inside the block',C.green);
}
VA.scan(x,W,H);
}
VA.run(draw,CYCLE,null,'bl','bl-cv');
"
cat(paste0(
"\n",
"\n"))
```
The default threshold allows about one edit per five characters, so common typos
resolve while genuinely different names do not. Fuzzy matching is controlled by
`fuzzy`, `fuzzy_threshold`, and `fuzzy_method`; the
[fuzzy-matching vignette](https://gillescolling.com/taxify/articles/fuzzy-matching.html)
covers the sub-blocking for very large genera and the genus-typo fallback in full.
## Check the batch at a glance
`summary()` prints a digest, the fastest way to see whether a run went cleanly.
```{r}
summary(res)
```
```
#> ── taxify results ──────────────────────────────────────────────────────────
#> backbone: WFO v2024-12 | 10 names submitted
#>
#> matched 10 (exact: 6, case-insensitive: 1, fuzzy: 2, abbrev: 1)
#> ────────────────────────────────────────────────────────────
#> taxon groups: angiosperm: 8 gymnosperm: 1 unknown: 1
```
The digest reports the backbone and version, the match-route breakdown (all ten
resolved here, including the abbreviated `Q. petraea`), and the taxon-group mix.
When a name is out of scope (an animal in a plant-only backbone) or genuinely
absent, the digest tallies it and suggests an alternative backbone. The match types and the multi-backbone fallback
(`backbone = c("wfo", "col", "gbif")`) are covered in the
[backbones vignette](https://gillescolling.com/taxify/articles/backbones.html).
## Offline, and how much faster
Every match runs against the local snapshot, so a run reproduces exactly and the
`backbone_version` column records the WFO release and download date for a methods
section. On the same task many in this field reach for, WorldFlora's `WFO.match`,
both run against a local copy and return the same matches; the difference is where
the matching happens. taxify scores names in C against the compiled backbone,
WorldFlora in R. On 1,000 plant names with fuzzy matching on (Windows, R 4.5.2):
```{r benchmark-anim, echo = FALSE, results = "asis", eval = TRUE}
body <- "
var s=VA.setup('sp-cv'); if(!s)return; var x=s.ctx,W=s.w,H=s.h,C=VA.C;
var BX=150,BW=W-150-150,b1=96,b2=190,bh=46;
var PERIOD=17,HOLD=4;
function fmt(sec){if(sec<60)return sec.toFixed(1)+' s';var m=sec/60;return sec.toFixed(0)+' s ('+m.toFixed(0)+' min)';}
function bar(y,frac,col,name,done,lab){
x.fillStyle=C.off;x.fillRect(BX,y,BW,bh);
var w=Math.max(frac>0?3:0,frac*BW);
if(done)VA.glow(x,col,10);x.fillStyle=col;x.fillRect(BX,y,w,bh);VA.noglow(x);
x.strokeStyle=C.grid;x.strokeRect(BX,y,BW,bh);
x.fillStyle=C.ink;x.font=VA.F(14,true);x.textAlign='right';x.fillText(name,BX-12,y+bh/2+5);
x.fillStyle=done?col:C.mut;x.font=VA.F(13,true);x.textAlign='left';x.fillText(lab,BX+w+12,y+bh/2+5);
}
function draw(tc){VA.bg(x,W,H);
VA.glow(x,C.cyan,7);x.fillStyle=C.cyan;x.textAlign='left';x.font=VA.F(15,true);x.fillText('FUZZY MATCH :: 1,000 NAMES',16,28);VA.noglow(x);
var run=Math.min(tc,PERIOD-HOLD);var prog=run/(PERIOD-HOLD);
var simSec=prog*WFO;
var taxDone=simSec>=TAX;
var taxFrac=taxDone?1:simSec/TAX;
var wfoFrac=Math.min(1,simSec/WFO);
bar(b1,taxFrac,C.green,'taxify',taxDone,taxDone?fmt(TAX):'');
bar(b2,wfoFrac,C.red,'WorldFlora',wfoFrac>=1,wfoFrac>=1?fmt(WFO):fmt(simSec));
x.fillStyle=C.mut;x.font=VA.F(12);x.textAlign='left';x.fillText('elapsed (simulated): '+fmt(simSec),BX,b2+bh+22);
if(taxDone&&tc>PERIOD-HOLD-0.01){var aa=VA.clamp((tc-(PERIOD-HOLD))/1.0,0,1);x.globalAlpha=aa;VA.glow(x,C.green,8);x.fillStyle=C.green;x.font=VA.F(20,true);x.textAlign='center';x.fillText(Math.round(WFO/TAX)+'x faster',W/2,H-40);VA.noglow(x);x.globalAlpha=1;}
VA.scan(x,W,H);
}
VA.run(draw,PERIOD,null,'sp','sp-cv');
"
cat(paste0(
"\n",
"\n"))
```
On 1,000 names that all carry a one-character typo, taxify takes 18.8 s and
WorldFlora 4,192 s. Exact matching is closer, 2.2 s against 17.1 s. The genus
blocking is what opens the gap: a typo competes only against names in its own
genus, so the work does not grow with the size of the backbone.
`scripts/benchmark-worldflora.R` in the repository produces both figures, and the
[large-scale vignette](https://gillescolling.com/taxify/articles/large-scale.html)
covers the batch strategy for lists above 100,000 names.
## Add your own attributes
Once names resolve to an accepted name, any table keyed on species joins cleanly.
`add_data()` takes a data.frame, CSV, XLSX, or SQLite file, runs its species
column through the same backbone, and joins on the accepted name, so a synonym on
either side still lines up.
```{r}
my_traits <- data.frame(
species = c("Quercus pedunculata", # synonym of Q. robur
"Pinus sylvestris",
"Betula pendula"),
seed_mass_mg = c(3200, 7.5, 0.2)
)
taxify(c("Quercus robur", "Pinus sylvestris", "Betula pendula")) |>
add_data(my_traits, species_col = "species")
```
```
#> add_data: 3 of 3 species matched (100.0%). 0 names in data unmatched.
#> input_name accepted_name seed_mass_mg
#> 1 Quercus robur Quercus robur 3200.0
#> 2 Pinus sylvestris Pinus sylvestris 7.5
#> 3 Betula pendula Betula pendula 0.2
```
The trait table used *Quercus pedunculata* and the result used *Quercus robur*; a
plain `merge()` would have missed that row. `add_data()` joins on the accepted
name, so it lines up. Formats, auto-detection, and strict duplicate handling are
in the [custom-data vignette](https://gillescolling.com/taxify/articles/custom-data.html).
## The enrichment layers
taxify also ships published trait and status layers that attach on the accepted
name. There are more than eighty, across the tree of life and for the
conservation and invasion records this kind of work needs. Each `add_*()` matches its own
source against the backbone and attaches on the accepted name, so any of them
stacks into a pipeline the same way. Run `list_enrichments()` for the current set,
versions, and coverage.
```{r enrich-anim, echo = FALSE, results = "asis", eval = TRUE}
con_data <- "var G=[
{n:'Plants',s:'Plants',c:'#6bcb77',L:[['add_zanne()','woody / herbaceous'],['add_eive()','EIVE indicator values'],['add_diaz_traits()','seed mass & height'],['add_leda()','LEDA life-history'],['add_wcvp()','native range'],['add_glonaf()','naturalized alien flora'],['add_baseflor()','ecological traits'],['add_ecoflora()','British flora'],['add_floraweb()','German flora'],['add_pignatti()','Italian indicator values'],['add_fungalroot()','mycorrhizal type']]},
{n:'Birds & mammals',s:'Birds/mammals',c:'#3ad7ff',L:[['add_avonet()','bird morphology'],['add_elton_traits()','diet & body mass'],['add_pantheria()','mammal life-history']]},
{n:'Amphibians & reptiles',s:'Amph & reptiles',c:'#b388ff',L:[['add_amphibio()','amphibian traits'],['add_repttraits()','reptile traits & range']]},
{n:'Fish',s:'Fish',c:'#4d96ff',L:[['add_fishmorph()','morphology'],['add_fishbase()','ecology']]},
{n:'Arthropods',s:'Arthropods',c:'#ff8fd0',L:[['add_arthropod_traits()','arthropod traits'],['add_leptraits()','butterfly traits']]},
{n:'Fungi',s:'Fungi',c:'#ffd93d',L:[['add_fungal_traits()','fungal traits'],['add_funguild()','trophic guild']]},
{n:'Algae',s:'Algae',c:'#33ffd0',L:[['add_algae_traits()','algal traits']]},
{n:'Cross-taxon animals',s:'Cross-taxon',c:'#ffc24b',L:[['add_animaltraits()','body mass & metabolism'],['add_anage()','longevity & ageing']]},
{n:'Status, alien & names',s:'Status & alien',c:'#ff3b6b',L:[['add_iucn()','IUCN Red List'],['add_griis()','GRIIS invasive'],['add_alien_first_records()','first-record years'],['add_common_names()','vernacular names']]}
];
"
body <- "
var s=VA.setup('en-cv'); if(!s)return; var x=s.ctx,W=s.w,H=s.h,C=VA.C;
var NG=G.length;
var cx=280,cy=298,R=192;
var px0=556,pw=W-556-12;
var nLayers=0;for(var gi=0;gi=cy;var ly=n.y+(below?rad+15:-rad-8);x.fillText(G[i].s,n.x,ly);x.fillStyle=foc?G[i].c:C.mut;x.font=VA.F(10);x.fillText('('+G[i].L.length+')',n.x,ly+(below?13:-13));}
function travel(i,t){var n=node(i);var u=t-Math.floor(t);var tx=VA.lerp(n.x,cx,VA.easeOut(u)),ty=VA.lerp(n.y,cy,VA.easeOut(u));VA.glow(x,G[i].c,8);x.fillStyle=G[i].c;x.beginPath();x.arc(tx,ty,3.6,0,6.2832);x.fill();VA.noglow(x);}
function panel(i,lt){var g=G[i];x.textAlign='left';VA.glow(x,g.c,8);x.fillStyle=g.c;x.font=VA.F(15,true);x.fillText(g.n,px0,92);VA.noglow(x);x.strokeStyle=g.c;x.lineWidth=2;x.beginPath();x.moveTo(px0,102);x.lineTo(px0+pw,102);x.stroke();x.lineWidth=1;for(var k=0;k\n",
"\n"))
```
For invasion work the GloNAF, GRIIS, and alien-first-record layers attach
naturalized status, invasive status, and first-record years on the same accepted
name, so a resolved species list carries its invasion history without a second
join. The full menu and per-layer detail are in the
[enrichments vignette](https://gillescolling.com/taxify/articles/enrichments.html).
## Stack layers, then test an idea
Field lists run to hundreds of names. Here is a realistic one, about a hundred
European species, matched in one call. Pick any layers and stack them: woodiness,
the EIVE ecological indicator values, and plant height from the Diaz global trait
dataset all attach on the accepted name.
```{r}
field <- c(
"Quercus petraea", "Pinus sylvestris", "Picea abies", "Betula pendula",
"Acer pseudoplatanus", "Acer platanoides", "Acer campestre", "Corylus avellana",
"Fraxinus excelsior", "Carpinus betulus", "Sorbus aucuparia", "Tilia cordata",
"Ulmus glabra", "Alnus glutinosa", "Salix caprea", "Populus tremula",
"Prunus avium", "Prunus spinosa", "Crataegus monogyna", "Sambucus nigra",
"Cornus sanguinea", "Viburnum opulus", "Euonymus europaeus", "Ligustrum vulgare",
"Frangula alnus", "Juniperus communis", "Taxus baccata", "Larix decidua",
"Abies alba", "Rosa canina", "Rubus idaeus", "Hedera helix",
"Clematis vitalba", "Berberis vulgaris", "Betula pubescens", "Prunus padus",
"Rhamnus cathartica", "Lonicera xylosteum", "Trifolium repens", "Trifolium pratense",
"Festuca ovina", "Dactylis glomerata", "Plantago lanceolata", "Plantago major",
"Plantago media", "Achillea millefolium", "Ranunculus acris", "Ranunculus repens",
"Urtica dioica", "Poa pratensis", "Poa annua", "Galium mollugo",
"Galium aparine", "Bellis perennis", "Cardamine pratensis", "Cirsium arvense",
"Cirsium vulgare", "Daucus carota", "Heracleum sphondylium", "Anthriscus sylvestris",
"Lotus corniculatus", "Medicago lupulina", "Vicia cracca", "Lathyrus pratensis",
"Stellaria media", "Silene dioica", "Silene vulgaris", "Geranium pratense",
"Geranium robertianum", "Glechoma hederacea", "Lamium album", "Prunella vulgaris",
"Ajuga reptans", "Veronica chamaedrys", "Rumex acetosa", "Rumex obtusifolius",
"Chenopodium album", "Capsella bursa-pastoris", "Senecio vulgaris", "Leucanthemum vulgare",
"Centaurea jacea", "Knautia arvensis", "Campanula rotundifolia", "Primula veris",
"Anemone nemorosa", "Filipendula ulmaria", "Lythrum salicaria", "Robinia pseudoacacia",
"Solidago canadensis", "Solidago gigantea", "Impatiens glandulifera",
"Heracleum mantegazzianum", "Prunus serotina", "Quercus rubra",
"Quercus robur L.", "FAGUS SYLVATICA", "Quercus robus",
"cf. Taraxacum officinale", "Quercus pedunculata", "Festuca rubrra"
)
dat <- taxify(field) |>
add_zanne() |>
add_eive() |>
add_diaz_traits()
head(dat[, c("accepted_name", "woodiness", "eive_light",
"eive_reaction", "eive_nutrients", "plant_height_m")], 6)
```
```
#> accepted_name woodiness eive_light eive_reaction eive_nutrients plant_height_m
#> 1 Quercus petraea woody 5.83 4.79 3.55 31.44
#> 2 Pinus sylvestris woody 7.10 5.13 2.69 19.03
#> 3 Picea abies woody 4.36 4.24 4.31 40.69
#> 4 Betula pendula woody 6.84 4.38 3.64 12.02
#> 5 Acer pseudoplatanus woody 3.80 5.92 6.89 24.46
#> 6 Acer platanoides woody 3.87 6.34 5.66 21.93
```
A few species lack an EIVE or a height value, so those cells are `NA`; R's
statistics functions drop incomplete rows on their own. From here it is ordinary
analysis. Do species on more base-rich soils also sit higher on the nutrient axis?
```{r}
cor.test(dat$eive_reaction, dat$eive_nutrients)
```
```
#> Pearson's product-moment correlation
#>
#> data: dat$eive_reaction and dat$eive_nutrients
#> t = 3.2378, df = 96, p-value = 0.001654
#> alternative hypothesis: true correlation is not equal to 0
#> 95 percent confidence interval:
#> 0.1230070 0.4821711
#> sample estimates:
#> cor
#> 0.3137695
```
Across about a hundred species the correlation is positive and significant, though
modest (r = 0.31, p = 0.002): base-rich soils tend to carry higher nutrient
values. The same three lines work for any attribute the package can attach.
## Where to go next
This vignette is the fast path. Each step has a dedicated vignette with the full
detail:
- [Choosing and combining backbones](https://gillescolling.com/taxify/articles/backbones.html)
- [Fuzzy matching](https://gillescolling.com/taxify/articles/fuzzy-matching.html)
- [Enrichments](https://gillescolling.com/taxify/articles/enrichments.html)
- [Custom data](https://gillescolling.com/taxify/articles/custom-data.html)
- [Hybrids and aggregates](https://gillescolling.com/taxify/articles/hybrids-and-aggregates.html)
- [Large-scale matching](https://gillescolling.com/taxify/articles/large-scale.html)
- [Migrating from taxize and WorldFlora](https://gillescolling.com/taxify/articles/migration.html)
```