
8. Benchmarking SDM Package Performance in R
[author name]
2026-06-12
Source:vignettes/articles/08_runtime-benchmark.Rmd
08_runtime-benchmark.RmdMotivation
Species distribution models (SDMs) are implemented in several R packages, each providing different workflows, abstractions, and computational strategies. Despite their widespread use, direct and reproducible comparisons of computational performance are rare, largely because differences in data preparation, validation strategies, and algorithm implementations obscure fair comparisons.
This document provides a fully reproducible benchmark comparing the runtime performance of four widely used SDM frameworks:
- biomod2
- sdm
- caretSDM
All packages are evaluated under identical conditions, using the same data, algorithms, and validation structure.
Benchmark design
To ensure fairness and reproducibility, we enforced the following constraints:
- Identical pseudoabsence method
- Identical environmental predictors
- Identical validation method
- Same algorithms across packages (Artificial Neural Network, Classification Tree Analysis, Flexible Discriminant Analysis, Boosted Regression Trees, Multiple Adaptive Regression Splines and Random Forest)
- No parallelization
- Runtime measured using the
benchpackage
Benchmarks included three stages: preprocessing (data formatting), processing (model fitting), and postprocessing (projection). A complete end-to-end benchmark was also conducted. Preprocessing included data formatting and pseudoabsence generation, processing included model fitting with cross-validation and ensembling, and postprocessing included projection to future scenarios.
Packages
library(terra)
library(sf)
library(bench)
library(biomod2)
library(sdm)
library(caretSDM)
library(RSNNS)
library(rpart)
library(mda)
library(gbm)
library(earth)
library(randomForest)Data
Presence–absence data
head(occ)
#> species decimalLongitude decimalLatitude
#> 327 Araucaria angustifolia -4700678 -3065133
#> 405 Araucaria angustifolia -4711827 -3146727
#> 404 Araucaria angustifolia -4711885 -3147170
#> 310 Araucaria angustifolia -4717665 -3142767
#> 49 Araucaria angustifolia -4726011 -3148963
#> 124 Araucaria angustifolia -4727265 -3148517Environmental predictors
bioc
#> stars object with 3 dimensions and 1 attribute
#> attribute(s):
#> Min. 1st Qu. Median Mean 3rd Qu. Max. NAs
#> current 14.58698 21.19678 298.9147 622.9417 1353.5 2368 1845
#> dimension(s):
#> from to offset delta refsys point values x/y
#> x 747 798 -180 0.1667 WGS 84 FALSE NULL [x]
#> y 670 706 90 -0.1667 WGS 84 FALSE NULL [y]
#> band 1 3 NA NA NA NA bio1 , bio4 , bio12Package-specific benchmark functions
biomod2
# ---------------------------
# biomod2 - Pre-processing
# ---------------------------
prep_biomod2 <- function() {
occ_biomod <- occ
occ_biomod$species <- rep(1, nrow(occ))
env <- terra::rast(stars::st_warp(bioc, crs = 6933))
sc <- list(
current = env,
scen1 = terra::rast(stars::st_warp(scen[1], crs = 6933)),
scen2 = terra::rast(stars::st_warp(scen[2], crs = 6933)),
scen3 = terra::rast(stars::st_warp(scen[3], crs = 6933)),
scen4 = terra::rast(stars::st_warp(scen[4], crs = 6933))
)
biomod_data <- BIOMOD_FormatingData(
resp.var = occ_biomod$species,
expl.var = env,
resp.xy = occ_biomod[, c("decimalLongitude", "decimalLatitude")],
resp.name = "species",
PA.nb.rep = 1,
PA.nb.absences = nrow(occ_biomod),
PA.strategy = "random"
)
list(data = biomod_data, scen = sc)
}
# ---------------------------
# biomod2 - Processing
# ---------------------------
fit_biomod2 <- function(prep) {
biomod_model <- BIOMOD_Modeling(
prep$data,
modeling.id = paste0("test_", Sys.time()),
models = c("ANN", "CTA", "FDA", "GBM", "MARS", "RF"),
CV.strategy = "kfold",
CV.nb.rep = 1,
CV.k = 5,
metric.eval = c("TSS", "AUCroc"),
do.progress = FALSE
)
biomod_ensmodel <- BIOMOD_EnsembleModeling(
bm.mod = biomod_model,
models.chosen = "all",
em.by = "all",
em.algo = c("EMmean", "EMwmean", "EMca"),
metric.select = c("AUCroc"),
metric.select.thresh = c(0.5)
)
list(
model = biomod_model,
ensemble = biomod_ensmodel,
scen = prep$scen
)
}
# ---------------------------
# biomod2 - Post-processing
# ---------------------------
post_biomod2 <- function(fit) {
biomod_proj <- lapply(names(fit$scen), function(nm) {
BIOMOD_Projection(
bm.mod = fit$model,
proj.name = nm,
new.env = fit$scen[[nm]],
models.chosen = "all",
overwrite = TRUE,
do.stack = TRUE,
nb.cpu = 1
)
})
mapply(function(proj, nm) {
BIOMOD_EnsembleForecasting(
bm.em = fit$ensemble,
bm.proj = proj,
proj.name = nm,
models.chosen = "all",
overwrite = TRUE
)
}, biomod_proj, names(fit$scen))
}
# ---------------------------
# biomod2 - Complete
# ---------------------------
run_biomod2 <- function() {
occ_biomod <- occ
occ_biomod$species <- rep(1, nrow(occ))
env <- terra::rast(stars::st_warp(bioc, crs = 6933))
sc <- list(
current = env,
scen1 = terra::rast(stars::st_warp(scen[1], crs = 6933)),
scen2 = terra::rast(stars::st_warp(scen[2], crs = 6933)),
scen3 = terra::rast(stars::st_warp(scen[3], crs = 6933)),
scen4 = terra::rast(stars::st_warp(scen[4], crs = 6933))
)
biomod_data <- BIOMOD_FormatingData(
resp.var = occ_biomod$species,
expl.var = env,
resp.xy = occ_biomod[, c("decimalLongitude", "decimalLatitude")],
resp.name = "species",
PA.nb.rep = 1,
PA.nb.absences = nrow(occ_biomod),
PA.strategy = "random"
)
biomod_model <- BIOMOD_Modeling(
biomod_data,
modeling.id = paste0("test_", Sys.time()),
models = c("ANN", "CTA", "FDA", "GBM", "MARS", "RF"),
CV.strategy = "kfold",
CV.nb.rep = 1,
CV.k = 5,
metric.eval = c("TSS", "AUCroc"),
do.progress = FALSE
)
biomod_ensmodel <- BIOMOD_EnsembleModeling(
bm.mod = biomod_model,
models.chosen = "all",
em.by = "all",
em.algo = c("EMmean", "EMwmean", "EMca"),
metric.select = c("AUCroc"),
metric.select.thresh = c(0.5)
)
biomod_proj <- lapply(names(sc), function(nm) {
BIOMOD_Projection(
bm.mod = biomod_model,
proj.name = nm,
new.env = sc[[nm]],
models.chosen = "all",
overwrite = TRUE,
do.stack = TRUE,
nb.cpu = 1
)
})
biomod_ensforecast <- mapply(function(proj, nm) {
BIOMOD_EnsembleForecasting(
bm.em = biomod_ensmodel,
bm.proj = proj,
proj.name = nm,
models.chosen = "all",
overwrite = TRUE
)
}, biomod_proj, names(sc))
}sdm
# ---------------------------
# sdm - Pre-processing
# ---------------------------
prep_sdm <- function() {
coords <- occ[, c("decimalLongitude", "decimalLatitude")]
names(coords) <- c("x", "y")
env <- terra::rast(stars::st_warp(bioc, crs = 6933))
pres_vals <- terra::extract(env, vect(coords))
pres_vals <- pres_vals[, -1]
pres_data <- cbind(species = 1, pres_vals)
bg <- sdm::background(env, n = nrow(pres_data), method = "gRandom")
bg$species <- 0
train <- rbind(dplyr::select(bg, -c("x", "y")), pres_data)
sc <- list(
current = env,
scen1 = terra::rast(stars::st_warp(scen[1], crs = 6933)),
scen2 = terra::rast(stars::st_warp(scen[2], crs = 6933)),
scen3 = terra::rast(stars::st_warp(scen[3], crs = 6933)),
scen4 = terra::rast(stars::st_warp(scen[4], crs = 6933))
)
d <- sdmData(
formula = species ~ bio1 + bio4 + bio12,
train = train
)
list(data = d, scen = sc)
}
# ---------------------------
# sdm - Processing
# ---------------------------
fit_sdm <- function(prep) {
m <- sdm(
formula = species ~ bio1 + bio4 + bio12,
data = prep$data,
methods = c("mlp", "rpart", "fda", "brt", "mars", "rf"),
replication = "cv",
cv.folds = 5
)
list(model = m, scen = prep$scen)
}
# ---------------------------
# sdm - Post-processing
# ---------------------------
post_sdm <- function(fit) {
list(
unweighted = lapply(fit$scen, function(r) {
ensemble(fit$model,
newdata = r,
setting = list(method = "unweighted")
)
}),
weighted = lapply(fit$scen, function(r) {
ensemble(fit$model,
newdata = r,
setting = list(
method = "weighted", stat = "AUC",
expr = "auc > 0.5"
)
)
}),
pa = lapply(fit$scen, function(r) {
ensemble(fit$model,
newdata = r,
setting = list(method = "pa", opt = 2)
)
})
)
}
# ---------------------------
# sdm - Complete
# ---------------------------
run_sdm <- function() {
coords <- occ[, c("decimalLongitude", "decimalLatitude")]
names(coords) <- c("x", "y")
env <- terra::rast(stars::st_warp(bioc, crs = 6933))
sc <- list(
current = env,
scen1 = terra::rast(stars::st_warp(scen[1], crs = 6933)),
scen2 = terra::rast(stars::st_warp(scen[2], crs = 6933)),
scen3 = terra::rast(stars::st_warp(scen[3], crs = 6933)),
scen4 = terra::rast(stars::st_warp(scen[4], crs = 6933))
)
pres_vals <- terra::extract(env, vect(coords))
pres_vals <- pres_vals[, -1]
pres_data <- cbind(species = 1, pres_vals)
bg <- sdm::background(env, n = nrow(pres_data), method = "gRandom")
bg$species <- 0
train <- rbind(dplyr::select(bg, -c("x", "y")), pres_data)
d <- sdmData(
formula = species ~ bio1 + bio4 + bio12,
train = train
)
m <- sdm(
formula = species ~ bio1 + bio4 + bio12,
data = d,
methods = c("mlp", "rpart", "fda", "brt", "mars", "rf"),
replication = "cv",
cv.folds = 5
)
list(
unweighted = lapply(sc, function(r) {
ensemble(m,
newdata = r,
setting = list(method = "unweighted")
)
}),
weighted = lapply(sc, function(r) {
ensemble(m,
newdata = r,
setting = list(
method = "weighted", stat = "AUC",
expr = "auc > 0.5"
)
)
}),
pa = lapply(sc, function(r) {
ensemble(m,
newdata = r,
setting = list(method = "pa", opt = 2)
)
})
)
}Note: The
sdmpackage does not natively accept externally defined folds. Therefore, although the number of folds is controlled, the exact split structure may differ slightly.
caretSDM
# ---------------------------
# caretSDM - Pre-processing
# ---------------------------
prep_caretSDM <- function() {
sa <- sdm_area(bioc) |>
add_scenarios(scen)
oc <- occurrences_sdm(occ, occ_crs = 6933)
input_sdm(oc, sa) |>
pseudoabsences(method = "random", n_set = 1)
}
# ---------------------------
# caretSDM - Processing
# ---------------------------
fit_caretSDM <- function(prep) {
prep |>
train_sdm(
algo = c("mlp", "rpart", "fda", "gbm", "gcvEarth", "rf"),
tuneLength = 1,
ctrl = caret::trainControl(
method = "cv",
number = 5,
classProbs = TRUE,
returnResamp = "none",
savePredictions = "final",
summaryFunction = summary_sdm
)
)
}
# ---------------------------
# caretSDM - Post-processing
# ---------------------------
post_caretSDM <- function(fit) {
fit |>
predict_sdm(th = 0.5) |>
ensemble_sdm(c("average", "weighted_average", "committee_average"))
}
# ---------------------------
# caretSDM - Complete
# ---------------------------
run_caretSDM <- function() {
sa <- sdm_area(bioc) |>
add_scenarios(scen)
oc <- occurrences_sdm(occ, occ_crs = 6933)
input_sdm(oc, sa) |>
pseudoabsences(method = "random", n_set = 1) |>
train_sdm(
algo = c("mlp", "rpart", "fda", "gbm", "gcvEarth", "rf"),
tuneLength = 1,
ctrl = caret::trainControl(
method = "cv",
number = 5,
classProbs = TRUE,
returnResamp = "none",
savePredictions = "final",
summaryFunction = summary_sdm
)
) |>
predict_sdm(th = 0.5) |>
ensemble_sdm(c("average", "weighted_average", "committee_average"))
}Benchmark execution
Before benchmarking, users are encouraged to run each function once to avoid first-run overhead (e.g. package initialization). Benchmark was executed in the authors’ personal computer. Outputs were saved as RDS files and are imported in a hidden chunk. This was necessary, since biomod2 had very divergent benchmark values when running it interactively in RStudio (which is the main way users operate SDM analysis) and running it through knitr or through GitHub pages.
prep_biomod2_res <- prep_biomod2()
prep_sdm_res <- prep_sdm()
prep_caretSDM_res <- prep_caretSDM()
bench_res_prep <- bench::mark(
biomod2 = prep_biomod2(),
sdm = prep_sdm(),
caretSDM = prep_caretSDM(),
iterations = 25,
check = FALSE,
min_time = Inf
)
fit_biomod2_res <- fit_biomod2(prep_biomod2_res)
fit_sdm_res <- fit_sdm(prep_sdm_res)
fit_caretSDM_res <- fit_caretSDM(prep_caretSDM_res)
bench_res_fit <- bench::mark(
biomod2 = fit_biomod2(prep_biomod2_res),
sdm = fit_sdm(prep_sdm_res),
caretSDM = fit_caretSDM(prep_caretSDM_res),
iterations = 25,
check = FALSE,
min_time = Inf
)
post_biomod2(fit_biomod2_res)
post_sdm(fit_sdm_res)
post_caretSDM(fit_caretSDM_res)
bench_res_post <- bench::mark(
biomod2 = post_biomod2(fit_biomod2_res),
sdm = post_sdm(fit_sdm_res),
caretSDM = post_caretSDM(fit_caretSDM_res),
iterations = 25,
check = FALSE,
min_time = Inf
)
unlink("species", recursive = TRUE, force = TRUE)
bench_res_complete <- bench::mark(
biomod2 = run_biomod2(),
sdm = run_sdm(),
caretSDM = run_caretSDM(),
iterations = 25,
check = FALSE,
min_time = Inf
)Results
bench_res_prep
#> # A tibble: 3 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 biomod2 129.5ms 168.91ms 4.37 5.34MB 0.699
#> 2 sdm 86.1ms 94.98ms 9.49 5.58MB 1.14
#> 3 caretSDM 782ms 1.09s 0.891 7.82MB 1.78
bench_res_fit
#> # A tibble: 3 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 biomod2 8.81s 10.02s 0.0971 708.96MB 0.388
#> 2 sdm 14.08s 14.84s 0.0641 2.38GB 0.708
#> 3 caretSDM 1.47s 1.79s 0.519 582.84MB 1.70
bench_res_post
#> # A tibble: 3 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 biomod2 14.4s 15.9s 0.0627 846MB 0.374
#> 2 sdm 14.7s 15.1s 0.0642 773MB 0
#> 3 caretSDM 466ms 548.4ms 1.71 44MB 0.273
bench_res_complete
#> # A tibble: 3 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 biomod2 22.21s 26.23s 0.0382 1.55GB 0.376
#> 2 sdm 34.21s 36.24s 0.0263 3.02GB 0.133
#> 3 caretSDM 2.49s 2.63s 0.372 631.71MB 1.12The table above summarizes the median runtime, iteration rate, and memory allocation for each package under identical conditions.
Interpretation
The benchmark results reveal clear differences in computational performance across the evaluated SDM frameworks, with the magnitude of these differences varying substantially among workflow stages.
Pre-processing
During the preprocessing stage, both biomod2 and sdm completed data preparation rapidly. In contrast, caretSDM required substantially more time, despite still being a very low running time (< 1s).
This difference primarily reflects the greater amount of internal data structuring performed by caretSDM, including the creation of standardized SDM objects, explicit pseudoabsence management, and preparation of workflow metadata used in later modeling and prediction steps. By contrast, biomod2 and sdm perform less structural preprocessing, relying more directly on raw data objects, with sdm package being the most efficient package in this stage.
Model fitting
Model fitting is more computationally demanding. Here, biomod2 exhibited the fastest performance, followed by sdm and caretSDM. Memory allocation during this stage also differed substantially across packages. The sdm package required the largest memory allocation, whereas caretSDM and biomod2 used a more close amount, with caretSDM allocating slightly more memory than biomod2.
These differences largely arise from how each framework orchestrates model training. The modeling functions in biomod2 appear to implement relatively efficient internal routines for coordinating algorithms and cross-validation. In contrast, caretSDM relies on the training infrastructure of the caret framework, which provides highly flexible resampling and tuning capabilities but introduces additional overhead during model training.
Post-processing (projection and ensembling)
The package caretSDM excels during the post-processing stage, which is known to be the stage that requires the highest amount of processing time. The sdm and biomod2 packages did not differed in processing time, with biomod2 allocating slightly more memory.
The improvement demonstrated by caretSDM is result of its Machine Learning approach, where the package projects models built with complete data and trained with optimized configurations infered from the internal validation process. In the other hand, biomod2 and sdm packages project all submodels generated through the pipeline, costing more memory and time to obtain results during this stage.
End-to-end workflow
When considering the complete workflow (preprocessing, model fitting, and projection), caretSDM was the fastest framework, followed by the biomod2 package and finally the sdm package. Memory usage followed the same pattern, with caretSDM allocating the fewest memory, followed by biomod2 and sdm.
Overall implications
These results highlight that differences among SDM frameworks arise not only from algorithm implementation but also from workflow architecture. Packages such as biomod2 and sdm prioritize computational efficiency by performing fewer intermediate abstractions and operating closer to the underlying modeling functions. In contrast, caretSDM emphasizes workflow standardization, object consistency, and integration with a general machine-learning framework, which introduces additional computational overhead but may provide advantages in reproducibility, extensibility, and workflow transparency. Moreover, caretSDM also needed substantial less lines of code to perform the same tasks, while also using only functions from its own package. This may be an advantage for users who prefer a more streamlined workflow or want to spend more coding time in other stages of the modeling process.
During preprocessing, this benchmark avoided the use of multiple sets of pseudoabsences to ensure that all packages were evaluated under identical conditions. However, in practice, users may choose to generate multiple sets of pseudoabsences to improve model robustness, which would likely amplify the observed differences in preprocessing time among packages. This approach was intentional due to the lack of easy way to perform this task in sdm package, which would require users to implement custom code to generate multiple sets of pseudoabsences and manage them across modeling iterations. This would probably reflect more a difference in user coding ability than in package performance, which is not the focus of this benchmark.
The use of multiple sets of pseudoabsences would likely amplify the observed differences in model fitting time among packages in the processing step. In the same way, hyperparameter tuning was let out of this benchmark to ensure that all packages were evaluated under identical conditions. However, users may choose to perform tuning to optimize model performance particularly when using caretSDM, which provides more extensive and auto-tuning capabilities. As previously stated, to include these methods in other packages would probably reflect more a difference in user coding ability than in package performance, which is not the focus of this benchmark.
Finally, in postprocessing, the differences between packages reflect the design choices made by each framework regarding how projections and ensemble predictions are managed. While the more structured approach of caretSDM provides advantages in terms of workflow consistency and downstream analysis, it introduces additional computational overhead compared with the more direct implementations used in biomod2 and sdm. Despite that, the projection of optimized models shrinks memory usage and processing time, allowing for more robust projections (fine-tuned models) to be obtained using the same amount of time and memory.
Moreover, considering the complete workflow, caretSDM excels in time and memory management. Despite not being the most efficient package during neither the pre-processing of data, due to internal standardization functions, nor the processing of models, caretSDM accelerates the post-processing step, through a Machine Learning approach.
Reproducibility
Runtime benchmarks were conducted on an Apple Silicon Mac (ARM64 M1 processor) running R 4.6.0. Absolute execution times may vary across hardware and operating systems. Users are encouraged to rerun the analysis on their own hardware to assess absolute performance differences.
Limitations
- No parallel processing was used;
- Performance may vary with dataset size, predictor dimensionality, and hardware;
- If you want to see your package here or if you think I am missing some coding or function from sdm and/or biomod2, please contact me on [authors name]@gmail.com.
Session information
sessionInfo()
# R version 4.6.0 (2026-04-24)
# Platform: aarch64-apple-darwin23
# Running under: macOS Tahoe 26.5
#
# Matrix products: default
# BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
# LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
#
# locale:
# [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#
# time zone: America/Sao_Paulo
# tzcode source: internal
#
# attached base packages:
# [1] splines stats graphics grDevices utils datasets methods base
#
# other attached packages:
# [1] caret_7.0-1 lattice_0.22-9 ggplot2_4.0.3 kernlab_0.9-33 glmnet_5.0
# [6] Matrix_1.7-5 dismo_1.3-16 raster_3.6-32 sp_2.2-1 RSNNS_0.4-18
# [11] Rcpp_1.1.1-1.1 caretSDM_1.9.4 sdm_1.2-59 xgboost_3.2.1.1 randomForest_4.7-1.2
# [16] maxnet_0.1.4 earth_5.3.5 plotmo_3.7.0 plotrix_3.8-14 Formula_1.2-5
# [21] gbm_2.2.3 mgcv_1.9-4 nlme_3.1-169 gam_1.22-7 foreach_1.5.2
# [26] mda_0.5-5 class_7.3-23 cito_1.1 rpart_4.1.27 nnet_7.3-20
# [31] biomod2_4.3-4-6 bench_1.1.4 sf_1.1-1 terra_1.9-27
#
# loaded via a namespace (and not attached):
# [1] RColorBrewer_1.1-3 torch_0.17.0 wk_0.9.5 shape_1.4.6.1
# [5] ECDFniche_0.5 rstudioapi_0.18.0 jsonlite_2.0.0 magrittr_2.0.5
# [9] farver_2.1.2 CoordinateCleaner_3.0.1 fs_2.1.0 vctrs_0.7.3
# [13] polynom_1.4-1 s2_1.1.9 pROC_1.19.0.1 parallelly_1.47.0
# [17] KernSmooth_2.23-26 plyr_1.8.9 stars_0.7-2 lubridate_1.9.5
# [21] whisker_0.4.1 lifecycle_1.0.5 iterators_1.0.14 pkgconfig_2.0.3
# [25] R6_2.6.1 future_1.70.0 digest_0.6.39 reshape_0.8.10
# [29] timechange_0.4.0 httr_1.4.8 abind_1.4-8 compiler_4.6.0
# [33] proxy_0.4-29 bit64_4.8.2 withr_3.0.2 S7_0.2.2
# [37] backports_1.5.1 DBI_1.3.0 R.utils_2.13.0 ecospat_4.1.3
# [41] MASS_7.3-65 lava_1.9.1 classInt_0.4-11 ggpp_0.6.0
# [45] gtools_3.9.5 oai_0.4.0 ModelMetrics_1.2.2.2 tools_4.6.0
# [49] units_1.0-1 otel_0.2.0 rgbif_3.8.5 future.apply_1.20.2
# [53] R.oo_1.27.1 glue_1.8.1 callr_3.7.6 profmem_0.7.0
# [57] grid_4.6.0 stringdist_0.9.17 checkmate_2.3.4 reshape2_1.4.5
# [61] generics_0.1.4 recipes_1.3.3 gtable_0.3.6 R.methodsS3_1.8.2
# [65] tidyr_1.3.2 data.table_1.18.4 xml2_1.5.2 pillar_1.11.1
# [69] stringr_1.6.0 ggspatial_1.1.10 dplyr_1.2.1 survival_3.8-6
# [73] bit_4.6.0 tidyselect_1.2.1 coro_1.1.0 lemon_0.5.2
# [77] knitr_1.51 gridExtra_2.3 stats4_4.6.0 xfun_0.57
# [81] hardhat_1.4.3 checkCLI_1.0 timeDate_4052.112 stringi_1.8.7
# [85] lazyeval_0.2.3 evaluate_1.0.5 codetools_0.2-20 tibble_3.3.1
# [89] cli_3.6.6 processx_3.9.0 globals_0.19.1 PresenceAbsence_1.1.11
# [93] rnaturalearth_1.2.0 parallel_4.6.0 gower_1.0.2 listenv_0.10.1
# [97] ipred_0.9-15 scales_1.4.0 prodlim_2026.03.11 e1071_1.7-17
# [101] purrr_1.2.2 geosphere_1.6-8 rlang_1.2.0