A compiled, harmonized database of species body masses (in grams) drawn from many published sources spanning the full diversity of heterotrophic life — from microbes to large vertebrates. Developed in support of FracFeed_DB and TaxonBodyMassML.
Body mass data are compiled from primary sources and databases covering organisms across all major taxonomic groups, including bacteria, protists, marine zooplankton and invertebrates, spiders, fish, amphibians, reptiles, birds, and mammals (modern and fossil). Sources include both ecological databases (food-web, metabolic-scaling, predator-prey, movement) and taxonomic trait databases (life-history, body size). Live downloads from FishBase and SeaLifeBase are supported via the rfishbase package.
Bibliographic information for all sources is in Bib/TaxonBodyMass_Citations.bib.
The repository LICENSE is CC BY-NC 4.0 (Creative Commons Attribution-NonCommercial 4.0 International; owner decision of 2026-10-06, replacing the CC BY 4.0 of earlier commits and releases, which keep the licence they carried) and covers what the lab wrote: the pipeline code, the per-source parse scripts and READMEs, the parsed and transcribed tables, the compiled outputs (TaxonBodyMass.csv, TaxonBodyMass_GenusLevel.csv, TaxonBodyMass_Provenance.csv.gz) and the bibliographies, to the extent the lab holds rights in them. The NonCommercial term applies to all of it: the compiled tables, the bibliographies, the provenance table and the code in this repository may be shared and adapted with attribution for non-commercial purposes only; a commercial use needs separate permission from the owner. It does not relicense any third-party file: every raw source file kept under sources/databases/ and sources/conversion_factors/ stays under the terms of its licensor, recorded in the Licence: line of the folder README, in the tracked manifest sources/source_files.csv (one row per tracked file with size, SHA-256, role, licence class, attribution text and the page to check; regenerated by Rscript R/library/licence_manifest.r) and in sources/LICENSES.md, which is generated from the manifest (issue #6, licence audit of 2026-10-06). Three carve-outs from the repository licence: (1) the non-commercial and share-alike sources, whose files are redistributed here under their own terms only: FishBase and SeaLifeBase (CC BY-NC 4.0; downloaded at run time, nothing stored), Lane 2019 (CC BY-NC 4.0), Cai et al. 2025 and Hoehler et al. 2023 (CC BY-NC-ND 4.0), Animal Diversity Web through Quaardvark (CC BY-NC-SA 3.0), Pata & Hunt 2025 (CC BY-SA 4.0) and Hechinger et al. 2011 (non-commercial scientific use); the compiled species means that incorporate their values are published under CC BY-NC 4.0 as aggregated facts with attribution, which aligns the repository with the non-commercial term of these sources, and whether a downstream use is also bound by the share-alike (Pata & Hunt) or no-derivatives (Cai, Hoehler) terms is for the user of the values to decide (the sources behind every value are listed in source_mass and the provenance table, so they can be excluded); (2) publisher files (article PDFs and journal supplements of Wiley, Springer Nature, PNAS, Elsevier and Cambridge University Press), whose copyright is with the publishers: where the source data is available in a structured format (xlsx, csv, or a PDF whose tables are parseable), the data is extracted to a CSV file that is committed in place of the PDF; the publisher PDF itself is not committed but documented in the folder README (owner rule, issue #124, 2026-10-11; their copyright stays with the publishers, see sources/LICENSES.md); (3) the unpublished MOM v10.2 workbook (issue #4). Sources whose terms are not confirmed are marked Licence: unknown; owner to check <URL> (not yet reviewed) or Licence: unknown; owner unable to confirm <URL> (reviewed but no explicit licence found) and listed in sources/LICENSES.md and in reports/warnings_licences.md at every run; nothing is asserted from a deposit page that is not quoted on disk. CheckSourceDocs() stops a run when a source folder has no Licence: line or no row in the manifest. None of this is legal advice.
The pipeline is written in R. The main entry point is R/RunMe.r, which orchestrates all steps:
-
Per-source parsing — each source has its own script at
sources/databases/<SourceName>/BodyMass_<SourceName>.rthat reads raw data files, extracts taxon names and body masses, converts units to grams, and saves a cached.Rdatafile tosources/Rdata/. Sources that report dry, ash-free dry or carbon mass are converted to wet grams with the group-specific factors inR/library/mass_conversion.r(ToWetMass()); each such script'sREADME.mdstates the mass type reported by the source and the conversion group applied, and the CiteIDs of the conversion references are appended to the record'ssource_masslabel (e.g.Kendall_etal_2019; Studier_1992) so they are cited alongside the source. Rows that the source itself flags as imputed, genus-averaged or taken from another species are removed in the parse scripts (DropImputed()inR/library/helpers.r) and logged toaudit/imputed_rows.csvtogether with the records the raw-name rules of step 2 drop (life stages and small size classes) and the entries of the live download frames, which the download scripts save inaudit/imputed_rows_live.csv(issue #40; the file is written after step 2, only when the parse scripts ran, and is the same whatever the download flags were); each source'sREADME.mdrecords this in anImputed rows:line, whichR/library/check_source_docs.rchecks at every run, together with aLicence:line stating the terms of the folder's raw files (Licence: <class> (<where stated>; <URL>). <note>, orLicence: unknown; owner to check <URL>), which must agree with the manifestsources/source_files.csv(Data licences above; a folder without the line or without a manifest row stops the run, an unknown licence is warned about and written toreports/warnings_licences.md; exercised without network access byRscript R/library/tests/test_licence_docs.R). These cached files are git-ignored and regenerated by settingrecompile = TRUEinRunMe.r. -
Taxon name cleaning — applied uniformly to every source before merging. The rule of the database is that a cleaned name has two parts,
Genus_species, or one,Genus, and nothing but letters; every raw name that does not already look so is reduced to that form by an explicit rule, and a raw name no rule covers stops the run (issue #38):R/library/fix_formatting.r—FixFormatting()first normalizes encoding (only strings that are not valid UTF-8 are converted from Latin-1; diacritics are then transliterated to ASCII, soNausithoë rubrabecomesNausithoe_rubra) and strips the sex signs ♀/♂, keeping the record as the species' value (the twoTetrao urogallus ♀records of Makarieva_2008 areTetrao_urogallusrecords; issue #37). It then parses the name into its bracket groups and tokens and applies the tracked vocabularyaudit/raw_name_patterns.csv(one regex per row with itsscope,class,action,noteand date; seeaudit/README.md): a capitalised word in brackets between genus and epithet is a subgenus and is removed (Jaera (Jaera) albifrons->Jaera_albifrons; a genus written twice,Castor Castor canadensis, loses the repeat); sex marks ((F),Female,males, the signs) and form, strain, region ((colonial),UAB CTIP,Bulgaria) annotations are stripped and the record kept (the form written in place of the epithet, Brose_2005'sConochilus_colonial, folds to the genus-level recordConochilus; issue #48); of the Brose soil-web size classes the medium and large ones ({m},{l},{xl},{xxl},{xxxl},large) are stripped and the record kept while the small ones ({xs},{s},small) are non-adult records and are dropped throughDropImputed()(owner decision 2026-10-04); a species group marker (Polypedilum halterale group,Hylaeus modestus grp) is stripped and the record credited to the nominal species; a synonym or common name in brackets after the binomial and an authority (L.,(Gmelin, 1789)) are stripped; a lowercase third token, with or without a rank marker (var.,ssp.), is a subspecies or variety epithet and folds into the species (trinomial:Acanthiza pusilla apicalis->Acanthiza_pusilla); a placeholder epithet (sp.,spp.,spec.,indet.,ssp,undefinable,undetermined, morphospecies codes such assp2orlassp8,species A,Unidentified) or an identification qualifier (cf.,aff.,nr.) leaves the function as the markerGenus_sp/Genus_cf(or the word as written:Genus_spp,Genus_spec,Genus_indet,Genus_nr), whichfix_nontaxa.rremoves, as before; life-stage annotations ((N1),(IV),(megalops),Juveniles,immature) make the record a non-adult and it is dropped throughDropImputed()(scope rule of issue #8); hybrids (Anas platyrhynchos x rubripes->Anas_platyrhynchos) and names listing two or more ambiguous alternatives (Empidonax traillii/alnorum->Empidonax_traillii,Pipilo maculatus, ocai->Pipilo_maculatus) are cut at the separator and credited to the first name written, a bare genus before the separator giving a genus-level record (owner decision 2026-10-04); the wordhybridin place of the epithet (Anas hybrid, VertNet) is a hybrid of unrecorded parentage and is credited to the bare genus the same way (issue #48; until then GBIF matchedAnas_hybridto Chloephaga hybrida); characters that cannot occur in a Latin name (symbols) are removed. Whitespace and capitalization are normalized throughout. Every name a rule touched is listed by class and source inreports/warnings_raw_names.md; a bracket group or trailing token no rule covers is an error: the name keeps its brackets andCheckRawNames()(below) stops the run, so a new pattern is classified by hand instead of being folded into a plausible binomial (which is howCrithidia (Strigomonas) oncopeltibecame the unresolvableCrithidia_strigomonas). Exercised without network access byRscript R/library/tests/test_raw_name_rules.R(one fixture per class, the report, the format of the rename tables, 60 pinned ordinary names) andRscript R/library/tests/test_fix_formatting_encoding.RR/library/fix_misspellings.r— corrects known misspellingsR/library/fix_nontaxa.r— removes placeholder identifiers (theGenus_sp,_spp,_spec,_indet,_type,_cf,_aff,_nr,_unkmarkers, COMBINE'sNot_recognisedand the_fragment(s)/_immature(s)group labels of Hrycik_2024), functional-group labels and common names that the food-web compilations use as names (Bacteria,Ciliates,Plankton, the Chichewa fish names of the Lake Malawi web, ...; a one-token label would otherwise be filed as a genus-level record, issue #44), and other non-taxon entries. Exercised without network access byRscript R/library/tests/test_nontaxa_labels.RR/library/filter_extinct.r— removes extinct species (list inaudit/extinct_taxa.csv, compiled by hand withR/library/build_extinct_taxa.r(not sourced byRunMe.r) from the status columns of MOM v10.2, PHYLACINE 1.2 and AVONET); the MOM and PHYLACINE scripts also drop their own extinct rowsR/library/check_taxon_names.r— three guards.CheckRawNames()runs right afterFixFormatting()and stops the run with the list of raw names (and their sources and row counts) that carried brackets or trailing tokens the vocabulary does not cover (issue #38).CheckTaxonNames()runs after the chain and stops the run if any cleaned name is notGenus_speciesorGenus(whitespace, brackets, digits, a third token), which only a malformed replacement value in a rename table can produce;RunMe.rtells species-level from genus-level records by the underscore when it merges the sources, so such a name would be filed as a genus (issue #28). A unit test asserts that every key and value offix_misspellings.rand every entry offix_nontaxa.rhas that format and that no key is duplicated (thefix_taxonomy_ranks.rkeys match thespeciescolumn,Genus epithetwith a space, and are checked as such).CheckSheetTaxa()runs in step 3 on the rows of the lab Sheet, which are bound after the chain and so pass neitherFixFormatting()norCheckTaxonNames(): every Sheet taxon with a mass must beGenus_speciesorGenus, since the override matches Sheet names against the cleaned source names by equality; a three-part name (a subspecies) folds to the species with a warning, as the source trinomials do, and a cell with brackets, blanks, a digit, a lowercase genus or no name stops the run with the offending rows (taxon, mass, source) and a request to correct the Sheet (issue #48). Exercised without network access byRscript R/library/tests/test_taxon_name_guard.Randtest_raw_name_rules.R
-
Google Sheet override — lab-curated values (
BM_datatab) take priority over the compiled sources. The tab is read from its tracked copysources/BM_data_snapshot.csv(the Sheet's rows and columns, every cell as text;R/library/sheet_snapshots.r): a run withDataSheets = TRUEat the top ofRunMe.rfirst downloads the three Sheet tabs the pipeline uses and rewrites their copies, printing for each the rows added, removed and changed against the committed file, and the run goes on; withDataSheets = FALSE(the committed default) the copies are read as they are, no Google token is needed, and the run says when each copy was last committed, so a stale copy is visible (issue #118). The outputs are the same whichever mode wrote the copies. The Sheet rows with a mass are first checked byCheckSheetTaxa()(R/library/check_taxon_names.r; section 4 ofRunMe.r): every taxon must be a cleaned name,Genus_speciesorGenus, a three-part name folds to the species with a warning, and anything else stops the run with the offending rows (issue #48). The rows are then split by the rule that tells the two kinds of record apart throughout the pipeline, the underscore (R/library/sheet_override.r, issue #57): a species-level row (Genus_species) replaces all compiled records of that species, so the Sheet value is the species' only record; a genus-level row (a bareGenus) is a genus-only record: it replaces the sources' genus-only rows written as that bare name (matched, like the species rows, by equality of the cleaned name) and joins the genus-only path of step 6, where it is resolved at genus rank, filtered, combined per genus and source and de-duplicated like the sources' rows. A genus-level Sheet row does not touch the species-level values of the genus (Lepidostomareplaces the sources'Lepidostomarows and leavesLepidostoma_hirtumalone; the genus mean of step 6 averages both). Until #57 every Sheet row entered the species path, where a bare name was dropped as unresolved or, forHypopomus, resolved by ITIS to the species Hypopomus artedi; the rows such names left in the species enrichment cache are dropped when the cache is loaded (step 4). Exercised without network access byRscript R/library/tests/test_sheet_override.R. -
Taxonomy enrichment and autotroph removal — every unique name is resolved once to an accepted species and its full kingdom-to-species classification through GBIF, NCBI, WoRMS, Catalogue of Life (the ChecklistBank name-usage matcher of the COL checklist,
dataset/3LR/match/nameusage; until issue #103 the stage called the names-index endpoint and never resolved a name), ITIS and Wikidata in turn, with a final GBIF backfill of missing higher ranks (R/library/enrich_taxonomy.r; details insources/passes/enrichment_pipeline.md). A later stage never replaces a species an earlier one set; the names Catalogue of Life resolves (names GBIF, NCBI and WoRMS lack) are listed inreports/warnings_taxonomy.mdwith the matched name, match type and status. The Catalogue of Life stage is exercised without network access byRscript R/library/tests/test_col_stage.Ron recorded responses. Results are cached insources/enrich_cache.Rdata(objectenrich_cache; the genus-rank resolutions of step 6 are cached in the same file asgenus_cache) so re-runs only query new names; the cache holds species-level names only, and a bare name an earlier run left there (step 3, issue #57) is dropped when it is loaded. Manual corrections live inR/library/fix_taxonomy_ranks.r. Its Part 2b guards the autotroph filter that follows (issue #81): when an authority's kingdom is Plantae, Viridiplantae or Fungi but the lower ranks are an animal's (an exact homonym in two kingdoms, where GBIF abstains and NCBI answers with the plant, Myrmecia pyriformis the bull ant and a green alga; or a GBIF fuzzy match below confidence 90 into the other kingdom, Parus humilis -> Pyrus humilis), the row keeps the animal's ranks: the authority's kingdom is recorded in the cache columnkingdom_conflict, the kingdom becomes Animalia, the plant ranks are cleared for rank inference, and the species is kept only when it is the input binomial (otherwise it is cleared and the name is unresolved). Such rows are listed inreports/warnings_taxonomy.mdunder Kingdom conflicts corrected before the autotroph filter, with their sources and record counts; before #81 the filter dropped them on the kingdom alone and no report saw them. The known cases have full Part 7 overwrites. Exercised without network access byRscript R/library/tests/test_kingdom_conflicts.R.R/library/filter_autotrophs.rthen removes autotrophs: the kingdoms Plantae, Viridiplantae and Fungi; predominantly photosynthetic phyla (Ochrophyta, Bacillariophyta, Haptophyta, Cryptophyta, Chlorophyta, Rhodophyta, Charophyta, Glaucophyta, Streptophyta, Euglenophyta, Cyanobacteria, Cyanobacteriota); and a curated list of photosynthetic or mixotrophic dinoflagellate and euglenid genera. Heterotrophic protists (ciliates, foraminifera, apicomplexans, kinetoplastids, amoebae, heterotrophic dinoflagellates and euglenids) are retained. -
Merging, de-duplication and range filter — within each source, body masses are summarised as the geometric mean across records resolving to the same accepted species. Records are grouped by the source label (the first token of
source_mass; the conversion CiteIDs appended after;do not split a source), and the six VertNet dumps are pooled as one source because thetraitsdump re-exports specimens of the class dumps (every dump label stays insource_mass). Before this first pass the records are put in a fixed order (input name, source label,source_mass, value; byte order,OrderForPass1()inR/library/helpers.r):merge()leaves the records of a name in the order of the bound source frames, and the floating-point sum behind a geometric mean depends on that order, so without it the last digit of a fewlog10_rangevalues moved whenever a source or a cleaning rule reordered rows (issue #53). Exercised without network access byRscript R/library/tests/test_output_stability.R.The same datum often reaches the database through several compilations (AVONET and EltonTraits both reproduce Dunning's CRC Handbook; COMBINE reproduces Amniote, PHYLACINE and MOM; Hoehler et al. 2023 reproduces Makarieva et al. 2008, which reproduces Chown et al. 2007; Meiri 2018 applies the Feldman et al. 2016 equations to the same lengths, and so on). Averaging such copies as independent estimates inflates
nand biases the cross-source mean, soR/library/dedupe_sources.rcollapses them before Pass 2 (issue #5). Two layers combine: the hand-maintained dependency registryBib/source_dependencies.csv(one row per edgechild_label -> parent_label, see below) collapses a child value into its parent, or into a sibling that shares a parent outside the database, only when the two values also agree within the edge's tolerance (tol_log10: 1e-6 for verbatim copies, 0.0105 where the child re-rounded or re-derived the value); edges are transitive, andprovenance_onlyedges document a relation whose values generally differ: they grant no tolerance-based (registry) collapse. A blind rule then collapses values that are the same number to at least three significant digits in any two sources of a species, whatever the registry says about the pair (identical values are the same datum; on aprovenance_onlypair such a collapse is labelledblind (provenance_only edge)in the summary report); values of one or two significant digits, round numbers such as 20 or 1500 g, are left alone as ambiguous. Matching values form a component per species; the registry ancestor (or the lowest-prioritysibling) is kept and the others are recorded insource_dependenciesasdropped<kept.reports/dedupe_summary.mdlists every edge with the species it shares and collapsed, the species left with a single independent value, and the residual identical pairs the registry does not explain (candidates for new edges or shared primary literature).Across sources, the independent values are combined as the arithmetic mean (owner decision 2026-10-03, issue #16): the arithmetic mean is the published convention of the database, so outputs stay comparable with earlier releases and with the manuscript; within-source skew is already handled by the geometric mean of the first pass, and the cross-source step combines per-source central values, where the choice matters less once duplicated values are collapsed; switching to the geometric mean would have moved 795 species by more than 0.1 log10, always downward, a larger and systematic shift than de-duplication, and would entangle two effects that should stay separable; neither choice affects
log10_rangeor the range filter.log10_rangerecords the spread of the values in the mean (log10 of the largest over the smallest, written to six decimals; the range filter below and the QC reports use the unrounded value),source_masslists every contributing source,ncounts every record, andn_sources,n_independentandsource_dependenciesrecord the de-duplication.R/library/check_enriched.rwrites QC reports toreports/(warnings_taxonomy.md, which also lists every cleaned name no enrichment stage resolved, species-level names and genus-only names (step 6) alike, with its sources and row counts;warnings_mass_values.md;warnings_name_change.md), and species whose values in the mean disagree by more than one order of magnitude (log10_range > 1) are removed (R/library/remove_high_range.r). Counts from this step (includingnValuesCollapsed,nSpeciesSingleDatum,nDependencyEdges,nValuesExcluded,nSpeciesRescuedandnRemovedHighRange) are written to../TaxonBodyMassML/ms/numbers_db.texfor the manuscript.Record-level range rule (issue #34, owner decisions of 2026-10-06;
R/library/exclude_discordant.r, run between the de-duplication and the cross-source mean). Until #34 a species whose independent values spanned more than an order of magnitude was removed whole; in four of five such multi-value species a single value caused the spread (a VertNet juvenile, a dry mass taken as wet, a jellyfish ephyra, a web-level food-web datum) while the others agreed closely.ExcludeDiscordantValues()now judges each such species by a short cascade and excludes one value (or one side) from its mean when the cascade singles it out: (A) leave-one-out,loo_unique: exactly one value whose exclusion brings the rest within 1 log10, provided the rest span at least two evidence groups (sources the dependency registry relates, directly or through a common parent, count as one group, so two near-copies cannot outvote one independent value); (A2) several such values: the one of lowest trust tier when the tiers single it out (tier_tiebreak), else the one farthest from the median of the others (distance_tiebreak); (D) two values, or one evidence group against one value: the lower-trust side when the tiers differ (two_value_tier), the kept side itself spanning at most 1 log10; (E) otherwise nothing is excluded and the species falls to thelog10_range > 1filter as before (unresolved:same_tier,maximum_cannot_decide,one_group,no_single_culprit). Trust tiers come from the owner-editablevalue_tiercolumn ofBib/source_provenance_classes.csv: 1 for compiled or measured adult values (the default), 2 for maxima-based or allometric sources (Feldman_etal_2016,Meiri_2018,fishbase,sealifebase), 3 for single specimens, individuals, converted or web-level values (the VertNet dumps,Brose_2005,Brose_etal_2018,Vanni_2017,Gonzalez_2025,Castro_2025,DeLong_etal_2010,Kiorboe_2013,Kiorboe_2014,Pata_2025); a value converted from dry, ash-free dry, carbon or energy mass (a conversion CiteID after the label insource_mass, step 1) is tier 3 whatever its source's tier. The tiers are direction-blind (a low and a high outlier are judged alike), and a tier-2 (maxima-based) value may be excluded but never decides against another value: in (A2) it never outranks a tier-3 candidate (the tie falls to the distance) and in (D) a tier-2 side never excludes a tier-3 side. No record-count tie-break is used, and no per-species rule exists (the per-species rule files of 2026-08 were retired under #24). Copies the de-duplication collapsed into an excluded value are excluded with it. An excluded value stays insource_massandn, is left out ofmass_g,n_independentandlog10_range, and is named insources_excluded(label<rule,n_excludedcounting them); its provenance rows stay inTaxonBodyMass_Provenance.csv.gzwithrecord_statusexcluded_<rule>.reports/excluded_records.csvis the live list of a run's exclusions (species and genus level, one row per excluded value with the kept values beside it;single_value_rescuemarks the species left with one kept value, which the tiers rather than the data decided and which are flagged SUSPICIOUS for review, owner decision 2026-10-06) andreports/warnings_mass_values.mdlists them in its own section with a sub-section for the single-value rescues; the registeraudit/flagged_species.csvholds the one append-only batch of the adoption (methodrange_rule_2026-10-06: one row per excluded value and one SUSPICIOUS row per single-value rescue;audit/README.md). Genus-only records are judged by the same module in step 6. Exercised without network access byRscript R/library/tests/test_exclude_discordant.R. -
Genus-level aggregation —
TaxonBodyMass_GenusLevel.csvgives, for every accepted genus, the arithmetic mean of the cross-source means of its species (step 5) and of its genus-only record, each with the weight of one value; synonymous or misspelt input genera therefore fold into the accepted name. A genus-only record is a value for an organism identified to genus only, measured or compiled as the source reports it (population means included, issue #29): a name written as a bare genus (placeholder names such asGenus sp.orGenus spec.are dropped instead, issue #43), including the genus-level rows of the lab Sheet, which replace the sources' rows of the same bare name and leave the species-level values of the genus alone (step 3, issue #57). Such records get the machinery of the species path (R/library/enrich_genus.r, issue #49). Every bare name is resolved once at genus rank, through the accepted genera of the enrichment cache and the GBIF backbone exact-name lookup, with the class/order/family hints the source gives (VertNet, Castro_2025, Pata_2025, Makarieva_2008): synonyms and misspellings fold into the accepted genus (Dendroica->Setophaga,Clethrionomys->Myodes), homonyms are settled by the hints or, failing those, by preferring the heterotroph kingdoms (GBIF's name matcher filesColeopteraandOligochaetaas plant genera), and names of ranks above genus leave the table: the backbone's own family, order and class usages (Staphylinidae,Araneae,Oligochaeta), the majority reading of the other GBIF checklists for names the backbone lacks or misfiles (Crustacea,Heteroptera,Tanytarsini,Polychaeta), a two-name curated list (Anisoptera,Hydracarina) and the family-group suffixes; fuzzy matches are accepted only for names of six letters or more with no exact usage at any rank, at confidence 80 or more and at most two letters apart. The answers are cached asgenus_cacheinsources/enrich_cache.Rdata; names no stage resolved are listed inreports/warnings_taxonomy.mdwith their sources and rows, and their rows are dropped. The resolved records are filtered withFilterAutotrophs()(the extinct list is species-level and does not apply), combined as one value per genus and source (geometric mean, VertNet dumps pooled), de-duplicated with the registry of step 5 (Brose_etal_2018->Brose_2005, ->Hechinger_etal_2011), and combined as the arithmetic mean of the independent per-source values: that single record enters the genus mean with the weight of one species, andn_independentcounts the species' independent values plus one per independent source of genus-only records (owner decision 2026-10-04; one record per source instead would have moved 48 genera by at most 0.17 log10). Names of ranks above genus are dropped without a separate output (167 names and 9,249 rows at the 2026-10-04 run; owner decision). A genus-only record is judged against the genus's species cross-source means by the record-level range rule of step 5 (ExcludeDiscordantGenusRecords(), issue #34): more than 1 log10 from the median of two or more species it is excluded from the genus mean (genus_only_discordant); against a single species the two-value tier rule applies, the record being the only side that can be excluded (two_value_tier; a same-tier pair, or a species value that is a tier-2 maximum against a tier-3 record, leaves the record in and listed). An excluded record stays in the genus row'ssource_massbut not inmass_g,norn_independent; a genus with no other contributor has no row.reports/genus_only_records.mdlists, at every run, the names resolved above genus, the autotroph genera removed, the fuzzy matches and homonym choices to check by eye, the synonyms folded, the values collapsed as copies, and the genus-only records more than an order of magnitude from the genus's species values with their status. Exercised without network access byRscript R/library/tests/test_genus_only_records.R. -
Citations and provenance (issue #1) —
R/RunMe.rsection 8 reads both bibliographies, the hand-curatedBib/TaxonBodyMass_Citations.bib(BibDesk) and the generatedBib/TaxonBodyMass_PrimaryCitations.bib(built byR/library/citations/run_citations.r --bibfrom Crossref metadata or owner-approved fields, never edited by hand; a key present in both files stops the run), and both citation tabs of the lab Sheet from their tracked copiesBib/BM_citations_snapshot.csvandBib/BM_primary_citations_snapshot.csv(BM_citations: source labels and conversion references -> bib keys;BM_primary_citations: the verified primary references appended byrun_citations.r --sheet, which refreshes both copies, as does aRunMe.rrun withDataSheets = TRUE; step 3, issue #118). Sheet rows whose key is in neither bib are listed inreports/warnings_citations.mdinstead of being dropped silently.Bib/TaxonBodyMass_CitationCiteIDs.csvkeepsBibcite, CiteIDas its first two columns and gainsdoi,roleandbib_file. Every record'ssource_massis split before step 5 into the source label and the conversion CiteIDs appended byLabelWithConversion(), the parse scripts' per-record reference keys (ref_keys) travel through the first averaging pass, andTaxonBodyMass_Provenance.csv.gzlinks every species ofTaxonBodyMass.csvto its source labels and, where a source's reference list has been ingested and verified (sources/databases/<Src>/primary_references.csv), to the study that measured the animal, with the hop and provenance type of each reference; records without a reference get the default of the registryBib/source_provenance_classes.csv. A reference that is itself a source label of the database (recognised by CiteID, bib key or DOI; Herberstein et al. 2022 citing Chown et al. 2007) is an intermediate compilation: where the intermediate's own records of the species resolve to primary references the row goes through it (hop2,compiled_via_compilation,via_cite_idthe intermediate), where the intermediate measured the species itself the row cites it as the measurement (compiled_from), and otherwise the intermediate is the terminal citation (compilation_terminal) until its reference list is ingested; no third hop is attempted. The checks (every primary bib key in a bib file, every primary CiteID in a Sheet tab, every accepted reference with a DOI or an approval, per-source coverage) are written toreports/warnings_citations.md. The tooling, its decision rules and its hallucination-robustness rules are described inR/library/citations/README.md;RunMe.rsources only its offline part and never calls Crossref, OpenAlex or Scite.
Bib/source_dependencies.csv records which sources copy or derive their body masses from which others. (The provenance registry Bib/source_provenance_classes.csv, which classifies every source for the primary-source attribution of issue #1, is described in R/library/citations/README.md.) Columns: child_label (a source_mass label), parent_label (a label, or the name of a compilation outside the database such as Dunning_2008, White_2006, Nagy_1999, CareyJudge_2000), parent_in_db (TRUE/FALSE), relation (copies, via an intermediate compilation, shared_primary, derived_same_input, or provenance_only), tol_log10 (the largest |log10 difference| at which a child value counts as the parent's), priority (which of several siblings of an external parent is kept: the lowest number; one value per label), status (confirmed by the source's own documentation or suspected from the values alone), n_rows_child (records the child attributes to the parent where it does so per row), evidence and added. Where several routes connect two labels the loosest tolerance applies. LoadSourceDependencies() stops the pipeline when a child or an in-database parent is not a label of any source frame, when an external parent is one, on a cycle, or on conflicting priorities, so a renamed or removed source has to be reflected in the registry. Add an edge when a source's documentation or a high fraction of identical values (see the residual table of reports/dedupe_summary.md) shows that it reproduces another; use provenance_only when the values differ (the pair then gets no tolerance, but values identical to three significant digits are still collapsed by the blind rule and counted per edge in the report). The helper is tested without network access by
Rscript R/library/tests/test_dedupe_sources.Rand Rscript R/library/dedupe_audit.r [--before <earlier TaxonBodyMass.csv>] replicates Pass 1, the de-duplication and Pass 2 from sources/Rdata and checks the released CSV against it (invariants, agreement, per-edge fractions, the shifts the de-duplication causes).
| File | Description |
|---|---|
TaxonBodyMass.csv |
One row per accepted species. Columns: genus, species, taxon (a representative input name), taxon_provided (all input names, ;-separated), log10_range (spread of the independent per-source values: log10 of the largest over the smallest, six decimals), mass_g (grams, arithmetic mean of the independent values, four significant figures), source_mass (;-separated labels of every contributing source and conversion reference), n (records in all sources), n_sources (per-source values, the VertNet dumps counted once), n_independent (values entering the mean), source_dependencies (values collapsed as copies, dropped<kept pairs ;-separated, empty when none; pooled VertNet dumps appear joined by +), n_excluded and sources_excluded (per-source values the record-level range rule left out of the mean, step 5, issue #34: the count, and label<rule pairs ;-separated, a collapsed copy carrying its parent's rule; empty when none), kingdom, phylum, class, order, family, taxonomy_source, gbif_confidence, gbif_status, gbif_family, gbif_order, species_changed |
TaxonBodyMass_GenusLevel.csv |
One row per accepted genus (pipeline step 6): mass_g (grams, the arithmetic mean of the species' cross-source means and of the genus-only record, from the sources or the lab Sheet, four significant figures), source_mass (;-separated distinct labels and conversion references of every contributor, as in TaxonBodyMass.csv; joined by - before issue #1, which hyphenated labels made ambiguous), n (records) and n_independent (independent values: the species' plus one per independent source of genus-only records). Names of ranks above genus, unresolved bare names and autotroph genera are not in the table |
Bib/source_dependencies.csv |
The source dependency registry used for de-duplication (see pipeline step 5) |
reports/excluded_records.csv |
The live list of the values the record-level range rule excluded in the run (pipeline step 5, issue #34): level (species or genus), genus, species, taxon, source_label, source_mass, mass_g (the excluded Pass-1 value), n, value_tier, rule, distance_log10 (log10 of the value over the median of the kept values, signed), copy_of (the parent of a collapsed copy excluded with it), n_kept, single_value_rescue (TRUE when the species, or the genus, rests on one kept value), kept_mass_g (the mean of the kept values; for a genus row the median of the species means), kept_values. No timestamp; rewritten at every run |
Bib/source_provenance_classes.csv |
The registry of every source_mass label for the primary-source attribution of issue #1: class (primary, compilation, derived, database, live, unknown), default_provenance_type (the provenance type a record gets when it carries no resolved primary reference), equation_bibcite (allometry-derived sources) and notes; R/RunMe.r stops on an unregistered label (R/library/citations/provenance.r) |
Bib/pending_citations.csv |
The review queue of the citation tooling (R/library/citations/README.md): references the automated verification could not settle, with their candidates; the owner's committed decision approves them |
Bib/scite_checks.csv |
The retraction / correction screen of accepted DOIs (checked_by: scite-mcp, consensus-mcp or none), written in Claude Code sessions and read by the citation tooling |
reports/dedupe_summary.md |
Per-edge and residual summary of the de-duplication, regenerated at every run |
reports/genus_only_records.md |
How the genus-only records resolved at genus rank (pipeline step 6): the names above genus and the autotroph genera excluded, the fuzzy matches and homonym choices to check by eye, the synonyms folded, the values collapsed as copies, and the records more than an order of magnitude from the genus's species values (listed, not removed: issue #34); regenerated at every run (issue #49) |
reports/warnings_raw_names.md |
Every raw taxon name that FixFormatting() changed beyond blank -> underscore or classified by a rule of audit/raw_name_patterns.csv, by class and source, with the names dropped and the names no rule covers (empty when the run passes); regenerated at every run (issue #38) |
audit/raw_name_patterns.csv |
The vocabulary of raw-name rules FixFormatting() applies (pipeline step 2): pattern, scope, class, action, note, added; maintained by hand, validated on loading (see audit/README.md) |
audit/imputed_rows.csv |
Log of the rows dropped as imputed by the parse scripts, as life stages or small size classes by the raw-name rules and as group-level placeholders by the live DataRetriever download, one row per DropImputed() call (source, reason, n_dropped, n_taxa, n_kept), ordered by source; rewritten by RunMe.r at every run with recompile = TRUE, merging the saved entries of the live download frames (audit/imputed_rows_live.csv), so that a run with the download flags off reproduces it byte for byte (issue #40; see audit/README.md) |
audit/imputed_rows_live.csv |
The DropImputed() entries of the four live download frames, with frame, written (download date) and frame_md5; saved by R/library/data_retrieve.r, data_vertnet.r and data_fishbase.r when a frame is downloaded (each replacing its own frame's rows) and merged into audit/imputed_rows.csv at every run; checked against the cached frames by R/library/check_cache.r (see audit/README.md) |
audit/extinct_taxa.csv |
The extinct-taxa list applied by RemoveExtinct() (pipeline step 2), rebuilt by hand with R/library/build_extinct_taxa.r from the MOM, PHYLACINE and AVONET status columns |
TaxonBodyMass_Provenance.csv.gz |
One row per species x source label x reference for the species of TaxonBodyMass.csv (pipeline step 7, issue #1): genus, species, taxon, source_mass (one label), source_bibcite, origin (pipeline or BM_data), hop (0: the source measured the animal or a conversion factor; 1: the source cites the reference; 2: through an intermediate compilation that is itself a source label, whose per-record reference is the primary), via_cite_id (the intermediate's CiteID on hop-2 rows), ref_role (measurement, compilation, equation, conversion, self), provenance_type (measured_in_source, compiled_from, compiled_via_compilation, compilation_terminal, derived_allometry, derived_imputed, database_record, conversion_factor, unknown), primary_cite_id, primary_bibcite, primary_doi, match_status (the verification status of the reference; unmatched_key for a key the source's reference list lacks, uningested for a source whose reference list has not been ingested yet), n_records (records behind the row), record_status (kept when the value behind the row is in the species' mean, collapsed copies included; excluded_<rule> when the record-level range rule left it out, step 5, issue #34; the excluded records keep their rows). Gzipped CSV, deterministic (no timestamp); rows in a fixed order |
sources/source_files.csv |
The licence manifest (Data licences above): one row per tracked file under sources/databases/ and sources/conversion_factors/ plus one live row per source downloaded at run time: folder, label, path, role (raw, parsed, derived, code, doc, live), bytes, sha256, licence_class, statement_source, redistributable, attribution, check_url, notes; sizes and hashes regenerated and hand columns kept by Rscript R/library/licence_manifest.r, which also writes sources/LICENSES.md |
sources/LICENSES.md |
Generated from the manifest: the scope of the repository licence, the position on non-commercial and share-alike sources, a table per licence class with attribution texts, the carve-outs, the publisher files, the sources not redistributed and the "unknown; owner to check" list |
reports/warnings_licences.md |
Written by CheckSourceDocs() at every run: the source folders whose licence is unknown (with the URL to check), the non-commercial and share-alike carve-outs, README Licence: lines that disagree with the manifest, and folders without the line or absent from the manifest |
reports/warnings_citations.md |
The checks of pipeline step 7: totals, problems (bib keys, CiteID rows, accepted references without DOI or bib entry), the Sheet rows whose Bibcite is in neither bib, the labels without a CiteID row, and the per-source coverage of the provenance table (record links resolved to a primary reference, reference counts by status); regenerated at every run |
Bib/TaxonBodyMass_CitationCiteIDs.csv |
Maps BibTeX keys to source_mass labels for citation tracing: Bibcite, CiteID (the first two columns, read by TaxonBodyMassML), doi (from the bib entry, when it has one), role (source, conversion or primary), bib_file (Citations or PrimaryCitations) |
Bib/TaxonBodyMass_Citations.bib |
Hand-curated BibTeX bibliography (BibDesk) of the sources, the conversion references and the lab Sheet's citations |
Bib/TaxonBodyMass_PrimaryCitations.bib |
Generated bibliography of the verified primary references (run_citations.r --bib; %% GENERATED, never edited by hand, re-derivable from the primary_references.csv files and the response cache) |
sources/BM_data_snapshot.csv, Bib/BM_citations_snapshot.csv, Bib/BM_primary_citations_snapshot.csv |
The three tabs of the lab Sheet that the pipeline reads (steps 3 and 7), as last imported by RunMe.r with DataSheets = TRUE or, the two citation tabs, by run_citations.r --sheet: the Sheet's rows and columns in its order, every cell as text (an empty cell stays empty; a number as a 15-significant-digit decimal, which reads back exactly for every hand-typed value and within 1e-15 of the Sheet's value for the results of Sheet formulas; owner decision 2026-10-05), written by SnapshotSheetTab() in R/library/sheet_snapshots.r. The pipeline always reads these copies, so a run with DataSheets = FALSE needs no Google token and reproduces the outputs of the import that wrote them; run_citations.r --sheet is also the record of every append (issue #118) |
The tables are reproducible byte for byte: for a given cache (sources/Rdata, sources/enrich_cache.Rdata) and given tracked copies of the lab Sheet tabs (above), two runs write identical TaxonBodyMass.csv, TaxonBodyMass_GenusLevel.csv, TaxonBodyMass_Provenance.csv.gz, reports/excluded_records.csv, audit/imputed_rows.csv and Bib/TaxonBodyMass_CitationCiteIDs.csv, because the records are put in a fixed order before the first averaging pass and the derived floating-point columns are rounded at write time (log10_range to six decimals, mass_g to four significant figures; issue #53). The reports under reports/ differ between runs only in their timestamp line.
Install the required R packages:
install.packages(c("plyr", "dplyr", "googlesheets4", "stringr", "readxl"))
install.packages("rfishbase")
install.packages("rdataretriever")The rdataretriever package requires Python and the retriever Python package. It is used only when DataRetrieve = TRUE in RunMe.r. RunMe.r points reticulate at a dedicated venv at ~/.local/share/r-rdataretriever pinned to Python 3.11 and retriever 2.4.0 (the compatibility patch in R/library/data_retrieve.r targets the 2.x API; retriever 3.x is not supported). Create it once with uv (brew install uv):
uv venv --python 3.11 ~/.local/share/r-rdataretriever
uv pip install --python ~/.local/share/r-rdataretriever "retriever==2.4.0" "setuptools<70"retriever 2.4.0 fails on import with packaging.version.InvalidVersion: Invalid version: '' when any cached dataset script has an empty minimum-version field. Fix it by adding an and mod_ver guard in the venv's retriever/lib/scripts.py:
sed -i '' 's/if hasattr(module, "retriever_minimum_version"):/if hasattr(module, "retriever_minimum_version") and mod_ver:/' \
~/.local/share/r-rdataretriever/lib/python3.11/site-packages/retriever/lib/scripts.pyA message such as tidycensus is supported by Retriever version 3.0.1-dev on load is harmless; it only means that one dataset script is skipped.
To regenerate all per-source .Rdata files and recompile the database from scratch, set recompile = TRUE at the top of R/RunMe.r before running it; the download flags DataRetrieve, DataVertNet and DataFishbase control the four live-download frames, and DataSheets whether the lab Google Sheet is imported: TRUE downloads its three tabs through googlesheets4 (a Google token: a browser prompt on first use, the cached token without a prompt afterwards) and refreshes the tracked copies, printing what changed; FALSE, the committed default, reads the copies and needs no token, so a fresh clone runs as it is (the run stops at once, naming the files, if a copy is missing). Contact mark.novak@oregonstate.edu to contribute new sources or curated values.
The run stops before any cached frame is loaded (step 2) when:
- a parse script fails (
recompile = TRUE) -- every failure is printed as anERROR:line and all of them are listed in the stop message, so one run reports them all. Fix the scripts and rerun withrecompile = TRUE. - a cached frame was not rewritten (
recompile = TRUE) -- a frame insources/Rdata/is older than the loop, e.g. an orphan left by a renamed or removed source, or a script that ran without saving. Delete the orphan or fix the script. - an expected frame is missing (whatever
recompileis) --R/library/check_cache.rexpectsBodyMass_<Source>.Rdatafor everysources/databases/<Source>/BodyMass_<Source>.rplus the four download frames, and the stop message names the flag to set for each missing frame (recompile,DataRetrieve,DataVertNetorDataFishbase). Frames that no current script writes only raise a warning. The three scripts whosesave()target differs from their folder name are listed inframe_overridesincheck_cache.r; a new source whose script saves under another name must be added there, otherwise the check stops and says so.
These guards are exercised on a throwaway source tree, without network access, by
Rscript R/library/tests/test_recompile_guard.RAfter the completeness check, CheckImputedLive() (R/library/check_cache.r) warns, without stopping, when a live frame in sources/Rdata/ is not the file whose download saved its rows in audit/imputed_rows_live.csv (the frame was downloaded again without saving its entries, or the cache was copied from a run with a different frame); rerun with that frame's flag set to TRUE to refresh the frame and its entries together (issue #40). The merge of the saved entries into audit/imputed_rows.csv and this check are exercised by
Rscript R/library/tests/test_imputed_live.RTaxonBodyMass.csv and TaxonBodyMass_GenusLevel.csv are tracked at the repository root and can be used directly without running the pipeline.
Steps 3 and 7 of the pipeline take the lab-curated mass overrides and the citation maps from a lab-internal Google Sheet. The three tabs the pipeline reads are tracked in this repository as CSV copies (sources/BM_data_snapshot.csv, Bib/BM_citations_snapshot.csv, Bib/BM_primary_citations_snapshot.csv; Outputs above), and the pipeline reads the copies whatever DataSheets is set to, so the full pipeline, lab-curated values included, is reproducible offline from a clone with DataSheets = FALSE (the committed default) and without access to the Sheet: the compiled output CSVs committed here are the output of the copies committed with them. DataSheets = TRUE is for the lab: it downloads the tabs with the lab's Google token, rewrites the copies, reports the rows added, removed and changed, and the refreshed copies are committed together with the outputs they produce (issue #118).
Manuscript in preparation.