---
title: "Evaluating a Text Classifier"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Evaluating a Text Classifier}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

## Why evaluation design matters

Text models can appear accurate because related documents occur in both
training and test data, preprocessing used the entire dataset, or accuracy
hides poor minority-class performance. Evaluation must mirror the intended
use of the classifier.

```{r}
library(textclassificationtutorial)

text <- c(
  "analyze customer data", "build statistical models",
  "create predictive analytics", "report business metrics",
  "provide patient care", "support clinical treatment",
  "coordinate nursing care", "assist hospital patients",
  "analyze experimental results", "develop data dashboard",
  "monitor patient health", "coordinate clinical team"
)
label <- rep(c("data", "care"), each = 4)
label <- c(label, "data", "data", "care", "care")
```

## Stratified folds

Stratification distributes each class across folds. The smallest class must
have at least `k` observations.

```{r}
folds <- stratified_folds(label, k = 3, seed = 2026)
folds
```

For nested documents—sentences within vacancies, employees within teams, or
posts within authors—split on the higher-level entity instead. Ordinary
stratification does not prevent grouped leakage.

## Cross-validation loop

The vocabulary must be learned from the training documents. Test documents are
then aligned to that training vocabulary by `predict.text_nb()`.

```{r}
fold_results <- lapply(folds, function(test_index) {
  train_index <- setdiff(seq_along(text), test_index)

  train_clean <- preprocess_text(text[train_index])
  test_clean <- preprocess_text(text[test_index])

  train_dtm <- document_term_matrix(train_clean)
  test_dtm <- document_term_matrix(test_clean)

  model <- fit_naive_bayes(train_dtm, label[train_index])
  estimate <- predict(model, test_dtm)

  classification_metrics(
    truth = label[test_index],
    estimate = estimate,
    positive = "data"
  )
})

results <- do.call(rbind, fold_results)
results
colMeans(results[c(
  "accuracy", "balanced_accuracy", "precision", "recall", "f1"
)], na.rm = TRUE)
```

## Interpret the metrics

- **Precision** asks what proportion of predicted positive documents is truly
  positive.
- **Recall** asks what proportion of true positive documents was found.
- **Specificity** is recall for the negative class.
- **Balanced accuracy** averages recall and specificity.
- **F1** is the harmonic mean of precision and recall.

Choose metrics before examining results. In rare-category classification,
ordinary accuracy can be high even when the classifier never detects the class
of interest.

## From tutorial to research

For a substantive study, add:

1. a held-out final test set;
2. grouped or temporal resampling where the data structure requires it;
3. training-fold-only tuning and feature selection;
4. confidence intervals across appropriate sampling units;
5. human review of false positives and false negatives;
6. documentation of annotation quality and inter-rater agreement;
7. fairness and drift checks for the deployment context.
