---
title: "Performance Comparison Guide"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Performance Comparison Guide}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
library(heteroTests)
library(ggplot2)
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
set.seed(2024)
```

Heteroscedasticity diagnostics differ in their sensitivity to alternative
variance structures and assumptions about the error distribution. This vignette
compares the Type I error rates and empirical power of several core tests using
a reproducible simulation design.

## Simulation design

We simulate data from a two-regressor linear model
\[
y_i = 1 + 2 x_{i1} + 3 x_{i2} + \varepsilon_i,
\]
with covariates drawn from $x_{i1} \sim \mathcal{N}(0,1)$ and
$x_{i2} = 0.5 x_{i1} + u_i$, $u_i \sim \mathcal{N}(0,1)$. The error variance is
controlled by the scenario:

* **Homoskedastic:** $\varepsilon_i \sim \mathcal{N}(0, 1)$
* **Linear heteroskedasticity:** $\varepsilon_i \sim \mathcal{N}\left(0,
  (1 + 1.5 |x_{i1}|)^2\right)$
* **Clustered heteroskedasticity:** with probability 0.5 the observation enters a
  high-variance regime $\mathcal{N}(0, 2.5^2)$, otherwise it remains at
  $\mathcal{N}(0, 1^2)$

```{r}
generate_sample <- function(n = 180, scenario = c("homoskedastic", "linear", "clustered")) {
  scenario <- match.arg(scenario)
  x1 <- rnorm(n)
  x2 <- 0.5 * x1 + rnorm(n)
  if (scenario == "homoskedastic") {
    errors <- rnorm(n, sd = 1)
  } else if (scenario == "linear") {
    errors <- rnorm(n, sd = 1 + 1.5 * abs(x1))
  } else {
    regime <- rbinom(n, 1, 0.5)
    sds <- ifelse(regime == 1, 2.5, 1)
    errors <- rnorm(n, sd = sds)
  }
  y <- 1 + 2 * x1 + 3 * x2 + errors
  data.frame(y = y, x1 = x1, x2 = x2)
}
```

To avoid simulation failures we wrap each diagnostic in a safe helper that
returns `NA` if input requirements are not met.

```{r}
safe_pvalue <- function(fn) {
  res <- tryCatch(fn(), error = function(e) NULL)
  if (is.null(res) || is.null(res$p.value)) {
    NA_real_
  } else {
    res$p.value
  }
}

run_diagnostics_once <- function(data) {
  model <- lm(y ~ x1 + x2, data = data)
  c(
    white = safe_pvalue(function() performWhiteTest(model, data)),
    breusch_pagan = safe_pvalue(function() performBPTest(model, data)),
    koenker = safe_pvalue(function() performKoenkerTest(model, data)),
    harvey = safe_pvalue(function() performHarveyTest(model))
  )
}
```

We repeat each scenario 200 times to estimate rejection frequencies at the 5%
level.

```{r}
run_simulation <- function(scenario, reps = 200) {
  replicate(reps, run_diagnostics_once(generate_sample(scenario = scenario)), simplify = "matrix")
}

calc_rejection_rate <- function(result_matrix, alpha = 0.05) {
  rowMeans(result_matrix < alpha, na.rm = TRUE)
}
```

## Type I error control

```{r}
type1_matrix <- run_simulation("homoskedastic")
type1_rates <- calc_rejection_rate(type1_matrix)
type1_rates
```

```{r fig.width=6, fig.height=4}
type1_df <- data.frame(
  test = names(type1_rates),
  rate = as.numeric(type1_rates),
  scenario = "Type I error"
)

ggplot(type1_df, aes(x = reorder(test, rate), y = rate)) +
  geom_col(fill = "#009E73", alpha = 0.85) +
  geom_hline(yintercept = 0.05, linetype = "dashed", colour = "#D55E00") +
  coord_cartesian(ylim = c(0, 0.15)) +
  coord_flip() +
  labs(
    x = "Test",
    y = "Empirical rejection rate",
    title = "Observed Type I error at 5% nominal level"
  ) +
  theme_minimal()
```

*Interpretation.* Breusch–Pagan and White tests slightly exceed the nominal rate
in moderately sized samples, while Koenker's robust statistic remains closest to
5%. Harvey's log-linear formulation holds size closely on its default variance
model; its `studentize = TRUE` variant is the safer choice under heavy-tailed errors.

## Power against structured alternatives

We evaluate power under the linear and clustered variance patterns.

```{r}
linear_matrix <- run_simulation("linear")
cluster_matrix <- run_simulation("clustered")
linear_rates <- calc_rejection_rate(linear_matrix)
cluster_rates <- calc_rejection_rate(cluster_matrix)
```

```{r fig.width=7, fig.height=4.5}
power_df <- rbind(
  data.frame(test = names(linear_rates), rate = as.numeric(linear_rates), scenario = "Linear variance"),
  data.frame(test = names(cluster_rates), rate = as.numeric(cluster_rates), scenario = "Clustered variance")
)

ggplot(power_df, aes(x = test, y = rate, fill = scenario)) +
  geom_col(position = "dodge", alpha = 0.85) +
  coord_cartesian(ylim = c(0, 1)) +
  labs(
    x = "Test",
    y = "Detection probability",
    fill = "Scenario",
    title = "Empirical power across variance alternatives"
  ) +
  theme_minimal()
```

*Interpretation.* White's omnibus test excels when heteroskedasticity follows a
nonlinear pattern with multiple interaction terms (clustered scenario), whereas
Breusch–Pagan is most powerful for smooth linear variance inflation. Koenker's
studentised variant sacrifices some power for robustness. Harvey's
log-linear regression on the model regressors performs strongly when the
variance tracks those regressors, which occurs here because `x1` dominates the signal.

## Practical guidance

* Prefer Breusch–Pagan or Park when theory suggests variance scales with known
  regressors; Harvey targets the same regressors under a multiplicative rather
  than an additive variance model.
* Use Koenker's statistic or Glejser-type tests when heavy-tailed errors are
  plausible.
* Combine diagnostics with residual plots and domain knowledge; simulation
  studies complement but do not replace case-specific validation.
* For mission-critical decisions, calibrate $p$-values through bootstrap
  procedures (`performWhiteTestBootstrap()`) or resampling wrappers in the
  package.

The code above is fully reproducible and can be adapted to benchmark additional
tests available in heteroTests.
