Nothing
#' @useDynLib ppforest2
#' @importFrom Rcpp evalCpp
#' @importFrom stats model.frame model.matrix model.response formula predict sd terms update fitted residuals nobs
NULL
#' Trains a Projection-Pursuit oblique decision tree.
#'
#' This function trains a Projection-Pursuit oblique decision tree using either a formula and data frame interface or a matrix-based interface. When using the formula interface, specify the model formula and the data frame containing the variables. For the matrix-based interface, provide matrices for the features and labels directly.
#' If \code{lambda = 0}, the model is trained using Linear Discriminant Analysis (LDA). If \code{lambda > 0}, the model is trained using Penalized Discriminant Analysis (PDA).
#'
#' Mode is taken from the \code{mode} argument when explicit, and otherwise auto-detected from `y` (factor/character → classification, numeric → regression). Pass \code{mode = "classification"} to force classification on integer labels (e.g. binary 0/1), or \code{mode = "regression"} to assert intent on numeric responses.
#'
#' @param formula A formula of the form \code{y ~ x1 + x2 + ...}, where \code{y} is a vector of labels and \code{x1}, \code{x2}, ... are the features.
#' @param data A data frame containing the variables in the formula.
#' @param x A matrix containing the features for each observation.
#' @param y A matrix containing the labels for each observation.
#' @param mode Training mode: either \code{"classification"} or \code{"regression"}. When \code{NULL} (default), mode is auto-detected from \code{y}'s type — factor or character vectors trigger classification, numeric vectors trigger regression. Setting it explicitly is useful for the binary-integer-labels case (\code{mode = "classification"} with integer 0/1 labels) and for failing fast on a type mismatch (\code{mode = "regression"} with a factor \code{y} errors immediately).
#' @param lambda A regularization parameter (default: 0.5). If \code{lambda = 0}, the model is trained using Linear Discriminant Analysis (LDA). If \code{lambda > 0}, the model is trained using Penalized Discriminant Analysis (PDA). The default uses PDA because pure LDA (\code{lambda = 0}) is ill-conditioned when there are more variables than effective observations (see the "Known limitations" section of the README). Cannot be used together with \code{pp}.
#' @param seed An optional integer seed for reproducibility. If \code{NULL} (default), a seed is drawn from R's RNG, so \code{set.seed()} controls reproducibility. If an integer is provided, that value is used directly.
#' @param pp A projection pursuit strategy object created by \code{\link{pp_pda}}. Cannot be used together with \code{lambda}.
#' @param cutpoint A split cutpoint strategy object created by \code{\link{cutpoint_mean_of_means}} (default).
#' @param stop A stopping rule object. Default depends on mode:
#' \code{\link{stop_pure_node}()} for classification, and
#' \code{stop_any(stop_min_size(5), stop_min_variance(0.01))} for regression.
#' @param binarize A binarization strategy object. Default depends on mode:
#' \code{\link{binarize_largest_gap}()} for classification, and
#' \code{\link{binarize_disabled}()} for regression (regression's default
#' grouping always yields a 2-group partition, so no binarization is needed).
#' @param grouping A grouping strategy object. Default depends on mode:
#' \code{\link{grouping_by_label}()} for classification, and
#' \code{\link{grouping_by_cutpoint}()} for regression.
#' @param leaf A leaf strategy object. Default depends on mode:
#' \code{\link{leaf_majority_vote}()} for classification, and
#' \code{\link{leaf_mean_response}()} for regression.
#' @return A \code{pptr} model. Its S3 class vector is
#' \code{c("pptr_classification", "pptr", "ppmodel")} or
#' \code{c("pptr_regression", "pptr", "ppmodel")} depending on the mode.
#' @seealso \code{\link{predict.pptr_classification}}, \code{\link{predict.pptr_regression}}, \code{\link{formula.ppmodel}}, \code{\link{print.pptr}}, \code{\link{save_json}}, \code{\link{load_json}}, \code{\link{pp_tree}} for parsnip integration
#' @examples
#'
#' # Example 1: formula interface with the `iris` dataset
#' pptr(Species ~ ., data = iris)
#'
#' # Example 2: formula interface with the `iris` dataset with regularization
#' pptr(Species ~ ., data = iris, lambda = 0.5)
#'
#' # Example 3: matrix interface with the `iris` dataset
#' pptr(x = iris[, 1:4], y = iris[, 5])
#'
#' @export
pptr <- function(
formula = NULL,
data = NULL,
x = NULL,
y = NULL,
mode = NULL,
lambda = 0.5,
seed = NULL,
pp = NULL,
cutpoint = NULL,
stop = NULL,
binarize = NULL,
grouping = NULL,
leaf = NULL) {
# See the matching comment in `pprf()` — capture the call up front so
# `update()` can rebuild and re-evaluate it.
cl <- match.call()
if (!is.null(seed) && (!is.numeric(seed) || length(seed) != 1 || seed != as.integer(seed)))
stop("`seed` must be a single integer or NULL.")
args <- resolve_model_data(formula, data, x, y, mode = mode)
mode <- args$mode
strategies <- resolve_strategies(
pp = pp, lambda = lambda, lambda_missing = missing(lambda),
cutpoint = cutpoint, stop = stop, binarize = binarize, grouping = grouping,
leaf = leaf)
x <- args$x
y <- args$y
groups <- args$groups
formula <- args$formula
if (is.null(seed)) {
seed <- sample.int(.Machine$integer.max, 1L)
}
training_spec <- list(
pp = strategies$pp,
vars = strategies$vars,
cutpoint = strategies$cutpoint,
stop = strategies$stop,
binarize = strategies$binarize,
grouping = strategies$grouping,
leaf = strategies$leaf,
mode = mode,
size = 0L,
seed = as.integer(seed),
threads = 0L,
max_retries = 3L)
# `ppforest2_train` is mode-aware on the C++ side: it dispatches on
# `training_spec$mode` and applies the appropriate index decode + sort.
model <- ppforest2_train(training_spec, args$x, args$y)
if (isTRUE(model$degenerate)) {
warning("Some splits could not separate groups (degenerate nodes). ",
"This can be caused by ill-conditioned variables in the input data. ",
"Degenerate nodes predict the group with the most observations.",
call. = FALSE)
}
model$call <- cl
model$seed <- seed
model$groups <- groups
model$formula <- formula
model$mode <- mode
model$x <- x
model$y <- y
scale <- apply(x, 2, sd)
scale[scale == 0] <- 1
model$vi <- list(
scale = scale,
projections = ppforest2_vi_projections_tree(model, ncol(x), scale)
)
model$.cache <- .new_cache()
# Class is set by the Rcpp wrap layer (see `make_model_class` in
# bindings/R/inst/include/ppforest2.h), which derives it from
# `tree.training_spec->mode`. Don't reassign here — that would let the
# R-side guess drift away from the C++ truth.
model
}
# ---------------------------------------------------------------------------
# Prediction: split per mode.
# ---------------------------------------------------------------------------
#' Predicts labels or per-group one-hot proportions from a pptr model (classification mode).
#'
#' @param object A \code{pptr_classification} model.
#' @param new_data A data frame or matrix of new observations. If \code{NULL}, the first positional argument in \code{...} is used for backward compatibility.
#' @param type \code{"class"} (default) returns a factor of predicted labels; \code{"prob"} returns a data frame with 1.0 for the predicted group and 0.0 elsewhere.
#' @param ... Backward-compat positional `new_data`.
#' @return A factor or data frame.
#' @seealso \code{\link{pptr}}, \code{\link{predict.pptr_regression}}
#' @export
predict.pptr_classification <- function(object, new_data = NULL, type = NULL, ...) {
x <- process_predict_arguments(object, new_data, ...)
if (is.null(type)) type <- "class"
if (type == "prob") {
probs <- ppforest2_predict_tree_prob(object, x)
df <- as.data.frame(probs)
colnames(df) <- object$groups
return(df)
}
if (type != "class") {
stop("`type = \"", type, "\"` is not supported for classification trees. ",
"Use \"class\" (default) or \"prob\".", call. = FALSE)
}
y <- ppforest2_predict_tree(object, x)
as.factor(object$groups[y])
}
#' Predicts numeric responses from a pptr model (regression mode).
#'
#' @param object A \code{pptr_regression} model.
#' @param new_data A data frame or matrix of new observations.
#' @param type Must be \code{"response"} (default).
#' @param ... Backward-compat positional `new_data`.
#' @return A numeric vector.
#' @seealso \code{\link{pptr}}, \code{\link{predict.pptr_classification}}
#' @export
predict.pptr_regression <- function(object, new_data = NULL, type = NULL, ...) {
x <- process_predict_arguments(object, new_data, ...)
if (is.null(type)) type <- "response"
if (type %in% c("class", "prob")) {
stop("`type = \"", type, "\"` is not available for regression models. ",
"Use `type = \"response\"`.", call. = FALSE)
}
if (type != "response") {
stop("`type = \"", type, "\"` is not recognised. Use \"response\".", call. = FALSE)
}
as.numeric(ppforest2_predict_tree(object, x))
}
# ---------------------------------------------------------------------------
# print.pptr -- tree structure. Leaf label formatting is mode-specific via
# the `print_node` generic dispatched on the model's class.
# ---------------------------------------------------------------------------
#' Prints the structure of a pptr tree.
#' @param x A \code{pptr} model.
#' @param ... Unused.
#' @return Invisibly returns the input \code{pptr} model \code{x} (unchanged).
#' Called for its side effect of printing the tree structure -- the oblique
#' split rules and leaf predictions -- to the console.
#' @export
print.pptr <- function(x, ...) {
cat("\n")
if (!is.null(x$call)) {
cat("Call: ", paste(deparse(x$call, width.cutoff = 80L), collapse = "\n "), "\n\n", sep = "")
}
cat("Projection-Pursuit Oblique Decision Tree:\n")
print_node(x, x$root)
cat("\n")
invisible(x)
}
# Internal generic: walks the tree recursively, dispatching on the model's
# class to format leaf values differently per mode.
print_node <- function(model, node, depth = 0) UseMethod("print_node")
#' @export
print_node.pptr_classification <- function(model, node, depth = 0) {
.print_node_impl(model, node, depth, function(value) model$groups[value])
}
#' @export
print_node.pptr_regression <- function(model, node, depth = 0) {
.print_node_impl(model, node, depth, function(value) format(as.numeric(value), digits = 4))
}
# Shared recursion. `format_leaf` turns the raw leaf value into a display string.
.print_node_impl <- function(model, node, depth, format_leaf) {
indent <- paste(rep(" ", depth), collapse = "")
if (!is.null(node$value)) {
cat(indent, "Predict:", format_leaf(node$value), "\n")
return(invisible(NULL))
}
projection_str <- paste(
"[", paste(round(node$projector, 2), collapse = " "), "] * x",
collapse = ""
)
cat(indent, "If (", projection_str, ") < ", node$cutpoint, ":\n", sep = "")
if (!is.null(node$lower)) {
print_node(model, node$lower, depth + 1)
}
cat(indent, "Else:\n", sep = "")
if (!is.null(node$upper)) {
print_node(model, node$upper, depth + 1)
}
}
# ---------------------------------------------------------------------------
# summary -- layered via NextMethod:
# summary.pptr_classification / summary.pptr_regression
# -> summary.pptr (tree-level header + VI table)
# -> summary.ppmodel (data summary block)
# ---------------------------------------------------------------------------
#' @export
summary.pptr <- function(object, ...) {
model <- object
if (is.null(model$x)) {
cat("\n(Empty pptr model -- no training data available.)\n")
return(invisible(model))
}
cat("\n")
cat(if (identical(model$mode, "regression")) {
"Projection-Pursuit Oblique Regression Tree\n"
} else {
"Projection-Pursuit Oblique Decision Tree\n"
})
cat("\n")
print_training_spec(model$training_spec)
NextMethod() # summary.ppmodel
invisible(model)
}
#' @export
summary.pptr_classification <- function(object, ...) {
NextMethod()
model <- object
cat("Confusion Matrix:\n\n")
print_confusion_matrix(ppforest2_predict_tree(model, model$x), model)
cat("\n")
.print_vi_table(model, include_oob_importances = FALSE)
invisible(model)
}
#' @export
summary.pptr_regression <- function(object, ...) {
NextMethod()
model <- object
preds <- ppforest2_predict_tree(model, model$x)
y <- model$y
mse <- mean((preds - y)^2)
mae <- mean(abs(preds - y))
ss_tot <- sum((y - mean(y))^2)
r2 <- if (ss_tot > 0) 1 - sum((preds - y)^2) / ss_tot else 0
cat("Training Metrics:\n")
cat(" MSE:", format(mse, nsmall = 6), "\n")
cat(" MAE:", format(mae, nsmall = 6), "\n")
cat(" R\u00b2: ", format(r2, nsmall = 6), "\n\n")
.print_vi_table(model, include_oob_importances = FALSE)
invisible(model)
}
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.