(v3.0.1.9094) Taxonomy build: resolve cross-domain genus homonyms by status and source (#309)

Genus resolution now prefers LPSN/MycoBank over GBIF, then accepted over
synonym, then source, then rank, and only then domain. Losing LPSN/MycoBank
homonyms are removed instead of moved into another domain. Adds integrity
checks that LPSN records stay prokaryotic and MycoBank records stay fungal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
dr. M.S. (Matthijs) BerendsandClaude Opus 5.5 committed 2026-10-03 08:08:46 +02:00
1 parent dab4c47e86
commit b264f175f4
3 files changed
+83 -17

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
Package: AMR
Version: 3.0.1.9092
Date: 2026-10-01
Version: 3.0.1.9094
Date: 2026-10-03
Title: Antimicrobial Resistance Data Analysis
Description: Functions to simplify and standardise antimicrobial resistance (AMR)
data analysis and to work with microbial and antimicrobial properties by
+1 -1
View File
@@ -1,4 +1,4 @@
# AMR 3.0.1.9092
# AMR 3.0.1.9094
Planned as v3.1.0, mid October 2026.
@@ -1242,8 +1242,19 @@ taxonomy <- taxonomy %>%
taxonomy %>% count(rank, sort = TRUE)
# Resolve genera that appear in multiple domains (e.g. Giardia in Protozoa
# and Animalia). Prefer Bacteria > Fungi > Protozoa > Archaea > Chromista >
# Animalia, then overwrite domain-to-family for the entire genus.
# and Animalia, or Trichophyton in Fungi and as a cyanobacterium in GBIF).
# The winning lineage per genus is decided in this order:
# 1. Authoritative sources (LPSN, MycoBank) always beat GBIF and other sources,
# so GBIF can never overrule LPSN or MycoBank.
# 2. Accepted beats non-accepted, based on the genus-rank record of that
# source/domain (or its species if the genus record is missing). So a
# synonymic bacterial genus in LPSN loses from an accepted fungal genus in
# MycoBank with the same name (e.g. Pirella), and vice versa.
# 3. Source: LPSN > MycoBank > GBIF > other.
# 4. Genus-rank records over species/subspecies records.
# 5. Domain: Bacteria > Fungi > Protozoa > Archaea > Chromista > Animalia >
# Plantae, which only breaks ties within the same source (e.g. Giardia within GBIF).
# The winning lineage then overwrites domain-to-family for the entire genus.
taxonomy %>% count(domain, sort = TRUE)
domain_priority <- c(
"Bacteria" = 1L,
@@ -1254,18 +1265,41 @@ domain_priority <- c(
"Animalia" = 6L,
"Plantae" = 7L
)
source_priority <- c("LPSN" = 1L, "MycoBank" = 2L, "GBIF" = 3L, "manually added" = 4L)
# find the best domain per genus from genus-rank records
# find the best domain per genus, from all records of that genus (so that e.g. a
# MycoBank species also counts when the genus-rank record only came from GBIF)
best_domain <- taxonomy %>%
filter(rank == "genus", genus != "", domain %in% names(domain_priority)) %>%
filter(
genus != "",
rank %in% c("genus", "species", "subspecies"),
domain %in% names(domain_priority),
# a source only counts within its own scope, so that a lineage inherited
# from an older (possibly flawed) version of `microorganisms` cannot vote
!(source == "LPSN" & !domain %in% c("Bacteria", "Archaea")),
!(source == "MycoBank" & domain != "Fungi")
) %>%
group_by(genus, source, domain) %>%
mutate(
dprio = domain_priority[domain],
sprio = recode_values(source,
"LPSN" ~ 1L,
"MycoBank" ~ 2L,
"GBIF" ~ 3L,
default = 4L)) %>%
arrange(genus, dprio, sprio) %>%
# status of this genus in this source/domain: from the genus-rank record if
# available, otherwise accepted if any of its species/subspecies is accepted
genus_status = if (any(rank == "genus")) {
status[rank == "genus"][1]
} else if (any(status == "accepted")) {
"accepted"
} else {
status[1]
}
) %>%
ungroup() %>%
mutate(
tprio = if_else(source %in% c("LPSN", "MycoBank"), 1L, 2L),
stprio = if_else(genus_status %in% "accepted", 1L, 2L),
sprio = coalesce(unname(source_priority[source]), 5L),
rprio = if_else(rank == "genus", 1L, 2L),
dprio = unname(domain_priority[domain])
) %>%
arrange(genus, tprio, stprio, sprio, rprio, dprio) %>%
distinct(genus, .keep_all = TRUE) %>%
select(genus, best_domain = domain, best_kingdom = kingdom,
best_phylum = phylum, best_class = class,
@@ -1273,6 +1307,15 @@ best_domain <- taxonomy %>%
taxonomy <- taxonomy %>%
left_join(best_domain, by = "genus") %>%
# LPSN and MycoBank records of a losing homonym are different organisms under
# another nomenclatural code, so remove them instead of moving them to the
# winning domain (e.g. the bacterial Pirella species if the fungal Pirella wins)
filter(
!(genus != "" & !is.na(best_domain) & source == "LPSN" &
!best_domain %in% c("Bacteria", "Archaea")),
!(genus != "" & !is.na(best_domain) & source == "MycoBank" &
best_domain != "Fungi")
) %>%
mutate(
domain = if_else(genus != "" & !is.na(best_domain), best_domain, domain),
kingdom = if_else(genus != "" & !is.na(best_kingdom), best_kingdom, kingdom),
@@ -1286,6 +1329,20 @@ taxonomy <- taxonomy %>%
c("genus", "species", "subspecies"), best_family, family)) %>%
select(-starts_with("best_"))
# integrity tests: no LPSN record may have left the prokaryotes and no MycoBank
# record may have left the fungi (see issue #309, Trichophyton as cyanobacterium).
# Both must return 0 rows - otherwise check the genus resolution above.
taxonomy %>%
filter(source == "LPSN", !domain %in% c("Bacteria", "Archaea", "", NA)) %>%
count(domain, genus, sort = TRUE)
taxonomy %>%
filter(source == "MycoBank", !domain %in% c("Fungi", "", NA)) %>%
count(domain, genus, sort = TRUE)
# and these known cross-domain homonyms must end up in the right domain
taxonomy %>%
filter(rank == "genus", genus %in% c("Trichophyton", "Pirella", "Tubercularia")) %>%
select(fullname, domain, kingdom, phylum, family, source)
# check if some genera within kingdoms have multiple families / orders, etc:
taxonomy %>%
filter(genus != "") %>%
@@ -1345,9 +1402,12 @@ taxonomy <- taxonomy %>%
rank == "kingdom" ~ kingdom,
rank == "domain" ~ domain,
TRUE ~ paste(genus, species, subspecies) # already trimmed 7 lines up
))) %>%
arrange(fullname) %>%
distinct(fullname, .keep_all = TRUE)
))) %>%
# keep the record of the most authoritative source: LPSN > MycoBank > GBIF > other
mutate(sprio = coalesce(unname(source_priority[source]), 5L)) %>%
arrange(fullname, sprio) %>%
distinct(fullname, .keep_all = TRUE) %>%
select(-sprio)
# *** Save intermediate results (0) *** -----------------------------------------------------------
@@ -3294,6 +3354,12 @@ microorganisms$fullname[
any(duplicated(taxonomy$mo))
any(duplicated(taxonomy$fullname))
# LPSN records must be prokaryotes and MycoBank records must be fungi, also in their MO code (issue #309)
all(taxonomy$domain[taxonomy$source == "LPSN"] %in% c("Bacteria", "Archaea"))
all(taxonomy$mo[taxonomy$source == "LPSN"] %like% "^(B|A)_")
all(taxonomy$domain[taxonomy$source == "MycoBank"] == "Fungi")
all(taxonomy$mo[taxonomy$source == "MycoBank"] %like% "^F_")
# Update other data sets --------------------------------------------------------------------------