Nothing
#' @include FamiliarS4Generics.R
#' @include FamiliarS4Classes.R
NULL
# familiarXGBoost --------------------------------------------------------------
setClass(
"familiarXGBoost",
contains = "familiarModel",
slots = list(
"encoding_reference_table" = "ANY",
"feature_order" = "character"
),
prototype = list(
"encoding_reference_table" = NULL,
"feature_order" = character()
)
)
setClass("familiarXGBoostLM", contains = "familiarXGBoost")
setClass("familiarXGBoostTree", contains = "familiarXGBoost")
setClass("familiarXGBoostDart", contains = "familiarXGBoost")
# initialize -------------------------------------------------------------------
setMethod(
"initialize",
signature(.Object = "familiarXGBoost"),
function(.Object, ...) {
# Update with parent class first.
.Object <- callNextMethod()
# Set required package
.Object@package <- "xgboost"
return(.Object)
}
)
# is_available -----------------------------------------------------------------
setMethod(
"is_available",
signature(object = "familiarXGBoost"),
function(object, ...) {
# Extract learner and outcome_type from the familiarModel object.
outcome_type <- object@outcome_type
# Strip booster type from the learner.
learner <- sub_all_patterns(
x = object@learner,
pattern = c("xgboost_lm", "xgboost_tree", "xgboost_dart"),
replacement = "",
fixed = TRUE
)
if (startsWith(learner, "_")) {
learner <- sub(
x = learner,
pattern = "_",
replacement = "",
fixed = TRUE
)
}
if (
outcome_type == "continuous" &&
learner %in% c("", "gaussian", "gamma", "poisson")
) {
return(TRUE)
} else if (outcome_type == "multinomial" && learner %in% c("", "logistic")) {
return(TRUE)
} else if (outcome_type == "binomial" && learner %in% c("", "logistic")) {
return(TRUE)
} else if (outcome_type == "survival" && learner %in% c("", "cox")) {
return(TRUE)
} else if (outcome_type == "count" && learner %in% c("", "poisson", "gaussian")) {
..deprecation_count()
return(FALSE)
}
return(FALSE)
}
)
# get_default_hyperparameters --------------------------------------------------
setMethod(
"get_default_hyperparameters",
signature(object = "familiarXGBoost"),
function(
object,
data = NULL,
user_list = NULL,
...
) {
# Initialise list and declare hyperparameter entries
param <- list()
param$sign_size <- list()
param$learn_objective <- list()
# General parameters
param$n_boost <- list()
param$learning_rate <- list()
param$lambda <- list()
param$alpha <- list()
param$sample_weighting <- list()
param$sample_weighting_beta <- list()
# Tree specific parameters
if (is(object, "familiarXGBoostTree") || is(object, "familiarXGBoostDart")) {
param$tree_depth <- list()
param$sample_size <- list()
param$min_child_weight <- list()
param$gamma <- list()
}
# DART-specific parameters
if (is(object, "familiarXGBoostDart")) {
param$sample_type <- list()
param$rate_drop <- list()
}
# If data is explicitly NULL, return the list with hyperparameter names
# only.
if (is.null(data)) return(param)
# Extract outcome type.
outcome_type <- object@outcome_type
# Determine number of samples.
n_samples <- get_n_samples(data)
# signature size -----------------------------------------------------------
param$sign_size <- .get_default_sign_size(data = data)
# model objective ----------------------------------------------------------
# xgboost has several families, or rather objectives, that can be used.
# Read family string by parsing learner.
fam <- sub_all_patterns(
x = object@learner,
pattern = c("xgboost_lm", "xgboost_tree", "xgboost_dart"),
replacement = "",
fixed = TRUE
)
if (fam != "") {
fam <- sub(
x = fam,
pattern = "_",
replacement = "",
fixed = TRUE
)
}
# Define the objective based on the name of the learner.
if (fam == "") {
# No specific objective is provided.
if (outcome_type == "continuous") {
learn_objective_default <- c("gaussian", "gamma")
} else if (outcome_type == "binomial") {
learn_objective_default <- "binomial_logistic"
} else if (outcome_type == "multinomial") {
learn_objective_default <- "multinomial_logistic"
} else if (outcome_type == "survival") {
learn_objective_default <- "cox"
}
} else if (fam == "logistic") {
# Logistic learner objectives form a collection, and are now uniquely
# defined according to the type of outcome.
if (outcome_type == "binomial") {
learn_objective_default <- "binomial_logistic"
} else if (outcome_type == "multinomial") {
learn_objective_default <- "multinomial_logistic"
}
} else {
# Other objectives are uniquely defined.
learn_objective_default <- fam
}
# Set the learn_objective parameter
param$learn_objective <- .set_hyperparameter(
default = learn_objective_default,
type = "factor",
range = learn_objective_default,
randomise = length(learn_objective_default) > 1L
)
# number of boosting iterations --------------------------------------------
# This hyper-parameter is expressed on the log 10 scale. It is called
# nrounds in xgboost.
param$n_boost <- .set_hyperparameter(
default = c(0.0, 1.0, 2.0, 3.0),
type = "numeric",
range = c(0.0, 3.0),
valid_range = c(0.0, Inf),
randomise = TRUE
)
# learning rate ------------------------------------------------------------
# Learning rate is on a log10 scale from -5 to 0 (0.00001 - 1), and
# determines how fast the algorithm tries to learn. Lower values typically
# lead to better models, but take longer to learn. This parameter is called
# eta by xgboost.
param$learning_rate <- .set_hyperparameter(
default = c(-3.0, -2.0, -1.0),
type = "numeric",
range = c(-5.0, 0.0),
valid_range = c(-Inf, 0.0),
randomise = TRUE
)
# L2 regularisation term ---------------------------------------------------
# The L2 regularisation term lambda lies in the half-open range [0, inf).
# This term is implemented as a power(10) with a 10^-6 offset.
param$lambda <- .set_hyperparameter(
default = c(-6.0, -3.0, -1.0, 1.0, 3.0),
type = "numeric",
range = c(-6.0, 3.0),
valid_range = c(-6.0, Inf),
randomise = TRUE
)
# L1 regularisation term ---------------------------------------------------
# The L1 regularisation term alpha is implemented as the L2 regularisation
# term.
param$alpha <- .set_hyperparameter(
default = c(-6.0, -3.0, -1.0, 1.0, 3.0),
type = "numeric",
range = c(-6.0, 3.0),
valid_range = c(-6.0, Inf),
randomise = TRUE
)
# sample weighting method --------------------------------------------------
# Class imbalances may lead to learning majority classes. This can be
# partially mitigated by increasing weight of minority classes.
param$sample_weighting <- .get_default_sample_weighting_method(outcome_type = object@outcome_type)
# effective number of samples beta -----------------------------------------
# Specifies the beta parameter for effective number sample weighting method.
# See Cui et al. (2019).
param$sample_weighting_beta <- .get_default_sample_weighting_beta(
method = c(
param$sample_weighting$init_config,
user_list$sample_weighting
),
outcome_type = object@outcome_type
)
# Parameters for tree-based gradient boosting
if (is(object, "familiarXGBoostTree") || is(object, "familiarXGBoostDart")) {
# maximum tree depth -----------------------------------------------------
# This hyperparameter is only used by tree models. The parameter is called
# max_depth by xgboost. Larger depths increase the risk of overfitting.
param$tree_depth <- .set_hyperparameter(
default = c(1L, 2L, 3L, 7L),
type = "integer",
range = c(1L, 10L),
valid_range = c(1L, Inf),
randomise = TRUE
)
# data subsampling fraction ----------------------------------------------
# Trees may be grown using only a subset of the data to limit overfitting.
# The parameter is called subsample in xgboost.
param$sample_size <- .set_hyperparameter(
default = c(0.30, 0.50, 0.70, 1.00),
type = "numeric",
range = c(2.0 / n_samples, 1.0),
valid_range = c(0.0, 1.0),
randomise = TRUE
)
# minimum sum of instance weight -----------------------------------------
# Minimum sum of instance weight (hessian) needed in a child. If the tree
# partition step results in a leaf node with the sum of instance weight
# less than min_child_weight, then the building process will give up
# further partitioning. In linear regression mode, this simply corresponds
# to minimum number of instances needed to be in each node. The larger,
# the more conservative the algorithm will be. (source:xgboost
# documentation)
#
# We implement this on a power(10) scale, with -1 offset.
param$min_child_weight <- .set_hyperparameter(
default = c(0.0, 1.0, 2.0),
type = "numeric",
range = c(0.0, 2.0),
valid_range = c(0.0, Inf),
randomise = TRUE
)
# minimum splitting error reduction --------------------------------------
# Minimum error reduction required for splitting. This hyper-parameters is
# called gamma or min_split_loss in xgboost. We implement it on the
# power(10) scale, with 10^-6 offset.
#
# For continuous type outcomes, this parameter can be a bit tricky due to
# a wide range in possible scales, and thus in error values. This is
# resolved by normalising the outcome to the [0, 1] range.
param$gamma <- .set_hyperparameter(
default = c(-6.0, -3.0, -1.0, 1.0, 3.0),
type = "numeric",
range = c(-6.0, 3.0),
valid_range = c(-6.0, Inf),
randomise = TRUE
)
}
# Parameters for dart tree-based gradient boosting
if (is(object, "familiarXGBoostDart")) {
## dart booster sample type -----------------------------------------------
# Select the sample algorithm used by Dart booster.
param$sample_type <- .set_hyperparameter(
default = c("uniform", "weighted"),
type = "factor",
range = c("uniform", "weighted"),
randomise = TRUE
)
## dart booster tree drop rate --------------------------------------------
# Fraction of previous trees to drop during dropout.
param$rate_drop <- .set_hyperparameter(
default = c(0.0, 0.1, 0.3),
type = "numeric",
range = c(0.0, 1.0),
randomise = TRUE
)
}
# Return hyper-parameters
return(param)
}
)
# get_prediction_type ----------------------------------------------------------
setMethod(
"get_prediction_type",
signature(object = "familiarXGBoost"),
function(object, type = "default") {
if (object@outcome_type != "survival") return(callNextMethod())
# The prediction type is a bit more complicated for xgboost methods.
if (type == "default") {
if (as.character(object@hyperparameters$learn_objective %in% c("cox"))) {
return("hazard_ratio")
}
} else if (type == "survival_probability") {
return("survival_probability")
} else {
..error_reached_unreachable_code(paste0(
"get_prediction_type,familiarXGBoost: unknown type (", type,
") for the current objective (",
as.character(object@hyperparameters$learn_objective), ")."
))
}
}
)
# ..train ----------------------------------------------------------------------
setMethod(
"..train",
signature(
object = "familiarXGBoost",
data = "dataObject"
),
function(object, data, ...) {
# Suppress NOTES due to non-standard evaluation in data.table
outcome <- NULL
# Aggregate repeated measurement data - xgboost does not facilitate repeated
# measurements.
data <- aggregate_data(data = data)
# Check if training data is ok.
if (reason <- has_bad_training_data(object = object, data = data)) {
return(callNextMethod(object = .why_bad_training_data(
object = object,
reason = reason
)))
}
# Check if hyperparameters are set.
if (is.null(object@hyperparameters)) {
return(callNextMethod(object = ..update_errors(
object = object,
..error_message_no_optimised_hyperparameters_available()
)))
}
# Check that required packages are loaded and installed.
require_package(object, "train")
# Use effect coding to convert categorical data into encoded data - this is
# required to deal with factors with missing/new levels between training and
# test data sets.
encoded_data <- encode_categorical_variables(
data = data,
object = object,
encoding_method = "dummy",
drop_levels = FALSE
)
# Find feature columns in the data.
feature_columns <- get_feature_columns(x = encoded_data$encoded_data)
# Find outcome columns in data table.
outcome_columns <- get_outcome_columns(x = object)
# Set y (response variable)
if (object@outcome_type %in% c("binomial", "multinomial", "continuous")) {
y <- encoded_data$encoded_data@data[[outcome_columns]]
} else if (object@outcome_type == "survival") {
y <- survival::Surv(
encoded_data$encoded_data@data[[outcome_columns[1L]]],
encoded_data$encoded_data@data[[outcome_columns[2L]]]
)
}
# Set weights.
weights <- create_instance_weights(
data = encoded_data$encoded_data,
method = object@hyperparameters$sample_weighting,
beta = ..compute_effective_number_of_samples_beta(
object@hyperparameters$sample_weighting_beta
),
normalisation = "average_one"
)
# Create data object
x <- encoded_data$encoded_data@data[, mget(feature_columns)]
# Identify the booster to use.
if (is(object, "familiarXGBoostLM")) {
booster <- "gblinear"
} else if (is(object, "familiarXGBoostTree")) {
booster <- "gbtree"
} else if (is(object, "familiarXGBoostDart")) {
booster <- "dart"
} else {
..error_reached_unreachable_code(paste0(
"..train,familiarXGBoost: could not set booster for object of unknown class: ",
paste_s(class(object))
))
}
# Select shared arguments.
learner_arguments <- list(
"booster" = booster,
"nthreads" = 1L,
"learning_rate" = 10.0^object@hyperparameters$learning_rate,
"reg_lambda" = 10.0^object@hyperparameters$lambda - 10.0^-6.0,
"reg_alpha" = 10.0^object@hyperparameters$alpha - 10.0^-6.0,
"objective" = ..get_distribution_family(object),
"x" = x,
"y" = y,
"weight" = weights,
"nrounds" = round(10.0^object@hyperparameters$n_boost),
"verbosity" = 0L
)
if (is(object, "familiarXGBoostTree") || is(object, "familiarXGBoostDart")) {
learner_arguments <- c(
learner_arguments,
list(
"max_depth" = object@hyperparameters$tree_depth,
"subsample" = object@hyperparameters$sample_size,
"min_child_weight" = 10.0^object@hyperparameters$min_child_weight - 1.0,
"min_split_loss" = 10.0^object@hyperparameters$gamma - 10.0^-6.0
)
)
}
if (is(object, "familiarXGBoostDart")) {
learner_arguments <- c(
learner_arguments,
list(
"sample_type" = as.character(object@hyperparameters$sample_type),
"rate_drop" = object@hyperparameters$rate_drop
)
)
}
# Train the model.
model <- do.call_with_handlers(
xgboost::xgboost,
args = learner_arguments
)
# Extract values.
object <- ..update_warnings(object = object, model$warning)
object <- ..update_errors(object = object, model$error)
model <- model$value
# Check if the model trained at all.
if (!is.null(object@messages$error)) return(callNextMethod(object = object))
# Add model. Note that we do not use xgboost::xgb.save.raw to save raw model
# data, since this prevents the model being used for variable importance.
object@model <- model
# Add the contrast references to model_list
object@encoding_reference_table <- encoded_data$reference_table
# Add feature order
object@feature_order <- feature_columns
# Set learner version
object <- set_package_version(object)
return(object)
}
)
# ..train_naive ----------------------------------------------------------------
setMethod(
"..train_naive",
signature(
object = "familiarXGBoost",
data = "dataObject"
),
function(object, data, ...) {
if (object@outcome_type %in% c("continuous", "binomial", "multinomial")) {
# Turn into a Naive model.
object <- methods::new("familiarNaiveModel", object)
} else if (object@outcome_type %in% c("survival")) {
# Turn into a Naive Cox model.
object <- methods::new("familiarNaiveCoxModel", object)
}
return(..train(
object = object,
data = data,
...
))
}
)
# ..predict --------------------------------------------------------------------
setMethod(
"..predict",
signature(
object = "familiarXGBoost",
data = "dataObject"
),
function(
object,
data,
type = "default",
time = NULL,
...
) {
# Check that required packages are loaded and installed.
require_package(object, "predict")
# Check if the model was trained.
if (!model_is_trained(object)) return(callNextMethod())
# Check if the data is empty.
if (is_empty(data)) return(callNextMethod())
# Load model through unserialisation.
if (inherits(object@model, "raw")) {
object@model <- xgboost::xgb.load.raw(object@model)
}
# Encode data so that the features are the same as in the training.
encoded_data <- encode_categorical_variables(
data = data,
object = object,
encoding_method = "dummy",
drop_levels = FALSE
)
if (type == "default") {
# default ----------------------------------------------------------------
model_predictions <- predict(
object = object@model,
newdata = as.matrix(encoded_data$encoded_data@data[, mget(object@feature_order)])
)
if (object@outcome_type == "binomial") {
# binomial outcomes ----------------------------------------------------
# Obtain class levels.
class_levels <- get_outcome_class_levels(x = object)
# Add class probabilities (glm always gives probability for the second
# class).
prediction_list <- list()
prediction_list[[tail(class_levels, n = 1L)]] <- model_predictions
# Store as prediction table.
prediction_table <- as_prediction_table(
x = prediction_list,
type = "classification",
data = data,
model_object = object
)
} else if (object@outcome_type == "multinomial") {
# multinomial outcomes -------------------------------------------------
# Obtain class levels.
class_levels <- get_outcome_class_levels(x = object)
prediction_list <- list()
for (ii in seq_along(class_levels)) {
if (is.matrix(model_predictions)) {
prediction_list[[class_levels[ii]]] <- model_predictions[, ii]
} else {
prediction_list[[class_levels[ii]]] <- model_predictions[ii]
}
}
# Store as prediction table.
prediction_table <- as_prediction_table(
x = prediction_list,
type = "classification",
data = data,
model_object = object
)
} else if (object@outcome_type %in% c("continuous")) {
# Numerical outcomes ---------------------------------------------------
# Map predictions back to original scale.
model_predictions <- model_predictions
# Store as prediction table.
prediction_table <- as_prediction_table(
x = model_predictions,
type = "regression",
data = data,
model_object = object
)
} else if (object@outcome_type %in% c("survival")) {
# Survival outcomes ----------------------------------------------------
# Store as prediction table. Note that the raw prediction output needs
# to be recalibrated.
prediction_table <- as_prediction_table(
x = model_predictions,
type = "survival",
data = data,
model_object = object
)
} else {
..error_outcome_type_not_implemented(object@outcome_type)
}
return(prediction_table)
} else if (type == "survival_probability" && object@outcome_type == "survival") {
# Only compute for cox-like objectives.
if (!as.character(object@hyperparameters$learn_objective) %in% c("cox")) {
return(callNextMethod())
}
# If time is unset, read the max time stored by the model.
if (is.null(time)) time <- object@settings$time_max
return(.survival_probability_relative_risk(
object = object,
data = data,
time = time
))
} else if (!.is_available_prediction_type(type)) {
# user-specified method --------------------------------------------------
# Note that xgboost:::predict.xgb.Booster does not have a type argument.
return(predict(
object = object@model,
newdata = encoded_data$encoded_data@data[, mget(object@feature_order)],
...
))
} else {
..error_no_predictions_possible(object, type)
}
}
)
# ..get_prediction_table_type --------------------------------------------------
setMethod(
"..get_prediction_table_type",
signature(object = "familiarXGBoost"),
function(object, type, ...) {
prediction_table_type <- NULL
if (object@outcome_type %in% c("survival") && type == "default") {
prediction_table_type <- "survival"
} else {
prediction_table_type <- callNextMethod()
}
return(prediction_table_type)
}
)
# ..vimp -----------------------------------------------------------------------
setMethod(
"..vimp",
signature(object = "familiarXGBoost"),
function(object, ...) {
# Suppress NOTES due to non-standard evaluation in data.table
Weight <- NULL
# Check if the model has been trained upon retry.
if (!model_is_trained(object)) return(callNextMethod())
# Check that required packages are loaded and installed.
require_package(object, "vimp")
# Load model through unserialisation. Note that this currently does not
# produce a model that can be used to infer variable importance.
if (inherits(object@model, "raw")) {
object@model <- xgboost::xgb.load.raw(object@model)
}
# Use xgboost::xgb.importance function from xgboost to extract a
# data.table.
xgboost_score <- tryCatch(
xgboost::xgb.importance(model = object@model),
error = identity
)
if (inherits(xgboost_score, "error")) return(callNextMethod())
if (is(object, "familiarXGBoostTree") || is(object, "familiarXGBoostDart")) {
# Process to variable importance table.
vimp_table <- data.table::data.table(
"score" = xgboost_score$Gain,
"name" = xgboost_score$Feature
)
} else if (is(object, "familiarXGBoostLM")) {
# Output are linear coefficients, which may be negative. We keep
# the maximum weight of each feature.
xgboost_score <- xgboost_score[, list(Weight = max(abs(Weight))), by = "Feature"]
# Parse score to data.table
vimp_table <- data.table::data.table(
"score" = xgboost_score$Weight,
"name" = xgboost_score$Feature
)
} else {
..error_reached_unreachable_code(paste0(
"..vimp,familiarXGBoost: could not process vimp for object of unknown class: ",
paste_s(class(object))
))
}
# Create variable importance object.
vimp_object <- methods::new("vimpTable",
vimp_table = vimp_table,
encoding_table = object@encoding_reference_table,
score_aggregation = "max",
invert = TRUE
)
return(vimp_object)
}
)
# ..set_calibration_info -------------------------------------------------------
setMethod(
"..set_calibration_info",
signature(object = "familiarXGBoost"),
function(object, data) {
# Check if calibration info already.
if (has_calibration_info(object)) return(object)
if (object@outcome_type == "survival") {
# Determine baseline survival.
object@calibration_info <- get_baseline_survival(data = data)
} else {
return(callNextMethod())
}
return(object)
}
)
# ..get_distribution_family ----------------------------------------------------
setMethod(
"..get_distribution_family",
signature(object = "familiarXGBoost"),
function(object) {
# Obtain family from the hyperparameters.
objective <- object@hyperparameters$learn_objective
# Check that the learn_objective hyperparameter exists.
if (!is.character(objective) && !is.factor(objective)) {
..error_reached_unreachable_code(paste0(
"..get_distribution_family,familiarXGBoost: learn_objective ",
"hyperparameter was not set."
))
}
# Load objective for extreme gradient boosting.
if (objective == "gaussian") {
boost_objective <- "reg:squarederror"
} else if (objective == "multinomial_logistic") {
boost_objective <- "multi:softprob"
} else if (objective == "binomial_logistic") {
boost_objective <- "binary:logistic"
} else if (objective == "poisson") {
boost_objective <- "count:poisson"
} else if (objective == "gamma") {
boost_objective <- "reg:gamma"
} else if (objective == "cox") {
boost_objective <- "survival:cox"
} else {
..error_reached_unreachable_code(paste0(
"..get_distribution_family,familiarXGBoost: unknown learn objective: ",
objective
))
}
return(boost_objective)
}
)
# ..set_recalibration_model ----------------------------------------------------
setMethod(
"..set_recalibration_model",
signature(
object = "familiarXGBoost",
data = "dataObject"
),
function(object, data, time = NULL) {
# Recalibration is performed using standard methods
if (object@outcome_type %in% c("survival")) {
# Calibrate the models.
object@calibration_model <- .set_recalibration(
object = object,
data = data,
time = time
)
# Return object.
return(object)
} else {
return(callNextMethod())
}
}
)
# .trim_model ------------------------------------------------------------------
setMethod(
".trim_model",
signature(object = "familiarXGBoost"),
function(object, ...) {
# Set is_trimmed to TRUE.
object@is_trimmed <- TRUE
# Prevent trimming of raw, serialised model information.
if (inherits(object@model, "raw")) return(object)
# Add show.
object <- .capture_show(object)
# Default method for models that lack a more specific method.
return(object)
}
)
.get_available_xgboost_lm_learners <- function(show_general = TRUE) {
# Learners
learners <- c(
"xgboost_lm", "xgboost_lm_logistic", "xgboost_lm_gaussian",
"xgboost_lm_poisson", "xgboost_lm_gamma", "xgboost_lm_cox"
)
if (!show_general) {
learners <- setdiff(learners, c("xgboost_lm"))
}
return(learners)
}
.get_available_xgboost_tree_learners <- function(show_general = TRUE) {
# Learners
learners <- c(
"xgboost_tree", "xgboost_tree_logistic", "xgboost_tree_gaussian",
"xgboost_tree_poisson", "xgboost_tree_gamma", "xgboost_tree_cox"
)
if (!show_general) {
learners <- setdiff(learners, c("xgboost_tree"))
}
return(learners)
}
.get_available_xgboost_dart_learners <- function(show_general = TRUE) {
# Learners
learners <- c(
"xgboost_dart", "xgboost_dart_logistic", "xgboost_dart_gaussian",
"xgboost_dart_poisson", "xgboost_dart_gamma", "xgboost_dart_cox"
)
if (!show_general) {
learners <- setdiff(learners, c("xgboost_dart"))
}
return(learners)
}
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.