Nothing
#' @title Create 3D Ecological Niche Models with Maxent
#'
#' @description Uses MaxEnt from `predicts` package to test multiple models with
#' different feature class combinations, regularization multipliers, and a user
#' supplied partitioning scheme for training and testing. The function outputs
#' model objects, model results, as well as prediction SpatRasters of each model
#' projected back onto geographic space using the supplied environmental SpatRaster
#' stacks. Note: you will need to install `rJava` to successfully run this function.
#'
#' @param maxent_df 'data.frame' where the first column is a vector
#' of presences named "p" containing 1's and 0's. Each row represents a cell in the
#' spatRaster volume with an x, y, z coordinate, and 1's are presences while 0's are
#' absences, or background points. Other columns are environmental variable values
#' extracted at the occurrence and background points, and should have the same names
#' as the names of the environmental layers in the projection_layers list of SpatRaster
#' stacks.
#'
#' @param wanted_fc a character vector giving what feature class
#' combinations should be tried. "L" refers to linear, "Q" refers to quadratic, "H"
#' refers to hinge, and "P" refers to product. Should be in the format c("L", "Q", "LQ")
#' etc.
#'
#' @param wanted_rm regularization multipliers to be tried in format c(1:4)
#'
#' @param wanted_partition optional, should be the output of 'partition_3D'. if no
#' partition is supplied, all points will be used for training
#'
#' @param projection_layers `list` of SpatRaster stacks by depth for predictions,
#' must all be cropped to the depth slice with the largest extent and masked
#' to the accessible area of the matching depth slice. Each element is a depth slice,
#' and each stack should contain all of the environmental variables used in the model
#' with the same names as that used in the model.
#'
#' @param occs `data.frame` of longitude, latitude, and depth occurrences with columns
#' named "longitude", "latitude", and "depth".
#'
#' @param depth_list vector of depths corresponding to depth slices of list elements of
#' projection_layers. Should be positive and go from shallowest depth to deepest depth
#'
#' @details The names of the projection_layers should be the same as the column names
#' of the environmental variables in maxent_df. The number of models output will be the
#' number of feature classes multiplied by the number of regularization multipliers.
#' For example, a wanted_fc of c("L", "Q", "P") and a wanted_rm of c(1:3) will output 9
#' total models.
#'
#' @return An object of class `list` with four components:
#'
#' $models, a list containing each model object produced.
#'
#' $results, a `data.frame` where each row is a model corresponding to the list element
#' in $models and $predictions. If there was no partition supplied for training and
#' testing, each column will report the feature class, regularization multiplier, AUC,
#' total coefficients, nonzero coefficients, AICc, and delta AICc. if a partition is
#' used, it will report the average of these statistics across all partitions.
#'
#' $predictions, a `list` of spatRaster stacks where each list element is a model
#' corresponding to the rows of $results and elements of $models projected onto the
#' supplied projection_layers. Each layer in the stack is a depth slice.
#'
#' $partition_results, a `list` object of the same length as the number of partition
#' groups containing a `data.frame` of results for each model for each partition. Only
#' produced if a partition is supplied.
#'
#' @examples
#'
#' library(dplyr)
#' library(predicts)
#' library(terra)
#'
#' # creating list of spatraster stacks where each element is a depth slice
#' r1_d1 <- rast(ncol = 100, nrow = 100)
#' set.seed(0)
#' values(r1_d1) <- as.numeric(sample(c(1:100), size = 1000, replace = TRUE))
#' r2_d1 <- rast(ncol = 100, nrow = 100)
#' set.seed(0)
#' values(r2_d1) <- as.numeric(sample(c(1:1000), size = 1000, replace = FALSE))
#' r1_d2 <- r1_d1
#' values(r1_d2) <- as.numeric(values(r1_d1)+10)
#' r2_d2 <- r2_d1
#' values(r2_d2) <- as.numeric(values(r2_d1)+10)
#' d1 <- c(r1_d1, r2_d1)
#' names(d1) <- c("valsr1", "valsr2")
#' d2 <- c(r1_d2, r2_d2)
#' names(d2) <- c("valsr1", "valsr2")
#' envlist <- list(d1, d2)
#'
#' # creating occs and bgs
#' set.seed(0)
#' occs <- sample(c(1:nrow(crds(envlist[[1]][[1]]))), size = 50, replace = FALSE)
#' bgs <- sample(c(1:nrow(crds(envlist[[1]][[1]]))), size = 500, replace = FALSE)
#'
#' occ_indices <- sample(c(1:nrow(crds(envlist[[1]][[1]]))), size = 50, replace = FALSE)
#' bg_indices <- sample(c(1:nrow(crds(envlist[[1]][[1]]))), size = 500, replace = FALSE)
#'
#' occs_d1 <- crds(envlist[[1]][[1]])[occ_indices[1:25],]
#' occs_d2 <- crds(envlist[[2]][[1]])[occ_indices[26:50],]
#' bg_d1 <- crds(envlist[[1]][[1]])[bg_indices[1:250],]
#' bg_d2 <- crds(envlist[[2]][[1]])[bg_indices[251:500],]
#'
#' # extracting at occs and bgs
#' occ_valsr1_d1 <- extract(envlist[[1]][[1]], occs_d1)
#' occ_valsr1_d2 <- extract(envlist[[2]][[1]], occs_d2)
#' occ_valsr2_d1 <- extract(envlist[[1]][[2]], occs_d1)
#' occ_valsr2_d2 <- extract(envlist[[2]][[2]], occs_d2)
#'
#' occ_valsr1 <- rbind(occ_valsr1_d1, occ_valsr1_d2)
#' occ_valsr2 <- rbind(occ_valsr2_d1, occ_valsr2_d2)
#'
#' bg_valsr1_d1 <- extract(envlist[[1]][[1]], bg_d1)
#' bg_valsr1_d2 <- extract(envlist[[2]][[1]], bg_d2)
#' bg_valsr2_d1 <- extract(envlist[[1]][[2]], bg_d1)
#' bg_valsr2_d2 <- extract(envlist[[2]][[2]], bg_d2)
#'
#' bg_valsr1 <- rbind(bg_valsr1_d1, bg_valsr1_d2)
#' bg_valsr2 <- rbind(bg_valsr2_d1, bg_valsr2_d2)
#'
#' valsr1 <- rbind(occ_valsr1, bg_valsr1)
#' valsr2 <- rbind(occ_valsr2, bg_valsr2)
#'
#' p1 <- rep(1, times = 50)
#' p0 <- rep(0, times = 500)
#' p <- c(p1, p0)
#'
#' maxdf <- data.frame(p, valsr1, valsr2)
#'
#' coords <- rbind(occs_d1, occs_d2)
#' colnames(coords) <- c("longitude", "latitude")
#' depth_vector <- c(rep(1, times = 25), rep(2, times = 25))
#'
#' # Use data.frame instead of cbind so $depth is completely valid inside maxent_3D
#' occs_dataframe <- data.frame(coords, depth = depth_vector)
#'
#'\donttest{
#' # Pass the clean data.frame to the function
#' if(requireNamespace("rJava", quietly = TRUE)){
#' result <- maxent_3D(maxent_df = maxdf, wanted_fc = c("L", "Q"),
#' wanted_rm = c(1:2), projection_layers = envlist,
#' occs = occs_dataframe, depth_list = c(1,2))
#' }
#' }
#'
#' @importFrom terra extract
#'
#' @import predicts
#'
#' @keywords MaxEnt
#'
#' @export
maxent_3D <- function(maxent_df, wanted_fc, wanted_rm, wanted_partition=NULL,
projection_layers, occs, depth_list) {
if(!requireNamespace("rJava", quietly = TRUE)){
warning(message("rJava not available, cannot run MaxEnt."))
return(NULL)
}
# initial check of proper formatting
if(!("p" %in% colnames(maxent_df))) {
warning(message("Argument 'maxent_df' is missing column 'p'.\n"))
return(NULL)
}
if(any(!(c("longitude", "latitude", "depth") %in% colnames(occs)))) {
warning(message("Argument 'occs' should have columns named 'longitude',
'latitude' and 'depth'.\n"))
return(NULL)
}
if(!(is.numeric(wanted_rm))) {
warning(message("Argument 'wanted_rm' must be a numeric vector.\n"))
return(NULL)
}
if(any(!(names(projection_layers[[1]]) %in% colnames(maxent_df)))) {
warning(message("Argument 'projection_layers' should contain the same names as
the column names for 'maxent_df'.\n"))
return(NULL)
}
if(!(is.numeric(depth_list)) | any(depth_list < 0)) {
warning(message("Argument 'depth_list' should be numeric, go from the shallowest
to the deepest depth, and should be all positive.\n"))
return(NULL)
}
# Confirm predicts is using the correct .jar file
pred_jar_dir <- system.file("java", package = "predicts")
pred_jar <- file.path(pred_jar_dir, "maxent.jar")
rJava::.jinit(classpath = pred_jar, force.init = TRUE)
# lets remove incomplete cases from the maxent_df so all points match
maxent_df_present <- maxent_df[maxent_df$p == 1,]
maxent_df_absent <- maxent_df[maxent_df$p == 0,]
maxent_df_present <- maxent_df_present[complete.cases(maxent_df_present),]
df_for_maxent <- rbind(maxent_df_present, maxent_df_absent)
# first we'll make the fc input readable to maxent
new_wanted_fc <- vector("list", length = length(wanted_fc))
for(i in 1:length(wanted_fc)) {
if(wanted_fc[i] == "L") {
wanted_fc1 <- c("noquadratic", "noproduct", "nohinge", "noautofeature")
} else if(wanted_fc[i] == "Q") {
wanted_fc1 <- c("nolinear", "noproduct", "nohinge", "noautofeature")
} else if(wanted_fc[i] == "H") {
wanted_fc1 <- c("nolinear", "noquadratic", "noproduct", "noautofeature")
} else if(wanted_fc[i] == "P") {
wanted_fc1 <- c("nolinear", "noquadratic", "nohinge", "noautofeature")
} else if(wanted_fc[i] == "LQ") {
wanted_fc1 <- c("nohinge", "noproduct", "noautofeature")
} else if(wanted_fc[i] == "LH") {
wanted_fc1 <- c("noproduct", "noquadratic", "noautofeature")
} else if(wanted_fc[i] == "LP") {
wanted_fc1 <- c("noquadratic", "nohinge", "noautofeature")
} else if(wanted_fc[i] == "QH") {
wanted_fc1 <- c("nolinear", "noproduct", "noautofeature")
} else if(wanted_fc[i] == "QP") {
wanted_fc1 <- c("nolinear", "nohinge", "noautofeature")
} else if(wanted_fc[i] == "HP") {
wanted_fc1 <- c("nolinear", "noquadratic", "noautofeature")
} else if(wanted_fc[i] == "LQP") {
wanted_fc1 <- c("nohinge", "noautofeature")
} else if(wanted_fc[i] == "LQH") {
wanted_fc1 <- c("noproduct", "noautofeature")
} else if(wanted_fc[i] == "QHP") {
wanted_fc1 <- c("nolinear", "noautofeature")
} else if(wanted_fc[i] == "LHP") {
wanted_fc1 <- c("noquadratic", "noautofeature")
} else if(wanted_fc[i] == "LQHP") {
wanted_fc1 <- c("noautofeature")
} else {
warning(message("Not a usable feature class.\n"))
return(NULL)
}
new_wanted_fc[[i]] <- wanted_fc1
}
# next we'll make a full list of the fc and rm combinations
# j loop for each rm, make a list the length of rm attached to fc[i]
# so there will be i lists for each fc
# concatenate the i lists into one list to make the full combined rm and fc list
fc_perm_list <- vector("list", length = length(new_wanted_fc))
for(i in 1:length(new_wanted_fc)) {
rm_perm_list <- vector("list", length = length(wanted_rm))
for(j in 1:length(wanted_rm)) {
rm_element <- wanted_rm[j]
fc_element <- new_wanted_fc[[i]]
rm_perm_list[[j]] <- list(rm_element, fc_element)
}
fc_perm_list[[i]] <- rm_perm_list
}
final_perm_call <- vector("list", length = length(wanted_fc))
for(i in 1:length(wanted_fc)) {
rm_parts <- vector(length = length(wanted_rm))
for(j in 1:length(wanted_rm)) {
rm_parts[j] <- paste0("fc_perm_list[[", i, "]][[", j, "]]")
}
final_perm_call[[i]] <- rm_parts
}
final_perm_call_real <- unlist(final_perm_call)
final_perm_list <- vector("list", length = length(final_perm_call_real))
for(i in 1:length(final_perm_call_real)) {
final_perm_list[[i]] <- eval(parse(text = final_perm_call_real[i]))
}
# if there is no partitioning scheme, a model will be run that is trained
# on all of the data. These models will also be returned as the models
# run for a partitioning scheme, but the partitioning scheme run will also
# return validation statistics. it can only return AUC
if(is.null(wanted_partition)) {
model_list <- vector("list", length = length(final_perm_list))
auc_list <- vector("list", length = length(final_perm_list))
total_coef_list <- vector(length = length(final_perm_list))
nonzero_coef_list <- vector(length = length(final_perm_list))
predictions_list <- vector("list", length = length(final_perm_list))
AICc_list <- vector(length = length(final_perm_list))
print("running models")
for(i in 1:length(final_perm_list)) {
# create model
mod1 <- predicts::MaxEnt(x = df_for_maxent[,!names(df_for_maxent) %in% "p"],
p = c(df_for_maxent[,"p"]),
args = c(paste0("betamultiplier=",
final_perm_list[[i]][[1]]),
paste0(final_perm_list[[i]][[2]])))
# save model
model_list[[i]] <- mod1
# retrieve AUC
auc_list[[i]] <- mod1@results[which(rownames(mod1@results) ==
"Training.AUC"),1]
# retrieve coefficents
all_coef <- suppressWarnings(as.numeric(unlist(strsplit(mod1@lambdas, ","))))
all_coef <- all_coef[-which(is.na(all_coef))]
total_coef_list[i] <- length(all_coef)
nonzero_coef <- all_coef[which(all_coef != 0)]
nonzero_coef <- nonzero_coef[which(nonzero_coef != 0.000000e+00)]
nonzero_coef_list[i] <- length(nonzero_coef)
# generate predictions and pull out values at occurrences for AICc calc
predicted_suit_list <- vector("list", length = length(projection_layers))
wanted_val_list <- vector("list", length = length(projection_layers))
for(j in 1:length(projection_layers)) {
predicted_suit <- predict(mod1, projection_layers[[j]])
predicted_suit_list[[j]] <- predicted_suit
standard_suit <- predicted_suit
values(standard_suit) <- values(standard_suit)/max(values(standard_suit),
na.rm = TRUE)
needed_occs <- occs[occs$depth == depth_list[j],]
if(nrow(needed_occs > 0)) {
wanted_val_list[[j]] <- terra::extract(standard_suit,
data.frame(needed_occs$longitude,
needed_occs$latitude))
} else {
wanted_val_list[[j]] <- NA
}
}
wanted_val_final <- do.call(rbind, wanted_val_list)[,1]
wanted_val_final <- wanted_val_final[which(!(is.na(wanted_val_final)))]
predictions_list[[i]] <- rast(predicted_suit_list)
# calculating AICc
bigk <- length(nonzero_coef)
likelihood <- sum(log(wanted_val_final))
littlen <- length(wanted_val_final)
if(bigk == (littlen - 1)) {
AICc_list[i] <- NA
} else {
AICc <- ((2*bigk) - (2*likelihood)) +
(((2*bigk)*(bigk + 1))/(littlen - bigk - 1))
AICc_list[i] <- AICc
}
}
all_auc <- do.call(rbind, auc_list)
rm <- rep(wanted_rm, times = length(wanted_fc))
fc_list <- vector("list", length = length(wanted_fc))
for(i in 1:length(wanted_fc)) {
fc_list[[i]] <- rep(wanted_fc[i], times = length(wanted_rm))
}
fc <- unlist(fc_list)
delta.AICc <- AICc_list - min(AICc_list, na.rm = T)
mod_results <- data.frame(fc, rm, all_auc[,1], total_coef_list, nonzero_coef_list,
AICc_list, delta.AICc)
colnames(mod_results) <- c("fc", "rm", "train.AUC", "toal.coef", "nonzero.coef",
"AICc", "delta.AICc")
final_output <- list(models = model_list, predictions = predictions_list,
results = mod_results)
# generating models for a given partition scheme
} else {
# partition warnings
if(!(all(is.list(wanted_partition),
c("occ_partitions", "bg_partitions") %in% names(wanted_partition)))) {
warning(message("Argument 'wanted_partition' should be a list containing the
named elements 'occ_partitions' and 'bg_partitions', as output
by partition_3D().\n"))
return(NULL)
}
if(length(wanted_partition$occ_partitions) != nrow(maxent_df[maxent_df$p == 1,])) {
warning(message("Number of occurrences in partition does not match number of
occurrences in 'maxent_df'.\n"))
return(NULL)
}
# full models for model part of output
model_list <- vector("list", length = length(final_perm_list))
print("running models")
for(i in 1:length(final_perm_list)) {
# create model
mod1 <- MaxEnt(x = df_for_maxent[,-1], p = df_for_maxent[,1],
args = c(paste0("betamultiplier=",
final_perm_list[[i]][[1]]),
paste0(final_perm_list[[i]][[2]])))
# save model
model_list[[i]] <- mod1
}
num_of_partitions <- unique(wanted_partition$bg_partitions)
# generating training and testing models for each partition
part_model_list <- vector("list", length = length(num_of_partitions))
part_eval_list <- vector("list", length = length(num_of_partitions))
part_predictions_list <- vector("list", length = length(num_of_partitions))
present_only <- df_for_maxent[which(df_for_maxent$p == 1),]
absent_only <- df_for_maxent[which(df_for_maxent$p == 0),]
for (i in (1:length(num_of_partitions))) {
test_p <- present_only[which(wanted_partition$occ_partitions ==
num_of_partitions[i]),]
train_p <- present_only[which(wanted_partition$occ_partitions !=
num_of_partitions[i]),]
test_a <- absent_only[which(wanted_partition$bg_partitions ==
num_of_partitions[i]),]
train_a <- absent_only[which(wanted_partition$bg_partitions !=
num_of_partitions[i]),]
test_full <- rbind(test_p, test_a)
train_full <- rbind(train_p, train_a)
mod_per_param_list <- vector("list", length = length(final_perm_list))
auc_val_per_param_list <- vector(length = length(final_perm_list))
auc_train_per_param_list <- vector("list", length = length(final_perm_list))
total_coef_list <- vector(length = length(final_perm_list))
nonzero_coef_list <- vector(length = length(final_perm_list))
predictions_list <- vector("list", length = length(final_perm_list))
print(paste0("training and testing models for partition ", i))
for(j in 1:length(final_perm_list)) {
# generating each model combination for partition i
train_model <- MaxEnt(x = train_full[,-1], p = train_full[,1],
args = c(paste0("betamultiplier=",
final_perm_list[[j]][[1]]),
paste0(final_perm_list[[j]][[2]])))
# saving model
mod_per_param_list[[j]] <- train_model
# retrieving validation auc
test_full_present <- test_full[test_full$p == 1,]
test_full_absent <- test_full[test_full$p == 0,]
ev <- predicts::pa_evaluate(p = test_full_present[,-1],
a = test_full_absent[,-1],
model = train_model)
auc_val_per_param_list[j] <- ev@stats$auc
# retrieving training AUC
auc_train_per_param_list[[j]] <-
train_model@results[which(rownames(train_model@results) == "Training.AUC"),1]
# retrieve coefficents
all_coef <- suppressWarnings(as.numeric(unlist(strsplit(train_model@lambdas, ","))))
all_coef <- all_coef[-which(is.na(all_coef))]
total_coef_list[j] <- length(all_coef)
nonzero_coef <- all_coef[which(all_coef != 0)]
nonzero_coef <- nonzero_coef[which(nonzero_coef != 0.000000e+00)]
nonzero_coef_list[j] <- length(nonzero_coef)
# generate predictions; AICc will be calculated after model averaging across
# partitions
predicted_suit_list <- vector("list", length = length(projection_layers))
wanted_val_list <- vector("list", length = length(projection_layers))
for(k in 1:length(projection_layers)) {
predicted_suit <- predict(train_model, projection_layers[[k]])
predicted_suit_list[[k]] <- predicted_suit
}
predictions_list[[j]] <- predicted_suit_list
}
part_model_list[[i]] <- mod_per_param_list
all_auc_train <- do.call(rbind, auc_train_per_param_list)
rm <- rep(wanted_rm, times = length(wanted_fc))
fc_list <- vector("list", length = length(wanted_fc))
for(j in 1:length(wanted_fc)) {
fc_list[[j]] <- rep(wanted_fc[j], times = length(wanted_rm))
}
fc <- unlist(fc_list)
mod_results <- data.frame(fc, rm, all_auc_train, auc_val_per_param_list,
total_coef_list,
nonzero_coef_list)
colnames(mod_results) <- c("fc", "rm", "train.AUC", "val.AUC", "total.coef",
"nonzero.coef")
part_eval_list[[i]] <- mod_results
part_predictions_list[[i]] <- predictions_list
}
# creating a mod results dataframe averaged across all partitions
wantdf <- part_eval_list[[1]][,-c(1:2)]
cell_num <- (nrow(wantdf)*ncol(wantdf))
avg_mod_result <- matrix(nrow = nrow(wantdf),
ncol = ncol(wantdf))
for(i in 1:cell_num) {
avg_vec <- vector(length = length(num_of_partitions))
for(j in 1:length(num_of_partitions)) {
wantdf <- part_eval_list[[j]][,-c(1:2)]
avg_vec[j] <- as.matrix(wantdf)[i]
}
avg_mod_result[i] <- mean(avg_vec)
}
avg_mod_result <- data.frame(avg_mod_result)
colnames(avg_mod_result) <- c("avg.train.AUC", "avg.val.AUC", "avg.total.coef",
"avg.nonzero.coef")
avg_mod_result <- cbind(part_eval_list[[1]][,1:2], avg_mod_result)
# creating an average suitability list across partitions for final predictions
# and average AICc
avg_predictions_list <- vector("list", length = length(final_perm_list))
for(i in 1:length(final_perm_list)) {
avg_suit_list <- vector("list", length = length(projection_layers))
for(j in 1:length(projection_layers)) {
partition_layers <- vector("list", length = length(num_of_partitions))
for(k in 1:length(num_of_partitions)) {
partition_layers[[k]] <- part_predictions_list[[k]][[i]][[j]]
}
avg_across_depth_for_model_i <- terra::app(rast(partition_layers), mean)
avg_suit_list[[j]] <- avg_across_depth_for_model_i
}
avg_predictions_list[[i]] <- rast(avg_suit_list)
}
# calculating average AICc for each model type
avg.AICc <- vector(length = length(final_perm_list))
for(i in 1:length(final_perm_list)) {
wanted_val_list <- vector("list", length = length(projection_layers))
for(j in 1:length(projection_layers)) {
standard_suit <- avg_predictions_list[[i]][[j]]
values(standard_suit) <- values(standard_suit)/max(values(standard_suit),
na.rm = T)
needed_occs <- occs[occs$depth == depth_list[j],]
if(nrow(needed_occs) > 0) {
wanted_val_list[[j]] <- terra::extract(standard_suit,
data.frame(needed_occs$longitude, needed_occs$latitude))
} else {
wanted_val_list[[j]] <- NA
}
}
wanted_val_final <- do.call(rbind, wanted_val_list)[,1]
wanted_val_final <- wanted_val_final[which(!(is.na(wanted_val_final)))]
# calculating AICc
bigk <- avg_mod_result$avg.nonzero.coef[i]
likelihood <- sum(log(wanted_val_final))
littlen <- length(wanted_val_final)
if(bigk == (littlen - 1)) {
avg.AICc[i] <- NA
} else {
AICc <- ((2*bigk) - (2*likelihood)) +
(((2*bigk)*(bigk + 1))/(littlen - bigk - 1))
avg.AICc[i] <- AICc
}
}
avg.delta.AICc <- avg.AICc-min(avg.AICc)
avg_mod_result <- cbind(avg_mod_result, avg.AICc, avg.delta.AICc)
final_output <- list(models = model_list, predictions = avg_predictions_list,
results = avg_mod_result, partition_results = part_eval_list)
}
return(final_output)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.