Skip to content
ptarrosoPublic

About

PResence-Only NICHE modelling

Resources

Stars

1 star

Watchers

2 watching

Forks

Latest commit

 

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

proniche: PResence-Only NICHE modelling

R package for computing niche models using a variety of truly presence-only methods (i.e., methods that use only environmental info from the presence sites, with no background or pseudo-absence data):

  • Bioclim (rectangular environmental envelope)
  • Convex hull (in environmental space)
  • Domain
  • Mahalanobis distance
  • Kernel density estimate
  • Multivariate normal distribution

Reference

Barbosa A.M. & Tarroso P. (2026) proniche – an R package for presence-only niche modelling in R. Biodiversity Informatics, in press

User guide

(Install and) load packages

# devtools::install_github("https://github.com/ptarroso/proniche")
# or:
# remotes::install_github("https://github.com/ptarroso/proniche")

# install.packages("terra")
# install.packages("geodata")

library(proniche)
library(terra)
library(geodata)

Import some example data within a region

# you can use the example data bundled with the 'proniche' package:

vars <- terra::rast(system.file("extdata/vars.tif", package = "proniche"))
occs <- read.csv(system.file("extdata/occs.csv", package = "proniche"))


# or you can download and select your own data of interest, e.g.:

vars <- geodata::worldclim_global(var = "bio", res = 5, path = "outputs")
names(vars) <- sub("wc2.1_5m_", "", names(vars))
vars <- vars[[c("bio_1", "bio_6", "bio_12", "bio_14")]]

# crop to region of interest:
vars <- terra::crop(vars, terra::ext(-10, 4, 36, 44))  # xmin, xmax, ymin, ymax
terra::plot(vars)

# import species occurrence records in this region:
occs <- geodata::sp_occurrence(genus = "Tuta", 
                               species = "absoluta", 
                               ext = vars, 
                               args = "occurrenceStatus=Present")

# note that data cleaning should normally be performed at this point!
# e.g. with fuzzySim::cleanCoords() or CoordinateCleaner::clean_coordinates()
# we omit this step here, as it may be complex and it exceeds our scope

occs <- subset(occs, select = c("lon", "lat"))

# map the occurrence records:
terra::plot(vars[[1]] * 0, col = "tan", background = "lightblue",
            legend = FALSE, main = "Presences")
points(occs, pch = 20, cex = 0.2)

Extract environmental values at the presence points

vals <- terra::extract(vars, occs, ID = FALSE)

# remove duplicates:
nrow(vals)
#> [1] 383
vals <- unique(vals)
nrow(vals)
#> [1] 80

head(vals)
#>       bio_1    bio_6 bio_12 bio_14
#> 1  14.71175 1.984000    386      4
#> 3  16.73919 5.720833    570     14
#> 7  17.62214 5.912360    268      1
#> 12 17.96979 6.000000    210      1
#> 16 16.66498 8.770213    495      1
#> 19 17.76883 6.351948    294      1

Plot frequency distributions

par(mfrow = c(2, 2), mar = c(2, 2, 2, 1))
proniche::freqPlot(vals, vars)

Fit true presence-only models

bc_fit <- proniche::promodel(vals, method = "bioclim")
ch_fit <- proniche::promodel(vals, method = "convexhull")
dm_fit <- proniche::promodel(vals, method = "domain")
mm_fit <- proniche::promodel(vals, method = "mahalanobis")
km_fit <- proniche::promodel(vals, method = "kernel")
mv_fit <- proniche::promodel(vals, method = "mvnormal")

Plot models in environmental space

The plots show the bidimensional environmental space defined by two variables at a time. By default, the first two variables in the model are used, but the cols argument can define a different two-variable combination:

par(mfrow = c(2, 3))

plot(bc_fit, main = "Bioclim")
plot(ch_fit, main = "Convex Hull")
plot(dm_fit, main = "Domain")
plot(mm_fit, main = "Mahalanobis")
plot(km_fit, main = "Kernel")
plot(mv_fit, main = "Multivariate Normal")

plot(bc_fit, cols = c(2, 4), main = "Bioclim")
plot(ch_fit, cols = c(2, 4), main = "Convex Hull")
plot(dm_fit, cols = c(2, 4), main = "Domain")
plot(mm_fit, cols = c(2, 4), main = "Mahalanobis")
plot(km_fit, cols = c(2, 4), main = "Kernel")
plot(mv_fit, cols = c(2, 4), main = "Multivariate Normal")

Map model predictions

bc_pred <- predict(bc_fit, vars)
ch_pred <- predict(ch_fit, vars)
dm_pred <- predict(dm_fit, vars)
mm_pred <- predict(mm_fit, vars)
km_pred <- predict(km_fit, vars)
mv_pred <- predict(mv_fit, vars)

par(mfrow = c(2, 3))
terra::plot(bc_pred, type = "continuous", main = "Bioclim")
terra::plot(ch_pred, type = "continuous", main = "Convex Hull")
terra::plot(dm_pred, type = "continuous", main = "Domain")
terra::plot(mm_pred, type = "continuous", main = "Mahalanobis")
terra::plot(km_pred, type = "continuous", main = "Kernel")
terra::plot(mv_pred, type = "continuous", main = "Multivariate Normal")

Reclassify predictions into a comparable scale

From Formoso-Freire et al. (2021):

“This function rescales the predictions into a ranked suitability value for each grid cell, thus informing about the suitability of climatic conditions in relative terms and hence which locations are more suitable than others. Current implementations of BIOCLIM compute a percentile distribution of the values of each environmental variable at known presence locations. Then, the closer to the 50th percentile (the median), the more suitable each location is according to that variable. However, the more variables are included in the model, the less suitable any location is considered, because it is less likely to be close to the median for all variables, and more likely to be outside the range observed at known presences for at least one variable. Our proposed rescaling of Bioclim predictions removes the dependence of the potential distribution on the number of variables included, and it provides more realistic predictions”.

bc_rcl <- proniche::quantReclass(bc_pred[[1]])
ch_rcl <- proniche::quantReclass(ch_pred[[1]])
dm_rcl <- proniche::quantReclass(dm_pred[[1]])
mm_rcl <- proniche::quantReclass(mm_pred[[1]])
km_rcl <- proniche::quantReclass(km_pred[[1]])
mv_rcl <- proniche::quantReclass(mv_pred[[1]])

par(mfrow = c(2, 3))
terra::plot(bc_rcl, range = c(0, 1), type = "continuous", main = "Bioclim")
terra::plot(ch_rcl, range = c(0, 1), type = "continuous", main = "Convex Hull")
terra::plot(dm_rcl, range = c(0, 1), type = "continuous", main = "Domain")
terra::plot(mm_rcl, range = c(0, 1), type = "continuous", main = "Mahalanobis")
terra::plot(km_rcl, range = c(0, 1), type = "continuous", main = "Kernel")
terra::plot(mv_rcl, range = c(0, 1), type = "continuous", main = "Multivariate Normal")

Truncated predictions

It is also possible to define a hard boundary for the inferred environmental niche, predicting only the pixels inside the defined envelope:

bc_fit_trunc <- truncate(bc_fit, p_obs = 1)
ch_fit_trunc <- truncate(ch_fit, p_obs = 1)
dm_fit_trunc <- truncate(dm_fit, p_obs = 1)
mm_fit_trunc <- truncate(mm_fit, p_obs = 1)
km_fit_trunc <- truncate(km_fit, p_obs = 1)
mv_fit_trunc <- truncate(mv_fit, p_obs = 1)

bc_pred_trunc <- predict(bc_fit_trunc, vars, type = "truncated")
ch_pred_trunc <- predict(ch_fit_trunc, vars, type = "truncated")
dm_pred_trunc <- predict(dm_fit_trunc, vars, type = "truncated")
mm_pred_trunc <- predict(mm_fit_trunc, vars, type = "truncated")
km_pred_trunc <- predict(km_fit_trunc, vars, type = "truncated")
mv_pred_trunc <- predict(mv_fit_trunc, vars, type = "truncated")

plot(c(bc_pred_trunc, ch_pred_trunc, dm_pred_trunc,
        mm_pred_trunc, km_pred_trunc, mv_pred_trunc), 
      mar = c(1.5, 1.5, 2, 2))

As shown above, truncation works well for most methods based on the proportion of known presences correctly classified. The exception is Domain, because it classifies similarity to any of the presences. Thus, every presence has a similarity value of 1, because it is equal at least to itself. It is not possible in this case to set a threshold that correctly classifies a given proportion of presences, because they will all always be correctly classified. A workaround is to define the threshold using test data, for example from a buffer around the presence points. This way we have a range of climatic similarity values in the spatial vicinity of the presences and we can choose a threshold:

buf <- buffer(terra::vect(occs), 1000)
test_data <- terra::extract(vars, buf, ID = FALSE)
dm_fit_trunc_test <- truncate(dm_fit, test_data = test_data)
dm_pred_trunc_test <- predict(dm_fit_trunc_test, vars, type = "truncated")
plot(dm_pred_trunc_test, main = "Domain truncated from test data")

Ensemble predictions

There are several ways to ensemble model predictions. The example below simply uses the local mean prediction across models, showing also the variance for an assessment of uncertainty:

preds <- c(bc_rcl, ch_rcl, dm_rcl, mm_rcl, km_rcl, mv_rcl)

ens_mean <- terra::app(preds, "mean")
ens_var <- terra::app(preds, "var")

par(mfrow = c(1, 2))
terra::plot(ens_mean, range = c(0, 1), main = "Ensemble mean")
terra::plot(ens_var, range = c(0, 1), main = "Ensemble variance")

Evaluate predictions

The following example code lines compute several different evaluation metrics for the ensemble mean model (this section requires loading a few more packages):

library(modEvA)
library(pROC)
library(kuenm)
library(raster)

par(mfrow = c(2, 2))

# evaluation functions in the modEvA package accept spatial data directly:

modEvA::threshMeasures(obs = occs, pred = ens_mean, thresh = "maxTSS", 
                       measures = c("CCR", "Sensitivity", "Specificity", 
                       "kappa", "TSS"), rm.dup = TRUE, standardize = FALSE,
                       main = "Threshold-based metrics")

modEvA::Boyce(obs = occs, pred = ens_mean, rm.dup.points = TRUE,
              main = "Continuous Boyce Index")

modEvA::AUC(obs = occs, pred = ens_mean, rm.dup = TRUE, main = "ROC curve")


# partial AUC in the pROC package requires instead a presence/absence table:

dat <- modEvA::inputMunch(obs = occs, pred = ens_mean, rm.dup = TRUE)

str(dat)

proc <- pROC::roc(response = dat$obs, predictor = dat$pred, 
                  partial.auc = c(0, 0.2), partial.auc.correct = TRUE)

pROC::plot.roc(proc, print.auc = TRUE, auc.polygon = TRUE, 
               max.auc.polygon = TRUE)

# we can also compute the partial ROC ratio under a maximum potential error:

procratio <- kuenm::kuenm_proc(occ.test = occs, 
                               model = raster::raster(ens_mean), 
                               threshold = 5)

procratio$pROC_summary
#> Mean_AUC_ratio_at_5%            pval_pROC 
#>             1.418404             0.000000

About

PResence-Only NICHE modelling

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages