---
title: "Old Faithful Geyser example"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Old Faithful Geyser example}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r}
#| include: false

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  out.width = "100%",
  dpi = 200
)
options(width = 85)
set.seed(1967)
```

```{r}
#| label: setup

library(weird)
```

The `oldfaithful` data set contains eruption data from the Old Faithful Geyser in Yellowstone National Park, Wyoming, USA, from 2017 to 2023.
The data were obtained from the [geysertimes.org](https://geysertimes.org) website.
Recordings are incomplete, especially during the winter months when observers may not be present.
There also appear to be some recording errors.
The data set contains `r NROW(oldfaithful)` observations of 3 variables: `time` giving the time at which each eruption began, `recorded_duration` giving the length of the eruption as recorded, `duration` giving the length of the eruption in seconds, and `waiting` giving the time to the next eruption in seconds.

```{r}
#| label: oldfaithful

oldfaithful
```

## Kernel density estimates

The package provides the `kde_bandwidth()` function for estimating the bandwidth of a kernel density estimate, `dist_kde()` for constructing the distribution, and `gg_density()` for plotting the resulting density.
The figure below shows the kernel density estimate of the `duration` variable obtained using these functions.

```{r}
#| label: of-density

dist_kde(oldfaithful$duration) |>
  gg_density(show_points = TRUE, jitter = TRUE) +
  labs(x = "Duration (seconds)")
```

The same functions also work with bivariate data.
The figure below shows the kernel density estimate of the `duration` and `waiting` variables.

```{r}
#| label: of-density2

oldfaithful |>
  select(duration, waiting) |>
  dist_kde() |>
  gg_density(show_points = TRUE, alpha = 0.15) +
  labs(x = "Duration (seconds)", y = "Waiting time (seconds)")
```

## Statistical tests

Some old methods of anomaly detection used statistical tests.
While these are not recommended, they are still widely used, and are provided in the package for comparison purposes.

```{r}
#| label: of-test

oldfaithful |> filter(peirce_anomalies(duration))
oldfaithful |> filter(chauvenet_anomalies(duration))
oldfaithful |> filter(grubbs_anomalies(duration))
oldfaithful |> filter(dixon_anomalies(duration))
```

There are at least three anomalies in this example (due to recording errors), but none of these methods detect them all.
An explanation of these tests is provided in [Chapter 4 of the book](https://OTexts.com/weird/04-tests.html)

## Boxplots

Boxplots are widely used for anomaly detection.
Here are three variations of boxplots applied to the `duration` variable.

```{r}
#| label: of-boxplot
#| fig-height: 1.5

oldfaithful |>
  ggplot(aes(x = duration)) +
  geom_boxplot() +
  scale_y_discrete() +
  labs(y = "", x = "Duration (seconds)")
oldfaithful |> gg_hdrboxplot(duration) + labs(x = "Duration (seconds)")
oldfaithful |>
  gg_hdrboxplot(duration, show_points = TRUE) +
  labs(x = "Duration (seconds)")
```

The latter two plots are highest density region (HDR) boxplots, which allow the bimodality of the data to be seen.
The dark shaded region contains 50% of the observations, while the lighter shaded region contains 99% of the observations.
The plots use vertical jittering to reduce overplotting, and highlight potential outliers (those points lying outside the 99% HDR which have surprisal probability less than 0.0005).
An explanation of these plots is provided in [Chapter 5 of the book](https://OTexts.com/weird/05-boxplots.html).

It is also possible to produce bivariate boxplots.
Several variations are provided in the package.
Here are two types of bagplot.

```{r}
#| label: of-boxplot2

oldfaithful |>
  gg_bagplot(duration, waiting) +
  labs(x = "Duration (seconds)", y = "Waiting time (seconds)")
oldfaithful |>
  gg_bagplot(duration, waiting, show_points = TRUE) +
  labs(x = "Duration (seconds)", y = "Waiting time (seconds)")
```

And here are two types of HDR boxplot

```{r}
#| label: of-boxplot3

oldfaithful |>
  gg_hdrboxplot(duration, waiting) +
  labs(x = "Duration (seconds)", y = "Waiting time (seconds)")
oldfaithful |>
  gg_hdrboxplot(duration, waiting, show_points = TRUE) +
  labs(x = "Duration (seconds)", y = "Waiting time (seconds)")
```

The latter two plots show possible outliers in black (again, defined as points outside the 99% HDR which have surprisal probability less than 0.0005).

## Scoring functions

Several functions are provided for providing anomaly scores for all observations.

- The `surprisals()` function uses either a fitted statistical model, or a kernel density estimate, to compute surprisals.
- The `surprisals_prob()` function computes the probability of obtaining surprisal values at least as extreme as those observed.
- The `stray_scores()` function uses the stray algorithm to compute anomaly scores.
- The `lof_scores()` function uses local outlier factors to compute anomaly scores.
- The `glosh_scores()` function uses the Global-Local Outlier Score from Hierarchies algorithm to compute anomaly scores.

Here are the top 0.02% most anomalous observations identified by each of the methods.

```{r}
#| label: of-scores

oldfaithful |>
  mutate(
    surprisal = surprisals(cbind(duration, waiting)),
    surprisal_prob = surprisals_prob(
      cbind(duration, waiting),
      approximation = "gpd"
    ),
    strayscore = stray_scores(cbind(duration, waiting)),
    lofscore = lof_scores(cbind(duration, waiting), k = 150),
    gloshscore = glosh_scores(cbind(duration, waiting))
  ) |>
  filter(
    surprisal_prob < 0.002 |
      strayscore > quantile(strayscore, prob = 0.998) |
      lofscore > quantile(lofscore, prob = 0.998) |
      gloshscore > quantile(gloshscore, prob = 0.998)
  ) |>
  arrange(surprisal_prob)
```

## Robust multivariate scaling

Some anomaly detection methods require the data to be scaled first, so all observations are on the same scale.
However, many scaling methods are not robust to anomalies.
The `mvscale()` function provides a multivariate robust scaling method, that optionally takes account of the relationships between variables, and uses robust estimates of center, scale and covariance by default.
The centers are removed using medians, the scale function is the IQR, and the covariance matrix is estimated using a robust OGK estimate.
The data are scaled using the Cholesky decomposition of the inverse covariance.
Then the scaled data are returned.
The scaled variables are rotated to be orthogonal, so are renamed as `z1`, `z2`, etc. Non-rotated scaling is possible by setting `cov = NULL`.

```{r}
#| label: of-mvscale

mvscale(oldfaithful)
mvscale(oldfaithful, cov = NULL)
```
