--- title: "Compatibility with CoordinateCleaner" description: > Where the two packages agree, how that is tested, where the results differ and why, with timings. output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Compatibility with CoordinateCleaner} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") has_cc <- requireNamespace("CoordinateCleaner", quietly = TRUE) && requireNamespace("geosphere", quietly = TRUE) ``` OccurrenceCleaner extends the record-cleaning tests developed in [CoordinateCleaner](https://ropensci.github.io/CoordinateCleaner/) (Zizka et al. [2019](https://doi.org/10.1111/2041-210X.13152)) to tables containing millions to billions of records. It processes large datasets in chunks from disk, including snapshots of the Global Biodiversity Information Facility (GBIF), and adds tests based on GBIF metadata and genus habitats. Each test stores a flag alongside the original record. You can then inspect what was flagged, compare cleaning rules, and choose which records to exclude without rerunning the tests. This makes it practical to explore how cleaning choices affect an entire dataset. CoordinateCleaner provides the underlying test definitions, default distances, and methods for identifying coordinate errors, outliers, dataset-wide patterns, and fossil-age problems. It underwent rOpenSci software peer review in 2018 and remains the methodological reference to cite for these tests. OccurrenceCleaner was first released in 2026; its agreement with CoordinateCleaner is checked by its test suite, and it has yet to undergo independent software review. This article shows where the two packages agree, under which settings, and where their results change and why. It maps each CoordinateCleaner function to its counterpart, describes how agreement is tested and how to check it on your own records, and gives timings for the tests both packages run. The chunks that call CoordinateCleaner run only when it is installed; it is a suggested package, not a dependency. Installation is covered in [Get started](OccurrenceCleaner.html). ## Running both packages on the same records Both packages can run several tests in one call and return every record, with one logical column per test. The columns mean opposite things. In CoordinateCleaner, `TRUE` marks a record that passed the test. In OccurrenceCleaner, `TRUE` marks a record that failed it. Comparing the two outputs therefore means inverting one of them. The example data are 4,000 records placed within 30 km of five capitals, so that some of them lie inside the 10 km buffer of the capital test. Fifty of them are then overwritten with errors: - 20 are placed at (0, 0), which the zero and equal tests flag in both packages. - 20 are given a latitude equal to their longitude, for the equal test. - 10 are given coordinates within 0.3 degrees of zero, where the zero tests of the two packages differ. ```{r both-data, eval = has_cc} library(OccurrenceCleaner) set.seed(3) caps <- cbind(lon = c(16.37, 2.35, -3.70, 13.40, 69.17), lat = c(48.21, 48.86, 40.42, 52.52, 34.53)) k <- sample(5, 4000, TRUE) p <- geosphere::destPoint(caps[k, ], b = runif(4000, 0, 360), d = 30000 * sqrt(runif(4000))) x <- data.frame(species = sample(c("a", "b"), 4000, TRUE), decimalLongitude = round(p[, 1], 4), decimalLatitude = round(p[, 2], 4)) x[1:20, 2:3] <- 0 x[21:40, 3] <- x[21:40, 2] x[41:50, 2:3] <- round(runif(20, -0.3, 0.3), 4) ``` Run seven tests in both packages. `reference = "CoordinateCleaner"` reads that package's own reference points at run time, so both test against the same points: ```{r both-run, eval = has_cc, message = FALSE, warning = FALSE} cc <- CoordinateCleaner::clean_coordinates( x, lon = "decimalLongitude", lat = "decimalLatitude", species = "species", tests = c("capitals", "centroids", "equal", "gbif", "institutions", "zeros", "duplicates"), value = "spatialvalid", verbose = FALSE) oc <- flag_occurrences(x, tests = c("invalid", "zero", "equal", "capital", "centroid", "institution", "gbif_headquarters", "duplicate"), reference = "CoordinateCleaner") ``` Per test, the number of records each package flags and the share of records with the same verdict: ```{r both-agree, eval = has_cc} pairs <- c(.zer = "zero", .equ = "equal", .cap = "capital", .cen = "centroid", .inst = "institution", .gbf = "gbif_headquarters", .dpl = "duplicate") agree <- vapply(names(pairs), function(cn) mean((!cc[[cn]]) == oc[[paste0("flag_", pairs[[cn]])]], na.rm = TRUE), numeric(1)) knitr::kable(data.frame(CoordinateCleaner = names(pairs), OccurrenceCleaner = pairs, flagged_cc = vapply(names(pairs), function(cn) sum(!cc[[cn]], na.rm = TRUE), 1), flagged_oc = vapply(pairs, function(t) sum(oc[[paste0("flag_", t)]]), 1), agreement = round(agree, 5)), row.names = FALSE) ``` Five of the seven tests give the same verdict on every record. `zero` differs for the 10 records near but not at (0, 0), and `capital` for records at the edge of the buffer; [Where the results differ](#where-the-results-differ) explains both. ### Removing the flagged records CoordinateCleaner's single-test functions return the records that pass by default (`value = "clean"`), so a chain of them removes records as it goes: ```{r cc-pipe, eval = FALSE} library(CoordinateCleaner) clean <- x |> cc_val() |> cc_equ() |> cc_zero() |> cc_cap() |> cc_cen() |> cc_inst() |> cc_gbif() |> cc_sea() ``` Each test here has a cleaning function, `clean_()`, which runs the test and returns the records it does not flag: ```{r oc-pipe, eval = FALSE} clean <- x |> clean_invalid() |> clean_equal() |> clean_zero() |> clean_capital() |> clean_centroid() |> clean_institution() |> clean_gbif_headquarters() |> clean_sea() ``` Both chains run tests in sequence, with each test receiving only the records kept by the previous steps. For the record-level tests shown here, this gives the same result as testing the full input. However, the differences explained in [Where the results differ](#where-the-results-differ) can affect which records are kept. Each cleaning function accepts its test settings as arguments. For example, `clean_capital(x, buffer = 5000)` sets the buffer to 5000 metres. The function's help page, `?clean_capital`, documents the corresponding test. To keep all records and decide later which flags should lead to exclusion, use `flag_occurrences()` first, then `keep_unflagged()`: ```{r oc-flag, eval = FALSE} f <- flag_occurrences(x, tests = c("invalid", "equal", "zero", "capital", "centroid", "institution", "gbif_headquarters", "sea")) clean <- keep_unflagged(f) ``` `keep_unflagged()` keeps the records that no test that ran has flagged. Its `tests` argument sets which flags exclude a record, so a different rule needs no second run. This rule keeps the records near capitals for inspection: ```{r oc-keep, eval = FALSE} near_capital <- keep_unflagged(f, tests = c("invalid", "equal", "zero", "centroid", "institution", "gbif_headquarters", "sea")) ``` A grouped test (`duplicate`, `outlier`, the fossil outlier tests, the dataset tests) compares a record with the other records of its group. In a chain its result depends on which records the earlier steps removed, while `flag_occurrences()` runs it on the full input. ## How agreement is tested The package's test suite runs [CoordinateCleaner](https://ropensci.github.io/CoordinateCleaner/) 3.0.1 and `flag_occurrences()` on sampled records for every test both packages share, and requires identical flags except in the cases described under [Where the results differ](#where-the-results-differ). A second run of the suite uses the development version of CoordinateCleaner from GitHub, so a change there shows up as a failing test. The duplicate test is compared both on a data frame and on a table streamed from disk, where it still looks at the whole table, as CoordinateCleaner does. These tests show agreement on the sampled records. They do not show that either package's flags are right for a given analysis; that depends on the data and the question, as CoordinateCleaner's own tutorial points out. ### Checking agreement on your own records To check agreement on your own data, run both packages on a sample of it as in the chunks above, with the same tests and the same reference data: - `reference = "CoordinateCleaner"`, so both use CoordinateCleaner's reference points. - `land_scale = 50` when the sea test is included, since `clean_coordinates()` uses the 1:50 million land and `flag_occurrences()` defaults to 1:110 million. - The buffers you set in CoordinateCleaner (`capitals_rad`, ...) passed in `buffers`, mapped in [Arguments and output](#arguments-and-output). The outlier test, the fossil outlier tests and the two dataset tests compare a record with the other records of its species, taxon or dataset, so their flags depend on the rest of the input. Compare them on the same input in both packages; adding records of a species can change whether a record of it is an outlier. A disagreement outside the cases described below is a bug in OccurrenceCleaner and can be reported on its [issue tracker](https://github.com/gcol33/OccurrenceCleaner/issues). ## Function mapping CoordinateCleaner has one function per test, `cc_cap()` for capitals, `cc_sea()` for the sea and so on, and three functions that run several tests at once: [`clean_coordinates()`](https://ropensci.github.io/CoordinateCleaner/reference/clean_coordinates.html) and [`clean_fossils()`](https://ropensci.github.io/CoordinateCleaner/reference/clean_fossils.html), which return one column per test, and [`clean_dataset()`](https://ropensci.github.io/CoordinateCleaner/reference/clean_dataset.html), which returns a table per dataset. In OccurrenceCleaner, `flag_occurrences()` runs any selected combination of tests. A test is chosen by name, its buffer is set in `buffers` and its other options in `settings`. The first table gives each test's name in the three places it appears: CoordinateCleaner's test function, the `tests` argument of CoordinateCleaner's combined functions, and the `tests` argument of `flag_occurrences()`. The second table maps the arguments and the forms of output, and the third the functions around the tests. A test's cleaning function, `clean_capital()` for the capital test and so on, removes the flagged records as CoordinateCleaner's test functions do by default; [Removing the flagged records](#removing-the-flagged-records) shows both chains. The packages share no function names, so both can be attached without one hiding the other's functions. ### Tests ```{r mapping-tests, echo = FALSE} code <- function(x) paste0("`", x, "`") knitr::kable(data.frame( `Test function` = c(code(c("cc_val()", "cc_zero()", "cc_equ()", "cc_cap()", "cc_cen()", "cc_inst()", "cc_gbif()", "cc_sea()", "cc_urb()", "cc_coun()", "cc_dupl()", "cc_outl()", "cc_iucn()", "cc_aohi()", "cf_equal()", "cf_age()", "cf_outl()")), '`cf_range(method = "quantile")` or `"mad"`', code(c("cd_ddmm()", "cd_round()"))), `tests in clean_coordinates() and others` = c("always runs; `validity` in `clean_fossils()`", code(c("zeros", "equal", "capitals", "centroids", "institutions", "gbif", "seas", "urban", "countries", "duplicates", "outliers", "range", "aohi", "agesequal")), "not in `clean_fossils()`", code(c("spatiotemp", "temprange")), "`ddmm` in `clean_dataset()`", "`periodicity` in `clean_dataset()`"), `tests in flag_occurrences()` = code(c("invalid", "zero", "equal", "capital", "centroid", "institution", "gbif_headquarters", "sea", "urban", "country_mismatch", "duplicate", "outlier", "range", "aohi", "age_equal", "age_outlier", "spatiotemporal_outlier", "age_range_outlier", "dataset_ddmm", "dataset_rounded")), check.names = FALSE)) ``` `clean_coordinates()` always checks for missing or out-of-range coordinates first, and if any coordinate fails, it stops and returns that result alone. Here that check is the `invalid` test, and the other tests still run on the records that pass it. ### Arguments and output ```{r mapping-args, echo = FALSE} knitr::kable(data.frame( Setting = c("Running several tests", "Buffer around the reference points", "Options of the outlier and dataset tests", "Record counts for the sampling correction of the outlier test", "Your own reference points", "Scale of the land polygons in the sea test", "Columns that define a duplicate", "Limit on the width of a fossil's age range", "Removing the flagged records", "Keeping every record with its flags"), CoordinateCleaner = c("`clean_coordinates()`, `clean_fossils()`, `clean_dataset()`", "`capitals_rad`, `centroids_rad`, `inst_rad`, ...", "`method`, `mltpl`, `size_thresh`, `replicates`, ...", "`cc_outl(sampling_thresh = ...)`, counts queried from GBIF", "`ref` of each test function; `capitals_ref`, `seas_ref`, ... in `clean_coordinates()`", "`seas_scale` (default 50)", "`cc_dupl(additions = ...)`", '`cf_range(method = "time", max_range = ...)`', '`value = "clean"`', '`value = "flagged"` or `"spatialvalid"`'), OccurrenceCleaner = c("`flag_occurrences(x, tests = ...)`", "`buffers = list(capital = ..., centroid = ..., institution = ...)`", "`settings = list(outlier = list(method = ...), ...)`", "the same option in `settings`, with the counts given as a table in `gazetteers = list(sampling = ...)`", "`gazetteers = list(capital = ..., sea = ...)`", "`land_scale` (default 110, as `cc_sea()`)", "`duplicate_by`", "no test; `tally_flags(bins = ...)` on the range width", "`clean_capital()` and the other cleaning functions, or `keep_unflagged(f, tests = rule)` after flagging", "the flag columns; `as_spatialvalid(f)` for the `spatialvalid` layout")), row.names = FALSE) ``` ### Functions around the tests The functions of CoordinateCleaner 3.0.1 besides the tests correspond to these: ```{r not-ported, echo = FALSE} knitr::kable(data.frame( CoordinateCleaner = c("cc_outl(sampling_thresh = ...)", "write_pyrate()", "plot()", "summary()", "is.spatialvalid()"), `what it does` = c("keeps outliers only in well-sampled countries, with record counts queried online", "writes fossil occurrences as PyRate input files", "maps flagged records", "counts flagged records per test", "checks the class of a clean_coordinates() result"), OccurrenceCleaner = c("the same rule, with the record counts given as a table (gazetteers = list(sampling = ...))", "export_pyrate(), the same file layout", "plot(f, type = \"map\")", "summary(f)", "the occ_flags class"), check.names = FALSE), row.names = FALSE) ``` CoordinateCleaner's [outlier test](https://ropensci.github.io/CoordinateCleaner/reference/cc_outl.html) has an optional sampling correction, which keeps outliers flagged only in well-sampled countries. With the correction on, it downloads the record count of each country from GBIF at run time, so the same call can return different results as GBIF grows. OccurrenceCleaner takes the counts as a table and writes the table's checksum to the reference label. The PyRate export is called `export_pyrate()`. CoordinateCleaner's is [`write_pyrate()`](https://ropensci.github.io/CoordinateCleaner/reference/write_pyrate.html), and with different names both stay usable when the two packages are attached. Code written for CoordinateCleaner's output runs on a converted result, made with `as_spatialvalid()`. The converted result has the record columns, one column per test, the summary column and CoordinateCleaner's class. The test columns carry CoordinateCleaner's names and its sign, `TRUE` for a pass. CoordinateCleaner's summary and plot methods run on the result unchanged, and so do filters on the summary column. For the validity, equal and duplicate tests, the package tests compare each column of a converted result with CoordinateCleaner's own output. ```{r spatialvalid, eval = has_cc} sv <- as_spatialvalid(oc) head(sv[, c(".zer", ".equ", ".cap", ".dpl", ".summary")]) summary(sv) ``` ## Where the results differ Most differences reflect design choices in OccurrenceCleaner: how buffers are measured, how invalid coordinates and random draws are handled, and which reference data are used by default. Four other differences arise from reported issues in CoordinateCleaner 3.0.1 ([#114](https://github.com/ropensci/CoordinateCleaner/issues/114), [#99](https://github.com/ropensci/CoordinateCleaner/issues/99), [#110](https://github.com/ropensci/CoordinateCleaner/issues/110), [#66](https://github.com/ropensci/CoordinateCleaner/issues/66)), where tests either behave differently from their documentation or fail with an error. These issues may be resolved in later versions. ### Design differences #### Buffer circles The packages use different methods to test whether a record falls within a buffer: - CoordinateCleaner approximates the buffer with a polygon. - OccurrenceCleaner calculates the distance to the reference location directly. Because the polygon's edges lie slightly inside the circle, records near the boundary can give different results. In this example, two records are placed 9,995 metres from the same reference point in Kabul, just inside a 10,000-metre buffer. One is aligned with a polygon vertex, while the other lies halfway between two vertices. Both are within the distance limit, but the polygon may exclude the second record. ```{r band, eval = has_cc, message = FALSE, warning = FALSE} e <- new.env() utils::data("countryref", package = "CoordinateCleaner", envir = e) kabul <- unlist( e$countryref[ e$countryref$iso3 == "AFG", c("capital.lon", "capital.lat") ][1, ] ) p <- geosphere::destPoint(kabul, b = c(0, 2.5), d = 9995) y <- data.frame( species = "a", decimalLongitude = p[, 1], decimalLatitude = p[, 2] ) data.frame( bearing = c(0, 2.5), distance_m = 9995, cc_flagged = !CoordinateCleaner::cc_cap( y, value = "flagged", verbose = FALSE ), oc_flagged = flag_occurrences( y, tests = "capital", reference = "CoordinateCleaner" )$flag_capital ) ``` Both packages flag the first record. Only OccurrenceCleaner flags the second, which falls just outside CoordinateCleaner's polygon despite being within the 10,000-metre buffer. Because both packages use the same reference point and buffer radius, the difference comes entirely from how they define the boundary. #### Records without valid coordinates A record with a missing or out-of-range coordinate is treated differently by each package. In CoordinateCleaner the outcome depends on the test: the zero and equal tests return `NA` for it, the capital test treats it as clean, and the duplicate test flags the second of two such records. OccurrenceCleaner gives it the `invalid` flag and skips the other spatial tests. Including `invalid` in a cleaning rule removes these records; leaving it out keeps them, with no spatial flag. #### Random age draws CoordinateCleaner's fossil tests draw one age uniformly from each record's age range, run the test five times and keep the majority vote. The draws come from the session's random stream in input order, so the flags change with the seed and with the row order. OccurrenceCleaner follows the same procedure but seeds a stream per taxon and orders the draws by record content, so the same records give the same flags whatever their order and however they are split across files. The seed is a setting of the age outlier test (`seed` under `age_outlier` in `settings`). When every age range has zero width nothing is drawn and the two packages give identical flags. When ages have to be drawn, the flags of a single run can differ record by record between the packages, even though the procedure is the same; the test suite checks only the zero-width case. #### Records without a dataset key Records without a dataset key are left unflagged by both dataset tests here; CoordinateCleaner's `cd_ddmm()` flags them. #### Country codes CoordinateCleaner compares the record's country code with the ISO alpha-3 code of the polygon the record falls in. GBIF stores country codes as alpha-2, so a GBIF table has to be converted before the test runs. OccurrenceCleaner accepts alpha-2, alpha-3 and Natural Earth's own three-letter code, so no conversion is needed. For records whose code is alpha-3 and whose country is in Natural Earth, the two tests agree, apart from the three countries described under [Records in France, Norway and Kosovo](#records-in-france-norway-and-kosovo). #### Gazetteers By default the gazetteers are rebuilt from the sources CoordinateCleaner's tables were originally derived from. They hold the same kinds of points but not the same coordinates; the [Reference data](reference-data.html) article lists the sources and measures the differences. Points from sources CoordinateCleaner does not use, such as Wikidata institutions and Natural Earth capitals, are marked as extensions and included by default; [Extension points](reference-data.html#extension-points) shows how to leave them out. Setting `reference = "CoordinateCleaner"` reads CoordinateCleaner's own tables from your installation instead, and then reproduces its flags up to the differences listed on this page. ### Behaviour of CoordinateCleaner 3.0.1 #### The zero buffer Both packages flag records whose longitude or latitude is exactly zero, and both also flag a buffer around (0, 0). The buffer differs. CoordinateCleaner 3.0.1 applies a radius of 0.5 metres, while [its documentation](https://ropensci.github.io/CoordinateCleaner/reference/cc_zero.html) specifies 0.5 degrees; OccurrenceCleaner uses the documented 0.5 degrees, so it flags a larger area around the origin. Setting the buffer to zero (`buffers = list(zero = 0)`) turns the buffer off and keeps only the exact-zero check. ```{r zero-diff, eval = has_cc, message = FALSE, warning = FALSE} z <- data.frame(species = "a", decimalLongitude = c(0, 0.3, 0.001), decimalLatitude = c(12, 0.3, 0.001)) data.frame(z[, 2:3], cc_flagged = !CoordinateCleaner::cc_zero(z, value = "flagged", verbose = FALSE), oc_flagged = flag_occurrences(z, tests = "zero")$flag_zero, oc_zero_buffer_0 = flag_occurrences(z, tests = "zero", buffers = list(zero = 0))$flag_zero) ``` #### Records in France, Norway and Kosovo CoordinateCleaner 3.0.1 flags every record in France, Norway and Kosovo as a country mismatch. Natural Earth gives these three countries no ISO code in the column CoordinateCleaner reads (the value is `-99`), so no record can match. OccurrenceCleaner reads Natural Earth's alternative code columns (`ISO_A2_EH`, `ISO_A3_EH`), which are filled for all three. #### Outliers of large species For species of 10,000 records or more, and for every species when `thinning = TRUE`, CoordinateCleaner's outlier test switches to a raster approximation: it counts records per cell of a 0.5-degree grid, computes the distance statistics between cells, and selects the outlier cells. In CoordinateCleaner 3.0.1 this path flags no records. Twenty records 1,000 km from the other 11,980 of their species show it: ```{r raster, eval = has_cc, message = FALSE, warning = FALSE} set.seed(1) n <- 12000 r <- data.frame(species = "a", decimalLongitude = c(runif(n - 20, 10, 20), runif(20, 120, 130)), decimalLatitude = c(runif(n - 20, 40, 50), runif(20, -30, -20))) c(cc_flagged = sum(!CoordinateCleaner::cc_outl(r, value = "flagged", verbose = FALSE)), oc_flagged = sum(flag_occurrences(r, tests = "outlier")$flag_outlier)) ``` OccurrenceCleaner's outlier test uses the same grid, weights and statistics and flags the records of the outlier cells. Below 10,000 records, where CoordinateCleaner computes exact distances, the two give identical flags. #### Small datasets CoordinateCleaner's [`cd_round()`](https://ropensci.github.io/CoordinateCleaner/reference/cd_round.html) skips datasets with fewer distinct coordinates than its minimum, but in version 3.0.1 an input that contains such a dataset next to larger ones stops with an error, so no dataset is tested. OccurrenceCleaner leaves the small dataset untested and tests the others. ## Timings The timings measure how long each package takes to run the tests both share, on synthetic records on one machine. They do not measure agreement, which the test suite above covers, or how either package performs on real occurrence data with its clustering and duplicates. The tables give the elapsed time in seconds of one call per test, on one million and on ten million records; lower values mean faster runs. In both packages the call used CoordinateCleaner's reference points. The records were placed at random, a tenth of them within 30 km of a capital. Each time is the fastest of three runs. A CoordinateCleaner test whose single run took longer than 200 s was run once, which applies to the capital and centroid tests at ten million records. Each timing covers the whole call, including reading the reference points; the land polygons of both packages were loaded before timing started. The timer resolves 0.01 s. The machine was an AMD Ryzen 7 5700G with 64 GB of memory, running Windows 11 and R 4.6.1, with one process, OccurrenceCleaner 0.2.0 and CoordinateCleaner 3.0.1. ### One million records | Test | OccurrenceCleaner (s) | CoordinateCleaner (s) | |:---|---:|---:| | `capital` | 0.14 | 24.64 | | `centroid` | 0.16 | 33.80 | | `institution` | 0.15 | 0.92 | | `gbif_headquarters` | 0.08 | 0.58 | | `sea` | 0.06 | 5.62 | | `zero` | 0.05 | 0.59 | | `equal` | 0.03 | < 0.01 | | `invalid` | 0.02 | 4.72 | | `duplicate` | 1.12 | 1.17 | ### Ten million records | Test | OccurrenceCleaner (s) | CoordinateCleaner (s) | |:---|---:|---:| | `capital` | 1.44 | 238.68 | | `centroid` | 1.63 | 245.39 | | `institution` | 1.68 | 7.38 | | `gbif_headquarters` | 0.80 | 6.35 | | `sea` | 0.66 | 58.43 | | `zero` | 0.47 | 6.50 | | `equal` | 0.37 | 0.06 | | `invalid` | 0.31 | 43.33 | | `duplicate` | 12.07 | 11.15 | The largest differences are in the capital and centroid tests: at ten million records they take 1.44 s and 1.63 s here, and 238.68 s and 245.39 s in CoordinateCleaner. CoordinateCleaner is faster on the `equal` test (0.06 s against 0.37 s) and on the `duplicate` test (11.15 s against 12.07 s). For the proximity tests, CoordinateCleaner tests the records against a buffer polygon around the reference points, and this package looks up each record's nearest reference point in a kd-tree. ### The shared tests in one call With its own reference data, `flag_occurrences()` runs the nine CoordinateCleaner tests that need only coordinates and a species (`invalid`, `zero`, `equal`, `capital`, `centroid`, `institution`, `gbif_headquarters`, `sea` and `duplicate`) in one call in 1.91 s on one million records and 19.45 s on ten million. The benchmark times this call for OccurrenceCleaner only. [Large tables on disk](on-disk.html) shows how a table on disk is read in chunks. The benchmark script is `bench/benchmark.R` in the source repository, and its results are in `bench/results/`. ## When is CoordinateCleaner the better choice? CoordinateCleaner has been in use since its first CRAN release in 2017, went through rOpenSci review, and its tests have been applied and cited in published work. OccurrenceCleaner's agreement with it rests on the test suite described above, on the records that suite samples. CoordinateCleaner may be the better choice when: - Your data fits in memory and runs through CoordinateCleaner in an acceptable time, and your workflow and its methods section are already built on it. - You want `cc_outl(sampling_thresh)` to query GBIF's current record counts at run time rather than supply them as a table. - Your cleaning uses only `equal` and `duplicate`, where CoordinateCleaner is as fast or faster in the timings above. ## References Zizka A, Silvestro D, Andermann T, Azevedo J, Duarte Ritter C, Edler D, Farooq H, Herdean A, Ariza M, Scharn R, Svanteson S, Wengström N, Zizka V, Antonelli A (2019). CoordinateCleaner: standardized cleaning of occurrence records from biological collection databases. *Methods in Ecology and Evolution* 10:744-751. [doi:10.1111/2041-210X.13152](https://doi.org/10.1111/2041-210X.13152)