---
title: "High-Dimensional Classification Boundaries"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{High-Dimensional Classification Boundaries}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse   = TRUE,
  comment    = "#>",
  fig.width  = 6,
  fig.height = 5
)
```

## The problem with more than two features

A boundary plot requires two axes. When a classifier is trained on more than two
features, there is no single 2D picture that faithfully represents the full
decision boundary. The boundary exists in a higher-dimensional space.

`classbound` provides two strategies for visualizing high-dimensional boundaries:

| Strategy | What it shows |
|---|---|
| **2D Slice** | Two selected features on the axes; all other features are fixed at reference values |
| **Projection** | All features combined into two projected axes via a linear transformation |

Neither is universally better. Which to choose depends on your question.

## Setup

```{r setup_data, message=FALSE, warning=FALSE}
library(classbound)
library(palmerpenguins)

# Use three numeric features from palmerpenguins
penguins <- na.omit(palmerpenguins::penguins[
  ,
  c("species", "bill_length_mm", "bill_depth_mm", "flipper_length_mm")
])

# Fit a decision tree on all three features
m3 <- fit_model(
  penguins, species ~ bill_length_mm + bill_depth_mm + flipper_length_mm,
  rpart::rpart
)
m3
```

## 2D Slice

A 2D slice selects two features as the plot axes and holds all other features fixed
at their training-set median (for numeric features) or mode (for categorical features).

The slice shows how the classifier behaves at a particular cross-section of the
feature space. Two observations that appear in the same region may be separated
by the classifier in a dimension that is being held fixed.

```{r slice, message=FALSE, warning=FALSE}
# Visualize bill_length_mm vs bill_depth_mm
# flipper_length_mm is automatically fixed at its training-set median
m3_slice <- boundary_compute(
  m3,
  feature_range = list(bill_length_mm = c(30, 60), bill_depth_mm = c(10, 25)),
  resolution    = 60
)

plot_boundary(
  m3_slice,
  obs_data   = penguins,
  x_col      = "bill_length_mm",
  y_col      = "bill_depth_mm",
  true_label = "species"
)
```

The warning message tells you which features were automatically imputed, and at what
values. You can override this with the `reference` argument:

```{r slice_reference, message=FALSE, warning=FALSE}
# Fix flipper_length_mm at a specific value instead of the median
m3_slice2 <- boundary_compute(
  m3,
  feature_range = list(bill_length_mm = c(30, 60), bill_depth_mm = c(10, 25)),
  resolution    = 60,
  reference     = list(flipper_length_mm = 200)
)

plot_boundary(m3_slice2,
  obs_data = penguins,
  x_col = "bill_length_mm", y_col = "bill_depth_mm",
  true_label = "species"
)
```

The boundary shape changes as the reference value changes, because a different slice
through the 3D boundary is being rendered.

## Projection

A projection collapses all features into two new axes by multiplying the feature
matrix by a 2-column basis matrix. Rather than holding features fixed, it combines
them: each projected point summarizes information from all original features simultaneously.

> **Note on Explorapp vs Scripts:** In the `explorapp()` interactive UI, Projection mode uses PCA automatically. In the programmatic API (like the script below), you provide the projection matrix yourself, allowing you to use PCA, `tourr` projections, or any other projection method of your choice.


The basis must be an orthonormal matrix (columns are unit vectors, perpendicular to
each other). PCA rotation matrices satisfy this by construction.

```{r projection, message=FALSE, warning=FALSE}
feat_cols <- c("bill_length_mm", "bill_depth_mm", "flipper_length_mm")

# Compute PCA on the three numeric features
pca <- prcomp(penguins[, feat_cols], scale. = TRUE)
basis <- pca$rotation[, 1:2] # first two principal components

# Project the training data manually to get axis ranges
x_mat <- scale(penguins[, feat_cols], center = pca$center, scale = pca$scale)
z_mat <- x_mat %*% basis

# Compute boundary in projected space, inverse-project for prediction
m3_proj <- boundary_compute(
  m3,
  feature_range = list(
    PC1 = range(z_mat[, 1]) + c(-0.5, 0.5),
    PC2 = range(z_mat[, 2]) + c(-0.5, 0.5)
  ),
  resolution = 60,
  projection = list(basis = basis, center = pca$center, scale = pca$scale)
)

plot_boundary(
  m3_proj,
  obs_data   = penguins,
  x_col      = "PC1",
  y_col      = "PC2",
  true_label = "species"
)
```

Points are forward-projected onto the plane for overlay. Points that are far from the
projection plane in the original 3D space are rendered with lower opacity (depth fading),
giving a visual indication of how faithfully each point's position is captured.

## When to use each strategy

**2D Slice** is the right choice when:
- You want to examine how a specific pair of features interacts
- Other dimensions are genuinely unimportant or you want to hold them constant
- You want to compare the boundary at different values of a third variable

**Projection** is the right choice when:
- All features contribute to the boundary
- You want a global overview of the multi-dimensional structure
- You are using `tourr` to animate through different projection angles

## High-dimensional data: data69_1

The package includes the UCI Waveform dataset (`data69_1`) with 21 numeric features
and 3 classes. This dataset is better suited than `palmerpenguins` when genuinely
high-dimensional boundary exploration is the goal.

```{r data69, eval=FALSE}
data(data69_1)
dim(data69_1) # 5000 x 22

# Fit on first 5 features for a manageable example
d <- data69_1[1:500, c("Y", "V1", "V2", "V3", "V4", "V5")]
d$Y <- as.factor(d$Y)
m_nd <- fit_model(d, Y ~ ., rpart::rpart)

# 2D slice: V1 vs V2, others at median
m_nd_slice <- boundary_compute(m_nd,
  feature_range = list(V1 = c(-3, 3), V2 = c(-3, 3)),
  resolution    = 50
)

plot_boundary(m_nd_slice,
  obs_data = d,
  x_col = "V1", y_col = "V2", true_label = "Y"
)
```

For the full high-dimensional tourr workflow using `data69_1`, see
`vignette("tourr-workflow")`.
