| Title: | Fast Interpretable Greedy-Tree Sums for Tree Ensembles |
|---|---|
| Description: | Flexible, interpretable machine learning algorithm for additive tree sums (FIGS). Fits a sum of shallow classification and regression trees (CART) by greedily minimizing residual impurity, growing a new tree or deepening an existing one at each step, whichever reduces the residuals most. Supports regression and two-class classification, variable importance, bootstrap ensembling and seamless integration with 'parsnip' and 'tidymodels' workflows. The method is described in Tan et al. (2023) <doi:10.1073/pnas.2310151122>. |
| Authors: | João Paulo Assis Bonifácio [aut, cre, cph] (ORCID: <https://orcid.org/0009-0009-2335-8391>), Geraldo Magela da Cruz Pereira [aut, cph] (ORCID: <https://orcid.org/0000-0001-6280-4870>), Pedro Mambelli Fernandes [aut, cph] (ORCID: <https://orcid.org/0009-0001-1695-279X>), João Vitor Andrade Alves de Souza [aut, cph] (ORCID: <https://orcid.org/0009-0006-2780-318X>) |
| Maintainer: | João Paulo Assis Bonifácio <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.0 |
| Built: | 2026-09-23 17:35:36 UTC |
| Source: | https://github.com/bonijoao/figsr |
bagging_figs() fits an ensemble of FIGS models using bootstrap resampling
to increase stability and predictive performance on noisy datasets.
bagging_figs( formula, data, n_estimators = 10, max_splits = 6, min_n = 5, mode = "regression", na_method = c("omit", "mia"), ... )bagging_figs( formula, data, n_estimators = 10, max_splits = 6, min_n = 5, mode = "regression", na_method = c("omit", "mia"), ... )
formula |
A formula. |
data |
A data frame. |
n_estimators |
Integer. Number of bootstrap FIGS models to train. Default is 10. |
max_splits |
Integer. Maximum splits per individual FIGS model. Default is 6. |
min_n |
Integer. Minimum node size. Default is 5. |
mode |
Character. |
na_method |
Character. |
... |
Additional arguments passed to |
An object of class bagging_figs_fit.
set.seed(42) df <- data.frame(x1 = rnorm(50), x2 = rnorm(50), y = rnorm(50)) bag_fit <- bagging_figs(y ~ x1 + x2, data = df, n_estimators = 3)set.seed(42) df <- data.frame(x1 = rnorm(50), x2 = rnorm(50), y = rnorm(50)) bag_fit <- bagging_figs(y ~ x1 + x2, data = df, n_estimators = 3)
figs() fits a Fast Interpretable Greedy-Tree Sums model for regression or
binary classification tasks.
figs( formula, data, max_splits = 10, max_trees = NULL, min_n = 5, mode = "regression", subset = NULL, na.action = stats::na.omit, na_method = c("omit", "mia"), ... )figs( formula, data, max_splits = 10, max_trees = NULL, min_n = 5, mode = "regression", subset = NULL, na.action = stats::na.omit, na_method = c("omit", "mia"), ... )
formula |
A formula specifying outcome and predictor variables. |
data |
A data frame containing training data. |
max_splits |
Integer. Maximum total number of splits across all trees in the sum. Default is 10. |
max_trees |
Integer. Maximum number of trees allowed in the sum. Default is NULL (unconstrained up to max_splits). |
min_n |
Integer. Minimum number of observations required in a node to split. Default is 5. |
mode |
Character. Either |
subset |
An optional vector selecting the rows of |
na.action |
A function describing what to do with missing values, passed
to |
na_method |
Character. How to treat missing predictor values:
|
... |
Additional arguments, currently ignored. Case weights are not
supported and passing |
Trees are grown one split at a time. At each step every leaf of every existing
tree, plus the root of a possible new tree, competes for the split that most
reduces the residual sum of squares; the single best candidate is taken and
the residuals are recomputed against the whole sum. Growth stops at
max_splits splits or when no candidate improves the fit.
Classification is limited to two classes. A factor outcome with more than two levels raises an error. The binary case is fitted on the 0/1 encoding of the outcome using the same sum-of-squares criterion, so the sum of the leaf values already estimates the probability of the second level; it is clamped to the unit interval at prediction time rather than passed through a link function.
Predictors are used exactly as supplied: numeric features are split at
midpoints between sorted unique values, capped at 30 sample quantiles, and
factor features are split by enumerating subsets of their levels, which is
skipped above 10 levels. A factor level that was not seen in training raises
an error at prediction time, as it does for the other model-frame based
fitting functions in stats.
Missing predictor values are governed by na_method. With "omit", the
default, rows with a missing predictor are dropped at fit time by
stats::model.frame() and raise an error at prediction time. With "mia"
(missingness incorporated in attributes, Twala et al. 2008) every row is
kept: for a predictor with missing values the split search also tries
sending them to the left or to the right of each cutpoint, and splitting on
whether the value is missing at all, and keeps whichever reduces the
residual sum of squares most. The learned direction is used at prediction
time. A missing value in a predictor that was complete in training raises
an error, because no direction was learned; impute such values first, for
example with recipes::step_impute_knn(). Even under "mia", a branch of
the tree that no missing value reached during training has not learned a
direction, so a missing value reaching that same branch at prediction time
still raises an error. Rows with a missing outcome are dropped with a
warning under na_method = "mia" only; under "omit" they are dropped
silently by stats::na.omit().
An object of class figsr_fit containing fitted tree structures, predictions, and metadata.
Twala, B. E. T. H., Jones, M. C. and Hand, D. J. (2008). Good methods for coping with missing data in decision trees. Pattern Recognition Letters, 29(7), 950-956.
set.seed(123) df <- data.frame( x1 = rnorm(100), x2 = rnorm(100), y = rnorm(100) ) fit <- figs(y ~ x1 + x2, data = df, max_splits = 4) print(fit)set.seed(123) df <- data.frame( x1 = rnorm(100), x2 = rnorm(100), y = rnorm(100) ) fit <- figs(y ~ x1 + x2, data = df, max_splits = 4) print(fit)
figs_tree() defines a Fast Interpretable Greedy-Tree Sums model for use with
the parsnip and tidymodels ecosystem.
figs_tree( mode = "regression", engine = "figsr", max_splits = NULL, max_trees = NULL, min_n = NULL )figs_tree( mode = "regression", engine = "figsr", max_splits = NULL, max_trees = NULL, min_n = NULL )
mode |
A single character string for the prediction outcome mode:
|
engine |
A single character string for the computational engine. Only
|
max_splits |
An integer for the maximum total splits across all trees.
|
max_trees |
An integer for the maximum number of trees. |
min_n |
An integer for the minimum number of data points in a node to
split. |
A parsnip model specification object.
library(parsnip) spec <- figs_tree(max_splits = 8) |> set_engine("figsr") |> set_mode("regression") speclibrary(parsnip) spec <- figs_tree(max_splits = 8) |> set_engine("figsr") |> set_mode("regression") spec
figsr_fit Modelfigsr_importance() computes the total reduction in residual impurity (Sum of Squares gain)
attributable to each predictor feature across all trees in the fitted FIGS model.
figsr_importance(object, relative = TRUE)figsr_importance(object, relative = TRUE)
object |
A fitted |
relative |
Logical. If |
Each split node stores the reduction in residual sum of squares it achieved at the moment it was chosen. Importance for a feature is the sum of those gains over every split made on that feature, across all trees in the sum. Features never selected receive a gain of zero.
When the model was fitted with na_method = "mia", a split on whether a
predictor is missing is reported on its own row, named missing(<predictor>),
because such a split never consults the predictor's value. A split that
merely sends missing values to one side is credited to the predictor.
A tibble with one row per predictor and columns feature, gain
(raw sum-of-squares reduction) and importance, sorted by decreasing
importance.
set.seed(42) df <- data.frame(x1 = rnorm(50), x2 = rnorm(50)) df$y <- 2 * (df$x1 > 0) + rnorm(50, sd = 0.2) fit <- figs(y ~ x1 + x2, data = df, max_splits = 3) figsr_importance(fit)set.seed(42) df <- data.frame(x1 = rnorm(50), x2 = rnorm(50)) df$y <- 2 * (df$x1 > 0) + rnorm(50, sd = 0.2) fit <- figs(y ~ x1 + x2, data = df, max_splits = 3) figsr_importance(fit)
fit_figs() is the function parsnip calls when fitting a figs_tree()
specification with the "figsr" engine. It accepts either the formula
interface or the x/y interface and forwards to figs(). Users normally
call figs() or fit() instead.
fit_figs( formula = NULL, data = NULL, x = NULL, y = NULL, max_splits = 10, max_trees = NULL, min_n = 5, mode = "regression", na_method = c("omit", "mia"), ... )fit_figs( formula = NULL, data = NULL, x = NULL, y = NULL, max_splits = 10, max_trees = NULL, min_n = 5, mode = "regression", na_method = c("omit", "mia"), ... )
formula |
A formula specifying outcome and predictors. |
data |
A data frame containing the training data. |
x |
A data frame or matrix of predictors. |
y |
An outcome vector. |
max_splits |
Integer. Maximum total number of splits across all trees. |
max_trees |
Integer. Maximum number of trees in the sum. |
min_n |
Integer. Minimum number of observations in a node to split. |
mode |
Character. Either |
na_method |
Character. |
... |
Additional arguments passed to |
An object of class figsr_fit.
set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) fit_figs(x = df[, c("x1", "x2")], y = df$y, max_splits = 3)set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) fit_figs(x = df[, c("x1", "x2")], y = df$y, max_splits = 3)
Dials Parameter for Maximum Splits
max_splits(range = c(2L, 20L), trans = NULL)max_splits(range = c(2L, 20L), trans = NULL)
range |
A numeric vector of length 2 for lower and upper limits. |
trans |
A trans object. |
A dials parameter object.
max_splits() max_splits(range = c(2L, 40L))max_splits() max_splits(range = c(2L, 40L))
Dials Parameter for Maximum Trees
max_trees(range = c(1L, 10L), trans = NULL)max_trees(range = c(1L, 10L), trans = NULL)
range |
A numeric vector of length 2 for lower and upper limits. |
trans |
A trans object. |
A dials parameter object.
max_trees() max_trees(range = c(1L, 5L))max_trees() max_trees(range = c(1L, 5L))
Diagnostic measurements for 768 women of Pima Indian heritage, aged at
least 21, together with whether diabetes was diagnosed within five years.
Five columns carry physiologically impossible zeros that are, in fact,
missing measurements; they are recoded to NA in this copy of the data,
as documented under Details.
pima_diabetespima_diabetes
A data frame with 768 rows and 9 columns:
Number of times pregnant.
Plasma glucose concentration at 2 hours in an oral glucose tolerance test.
Diastolic blood pressure (mm Hg).
Triceps skinfold thickness (mm).
2-hour serum insulin (mu U/mL).
Body mass index, weight in kg divided by height in m, squared.
A score of diabetes likelihood based on family history.
Age in years.
Factor with levels "no" and "yes": whether diabetes
was diagnosed within five years.
Glucose, BloodPressure, SkinThickness, Insulin and BMI cannot be
zero in a living patient; a 0 recorded for any of them is a missing
measurement encoded as if it were data, a well-documented quirk of this
data set. Those zeros are recoded to NA here, the standard first
preprocessing step for it (see data-raw/pima_diabetes.R in the source
package for the exact recoding). Insulin is missing in 48.7% of records
and SkinThickness in 29.6%; only 51% of rows are complete on all five.
Whether a measurement is missing is not unrelated to the outcome; see
vignette("missing-values", package = "figsr") for a worked example using
na_method = "mia".
Smith, J. W., Everhart, J. E., Dickson, W. C., Knowler, W. C., and Johannes, R. S. (1988). Using the ADAP learning algorithm to forecast the onset of diabetes mellitus. Proceedings of the Symposium on Computer Applications and Medical Care, 261-265. Distributed by the UCI Machine Learning Repository and, from there, on Kaggle as https://www.kaggle.com/datasets/uciml/pima-indians-diabetes-database.
data(pima_diabetes) summary(pima_diabetes)data(pima_diabetes) summary(pima_diabetes)
figsr_fit Modelsplot.figsr_fit() visualizes individual decision trees comprising the
Fast Interpretable Greedy-Tree Sums (FIGS) model using 100% Base R graphics
(zero external package dependencies).
## S3 method for class 'figsr_fit' plot( x, tree_idx = NULL, style = c("scientific", "modern", "classic"), main = NULL, tree_names = NULL, ... )## S3 method for class 'figsr_fit' plot( x, tree_idx = NULL, style = c("scientific", "modern", "classic"), main = NULL, tree_names = NULL, ... )
x |
A fitted |
tree_idx |
Optional integer vector specifying indices of trees to plot. Default is |
style |
Character string specifying the visual style: |
main |
Optional character string. An overall title drawn once above
every tree panel. Default is |
tree_names |
Optional character vector overriding the per-tree panel
titles ( |
... |
Additional arguments, currently ignored. |
Invisible NULL.
set.seed(42) df <- data.frame(x1 = rnorm(50), x2 = rnorm(50), y = rnorm(50)) fit <- figs(y ~ x1 + x2, data = df, max_splits = 3) plot(fit) plot(fit, style = "scientific") plot(fit, tree_idx = 1) plot(fit, main = "FIGS tree sum", tree_names = "one tree of the sum")set.seed(42) df <- data.frame(x1 = rnorm(50), x2 = rnorm(50), y = rnorm(50)) fit <- figs(y ~ x1 + x2, data = df, max_splits = 3) plot(fit) plot(fit, style = "scientific") plot(fit, tree_idx = 1) plot(fit, main = "FIGS tree sum", tree_names = "one tree of the sum")
figsr_fit
Internal / Parsnip Predict Bridge Function for figsr_fit
predict_figs(object, new_data, type = "numeric", ...)predict_figs(object, new_data, type = "numeric", ...)
object |
A fitted |
new_data |
A data frame of new predictor observations. |
type |
Character. |
... |
Additional arguments. |
A tibble of predictions.
set.seed(42) df <- data.frame(x1 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) fit <- figs(y ~ x1, data = df, max_splits = 3) predict_figs(fit, new_data = df, type = "numeric")set.seed(42) df <- data.frame(x1 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) fit <- figs(y ~ x1, data = df, max_splits = 3) predict_figs(fit, new_data = df, type = "numeric")
bagging_figs_fit ModelsPredict Method for bagging_figs_fit Models
## S3 method for class 'bagging_figs_fit' predict(object, new_data, type = NULL, ...)## S3 method for class 'bagging_figs_fit' predict(object, new_data, type = NULL, ...)
object |
A fitted |
new_data |
A data frame of predictor values. |
type |
Character. For classification, either |
... |
Additional arguments. |
A tibble containing averaged ensemble predictions.
set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) bag_fit <- bagging_figs(y ~ x1 + x2, data = df, n_estimators = 3) predict(bag_fit, new_data = df)set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) bag_fit <- bagging_figs(y ~ x1 + x2, data = df, n_estimators = 3) predict(bag_fit, new_data = df)
figsr_fit ModelsPredict Method for figsr_fit Models
## S3 method for class 'figsr_fit' predict(object, new_data, type = NULL, ...)## S3 method for class 'figsr_fit' predict(object, new_data, type = NULL, ...)
object |
A fitted |
new_data |
A data frame of new predictor observations. |
type |
Character. Either |
... |
Additional arguments. |
A tibble with predictions in standardized tidymodels format.
set.seed(42) df <- data.frame(x1 = rnorm(50), y = rnorm(50)) fit <- figs(y ~ x1, data = df) predict(fit, new_data = df)set.seed(42) df <- data.frame(x1 = rnorm(50), y = rnorm(50)) fit <- figs(y ~ x1, data = df) predict(fit, new_data = df)
figsr_fit ModelsPrint Method for figsr_fit Models
## S3 method for class 'figsr_fit' print(x, ...)## S3 method for class 'figsr_fit' print(x, ...)
x |
A |
... |
Additional arguments, currently ignored. |
x, invisibly. Called for the side effect of printing a summary of
the fitted tree sum to the console.
set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) print(figs(y ~ x1 + x2, data = df, max_splits = 3))set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) print(figs(y ~ x1 + x2, data = df, max_splits = 3))
figsr_fit Modelssummary.figsr_fit() prints human-readable decision rules for each tree in the sum.
## S3 method for class 'figsr_fit' summary(object, ...)## S3 method for class 'figsr_fit' summary(object, ...)
object |
A |
... |
Additional arguments, currently ignored. |
object, invisibly. Called for the side effect of printing the
IF-THEN decision rules of each tree to the console.
set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) summary(figs(y ~ x1 + x2, data = df, max_splits = 3))set.seed(42) df <- data.frame(x1 = rnorm(60), x2 = rnorm(60)) df$y <- 2 * (df$x1 > 0) + rnorm(60, sd = 0.2) summary(figs(y ~ x1 + x2, data = df, max_splits = 3))
update() changes the arguments of a figs_tree() specification in place,
the way tidymodels users expect of any model specification.
## S3 method for class 'figs_tree' update( object, max_splits = NULL, max_trees = NULL, min_n = NULL, fresh = FALSE, ... )## S3 method for class 'figs_tree' update( object, max_splits = NULL, max_trees = NULL, min_n = NULL, fresh = FALSE, ... )
object |
A |
max_splits |
An integer for the maximum total splits across all trees. |
max_trees |
An integer for the maximum number of trees. |
min_n |
An integer for the minimum number of data points in a node to split. |
fresh |
Logical. Should the arguments be replaced rather than merged? |
... |
Not used. |
An updated figs_tree() specification.
spec <- figs_tree(max_splits = 4) update(spec, max_splits = 8)spec <- figs_tree(max_splits = 4) update(spec, max_splits = 8)