---
title: "Variable selection without an outcome: unsupervised varPro"
author: "John Ehrlinger"
date: today
format:
  html:
    fig-format: png
    fig-dpi: 96
    toc: true
    toc-depth: 2
    html-math-method: mathjax
    code-fold: false
bibliography: ggRandomForests.bib
vignette: >
  %\VignetteIndexEntry{Variable selection without an outcome: unsupervised varPro}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

```{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(
  message    = FALSE,
  warning    = FALSE,
  fig.width  = 7,
  fig.height = 4.5,
  cache      = TRUE,
  cache.path = "uvarpro_cache/"
)
old_opts <- options(mc.cores = 1, rf.cores = 1)
```

```{r libs}
library(ggplot2)

# Match the pattern used by the other vignettes: try the installed
# package first, fall back to pkgload::load_all() for the R CMD check
# vignette rebuild where the package isn't yet on .libPaths(). All varPro
# calls below are ::-qualified, so no library(varPro) is needed.
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.")
}
```

# Importance without a response

Most measures of variable importance start from a question: which predictors
help explain *this* outcome? Permutation VIMP, the varPro release rules, the
per-rule lasso weights behind `gg_beta_varpro()`: all of them score a variable
by how much it moves a response `y`. The companion
[varPro vignette](varpro.html) walks that supervised toolkit end to end.

But sometimes there is no `y`. You have a matrix of predictors and you want to
know which columns carry the structure of the data: which variables define its
shape, which travel together, and which are close to noise. That is the job of
unsupervised varPro. `varPro::uvarpro()` grows a forest on the predictor matrix
alone, with no response, and scores each variable by how much entropy it
contributes to the regions the forest carves out [@Lu2024varpro;
@Ishwaran:varPro:software:2026]. "Important"
here
does not mean useful for prediction. It means a variable helps reconstruct the
feature space that the others cannot.

This is a short vignette. It walks the three `gg_*` views of a single
`uvarpro()` fit, and each one answers a different question about that fit: what
depends on what, what ranks highest, and where to draw the line between signal
and noise.

# One fit, three views

We'll use `mtcars`. Its columns are all numeric, and several of them measure
closely related things (displacement, horsepower, weight, and cylinder count
all track engine size), so the unsupervised structure is easy to read.

```{r fit}
set.seed(1)
u <- varPro::uvarpro(mtcars, ntree = 50)
```

Notice what `uvarpro()` was handed: the data frame, and nothing else. No
formula, no outcome column singled out. All three views below read off this one
fit. Two of them rest on the same `get.beta.entropy()` matrix, which is the only
part worth caching, so we compute it once and pass it to both rather than paying
for it twice:

```{r beta_fit}
beta_fit <- varPro::get.beta.entropy(u)
```

## What depends on what: `gg_udependent()`

`gg_udependent()` reads cross-variable structure off the fit and draws it as a
network: nodes are variables, edges are dependencies above a configurable
threshold. The picture is built with `ggraph`, which lives in `Suggests` rather
than `Imports`, so install it if you want this view.

```{r udep, eval = requireNamespace("ggraph", quietly = TRUE)}
plot(gg_udependent(u))
```

Clusters of mutually connected variables are worth a second look. Because the
fit has no response, a cluster tells you these columns are correlated in feature
space regardless of whether any of them would matter for a prediction. When you
do have a downstream model, that is useful on its own: a variable can be
important for prediction and still sit in a tight cluster with a near-duplicate,
and in that case parsimony may favor dropping one member of the cluster without
losing much.

## What ranks highest: `gg_beta_uvarpro()`

The network shows *structure*; `gg_beta_uvarpro()` turns the same fit into a
*ranking*. It is the unsupervised analogue of `gg_beta_varpro()`: from the
entropy matrix it aggregates the per-region lasso coefficients into a mean
absolute weight per variable (`beta_mean`), orders them most-important first,
and flags the ones above a selection cutoff. Here we hand it the `beta_fit` we
already computed.

```{r beta}
plot(gg_beta_uvarpro(u, beta_fit = beta_fit))
```

Read it as the unsupervised counterpart of a VIMP bar chart. The tall bars are
the variables that most define the structure of the predictor space. Paired with
the network above, you get both halves of the story: *which* variables carry the
most unsupervised signal, and *how* they group.

## Where to draw the line: `gg_sdependent()`

A ranking invites the obvious next question: where is the cut? Which variables
are signal, and which are noise? `gg_sdependent()` answers that narrower
question off the same fit. It wraps `varPro::sdependent()` and returns one row
per candidate variable (an importance score, its degree in the dependency
graph, and a `signal` flag), drawn as a ranked lollipop.

```{r sdep}
plot(gg_sdependent(u, beta_fit = beta_fit))
```

Where `gg_beta_uvarpro()` ranks *all* the variables, `gg_sdependent()` makes the
cut explicit, separating the columns the unsupervised analysis treats as signal
from the ones it treats as noise.

# Reading the three together

The three views are one workflow. Start with
`gg_udependent()` to see the structure, rank it with `gg_beta_uvarpro()`, then
let `gg_sdependent()` draw the signal-versus-noise line. They all derive from the
one `uvarpro()` fit, and two of them from the one `get.beta.entropy()` matrix, so
computing that matrix once (as we did above) and passing it in keeps the whole
sequence cheap.

For the supervised side of varPro (VIMP, partial dependence, per-rule lasso
refinement, local importance, and anomaly scoring), see the companion
[varPro vignette](varpro.html).

```{r}
#| label: restore-options
#| include: false
#| cache: false
options(old_opts)
```

# References
