From 823917bdf75ea0727202fe48703d9e37f322d91c Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Sat, 11 Jul 2026 00:10:13 +0200 Subject: [PATCH 1/6] workaround to detect if is valid --- DESCRIPTION | 2 +- NEWS.md | 10 +++++ R/autoplot.R | 5 +-- R/glex.R | 61 +++++++++++++++++++++++++++++- R/glex_vi.R | 1 - man/glex.Rd | 6 ++- tests/testthat/test-glex-xgboost.R | 48 +++++++++++++++++++++-- 7 files changed, 122 insertions(+), 11 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 767dbbb..77257df 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: glex Title: Global Explanations for Tree-Based Models -Version: 0.6.0 +Version: 0.6.0.9000 Authors@R: c( person(c("Marvin", "N."), "Wright", , "cran@wrig.de", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-8542-6291")), diff --git a/NEWS.md b/NEWS.md index 25bc0c7..67ec9d2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,13 @@ +# glex 0.6.0.9000 (development version) + +* `$shap` is now `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 + violating the efficiency property. Previously, misleading values were returned. +* Tests that fit `randomPlantedForest` models are skipped on Windows for now: an + out-of-bounds read in randomPlantedForest's `purify_3()` crashes R there + (fixed upstream in PlantedML/randomPlantedForest#61, not yet merged). + # glex 0.6.0 * Extended compatibility with `xgboost`, now requiring `xgboost (>= 3.0.0)` in `Suggests:` diff --git a/R/autoplot.R b/R/autoplot.R index 1e5e360..b7924fe 100644 --- a/R/autoplot.R +++ b/R/autoplot.R @@ -64,7 +64,6 @@ autoplot.glex_vi <- function( by_what <- ifelse(by_degree, "Degree", "Term") score <- switch(scale, absolute = "m", relative = "m_rel") - # FIXME: data.table NSE stuff m <- m_rel <- degree <- NULL aggr_degree <- function(x) { @@ -77,9 +76,7 @@ autoplot.glex_vi <- function( } # If max_interaction or threshold would select any observations, we summarize accordingly - if ( - any(object$degree > max_interaction | abs(object[[score]]) <= threshold) - ) { + if (any(object$degree > max_interaction | abs(object[[score]]) <= threshold)) { # Sanity check that we're aggregating correctly by keeping track of the sums old_sum <- sum(object[[score]]) diff --git a/R/glex.R b/R/glex.R index 7abea01..f451577 100644 --- a/R/glex.R +++ b/R/glex.R @@ -20,7 +20,11 @@ #' #' @return Decomposition of the regression or classification function. #' A `list` with elements: -#' * `shap`: SHAP values (`xgboost` method only). +#' * `shap`: SHAP values (`xgboost` and `ranger` methods only). These are reconstructed +#' from the functional decomposition, which is only possible if the decomposition is +#' complete: if it is constrained via `max_interaction` or `features`, the components +#' no longer sum to the full model prediction and the SHAP efficiency property cannot +#' hold, so `shap` is set to `NA` (with a warning). #' * `m`: Functional decomposition into all main and interaction #' components in the model, up to the degree specified by `max_interaction`. #' The variable names correspond to the original variable names, @@ -553,6 +557,43 @@ tree_fun_emp_fastPD <- function( } +#' Upper bound on the interaction order present in the decomposition. +#' Interactions can only arise between features sharing a root-to-leaf path, so +#' per tree the order is bounded by both its depth and its number of distinct +#' features; the model bound is the maximum over trees. This can overestimate +#' (a path may split repeatedly on one feature), never underestimate. +#' Read from the trees table rather than model metadata because ranger has no +#' usable depth field (max.depth = 0 means unlimited) and xgboost's config +#' parsing is version-fragile. +#' Relies on child node ids being larger than their parent's, which holds for +#' both xgb.model.dt.tree(use_int_id = TRUE) and ranger::treeInfo() numbering. +#' @param trees data.table with columns Node, Yes, No, Feature_num, Tree; +#' leaves have Yes = NA +#' @keywords internal +#' @noRd +max_order_bound <- function(trees) { + Tree <- Node <- NULL + + max(vapply( + 0:max(trees$Tree), + function(tree) { + tree_info <- trees[Tree == tree, ][order(Node)] + depth <- integer(nrow(tree_info)) + for (i in seq_len(nrow(tree_info))) { + if (!is.na(tree_info$Yes[i])) { + depth[tree_info$Yes[i] + 1L] <- depth[i] + 1L + depth[tree_info$No[i] + 1L] <- depth[i] + 1L + } + } + n_features <- length(unique(tree_info$Feature_num[ + tree_info$Feature_num > 0L + ])) + min(max(depth), n_features) + }, + integer(1) + )) +} + #' Internal tree function wrapper that returns the actual tree function #' @param trees data.table #' @param x observerations, matrix like data-structure @@ -676,6 +717,15 @@ calc_components <- function( 1L all_S <- get_all_subsets_cpp(sort(unique(features_num)), max_interaction) } + # SHAP values can only be reconstructed from a complete decomposition: if terms + # are dropped via max_interaction or features, the components no longer sum to + # the full model prediction and the SHAP efficiency property cannot hold. + shap_valid <- max_interaction >= max_order_bound(trees) + if (!is.null(features)) { + used_features_num <- trees[Feature_num > 0, sort(unique(Feature_num))] + shap_valid <- shap_valid && all(used_features_num %in% features_num) + } + # Keep only those with not more than max_interaction involved features d <- lengths(all_S) @@ -731,6 +781,15 @@ calc_components <- function( FUN.VALUE = numeric(nrow(x)) ) + if (!shap_valid) { + warning( + "SHAP values set to NA: the decomposition is constrained by `max_interaction` or ", + "`features` and does not sum to the full model prediction, so SHAP values cannot ", + "be reconstructed from it (the efficiency property would be violated)." + ) + shap[] <- NA_real_ + } + # Return shap values, decomposition and intercept ret <- list( shap = data.table::setDT(as.data.frame(shap)), diff --git a/R/glex_vi.R b/R/glex_vi.R index d992dbf..eba5f11 100644 --- a/R/glex_vi.R +++ b/R/glex_vi.R @@ -45,7 +45,6 @@ glex_vi <- function(object, ...) { checkmate::assert_class(object, classes = "glex") - # FIXME: data.table NSE warnings term <- degree <- m <- m_rel <- NULL m_long <- melt_m(object$m, object$target_levels) diff --git a/man/glex.Rd b/man/glex.Rd index 971f1c7..057eca0 100644 --- a/man/glex.Rd +++ b/man/glex.Rd @@ -57,7 +57,11 @@ If not set in \code{xgboost}, the default value of \code{6} is assumed.} Decomposition of the regression or classification function. A \code{list} with elements: \itemize{ -\item \code{shap}: SHAP values (\code{xgboost} method only). +\item \code{shap}: SHAP values (\code{xgboost} and \code{ranger} methods only). These are reconstructed +from the functional decomposition, which is only possible if the decomposition is +complete: if it is constrained via \code{max_interaction} or \code{features}, the components +no longer sum to the full model prediction and the SHAP efficiency property cannot +hold, so \code{shap} is set to \code{NA} (with a warning). \item \code{m}: Functional decomposition into all main and interaction components in the model, up to the degree specified by \code{max_interaction}. The variable names correspond to the original variable names, diff --git a/tests/testthat/test-glex-xgboost.R b/tests/testthat/test-glex-xgboost.R index 43b72fa..ecb3dd2 100644 --- a/tests/testthat/test-glex-xgboost.R +++ b/tests/testthat/test-glex-xgboost.R @@ -17,14 +17,14 @@ test_that("max_interaction respects xgb's max_depth", { test_that("features argument only calculates for given features", { x <- as.matrix(mtcars[, -1]) xg <- xgboost(x, mtcars$mpg, nrounds = 10, verbosity = 0) - glexb <- glex(xg, x, features = c("cyl", "disp")) + glexb <- suppressWarnings(glex(xg, x, features = c("cyl", "disp"))) expect_equal(colnames(glexb$m), c("cyl", "cyl:disp", "disp")) }) test_that("features argument results in same values as without", { x <- as.matrix(mtcars[, -1]) xg <- xgboost(x, mtcars$mpg, nrounds = 10, verbosity = 0) - glexb1 <- glex(xg, x, features = c("cyl", "disp")) + glexb1 <- suppressWarnings(glex(xg, x, features = c("cyl", "disp"))) glexb2 <- glex(xg, x) cols <- c("cyl", "disp", "cyl:disp") expect_equal(glexb1$m[, ..cols], glexb2$m[, ..cols]) @@ -33,11 +33,53 @@ test_that("features argument results in same values as without", { test_that("features argument works together with max_interaction", { x <- as.matrix(mtcars[, -1]) xg <- xgboost(x, mtcars$mpg, nrounds = 10, verbosity = 0) - glexb <- glex(xg, x, features = c("cyl", "disp"), max_interaction = 1) + glexb <- suppressWarnings(glex( + xg, + x, + features = c("cyl", "disp"), + max_interaction = 1 + )) expect_equal(colnames(glexb$m), c("cyl", "disp")) }) +test_that("shap is NA when decomposition is constrained", { + set.seed(1) + x <- as.matrix(mtcars[, -1]) + xg <- xgboost( + x, + mtcars$mpg, + nrounds = 10, + max_depth = 4, + verbosity = 0, + nthreads = 1 + ) + + # constrained via max_interaction + expect_warning( + gl_mi <- glex(xg, x, max_interaction = 1), + "efficiency property" + ) + expect_true(all(is.na(gl_mi$shap))) + expect_false(anyNA(gl_mi$m)) + + # constrained via features + expect_warning( + gl_ft <- glex(xg, x, features = c("cyl", "disp")), + "efficiency property" + ) + expect_true(all(is.na(gl_ft$shap))) + + # unconstrained: shap present and satisfies the efficiency property + gl_full <- glex(xg, x) + expect_false(anyNA(gl_full$shap)) + expect_equal( + unname(gl_full$intercept + rowSums(gl_full$shap)), + unname(predict(xg, x)), + tolerance = 1e-5 + ) +}) + x_train <- as.matrix(mtcars[1:26, -1]) x_test <- as.matrix(mtcars[27:32, -1]) y_train <- mtcars$mpg[1:26] From e905beaf6b14919e25f36913537ee15b7cd20512 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Sat, 11 Jul 2026 15:35:47 +0200 Subject: [PATCH 2/6] modernize docs --- R/glex.R | 40 ++++++++++++++++++++-------------------- R/glex_explain.R | 4 +--- R/glex_vi.R | 9 ++++----- R/plot_main_effect.R | 8 ++------ R/plot_twoway_effects.R | 4 +--- R/print.R | 4 +--- R/utils-components.R | 4 +--- man/glex.Rd | 36 +++++++++++++++++++++--------------- man/glex_explain.Rd | 4 ++-- man/glex_vi.Rd | 9 +++++---- man/plot_components.Rd | 8 ++++---- man/plot_pdp.Rd | 4 ++-- man/print.glex.Rd | 4 ++-- man/subset_components.Rd | 5 ++--- 14 files changed, 68 insertions(+), 75 deletions(-) diff --git a/R/glex.R b/R/glex.R index f451577..1de5988 100644 --- a/R/glex.R +++ b/R/glex.R @@ -15,7 +15,7 @@ #' 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. @@ -48,17 +48,15 @@ glex.default <- function(object, ...) { #' @rdname glex #' @export -#' @examples +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" #' #' # 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( @@ -89,15 +87,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, ]) #' @@ -106,7 +104,6 @@ glex.rpf <- function(object, x, max_interaction = NULL, features = NULL, ...) { #' doParallel::registerDoParallel() #' glex(xg, x[27:32, ]) #' } -#' } glex.xgb.Booster <- function( object, x, @@ -280,20 +277,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, ]) #' @@ -302,7 +303,6 @@ get_xgb_base_score <- function(object) { #' doParallel::registerDoParallel() #' glex(rf, x[27:32, ]) #' } -#' } glex.ranger <- function( object, x, @@ -598,7 +598,7 @@ max_order_bound <- 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 b43605a..1313437 100644 --- a/R/glex_explain.R +++ b/R/glex_explain.R @@ -30,10 +30,9 @@ #' @export #' @family Visualization functions #' -#' @examples +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" #' 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..8ad9d0e 100644 --- a/R/glex_vi.R +++ b/R/glex_vi.R @@ -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..1c20cba 100644 --- a/R/plot_main_effect.R +++ b/R/plot_main_effect.R @@ -16,8 +16,7 @@ #' @export #' @seealso [plot_pdp()] #' -#' @examples -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" #' 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, ...) } @@ -51,8 +49,7 @@ plot_main_effect <- function(object, predictor, rug_sides = "b", ...) { #' @export #' @seealso [plot_main_effect()] #' @family Visualization functions -#' @examples -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" #' 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..17b25f8 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) && .Platform$OS.type != "windows" #' 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 e51b262..51c3a49 100644 --- a/R/print.R +++ b/R/print.R @@ -8,14 +8,12 @@ #' #' @export #' @importFrom utils str -#' @examples +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" #' # 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/utils-components.R b/R/utils-components.R index 920abe9..c23052c 100644 --- a/R/utils-components.R +++ b/R/utils-components.R @@ -9,8 +9,7 @@ #' #' @rdname subset_components #' @export -#' @examples -#' if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" #' 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/man/glex.Rd b/man/glex.Rd index 057eca0..a8a3a97 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.} @@ -79,27 +79,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) && .Platform$OS.type != "windows") 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, ]) @@ -108,15 +110,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, ]) @@ -125,10 +129,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..dead1cb 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) && .Platform$OS.type != "windows") 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_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..1ac14b4 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) && .Platform$OS.type != "windows") 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) && .Platform$OS.type != "windows") 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..8483f84 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) && .Platform$OS.type != "windows") 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..f159f6b 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) && .Platform$OS.type != "windows") 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..cdee181 100644 --- a/man/subset_components.Rd +++ b/man/subset_components.Rd @@ -24,7 +24,7 @@ subset_component_names(components, term) Subset components } \examples{ -if (requireNamespace("randomPlantedForest", quietly = TRUE)) { +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows") 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} } From 366d7748fdb17172df6d9d5f56f959aced4df22f Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Sat, 18 Jul 2026 02:19:11 +0200 Subject: [PATCH 3/6] require randomPlantedForest >= 0.3.0, drop Windows skips rpf 0.3.0 fixes the purify_3() out-of-bounds read that crashed R on Windows (PlantedML/randomPlantedForest#61), so the skip_on_os() guards and the OS condition in the rpf examples are no longer needed. Test skips now pin the minimum version instead. Co-Authored-By: Claude Fable 5 --- DESCRIPTION | 2 +- NEWS.md | 4 ++ R/glex.R | 2 +- R/glex_explain.R | 2 +- R/plot_main_effect.R | 4 +- R/plot_twoway_effects.R | 2 +- R/print.R | 2 +- R/utils-components.R | 2 +- attic/misc.R | 8 ++-- attic/reweighting-giuseppe.R | 50 ++++++++++----------- attic/surv-test.R | 4 +- data-raw/bike_xg.R | 34 +++++++------- man/glex.Rd | 2 +- man/glex_explain.Rd | 2 +- man/plot_components.Rd | 4 +- man/plot_pdp.Rd | 2 +- man/print.glex.Rd | 2 +- man/subset_components.Rd | 2 +- tests/testthat/test-constrained-remainder.R | 15 +++---- tests/testthat/test-glex_explain.R | 18 +++----- tests/testthat/test-glex_vi.R | 18 +++----- tests/testthat/test-plot_main_effect.R | 9 ++-- tests/testthat/test-plot_pdp.R | 9 ++-- tests/testthat/test-plot_threeway_effects.R | 9 ++-- tests/testthat/test-plot_twoway_effects.R | 9 ++-- tests/testthat/test-print-glex.R | 3 +- tests/testthat/test-rpf-sum-identity.R | 24 +++------- 27 files changed, 105 insertions(+), 139 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 77257df..7c78f06 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -38,7 +38,7 @@ Suggests: ISLR2, knitr, patchwork, - randomPlantedForest, + randomPlantedForest (>= 0.3.0), ranger, rmarkdown, testthat (>= 3.0.0), 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/glex.R b/R/glex.R index 6411e0f..89db1b1 100644 --- a/R/glex.R +++ b/R/glex.R @@ -71,7 +71,7 @@ glex.default <- function(object, ...) { #' @rdname glex #' @export -#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' #' # Random Planted Forest ----- #' library(randomPlantedForest) diff --git a/R/glex_explain.R b/R/glex_explain.R index 6242d71..d6d3d58 100644 --- a/R/glex_explain.R +++ b/R/glex_explain.R @@ -30,7 +30,7 @@ #' @export #' @family Visualization functions #' -#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' set.seed(1) #' # Random Planted Forest ----- #' library(randomPlantedForest) diff --git a/R/plot_main_effect.R b/R/plot_main_effect.R index 1c20cba..febc22e 100644 --- a/R/plot_main_effect.R +++ b/R/plot_main_effect.R @@ -16,7 +16,7 @@ #' @export #' @seealso [plot_pdp()] #' -#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # introduce factor variables to show categorical feature handling @@ -49,7 +49,7 @@ plot_main_effect <- function(object, predictor, rug_sides = "b", ...) { #' @export #' @seealso [plot_main_effect()] #' @family Visualization functions -#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # introduce factor variables to show categorical feature handling diff --git a/R/plot_twoway_effects.R b/R/plot_twoway_effects.R index 17b25f8..5fc1e8c 100644 --- a/R/plot_twoway_effects.R +++ b/R/plot_twoway_effects.R @@ -1,6 +1,6 @@ #' @rdname plot_components #' @export -#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # 2-degree interaction effects ---- diff --git a/R/print.R b/R/print.R index bb073ad..dbc7463 100644 --- a/R/print.R +++ b/R/print.R @@ -8,7 +8,7 @@ #' #' @export #' @importFrom utils str -#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' # Random Planted Forest ----- #' library(randomPlantedForest) #' rp <- rpf(mpg ~ hp + wt + drat, data = mtcars[1:26, ], max_interaction = 2) diff --git a/R/utils-components.R b/R/utils-components.R index c23052c..1fc0f6b 100644 --- a/R/utils-components.R +++ b/R/utils-components.R @@ -9,7 +9,7 @@ #' #' @rdname subset_components #' @export -#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows" +#' @examplesIf requireNamespace("randomPlantedForest", quietly = TRUE) #' library(randomPlantedForest) #' #' # introduce factor variables to show categorical feature handling diff --git a/attic/misc.R b/attic/misc.R index 7347e6c..6e20555 100644 --- a/attic/misc.R +++ b/attic/misc.R @@ -87,13 +87,13 @@ xdat <- data.frame( x3 = runif(100) ) -xdat$lp = 3 * xdat$x1 + 0.5 * (xdat$x2 + xdat$x3) + 3 * abs(xdat$x1 * xdat$x3) -xdat$p = 1 / (1 + exp(-xdat$lp)) -xdat$y = factor( +xdat$lp <- 3 * xdat$x1 + 0.5 * (xdat$x2 + xdat$x3) + 3 * abs(xdat$x1 * xdat$x3) +xdat$p <- 1 / (1 + exp(-xdat$lp)) +xdat$y <- factor( rbinom(100, size = 1, prob = xdat$p), labels = c("Negative", "Positive") ) -xdat$yk = factor( +xdat$yk <- factor( rbinom(100, size = 2, prob = xdat$p), labels = c("N", "P", "K") ) diff --git a/attic/reweighting-giuseppe.R b/attic/reweighting-giuseppe.R index 373f77a..413f2cb 100644 --- a/attic/reweighting-giuseppe.R +++ b/attic/reweighting-giuseppe.R @@ -4,13 +4,13 @@ library(mlr3verse) set.seed(21) data("kc_housing", package = "mlr3data") -train.inds = !grepl(pattern = "2015", kc_housing$date) -test.inds = grepl(pattern = "2015", kc_housing$date) -kc_housing$yr_renovated = kc_housing$sqft_basement = kc_housing$date = NULL +train.inds <- !grepl(pattern = "2015", kc_housing$date) +test.inds <- grepl(pattern = "2015", kc_housing$date) +kc_housing$yr_renovated <- kc_housing$sqft_basement <- kc_housing$date <- NULL -task = as_task_regr(x = kc_housing, target = "price") +task <- as_task_regr(x = kc_housing, target = "price") -learners = list( +learners <- list( lrn("regr.ranger"), lrn("regr.lm"), lrn("regr.featureless"), @@ -26,45 +26,45 @@ learners = list( ) ) -design = benchmark_grid(task, learners, rsmp("cv", folds = 3)) -bench = benchmark(design) +design <- benchmark_grid(task, learners, rsmp("cv", folds = 3)) +bench <- benchmark(design) bench$aggregate(msr("regr.rmse")) # xgb hyperpars taken from https://mlr-org.com/gallery/basic/2020-01-30-house-prices-in-king-county/index.html -xgb = learners[[4]] +xgb <- learners[[4]] xgb$train(task, row_ids = which(train.inds)) -pred = xgb$predict(task, row_ids = which(test.inds)) +pred <- xgb$predict(task, row_ids = which(test.inds)) pred$score(msr("regr.rmse")) # Decompose -glex_xgb_train = glex( +glex_xgb_train <- glex( xgb$model, x = as.matrix(task$data(rows = which(train.inds))) ) -glex_xgb_test = glex( +glex_xgb_test <- glex( xgb$model, x = as.matrix(task$data(rows = which(test.inds))) ) # Create new data using found components -train2 = cbind(glex_xgb_train$m, glex_xgb_train$intercept) -test2 = cbind(glex_xgb_test$m, glex_xgb_test$intercept) -ytrain = kc_housing$price[train.inds] -ytest = kc_housing$price[test.inds] +train2 <- cbind(glex_xgb_train$m, glex_xgb_train$intercept) +test2 <- cbind(glex_xgb_test$m, glex_xgb_test$intercept) +ytrain <- kc_housing$price[train.inds] +ytest <- kc_housing$price[test.inds] # Train a LASSO to obtain a sparse linear combination of the found components library(glmnet) -train2mat = as.matrix(as.data.frame(train2)) -test2mat = as.matrix(as.data.frame(test2)) -lasso = cv.glmnet(train2mat, ytrain) +train2mat <- as.matrix(as.data.frame(train2)) +test2mat <- as.matrix(as.data.frame(test2)) +lasso <- cv.glmnet(train2mat, ytrain) #plot(lasso) #coef(lasso, s = "lambda.min") # Train a LASSO with post-hoc feature removal of zipcode for fairness reasons # (zipcode is sometimes associated with rassism) -train2fair = train2mat[, !grepl("zipcode", colnames(train2mat))] -test2fair = test2mat[, !grepl("zipcode", colnames(test2mat))] -lasso_fair = cv.glmnet(train2fair, ytrain) +train2fair <- train2mat[, !grepl("zipcode", colnames(train2mat))] +test2fair <- test2mat[, !grepl("zipcode", colnames(test2mat))] +lasso_fair <- cv.glmnet(train2fair, ytrain) # Compare models: original xgb model vs. LASSO on found components vs. LASSO without zipocde sqrt(mean((pred$response - ytest)^2)) @@ -72,10 +72,10 @@ sqrt(mean((predict(lasso, newx = test2mat, s = "lambda.min") - ytest)^2)) sqrt(mean((predict(lasso_fair, newx = test2fair, s = "lambda.min") - ytest)^2)) # component indices containing "zipcode", i.e. main or interaction effects of zipcode -idx_unfair = glex:::find_term_matches("zipcode", names(glex_xgb_test$m)) -components_fair = glex_xgb_test$m[, -idx_unfair, with = FALSE] +idx_unfair <- glex:::find_term_matches("zipcode", names(glex_xgb_test$m)) +components_fair <- glex_xgb_test$m[, -idx_unfair, with = FALSE] # Prediction = sum of components (w/o zipcode) + intercept (average prediction) -components_fair_sum = rowSums(components_fair) + glex_xgb_test$intercept +components_fair_sum <- rowSums(components_fair) + glex_xgb_test$intercept # "Raw" prediction of XGb with zipcode removed sqrt(mean((components_fair_sum - ytest)^2)) @@ -93,7 +93,7 @@ c( sort() # Variable importance of components containing zipcode -vi = glex_vi(glex_xgb_test) +vi <- glex_vi(glex_xgb_test) # VIs of everything containing zipcode: mostly 2nd order effects vi[glex:::find_term_matches("zipcode", vi$term), ] |> diff --git a/attic/surv-test.R b/attic/surv-test.R index 81ac88c..3f01aef 100644 --- a/attic/surv-test.R +++ b/attic/surv-test.R @@ -2,7 +2,7 @@ library(mlr3proba) library(mlr3extralearners) library(mlr3pipelines) -task = tsk("lung") +task <- tsk("lung") lrn_xgb <- po("encode") %>>% lrn("surv.xgboost.cox", nrounds = 500, max_depth = 3) |> @@ -11,7 +11,7 @@ lrn_xgb <- po("encode") %>>% lrn_xgb$train(tsk("lung")) xgbmod <- lrn_xgb$model$surv.xgboost.cox$model$model -penc = po("encode") +penc <- po("encode") X <- penc$train(list(task))[[1]]$data(cols = task$feature_names) xg_glex <- glex::glex(xgbmod, x = X) 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/glex.Rd b/man/glex.Rd index 4115409..e596fe2 100644 --- a/man/glex.Rd +++ b/man/glex.Rd @@ -106,7 +106,7 @@ The different weighting methods are described in detail in Liu et al. (2025). Th method is \code{"fastpd"} as it consistently estimates the correct partial dependence function. } \examples{ -\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows") withAutoprint(\{ # examplesIf} +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} # Random Planted Forest ----- library(randomPlantedForest) diff --git a/man/glex_explain.Rd b/man/glex_explain.Rd index dead1cb..bcdf41e 100644 --- a/man/glex_explain.Rd +++ b/man/glex_explain.Rd @@ -44,7 +44,7 @@ recommended to use \code{predictors}, \code{max_interaction}, or \code{threshold elements in the plot. } \examples{ -\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows") withAutoprint(\{ # examplesIf} +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} set.seed(1) # Random Planted Forest ----- library(randomPlantedForest) diff --git a/man/plot_components.Rd b/man/plot_components.Rd index 1ac14b4..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{ -\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows") withAutoprint(\{ # examplesIf} +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # introduce factor variables to show categorical feature handling @@ -52,7 +52,7 @@ plot_main_effect(components, "wt") plot_main_effect(components, "cyl") \dontshow{\}) # examplesIf} # plot_threeway_effects(components, c("hr", "temp", "workingday")) -\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows") withAutoprint(\{ # examplesIf} +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # 2-degree interaction effects ---- diff --git a/man/plot_pdp.Rd b/man/plot_pdp.Rd index 8483f84..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{ -\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows") withAutoprint(\{ # examplesIf} +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # introduce factor variables to show categorical feature handling diff --git a/man/print.glex.Rd b/man/print.glex.Rd index f159f6b..33d68a6 100644 --- a/man/print.glex.Rd +++ b/man/print.glex.Rd @@ -17,7 +17,7 @@ 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) && .Platform$OS.type != "windows") withAutoprint(\{ # examplesIf} +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} # Random Planted Forest ----- library(randomPlantedForest) rp <- rpf(mpg ~ hp + wt + drat, data = mtcars[1:26, ], max_interaction = 2) diff --git a/man/subset_components.Rd b/man/subset_components.Rd index cdee181..e062f1b 100644 --- a/man/subset_components.Rd +++ b/man/subset_components.Rd @@ -24,7 +24,7 @@ subset_component_names(components, term) Subset components } \examples{ -\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE) && .Platform$OS.type != "windows") withAutoprint(\{ # examplesIf} +\dontshow{if (requireNamespace("randomPlantedForest", quietly = TRUE)) withAutoprint(\{ # examplesIf} library(randomPlantedForest) # introduce factor variables to show categorical feature handling 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 From 909b8a6938bf5842e68df1bbd62a4036ddc71bb4 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Sat, 18 Jul 2026 02:23:25 +0200 Subject: [PATCH 4/6] modernize roxygen2 usage roxygen2 8.0.0 conventions: Config/roxygen2/markdown field instead of the Roxygen list, @returns instead of @return, and markdown bullets instead of the last remaining \describe block. Fix a typo in the bike data docs. Co-Authored-By: Claude Fable 5 --- DESCRIPTION | 2 +- R/autoplot.R | 2 +- R/data_bike.R | 2 +- R/glex-options.R | 42 +++++++++++++++++++--------------------- R/glex.R | 2 +- R/glex_explain.R | 2 +- R/glex_vi.R | 2 +- R/plot_main_effect.R | 4 ++-- R/theme_glex.R | 2 +- R/utils-components.R | 6 +++--- man/bike.Rd | 2 +- man/glex_options.Rd | 18 ++++++++--------- man/subset_components.Rd | 4 ++-- 13 files changed, 44 insertions(+), 46 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7c78f06..5571ec9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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/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 89db1b1..1e6146d 100644 --- a/R/glex.R +++ b/R/glex.R @@ -18,7 +18,7 @@ #' @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 diff --git a/R/glex_explain.R b/R/glex_explain.R index d6d3d58..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. diff --git a/R/glex_vi.R b/R/glex_vi.R index 8ad9d0e..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. diff --git a/R/plot_main_effect.R b/R/plot_main_effect.R index febc22e..bb590f6 100644 --- a/R/plot_main_effect.R +++ b/R/plot_main_effect.R @@ -11,7 +11,7 @@ #' 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()] @@ -44,7 +44,7 @@ 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()] 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 1fc0f6b..d798e92 100644 --- a/R/utils-components.R +++ b/R/utils-components.R @@ -3,9 +3,9 @@ #' @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 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_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/subset_components.Rd b/man/subset_components.Rd index e062f1b..d11223e 100644 --- a/man/subset_components.Rd +++ b/man/subset_components.Rd @@ -16,8 +16,8 @@ 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{ From e01ac10daab863ee6875c9afc86dbeb4892a9036 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Sat, 18 Jul 2026 03:05:52 +0200 Subject: [PATCH 5/6] ignore attic formats --- attic/misc.R | 8 +++--- attic/reweighting-giuseppe.R | 50 ++++++++++++++++++------------------ attic/surv-test.R | 4 +-- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/attic/misc.R b/attic/misc.R index 6e20555..7347e6c 100644 --- a/attic/misc.R +++ b/attic/misc.R @@ -87,13 +87,13 @@ xdat <- data.frame( x3 = runif(100) ) -xdat$lp <- 3 * xdat$x1 + 0.5 * (xdat$x2 + xdat$x3) + 3 * abs(xdat$x1 * xdat$x3) -xdat$p <- 1 / (1 + exp(-xdat$lp)) -xdat$y <- factor( +xdat$lp = 3 * xdat$x1 + 0.5 * (xdat$x2 + xdat$x3) + 3 * abs(xdat$x1 * xdat$x3) +xdat$p = 1 / (1 + exp(-xdat$lp)) +xdat$y = factor( rbinom(100, size = 1, prob = xdat$p), labels = c("Negative", "Positive") ) -xdat$yk <- factor( +xdat$yk = factor( rbinom(100, size = 2, prob = xdat$p), labels = c("N", "P", "K") ) diff --git a/attic/reweighting-giuseppe.R b/attic/reweighting-giuseppe.R index 413f2cb..373f77a 100644 --- a/attic/reweighting-giuseppe.R +++ b/attic/reweighting-giuseppe.R @@ -4,13 +4,13 @@ library(mlr3verse) set.seed(21) data("kc_housing", package = "mlr3data") -train.inds <- !grepl(pattern = "2015", kc_housing$date) -test.inds <- grepl(pattern = "2015", kc_housing$date) -kc_housing$yr_renovated <- kc_housing$sqft_basement <- kc_housing$date <- NULL +train.inds = !grepl(pattern = "2015", kc_housing$date) +test.inds = grepl(pattern = "2015", kc_housing$date) +kc_housing$yr_renovated = kc_housing$sqft_basement = kc_housing$date = NULL -task <- as_task_regr(x = kc_housing, target = "price") +task = as_task_regr(x = kc_housing, target = "price") -learners <- list( +learners = list( lrn("regr.ranger"), lrn("regr.lm"), lrn("regr.featureless"), @@ -26,45 +26,45 @@ learners <- list( ) ) -design <- benchmark_grid(task, learners, rsmp("cv", folds = 3)) -bench <- benchmark(design) +design = benchmark_grid(task, learners, rsmp("cv", folds = 3)) +bench = benchmark(design) bench$aggregate(msr("regr.rmse")) # xgb hyperpars taken from https://mlr-org.com/gallery/basic/2020-01-30-house-prices-in-king-county/index.html -xgb <- learners[[4]] +xgb = learners[[4]] xgb$train(task, row_ids = which(train.inds)) -pred <- xgb$predict(task, row_ids = which(test.inds)) +pred = xgb$predict(task, row_ids = which(test.inds)) pred$score(msr("regr.rmse")) # Decompose -glex_xgb_train <- glex( +glex_xgb_train = glex( xgb$model, x = as.matrix(task$data(rows = which(train.inds))) ) -glex_xgb_test <- glex( +glex_xgb_test = glex( xgb$model, x = as.matrix(task$data(rows = which(test.inds))) ) # Create new data using found components -train2 <- cbind(glex_xgb_train$m, glex_xgb_train$intercept) -test2 <- cbind(glex_xgb_test$m, glex_xgb_test$intercept) -ytrain <- kc_housing$price[train.inds] -ytest <- kc_housing$price[test.inds] +train2 = cbind(glex_xgb_train$m, glex_xgb_train$intercept) +test2 = cbind(glex_xgb_test$m, glex_xgb_test$intercept) +ytrain = kc_housing$price[train.inds] +ytest = kc_housing$price[test.inds] # Train a LASSO to obtain a sparse linear combination of the found components library(glmnet) -train2mat <- as.matrix(as.data.frame(train2)) -test2mat <- as.matrix(as.data.frame(test2)) -lasso <- cv.glmnet(train2mat, ytrain) +train2mat = as.matrix(as.data.frame(train2)) +test2mat = as.matrix(as.data.frame(test2)) +lasso = cv.glmnet(train2mat, ytrain) #plot(lasso) #coef(lasso, s = "lambda.min") # Train a LASSO with post-hoc feature removal of zipcode for fairness reasons # (zipcode is sometimes associated with rassism) -train2fair <- train2mat[, !grepl("zipcode", colnames(train2mat))] -test2fair <- test2mat[, !grepl("zipcode", colnames(test2mat))] -lasso_fair <- cv.glmnet(train2fair, ytrain) +train2fair = train2mat[, !grepl("zipcode", colnames(train2mat))] +test2fair = test2mat[, !grepl("zipcode", colnames(test2mat))] +lasso_fair = cv.glmnet(train2fair, ytrain) # Compare models: original xgb model vs. LASSO on found components vs. LASSO without zipocde sqrt(mean((pred$response - ytest)^2)) @@ -72,10 +72,10 @@ sqrt(mean((predict(lasso, newx = test2mat, s = "lambda.min") - ytest)^2)) sqrt(mean((predict(lasso_fair, newx = test2fair, s = "lambda.min") - ytest)^2)) # component indices containing "zipcode", i.e. main or interaction effects of zipcode -idx_unfair <- glex:::find_term_matches("zipcode", names(glex_xgb_test$m)) -components_fair <- glex_xgb_test$m[, -idx_unfair, with = FALSE] +idx_unfair = glex:::find_term_matches("zipcode", names(glex_xgb_test$m)) +components_fair = glex_xgb_test$m[, -idx_unfair, with = FALSE] # Prediction = sum of components (w/o zipcode) + intercept (average prediction) -components_fair_sum <- rowSums(components_fair) + glex_xgb_test$intercept +components_fair_sum = rowSums(components_fair) + glex_xgb_test$intercept # "Raw" prediction of XGb with zipcode removed sqrt(mean((components_fair_sum - ytest)^2)) @@ -93,7 +93,7 @@ c( sort() # Variable importance of components containing zipcode -vi <- glex_vi(glex_xgb_test) +vi = glex_vi(glex_xgb_test) # VIs of everything containing zipcode: mostly 2nd order effects vi[glex:::find_term_matches("zipcode", vi$term), ] |> diff --git a/attic/surv-test.R b/attic/surv-test.R index 3f01aef..81ac88c 100644 --- a/attic/surv-test.R +++ b/attic/surv-test.R @@ -2,7 +2,7 @@ library(mlr3proba) library(mlr3extralearners) library(mlr3pipelines) -task <- tsk("lung") +task = tsk("lung") lrn_xgb <- po("encode") %>>% lrn("surv.xgboost.cox", nrounds = 500, max_depth = 3) |> @@ -11,7 +11,7 @@ lrn_xgb <- po("encode") %>>% lrn_xgb$train(tsk("lung")) xgbmod <- lrn_xgb$model$surv.xgboost.cox$model$model -penc <- po("encode") +penc = po("encode") X <- penc$train(list(task))[[1]]$data(cols = task$feature_names) xg_glex <- glex::glex(xgbmod, x = X) From b06775ae09a34dee32f60f82b1ce28fa9368eb78 Mon Sep 17 00:00:00 2001 From: Lukas Burk Date: Sat, 18 Jul 2026 03:10:47 +0200 Subject: [PATCH 6/6] add air exclusion --- air.toml | 1 + 1 file changed, 1 insertion(+) 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/"]