---
title: "Getting started with featR"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Getting started with featR}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(featR)
```

## Why featR

Feature selection in R is spread across a dozen packages, each with its own
calling convention and its own idea of what a result looks like. Comparing two
methods means learning two APIs and writing glue code to line up the answers.

featR wraps the common methods behind one convention and one return type, so
that swapping `fs_lasso()` for `fs_boruta()` is a one-word change. It also tries
hard not to flatter itself: where a method can leak information from a held-out
set, featR either prevents it or says so in the documentation.

## One convention

Every selection function takes the data first and the name of the outcome
column second:

```r
fs_<method>(data, target, ..., seed = NULL, verbose = FALSE, n_cores = 1L)
```

The methods that have no single outcome -- `fs_correlation()`,
`fs_unsupervised()`, `fs_pca()`, `fs_svd()` -- drop `target` and keep the rest.

Three conventions hold everywhere:

* **Sequential by default.** Parallelism is opt-in through `n_cores`, and
  requests are capped at the cores you actually have.
* **The RNG is never touched** unless you pass `seed`, and the previous random
  state is restored when the call returns.
* **Modeling engines are optional.** They live in `Suggests`; a function that
  needs one checks at the point of use and tells you what to install.

## One return type

Selection functions return an `fs_result`:

```{r}
d <- data.frame(
  spread = c(1, 2, 3, 4, 100, 6, 7, 8),
  flat   = rep(2, 8),
  gappy  = c(1, NA, 3, NA, 5, 6, NA, 8)
)

res <- fs_unsupervised(d, method = "variance", threshold = 1)
res
```

Seven components, the same for every method:

```{r}
selected(res)       # the features that were kept
res$scores          # per-feature scores, comparable within a method
res$method          # which method produced this
res$task            # "classification", "regression", or NA
names(res$details)  # everything method-specific
```

`summary()` adds the call and a ranked score table, with an asterisk marking
what was selected:

```{r}
summary(res)
```

Because the shape is shared, methods become interchangeable. `selected()` works
on the result of any of them.

## A supervised filter

`fs_supervised()` scores each feature against the target -- absolute Pearson
correlation for a numeric target, ANOVA F for a factor -- and keeps those past
a threshold:

```{r}
train <- data.frame(
  strong = c(1, 2, 3, 4, 5, 6),
  mirror = c(6, 5, 4, 3, 2, 1),
  noise  = c(1, 0, 1, 0, 1, 0),
  y      = c(1, 2, 3, 4, 5, 6)
)

fs_supervised(train, target = "y", threshold = 0.9)
```

Both `strong` and `mirror` have |r| = 1: a filter like this ranks features one
at a time, so it cannot tell you that the second is redundant given the first.
That is what `fs_correlation()` is for.

## Removing redundancy

`fs_correlation()` looks only at how the features relate to each other. With
`prune = TRUE` (the default) it returns a reduced set with no two members
correlated above the threshold:

```{r}
fs_correlation(train[, c("strong", "mirror", "noise")], threshold = 0.9)
```

Set `prune = FALSE` if you would rather see every feature involved in a
high-correlation pair, which is the set most correlation filters report.

## Information gain

`fs_infogain()` measures how many bits of uncertainty about the target each
feature removes. Numeric features are binned; dates expand into year, month and
day:

```{r}
ig <- data.frame(
  perfect = rep(c("a", "b", "c", "c"), 5),
  half    = rep(c("n1", "n2"), 10),
  target  = factor(rep(c("a", "b", "c", "c"), 5))
)

fs_infogain(ig, target = "target")$scores
```

Raw information gain rewards high-cardinality features -- an ID column can
score near the target's entropy while predicting nothing generalizable. Pass
`normalize = "gain_ratio"` to divide by each feature's own entropy, which
corrects that bias.

## Methods that need a modeling engine

The remaining methods depend on suggested packages, so they are shown here
without being run:

```r
# Regularization
fs_lasso(mtcars, "mpg", nfolds = 5, seed = 1)        # glmnet
fs_elastic(mtcars, "mpg", seed = 1)                  # caret + glmnet

# Wrappers and model-based
fs_boruta(iris, "Species", seed = 1)                 # Boruta
fs_randomforest(iris, "Species", task = "classification", seed = 1)
fs_recursivefeature(mtcars, "mpg", sizes = c(2, 4), seed = 1)  # caret
fs_stepwise(mtcars, "mpg", direction = "both")       # MASS
fs_mars(mtcars, "mpg", seed = 1)                     # caret + earth
fs_svm(iris, "Species", task = "classification",     # caret + kernlab
       feature_select = TRUE, select_method = "svm_rfe", seed = 1)
fs_bayes(mtcars, "mpg", predictors = c("wt", "hp"))  # brms + loo
```

Each errors with the exact `install.packages()` call it needs, so nothing fails
mysteriously.

## Choosing a method

| If you want | Use |
|---|---|
| A fast first cut on many features | `fs_unsupervised()`, `fs_supervised()` |
| To drop near-duplicate features | `fs_correlation()` |
| To rank categorical predictors of a categorical outcome | `fs_chi()`, `fs_infogain()` |
| A sparse, interpretable linear model | `fs_lasso()`, `fs_elastic()` |
| Every feature with any relevance, not a minimal set | `fs_boruta()` |
| Selection tied to the model you will actually deploy | `fs_recursivefeature()`, `fs_svm()` |
| Non-linear relationships found automatically | `fs_mars()` |
| Fewer dimensions rather than fewer variables | `fs_pca()`, `fs_svd()` |

Filters are cheap and model-agnostic but judge features one at a time. Wrappers
respect interactions but cost a model fit per subset and tie the answer to that
model. Neither is a substitute for validating the selected set on data that
played no part in choosing it.

## What featR will not do quietly

A few behaviors worth knowing, all documented per function:

* Class upsampling in `fs_mars()` and `fs_svm()` happens *inside* resampling
  folds, so resampled metrics are not inflated by duplicated rows.
* `fs_recursivefeature()` evaluates on rows the selection never saw and trains
  its final model on training rows only.
* `fs_lasso()` refuses to mean-impute by default, because imputing on the full
  data leaks across folds; pass `impute = "mean"` to opt in and get a warning.
* `fs_stepwise()` labels its p-values as invalid for inference, because
  post-selection they are.
* In-sample metrics are called in-sample.
