OccurrenceCleaner extends the record-cleaning tests developed in CoordinateCleaner (Zizka et al. 2019) 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.
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.
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:
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:
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)| CoordinateCleaner | OccurrenceCleaner | flagged_cc | flagged_oc | agreement |
|---|---|---|---|---|
| .zer | zero | 20 | 30 | 0.99750 |
| .equ | equal | 40 | 40 | 1.00000 |
| .cap | capital | 435 | 436 | 0.99975 |
| .cen | centroid | 29 | 29 | 1.00000 |
| .inst | institution | 0 | 0 | 1.00000 |
| .gbf | gbif_headquarters | 0 | 0 | 1.00000 |
| .dpl | duplicate | 18 | 18 | 1.00000 |
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 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:
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_<test>(), which runs the test and returns the
records it does not flag:
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 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():
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:
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
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. 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 = 50when the sea test is included, sinceclean_coordinates()uses the 1:50 million land andflag_occurrences()defaults to 1:110 million.The buffers you set in CoordinateCleaner (
capitals_rad, …) passed inbuffers, mapped in 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.
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()
and clean_fossils(),
which return one column per test, and clean_dataset(),
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
shows both chains.
The packages share no function names, so both can be attached without one hiding the other’s functions.
Tests
| Test function | tests in clean_coordinates() and others | tests in flag_occurrences() |
|---|---|---|
cc_val() |
always runs; validity in
clean_fossils()
|
invalid |
cc_zero() |
zeros |
zero |
cc_equ() |
equal |
equal |
cc_cap() |
capitals |
capital |
cc_cen() |
centroids |
centroid |
cc_inst() |
institutions |
institution |
cc_gbif() |
gbif |
gbif_headquarters |
cc_sea() |
seas |
sea |
cc_urb() |
urban |
urban |
cc_coun() |
countries |
country_mismatch |
cc_dupl() |
duplicates |
duplicate |
cc_outl() |
outliers |
outlier |
cc_iucn() |
range |
range |
cc_aohi() |
aohi |
aohi |
cf_equal() |
agesequal |
age_equal |
cf_age() |
not in clean_fossils()
|
age_outlier |
cf_outl() |
spatiotemp |
spatiotemporal_outlier |
cf_range(method = "quantile") or
"mad"
|
temprange |
age_range_outlier |
cd_ddmm() |
ddmm in clean_dataset()
|
dataset_ddmm |
cd_round() |
periodicity in
clean_dataset()
|
dataset_rounded |
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
| Setting | CoordinateCleaner | OccurrenceCleaner |
|---|---|---|
| Running several tests |
clean_coordinates(),
clean_fossils(), clean_dataset()
|
flag_occurrences(x, tests = ...) |
| Buffer around the reference points |
capitals_rad, centroids_rad,
inst_rad, … |
buffers = list(capital = ..., centroid = ..., institution = ...) |
| Options of the outlier and dataset tests |
method, mltpl,
size_thresh, replicates, … |
settings = list(outlier = list(method = ...), ...) |
| Record counts for the sampling correction of the outlier test |
cc_outl(sampling_thresh = ...), counts
queried from GBIF |
the same option in settings, with the
counts given as a table in
gazetteers = list(sampling = ...)
|
| Your own reference points |
ref of each test function;
capitals_ref, seas_ref, … in
clean_coordinates()
|
gazetteers = list(capital = ..., sea = ...) |
| Scale of the land polygons in the sea test |
seas_scale (default 50) |
land_scale (default 110, as
cc_sea()) |
| Columns that define a duplicate | cc_dupl(additions = ...) |
duplicate_by |
| Limit on the width of a fossil’s age range | cf_range(method = "time", max_range = ...) |
no test; tally_flags(bins = ...) on the
range width |
| Removing the flagged records | value = "clean" |
clean_capital() and the other cleaning
functions, or keep_unflagged(f, tests = rule) after
flagging |
| Keeping every record with its flags |
value = "flagged" or
"spatialvalid"
|
the flag columns; as_spatialvalid(f) for
the spatialvalid layout |
Functions around the tests
The functions of CoordinateCleaner 3.0.1 besides the tests correspond to these:
| CoordinateCleaner | what it does | OccurrenceCleaner |
|---|---|---|
| cc_outl(sampling_thresh = …) | keeps outliers only in well-sampled countries, with record counts queried online | the same rule, with the record counts given as a table (gazetteers = list(sampling = …)) |
| write_pyrate() | writes fossil occurrences as PyRate input files | export_pyrate(), the same file layout |
| plot( |
maps flagged records | plot(f, type = “map”) |
| summary( |
counts flagged records per test | summary(f) |
| is.spatialvalid() | checks the class of a clean_coordinates() result | the occ_flags class |
CoordinateCleaner’s outlier test 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(),
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.
sv <- as_spatialvalid(oc)
head(sv[, c(".zer", ".equ", ".cap", ".dpl", ".summary")])
#> .zer .equ .cap .dpl .summary
#> 1 FALSE FALSE TRUE TRUE FALSE
#> 2 FALSE FALSE TRUE TRUE FALSE
#> 3 FALSE FALSE TRUE FALSE FALSE
#> 4 FALSE FALSE TRUE FALSE FALSE
#> 5 FALSE FALSE TRUE FALSE FALSE
#> 6 FALSE FALSE TRUE FALSE FALSE
summary(sv)
#> .val .zer .equ .cap .cen .inst .gbf .dpl
#> 0 30 40 436 29 0 0 18
#> .summary
#> 491Where 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, #99, #110, #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.
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
)
#> bearing distance_m cc_flagged oc_flagged
#> 1 0.0 9995 TRUE TRUE
#> 2 2.5 9995 FALSE TRUEBoth 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.
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 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 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 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.
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)
#> decimalLongitude decimalLatitude cc_flagged oc_flagged oc_zero_buffer_0
#> 1 0.000 12.000 TRUE TRUE TRUE
#> 2 0.300 0.300 FALSE TRUE FALSE
#> 3 0.001 0.001 FALSE TRUE FALSERecords 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:
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))
#> cc_flagged oc_flagged
#> 0 20OccurrenceCleaner’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()
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 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
equalandduplicate, 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