diff --git a/DESCRIPTION b/DESCRIPTION index 77257df..5571ec9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -38,7 +38,7 @@ Suggests: ISLR2, knitr, patchwork, - randomPlantedForest, + randomPlantedForest (>= 0.3.0), ranger, rmarkdown, testthat (>= 3.0.0), @@ -51,7 +51,7 @@ Remotes: PlantedML/randomPlantedForest Additional_repositories: https://plantedml.r-universe.dev Config/roxygen2/version: 8.0.0 +Config/roxygen2/markdown: TRUE Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true -Roxygen: list(markdown = TRUE) diff --git a/NEWS.md b/NEWS.md index 8bc516b..6f50144 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # glex 0.6.0.9000 (development version) +* `randomPlantedForest (>= 0.3.0)` is now required (in `Suggests:`): it fixes an + out-of-bounds read in `purify_3()` that crashed R on Windows + (PlantedML/randomPlantedForest#61), so rpf tests and examples run on all platforms. + * `$shap` is now a scalar `NA` (with a warning) when the decomposition is constrained via `max_interaction` or `features`: a constrained decomposition does not sum to the full model prediction, so SHAP values cannot be reconstructed from it without diff --git a/R/autoplot.R b/R/autoplot.R index b7924fe..eb7020c 100644 --- a/R/autoplot.R +++ b/R/autoplot.R @@ -35,7 +35,7 @@ autoplot.glex <- function(object, predictors, ...) { #' `"Remaining terms"` label. #' @param ... (Unused) #' -#' @return A [`ggplot`][ggplot2::ggplot] object. +#' @returns A [`ggplot`][ggplot2::ggplot] object. #' @export #' @seealso [glex_vi] #' @family Visualization functions diff --git a/R/data_bike.R b/R/data_bike.R index d78c0a6..5bafc82 100644 --- a/R/data_bike.R +++ b/R/data_bike.R @@ -3,7 +3,7 @@ #' A reduced version of the `Bikeshare` data as included with `ISLR2`. #' The dataset has been converted to a [data.table::data.table()], with the following changes: #' -#' - `hr` has been copnverted to a numeric +#' - `hr` has been converted to a numeric #' - `workingday` was recoded to a binary `factor` with labels `c("No Workingday", "Workingday")` #' - `season` was recoded to a `factor` with labels `c("Winter", "Spring", "Summer", "Fall")` #' - Variables `atemp`, `day`, `registered` and `casual` were removed diff --git a/R/glex-options.R b/R/glex-options.R index 5be615d..0d768c6 100644 --- a/R/glex-options.R +++ b/R/glex-options.R @@ -3,28 +3,26 @@ #' The color choices used across all `glex` visualization functions can be #' adjusted globally via [options()]: #' -#' \describe{ -#' \item{`glex.palette`}{(`NULL`) Diverging palette used to color continuous -#' interaction effects in [plot_twoway_effects()] and -#' [plot_threeway_effects()]. The default `NULL` uses a blue/red gradient -#' built from `glex.colors_sign`, matching the look of the Python `shap` -#' and `shapiq` packages. Set to the name of a diverging -#' [scico][scico::scico] palette (e.g. `"vik"`, `"roma"`) to use that -#' instead.} -#' \item{`glex.palette_discrete`}{(`"Dark2"`) Discrete palette used to color -#' categorical predictors in interaction plots. Accepts a vector of colors -#' (used via [ggplot2::scale_color_manual()]), the string `"okabe-ito"` -#' (the colorblind-safe Okabe-Ito palette via [grDevices::palette.colors()]), -#' the name of a [scico][scico::scico] palette, or the name of an -#' [RColorBrewer][ggplot2::scale_color_brewer] palette.} -#' \item{`glex.colors_sign`}{(`c("#008BFB", "#FF0051")`) Two colors for -#' negative and positive contributions in [glex_explain()], also used as -#' the endpoints of the default continuous gradient. The defaults -#' follow the blue/red convention familiar from the Python `shap` and -#' `shapiq` packages.} -#' \item{`glex.color_line`}{(`"#194155"`) Color for main effect lines and -#' columns drawn by `autoplot()` and [plot_pdp()].} -#' } +#' * `glex.palette` (`NULL`): Diverging palette used to color continuous +#' interaction effects in [plot_twoway_effects()] and +#' [plot_threeway_effects()]. The default `NULL` uses a blue/red gradient +#' built from `glex.colors_sign`, matching the look of the Python `shap` +#' and `shapiq` packages. Set to the name of a diverging +#' [scico][scico::scico] palette (e.g. `"vik"`, `"roma"`) to use that +#' instead. +#' * `glex.palette_discrete` (`"Dark2"`): Discrete palette used to color +#' categorical predictors in interaction plots. Accepts a vector of colors +#' (used via [ggplot2::scale_color_manual()]), the string `"okabe-ito"` +#' (the colorblind-safe Okabe-Ito palette via [grDevices::palette.colors()]), +#' the name of a [scico][scico::scico] palette, or the name of an +#' [RColorBrewer][ggplot2::scale_color_brewer] palette. +#' * `glex.colors_sign` (`c("#008BFB", "#FF0051")`): Two colors for +#' negative and positive contributions in [glex_explain()], also used as +#' the endpoints of the default continuous gradient. The defaults +#' follow the blue/red convention familiar from the Python `shap` and +#' `shapiq` packages. +#' * `glex.color_line` (`"#194155"`): Color for main effect lines and +#' columns drawn by `autoplot()` and [plot_pdp()]. #' #' @examples #' # Use a scico palette for continuous effects instead of the default gradient diff --git a/R/glex.R b/R/glex.R index 3e692ad..1e6146d 100644 --- a/R/glex.R +++ b/R/glex.R @@ -15,10 +15,10 @@ #' Defaults to using all possible interactions available in the model.\cr #' For [`xgboost`][xgboost::xgb.train], this defaults to the `max_depth` parameter of the model fit.\cr #' If not set in `xgboost`, the default value of `6` is assumed. -#' @param features Vector of column names in x to calculate components for. Default is \code{NULL}, i.e. all features are used. +#' @param features Vector of column names in `x` to calculate components for. Default is `NULL`, i.e. all features are used. #' @param ... Further arguments passed to methods. #' -#' @return Decomposition of the regression or classification function. +#' @returns Decomposition of the regression or classification function. #' A `list` with elements: #' * `shap`: SHAP values, derived from the functional decomposition as #' \eqn{\phi_j = \sum_{S \ni j} m_S / |S|}. This reconstruction is only valid if the @@ -71,17 +71,15 @@ glex.default <- function(object, ...) { #' @rdname glex #' @export -#' @examples +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' #' # Random Planted Forest ----- -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { #' library(randomPlantedForest) #' #' rp <- rpf(mpg ~ ., data = mtcars[1:26, ], max_interaction = 2) #' #' glex_rpf <- glex(rp, mtcars[27:32, ]) #' str(glex_rpf, list.len = 5) -#' } glex.rpf <- function(object, x, max_interaction = NULL, features = NULL, ...) { if (!requireNamespace("randomPlantedForest", quietly = TRUE)) { stop(paste0( @@ -168,15 +166,15 @@ glex.rpf <- function(object, x, max_interaction = NULL, features = NULL, ...) { #' #' @param max_background_sample_size The maximum number of background samples used for the FastPD algorithm, only used when `weighting_method = "fastpd"`. Defaults to `nrow(x)`. #' @param weighting_method Use either "path-dependent", "fastpd" (default), or "empirical". See References for details. -#' @examples +#' @examplesIf requireNamespace("xgboost", quietly = TRUE) #' # xgboost ----- -#' if (requireNamespace("xgboost", quietly = TRUE)) { #' library(xgboost) #' x <- as.matrix(mtcars[, -1]) #' y <- mtcars$mpg #' xg <- xgboost(x[1:26, ], y[1:26], -#' max_depth = 4, learning_rate = .1, -#' nrounds = 10, verbosity = 0, nthreads = 1) +#' max_depth = 4, learning_rate = .1, +#' nrounds = 10, verbosity = 0, nthreads = 1 +#' ) #' glex(xg, x[27:32, ]) #' glex(xg, mtcars[27:32, ]) #' @@ -185,7 +183,6 @@ glex.rpf <- function(object, x, max_interaction = NULL, features = NULL, ...) { #' doParallel::registerDoParallel() #' glex(xg, x[27:32, ]) #' } -#' } glex.xgb.Booster <- function( object, x, @@ -365,20 +362,24 @@ get_xgb_base_score <- function(object) { #' @importFrom stats predict #' @importFrom utils combn #' @details -#' The different weighting methods are described in detail in Liu et al. (2024). The default method is "fastpd" as it consistently estimates the correct partial dependence function. +#' The different weighting methods are described in detail in Liu et al. (2025). The default +#' method is `"fastpd"` as it consistently estimates the correct partial dependence function. #' @references -#' Liu, J., Steensgaard, T., Wright, M. N., Pfister, N., & Hiabu, M. (2024). -#' \emph{Fast Estimation of Partial Dependence Functions using Trees}. -#' arXiv preprint \href{https://arxiv.org/abs/2410.13448}{arXiv:2410.13448}. -#' @examples +#' Liu, J., Steensgaard, T., Wright, M. N., Pfister, N., & Hiabu, M. (2025). +#' *Fast Estimation of Partial Dependence Functions using Trees*. +#' Proceedings of the 42nd International Conference on Machine Learning, PMLR 267:39496-39534. +#' [PMLR](https://proceedings.mlr.press/v267/liu25bm.html) | +#' [arXiv:2410.13448](https://arxiv.org/abs/2410.13448) +#' @examplesIf requireNamespace("ranger", quietly = TRUE) #' # ranger ----- -#' if (requireNamespace("ranger", quietly = TRUE)) { #' library(ranger) #' x <- as.matrix(mtcars[, -1]) #' y <- mtcars$mpg -#' rf <- ranger(x = x[1:26, ], y = y[1:26], -#' num.trees = 5, max.depth = 3, -#' node.stats = TRUE) +#' rf <- ranger( +#' x = x[1:26, ], y = y[1:26], +#' num.trees = 5, max.depth = 3, +#' node.stats = TRUE +#' ) #' glex(rf, x[27:32, ]) #' glex(rf, mtcars[27:32, ]) #' @@ -387,7 +388,6 @@ get_xgb_base_score <- function(object) { #' doParallel::registerDoParallel() #' glex(rf, x[27:32, ]) #' } -#' } glex.ranger <- function( object, x, @@ -838,7 +838,7 @@ max_order <- function(trees) { #' @param trees data.table #' @param x observerations, matrix like data-structure #' @param all_S all combinations of interactions up to certain order -#' @param weighting_method the weighting method that was supplied to \code{glex} +#' @param weighting_method the weighting method that was supplied to `glex` #' @keywords internal #' @noRd tree_fun_wrapper <- function( diff --git a/R/glex_explain.R b/R/glex_explain.R index d3e7724..09392af 100644 --- a/R/glex_explain.R +++ b/R/glex_explain.R @@ -18,7 +18,7 @@ #' Preferred value may depend on the number of vertical elements, hence it may be necessary to adjust #' this value as needed. #' -#' @return A [ggplot][ggplot2::ggplot] object. +#' @returns A [ggplot][ggplot2::ggplot] object. # Invisibly: A `list` with elements # * `components`: A [`data.table`] of the prediction components scaled by their degree of interaction, # grouped by their associated reference term. @@ -30,10 +30,9 @@ #' @export #' @family Visualization functions #' -#' @examples +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' set.seed(1) #' # Random Planted Forest ----- -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { #' library(randomPlantedForest) #' #' rp <- rpf(mpg ~ ., data = mtcars[1:26, ], max_interaction = 2) @@ -41,7 +40,6 @@ #' glex_rpf <- glex(rp, mtcars[27:32, ]) #' #' glex_explain(glex_rpf, id = 3, predictors = "hp", threshold = 0.01) -#' } glex_explain <- function( object, id, diff --git a/R/glex_vi.R b/R/glex_vi.R index eba5f11..72d13c0 100644 --- a/R/glex_vi.R +++ b/R/glex_vi.R @@ -3,7 +3,7 @@ #' @param object Object of class `glex`. #' @param ... (Unused) #' -#' @return A [data.table::data.table()] with columns: +#' @returns A [data.table::data.table()] with columns: #' * `degree` (`integer`): Degree of interaction of the `term`, with `1` being main effects, #' `2` being 2-degree interactions etc. #' * `term` (`character`): Model term, e.g. main effect `x1` or interaction term `x1:x2`, `x1:x3:x5` etc. @@ -24,24 +24,23 @@ #' \deqn{\mathtt{m\_rel} = \frac{\mathtt{m}}{m_0}} #' #' @seealso [autoplot.glex_vi] -#' @examples +#' @examplesIf requireNamespace("xgboost", quietly = TRUE) #' set.seed(1) #' #' # xgboost ----- -#' if (requireNamespace("xgboost", quietly = TRUE)) { #' library(xgboost) #' x <- as.matrix(mtcars[, -1]) #' y <- mtcars$mpg #' xg <- xgboost(x[1:26, ], y[1:26], -#' max_depth = 4, learning_rate = .1, -#' nrounds = 10, verbosity = 0, nthreads = 1) +#' max_depth = 4, learning_rate = .1, +#' nrounds = 10, verbosity = 0, nthreads = 1 +#' ) #' glex_xgb <- glex(xg, x[27:32, ]) #' vi_xgb <- glex_vi(glex_xgb) #' #' library(ggplot2) #' autoplot(vi_xgb) #' autoplot(vi_xgb, by_degree = TRUE) -#' } glex_vi <- function(object, ...) { checkmate::assert_class(object, classes = "glex") diff --git a/R/plot_main_effect.R b/R/plot_main_effect.R index 1d265e4..bb590f6 100644 --- a/R/plot_main_effect.R +++ b/R/plot_main_effect.R @@ -11,13 +11,12 @@ #' Default is `"b"` for both sides. Set to `"none"` to disable rug plot. #' @param ... Used for future expansion. #' -#' @return A `ggplot2` object. +#' @returns A `ggplot2` object. #' @import ggplot2 #' @export #' @seealso [plot_pdp()] #' -#' @examples -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # introduce factor variables to show categorical feature handling @@ -32,7 +31,6 @@ #' # Main effects ---- #' plot_main_effect(components, "wt") #' plot_main_effect(components, "cyl") -#' } plot_main_effect <- function(object, predictor, rug_sides = "b", ...) { plot_main_effect_impl(object, predictor, pdp = FALSE, ...) } @@ -46,13 +44,12 @@ plot_main_effect <- function(object, predictor, rug_sides = "b", ...) { #' @param predictor `(character(1))` predictor names, e.g. `"x1"` to plot #' main effect of `x1`. #' -#' @return A `ggplot2` object. +#' @returns A `ggplot2` object. #' @import ggplot2 #' @export #' @seealso [plot_main_effect()] #' @family Visualization functions -#' @examples -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # introduce factor variables to show categorical feature handling @@ -66,7 +63,6 @@ plot_main_effect <- function(object, predictor, rug_sides = "b", ...) { #' #' plot_pdp(components, "wt") #' plot_pdp(components, "cyl") -#' } plot_pdp <- function(object, predictor, rug_sides = "b", ...) { plot_main_effect_impl(object, predictor, pdp = TRUE) } diff --git a/R/plot_twoway_effects.R b/R/plot_twoway_effects.R index 3781805..5fc1e8c 100644 --- a/R/plot_twoway_effects.R +++ b/R/plot_twoway_effects.R @@ -1,7 +1,6 @@ #' @rdname plot_components #' @export -#' @examples -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # 2-degree interaction effects ---- @@ -16,7 +15,6 @@ #' # 2d categorical, heatmap of arbitrary orientation #' plot_twoway_effects(components, c("vs", "cyl")) #' plot_twoway_effects(components, c("cyl", "vs")) -#' } plot_twoway_effects <- function(object, predictors, rug_sides = "b", ...) { checkmate::assert_class(object, "glex") checkmate::assert_character(predictors, len = 2, unique = TRUE) diff --git a/R/print.R b/R/print.R index d866226..dbc7463 100644 --- a/R/print.R +++ b/R/print.R @@ -8,14 +8,12 @@ #' #' @export #' @importFrom utils str -#' @examples +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' # Random Planted Forest ----- -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { #' library(randomPlantedForest) #' rp <- rpf(mpg ~ hp + wt + drat, data = mtcars[1:26, ], max_interaction = 2) #' #' glex(rp, mtcars[27:32, ]) -#' } print.glex <- function(x, ...) { n <- nrow(x$x) n_m <- ncol(x$m) diff --git a/R/theme_glex.R b/R/theme_glex.R index f36e3e2..7f23b86 100644 --- a/R/theme_glex.R +++ b/R/theme_glex.R @@ -8,7 +8,7 @@ #' @param grid_x (`TRUE`) Display horizontal grid lines? #' @param grid_y (`FALSE`) Display vertical grid lines? #' -#' @return A `ggplot2` theme object +#' @returns A `ggplot2` theme object #' @export #' @import ggplot2 #' @examples diff --git a/R/utils-components.R b/R/utils-components.R index 920abe9..d798e92 100644 --- a/R/utils-components.R +++ b/R/utils-components.R @@ -3,14 +3,13 @@ #' @param components An object of class `glex`. #' @param term (`character(1)`) A main term name to subset by, e.g. `"x1"`. #' -#' @return -#' - `subset_components`: An object of class `glex`. -#' - `subset_component_names`: A character vector. +#' @returns +#' - `subset_components()`: An object of class `glex`. +#' - `subset_component_names()`: A character vector. #' #' @rdname subset_components #' @export -#' @examples -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # introduce factor variables to show categorical feature handling @@ -26,7 +25,6 @@ #' subset_components(components, "hp") #' #' subset_component_names(components, "hp") -#' } #' subset_components <- function(components, term) { checkmate::assert_string(term) diff --git a/air.toml b/air.toml index 2a2578a..829d478 100644 --- a/air.toml +++ b/air.toml @@ -6,3 +6,4 @@ line-ending = "lf" persistent-line-breaks = true # preserve existing line breaks when reformatting default-exclude = true # use built-in default exclusions assignment-style = "arrow" +exclude = ["attic/", ".git/"] diff --git a/data-raw/bike_xg.R b/data-raw/bike_xg.R index 0bae5cd..3d424bb 100644 --- a/data-raw/bike_xg.R +++ b/data-raw/bike_xg.R @@ -6,23 +6,25 @@ bike[, `:=`(mnth = as.integer(mnth), workingday = as.integer(workingday) - 1)] # Based on mlr3pipelines PipeOpEncode # https://github.com/mlr-org/mlr3pipelines/blob/master/R/PipeOpEncode.R -onehot_encode = function(x) { +onehot_encode <- function(x) { # Ensuring we have a data.table, even if it's a matrix for some reason - x = data.table::as.data.table(x) - x_names = colnames(x) - to_encode = x_names[vapply( + x <- data.table::as.data.table(x) + x_names <- colnames(x) + to_encode <- x_names[vapply( x_names, \(x) inherits(x[[x]], c("character", "factor")), logical(1) )] # If no categorical features are found, return input (as DT though) - if (length(to_encode) == 0) return(x) + if (length(to_encode) == 0) { + return(x) + } - contrast_list = sapply( + contrast_list <- sapply( to_encode, \(column_name) { - levels_in = unique(x[[column_name]]) + levels_in <- unique(x[[column_name]]) stats::contr.treatment(levels_in, contrasts = FALSE) }, @@ -30,18 +32,18 @@ onehot_encode = function(x) { USE.NAMES = TRUE ) - cols_encoded = sapply( + cols_encoded <- sapply( to_encode, \(column_name) { - x = as.character(x[[column_name]]) - current_contrasts = contrast_list[[column_name]] + x <- as.character(x[[column_name]]) + current_contrasts <- contrast_list[[column_name]] current_contrasts[match(x, rownames(current_contrasts)), , drop = FALSE] }, simplify = FALSE, USE.NAMES = TRUE ) - cols_encoded = data.table::as.data.table(cols_encoded) + cols_encoded <- data.table::as.data.table(cols_encoded) data.table::setnames( cols_encoded, names(cols_encoded), @@ -49,24 +51,26 @@ onehot_encode = function(x) { ) # Column rekajiggering would fail if input was single-column - if (length(x_names) == 1) return(cols_encoded) + if (length(x_names) == 1) { + return(cols_encoded) + } # Bind original data sans recodable variable with newly created ones cbind(x[, setdiff(x_names, to_encode), with = FALSE], cols_encoded) } -bmat = as.matrix(bike)[, "season", drop = FALSE] +bmat <- as.matrix(bike)[, "season", drop = FALSE] as.data.table(bmat) |> onehot_encode() -bike_enc = onehot_encode(bike) +bike_enc <- onehot_encode(bike) setcolorder( bike_enc, neworder = c("bikers", setdiff(names(bike_enc), "bikers")) ) -bike_xgb = list( +bike_xgb <- list( x = as.matrix(bike_enc[, -1]), label = bike_enc$bikers ) diff --git a/man/bike.Rd b/man/bike.Rd index e03c5ef..e3986c9 100644 --- a/man/bike.Rd +++ b/man/bike.Rd @@ -19,7 +19,7 @@ The dataset has been converted to a \code{\link[data.table:data.table]{data.tabl } \details{ \itemize{ -\item \code{hr} has been copnverted to a numeric +\item \code{hr} has been converted to a numeric \item \code{workingday} was recoded to a binary \code{factor} with labels \code{c("No Workingday", "Workingday")} \item \code{season} was recoded to a \code{factor} with labels \code{c("Winter", "Spring", "Summer", "Fall")} \item Variables \code{atemp}, \code{day}, \code{registered} and \code{casual} were removed diff --git a/man/glex.Rd b/man/glex.Rd index 8ab2d05..e596fe2 100644 --- a/man/glex.Rd +++ b/man/glex.Rd @@ -45,7 +45,7 @@ Defaults to using all possible interactions available in the model.\cr For \code{\link[xgboost:xgb.train]{xgboost}}, this defaults to the \code{max_depth} parameter of the model fit.\cr If not set in \code{xgboost}, the default value of \code{6} is assumed.} -\item{features}{Vector of column names in x to calculate components for. Default is \code{NULL}, i.e. all features are used.} +\item{features}{Vector of column names in \code{x} to calculate components for. Default is \code{NULL}, i.e. all features are used.} \item{...}{Further arguments passed to methods.} @@ -102,27 +102,29 @@ q-interaction SHAP for all values of q for tree-based models such as xgboost. For parallel execution using \code{xgboost} models, register a backend, e.g. with \code{doParallel::registerDoParallel()}. -The different weighting methods are described in detail in Liu et al. (2024). The default method is "fastpd" as it consistently estimates the correct partial dependence function. +The different weighting methods are described in detail in Liu et al. (2025). The default +method is \code{"fastpd"} as it consistently estimates the correct partial dependence function. } \examples{ +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} # Random Planted Forest ----- -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { library(randomPlantedForest) rp <- rpf(mpg ~ ., data = mtcars[1:26, ], max_interaction = 2) glex_rpf <- glex(rp, mtcars[27:32, ]) str(glex_rpf, list.len = 5) -} +\dontshow{\}) # examplesIf} +\dontshow{if (requireNamespace("xgboost", quietly = TRUE)) withAutoprint(\{ # examplesIf} # xgboost ----- -if (requireNamespace("xgboost", quietly = TRUE)) { library(xgboost) x <- as.matrix(mtcars[, -1]) y <- mtcars$mpg xg <- xgboost(x[1:26, ], y[1:26], - max_depth = 4, learning_rate = .1, - nrounds = 10, verbosity = 0, nthreads = 1) + max_depth = 4, learning_rate = .1, + nrounds = 10, verbosity = 0, nthreads = 1 +) glex(xg, x[27:32, ]) glex(xg, mtcars[27:32, ]) @@ -131,15 +133,17 @@ glex(xg, mtcars[27:32, ]) doParallel::registerDoParallel() glex(xg, x[27:32, ]) } -} +\dontshow{\}) # examplesIf} +\dontshow{if (requireNamespace("ranger", quietly = TRUE)) withAutoprint(\{ # examplesIf} # ranger ----- -if (requireNamespace("ranger", quietly = TRUE)) { library(ranger) x <- as.matrix(mtcars[, -1]) y <- mtcars$mpg -rf <- ranger(x = x[1:26, ], y = y[1:26], - num.trees = 5, max.depth = 3, - node.stats = TRUE) +rf <- ranger( + x = x[1:26, ], y = y[1:26], + num.trees = 5, max.depth = 3, + node.stats = TRUE +) glex(rf, x[27:32, ]) glex(rf, mtcars[27:32, ]) @@ -148,10 +152,12 @@ glex(rf, mtcars[27:32, ]) doParallel::registerDoParallel() glex(rf, x[27:32, ]) } -} +\dontshow{\}) # examplesIf} } \references{ -Liu, J., Steensgaard, T., Wright, M. N., Pfister, N., & Hiabu, M. (2024). +Liu, J., Steensgaard, T., Wright, M. N., Pfister, N., & Hiabu, M. (2025). \emph{Fast Estimation of Partial Dependence Functions using Trees}. -arXiv preprint \href{https://arxiv.org/abs/2410.13448}{arXiv:2410.13448}. +Proceedings of the 42nd International Conference on Machine Learning, PMLR 267:39496-39534. +\href{https://proceedings.mlr.press/v267/liu25bm.html}{PMLR} | +\href{https://arxiv.org/abs/2410.13448}{arXiv:2410.13448} } diff --git a/man/glex_explain.Rd b/man/glex_explain.Rd index 44c03a3..bcdf41e 100644 --- a/man/glex_explain.Rd +++ b/man/glex_explain.Rd @@ -44,9 +44,9 @@ recommended to use \code{predictors}, \code{max_interaction}, or \code{threshold elements in the plot. } \examples{ +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} set.seed(1) # Random Planted Forest ----- -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { library(randomPlantedForest) rp <- rpf(mpg ~ ., data = mtcars[1:26, ], max_interaction = 2) @@ -54,7 +54,7 @@ rp <- rpf(mpg ~ ., data = mtcars[1:26, ], max_interaction = 2) glex_rpf <- glex(rp, mtcars[27:32, ]) glex_explain(glex_rpf, id = 3, predictors = "hp", threshold = 0.01) -} +\dontshow{\}) # examplesIf} } \seealso{ Other Visualization functions: diff --git a/man/glex_options.Rd b/man/glex_options.Rd index 4c7fbcb..1aaba31 100644 --- a/man/glex_options.Rd +++ b/man/glex_options.Rd @@ -8,27 +8,27 @@ The color choices used across all \code{glex} visualization functions can be adjusted globally via \code{\link[=options]{options()}}: } \details{ -\describe{ -\item{\code{glex.palette}}{(\code{NULL}) Diverging palette used to color continuous +\itemize{ +\item \code{glex.palette} (\code{NULL}): Diverging palette used to color continuous interaction effects in \code{\link[=plot_twoway_effects]{plot_twoway_effects()}} and \code{\link[=plot_threeway_effects]{plot_threeway_effects()}}. The default \code{NULL} uses a blue/red gradient built from \code{glex.colors_sign}, matching the look of the Python \code{shap} and \code{shapiq} packages. Set to the name of a diverging \link[scico:scico]{scico} palette (e.g. \code{"vik"}, \code{"roma"}) to use that -instead.} -\item{\code{glex.palette_discrete}}{(\code{"Dark2"}) Discrete palette used to color +instead. +\item \code{glex.palette_discrete} (\code{"Dark2"}): Discrete palette used to color categorical predictors in interaction plots. Accepts a vector of colors (used via \code{\link[ggplot2:scale_color_manual]{ggplot2::scale_color_manual()}}), the string \code{"okabe-ito"} (the colorblind-safe Okabe-Ito palette via \code{\link[grDevices:palette.colors]{grDevices::palette.colors()}}), the name of a \link[scico:scico]{scico} palette, or the name of an -\link[ggplot2:scale_color_brewer]{RColorBrewer} palette.} -\item{\code{glex.colors_sign}}{(\code{c("#008BFB", "#FF0051")}) Two colors for +\link[ggplot2:scale_color_brewer]{RColorBrewer} palette. +\item \code{glex.colors_sign} (\code{c("#008BFB", "#FF0051")}): Two colors for negative and positive contributions in \code{\link[=glex_explain]{glex_explain()}}, also used as the endpoints of the default continuous gradient. The defaults follow the blue/red convention familiar from the Python \code{shap} and -\code{shapiq} packages.} -\item{\code{glex.color_line}}{(\code{"#194155"}) Color for main effect lines and -columns drawn by \code{autoplot()} and \code{\link[=plot_pdp]{plot_pdp()}}.} +\code{shapiq} packages. +\item \code{glex.color_line} (\code{"#194155"}): Color for main effect lines and +columns drawn by \code{autoplot()} and \code{\link[=plot_pdp]{plot_pdp()}}. } } \examples{ diff --git a/man/glex_vi.Rd b/man/glex_vi.Rd index a29adc7..139bd79 100644 --- a/man/glex_vi.Rd +++ b/man/glex_vi.Rd @@ -36,23 +36,24 @@ In turn, \code{m_rel} rescales \code{m} by the average prediction of the model ( \deqn{\mathtt{m\_rel} = \frac{\mathtt{m}}{m_0}} } \examples{ +\dontshow{if (requireNamespace("xgboost", quietly = TRUE)) withAutoprint(\{ # examplesIf} set.seed(1) # xgboost ----- -if (requireNamespace("xgboost", quietly = TRUE)) { library(xgboost) x <- as.matrix(mtcars[, -1]) y <- mtcars$mpg xg <- xgboost(x[1:26, ], y[1:26], - max_depth = 4, learning_rate = .1, - nrounds = 10, verbosity = 0, nthreads = 1) + max_depth = 4, learning_rate = .1, + nrounds = 10, verbosity = 0, nthreads = 1 +) glex_xgb <- glex(xg, x[27:32, ]) vi_xgb <- glex_vi(glex_xgb) library(ggplot2) autoplot(vi_xgb) autoplot(vi_xgb, by_degree = TRUE) -} +\dontshow{\}) # examplesIf} } \seealso{ \link{autoplot.glex_vi} diff --git a/man/plot_components.Rd b/man/plot_components.Rd index 3bba03c..ec7988c 100644 --- a/man/plot_components.Rd +++ b/man/plot_components.Rd @@ -35,7 +35,7 @@ Plotting the main effects among the prediction components is effectively identical to a partial dependence plot, centered to 0. } \examples{ -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # introduce factor variables to show categorical feature handling @@ -50,9 +50,9 @@ components <- glex(rpfit, mtcars) # Main effects ---- plot_main_effect(components, "wt") plot_main_effect(components, "cyl") -} +\dontshow{\}) # examplesIf} # plot_threeway_effects(components, c("hr", "temp", "workingday")) -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # 2-degree interaction effects ---- @@ -67,7 +67,7 @@ plot_twoway_effects(components, c("wt", "cyl")) # 2d categorical, heatmap of arbitrary orientation plot_twoway_effects(components, c("vs", "cyl")) plot_twoway_effects(components, c("cyl", "vs")) -} +\dontshow{\}) # examplesIf} } \seealso{ \code{\link[=plot_pdp]{plot_pdp()}} diff --git a/man/plot_pdp.Rd b/man/plot_pdp.Rd index 83e6f03..5765760 100644 --- a/man/plot_pdp.Rd +++ b/man/plot_pdp.Rd @@ -25,7 +25,7 @@ A version of \code{\link{plot_main_effect}} with the intercept term (horizontal resulting in a partial dependence plot. } \examples{ -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # introduce factor variables to show categorical feature handling @@ -39,7 +39,7 @@ components <- glex(rpfit, mtcars) plot_pdp(components, "wt") plot_pdp(components, "cyl") -} +\dontshow{\}) # examplesIf} } \seealso{ \code{\link[=plot_main_effect]{plot_main_effect()}} diff --git a/man/print.glex.Rd b/man/print.glex.Rd index 07a63f3..33d68a6 100644 --- a/man/print.glex.Rd +++ b/man/print.glex.Rd @@ -17,11 +17,11 @@ uses many terms, which leads to a large amount of column names of \verb{$m} bein This function wraps \code{\link[utils:str]{str()}} with a truncated output for a more compact representation. } \examples{ +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} # Random Planted Forest ----- -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { library(randomPlantedForest) rp <- rpf(mpg ~ hp + wt + drat, data = mtcars[1:26, ], max_interaction = 2) glex(rp, mtcars[27:32, ]) -} +\dontshow{\}) # examplesIf} } diff --git a/man/subset_components.Rd b/man/subset_components.Rd index e95e734..d11223e 100644 --- a/man/subset_components.Rd +++ b/man/subset_components.Rd @@ -16,15 +16,15 @@ subset_component_names(components, term) } \value{ \itemize{ -\item \code{subset_components}: An object of class \code{glex}. -\item \code{subset_component_names}: A character vector. +\item \code{subset_components()}: An object of class \code{glex}. +\item \code{subset_component_names()}: A character vector. } } \description{ Subset components } \examples{ -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # introduce factor variables to show categorical feature handling @@ -40,6 +40,5 @@ components <- glex(rpfit, mtcars) subset_components(components, "hp") subset_component_names(components, "hp") -} - +\dontshow{\}) # examplesIf} } diff --git a/tests/testthat/test-constrained-remainder.R b/tests/testthat/test-constrained-remainder.R index 2874719..e046c1a 100644 --- a/tests/testthat/test-constrained-remainder.R +++ b/tests/testthat/test-constrained-remainder.R @@ -149,8 +149,7 @@ test_that("ranger probability forest: remainder lives on the response scale", { # that default would fold the back-transformation into the remainder, and for binary # models compare against the wrong class entirely. test_that("rpf binary: remainder is on the raw score scale, for every loss", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purification OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") for (loss in c("L2", "logit", "exponential")) { set.seed(1) @@ -185,8 +184,7 @@ test_that("rpf binary: remainder is on the raw score scale, for every loss", { }) test_that("rpf binary: remainder does not target the response scale", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purification OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") set.seed(1) rp <- randomPlantedForest::rpf( y ~ x1 + x2 + x3, @@ -233,8 +231,7 @@ test_that("ranger: remainder completes a constrained decomposition", { }) test_that("rpf: remainder completes a constrained decomposition", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purification OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") set.seed(1) rp <- randomPlantedForest::rpf( mpg ~ cyl + hp + wt, @@ -259,8 +256,7 @@ test_that("rpf: remainder completes a constrained decomposition", { }) test_that("an inert constraint leaves no remainder", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purification OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") # A constant predictor cannot be split on, so every term involving it is exactly zero -- # on every platform, unlike a high-order term that merely happens to come out zero for a @@ -293,8 +289,7 @@ test_that("an inert constraint leaves no remainder", { }) test_that("rpf multiclass: remainder is class-wise, mirroring m", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purification OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") set.seed(1) mt <- mtcars mt$cyl <- factor(mt$cyl) diff --git a/tests/testthat/test-glex_explain.R b/tests/testthat/test-glex_explain.R index 0e2d2fe..fedad43 100644 --- a/tests/testthat/test-glex_explain.R +++ b/tests/testthat/test-glex_explain.R @@ -4,8 +4,7 @@ # Regression / rpf ------------------------------------------------------------------------------------------------ test_that("regression rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(mpg ~ cyl + hp + wt, data = mtcars, max_interaction = 3) gl <- glex(rp, mtcars) @@ -15,8 +14,7 @@ test_that("regression rpf", { # Binary / rpf ------------------------------------------------------------------------------------------------------ test_that("binary rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -27,8 +25,7 @@ test_that("binary rpf", { # Multiclass / rpf ------------------------------------------------------------------------------------------------ test_that("multiclass rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(yk ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -39,8 +36,7 @@ test_that("multiclass rpf", { # SHAP values come from `$shap` ----------------------------------------------------------------------------------- test_that("glex_explain plots the SHAP values stored in $shap", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -66,8 +62,7 @@ test_that("glex_explain plots the SHAP values stored in $shap", { }) test_that("glex_explain omits the SHAP bar for constrained decompositions", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -85,8 +80,7 @@ test_that("glex_explain omits the SHAP bar for constrained decompositions", { }) test_that("glex_explain works on objects without $shap", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) # Objects from earlier glex versions (and predict_components() output) have no diff --git a/tests/testthat/test-glex_vi.R b/tests/testthat/test-glex_vi.R index eab8715..64e1f5a 100644 --- a/tests/testthat/test-glex_vi.R +++ b/tests/testthat/test-glex_vi.R @@ -1,7 +1,6 @@ # Regression / rpf ------------------------------------------------------------------------------------------------ test_that("regression rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(mpg ~ cyl + hp + wt, data = mtcars, max_interaction = 3) gl <- glex(rp, mtcars) @@ -14,8 +13,7 @@ test_that("regression rpf", { }) test_that("regression rpf plot", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(mpg ~ cyl + hp + wt, data = mtcars, max_interaction = 3) gl <- glex(rp, mtcars) @@ -37,8 +35,7 @@ test_that("regression rpf plot", { # Binary / rpf ------------------------------------------------------------------------------------------------------ test_that("binary rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -51,8 +48,7 @@ test_that("binary rpf", { }) test_that("binary rpf plot", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -73,8 +69,7 @@ test_that("binary rpf plot", { # Multiclass / rpf ------------------------------------------------------------------------------------------------ test_that("multiclass rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(yk ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -87,8 +82,7 @@ test_that("multiclass rpf", { }) test_that("multiclass rpf plot", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf( yk ~ x1 + x2 + x3, data = xdat, diff --git a/tests/testthat/test-plot_main_effect.R b/tests/testthat/test-plot_main_effect.R index efb47a9..2228d9f 100644 --- a/tests/testthat/test-plot_main_effect.R +++ b/tests/testthat/test-plot_main_effect.R @@ -4,8 +4,7 @@ # Regression / rpf ------------------------------------------------------------------------------------------------ test_that("regression rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(mpg ~ cyl + hp + wt, data = mtcars, max_interaction = 3) gl <- glex(rp, mtcars) @@ -15,8 +14,7 @@ test_that("regression rpf", { # Binary / rpf ------------------------------------------------------------------------------------------------------ test_that("binary rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -29,8 +27,7 @@ test_that("binary rpf", { # Multiclass / rpf ------------------------------------------------------------------------------------------------ test_that("multiclass rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(yk ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) diff --git a/tests/testthat/test-plot_pdp.R b/tests/testthat/test-plot_pdp.R index bafc037..779af0d 100644 --- a/tests/testthat/test-plot_pdp.R +++ b/tests/testthat/test-plot_pdp.R @@ -4,8 +4,7 @@ # Regression / rpf ------------------------------------------------------------------------------------------------ test_that("regression rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(mpg ~ cyl + hp + wt, data = mtcars, max_interaction = 3) gl <- glex(rp, mtcars) @@ -15,8 +14,7 @@ test_that("regression rpf", { # Binary / rpf ------------------------------------------------------------------------------------------------------ test_that("binary rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -27,8 +25,7 @@ test_that("binary rpf", { # Multiclass / rpf ------------------------------------------------------------------------------------------------ test_that("multiclass rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(yk ~ x1 + x2 + x3, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) diff --git a/tests/testthat/test-plot_threeway_effects.R b/tests/testthat/test-plot_threeway_effects.R index bd54bf5..d2a6235 100644 --- a/tests/testthat/test-plot_threeway_effects.R +++ b/tests/testthat/test-plot_threeway_effects.R @@ -4,8 +4,7 @@ # Regression / rpf ------------------------------------------------------------------------------------------------ test_that("regression rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") mtcars$cyl <- factor(mtcars$cyl) rp <- rpf(mpg ~ cyl + hp + wt, data = mtcars, max_interaction = 3) gl <- glex(rp, mtcars) @@ -16,8 +15,7 @@ test_that("regression rpf", { # Binary / rpf ------------------------------------------------------------------------------------------------------ test_that("binary rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3 + x4 + x5 + x6, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -34,8 +32,7 @@ test_that("binary rpf", { # Multiclass / rpf ------------------------------------------------------------------------------------------------ test_that("multiclass rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(yk ~ x1 + x2 + x3 + x4 + x5 + x6, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) diff --git a/tests/testthat/test-plot_twoway_effects.R b/tests/testthat/test-plot_twoway_effects.R index 9905091..db2f14a 100644 --- a/tests/testthat/test-plot_twoway_effects.R +++ b/tests/testthat/test-plot_twoway_effects.R @@ -4,8 +4,7 @@ # Regression / rpf ------------------------------------------------------------------------------------------------ test_that("regression rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(mpg ~ cyl + hp + wt, data = mtcars, max_interaction = 3) gl <- glex(rp, mtcars) @@ -15,8 +14,7 @@ test_that("regression rpf", { # Binary / rpf ------------------------------------------------------------------------------------------------------ test_that("binary rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(y ~ x1 + x2 + x3 + x4 + x5, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) @@ -31,8 +29,7 @@ test_that("binary rpf", { # Multiclass / rpf ------------------------------------------------------------------------------------------------ test_that("multiclass rpf", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- rpf(yk ~ x1 + x2 + x3 + x4 + x5, data = xdat, max_interaction = 3) gl <- glex(rp, xdat) diff --git a/tests/testthat/test-print-glex.R b/tests/testthat/test-print-glex.R index e3c6120..9743ec2 100644 --- a/tests/testthat/test-print-glex.R +++ b/tests/testthat/test-print-glex.R @@ -1,6 +1,5 @@ test_that("print.glex works", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see test-rpf-sum-identity.R + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") set.seed(2) rp <- rpf(mpg ~ cyl + hp, data = mtcars) gl <- glex(rp, mtcars) diff --git a/tests/testthat/test-rpf-sum-identity.R b/tests/testthat/test-rpf-sum-identity.R index fda71ce..ea25c81 100644 --- a/tests/testthat/test-rpf-sum-identity.R +++ b/tests/testthat/test-rpf-sum-identity.R @@ -1,9 +1,3 @@ -# These tests crash R on Windows: randomPlantedForest's purify_3() has an -# out-of-bounds read (grid sized lim_list[dim-1].size() instead of .size() - 1 -# in src/lib/rpf.cpp, read at gridPoint + 1) that these tests happen to trigger. -# Fixed upstream in https://github.com/PlantedML/randomPlantedForest/pull/61 — -# remove the skips once that (or a minimal fix) is merged. - # For classification, rpf decomposes the *raw score*, which `predict(type = "numeric")` # returns. The default `type = "prob"` applies rpf's response function -- a clamp to # [0, 1] for `loss = "L2"`, the inverse link for `"logit"` / `"exponential"` -- which the @@ -18,8 +12,7 @@ # pass "fastpd" and "path-dependent" and assert the same thing twice. test_that("rpf binary: sum identity matches the predicted raw score", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see comment at top of file + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- randomPlantedForest::rpf( y ~ x1 + x2 + x3, @@ -36,8 +29,7 @@ test_that("rpf binary: sum identity matches the predicted raw score", { }) test_that("rpf multiclass: classwise sum identity holds up to the class intercept", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see comment at top of file + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- randomPlantedForest::rpf( yk ~ x1 + x2 + x3, @@ -69,8 +61,7 @@ test_that("rpf multiclass: classwise sum identity holds up to the class intercep }) test_that("rpf: shap is derived from components and satisfies efficiency", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see comment at top of file + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- randomPlantedForest::rpf( mpg ~ cyl + hp + wt, @@ -89,8 +80,7 @@ test_that("rpf: shap is derived from components and satisfies efficiency", { }) test_that("rpf: constrained decompositions invalidate shap", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see comment at top of file + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") rp <- randomPlantedForest::rpf( mpg ~ cyl + hp + wt, @@ -116,8 +106,7 @@ test_that("rpf: constrained decompositions invalidate shap", { }) test_that("rpf multiclass: shap mirrors the class-suffixed structure of m", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see comment at top of file + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") mt <- mtcars mt$cyl <- factor(mt$cyl) @@ -152,8 +141,7 @@ test_that("rpf multiclass: shap mirrors the class-suffixed structure of m", { }) test_that("rpf: constraints that drop only zero terms keep shap valid", { - skip_if_not_installed("randomPlantedForest") - skip_on_os("windows") # rpf purify_3() OOB read, see comment at top of file + skip_if_not_installed("randomPlantedForest", minimum_version = "0.3.0") # The inert term has to be zero *by construction*, not by luck of the fit. An earlier # version of this test fit at the maximum order and assumed the top-order term came out