TAI BUI
Back to curriculum
CapstonePython

Result Evaluator

The runner produced numbers. The evaluator decides whether they mean an improvement, a regression, or noise, and boils that down to a one line verdict.

TIME: ~90 minutesPREREQUISITES: Phase 19 Track A lessons 20-29

Learning Objectives

  • Compare a candidate run against a baseline using direction aware improvement and a fixed threshold.
  • Run a paired t test from scratch over per seed metrics and read the resulting p value.
  • Normalise log scaled metrics so a downstream report can blend them with linear metrics.
  • Emit a per hypothesis verdict that the orchestrator can attach to the queue from lesson fifty.
  • Keep every step pure so the same inputs always produce the same verdict.

Why a paired test

A paired test compares two configurations on matched runs: the same seeds and the same data, run once with the candidate and once with the baseline. Each seed contributes one difference. The mean of those differences is the effect, and the standard error of those differences is the noise floor.

The reason to pair is that a single number from the runner cannot tell you whether a change is real. The same configuration with a different seed gives a different perplexity, so one gap between two runs might be noise.

I implement the test from scratch, with no scipy.stats. The math is small enough to read in one screen.

text
diffs    = [a_i - b_i for i in seeds]
mean     = sum(diffs) / n
variance = sum((d - mean) ** 2 for d in diffs) / (n - 1)
t_stat   = mean / sqrt(variance / n)
df       = n - 1
p_value  = two_sided_p(t_stat, df)

The two sided p value comes from a regularised incomplete beta function. I ship a small implementation built on the Lentz continued fraction. The whole thing is sixty lines of stdlib math.

Check: Why a paired test0/4
1. What is a paired test in this evaluator?Define
2. In the paired test, what does the standard error of the per-seed differences represent?Explain
3. Per-seed differences are 1 and 3 (two seeds). Using the sample variance with n minus 1 in the denominator, what is the t statistic?Apply
4. Candidate scores are [1, 2, 3] and baseline scores are [3, 4, 5] on the same three seeds. What p value does paired_t_test return?Apply

Direction aware improvement

Improvement is the signed fraction by which the candidate beats the baseline, and "beats" depends on the metric. Some metrics improve when they go up (accuracy, throughput). Others improve when they go down (loss, perplexity, wall time). Each metric carries a direction field that says which.

text
if direction == "higher_is_better":
    improvement = (candidate - baseline) / abs(baseline)
elif direction == "lower_is_better":
    improvement = (baseline - candidate) / abs(baseline)

The sign matters. A negative improvement on a higher is better metric means the candidate is worse, and the verdict path reads the sign and the magnitude together.

A flat threshold (improvement_threshold=0.02, two percent) decides whether the change is large enough to call. Below it the verdict is "noise" regardless of the p value. The loop has no use for a change the user could not measure.

Check: Direction aware improvement0/9
1. What does a metric's direction field say?Define
2. What is improvement in the evaluator?Define
3. What is the improvement threshold?Define
4. Why does the improvement formula flip its sign for a lower is better metric?Explain
5. A loss (lower is better, linear scale) is 0.50 for the baseline and 0.45 for the candidate. What improvement does the evaluator report?Apply
6. Accuracy (higher is better) is 0.800 for the baseline and 0.815 for the candidate, with a tiny p value and the default thresholds. What verdict comes back?Apply
7. A teammate proposes dropping the improvement threshold and trusting the p value alone, since significance already filters out noise. What goes wrong?Analyze
8. Perplexity (lower is better) is lower for the candidate on every seed, yet the Verdict reports a negative improvement and a regressed label. Which mistake fits best?Analyze
9. Your team wants to tune improvement_threshold per hypothesis after glancing at results, so promising ones get called. What is the best response?Design

Architecture

Mermaid diagram

The evaluator runs three independent computations and joins them in the verdict path. Each one is a pure function with no shared state.

Check: Architecture0/2
1. What does it mean that each computation in the evaluator is a pure function?Define
2. You write a test for the evaluator: same inputs, two calls. What should you expect, and why?Explain

Log normalisation

Log normalisation takes the natural log of a metric before the improvement is computed, so metrics that grow exponentially can sit in one report next to linear ones. Perplexity is the standard case. It is exponential in loss, so a 0.1 drop in loss is a much larger drop in perplexity. Comparing perplexity directly across two configurations is fine. Blending it with linear metrics in a single report needs the transform.

The evaluator normalises any metric whose scale field is "log", and the threshold is then applied in log space. A perplexity drop from 32 to 28 has a raw log difference of log(28) - log(32) = -0.133. The evaluator then divides by the absolute baseline log and flips the sign for a lower is better metric, so improvement = (log(28) - log(32)) / abs(log(32)) with the sign flipped is about 0.0385. That is still above the two percent threshold.

text
if scale == "log":
    a = log(candidate)
    b = log(baseline)
else:
    a = candidate
    b = baseline

Metrics with scale="linear" (the default) skip the transform. One code path handles both.

One detail to keep straight: the improvement and the p value come from log-space values, but candidate_mean and baseline_mean in the Verdict are plain means of the original-scale metric (28 and 32 in the example, not their logs). Do not recompute the improvement from those two fields and expect it to match.

Check: Log normalisation0/5
1. What is log normalisation in the evaluator?Define
2. Perplexity is exponential in loss. What problem does that create when a report blends perplexity with linear metrics?Explain
3. For a log scale metric, what are candidate_mean and baseline_mean in the Verdict?Explain
4. Perplexity drops from 32 to 28 on a log scale, lower is better metric. About what improvement does the evaluator report?Apply
5. A report shows candidate_mean 28.1 and baseline_mean 32.0 for a log scale, lower is better metric, but improvement 0.0375 rather than (32 - 28.1) / 32, about 0.12. What explains the gap?Analyze

Per seed paired test

The runner from lesson fifty-two emits one final metrics blob per run. The paired test needs one blob per seed for the candidate and one per seed for the baseline. The orchestrator runs the same experiment under both configurations across a list of seeds and hands the evaluator two lists of ExperimentResult records.

The evaluator pairs them by seed, which lives in result.metrics["seed"], and walks the requested metric. Only seeds present in both lists are paired; seeds that appear in just one list are dropped silently. It raises a PairingError when the two lists share no seeds, when a result has no seed, or when a seed appears twice within one list, and the orchestrator should re-run.

Check: Per seed paired test0/4
1. What is a PairingError?Define
2. The candidate seeds are 0 to 7 and the baseline seeds are 4 to 11. What does the evaluator do?Explain
3. The candidate list has seeds 0, 1 and 1 (a repeat). What happens when it is evaluated?Apply
4. You can afford 12 training runs in total for one hypothesis. Which allocation works best with this evaluator?Design

The Verdict shape

A Verdict is the evaluator's output record for one hypothesis: the means, the signed improvement, the p value, both thresholds, a label, and a one line reason.

text
Verdict
  hypothesis_id          : int
  metric                 : str
  direction              : "higher_is_better" | "lower_is_better"
  scale                  : "linear" | "log"
  candidate_mean         : float
  baseline_mean          : float
  improvement            : float       (signed, fraction; see direction rules)
  p_value                : float | None  (None if n < 2)
  significance_threshold : float
  improvement_threshold  : float
  verdict                : "improved" | "regressed" | "noise" | "failed"
  rationale              : str

The label comes from a small decision table:

text
1. If any candidate or baseline result has terminal != "ok": verdict = "failed"
2. else if |improvement| < improvement_threshold:  verdict = "noise"
3. else if p_value is None or p_value > significance: verdict = "noise"
4. else if improvement > 0:                          verdict = "improved"
5. else:                                             verdict = "regressed"

The rationale is a one line, human readable sentence the orchestrator can log against the hypothesis id.

Check: The Verdict shape0/11
1. What is a Verdict?Define
2. What does the failed verdict label mean?Define
3. What is the significance threshold used for?Define
4. When does the paired t test helper return no p value (None)?Explain
5. Why does the evaluator return a failed verdict for a crashed run instead of labeling it noise?Explain
6. What happens if even one baseline result has a terminal label other than ok, while every candidate run is ok?Apply
7. You evaluate perplexity (lower is better, log scale) over 8 paired seeds. The improvement comes back at about 0.04, but the p value is 0.31. What is the verdict and why?Apply
8. On an accuracy metric (higher is better) the evaluator reports improvement -0.06 with p = 0.01. What does the decision table return?Apply
9. Every run finished ok, but only seed 5 appears in both the candidate and baseline lists. That one pair gives an improvement of 0.10 on a higher is better metric. What verdict comes back?Apply
10. The candidate list has only seed 0 and the baseline list has only seed 99, and one baseline run ended with terminal oom. What does evaluate return?Analyze
11. A loop labels most hypotheses noise although their p values are all below 0.001 and their improvements are about 0.01. Which gate explains this?Analyze
Author's Note

I'd decide the improvement threshold before looking at a single result. It encodes what the loop cares about, and it runs ahead of the significance gate, so it can overrule a p value. Set after seeing the numbers, it becomes a dial for steering verdicts.

My bet is that the "failed" label deserves suspicion when you build the orchestrator. A crashed run and a flat result both leave you without an improvement, but they are different situations. I'd keep them as separate verdicts, and I'd want the rationale to name the offending terminals so a person scanning the log can tell which one they are looking at.

I'd also hand-check a verdict before trusting the rest. Take the 32 to 28 perplexity example and work the log transform on paper. The log difference is -0.133, but the evaluator reports the improvement as a fraction of the baseline's log, so it prints a positive number near 0.04 for a lower-is-better metric. If your hand number and the printed one disagree after that conversion, I'd look at the direction and scale rules first.

How to read the code

code/main.py defines MetricSpec, Verdict, Evaluator, the t statistic and incomplete beta helpers, and a deterministic demo. The t test itself is pure stdlib math. numpy only reads the metrics list and computes means and variances.

code/tests/test_evaluator.py covers the improved path, the regressed path, the noise path (small improvement), the noise path (low n), the failed terminal path, the log normalised path, the t test against a known reference value, and the pairing error.

Where this slots in

Lesson fifty produced the hypothesis queue. Lesson fifty-one filtered out anything the literature had already settled. Lesson fifty-two ran the experiment under candidate and baseline configurations across seeds. Lesson fifty-three reads those runs and writes the verdict. The orchestrator stitches the four together:

text
for hypothesis in queue:
    literature = retrieval.search(hypothesis.text)
    if literature_settles(hypothesis, literature):
        attach(hypothesis, verdict="settled")
        continue
    candidates = runner.run_all(specs_for(hypothesis))
    baselines  = runner.run_all(baseline_specs_for(hypothesis))
    metric_spec = MetricSpec("perplexity", direction=LOWER, scale=LOG)
    verdict = evaluator.evaluate(hypothesis.id, metric_spec, candidates, baselines)
    attach(hypothesis, verdict)

That orchestrator is not part of this lesson. The four lessons compose into it with no glue beyond the dataclasses each one defines.

Warm-up0/3
1. You train the same model twice with the same settings but different random seeds, and the final accuracies differ slightly. What does that tell you about comparing two configurations using one run each?Warm-up
2. Which of these metrics improves when its value goes down?Warm-up
3. A significance test returns a p value of 0.40 for the gap between two models. What is the usual reading?Warm-up
Check: Where this slots in0/1
1. A single report must cover perplexity and accuracy for the same hypothesis. How should the two MetricSpecs be set up?Design

Code