Vol. 08 · ML & Decision Systems

Statistics for Evaluating LLM Systems

Basic concepts of statistics as applied to LLM experiments: quality metrics, variance and confidence intervals, hypothesis testing, A/B tests, LLM-as-judge, evaluation pitfalls and a practical checklist for a valid experiment.

01

LLM Quality Metrics

4 cards
01MetricsLog-Loss and Perplexity: one essence, two names

Log-loss (cross-entropy) measures how confidently the model predicts the correct token. Perplexity is simply an exponent of log-loss: “how many variants the model loses on average.” PPL=10 means the model “oscillates between 10 equally probable tokens” at each step.

Diagramtex
For one token y with predicted probability p(y):

  log-loss = −log p(y)
  ↑ if the model is confident: p(y)=0.9 → loss = 0.105
  ↑ if you are not sure: p(y)=0.1 → loss = 2.303

For the entire sequence of N tokens:

  Log-Loss = −(1/N) · Σᵢ log p(yᵢ | y₁…y_{i-1})
  ↑ average for all tokens

  Perplexity = exp(Log-Loss) = exp(−(1/N) · Σ log p(yᵢ))
  ↑ interpretation: average “selection size” of the model

Examples of PPL:
  PPL = 1000 ← bad model or out-of-domain text
  PPL = 20 ← GPT-2 on WikiText-103
  PPL = 6 ← Llama-3 70B in English text
  PPL = 1 ← ideal model (theoretically)

Communication with information bits:
  bits-per-char = log-loss / log(2) ← alternative notation

when to use

  • comparison of pretrain quality of models
  • assessment of quality degradation during quantization
  • PPL on the domain ≠ PPL on your task
  • does not reflect Useful framing for downstream tasks
02MetricsAccuracy, Precision, Recall, F1

For classification tasks (IntentRouter, sentiment, RAG relevance), accuracy often deceives when classes are imbalanced. F1 - harmonic mean Precision and Recall - fairer for rare classes.

formulas and error matrixtex
Predicted + Predicted −
Actual + TP FN
Actual − FP TN

Accuracy = (TP+TN) / (TP+TN+FP+FN)
→ cheats: 99% TN → accuracy=99% effortless

Precision = TP / (TP+FP) ← of all the “+”, how many “+” are true?
Recall = TP / (TP+FN) ← of all the real “+”s, how many did you find?

F1 = 2 · (P R) / (P+R) ← harmonic mean

Macro F1: average F1 for all classes (imbalance not taken into account)
Weighted F1: class frequency weighted
03MetricsBLEU, ROUGE and their limitations

BLEU and ROUGE measure the n-gram intersection between a generation and a reference. Historically the standard for translation and summarization, but poorly correlated with human assessment of the quality of free text.

mechanicstext
BLEU (precision-oriented):
  BLEU-N = intersection of N-grams(gen, ref) / len(gen)
  ↑ penalizes for output that is too short (BP)
  ↑ usually average BLEU-1..4

ROUGE-N (recall-oriented):
  ROUGE-N = intersection of N-grams(gen, ref) / len(ref)

ROUGE-L: longest common subsequence

Example:
  ref: "the cat was sitting on the rug"
  gen: "the rug is occupied by a cat"
  BLEU-1 = 0.5 (2 words out of 3 are in ref)
  but the meaning is correct → BLEU does not see this

BERTScore: cosine proximity of embeddings
  ↑ better for semantic quality

modern practice

  • BLEU/ROUGE - only as baseline
  • BERTScore - if you need automation
  • LLM-as-judge - for free generation
04MetricsMetrics for RAGs and agents

The RAG pipeline consists of two stages - retrieval and generation - and both need to be measured separately. The final quality of the answer depends on both, but different types of errors require different corrections.

RAGAS and componentssql
Retrieval:
  Hit Rate@k - relevant doc in the top-k?
  MRR@k - position of the first relevant
  Precision@k - share of relevant ones in the top k

Generation (RAGAS):
  Faithfulness - Is the answer based on context?
  Answer Relevancy - does the answer answer the question?
  Context Precision - does the context contain the answer?
  Context Recall - has all the required context been found?

Faithfulness Formula (LLM-judge):
  score = (#statements from context) / (#all statements)
  → catches hallucinations
02

Scatter, Variance and Confidence Intervals

4 cards
05ScatterMean, Variance, Standard Deviation

One assessment of the LLM system does not mean anything. What matters is the spread—how unstable the results are. The variance σ² and standard deviation σ measure this spread around the mean.

formulastex
# n runs of the experiment, xᵢ is the result of the i-th

Sample mean:
  x̄ = (1/n) · Σᵢ xᵢ

Sample variance (unbiased):
  s² = 1/(n−1) Σᵢ (xᵢ − x̄)²
  ↑ divide by n-1, not n (Bessel correction)

Standard Deviation:
  s = √s² ← same units as xᵢ

Example: accuracy on 10 test sets:
  [0.82, 0.79, 0.84, 0.81, 0.83,
   0.80, 0.85, 0.78, 0.82, 0.81]
  x = 0.815
  s = 0.021 ← spread ≈ 2%

interpretation

  • s small → results are stable
  • s large → more runs needed
  • one point instead of distribution = error
06ScatterStandard Error and Confidence Interval

The average is also a random variable. Standard error (SE) measures how closely the estimated mean reflects the “true.” A confidence interval (CI) gives a range within which there is a 95% probability that the true value of a metric lies.

formulastex
Standard error of the mean:
  SE = s/√n
  ↑ decreases with increasing sample
  ↑ for n=100: SE is 10 times less than n=1

95% Confidence Interval (normal):
  CI = x̄ ± 1.96 SE = x̄ ± 1.96 s/√n

Example:
  accuracy x̄ = 0.815, s = 0.021, n = 100
  SE = 0.021 / √100 = 0.0021
  95% CI = [0.811, 0.819]

  n = 10 → SE = 0.021/√10 = 0.0066
  95% CI = [0.802, 0.828] ← wide interval!

Conclusion: n=10 is a bad score. Need ≥ 50-100.
07ScatterBootstrap: CI without distribution assumptions

The formula 1.96·SE works under normal distribution. LLM metrics (F1, BLEU, win-rate) are often not normal. Bootstrap is a universal method: we sample with return and build an empirical distribution.

bootstrap CI algorithmsql
results = [0.82, 0.79, 0.84, 0.81, …] # n=100

bootstrap_means = []
for _ in range(10_000):
  sample = resample(results, n=100) # return
  bootstrap_means.append(mean(sample))

#95% CI = 2.5% and 97.5% percentiles
ci_low = percentile(bootstrap_means, 2.5)
ci_high = percentile(bootstrap_means, 97.5)

print(f"95% CI: [{ci_low:.3f}, {ci_high:.3f}]")

from scipy.stats import bootstrap
res = bootstrap((data,), np.mean, n_resamples=10000)

when to use

  • the metric is not normally distributed
  • small sample (n < 30)
  • complex metrics (BLEU, F1, win-rate)
  • gold standard for LLM experiments
08ScatterSources of variance in the LLM experiment

LLM adds its own layer of randomness on top of the normal sample variance. Before calculating the metric, you need to know what noise sources there are and which one dominates.

Diagramtext
Sources of variance:

① Temperature/sampling
  same prompt → different answers
  → run each prompt k≥3 times, take the average

② Prompt sensitivity
  "Answer briefly" vs "Give a short answer"
  → the difference can be ±5–10% on the metric

③ Test set sampling
  100 questions → another 100 questions → another result
  → bootstrap based on test set examples

④ Dispersion of the judge model
  LLM-as-judge is itself non-deterministic
  → run judge k≥3, consider inter-rater agreement

⑤ Order of few-shot examples
  mix → different result
  → average over several orders
03

Testing Statistical Hypotheses

4 cards
09HypothesesNull hypothesis, p-value and how not to read them

Hypothesis testing is a formal procedure for deciding whether the improvement is real or random. The p-value is the most commonly misinterpreted value in science. Let's figure out exactly what it means and what it doesn't.

Diagramtext
Staging:
  H₀ (null): "prompt A and prompt B give the same result"
  H₁ (alternative): "prompt B is better than prompt A"

p-value = P(get this or more extreme difference | H₀ is correct)

Example:
  Prompt A: accuracy = 0.780 on 200 examples
  Prompt B: accuracy = 0.810 on 200 examples
  p-value = 0.032

Correct interpretation:
  “If H₀ were true, the probability of seeing a difference ≥ 0.03 by chance = 3.2%”
  At threshold α=0.05 → we reject H₀, the difference is statistically significant

WRONG interpretation (common mistakes):
  ✗ “probability that H₀ is correct = 3.2%” ← no
  ✗ “probability that the result is random = 3.2%” ← no
  ✗ “the effect is large and important” ← p does not indicate the size of the effect
  ✗ “p=0.049 and p=0.051 are fundamentally different” ← no, this is a continuous scale

typical thresholds α

  • p < 0.05 - standard (5% probability of false alarm)
  • p < 0.01 - strict
  • p < 0.1 - soft (research)
  • p by itself without effect size is an incomplete picture
10Hypothesest-test: comparison of two systems

The t-test tests whether the observed difference in means could have occurred by chance? Paired t-test - for the same sets of questions (A and B were assessed on the same data). Independent - for different sets.

paired t-test (recommended for LLM)sql
# Both prompts were run on the same 100 questions
scores_A = [0.8, 0.6, 1.0, …] # 100 values
scores_B = [0.9, 0.7, 1.0, …] # 100 values

diff = scores_B - scores_A # pairwise differences
# H₀: mean(diff) = 0

t = mean(diff) / (std(diff) / √n)
# compare with t-distribution (n-1 degrees of freedom)

from scipy.stats import ttest_rel
t_stat, p_value = ttest_rel(scores_B, scores_A)

Why paired is better than independent:
  removes variance from question difficulty
  → one complex issue affects both systems
  → difference is cleaner
11HypothesesWilcoxon: nonparametrics for scores 1–5

When an LLM-judge or person gives marks 1–5, the data is ordinal, not interval. The t-test assumes normality - this is violated for scores. The Wilcoxon test is a nonparametric analogue of the paired t-test and makes no assumptions about distribution.

algorithm and applicationsql
# Ratings of two systems (15) using 50 examples:
grades_A = [3, 4, 3, 5, 2, …]
grades_B = [4, 4, 4, 5, 3, …]

from scipy.stats import wilcoxon
stat, p_value = wilcoxon(grades_B, grades_A,
                          alternative='greater')

Works through difference ranks:
  diff_i = B_i - A_i
  → rank |diff_i|
  → compare the sums of ranks “+and “−”

When to use:
  ✓ ratings 1-5, 1-10 (LLM-judge, human eval)
  ✓ ordinal metrics (pass@k ranks)
  ✓ small samples (n < 30)
12HypothesesMultiple comparisons and Bonferroni correction

If you compare 10 prompts in pairs, that’s 45 tests. With α=0.05 and 45 tests, ~2 false significant results are expected simply by chance. The Bonferroni correction adjusts the significance threshold.

problem and solutionsql
Family-wise error rate (FWER):
  P(at least 1 false alarm from m tests)
  = 1 − (1−α)^m

  m=45, α=0.051−(0.95)^45 = 0.90 = 90%!

Bonferroni correction:
  α* = α / m = 0.05 / 45 = 0.0011
use this threshold for each test

Benjamini-Hochberg correction (FDR):
  less strict, controls the rate of false discoveries
  → recommended for large numbers of comparisons

from statsmodels.stats.multitest import multipletests
reject, p_adj, _, _ = multipletests(
  p_values, method='bonferroni' # or 'fdr_bh'
)

rule

  • compared 10 prompts, took the best one without correction → error
  • Bonferroni - strictly, few comparisons (<20)
  • BH / FDR - flexible, many comparisons
04

A/B Test of LLM systems

3 cards
13A/B testEffect Size: the difference is significant - but is it big?

p-value says “whether the difference is due to chance”, but nothing about its magnitude. With large n, even a difference of 0.1% will be statistically significant. Effect size is a separate value that answers the question “how big is the effect?”

Diagramtex
Cohen's d - standardized mean difference:

  d = (μ_B − μ_A) / s_pooled
  s_pooled = √( (s_A² + s_B²) / 2 )

Cohen's d interpretation:
  d = 0.2 → small effect (almost imperceptible)
  d = 0.5 → average effect (noticeable in use)
  d = 0.8 → large effect (obviously better)

Example:
  Prompt A: x̄=0.780, s=0.12
  Prompt B: x̄=0.810, s=0.11
  s_pooled = √((0.0144+0.0121)/2) = 0.115
  d = (0.810−0.780) / 0.115 = 0.26 ← small effect

  n=200 → p=0.031 → statistically significant
  d=0.26 → but practically a small difference

Conclusion: always report BOTH p-value AND effect size.

practice

  • d > 0.5 → worth implementing
  • d = 0.2–0.5 → depends on the cost of change
  • d < 0.2 → most likely not worth it
  • for win-rate: OR / relative lift as effect size
14A/B testPower Analysis: how many examples do you need?

Power (test power) - the probability of detecting a real effect if there is one. Power = 1 − β, where β is the probability of missing an improvement (type II error). Standard: power ≥ 0.80. Power analysis answers the question “how many examples are needed” before the experiment?

formula and examplesql
Four related parameters (know 3 → find 4th):
  α (significance level, usually 0.05)
  power (1−β, typically 0.80)
  d (expected effect size)
  n (← what we are looking for)

Minimum n for paired t-test:
from statsmodels.stats.power import TTestPower
analysis = TTestPower()
n = analysis.solve_power(
  effect_size=0.3, # expected Cohen's d
  alpha=0.05,
  power=0.80
)
# → n ≈ 90 examples per group

Empirical guidelines for LLM:
  d=0.5 → n ≥ 34 (large expected effect)
  d=0.3 → n ≥ 90
  d=0.2 → n ≥ 200 (small effect - you need a lot)
15A/B testCorrect setup of an A/B experiment

Most errors in LLM experiments are not mathematical, but design ones: the test set is chosen incorrectly, systems are compared on different data, or the metric does not correspond to the task.

design checklistsql
① Formulate a hypothesis BEFORE the experiment
  "Prompt B gives higher F1 on task X"
not “let’s see what’s best”

② Fix the metric BEFORE the run
  → you cannot select a metric after, based on the best result

Use the SAME test set for A and B
  → paired test, removes variance from the difficulty of questions

④ The test set should not overlap with the dev set
  → otherwise you optimize for a test without knowledge

⑤ Run each system k≥3 times (at T>0)
  → average across runs, estimate σ as a function of temperature

⑥ Calculate n using power analysis IN ADVANCE
  → don't stop when p < 0.05 “for the first time”
05

LLM-specific assessment

4 cards
16LLM EvalLLM-as-Judge: Model Evaluator Statistics

LLM-as-judge is itself a stochastic process with variance. Before trusting his assessments, you need to measure the consistency of the judge with yourself and with people. Only then can the results be interpreted as a metric.

Diagramsql
Three questions about quality judge:

① Intra-rater reliability
  Run judge on some examples k=5 times
  σ_judge = std(scores per example)
if σ_judge > 0.5 points out of 5 → judge is unstable
  → average 35 runs before the final score

② Human correlation
  Take 50100 examples from human labels
  Spearman ρ = rank correlation(judge_score, human_score)
  ρ > 0.7 → judge is quite reliable
  ρ < 0.5 → judge is not suitable for this task

③ Consistency between judge models
  Claude judge vs GPT-4 judge → correlation?
  Cohen's κ for binary solutions (pass/fail)
  κ > 0.6 → good agreement

Inter-rater reliability (κ for two evaluators):
  κ = (P_observed − P_chance) / (1 − P_chance)
  κ = 0.2–0.4 → weak agreement
  κ = 0.4–0.6 → moderate
  κ = 0.6–0.8 → good → can be trusted

practice

  • validate judge on ~100 human-labeled examples
  • average 3+ runs of judge at T>0
  • set judge T=0 for reproducibility
  • one run judge = not a metric, but a random point
17LLM EvalWin-Rate and its statistics

Win-rate (the percentage of victories of A over B in pairwise comparisons) is an intuitive metric. But how do you know if the difference 55% vs 45% is significant? This is a problem for a fraction test - the binomial test or chi-square.

win-rate significance testsql
# 100 comparisons A vs B
wins_B = 58 # B won 58 times
n = 100 # total comparisons
# H₀: p(B wins) = 0.5 (equal systems)

from scipy.stats import binomtest
result = binomtest(wins_B, n, p=0.5,
                  alternative='greater')
print(result.pvalue) # → 0.044 → significant!

# CI for win-rate (Wilson interval):
from statsmodels.stats.proportion import proportion_confint
ci = proportion_confint(wins_B, n, method='wilson')
# → (0.483, 0.671)

Minimum significant difference=0.05):
  n=50 → win-rate > 64% significant
  n=100 → win-rate > 59% significant
  n=200 → win-rate > 56% significant
18LLM EvalElo Rating for models

When comparing more than two systems, pairwise win-rates are not transitive. Elo-rating (from chess) solves this problem: each win/loss updates the rating taking into account the expected result. Used in Chatbot Arena (LMSYS).

Elo mechanicstext
# Expected probability of A winning over B:
E_A = 1 / (1 + 10^((R_B − R_A) / 400))

# Rating update after A vs B match:
# S_A = 1 (win), 0.5 (draw), 0 (loss)
R_A_new = R_A + K (S_A − E_A)
#K = learning factor (usually 32)

Initial rating: 1000 for everyone
Difference 400 → expected ~10% wins for the weakest

Bootstrap CI for Elo (Chatbot Arena approach):
→ resample paired matches 1000 times
→ recalculate Elo for each bootstrap
→ 95% CI by percentiles
19LLM EvalAggregate Score: when the average deceives

An average score across multiple tasks/benchmarks masks uneven quality. A model can perform well on 9 out of 10 problems and fail on the 10th - the average score still looks good.

Diagramtex
Example:
             Task1 Task2 Task3 Task4 Task5 Avg
Model A: 0.90 0.85 0.88 0.91 0.87 0.882
Model B: 0.92 0.89 0.90 0.93 0.45 0.818

On average, A is better. But what if Task5 is your task?

See also:
  → minimum tasks (worst-case quality)
  → variance by task (how smooth)
  → profile by task (not only the unit)

Weighted average:
  score = Σᵢ wᵢ score_i
  → weigh the tasks in your use-case by frequency
  → MMLU takes 57 tasks → outweighs others
06

Pitfalls in Evaluating LLM Systems

3 cards
20TrapsData Contamination, Benchmark Overfitting and Goodhart's Law

The three most dangerous pitfalls when evaluating LLM: test data leaked into training, the metric no longer reflects real quality, or the model is “tailored” for a benchmark. All three make the comparison meaningless.

Diagramsql
Data Contamination:
  The test set accidentally ended up in the pretraining data of the model
  → the model “saw the answers” in the exam
  Symptom: PPL on the test set is abnormally low
  Sign: the model produces accurate verbatim answers
  Protection: use closed/fresh test sets
  Protection: membership inference test (Min-K% Prob)

② Benchmark Overfitting:
  The model was additionally trained on tasks similar to the benchmark
  → MMLU is growing, real tasks are not
  Symptom: gap between benchmark and live quality
  Protection: several different benchmarks
  Defense: human eval as ground truth

③ Goodhart's Law:
  "When a measure becomes a target, it ceases to be a good measure"
  You optimize ROUGE → the model generates n-grams from reference
  You optimize the LLM-judge score → the model learns to “like” judge
  Protection: regularly change metrics and test sets
  Defense: human evaluation as the final arbiter
21TrapsJudge's biases: attitude and verbosity

LLM-judge systematically distorts scores in favor of the first or second answer (positional bias) and in favor of longer answers (verbosity bias) - regardless of the quality of the content.

types of displacements and protectionsql
Positional bias:
  judge([A, B]) → A wins 65% of the time
  judge([B, A]) → B wins 62% of the time
  → the first one wins both times!
  Defense: Always run in both orders
  Result: win only if you win in BOTH runs
  or take the average if there is a draw

Verbosity bias:
  judge prefers a longer answer
  even if it's less accurate
  Defense: explicitly in the judge prompt: “length is not a criterion”
  Defense: normalize the score by the length of the answer

Self-enhancement bias:
  Claude judge overestimates Claude answers
  GPT-4 judge overestimates GPT-4 responses
  Protection: use judge from another provider
22TrapsPrompt Sensitivity: assessment instability

A difference in the formulation of a prompt by ±10% of the metric is a common phenomenon. This means that “comparing systems” without prompt control is actually “comparing prompts.” The result depends on who wrote the better prompt for their system.

how to controltext
Antipattern:
  System A: Prompt v7 (carefully written)
  System B: prompt v1 (draft)
  → you compare the quality of prompting, not systems

That's right - fix the prompt template:
  one template for both systems
  vary ONLY the parameter being studied

Or - multi-prompt evaluation:
  k=5 prompt options for each system
  estimate = mean ± std for all options
  → you evaluate robustness, not a point result

Sensitivity measurement:
  CV = std/mean (coefficient of variation)
  CV > 0.1 → system is unstable to formulation
07

Practice: Checklist and Rules

2 cards
23PracticeChecklist for a statistically valid LLM experiment

Go through this list before and after launch. If at least one point is missed, the conclusions are questionable.

Diagramtext
BEFORE THE EXPERIMENT:
  □ The hypothesis is formulated explicitly (H₀ and H₁)
  □ The metric is selected in advance and corresponds to the task
  □ n calculated using power analysis (power ≥ 0.8)
  □ The test set is representative of prod traffic
  □ The test set does not overlap with dev/train

LAUNCH:
  □ Both systems were run using the SAME examples
  □ For T > 0: each example is run k ≥ 3 times
  □ Judge runs in both orders (A,B) and (B,A)
  □Seed fixed (for reproducibility)
  □ Everything is logged: prompts, temperatures, model versions

ANALYSIS:
  □ CI calculated (bootstrap if the metric is not normal)
  □ Correct test selected (paired vs independent)
  □ If > 1 comparison – Bonferroni/BH correction applied
  □ Effect size calculated (Cohen's d or relative lift)
  □ The consistency of judge (κ or ρ with human) is checked

REPORT:
  □ n is specified explicitly
  □ CI is shown next to the mean: 0.815 ± 0.021 (95% CI)
  □ p-value AND effect size (not just one of the two)
  □ The source of variance is described (temperature? test-set sampling?)
  □ Versions of models and prompts are indicated
24PracticeQuick Reference: Method Selection

Quick cheat sheet: which statistical method to use in typical LLM scenarios.

TaskMethodPython
Compare 2 promptspaired t-testttest_rel(a, b)
Ratings 1–5 from judgeWilcoxonwilcoxon(a, b)
Win-rate significanceBinomial testbinomtest(w, n)
CI for any metricBootstrapbootstrap((x,), np.mean)
Compare N systemsElo + bootstrap CIelo_mle() + resample
N prompts → choosing the best+ Bonferronimultipletests(p_vals)
Judge vs human agreementSpearman ρ / Cohen κspearmanr / cohen_kappa
How many examples do you need?Power analysisTTestPower().solve_power

minimum stack

  • scipy.stats
  • statsmodels
  • numpy (bootstrap)
  • ragas (for RAG metrics)

No dead end

Keep moving through the map.

Continue in sequence, switch to a related guide, or return to the seven-track learning map.