R/rf.R

Defines functions rf

Documented in rf

#' @title Random forest models with Moran's I test of the residuals
#' @description A convenient wrapper for \link[ranger]{ranger} that completes its output by providing the Moran's I of the residuals for different distance thresholds, the rmse and nrmse (as computed by [root_mean_squared_error()]), and variable importance scores based on a scaled version of the data generated by \link[base]{scale}.
#' @param data Data frame with a response variable and a set of predictors. Default: `NULL`
#' @param dependent.variable.name Character string with the name of the response variable. Must be in the column names of `data`. If the dependent variable is binary with values 1 and 0, the argument `case.weights` of `ranger` is populated by the function [case_weights()]. Default: `NULL`
#' @param predictor.variable.names Character vector with the names of the predictive variables. Every element of this vector must be in the column names of `data`. Optionally, the result of [auto_cor()] or [auto_vif()]. Default: `NULL`
#' @param distance.matrix Squared matrix with the distances among the records in `data`. The number of rows of `distance.matrix` and `data` must be the same. If not provided, the computation of the Moran's I of the residuals is omitted. Default: `NULL`
#' @param distance.thresholds Numeric vector with neighborhood distances. All distances in the distance matrix below each value in `dustance.thresholds` are set to 0 for the computation of Moran's I. If `NULL`, it defaults to seq(0, max(distance.matrix), length.out = 4). Default: `NULL`
#' @param xy (optional) Data frame or matrix with two columns containing coordinates and named "x" and "y". It is not used by this function, but it is stored in the slot `ranger.arguments$xy` of the model, so it can be used by [rf_evaluate()] and [rf_tuning()]. Default: `NULL`
#' @param ranger.arguments Named list with \link[ranger]{ranger} arguments (other arguments of this function can also go here). All \link[ranger]{ranger} arguments are set to their default values except for 'importance', that is set to 'permutation' rather than 'none'. The ranger arguments `x`, `y`, and `formula` are disabled. Please, consult the help file of \link[ranger]{ranger} if you are not familiar with the arguments of this function.
#' @param scaled.importance Logical, if `TRUE`, the function scales `data` with \link[base]{scale} and fits a new model to compute scaled variable importance scores. This makes variable importance scores of different models somewhat comparable. Default: `FALSE`
#' @param seed Integer, random seed to facilitate reproducibility. If set to a given number, the returned model is always the same. Default: `1`
#' @param verbose Boolean. If TRUE, messages and plots generated during the execution of the function are displayed. Default: `TRUE`
#' @param n.cores Integer, number of cores to use. Default: `parallel::detectCores() - 1`
#' @param cluster A cluster definition generated with `parallel::makeCluster()`. This function does not use the cluster, but can pass it on to other functions when using the `%>%` pipe. It will be stored in the slot `cluster` of the output list. Default: `NULL`
#' @return A ranger model with several extra slots:
#' \itemize{
#'   \item `ranger.arguments`: Stores the values of the arguments used to fit the ranger model.
#'   \item `importance`: A list containing a data frame with the predictors ordered by their importance, a ggplot showing the importance values, and local importance scores (difference in accuracy between permuted and non permuted variables for every case, computed on the out-of-bag data).
#'   \item `performance`: performance scores: R squared on out-of-bag data, R squared (cor(observed, predicted) ^ 2), pseudo R squared (cor(observed, predicted)), RMSE, and normalized RMSE (NRMSE).
#'   \item `residuals`: residuals, normality test of the residuals computed with [residuals_test()], and spatial autocorrelation of the residuals computed with [moran_multithreshold()].
#' }
#' @details Please read the help file of \link[ranger]{ranger} for further details. Notice that the `formula` interface of \link[ranger]{ranger} is supported through `ranger.arguments`, but variable interactions are not allowed (but check [the_feature_engineer()]).
#' @examples
#' if(interactive()){
#'
#'  #loading example data
#'  data("plant_richness_df")
#'  data("distance_matrix")
#'
#'  #fittind random forest model
#'  out <- rf(
#'    data = plant_richness_df,
#'    dependent.variable.name = "richness_species_vascular",
#'    predictor.variable.names = colnames(plant_richness_df)[5:21],
#'    distance.matrix = distance_matrix,
#'    distance.thresholds = 0,
#'    n.cores = 1
#'  )
#'
#'  class(out)
#'
#'  #data frame with ordered variable importance
#'  out$importance$per.variable
#'
#'  #variable importance plot
#'  out$importance$per.variable.plot
#'
#'  #performance
#'  out$performance
#'
#'  #spatial correlation of the residuals
#'  out$spatial.correlation.residuals$per.distance
#'
#'  #plot of the Moran's I of the residuals for different distance thresholds
#'  out$spatial.correlation.residuals$plot
#'
#'  #predictions for new data as done with ranger models:
#'  predicted <- stats::predict(
#'    object = out,
#'    data = plant_richness_df,
#'    type = "response"
#'  )$predictions
#'
#'  #alternative data input methods
#'  ###############################
#'
#'  #ranger.arguments can contain ranger arguments and any other rf argument
#'  my.ranger.arguments <- list(
#'  data = plant_richness_df,
#'  dependent.variable.name = "richness_species_vascular",
#'  predictor.variable.names = colnames(plant_richness_df)[8:21],
#'  distance.matrix = distance_matrix,
#'  distance.thresholds = c(0, 1000)
#'  )
#'
#'  #fitting model with these ranger arguments
#'  out <- rf(
#'    ranger.arguments = my.ranger.arguments,
#'    n.cores = 1
#'    )
#'
#' }
#' @rdname rf
#' @export
#' @importFrom parallel detectCores
#' @importFrom tibble remove_rownames
#' @importFrom dplyr arrange
#' @importFrom stats formula reorder
#' @importFrom rlang .data
rf <- function(
  data = NULL,
  dependent.variable.name = NULL,
  predictor.variable.names = NULL,
  distance.matrix = NULL,
  distance.thresholds = NULL,
  xy = NULL,
  ranger.arguments = NULL,
  scaled.importance = FALSE,
  seed = 1,
  verbose = TRUE,
  n.cores = parallel::detectCores() - 1,
  cluster = NULL
){

  #giving priority to data not from ranger.arguments
  if(!is.null(data) & !is.null(ranger.arguments)){
    ranger.arguments$data <- NULL
    ranger.arguments$dependent.variable.name <- NULL
    ranger.arguments$predictor.variable.names <- NULL
  }

  #default ranger arguments
  num.trees <- 500
  mtry <- NULL
  mtry <- NULL
  importance <- "permutation"
  write.forest <- TRUE
  probability <- FALSE
  min.node.size <- NULL
  max.depth <- NULL
  replace <- TRUE
  sample.fraction <- ifelse(replace, 1, 0.632)
  case.weights <- NULL
  class.weights <- NULL
  splitrule <- NULL
  num.random.splits <- 1
  alpha <- 0.5
  minprop <- 0.1
  split.select.weights <- NULL
  always.split.variables <- NULL
  respect.unordered.factors <- NULL
  scale.permutation.importance <- TRUE
  local.importance <- TRUE
  regularization.factor <- 1
  regularization.usedepth <- FALSE
  keep.inbag <- FALSE
  inbag <- NULL
  holdout <- FALSE
  quantreg <- FALSE
  oob.error <- TRUE
  num.threads <- n.cores
  save.memory <- FALSE
  classification <- NULL

  #putting ranger arguments in the environment
  if(!is.null(ranger.arguments)){
    list2env(ranger.arguments, envir=environment())
  }

  #coerce to data frame if tibble
  if(inherits(data, "tbl_df") | inherits(data, "tbl")){
    data <- as.data.frame(data)
  }

  if(inherits(xy, "tbl_df") | inherits(xy, "tbl")){
    xy <- as.data.frame(xy)
  }

  #predictor.variable.names comes from auto_vif or auto_cor
  if(inherits(predictor.variable.names, "variable_selection")){
    predictor.variable.names <- predictor.variable.names$selected.variables
  } else {
    if(sum(predictor.variable.names %in% colnames(data)) < length(predictor.variable.names)){
      stop(
        paste0(
          "The predictor.variable.names ",
          paste0(
            predictor.variable.names[!(predictor.variable.names %in% colnames(data))],
            collapse = ", "
          ),
          " are missing from 'data'"
        )
      )
    }
  }

  #checking if dependent.variable.name and predictor.variable.names are in colnames(data) and are numeric
  if(!(dependent.variable.name %in% colnames(data))){
    stop(
      paste0(
        "The dependent.variable.name ",
        dependent.variable.name,
        " is not a column of 'data'."
      )
    )
  }

  #subset data
  data <- data[, c(dependent.variable.name, predictor.variable.names)]

  #setting up seed if available
  if(!is.null(seed)){
    set.seed(seed)
  }

  #scaling the data if required
  if(scaled.importance == TRUE){

    data.scaled <-  as.data.frame(scale(x = data))

    #check if there are NaN
    if(sum(apply(data.scaled, 2, is.nan)) > 0 | sum(apply(data.scaled, 2, is.infinite)) > 0){
      scaled.importance <- FALSE
      warning("The training data yields NaN or Inf when scaled, setting scaled.importance to FALSE.")
    }

  }

  #computing case weights if dependent.variable.name is binary
  is.binary <- is_binary(
    data = data,
    dependent.variable.name = dependent.variable.name
  )
  if(is.binary == TRUE & is.null(case.weights)){
    case.weights <- case_weights(
      data = data,
      dependent.variable.name = dependent.variable.name
    )
  }

  #ranger model for r-squared and predictions
  m <- ranger::ranger(
    data = data,
    dependent.variable.name = dependent.variable.name,
    num.trees = num.trees,
    mtry = mtry,
    importance = importance,
    write.forest = write.forest,
    probability = probability,
    min.node.size = min.node.size,
    max.depth = max.depth,
    replace = replace,
    sample.fraction = sample.fraction,
    case.weights = case.weights,
    class.weights = class.weights,
    splitrule = splitrule,
    num.random.splits = num.random.splits,
    alpha = alpha,
    minprop = minprop,
    split.select.weights = split.select.weights,
    always.split.variables = always.split.variables,
    respect.unordered.factors = respect.unordered.factors,
    scale.permutation.importance = scale.permutation.importance,
    local.importance = local.importance,
    regularization.factor = regularization.factor,
    regularization.usedepth = regularization.usedepth,
    keep.inbag = keep.inbag,
    inbag = inbag,
    holdout = holdout,
    quantreg = quantreg,
    oob.error = oob.error,
    num.threads = num.threads,
    save.memory = save.memory,
    verbose = verbose,
    seed = seed,
    classification = classification
  )

  #get variable importance
  variable.importance.global <- m$variable.importance
  variable.importance.local <- m$variable.importance.local

  #if scaled.importance is TRUE
  if(scaled.importance == TRUE){

    #ranger model for variable importance
    m.scaled <- ranger::ranger(
      data = data.scaled,
      dependent.variable.name = dependent.variable.name,
      num.trees = num.trees,
      mtry = mtry,
      importance = importance,
      write.forest = write.forest,
      probability = probability,
      min.node.size = min.node.size,
      max.depth = max.depth,
      replace = replace,
      sample.fraction = sample.fraction,
      case.weights = case.weights,
      class.weights = class.weights,
      splitrule = splitrule,
      num.random.splits = num.random.splits,
      alpha = alpha,
      minprop = minprop,
      split.select.weights = split.select.weights,
      always.split.variables = always.split.variables,
      respect.unordered.factors = respect.unordered.factors,
      scale.permutation.importance = FALSE,
      local.importance = local.importance,
      regularization.factor = regularization.factor,
      regularization.usedepth = regularization.usedepth,
      keep.inbag = keep.inbag,
      inbag = inbag,
      holdout = holdout,
      quantreg = quantreg,
      oob.error = oob.error,
      num.threads = num.threads,
      save.memory = save.memory,
      verbose = verbose,
      seed = seed,
      classification = classification
    )

    #overwrite variable importance
    variable.importance.global <- m.scaled$variable.importance
    variable.importance.local <- m.scaled$variable.importance.local

  }

  #adding model arguments
  m$ranger.arguments <- list(
    data = data,
    dependent.variable.name = dependent.variable.name,
    predictor.variable.names = predictor.variable.names,
    distance.matrix = distance.matrix,
    distance.thresholds = distance.thresholds,
    xy = xy,
    num.trees = num.trees,
    mtry = mtry,
    importance = importance,
    scaled.importance = scaled.importance,
    write.forest = write.forest,
    probability = probability,
    min.node.size = min.node.size,
    max.depth = max.depth,
    replace = replace,
    sample.fraction = sample.fraction,
    case.weights = case.weights,
    class.weights = class.weights,
    splitrule = splitrule,
    num.random.splits = num.random.splits,
    alpha = alpha,
    minprop = minprop,
    split.select.weights = split.select.weights,
    always.split.variables = always.split.variables,
    respect.unordered.factors = respect.unordered.factors,
    scale.permutation.importance = scale.permutation.importance,
    local.importance = local.importance,
    regularization.factor = regularization.factor,
    regularization.usedepth = regularization.usedepth,
    keep.inbag = keep.inbag,
    inbag = inbag,
    holdout = holdout,
    quantreg = quantreg,
    oob.error = oob.error,
    num.threads = num.threads,
    save.memory = save.memory,
    seed = seed,
    classification = classification
  )

  #importance slot
  if(importance == "permutation"){

    #importance slot
    m$importance <- list()

    #global importance
    #sign of the importance
    variable.importance.global.sign <- variable.importance.global
    variable.importance.global.sign[variable.importance.global.sign >= 0] <- 1
    variable.importance.global.sign[variable.importance.global.sign < 0 ] <- -1

    #applying sqrt
    variable.importance.global <- sqrt(abs(variable.importance.global)) * variable.importance.global.sign


    m$importance$per.variable <- data.frame(
      variable = names(variable.importance.global),
      importance = variable.importance.global
    ) %>%
      tibble::remove_rownames() %>%
      dplyr::arrange(dplyr::desc(importance)) %>%
      dplyr::mutate(importance = round(importance, 3)) %>%
      as.data.frame()

    m$importance$per.variable.plot <- plot_importance(
      m$importance$per.variable,
      verbose = verbose
    )

    #local importance (reconverting values from ^2 to sqrt())

    #matrix with sign of the value
    variable.importance.local.sign <- variable.importance.local
    variable.importance.local.sign[variable.importance.local.sign >= 0] <- 1
    variable.importance.local.sign[variable.importance.local.sign < 0 ] <- -1

    #applying sqrt
    variable.importance.local <- sqrt(abs(variable.importance.local)) * variable.importance.local.sign

    #saving to the slot
    m$importance$local <- variable.importance.local

  }

  #computing predictions
  predicted <- stats::predict(
    object = m,
    data = data,
    type = "response"
  )$predictions

  #saving predictions
  m$predictions <- list()
  m$predictions$values <- predicted

  #getting observed data
  observed <- data[, dependent.variable.name]

  #performance slot
  m$performance <- list()

  m$performance$r.squared.oob <- m$r.squared

  m$performance$r.squared <- cor(observed, predicted) ^ 2

  m$performance$pseudo.r.squared <- cor(
    observed,
    predicted
  )

  m$performance$rmse.oob <- sqrt(m$prediction.error)

  m$performance$rmse <- root_mean_squared_error(
    o = observed,
    p = predicted,
    normalization = "rmse"
  )
  names(m$performance$rmse) <- NULL
  m$performance$nrmse <- root_mean_squared_error(
    o = observed,
    p = predicted,
    normalization = "iq"
  )
  names(m$performance$nrmse) <- NULL
  m$performance$auc <- NA

  #compute AUC
  m$performance$auc <- auc(
    o = observed,
    p = predicted
  )

  #residuals
  m$residuals$values <- observed - predicted
  m$residuals$stats <- summary(m$residuals$values)

  #compute moran I of residuals if distance.matrix is provided
  if(!is.null(distance.matrix)){

    m$residuals$autocorrelation <- moran_multithreshold(
      x = m$residuals$values,
      distance.matrix = distance.matrix,
      distance.thresholds = distance.thresholds,
      verbose = verbose
    )

  }

  #normality of the residuals
  m$residuals$normality <- residuals_diagnostics(
    residuals = m$residuals$values,
    predictions = predicted
    )

  #plot of the residuals diagnostics
  m$residuals$diagnostics <- plot_residuals_diagnostics(
    m,
    verbose = verbose
  )

  #adding cluster
  if(!is.null(cluster)){
    m$cluster <- cluster
  }

  #adding rf class
  class(m) <- c("rf", "ranger")

  if(verbose == TRUE){
    print(m)
  }

  #return model
  m

}
BlasBenito/spatialRF documentation built on Sept. 1, 2022, 1:42 p.m.