knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
ppforest2 trains trees by composing six pluggable strategies:
| Strategy | Purpose | Built-in |
|----------|---------|----------|
| PP (projection pursuit) | Find the projection that best separates groups | pp_pda() — Penalized Discriminant Analysis |
| Vars (variable selection) | Select which variables are available at each split | vars_uniform(), vars_all() |
| Threshold (split cutpoint) | Compute the split cutpoint in projected space | cutpoint_mean_of_means() |
| Stop (stopping rule) | Decide when to stop growing | stop_pure_node() |
| Binarize (binarization) | Reduce multiclass to binary at each node | binarize_largest_gap() |
| Grouping (group partition) | Route observations to children | grouping_by_label() |
You can add new strategies without modifying the core tree-building logic. This vignette walks through the process.
Each strategy is an R list with a name field that identifies it, a display_name for summaries, and any parameters the strategy needs. The name must match a C++ strategy registered under the same name.
library(ppforest2) pp_pda(0.5) vars_uniform(n_vars = 2) cutpoint_mean_of_means()
When you call pptr() or pprf(), the strategy lists are passed to C++, where the name field dispatches to the corresponding C++ implementation. The actual computation (optimization, variable selection, cutpoint) happens entirely in C++.
Adding a strategy requires work on both sides:
Each strategy family has a base class with pure virtual methods. Your new strategy inherits from the appropriate base and implements them.
For example, a new projection pursuit strategy needs to implement optimize() (find the best projection):
// File: core/src/models/strategies/pp/MyMethod.hpp #pragma once #include "models/strategies/pp/ProjectionPursuit.hpp" #include "models/strategies/Strategy.hpp" #include "utils/JsonValidation.hpp" namespace ppforest2::pp { struct MyMethod : public ProjectionPursuit { explicit MyMethod(float alpha) : alpha_(alpha) {} std::string display_name() const override { return "My method"; } Result optimize( const types::FeatureMatrix& x, const stats::GroupPartition& group_spec) const override { // Find the optimal projector for the data. // Return Result{ projector_vector, index_value }. ... } nlohmann::json to_json() const override { return {{"name", "my_method"}, {"alpha", alpha_}}; } static ProjectionPursuit::Ptr from_json(const nlohmann::json& j) { JsonReader{j, "my_method"}.only_keys({"name", "alpha"}); return my_method(j.at("alpha").get<float>()); } PPFOREST2_REGISTER_STRATEGY(ProjectionPursuit, "my_method") private: const float alpha_; }; inline ProjectionPursuit::Ptr my_method(float alpha) { return std::make_shared<MyMethod>(alpha); } } // namespace ppforest2::pp
The key pieces:
to_json() serializes the strategy name and parameters. This is used for model persistence.from_json() deserializes from JSON and validates that no unexpected keys are present.PPFOREST2_REGISTER_STRATEGY registers the factory so JSON deserialization finds it automatically.display_name() returns a human-readable label for summaries.my_method()) is a convenience wrapper.The same pattern applies to variable selection strategies (select()), cutpoint strategies (cutpoint()), and the other strategy families. See the C++ documentation for complete interface definitions and examples.
After writing the .cpp file, add it to core/src/models/CMakeLists.txt.
Write an R function that creates a strategy list. The name field must match the C++ registration name exactly.
#' My custom projection pursuit strategy. #' #' @param alpha A tuning parameter. #' @return A \code{pp_strategy} object. #' @export pp_my_method <- function(alpha = 1.0) { if (!is.numeric(alpha) || length(alpha) != 1) stop("`alpha` must be a single number.") structure( list(name = "my_method", display_name = "My method", alpha = alpha), class = "pp_strategy" ) }
The constructor should:
pp_strategy, vars_strategy, cutpoint_strategy, stop_strategy, binarize_strategy, or grouping_strategy. This is checked by resolve_strategies().display_name for readable output in summary().to_json() in C++. The R list is converted to JSON and passed to from_json() on the C++ side.Once both sides are in place, the new strategy works like any built-in:
# Single tree tree <- pptr(Species ~ ., data = iris, pp = pp_my_method(alpha = 0.5)) # Forest forest <- pprf(Species ~ ., data = iris, pp = pp_my_method(alpha = 0.5), vars = vars_uniform(n_vars = 2)) # Summary shows the strategy summary(tree)
The strategy is also available from the CLI:
ppforest2 train -d iris.csv --pp my_method:alpha=0.5
And models trained with the new strategy can be saved and loaded as usual --- the JSON registry handles serialization automatically.
Controls how the tree finds the best linear combination of variables at each node.
index(x, group_spec, projector) -> scalar
optimize(x, group_spec) -> Result{projector, index}
optimize() is the main method. It receives the data matrix and group partition and returns the best projection vector. index() evaluates a given projection (used for variable importance calculations).
Controls which variables are available to projection pursuit at each split. This is what makes random forests "random".
select(x, group_spec, rng) -> Result{selected_indices, original_cols}
The returned VariableSelection::Result tracks which columns were selected so the reduced-space projector can be expanded back to the full feature space.
Controls where the split cutpoint is placed in the projected space.
cutpoint(group_1, group_2, projector) -> scalar
Receives the two groups (already partitioned by projection pursuit) and the projection vector. Returns the cutpoint value.
Controls when to stop growing the tree.
should_stop(group_partition, depth) -> bool
Controls how multiclass nodes (>2 groups) are reduced to a binary problem.
regroup(projected_x, group_partition) -> Result
Controls how observations are routed to children after a split.
split(partition, lower_group, upper_group) -> Result
core/src/models/strategies/<family>/MyStrategy.hpp (and .cpp if needed).ProjectionPursuit, VariableSelection, Cutpoint, StopRule, Binarization, or Grouping).to_json() with a "name" field.display_name() for human-readable summaries.static Ptr from_json() with key validation.PPFOREST2_REGISTER_STRATEGY(Base, "name")..cpp to core/src/models/CMakeLists.txt.MyStrategy.test.cpp (JSON round-trip + functional).display_name, and the correct S3 class.Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.