Background

As of version 2.0.0, taxastand parses and matches taxonomic names with a pure-R reimplementation of taxon-tools (parsenames and matchnames). Earlier versions shelled out to the original taxon-tools programs, either via a local install or the camwebb/taxon-tools:v1.3.0 Docker image.

The pure-R engines produce byte-for-byte identical output to the original tool (this is checked in the package’s test suite against the Docker image). The question this vignette answers is: how do they compare on speed?

The comparison below is against the aregex build of taxon-tools shipped in the Docker image, which uses gawk’s amatch() for fuzzy matching — the faster of the tool’s two fuzzy backends.

Setup

library(taxastand)
data(filmy_taxonomy)

dir <- tempfile("bench")
dir.create(dir)

# Best-of-n elapsed seconds for a zero-argument function
timeit <- function(f, reps = 2) {
  min(vapply(
    seq_len(reps),
    function(i) system.time(f())[["elapsed"]], numeric(1)
  ))
}

# Run a taxon-tools command in the Docker image, mounting `dir` at /data
docker_run <- function(args) {
  system2("docker", c("run", "--rm", "-v", paste0(dir, ":/data"), image, args),
    stdout = TRUE, stderr = FALSE
  )
}

base <- unique(filmy_taxonomy$scientificName)
base <- base[!is.na(base) & nzchar(base)]
length(base)
#> [1] 2727

Parsing

parse_rows <- lapply(c(1, 5, 10), function(mult) {
  names_vec <- rep(base, mult)
  records <- paste0("n", seq_along(names_vec), "|", names_vec)
  writeLines(records, file.path(dir, "p.txt"), useBytes = TRUE)
  n <- length(records)

  r_t <- timeit(function() taxastand:::tt_parsenames(records))
  d_t <- timeit(function() docker_run(c("parsenames", "/data/p.txt")))

  data.frame(
    `Names parsed` = n,
    `pure R (s)` = round(r_t, 2),
    `taxon-tools (s)` = round(d_t, 2),
    `R names/s` = round(n / r_t),
    `Speed-up` = sprintf("%.1fx", d_t / r_t),
    check.names = FALSE
  )
})
parse_tbl <- do.call(rbind, parse_rows)
knitr::kable(parse_tbl, align = "r")
Names parsed pure R (s) taxon-tools (s) R names/s Speed-up
2727 0.19 0.41 14583 2.2x
13635 0.99 1.25 13717 1.3x
27270 2.00 2.32 13662 1.2x

Matching

The reference (the whole taxonomy) and a query set (misspellings and author-dropped variants of a sample) are parsed up front, so that only the matching step is timed.

parse_lines <- function(x, prefix) {
  lines <- taxastand:::tt_parsenames(paste0(prefix, seq_along(x), "|", x))
  rest <- sub("^[^|]*\\|", "", lines)
  lines[gsub("\\|", "", rest) != ""] # drop unparseable names
}

ref_lines <- parse_lines(base, "r")

set.seed(1)
samp <- sample(base, 300)
mis <- vapply(samp, function(s) {
  cs <- strsplit(s, "")[[1]]
  pos <- grep("[a-z]", cs)
  if (length(pos) > 3) cs[pos[length(pos) %/% 2]] <- "x"
  paste(cs, collapse = "")
}, character(1))
query <- unique(c(samp, mis, sub(" [A-Z(].*$", "", samp)))
query_lines <- parse_lines(query, "q")

writeLines(ref_lines, file.path(dir, "ref.txt"), useBytes = TRUE)
writeLines(query_lines, file.path(dir, "query.txt"), useBytes = TRUE)

match_rows <- lapply(c(5, 10), function(e) {
  r_t <- timeit(function() {
    taxastand:::tt_matchnames(query_lines, ref_lines, max_dist = e)
  })
  d_t <- timeit(function() {
    docker_run(c(
      "matchnames", "-a", "/data/query.txt", "-b", "/data/ref.txt",
      "-o", "/data/out.txt", "-e", e, "-F"
    ))
  })

  data.frame(
    `max_dist` = e,
    `pure R (s)` = round(r_t, 2),
    `taxon-tools (s)` = round(d_t, 2),
    `Speed-up` = sprintf("%.1fx", d_t / r_t),
    check.names = FALSE
  )
})
match_tbl <- do.call(rbind, match_rows)
cat(sprintf(
  "%d queries against %d references\n",
  length(query_lines), length(ref_lines)
))
#> 896 queries against 2727 references
knitr::kable(match_tbl, align = "r")
max_dist pure R (s) taxon-tools (s) Speed-up
5 2.62 3.79 1.4x
10 3.44 6.69 1.9x

Takeaways

On this machine, parsing 2,727 names took 0.19s in pure R vs 0.41s via taxon-tools (2.2x), and matching at the default max_dist = 10 was 1.9x faster.

  • Parsing is several times faster in pure R. Both implementations have near-constant per-name cost; the gap is largest for small inputs, where Docker’s container start-up dominates, but pure R stays well ahead even at tens of thousands of names.

  • Matching is where the difference is dramatic, and it grows with max_dist. The original tool fuzzy-matches with gawk’s amatch() (TRE approximate-regex matching), whose cost climbs steeply as the allowed edit distance increases. The pure-R engine uses base R’s vectorised adist() restricted to within-genus candidates, which barely moves from max_dist 5 to 10.

  • Beyond raw speed, the pure-R implementation removes the need to install taxon-tools or run Docker at all, while producing identical results.

These numbers depend on hardware, dataset size, and the proportion of names that require fuzzy matching; re-render this vignette to measure them on your machine.

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
#>  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
#>  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
#> [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#> 
#> time zone: UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] taxastand_2.0.0.9000
#> 
#> loaded via a namespace (and not attached):
#>  [1] digest_0.6.39     desc_1.4.3        R6_2.6.1          fastmap_1.2.0    
#>  [5] xfun_0.60         magrittr_2.0.5    cachem_1.1.0      knitr_1.51       
#>  [9] htmltools_0.5.9   rmarkdown_2.31    lifecycle_1.0.5   cli_3.6.6        
#> [13] sass_0.4.10       pkgdown_2.2.1     textshaping_1.0.5 jquerylib_0.1.4  
#> [17] systemfonts_1.3.2 compiler_4.6.1    tools_4.6.1       ragg_1.5.2       
#> [21] bslib_0.11.0      evaluate_1.0.5    yaml_2.3.12       otel_0.2.0       
#> [25] jsonlite_2.0.0    rlang_1.3.0       fs_2.1.0