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

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

```{r setup}
library(evaluatellm)
```

An evaluation is an experiment. It draws a sample of questions from the much
larger set of questions someone could have written, runs a model on them, and
reports a mean. Like any experiment it has a standard error, and like any
experiment it can be too small to answer the question it was built to answer.

This vignette walks through the workflow on a simulated reading comprehension
evaluation: 50 passages, 8 questions each, two models.

## Building the evaluation object

Passages differ in difficulty, and every question about a passage inherits that
difficulty. This is what makes the questions dependent.

```{r}
passage_difficulty <- rnorm(50, 0, 1.3)
item_difficulty <- rep(passage_difficulty, each = 8) + rnorm(400, 0, 0.4)

skill <- c(new = 0.95, old = 0.72)
results <- do.call(rbind, lapply(names(skill), function(m) {
  data.frame(
    q       = 1:400,
    passage = rep(1:50, each = 8),
    model   = m,
    correct = rbinom(400, 1, plogis(skill[[m]] - item_difficulty))
  )
}))

e <- as_eval(results, score = correct, item = q, model = model,
             cluster = passage)
e
```

Declaring `cluster` is the single most consequential argument in the package.
Everything downstream uses it.

## Scoring one model

```{r}
ev_score(e, "new")
```

The design effect says the cluster-robust standard error is three times the
variance of the naive one. Turning the cluster correction off shows what would
otherwise have been reported:

```{r}
ev_score(e, "new", cluster = FALSE)
```

Same estimate, an interval roughly 40 per cent too narrow. `ev_cluster()` breaks
this down further, and `ev_icc()` returns the intra-cluster correlation on its
own, which is what you need to plan the next evaluation.

```{r}
ev_icc(e, "new")
```

## Comparing two models

Both models answered the same questions, so the comparison should be paired.

```{r}
ev_paired(e, model_a = "new", model_b = "old")
```

Two things are worth reading here. The correlation between the models' item
scores drives the variance reduction from pairing: the more alike the models,
the more pairing buys. And the difference is far less affected by clustering
than the level was, because taking the difference has already removed the
passage difficulty both models faced.

For models run on different question sets, `ev_unpaired()` gives a Welch
interval, at a real cost in power.

## Sizing an evaluation before running it

Run this first, not last.

```{r}
ev_mde(n_items = 400, p_a = 0.72, p_b = 0.70, correlation = 0.7,
       icc = 0.25, cluster_size = 8)
```

Four hundred clustered questions cannot resolve a two point gap. A null result
from this evaluation would say nothing about the models. To find the size that
would work:

```{r}
ev_power(delta = 0.02, p_a = 0.72, p_b = 0.70, correlation = 0.7,
         icc = 0.25, cluster_size = 8)
```

The best source for `sd_diff` is a pilot run passed back through `pilot =`.

## Several responses per question

Sampling `k` responses per question splits the observed spread into genuine
variation between questions and noise in the model's own sampling.

```{r}
p_i <- plogis(rnorm(200, 0.7, 1.1))
rep_d <- data.frame(
  q    = rep(1:200, each = 6),
  draw = rep(1:6, times = 200),
  s    = rbinom(1200, 1, rep(p_i, each = 6))
)
ev_resample(as_eval(rep_d, score = s, item = q, sample = draw))
```

The projection table is the useful part: more responses per question buys
precision only against the within-question term, and there is a floor no amount
of resampling can beat. Getting below it means writing more questions.

## Evaluations graded by a model judge

A judge is cheap and biased. Bias does not shrink as you grade more items, so
measure it first.

```{r}
truth <- rbinom(500, 1, 0.55)
judge_pilot <- truth
judge_pilot[truth == 0] <- rbinom(sum(truth == 0), 1, 0.35)
ev_judge_agreement(judge_pilot, truth)
```

Read the kappa rather than the raw agreement, and take the McNemar test
seriously: a judge whose errors run one way is putting that error into every
score it grades.

The fix is not a better prompt. Grade everything with the judge, label a random
subset by hand, and combine them.

```{r}
n_total <- 5000
truth_all <- rbinom(n_total, 1, 0.62)
judge <- ifelse(runif(n_total) < 0.87, truth_all, 1 - truth_all)
judge[truth_all == 0 & runif(n_total) < 0.15] <- 1

gold <- rep(NA_real_, n_total)
labelled <- sample(n_total, 250)
gold[labelled] <- truth_all[labelled]

ev_judge_debias(judge, gold)
```

The estimator is unbiased for any tuning constant, so no assumption about judge
quality is being smuggled in, and the default tuning guarantees it is never less
precise than ignoring the judge entirely. To size the labelling budget in
advance:

```{r}
ev_judge_power(n_total = 20000, correlation = 0.8, target_se = 0.01)
```

## Suites and leaderboards

Reporting the tasks a model won, out of the tasks it was tried on, needs a
multiplicity adjustment. `ev_multi()` applies one, pools the tasks by inverse
variance, and tests whether they are telling the same story.

```{r}
suite <- lapply(1:6, function(i) {
  a <- rbinom(250, 1, 0.72)
  b <- rbinom(250, 1, 0.68)
  d <- data.frame(q = rep(1:250, 2), m = rep(c("new", "old"), each = 250),
                  s = c(a, b))
  ev_paired(as_eval(d, score = s, item = q, model = m), "new", "old")
})
names(suite) <- paste0("task_", 1:6)
ev_multi(suite)
```

For leaderboards, `ev_rank()` bootstraps the ordering so you can see which
positions the data actually support, and `ev_elo()` fits Bradley-Terry ratings
with standard errors on pairwise preference data.

## Reporting

`ev_table()` flattens results into a data frame and `ev_plot()` draws them.

```{r, fig.width = 7, fig.height = 3.5}
ev_plot(suite, reference = 0, xlab = "accuracy gain")
```
