| Title: | Random Planted Forest: A Directly Interpretable Tree Ensemble |
|---|---|
| Description: | Implements Random Planted Forest (Hiabu, Mammen and Meyer, 2020) <doi:10.48550/arXiv.2012.14563>, a directly interpretable tree ensemble for regression and classification. Trees are restricted to a maximum number of interacting predictors, so the fitted model decomposes exactly into main effects and low-order interactions, a functional ANOVA decomposition that can be extracted for interpretation. Fitting and prediction are multithreaded, and fitted models can be serialized. |
| Authors: | Joseph Theo Meyer [aut], Munir Hiabu [aut] (ORCID: <https://orcid.org/0000-0001-5846-667X>), Maike Spankus [aut], Marvin N. Wright [aut] (ORCID: <https://orcid.org/0000-0002-8542-6291>), Lukas Burk [cre, aut] (ORCID: <https://orcid.org/0000-0001-7528-3795>) |
| Maintainer: | Lukas Burk <[email protected]> |
| License: | Apache License (>= 2) |
| Version: | 0.5.0.9000 |
| Built: | 2026-10-16 17:44:46 UTC |
| Source: | https://github.com/PlantedML/randomPlantedForest |
Method for bundle::bundle() wrapping rpf_marshal()/rpf_unmarshal(),
so rpf models work with the standard tidymodels serialization workflow.
Training data is not included; see rpf_marshal() for the implications.
## S3 method for class 'rpf' bundle(x, ...)## S3 method for class 'rpf' bundle(x, ...)
x |
|
... |
Unused. |
An object of class bundled_rpf for bundle(), or the restored
rpf object for unbundle().
fit <- rpf(mpg ~ wt + cyl, data = mtcars, ntrees = 10) b <- bundle::bundle(fit) tmp <- tempfile(fileext = ".rds") saveRDS(b, tmp) restored <- bundle::unbundle(readRDS(tmp)) predict(restored, mtcars)fit <- rpf(mpg ~ wt + cyl, data = mtcars, ntrees = 10) b <- bundle::bundle(fit) tmp <- tempfile(fileext = ".rds") saveRDS(b, tmp) restored <- bundle::unbundle(readRDS(tmp)) predict(restored, mtcars)
Illustrates how a Random Planted Forest predicts by drawing the trees of one tree family. Each tree belongs to a set of variables, and its leaves are boxes in those variables with a value each. The prediction for a point is the root value plus the values of all leaves containing it, averaged over tree families.
## S3 method for class 'rpf' plot( x, family = 1L, ..., type = c("tree", "boxes"), max_interaction = 2L, trees = NULL, class = NULL, factor_levels = TRUE )## S3 method for class 'rpf' plot( x, family = 1L, ..., type = c("tree", "boxes"), max_interaction = 2L, trees = NULL, class = NULL, factor_levels = TRUE )
x |
|
family |
|
... |
Reserved for future expansion. |
type |
|
max_interaction |
|
trees |
|
class |
|
factor_levels |
|
type = "tree" draws a split diagram in the style of Figure 1b of
Hiabu et al. (2023), with the trees in rows by number of variables, each
leaf filled by its value. The forest stores only its final leaves, not
the order of splits or which leaf a tree grew from, so the splits within
a tree are reconstructed: the diagram shows a split structure consistent
with the leaves, and dashed lines link trees to the trees they could
have grown from. Leaves that overlap other leaves of the same tree hang
from dotted lines. Readable for small fits, e.g. ntrees = 1 and
splits = 10.
type = "boxes" draws one panel per tree in variable space: leaves of
trees on one variable as bars from zero to their value, leaves of trees
on two variables as rectangles filled by their value. Overlapping leaves
add up, which the translucent drawing shows.
Trees on more than max_interaction variables, and trees not in trees,
are not drawn but summarized as a remainder, similar to the remainder term
of predict_components().
After purify(), the trees are the components of the functional
decomposition and only type = "boxes" applies. To plot components
averaged over all tree families, use predict_components(), e.g. with the
plotting functions in glex.
A ggplot for type = "tree", a patchwork for type = "boxes".
Hiabu, M., Mammen, E., Meyer, J. T. (2023). Random Planted Forest: a directly interpretable tree ensemble. arXiv:2012.14563.
rpfit <- rpf(mpg ~ wt + hp, data = mtcars, ntrees = 1, splits = 10) plot(rpfit) purify(rpfit) plot(rpfit, type = "boxes")rpfit <- rpf(mpg ~ wt + hp, data = mtcars, ntrees = 1, splits = 10) plot(rpfit) purify(rpfit) plot(rpfit, type = "boxes")
Prediction components are a functional decomposition of the model prediction. The sum of all components equals the overall predicted value for an observation.
predict_components(object, new_data, max_interaction = NULL, predictors = NULL)predict_components(object, new_data, max_interaction = NULL, predictors = NULL)
object |
|
new_data |
|
max_interaction |
|
predictors |
|
Extracts all possible components up to max_interaction degrees,
up to the value set when calling rpf(). The intercept is always included.
Optionally predictors can be specified to only include components including the given variables.
If max_interaction is greater than length(predictors), the max_interaction will be lowered accordingly.
The components are only unique for a purified forest.
An unpurified forest is purified in place on first use with the defaults of purify().
For other settings, such as more threads, call purify() before predict_components().
A list with elements:
m (data.table): Components for each main effect and
interaction term, representing the functional decomposition of the prediction.
All components together with the intercept sum up
to the prediction.
For multiclass classification, the number of output columns is multiplied by
the number of levels in the outcome.
intercept (numeric): Expected value of the prediction.
For multiclass classification, one value per target level, named by target_levels.
x (data.table): Copy of new_data containing predictors selected
by predictors.
target_levels (character): For multiclass classification only: Vector of target levels
which can be used to disassemble m, as names include both term and target level.
Depending on the number of predictors and max_interaction, the number of components will
increase drastically to sum(choose(ncol(new_data), seq_len(max_interaction))).
# Regression task, only some predictors train <- mtcars[1:20, 1:4] test <- mtcars[21:32, 1:4] set.seed(23) rpfit <- rpf(mpg ~ ., data = train, max_interaction = 3, ntrees = 30) # Extract all components, including main effects and interaction terms up to `max_interaction` (components <- predict_components(rpfit, test)) # sums to prediction cbind( m_sum = rowSums(components$m) + components$intercept, prediction = predict(rpfit, test) ) # Only get components with interactions of a lower degree, ignoring 3-way interactions predict_components(rpfit, test, max_interaction = 2) # Only retrieve main effects (main_effects <- predict_components(rpfit, test, max_interaction = 1)) # The difference is the combined contribution of interaction effects cbind( m_sum = rowSums(main_effects$m) + main_effects$intercept, prediction = predict(rpfit, test) )# Regression task, only some predictors train <- mtcars[1:20, 1:4] test <- mtcars[21:32, 1:4] set.seed(23) rpfit <- rpf(mpg ~ ., data = train, max_interaction = 3, ntrees = 30) # Extract all components, including main effects and interaction terms up to `max_interaction` (components <- predict_components(rpfit, test)) # sums to prediction cbind( m_sum = rowSums(components$m) + components$intercept, prediction = predict(rpfit, test) ) # Only get components with interactions of a lower degree, ignoring 3-way interactions predict_components(rpfit, test, max_interaction = 2) # Only retrieve main effects (main_effects <- predict_components(rpfit, test, max_interaction = 1)) # The difference is the combined contribution of interaction effects cbind( m_sum = rowSums(main_effects$m) + main_effects$intercept, prediction = predict(rpfit, test) )
Random Planted Forest Predictions
## S3 method for class 'rpf' predict( object, new_data, type = ifelse(object$mode == "regression", "numeric", "prob"), nthreads = NULL, ... )## S3 method for class 'rpf' predict( object, new_data, type = ifelse(object$mode == "regression", "numeric", "prob"), nthreads = NULL, ... )
object |
|
new_data |
|
type |
For classification and If |
nthreads |
|
... |
Not currently used, but required for extensibility. Unknown arguments are an error. |
For regression: A tbl with column .pred with
the same number of rows as new_data.
For classification: A tbl with one column .pred_<level>
for each level in y containing class probabilities if type = "prob".
For type = "class", one column .pred_class with class predictions is returned.
For type = "numeric" or "link", raw predictions are returned: one column
.pred for binary outcomes, and one column .pred_<level> per level for
multiclass outcomes. With loss = "logit", the first level is the reference
class and has no column, so K - 1 columns are returned for K levels.
# Regression with L2 loss rpfit <- rpf(y = mtcars$mpg, x = mtcars[, c("cyl", "wt")]) predict(rpfit, mtcars[, c("cyl", "wt")])# Regression with L2 loss rpfit <- rpf(y = mtcars$mpg, x = mtcars[, c("cyl", "wt")]) predict(rpfit, mtcars[, c("cyl", "wt")])
Print an rpf
## S3 method for class 'rpf' print(x, ...)## S3 method for class 'rpf' print(x, ...)
x |
|
... |
Further arguments passed to or from other methods. |
Invisibly: x.
rpf.
rpf(mpg ~ cyl + wt + drat, data = mtcars, max_interaction = 2, ntrees = 10)rpf(mpg ~ cyl + wt + drat, data = mtcars, max_interaction = 2, ntrees = 10)
These methods are provided to avoid flooding the console with long nested lists containing tree structures.
## S3 method for class 'rpf_forest' print(x, ...) ## S3 method for class 'rpf_forest' str(object, ...)## S3 method for class 'rpf_forest' print(x, ...) ## S3 method for class 'rpf_forest' str(object, ...)
x |
|
... |
Further arguments passed to or from other methods. |
object |
|
rpfit <- rpf(mpg ~ cyl + wt, data = mtcars, ntrees = 10) print(rpfit$forest) str(rpfit$forest)rpfit <- rpf(mpg ~ cyl + wt, data = mtcars, ntrees = 10) print(rpfit$forest) str(rpfit$forest)
Purification makes the components of the forest's functional decomposition
unique, which predict_components() relies on. Unless rpf() was called
with purify = TRUE, predict_components() purifies the forest on first
use.
purify(x, ..., maxp_interaction = NULL, mode = 2L, nthreads = NULL) is_purified(x)purify(x, ..., maxp_interaction = NULL, mode = 2L, nthreads = NULL) is_purified(x)
x |
|
... |
Reserved for future expansion. |
maxp_interaction |
|
mode |
|
nthreads |
|
The forest is modified in place: x and every copy of it are purified,
whether or not the result is assigned.
purify() is idempotent, meaning if the forest is already purified it just returns it unmodified.
purify() returns x invisibly. is_purified() returns TRUE or
FALSE.
rpfit <- rpf(mpg ~ ., data = mtcars, max_interaction = 2, ntrees = 10) is_purified(rpfit) purify(rpfit) is_purified(rpfit)rpfit <- rpf(mpg ~ ., data = mtcars, max_interaction = 2, ntrees = 10) is_purified(rpfit) purify(rpfit) is_purified(rpfit)
Fits a random planted forest (Hiabu, Mammen and Meyer, 2020) for regression
or classification. The model is a sum of trees that each split on at most
max_interaction predictors, so it decomposes into main effects and
interaction components; see predict_components().
rpf(x, ...) ## S3 method for class 'formula' rpf( formula, data, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... ) ## S3 method for class 'data.frame' rpf( x, y, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... ) ## S3 method for class 'matrix' rpf( x, y, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... ) ## S3 method for class 'recipe' rpf( x, data, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... )rpf(x, ...) ## S3 method for class 'formula' rpf( formula, data, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... ) ## S3 method for class 'data.frame' rpf( x, y, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... ) ## S3 method for class 'matrix' rpf( x, y, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... ) ## S3 method for class 'recipe' rpf( x, data, max_interaction = 2, ntrees = 50, splits = 30, split_structure = "leaves", split_try = 10, t_try = 0.4, max_candidates = 50, split_decay_rate = 0.1, delete_leaves = TRUE, loss = NULL, delta = 0.001, epsilon = 0.1, purify = FALSE, nthreads = 1, export_forest = FALSE, deterministic = FALSE, ... )
x, y
|
|
... |
Not currently used, but required for extensibility. Unknown (e.g. misspelled) arguments are an error. |
formula |
|
data |
|
max_interaction |
|
ntrees |
|
splits |
|
split_structure |
|
split_try |
|
t_try |
|
max_candidates |
|
split_decay_rate |
|
delete_leaves |
|
loss |
|
delta |
|
epsilon |
|
purify |
|
nthreads |
|
export_forest |
|
deterministic |
|
Start with max_interaction and splits. max_interaction sets which
effects the model can represent: 1 for an additive model, 2 (the
default) to add pairwise interactions, which can still be plotted as
heatmaps. Higher values are more flexible, but slower and the components are
harder to interpret. splits sets how closely the model
follows the data and is the parameter to tune. With a higher
max_interaction, splits are spread over more possible components, so tune
both together.
ntrees averages over bootstrap samples to reduce variance and fitting and
prediction time grow linearly with it. The default is lower than the
usual 500 trees of a random forest because each tree family is itself a
complete model, a sum of trees covering all components, rather than a
single tree. Averaging therefore stops paying off sooner, usually after a
few dozen families. The split search parameters
(split_structure, split_try, t_try, max_candidates,
split_decay_rate) trade speed for a more thorough search.
For classification, "exponential" (the default) and "logit" fit on the
link scale and are transformed to probabilities, "exponential" being faster.
"L1" and "L2" fit class indicators directly and their probabilities are
truncated to [0, 1].
split_structureIn each round, a t_try fraction of all possible splits (capped at
max_candidates) is drawn as candidates with weights
exp(-split_decay_rate * age). split_structure defines what a candidate is
and how its thresholds are evaluated.
leaves: Split candidates are (leaf, split-dimension) pairs. For each sampled
candidate, split_try thresholds are drawn uniformly from the valid range within
that leaf and evaluated to choose the best split.
hist: As leaves, but thresholds are drawn from boundaries of
quantile bins computed once per predictor, which makes evaluating them
faster on large data.
cur_trees_1: Split candidates are (current-tree, split-dimension) pairs. For each
sampled candidate, perform split_try evaluations. Each evaluation samples a leaf
from the set of valid current trees (with probability proportional to its number of
available thresholds) and then uniformly samples a single threshold within that leaf.
cur_trees_2: Split candidates are (current-tree, split-dimension) pairs. For each
sampled candidate, iterate through every
valid leaf. Within each leaf, sample split_try thresholds uniformly and
evaluate them.
res_trees: Split candidates are resulting trees. For each sampled candidate, run
split_try evaluations by sampling a (split-dimension, leaf) pair from all valid
pairs (with probability proportional to its number of available thresholds), then
uniformly sampling one threshold within that pair.
Object of class "rpf" with model object contained in $fit.
Hiabu, M., Mammen, E., & Meyer, J. T. (2020). Random Planted Forest: a directly interpretable tree ensemble. doi:10.48550/arXiv.2012.14563
# Regression with formula rpfit <- rpf(mpg ~ cyl + wt, data = mtcars) # Regression with x and y, allowing pairwise interactions rpfit <- rpf(x = mtcars[, c("cyl", "wt", "hp")], y = mtcars$mpg, max_interaction = 2) # Classification rpfit <- rpf(Species ~ ., data = iris, loss = "exponential")# Regression with formula rpfit <- rpf(mpg ~ cyl + wt, data = mtcars) # Regression with x and y, allowing pairwise interactions rpfit <- rpf(x = mtcars[, c("cyl", "wt", "hp")], y = mtcars$mpg, max_interaction = 2) # Classification rpfit <- rpf(Species ~ ., data = iris, loss = "exponential")
Every tree in a Random Planted Forest is a set of boxes (leaves) with a
value each, and the prediction is the sum of the values of all boxes
containing the point, averaged over tree families.
rpf_boxes() returns the boxes of one tree family as a data frame.
rpf_boxes(x, family = 1L, ...)rpf_boxes(x, family = 1L, ...)
x |
|
family |
|
... |
Reserved for future expansion. |
Before purification, these are the leaves as grown: boxes in one tree may
overlap, and the trees are not an identified decomposition.
After purify(), they are the cells of the purified grid, one
non-overlapping set per component of the functional decomposition.
As purify() modifies the forest in place, extract the unpurified boxes
before purifying.
Factor predictors are encoded as integer codes in the order of
x$factor_levels, with a box covering the codes c with
lower <= c < upper.
A base::data.frame with one row per box and columns
tree: The tree, named by its sorted variables as in predict_components(),
e.g. "x1:x2", or "(Intercept)".
order: The number of variables of the tree, 0 for the intercept.
box: The index of the box within its tree.
value: The value of the box. For multiclass classification, one
column value_<class> per outcome level instead.
<variable>_lower, <variable>_upper for every predictor: The bounds of
the box, NA for variables the tree does not use.
plot.rpf() to plot the boxes.
rpfit <- rpf(mpg ~ wt + hp, data = mtcars, ntrees = 5, splits = 10) head(rpf_boxes(rpfit))rpfit <- rpf(mpg ~ wt + hp, data = mtcars, ntrees = 5, splits = 10) head(rpf_boxes(rpfit))
The C++ forest does not survive saveRDS(); an rpf object restored via
readRDS() without rpf_marshal()/rpf_unmarshal() is unusable.
rpf_is_valid(x)rpf_is_valid(x)
x |
|
TRUE if the underlying model can be used, FALSE otherwise.
rpf_marshal() converts an rpf object into a plain R list safe for
saveRDS(); rpf_unmarshal() reverses it. The C++ forest behind an rpf
object does not survive R serialization, so this explicit round-trip is
required to store or transfer fitted models.
rpf_marshal(x, include_data = FALSE) rpf_unmarshal(blob)rpf_marshal(x, include_data = FALSE) rpf_unmarshal(blob)
x |
|
include_data |
|
blob |
|
Training data is only included with include_data = TRUE; without it, a
restored forest can predict (including purified prediction if the forest
was purified before marshaling) but can never be purified afterwards.
rpf_marshal() returns a list of class rpf_marshaled;
rpf_unmarshal() returns an object of class rpf.
fit <- rpf(mpg ~ wt + cyl, data = mtcars, ntrees = 10) blob <- rpf_marshal(fit) tmp <- tempfile(fileext = ".rds") saveRDS(blob, tmp) restored <- rpf_unmarshal(readRDS(tmp)) all.equal(predict(fit, mtcars), predict(restored, mtcars))fit <- rpf(mpg ~ wt + cyl, data = mtcars, ntrees = 10) blob <- rpf_marshal(fit) tmp <- tempfile(fileext = ".rds") saveRDS(blob, tmp) restored <- rpf_unmarshal(readRDS(tmp)) all.equal(predict(fit, mtcars), predict(restored, mtcars))