benchmark.RmdAs 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.
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)
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")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 <- which(grepl("[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)))
knitr::kable(match_tbl, align = "r")Docker (or the
camwebb/taxon-tools:v1.3.0image) was not available when this vignette was built, so the benchmark was not run. Install Docker and pull the image, then re-render this vignette to see live results.
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.