xbioclim vs terra: Side-by-Side Comparison
Source:vignettes/terra-comparison.Rmd
terra-comparison.RmdOverview
The terra package
includes terra::bioclim() which computes bioclimatic
variables from a SpatRaster stack.
xbioclim fills a complementary niche: it works directly
with plain R numeric vectors (and, by extension, matrices and data
frames), without requiring any spatial infrastructure.
This vignette walks through the key similarities and differences between the two packages so you can choose the right tool—or combine both—for your workflow.
Shared vocabulary
Both packages follow the same WorldClim variable definitions (BIO01–BIO19) and the same underlying formulas. If you already know how to interpret the variables from one package, the numbers produced by the other should be directly comparable.
Input format
| Feature | xbioclim | terra |
|---|---|---|
| Input type | Plain numeric vector (length 12) |
SpatRaster with 12 monthly layers |
| Spatial awareness | None – pure numeric | Full CRS + extent handling |
| Multi-pixel | Row-wise apply() over a matrix |
Operates natively on rasters |
| Installation | Lightweight (no system libs needed) | Requires GDAL/PROJ |
xbioclim: vector inputs
# Monthly climate normals for a single location
tas <- c(12.0, 13.3, 15.8, 17.0, 17.5, 16.5,
15.6, 15.6, 15.2, 14.5, 12.9, 12.0)
tasmax <- c(21.2, 23.0, 25.7, 26.6, 26.3, 24.4,
22.8, 22.9, 22.3, 21.7, 20.5, 20.4)
tasmin <- c( 3.5, 4.5, 6.9, 9.4, 10.8, 11.4,
10.8, 10.7, 9.9, 8.2, 5.9, 4.1)
pr <- c(10, 7, 11, 21, 52, 132,
163, 152, 116, 62, 16, 8)
# All 19 variables in one call
result_rx <- bioclim(tas, tasmax, tasmin, pr)
result_rx
#> bio01 bio02 bio03 bio04 bio05 bio06 bio07 bio08
#> 14.82500 15.14167 65.54834 180.46814 26.60000 3.50000 23.10000 15.90000
#> bio09 bio10 bio11 bio12 bio13 bio14 bio15 bio16
#> 12.43333 17.00000 12.30000 750.00000 163.00000 7.00000 93.81627 447.00000
#> bio17 bio18 bio19
#> 25.00000 205.00000 34.00000terra: raster inputs (conceptual)
The equivalent terra workflow requires a SpatRaster
where each layer holds one month of data. Below is a minimal
reproducible sketch—no real raster files are read from disk:
library(terra)
# Assume `r_tas`, `r_tasmax`, `r_tasmin`, `r_pr` are each SpatRasters
# with 12 layers (one per month), all sharing the same CRS and extent.
# terra::bioclim() accepts the four stacks and returns a 19-layer SpatRaster
bio_raster <- terra::bioclim(r_tas, r_tasmax, r_tasmin, r_pr)
# Extract values at a single cell (equivalent to xbioclim on one pixel)
cell_id <- cellFromXY(bio_raster, cbind(-99.1, 19.4))
values(bio_raster)[cell_id, ]Function-level comparison
Computing a single variable
xbioclim exposes every variable as its own named function, making intent explicit and reducing overhead when only one variable is needed:
bio04(tas) # Temperature Seasonality – xbioclim
#> [1] 180.4681
bio15(pr) # Precipitation Seasonality – xbioclim
#> [1] 93.81627In terra you would compute all 19 layers and then
subset the desired layer from the output SpatRaster:
# terra approach – always computes all 19
bio_raster <- terra::bioclim(r_tas, r_tasmax, r_tasmin, r_pr)
bio_raster[["bio04"]] # Temperature Seasonality layerComputing all 19 variables
result <- bioclim(tas, tasmax, tasmin, pr)
print(result)
#> bio01 bio02 bio03 bio04 bio05 bio06 bio07 bio08
#> 14.82500 15.14167 65.54834 180.46814 26.60000 3.50000 23.10000 15.90000
#> bio09 bio10 bio11 bio12 bio13 bio14 bio15 bio16
#> 12.43333 17.00000 12.30000 750.00000 163.00000 7.00000 93.81627 447.00000
#> bio17 bio18 bio19
#> 25.00000 205.00000 34.00000In terra the output is a SpatRaster with the same
variable names as layer names, so downstream raster algebra works
naturally:
bio_raster <- terra::bioclim(r_tas, r_tasmax, r_tasmin, r_pr)
names(bio_raster) # "bio01" ... "bio19"Applying xbioclim to tabular climate data
A common use-case outside of raster workflows is working with station data or point samples stored in a data frame. xbioclim is ideal here:
# Simulated station records: 3 stations × 12 months
set.seed(1)
n_stations <- 3
months <- 1:12
station_df <- data.frame(
station = rep(paste0("S", 1:n_stations), each = 12),
month = rep(months, times = n_stations),
tas = c(
c(5, 6, 9, 13, 17, 21, 23, 22, 18, 13, 8, 5), # temperate
c(24, 25, 26, 27, 28, 27, 26, 26, 26, 25, 24, 23), # tropical
c(-10, -8, -2, 5, 12, 17, 20, 18, 12, 4, -3, -8) # continental
),
tasmax = c(
c(8, 10, 14, 18, 23, 27, 30, 29, 25, 19, 13, 9),
c(30, 31, 32, 33, 34, 33, 32, 32, 32, 31, 30, 29),
c(-5, -3, 4, 11, 18, 24, 27, 25, 18, 9, 2, -3)
),
tasmin = c(
c(1, 2, 4, 8, 12, 16, 18, 17, 13, 8, 4, 1),
c(20, 21, 22, 23, 24, 23, 22, 22, 22, 21, 20, 19),
c(-16, -14, -9, -1, 6, 11, 14, 12, 6, -1, -8, -14)
),
pr = c(
c(60, 50, 45, 40, 35, 20, 10, 15, 35, 55, 65, 65),
c(5, 10, 20, 40, 80, 150, 180, 160, 120, 60, 20, 8),
c(25, 20, 22, 30, 45, 60, 70, 65, 45, 30, 28, 25)
)
)
# Apply bioclim() per station using split + lapply
# Convert station to character to ensure robust splitting
bio_list <- lapply(
split(station_df, as.character(station_df$station)),
function(d) bioclim(d$tas, d$tasmax, d$tasmin, d$pr)
)
# Combine into a data frame, keeping numeric columns numeric
bio_df <- do.call(rbind, lapply(names(bio_list), function(nm) {
as.data.frame(as.list(c(station = nm, bio_list[[nm]])))
}))
print(bio_df[, c("station", "bio01", "bio04", "bio12", "bio15")])
#> station bio01 bio04 bio12 bio15
#> 1 S1 13.3333333333333 647.216261298253 495 44.0176940797999
#> 2 S2 25.5833333333333 138.192699598142 853 87.9061331911443
#> 3 S3 4.75 1045.72542604006 465 43.9104818887438When to use each package
| Scenario | Recommended package |
|---|---|
| Processing global rasters (NetCDF, GeoTIFF) | terra |
| Computing variables for point/station data | xbioclim |
| Lightweight scripts with no spatial dependency | xbioclim |
| Integration into existing terra workflows | terra (or both) |
| Custom block processing with non-standard data | xbioclim |
SDM workflows that use terra::extract() output |
xbioclim |
Bridging both packages
If you extract raster values with terra and want to compute bioclim variables with xbioclim (e.g., for custom weighting or filtering), the workflow is:
library(terra)
library(xbioclim)
# 1. Extract monthly values at species occurrence points
pts <- vect(occurrences, geom = c("lon", "lat"), crs = "EPSG:4326")
tas_vals <- extract(r_tas, pts)[, -1] # drop ID column
tasmax_vals <- extract(r_tasmax, pts)[, -1]
tasmin_vals <- extract(r_tasmin, pts)[, -1]
pr_vals <- extract(r_pr, pts)[, -1]
# 2. Compute bioclim variables for each occurrence point
bio_matrix <- t(mapply(function(i) {
bioclim(
as.numeric(tas_vals[i, ]),
as.numeric(tasmax_vals[i, ]),
as.numeric(tasmin_vals[i, ]),
as.numeric(pr_vals[i, ])
)
}, seq_len(nrow(pts))))This hybrid approach is particularly useful for occurrence-based niche modelling, where you need bioclimatic values at irregular point locations rather than over a complete raster grid.