---
title: "Random Forest Classification with ggRandomForests"
author: "John Ehrlinger"
date: today
format:
  html:
    fig-format: png
    fig-dpi: 96
    toc: true
    toc-depth: 3
    html-math-method: mathjax
    code-fold: false
bibliography: ggRandomForests.bib
vignette: >
  %\VignetteIndexEntry{Random Forest Classification with ggRandomForests}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

::: {.callout-warning}
## Work in progress
This vignette is under active development. Code examples and narrative may
change before the next release.
:::

```{r setup, include=FALSE}
# vignette dir under R CMD check, package root under an interactive knit
.fo <- c("_fig_optim.R", file.path("vignettes", "_fig_optim.R"))
if (any(file.exists(.fo))) source(.fo[file.exists(.fo)][1])
knitr::opts_chunk$set(
  fig.align = "center",
  fig.width = 7,
  fig.height = 5,
  message = FALSE,
  warning = FALSE,
  comment = "#>"
)
options(mc.cores = 1, rf.cores = 1)
```

# Introduction

Random forests [@Breiman:2001] are a non-parametric ensemble method that
requires no distributional or functional assumptions on how covariates relate
to the response. For classification, each tree votes for a class, and the
forest reports the vote fractions as class probabilities. The
**randomForestSRC** package [@Ishwaran:RFSRC:2014] provides a unified
implementation for survival, regression, and classification forests.

**ggRandomForests** extracts tidy data objects from `rfsrc` fits and renders
them with **ggplot2** [@Wickham:2009], making it straightforward to explore how
a forest is constructed, which variables matter, and how the predicted
probabilities depend on individual predictors.

This vignette demonstrates a complete random forest classification workflow on
Fisher's iris data [@Fisher:1936]:

1. **Data exploration** --- EDA scatter panels, the two variable pairs that
   separate the three species
2. **Growing the forest** --- fitting an RF, checking OOB error convergence
3. **Variable selection** --- VIMP and minimal depth via `max.subtree()`
4. **SHAP analysis** --- per-observation, per-class additive explanations via
   `gg_shap()`
5. **Dependence plots** --- variable dependence and partial dependence via
   `gg_variable()` and `gg_partial_rfsrc()`
6. **Classification performance** --- ROC curves and AUC via `gg_roc()`

```{r packages}
library(ggplot2)
library(dplyr)
library(randomForestSRC)

if (requireNamespace("ggRandomForests", quietly = TRUE)) {
  library(ggRandomForests)
} else if (requireNamespace("pkgload", quietly = TRUE)) {
  pkgload::load_all(export_all = FALSE, helpers = FALSE, attach_testthat = FALSE)
} else {
  stop("Install ggRandomForests (or pkgload for dev builds) to render this vignette.")
}

theme_set(theme_bw())
```

# Data: Iris Flower Measurements

Fisher's iris data [@Fisher:1936] measures sepal and petal length and width
for 150 flowers, 50 each from three species: *setosa*, *versicolor*, and
*virginica*. It is a small data set, and that is a feature here, not a
flaw: every figure in this vignette renders in under a second, and the
structure is well-understood enough that any strange behavior stands out.

```{r data-load}
data(iris)
```

## Exploratory data analysis

Petal measurements separate the three species almost perfectly; sepal
measurements do not.

```{r eda-petal, fig.height=4, fig.cap="Petal length vs. petal width, colored by species."}
ggplot(iris, aes(x = Petal.Length, y = Petal.Width, color = Species)) +
  geom_point(size = 2, alpha = 0.7) +
  scale_color_brewer(palette = "Set2")
```

```{r eda-sepal, fig.height=4, fig.cap="Sepal length vs. sepal width, colored by species."}
ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) +
  geom_point(size = 2, alpha = 0.7) +
  scale_color_brewer(palette = "Set2")
```

*setosa* sits in its own corner of petal space, clean of the other two.
*versicolor* and *virginica* overlap in both plots, more so on sepals than
petals. Keep that overlap in mind --- it is the interesting case for
everything that follows, since *setosa* is trivial for the forest to get
right.

# Growing a Random Forest

We grow a classification forest using all four predictors. The `rfsrc()`
function detects the classification family from the factor response.

```{r rfsrc-fit}
set.seed(42)
rfsrc_iris <- rfsrc(Species ~ ., data = iris,
                    ntree = 200, importance = TRUE, err.block = 5)
rfsrc_iris
```

The forest grew `r rfsrc_iris$ntree` trees. The confusion matrix confirms
what the EDA suggested: every *setosa* is classified correctly, while
*versicolor* and *virginica* trade three misclassifications each way, for
an overall OOB misclassification rate of 4%.

## OOB error convergence

```{r error-plot, fig.height=4, fig.cap="OOB misclassification rate vs. number of trees, overall and per class."}
plot(gg_error(rfsrc_iris))
```

The *setosa* error drops to zero almost immediately. *versicolor* and
*virginica* settle in around 6% each, and the overall rate levels off near
4% well before 200 trees --- the forest is not still learning by the time we
stop growing it.

## OOB predictions

```{r rfsrc-predicted, fig.height=5, fig.cap="OOB predicted class probabilities, faceted by the flower's true species."}
plot(gg_rfsrc(rfsrc_iris))
```

Each panel is one true species; each point is one flower's predicted
probability for one of the three classes. *setosa* flowers pin their
probability at 1 for *setosa* and 0 for the other two --- no ambiguity at
all. The *versicolor* and *virginica* panels show real spread: most flowers
are still classified confidently, but a handful sit in the 0.2--0.6 range
where the forest is genuinely unsure which of the two they are.

# Variable Selection

## Variable importance (VIMP)

VIMP, computed by [randomForestSRC](https://www.randomforestsrc.org), measures
the increase in OOB prediction error when a variable's values are randomly
permuted across the out-of-bag observations [@Breiman:2001]. For a
classification forest, `gg_vimp()` returns one column per class alongside the
overall score, and `plot()` facets on that automatically.

```{r vimp-plot, fig.cap="VIMP ranking, overall and per class."}
plot(gg_vimp(rfsrc_iris))
```

`Petal.Length` and `Petal.Width` dominate every facet, `Sepal.Length` and
`Sepal.Width` barely register --- consistent with the EDA. Look closely at the
*virginica* facet: `Sepal.Length` has a small negative VIMP there (colored
differently), meaning permuting it very slightly *helped* virginica
predictions in this fit. That kind of disagreement across facets is exactly
what a per-class VIMP breakdown is for; a single overall score would have
hidden it. (This is a distinct question from the permutation-vs-varPro
comparison covered in `vignette("varpro", package = "ggRandomForests")`,
which contrasts *how* importance is measured rather than *for which class*.)

## Minimal depth

Minimal depth [@Ishwaran:2010] ranks variables by how close to the root node
they first split, on average.

```{r varsel}
md_iris <- max.subtree(rfsrc_iris)
```

The threshold is `r round(md_iris$threshold, 2)`, selecting
`r length(md_iris$topvars)` variables:
`r paste(md_iris$topvars, collapse = ", ")`. VIMP and minimal depth agree.

# SHAP Analysis

VIMP ranks importance averaged over the whole forest, and it does not change
with the question you ask. SHAP does: a classification forest predicts one
probability per class, and `gg_shap()` explains exactly one of those
probabilities at a time, chosen with the `which.class` argument. Ask for
class 1 (*setosa*) and you get contributions to the *setosa* probability;
switch to class 3 (*virginica*) and every contribution is recomputed for
*virginica* instead. Nothing else about the method changes --- same players,
same game, different payout to split.

We target *virginica* (`which.class = 3`), the harder of the two overlapping
species. With only four predictors, `kernelshap` runs in exact mode, so
explaining all 150 flowers takes about two seconds --- no need for the
random-sample workaround the regression vignette uses on the larger, higher-
dimensional Boston housing data.

```{r shap-fit}
set.seed(43)
gg_shp <- gg_shap(rfsrc_iris, bg_n = 50, which.class = 3)
```

## SHAP importance

```{r shap-importance-plot, fig.cap="Mean absolute SHAP value per predictor, virginica probability."}
plot(gg_shp, type = "importance")
```

Same two variables on top as VIMP and minimal depth, `Petal.Length` ahead of
`Petal.Width`. Three different mechanisms, one answer for the dominant
variables --- the disagreement worth watching for is further down the
ranking, not at the top.

## SHAP beeswarm

```{r shap-beeswarm-plot, fig.cap="SHAP beeswarm for virginica probability: every dot is one flower's contribution for one predictor."}
plot(gg_shp, type = "beeswarm")
```

`Petal.Width` shows a clean gradient: the yellow (wide petal) dots sit on the
positive side, pushing the predicted *virginica* probability up; the purple
(narrow petal) dots sit on the negative side, pulling it down. That is
exactly what you would expect from the EDA scatter --- *virginica* has the
widest petals of the three species.

## SHAP dependence

```{r shap-dependence-plot, fig.cap="SHAP dependence for Petal.Width, virginica probability."}
plot(gg_shp, type = "dependence", xvar = "Petal.Width")
```

The upward slope states the same relationship as a number instead of a
color: as `Petal.Width` increases, its contribution to the *virginica*
probability climbs from clearly negative to clearly positive, crossing zero
around the point where *versicolor* and *virginica* petals start to overlap
in the EDA plot.

# Variable Dependence

## Variable dependence plots

Variable dependence shows each flower's OOB predicted class probability
plotted against a predictor. For a classification forest, `gg_variable()`
returns one probability column per class and `plot()` facets on it, the same
way `gg_vimp()` does.

```{r vardep-panel, fig.cap="Variable dependence for Petal.Width, faceted by predicted-class probability."}
gg_v <- gg_variable(rfsrc_iris)

plot(gg_v, xvar = "Petal.Width")
```

Three curves, three stories: *setosa*'s probability collapses to zero past a
petal width of about 0.6, *versicolor* peaks in the middle of the range and
falls off on both sides, and *virginica* rises steadily from zero starting
around 1.0 --- the same three-panel pattern the beeswarm and dependence plots
already showed for *virginica* alone, now visible for all three classes at
once.

## Partial dependence

Partial dependence integrates out the effects of all other covariates, giving
a risk-adjusted view of each predictor's marginal effect on one class's
predicted probability [@Friedman:2000]. Unlike `gg_shap()`, `gg_partial_rfsrc()`
has no `which.class` argument for classification forests; it defers to
`randomForestSRC::partial.rfsrc()`'s own default, which targets the first
factor level, *setosa* here.

```{r partial-dep, fig.cap="Partial dependence for petal measurements, setosa probability."}
pd <- gg_partial_rfsrc(rfsrc_iris, xvar.names = c("Petal.Length", "Petal.Width"))
plot(pd)
```

Both curves fall sharply and then flatten near zero --- the risk-adjusted
version of what the EDA already showed: past a certain petal size, a flower
is essentially never *setosa*. Getting the same kind of curve for
*versicolor* or *virginica* instead means going back to `gg_shap()`:
re-run it with `which.class = 2` or `3`, then `shap_dependence()` on that
new object picks up wherever `which.class` left off.

# Classification Performance: ROC and AUC

ROC curves and AUC have no equivalent in the regression vignette --- they are
specific to classification, where "how well does the forest separate this
class from the rest" is itself a well-posed question. `gg_roc()` computes the
curve for one class at a time, indexed the same way as `gg_shap()`'s
`which.class`, just under the name `which_outcome`.

```{r roc-plot, fig.height=4, fig.cap="ROC curve for virginica vs. the rest."}
roc_virginica <- gg_roc(rfsrc_iris, which_outcome = 3)
plot(roc_virginica)
```

```{r auc-calc}
calc_auc(roc_virginica)
```

An AUC of `r round(calc_auc(roc_virginica), 3)` for *virginica* confirms the
forest separates it well, even given its overlap with *versicolor*. *setosa*'s ROC
curve is not worth plotting --- it is a right angle, AUC 1, the geometric
version of the confusion matrix's perfect row.

# Conclusion

We have walked a full random forest classification analysis with
**randomForestSRC** and **ggRandomForests**, and the pieces line up:

- `gg_error()` showed *setosa* converging immediately, *versicolor* and
  *virginica* settling near 6% each.
- VIMP (`gg_vimp()`) and minimal depth (`max.subtree()`) agreed:
  `Petal.Length` and `Petal.Width` dominate, for every class.
- `gg_shap()` gave the same top variables, but added something VIMP cannot:
  a signed, additive, per-flower contribution to one class's probability,
  chosen with `which.class`.
- `gg_variable()` and `gg_partial_rfsrc()` traced the marginal and
  risk-adjusted versions of the same curves the SHAP dependence plot already
  hinted at.
- `gg_roc()` and `calc_auc()` closed the loop with the classification-specific
  question SHAP and VIMP do not answer directly: how well does the forest
  separate each class from the rest.

The pattern from the regression vignette holds here too. Each `gg_*()`
function returns a tidy object; the plotting is a separate step. Use the
package's `plot()` methods when the default figure is what you want, and
reach for `ggplot2` directly when it is not.

# References
