---
title: "R6 Classes and tidymodels"
author: "Jodavid Ferreira"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{R6 Classes and tidymodels}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
has_tidymodels <- all(vapply(
  c("parsnip", "dials", "recipes", "rsample", "tune", "workflows", "yardstick"),
  requireNamespace, logical(1), quietly = TRUE
))
```

Since version 0.3.0, every classifier of `FuzzyClass` is an
[R6](https://r6.r-lib.org) class, and all of them can be used within the
[tidymodels](https://www.tidymodels.org) framework through the parsnip model
`fuzzy_naive_bayes()`. This vignette shows both interfaces.

```{r}
library(FuzzyClass)

set.seed(123)
train_index <- sample(nrow(iris), nrow(iris) * 0.7)
train_data <- iris[train_index, ]
test_data <- iris[-train_index, ]
```

## The R6 interface

Each classifier `X` has an R6 generator `XModel`, which inherits from the base
class `FuzzyClassifier`. A model is created with `$new()`, which receives the
hyperparameters of the classifier:

```{r}
model <- GauNBFuzzyParamModel$new(metd = 2)
model
```

`$fit()` estimates the model. While training, a message reports which
classifier is being estimated, the size of the data and the training time:

```{r}
model$fit(train = train_data[, -5], cl = train_data$Species)
model
```

`$predict()` classifies new observations, returning the predicted classes or,
with `type = "prob"`, the matrix of class probabilities:

```{r}
pred <- model$predict(test_data[, -5])
head(pred)
mean(pred == test_data$Species)

head(model$predict(test_data[, -5], type = "prob"))
```

Hyperparameters are changed with `$set_params()`, which resets the model so
that it must be trained again. Since `$fit()` and `$set_params()` return the
object, the calls can be chained; `verbose = FALSE` trains silently:

```{r}
model$set_params(metd = 5)$fit(train_data[, -5], train_data$Species, verbose = FALSE)
mean(model$predict(test_data[, -5]) == test_data$Species)
```

The training message can also be disabled for the whole session with
`options(FuzzyClass.verbose = FALSE)`.

### The functional interface

The functions of the previous versions are still available. They train the
model and return the R6 object, and the S3 method `predict()` is supported:

```{r}
fit <- FuzzyGaussianNaiveBayes(train = train_data[, -5], cl = train_data$Species)
class(fit)
head(predict(fit, test_data[, -5]))
head(predict(fit, test_data[, -5], type = "matrix"))
```

## Using FuzzyClass with tidymodels

`fuzzy_naive_bayes()` defines a parsnip model specification. The classifier
is chosen through the engine (see `?fuzzy_naive_bayes` for the full list):

```{r, eval = has_tidymodels, message = FALSE}
library(parsnip)
library(workflows)
library(recipes)
library(rsample)
library(tune)
library(yardstick)

spec <- fuzzy_naive_bayes(fuzzy = TRUE) |>
  set_engine("gaussian")
spec

iris_fit <- fit(spec, Species ~ ., data = train_data)
predict(iris_fit, test_data[1:5, ])
predict(iris_fit, test_data[1:5, ], type = "prob")
```

Arguments specific to a classifier are passed to `set_engine()`:

```{r, eval = has_tidymodels}
fuzzy_naive_bayes() |>
  set_engine("dw_gamma", wdelta = rep(1/3, 3), weta = rep(1/4, 4), verbose = FALSE) |>
  fit(Species ~ ., data = train_data) |>
  augment(new_data = test_data) |>
  yardstick::accuracy(truth = Species, estimate = .pred_class)
```

### Tuning

The main arguments `fuzzy`, `alpha_cut` and `ranking_method` can be tuned. Here
we tune the fuzzy-parameter Gaussian classifier inside a workflow with a
recipe, using cross-validation. The training messages are disabled while
tuning:

```{r, eval = has_tidymodels}
options(FuzzyClass.verbose = FALSE)

split <- initial_split(iris, prop = 0.7, strata = Species)
folds <- vfold_cv(training(split), v = 5, strata = Species)

wf <- workflow() |>
  add_recipe(recipe(Species ~ ., data = training(split))) |>
  add_model(
    fuzzy_naive_bayes(alpha_cut = tune(), ranking_method = tune()) |>
      set_engine("gaussian_fuzzy_param")
  )

set.seed(1)
tuned <- tune_grid(wf, resamples = folds, grid = 6,
                   metrics = metric_set(yardstick::accuracy, yardstick::roc_auc))
show_best(tuned, metric = "accuracy", n = 3)

final <- finalize_workflow(wf, select_best(tuned, metric = "accuracy")) |>
  last_fit(split, metrics = metric_set(yardstick::accuracy, yardstick::roc_auc))
collect_metrics(final)

options(FuzzyClass.verbose = TRUE)
```
