knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.width = 7,
fig.height = 5,
purl = FALSE
)
ext_file <- function(...) {
path <- system.file("extdata", ..., package = "gbif.range")
if (nzchar(path)) {
return(path)
}
normalizePath(file.path("..", "inst", "extdata", ...), mustWork = TRUE)
}
library(gbif.range)This vignette focuses on the core ecological idea behind
gbif.range: species ranges are inferred from occurrences,
but those occurrences are constrained by ecoregions rather than expanded
across the landscape as a purely geometric hull.
The two longer examples cover a broad terrestrial workflow using
Panthera tigris and a finer regional workflow using
Arctostaphylos alpinus in the European Alps with custom
ecoregions derived from environmental rasters.
The relevant functions are:
get_range() for the actual range inference,read_ecoreg() and ecoreg_list for packaged
large-scale ecoregions,make_ecoreg() for custom ecoregions derived from any
set of spatial raster layers,cv_range() and evaluate_range() for
evaluating the resulting maps.get_range() worksget_range() is designed for one focal species at a time.
The algorithm originates from Hagen et al. (2019) and can be summarized
as four linked operations:
This means that the final output reflects both the geometry of the occurrences and the eco-geographic structure of the study area.
The example below is deliberately small and fully offline. Two
synthetic rasters are used to create a custom ecoregion layer, then a
short occurrence table is passed to get_range().
set.seed(1)
# Create two simple environmental surfaces on a small study area.
r1 <- terra::rast(ncols = 20, nrows = 20, xmin = 0, xmax = 10, ymin = 0, ymax = 10)
terra::values(r1) <- rep(seq(0, 1, length.out = 20), each = 20)
r2 <- terra::rast(r1)
terra::values(r2) <- rep(seq(0, 1, length.out = 20), times = 20)
env <- c(r1, r2)
# Derive four custom ecoregions from the environmental layers.
eco <- make_ecoreg(env = env, nclass = 4)
#> CLARA algorithm processing...
#> Generating polygons...
# Define one species as an occurrence table in the format expected by get_range().
occ <- data.frame(
input_search = "example_species",
decimalLongitude = c(0.5, 1.2, 2.4, 3.6, 2.8, 6.0, 7.2, 7.4, 7.6, 8.8),
decimalLatitude = c(1.0, 0.1, 2.3, 2.5, 2.7, 5.0, 7.1, 7.3, 6.5, 7.7)
)
# Build the range from the occurrences and the ecoregion layer.
range_obj <- get_range(
occ_coord = occ,
ecoreg = eco,
ecoreg_name = "EcoRegion",
verbose = FALSE
)
# Plot
terra::plot(
range_obj$rangeOutput,
col = "#3c8d5a",
main = "A minimal ecoregion-constrained range"
)
graphics::points(occ$decimalLongitude, occ$decimalLatitude, pch = 4)Three practical points are worth noting here.
First, get_range() accepts any set of occurrence
coordinates as input via occ_coord — not just a
getGBIF object returned by get_gbif(). Any
data.frame with decimalLongitude,
decimalLatitude columns are valid. This means museum
records, citizen science exports, expert-curated datasets, or
coordinates from any other source can be used directly, as long as they
follow that column structure.
Second, custom ecoregions created with make_ecoreg() can
be passed directly into get_range(). This is useful for
regional studies where global ecoregions are too coarse.
Third, get_range() returns a getRange
object. The spatial output is stored in
range_obj$rangeOutput, while the original inputs are kept
in range_obj$init.args.
Large-scale analyses usually rely on one of the packaged ecoregion layers: terrestrial (eco_terra; Olson et al. 2001; The Nature Conservancy 2009), marine (eco_marine, eco_hd_marine; Spalding et al. 2007, 2012; The Nature Conservancy 2012), and freshwater (eco_fresh; Abell et al. 2008).
vapply(ecoreg_list, `[[`, character(1), "filename")
#> [1] "eco_terra" "eco_fresh" "eco_marine" "eco_hd_marine"The public pattern for a terrestrial analysis is:
# Read ecoregion
eco_terra <- read_ecoreg("eco_terra", save_dir = tempdir())
# Construct range (not run)
range <- get_range(
occ_coord = obs,
ecoreg = eco_terra,
ecoreg_name = "ECO_NAME"
)Beyond the packaged layers, any polygon object can serve as the
ecoreg argument — a SpatVector,
sf, or other spatial polygon class — provided it contains a
character column whose name is passed to ecoreg_name.
Habitat maps, expert-defined biogeographic units, and bioregions derived
from species composition data (Denelle et al. 2025) therefore need no
conversion step.
When no suitable polygon layer exists, make_ecoreg()
builds one by clustering raster data (Chauvier et al. 2021). Climate
layers are the most common input, but any spatially structured raster
works — species richness surfaces, biodiversity indices, habitat
suitability maps, or any combination of drivers thought to define
meaningful spatial units. This is typically the route for regional
analyses, where the packaged global layers are too coarse. The package
ships raster examples under inst/extdata, including
rst.tif:
# Construct ecoregion
rst <- terra::rast(ext_file("rst.tif"))
my_eco <- make_ecoreg(env = rst, nclass = 200)
# Construct range (not run)
range <- get_range(
occ_coord = obs,
ecoreg = my_eco,
ecoreg_name = "EcoRegion",
res = 0.05
)The key trade-off is ecological detail versus robustness to sampling density. Coarser ecoregions produce broader, smoother ranges. Finer ecoregions can better capture regional structure, but they also make the results more sensitive to local sampling gaps.
Tiger occurrences are a good example of the broad terrestrial use
case for gbif.range. The workflow is straightforward:
retrieve occurrences with a coarse precision filter, load the
terrestrial ecoregions of the world, and infer the range inside those
ecoregional boundaries.
# Step 1: download global tiger occurrences with a 100 km precision filter
obs_tiger <- get_gbif(
sp_name = "Panthera tigris",
grain = 100
)
# Step 2: load the packaged terrestrial ecoregion layer
eco_terra <- read_ecoreg("eco_terra", save_dir = tempdir())
# Step 3: infer the range with default parameters
range_tiger <- get_range(
occ_coord = obs_tiger,
ecoreg = eco_terra,
ecoreg_name = "ECO_NAME",
degrees_outlier = 5,
clust_pts_outlier = 4,
res = 0.1
)
# Plot
terra::plot(range_tiger$rangeOutput, col = "#238b45")
graphics::points(obs_tiger[, c("decimalLongitude", "decimalLatitude")], pch = 20)This is the typical continental to global pattern. The ecoregion
layer is externally defined and ecologically interpretable, while the
occurrence filter is intentionally conservative. In practice, this is
the kind of workflow where gbif.range is most useful as a
transparent alternative to unconstrained geometric envelopes.
The second workflow is deliberately different. Here the issue is not worldwide coverage, but the opposite: the global terrestrial ecoregions are too coarse for a mountain system with strong local climatic structure. A custom ecoregion layer is therefore built from packaged example CHELSA bioclimatic rasters (Karger et al. 2017).
# Step 1: define the Alps study region and retrieve occurrences.
alps_extent <- terra::vect(ext_file("shp_lonlat.shp"))
obs_arcto <- get_gbif(
sp_name = "Arctostaphylos alpinus",
geo = alps_extent,
grain = 1
)
# Step 2: create finer ecoregions from the packaged environmental rasters.
rst <- terra::rast(ext_file("rst.tif"))
eco_alps <- make_ecoreg(env = rst, nclass = 200)
# Step 3: infer the range at finer output resolution (~5x5-km resolution)
range_arcto <- get_range(
occ_coord = obs_arcto,
ecoreg = eco_alps,
ecoreg_name = "EcoRegion",
res = 0.05,
format = "SpatRaster"
)
# Plot
countries <- terra::vect(ext_file("world_countries.shp"))
terra::plot(alps_extent, col="#00000030", border = NULL)
plot(countries, col = "#cacee8", add = TRUE)
terra::plot(alps_extent, col="#00000020", border = NULL, add = TRUE)
terra::plot(range_arcto$rangeOutput, col = "#3c8d5a", add = TRUE)
graphics::points(obs_arcto[, c("decimalLongitude", "decimalLatitude")], pch = 20)This regional example shows why make_ecoreg() is part of
the main package workflow rather than an auxiliary convenience. At small
spatial extents, the ecological realism of the range map often depends
more on the choice of ecoregion layer than on minor changes in buffer
parameters.
The most important get_range() arguments fall into three
groups:
| Group | Arguments | Role |
|---|---|---|
| Outlier removal | degrees_outlier, clust_pts_outlier |
How aggressively isolated occurrences are discarded. Matters most when the input contains obvious anomalies or disjunct stray clusters of observations. |
| Buffering | buff_width_point, buff_incrmt_pts_line,
buff_width_polygon |
How singletons, linear clusters, and polygon hulls are buffered before the final ecoregion intersection. |
| Output | format, res |
Geometry type and, for rasters, resolution. Vector output suits exploratory work; gridded output is easier to stack across many species. |
Which group needs attention depends on scale.
At broad scale the defaults are usually a reasonable starting point, since the main goal is simply to remove very isolated records and obvious anomalies.
At regional scale the decisive settings often lie outside
get_range() altogether: the coordinate precision filter
(grain) in get_gbif() and the granularity
(nclass) of the make_ecoreg() layer. Species
with narrow, habitat-constrained distributions are the exception —
there, tightening the outlier and buffer parameters themselves can be
necessary, as shown below.
Paederota bonarota is a good example: restricted to
limestone/dolomite rock crevices, its Alpine distribution is essentially
limited to the Slovenian, Italian, and Austrian sectors regardless of
broader climatic suitability. Ideally this habitat constraint would be
encoded directly in the custom ecoregions — e.g. by adding a bedrock or
geology layer to make_ecoreg(). Since that is not done here
(eco_alps uses only climate), the outlier and buffer
parameters are tightened instead, as a geographic substitute for the
missing environmental constraint:
# Download Paederota bonarota within the same Alps study region
obs_paed <- get_gbif(
sp_name = "Paederota bonarota",
geo = alps_extent,
grain = 1
)
# Build the range with tightened outlier and buffer parameters, reflecting
# the species' narrow, rock-habitat-restricted distribution
range_paed <- get_range(
occ_coord = obs_paed,
ecoreg = eco_alps,
ecoreg_name = "EcoRegion",
res = 0.05,
degrees_outlier = 0.5,
buff_width_point = 0.5,
buff_incrmt_pts_line = 0.5,
buff_width_polygon = 0.5,
format = "sf"
)
# Plot
countries <- terra::vect(ext_file("world_countries.shp"))
terra::plot(alps_extent, col="#00000030", border = NULL)
plot(countries, col = "#cacee8", add = TRUE)
terra::plot(alps_extent, col="#00000020", border = NULL, add = TRUE)
terra::plot(merge_range(range_paed), col = "#3c8d5a", add = TRUE)
graphics::points(obs_paed[, c("decimalLongitude", "decimalLatitude")], pch = 20)The package provides two complementary ways to evaluate range maps.
cv_range() performs internal cross-validation of a
getRange object by repeatedly rebuilding the map from
subsets of the original occurrences:
evaluate_range() compares saved range outputs with
external validation layers. The package includes a small validation
example under inst/extdata:
# Set root
root_dir <- ext_file()
# Evaluate
res_eval <- evaluate_range(
root_dir = root_dir,
valData_dir = "SDM",
ecoRM_dir = "EcoRM",
print_map = FALSE,
verbose = FALSE,
valData_type = "TIFF"
)
# Summary
head(res_eval$df_eval)Together, cv_range() and evaluate_range()
let you tune parameter choices on well-documented taxa before applying
the same workflow to data-poor species.
The same logic can be used with the broad and regional examples above. For example, a block cross-validation of the tiger workflow is:
That kind of evaluation is particularly useful when deciding how strongly to filter outliers, how fine the ecoregion layer should be, or whether a raster output resolution is appropriate for later biodiversity summaries.
get_range() is best understood as an ecological
range-construction tool rather than a pure hull algorithm. The strength
of the approach lies in combining occurrence geometry with
eco-geographic structure. The rest of the package, including GBIF
retrieval and large disk-based workflows, is built to feed that core
mapping step in a transparent and reproducible way.
Abell, R., Thieme, M. L., Revenga, C., Bryer, M., Kottelat, M., Bogutskaya, N., … Petry, P. (2008). Freshwater ecoregions of the world: a new map of biogeographic units for freshwater biodiversity conservation. BioScience, 58(5), 403–414. https://doi.org/10.1641/B580507
Chauvier, Y., Zimmermann, N. E., Poggiato, G., Bystrova, D., Brun, P., & Thuiller, W. (2021). Novel methods to correct for observer and sampling bias in presence-only species distribution models. Global Ecology and Biogeography, 30(11), 2312–2325. https://doi.org/10.1111/geb.13383
Denelle, P., Leroy, B., & Lenormand, M. (2025). Bioregionalization analyses with the bioregion R package. Methods in Ecology and Evolution, 16, 496–506. https://doi.org/10.1111/2041-210X.14496
Hagen, O., Vaterlaus, L., Albouy, C., Brown, A., Leugger, F., Onstein, R. E., Novaes de Santana, C., Scotese, C. R., & Pellissier, L. (2019). Mountain building, climate cooling and the richness of cold-adapted plants in the Northern Hemisphere. Journal of Biogeography, 46(8), 1792–1807. https://doi.org/10.1111/jbi.13653
Karger, D. N., Conrad, O., Böhner, J., Kawohl, T., Kreft, H., Soria-Auza, R. W., Zimmermann, N. E., Linder, H. P., & Kessler, M. (2017). Climatologies at high resolution for the earth’s land surface areas. Scientific Data, 4, 170122. https://doi.org/10.1038/sdata.2017.122
Olson, D. M., Dinerstein, E., Wikramanayake, E. D., Burgess, N. D., Powell, G. V. N., Underwood, E. C., … Kassem, K. R. (2001). Terrestrial ecoregions of the world: a new map of life on Earth. BioScience, 51(11), 933–938. https://doi.org/10.1641/0006-3568(2001)051[0933:TEOTWA]2.0.CO;2
Spalding, M. D., Fox, H. E., Allen, G. R., Davidson, N., Ferdaña, Z. A., Finlayson, M., … Robertson, J. (2007). Marine ecoregions of the world: a bioregionalization of coastal and shelf areas. BioScience, 57(7), 573–583. https://doi.org/10.1641/B570707
Spalding, M. D., Agostini, V. N., Rice, J., & Grant, S. M. (2012). Pelagic provinces of the world: a biogeographic classification of the world’s surface pelagic waters. Ocean & Coastal Management, 60, 19–30. https://doi.org/10.1016/j.ocecoaman.2011.12.016
The Nature Conservancy (2009). Global Ecoregions, Major Habitat Types, Biogeographical Realms and The Nature Conservancy Terrestrial Assessment Units. Cambridge (UK): The Nature Conservancy. https://geospatial.tnc.org/datasets/b1636d640ede4d6ca8f5e369f2dc368b/about
The Nature Conservancy (2012). Marine Ecoregions and Pelagic Provinces of the World. Cambridge (UK): The Nature Conservancy. https://habitats.oceanplus.org