Evaluation & Analytics

Plots That Tell
the Truth

Your team ran one eval. Model B scored 85.1%, Model A scored 84.7%. One engineer's slide says "B crushes A." Another's says "no detectable difference." Both used the same CSV. This lesson makes you the prosecutor who catches the lie and the witness who builds a plot that survives cross-examination.

Prerequisites: The Statistics of Evaluation (for the SE / CI / p-value machinery every interval here consumes). Comfort with basic arithmetic. That's it.
11
Chapters
11
Live Simulations
Q&A
Interview Arsenal

Chapter 0: Same Data, Two Plots, Opposite Conclusions

Your team just finished evaluating two camera-trap species classifiers on the same 500 images. Model A got 84.7% accuracy; Model B got 85.1%. Two engineers each make one slide.

Slide 1 shows two bars with the y-axis running from 84% to 86%. Model B's bar towers over Model A's. The caption reads: "B crushes A." Slide 2 shows the same two bars, but the y-axis runs from 0% to 100% and each bar wears a 95% confidence-interval whisker. The bars are visually identical; the whiskers overlap almost completely. The caption reads: "A and B are statistically indistinguishable."

Neither engineer touched the data. The CSV is byte-for-byte the same. What changed between the two slides is the encoding — the free parameters of the plot: where the axis starts, what range it spans, whether uncertainty is drawn. A plot is not a photograph of your numbers. It is an argument, and every argument can be made honestly or dishonestly.

Think of a plot as a rendering pipeline: data → encodings (position, length, area, color) → the reader's visual system → a conclusion. Every stage between the data and the conclusion can distort. The bars in Slide 1 lied not because the accuracies were wrong, but because the reader's eye measured bar length while the axis quietly redefined what a unit of length means.

Edward Tufte gave this distortion a number. The lie factor is the size of the effect shown in the graphic divided by the size of the effect in the data. An honest plot has a lie factor near 1 — the picture exaggerates the effect by roughly 1x, i.e. not at all. A lie factor of 10 means the picture makes the effect look ten times bigger than it is.

Bar charts encode a value as length measured from zero. That is the whole reason a bar chart works: your eye compares the lengths of the bars. Truncate the baseline — start the axis at 84% instead of 0% — and you break the encoding. The drawn length no longer means the value. In the worked example below we will compute the lie factor of exactly this truncation: it comes out to 121. The picture exaggerates a real 0.47% edge into a 57% visual gap.

The second lie is not distortion but omission. Slide 1 had no error bars, so the reader had no way to see sampling noise. With n=500 images per model, the standard error of the difference in accuracy is about 2.3 percentage points — nearly six times larger than the 0.4-point gap being shouted about. The z-score of that gap is 0.18, which is pure noise. (The machinery for computing SE and z lives in The Statistics of Evaluation; here we only care that the plot hid it.)

The two lies compound viciously. Truncation inflates the visual gap 121x, and missing error bars remove the only cue that the gap is noise to begin with. The combination — a zoomed axis plus no uncertainty — is the single most common dishonest eval plot in ML papers, product reviews, and executive decks. Once you can spot it, you will see it everywhere.

Now, honesty is not the same as "always start at zero." A line chart of a loss curve legitimately zooms, because a line encodes value as position along a scale, not length from zero. Your eye reads where the point sits, not how long a bar is. The rule is subtler and more useful: the encoding must match what the reader's eye actually measures. Bars encode length, so a zero baseline is mandatory. Lines and points encode position, so zooming is allowed — but you must annotate the range so the reader knows the frame is cropped.

Misconception: "A plot is a neutral picture of the numbers — if the data is correct, the plot is correct." No. A plot is a rendering with free parameters (baseline, range, error bars, scale), and those parameters carry the argument. The same correct CSV rendered two ways produced a 121x visual exaggeration of a p=0.86 non-effect. Data integrity does not imply plot integrity — and the person reading your plot cannot audit your data, only your rendering.

Play with the simulation below. It draws the same two accuracies (A = 84.7%, B = 85.1%, n = 500) and lets you toggle the two lies on and off. Truncate the axis and watch B's bar balloon. Turn on the 95% CI whiskers and watch them swallow the gap. The live caption tells you what a naive reader would conclude, and the lie-factor badge quantifies the distortion in real time.

The Honesty Toggle

Same data every time: A = 84.7%, B = 85.1%. Truncate axis starts the y-axis at 84% (breaking the length encoding). Show 95% CI draws whiskers whose width shrinks as you raise n. The badge turns red and reads the lie factor when the plot is dishonest; green at 1.0x when it is honest.

n per model 500

Worked example: putting a number on the lie

Let us compute the lie factor by hand, then confirm the gap is not even real. Model A accuracy 0.847, Model B accuracy 0.851, each on n = 500 images. The dishonest slide plots bars from a baseline of 0.84.

(a) The lie factor. First, the heights as actually drawn. From the 0.84 baseline, A draws as a length of 0.847 − 0.840 = 0.007 and B draws as 0.851 − 0.840 = 0.011. The visual effect — how much bigger B looks than A — is the relative difference in drawn lengths:

visual effect = (0.011 − 0.007) / 0.007 = 0.004 / 0.007 = 0.5714

So B looks 57% taller than A. Now the data effect — how much better B actually is, relative to A's value:

data effect = (0.851 − 0.847) / 0.847 = 0.004 / 0.847 = 0.004723

B is a genuine 0.47% better. The lie factor is the ratio of the two:

lie factor = 0.5714 / 0.004723 = 121.0

The picture exaggerates the effect by 121 times. Tufte's threshold for "substantial distortion" is a lie factor outside the band 0.95 to 1.05. We are off by two orders of magnitude.

(b) Is the gap even real? A proportion measured on n samples has standard error √(p(1−p)/n). For A: √(0.847 × 0.153 / 500) = √(0.0002592) = 0.01610. For B: √(0.851 × 0.149 / 500) = √(0.0002536) = 0.01593. The SE of the difference combines them in quadrature (variances add):

SEdiff = √(0.01610² + 0.01593²) = √(0.0005128) = 0.02264

The z-score of the 0.004 gap is:

z = 0.004 / 0.02264 = 0.177  →  two-sided p = 0.860

The chart shouted a 57% visual difference about an effect whose p-value is 0.86 — a result you would see 86% of the time from two identical models. The plot manufactured a breakthrough out of noise.

python — from scratch
# (a) Tufte's lie factor for a truncated bar chart
def lie_factor(baseline, v1, v2):
    h1, h2 = v1 - baseline, v2 - baseline   # drawn heights
    visual = (h2 - h1) / h1               # relative gap in the picture
    data   = (v2 - v1) / v1               # relative gap in the numbers
    return visual / data

print(round(lie_factor(0.84, 0.847, 0.851), 1))   # 121.0
python — library equivalent (numpy + scipy)
import numpy as np
from scipy import stats
se = np.sqrt(0.847*0.153/500 + 0.851*0.149/500)
z  = 0.004 / se
print(round(se, 5), round(z, 3), round(2*(1-stats.norm.cdf(z)), 3))
# 0.02264 0.177 0.86  — same answer, three numbers, one non-effect

The honest render is a one-liner: draw the bars from zero and attach the whiskers.

python — the honest bar chart
ax.bar(x, accs, yerr=1.96*np.sqrt(accs*(1-accs)/n), capsize=4)
ax.set_ylim(0, 1)   # zero baseline is not optional for bars
Interview lens. A PM shows you a bar chart where your new ranking model "clearly beats" the old one and asks you to sign off on the launch. Walk through auditing that single chart. A staff-level answer runs a fixed checklist: (1) check the y-axis baseline and range — bars need a zero baseline, so eyeball or compute the lie factor; (2) ask for uncertainty — the n per condition, and whether the whiskers are SD, SE, or CI; (3) ask how the numbers were selected — one seed or the mean of several, one slice or overall; (4) reconstruct the honest version mentally — at this n, is the gap bigger than roughly two standard errors of the difference? The senior signal is volunteering the 121x-style quantification instead of vaguely saying "the axis looks truncated," and distinguishing the encoding rule for bars (zero baseline) from lines (zoom, but annotate).

By the end of this lesson you will hold a lie-detector toolkit. We will look at distributions before summaries (Ch1), put uncertainty on every comparison (Ch2), read training curves like an ECG (Ch3), decompress the classifier trade-off surface (Ch4), keep scaling-law extrapolations honest (Ch5), catch a slice collapsing while the average rises (Ch6), stack every lie at once in the Honest Plot Builder (Ch7), turn it all into reusable matplotlib code (Ch8), drill it as interview Q&A (Ch9), and wire it into the whole evaluation family (Ch10). Every chapter ends with an interview question, because staff engineers get tested on exactly this judgment.

A bar chart shows Model B's bar looking twice as tall as Model A's. The underlying accuracies are 84.7% and 85.1%. What made this possible?

Chapter 1: Distributions First — Bins, Bandwidths, and the ECDF

Before you ever compare two models with one number each, look at the full distribution of what they do. And beware: the tool you reach for to look — the histogram — has a hidden knob that can manufacture a story or erase one.

Take a concrete case. Your speech-recognition service records the latency of every request. The mean latency is 34 ms. But the mean is one number describing 200 numbers. Behind that 34 ms could hide a tight bell (every request is about 34 ms), a two-humped mix (cache hits at 15 ms, cache misses at 55 ms), or a long tail (most requests are fast but 2% are catastrophic 400 ms). Each shape demands a completely different engineering fix, and the mean cannot tell them apart.

The histogram is the default tool for seeing shape: chop the value range into bins, count how many points fall in each, draw bars. Its hidden argument is the bin width. Take the same 200 latencies. Five wide bins show one smooth hump — the bimodality is erased. Twenty medium bins show two clear modes — cache hit near 15 ms, cache miss near 55 ms. Two hundred tiny bins show spiky noise, every bin holding 0 or 1 points. Three bin widths, three incompatible stories, one dataset.

Rules of thumb exist precisely because eyeballing fails. Sturges' rule sets the bin count to 1 + log₂n — good for small, roughly-normal data, but it only looks at n, not the spread. Freedman-Diaconis sets the bin width to 2 × IQR / n1/3 — robust to outliers because it uses the interquartile range (IQR) rather than the full range, which a single 400 ms outlier would blow up. We will work the FD formula by hand below. Treat both as starting points, never as truth: always wiggle the width and check the story is stable.

Kernel density estimation (KDE) replaces hard bins with a smooth bump placed on each point, then sums the bumps into a curve. It looks more sophisticated, but it just relocates the knob: instead of bin width you now tune bandwidth. Too small and the curve is spaghetti; too large and it collapses to one bland hump that hides the bimodality exactly like the 5-bin histogram did. Silverman's rule gives a default, and the same stability test applies.

Now the hero of this chapter: the ECDF, the empirical cumulative distribution function. Sort the data, then plot — for each value on the x-axis — the fraction of points at or below it on the y-axis. There is no bin width, no bandwidth, no tuning parameter of any kind. Every single data point is visible as one step in a staircase. You can read the median, any tail percentile (p95, p99), and the fraction of requests below any SLO threshold directly off the curve. Two models overlay as two clean step curves — no alpha-blended histogram mush.

Learn to read the ECDF's shape. A steep region means many points are packed there — that is a mode. A flat plateau means a gap between clusters where almost no points live — that flat shelf in the middle of the staircase is the unmistakable signature of bimodality. A long shallow right tail means a heavy tail of rare large values. Our cache-hit / cache-miss latencies show a clear plateau between roughly 25 and 45 ms, where the two clusters have no overlap.

What about box plots? A box plot compresses the whole distribution to five numbers (roughly min, Q1, median, Q3, max), and that compression is exactly why it is the classic bimodality-hider: a two-humped distribution and a flat uniform one can produce nearly identical boxes. Violin plots fix this by drawing the KDE silhouette on each side — but they inherit KDE's bandwidth knob. A practical hierarchy for eval reports: ECDF when you need precision or want to overlay two systems; violin when you need to eyeball many groups at once; box plot only when you have dozens of groups and need compactness — and say so in the caption.

Close the loop to evaluation. Latency SLOs, per-example loss distributions, LLM-judge score distributions, reward distributions in RL — in every case, plot the distribution before trusting any mean-based comparison. A mean regression of +2 ms could be "everything got slightly slower" (probably harmless noise) or "a new 1% tail of 500 ms requests" (an incident). The mean is identical in both; only the distribution view distinguishes them.

Misconception: "The histogram shows me the distribution, objectively." It shows you the distribution filtered through a bin width you chose — and the default is chosen by your plotting library, not by your data. The same 200 latencies look unimodal at 5 bins and bimodal at 20. The ECDF is the only common distribution plot with zero tuning parameters. When a histogram and an ECDF of the same data disagree, believe the ECDF — only one of them has a knob someone could have turned.

Drag the bin slider below. One fixed dataset of 200 bimodal latencies never changes, but the histogram's story flips from "one hump" to "two modes" to "noise" as you slide. The ECDF panel beneath it never changes — watch its plateau stay put no matter what the histogram claims. Drag the threshold line to read off "X% of requests ≤ T ms."

The Bin-Width Trap

Top: histogram of 200 fixed bimodal latencies at the current bin count (markers show the Sturges and Freedman-Diaconis choices). Bottom: the ECDF of the same data — parameter-free, so its shape is stable. Drag the vertical line on the ECDF to read the fraction below a threshold. Resample draws a fresh 200 points to reveal which view is stable.

bins 9
threshold (ms) 30

Worked example: FD width and reading an ECDF

Take eight ASR request latencies in ms: 12, 15, 18, 22, 30, 45, 80, 210. We will compute (a) the Freedman-Diaconis bin width, (b) the ECDF value at 30 ms, and (c) how FD and Sturges behave for the full n = 200 set.

(a) FD width needs the IQR. With linear-interpolation percentiles on n = 8 sorted points, the Q1 position is 0.25 × (8−1) = 1.75 — three-quarters of the way from the 2nd value (15) to the 3rd (18):

Q1 = 15 + 0.75 × (18 − 15) = 15 + 2.25 = 17.25

The Q3 position is 0.75 × 7 = 5.25 — a quarter of the way from the 6th value (45) to the 7th (80):

Q3 = 45 + 0.25 × (80 − 45) = 45 + 8.75 = 53.75

So IQR = 53.75 − 17.25 = 36.5. The cube root of n is 81/3 = 2. Therefore:

FD width = 2 × IQR / n1/3 = 2 × 36.5 / 2 = 36.5 ms

(b) The ECDF at 30 ms. Count the points at or below 30: {12, 15, 18, 22, 30} — that is 5 of 8.

ECDF(30) = 5 / 8 = 0.625

(c) FD and Sturges for n = 200, IQR = 45. The cube root of 200 is 5.848, so:

FD width = 2 × 45 / 5.848 = 90 / 5.848 = 15.39 ms
Sturges bins = ⌈1 + log₂200⌉ = ⌈1 + 7.644⌉ = ⌈8.644⌉ = 9 bins

Notice how differently they behave. FD fixes the width from the data's spread; Sturges fixes the count from n alone and ignores spread entirely. Concretely: our n = 200 data spans some range R. Sturges says "use 9 bins" no matter what R is, so a dataset spanning 100 ms and one spanning 1000 ms both get 9 bins — the second gets 10x-wider bins and loses all fine structure. FD, keyed to the IQR (45 ms here), says "use 15.4 ms bins" regardless of a lone 400 ms outlier that would wreck any range-based rule. That robustness to outliers is exactly why FD is the safer default on latency and loss data, which are almost always right-skewed — but the only truly safe move is to check the ECDF too.

python — from scratch
import numpy as np
d = np.array([12,15,18,22,30,45,80,210.])

# manual ECDF: sort, then running fraction
xs = np.sort(d)
ys = np.arange(1, len(d)+1) / len(d)     # [1/8, 2/8, ... , 8/8]
# plt.step(xs, ys, where='post')  -> the staircase

# manual FD width
q1, q3 = np.percentile(d, [25, 75])
width = 2 * (q3 - q1) / len(d)**(1/3)
print(q1, q3, q3-q1, width)          # 17.25 53.75 36.5 36.5
print(np.mean(d <= 30))               # 0.625
python — library equivalent
import matplotlib.pyplot as plt
plt.hist(data, bins='fd')          # Freedman-Diaconis, chosen for you
plt.ecdf(data)                     # matplotlib >= 3.8
# or: import seaborn as sns; sns.ecdfplot(data)
Interview lens. You need to present p50/p95/p99 latency for two model-serving stacks to an infra review — what plot, and what are the failure modes of the alternatives? The strong answer is an overlaid ECDF (or, for heavy tails, the complementary CDF on a log-x axis): both stacks as step curves, horizontal guide lines at 0.5 / 0.95 / 0.99, so every percentile of both systems is readable from one plot — and crossings are visible (stack A better at p50, worse at p99 is a real, common trade-off that single-number comparisons hide). Histogram overlays fail via bin-width sensitivity and alpha-blend mush; box plots hide bimodality and the exact tail percentiles; a bar chart of p99 alone hides the crossing structure. Bonus points for noting that p99 estimates need on the order of thousands of samples to stabilize — deferring the CI machinery to eval-statistics.

We can now see distribution shape honestly. But the moment we start comparing two distributions with summary numbers, we need to put uncertainty on those numbers — and that is where three different whiskers, differing by a factor of five, cause more confusion than any other plotting decision. That is Chapter 2.

A box plot of per-request latencies looks perfectly ordinary, but users complain of two distinct experience tiers. Which plot would reveal the problem, and why?

Chapter 2: Error Bars Done Right — SD, SE, CI, and the Overlap Fallacy

You added whiskers to your bars — good instinct. But three different whiskers are commonly drawn on identical data, they differ by a factor of four, and most readers cannot tell you which one they are looking at. An unlabeled error bar is not evidence; it is decoration.

Start with the scandal. The same eval — mean 0.72, standard deviation 0.10, over n = 25 fine-tuning runs — can honestly wear three different whiskers. SD whiskers (±0.10) describe the spread of individual runs. SE whiskers (±0.02) describe the uncertainty in the mean. 95% CI whiskers (±0.041 with the small-sample correction) describe a range that would capture the true mean 95% of the time. Same data; whisker widths 0.10 vs 0.02 vs 0.041 — a 5x range. Without a caption saying which is which, the plot is meaningless.

When do you use which? Use SD when the reader should care about run-to-run variability itself — "how reproducible is this RL training?" Use SE or CI when the claim is about the mean — "model B's average score is higher." The CI is SE made interpretable: for n = 25 you multiply SE by the t critical value 2.064, not 1.96, because small samples need Student's t (the why lives in eval-statistics; here we just consume the number).

Walk the arithmetic pipeline once, by hand. SD = 0.10, n = 25, so the standard error is:

SE = SD / √n = 0.10 / √25 = 0.10 / 5 = 0.02

The normal 95% half-width would be 1.96 × 0.02 = 0.0392. The t-corrected half-width, with 24 degrees of freedom, is:

t0.975, 24 × SE = 2.064 × 0.02 = 0.0413

The t correction widened the interval by about 5% — small, but honest. Using 1.96 on n = 25 slightly understates your uncertainty every time.

Now the fallacy that fools even reviewers: "the CIs overlap, so the difference is not significant." This is false. Take two means, 0.72 and 0.78, each with SE 0.02. Their 95% CIs are [0.681, 0.759] and [0.741, 0.819] — they overlap by 0.018. Yet the test of the difference uses SEdiff = √(0.02² + 0.02²) = 0.0283, giving z = 0.06 / 0.0283 = 2.12, p = 0.034: significant at the 5% level. Overlapping CIs and a significant difference, both true at once.

Why? The difference's standard error grows by √2, not by 2. Comparing whisker tips implicitly demands the means be 2 × 1.96 = 3.92 SEs apart, but significance only needs 1.96 × √2 = 2.77 SEs apart. The visual "do the whiskers overlap" test is too conservative by a factor of √2 — it will make you dismiss real effects.

The professional fix: when the comparison is the point, plot the difference with its own CI and ask whether it excludes zero. Or use the 84% CI trick — two 84% intervals that just touch correspond to roughly p = 0.05 for two groups with similar SEs (we derive this exactly in Chapter 9). Difference-plots also compose beautifully: a forest plot of per-benchmark differences, each with its own CI, is the honest way to report a model comparison across a dozen benchmarks at once.

Paired data changes everything. If both models are evaluated on the same examples, plot the distribution or CI of the per-example differences. The pairing cancels example difficulty — a hard example that both models fail contributes nothing to the difference — and this often shrinks the interval dramatically. Showing two independent-looking CIs for paired data throws away exactly the information that makes the eval sensitive. (The paired-test math lives in eval-statistics; the plot rule is simply: paired data gets a difference plot.)

Make caption discipline a hard rule. Every error bar in every figure states (1) what statistic — SD, SE, or 95% CI; (2) the n; and (3) what unit is being resampled — runs? examples? seeds? Anthropic's 2024 note "Adding Error Bars to Evals" (Evan Miller) argued that most LLM eval reports omit all three, and made exactly this a norm-setting demand for the field.

Misconception: "Overlapping 95% confidence intervals prove there is no significant difference between two models." No. The whisker-tip test implicitly requires a 3.92-SE separation, while the actual significance threshold for a difference is 2.77 SEs — a √2 gap. Two CIs can overlap by a third of their length while the difference is significant at p = 0.03. When the comparison is the claim, plot the difference with its own CI and check whether it clears zero.

The simulation lets you feel the fallacy. On the left, one group's runs are drawn as jittered dots with all three whisker styles on the same mean, each labeled with its live width. On the right, two group means wear 95% CIs, plus a mini-axis showing the difference with its own CI and a verdict lamp. Drag group B's mean up and down; watch for the zone where the CIs overlap but the difference-CI still excludes zero — the fallacy zone glows.

The Whisker Trio and the Overlap Fallacy

Left: n draggable runs (slider) with SD, SE, and 95% CI whiskers on one mean — watch the 5x spread. Right: two means with 95% CIs and a difference axis with its own CI. Drag B's mean; when the CIs overlap yet the difference clears zero, the "fallacy zone" lamp lights.

n runs 25
mean B 0.780

Worked example: three whiskers, then the overlap fallacy

Part 1 — the three widths. 25 fine-tuning runs, mean 0.72, SD 0.10.

SE = 0.10 / √25 = 0.02
normal 95% half-width = 1.96 × 0.02 = 0.0392  →  CI = [0.6808, 0.7592]
t0.975, 24 = 2.0639  →  t-corrected half-width = 2.0639 × 0.02 = 0.0413

Part 2 — overlap yet significant. Two models, means 0.72 and 0.78, each SE 0.02. Using the normal half-width 0.0392:

CIA = 0.72 ± 0.0392 = [0.6808, 0.7592]
CIB = 0.78 ± 0.0392 = [0.7408, 0.8192]

They overlap: A's top (0.7592) exceeds B's bottom (0.7408) by 0.7592 − 0.7408 = 0.0184. Now test the difference directly:

SEdiff = √(0.02² + 0.02²) = √0.0008 = 0.02828
z = (0.78 − 0.72) / 0.02828 = 0.06 / 0.02828 = 2.121  →  p = 0.034

Significant at 5%, despite the visible overlap. If you had trusted the whiskers, you would have shipped the wrong conclusion.

python — from scratch
import numpy as np
from scipy import stats

se = 0.10 / np.sqrt(25)                        # 0.02
half_n = 1.96 * se                             # 0.0392
tcrit  = stats.t.ppf(0.975, 24)               # 2.0639
half_t = tcrit * se                           # 0.0413

# the overlap fallacy, quantified
overlap = (0.72+half_n) - (0.78-half_n)   # 0.0184
sed = np.sqrt(0.02**2 + 0.02**2)          # 0.02828
z = 0.06 / sed                              # 2.121
p = 2 * (1 - stats.norm.cdf(z))            # 0.034
print(round(overlap,4), round(sed,5), round(z,3), round(p,3))
python — library equivalent (seaborn bootstraps the CI)
import seaborn as sns
sns.pointplot(data=df, x='model', y='score', errorbar=('ci', 95))
# but the honest comparison plots the DIFFERENCE with its own CI:
d, sed = mb - ma, np.sqrt(sea**2 + seb**2)
ax2.errorbar([0], [d], yerr=1.96*sed); ax2.axhline(0)
Interview lens. Your eval report shows per-benchmark bars with whiskers, and a reviewer asks "are those error bars SD or CI, and over what?" — why does that question matter so much, and what standard would you institute? It matters because the three whiskers differ by up to the √n factor: SD describes run spread, SE describes mean uncertainty, CI is calibrated mean uncertainty — on n = 25 they were 0.10 vs 0.02 vs 0.041, and a reader who mistakes SD for CI will wrongly dismiss real effects (or vice versa). "Over what" matters equally: resampling over seeds, over eval examples, or over both answers different questions. A staff-level standard: every caption states statistic type, n, and resampled unit; comparisons additionally show the difference with its own CI, or use paired-difference intervals when models share eval examples. Reference point: Anthropic's 2024 "Adding Error Bars to Evals."

We now put honest uncertainty on any comparison of summary numbers. But the single plot every ML engineer stares at most is not a bar chart — it is a training curve, and its default dashboard view is quietly wrong on both axes. Chapter 3.

Model A: 95% CI [0.681, 0.759]. Model B: 95% CI [0.741, 0.819]. The intervals overlap. What can you conclude about significance at the 5% level?

Chapter 3: Training Curves — Smoothing, Log Scales, and Shape Literacy

The single plot every ML engineer stares at most is a loss curve. And the default dashboard view — linear y-axis, heavy smoothing — is quietly wrong on both axes at once. Let us fix both, and then learn to read curve shape like a cardiologist reads an ECG.

Frame it with a code-completion model pretraining run. Loss falls 8.0 → 4.0 in the first 2% of steps, then 4.0 → 2.0 over the entire rest of the run. On a linear y-axis, that second phase — where the model's actual competence is being won — is a nearly flat line hugging the bottom. The plot says "nothing is happening" while perplexity is halving.

The fix is a log-scale y-axis. On a log axis, equal vertical distances represent equal ratios, so 8→4 (halving) and 4→2 (halving) have identical visual size. Loss improvements are multiplicative, so the axis should be multiplicative too. This is not a trick to inflate progress — it is matching the encoding to the quantity, exactly the Chapter 0 principle.

Log-x is often right as well. Learning dynamics spread over orders of magnitude of steps, and the loss-versus-compute curves of scaling-law papers are log-log for exactly this reason (Chapter 5 completes that story). But first, the smoothing problem, which is subtler and more dangerous.

Raw per-step loss is noisy because each step sees a different random batch. Dashboards tame this with an exponential moving average (EMA): smoothed = β × previous + (1 − β) × current. The knob β sets an effective averaging window of about 1 / (1 − β) steps. So β = 0.9 is a 10-step memory, β = 0.99 a 100-step memory, and β = 0.999 — a common dashboard default! — a 1000-step memory.

Here is the catch: smoothing is a low-pass filter, and every filter lags. We will work this exactly below: a loss sequence dropping 4.0 → 2.0 over five readings, smoothed with β = 0.9, reads 2.64 at the moment the true loss is 2.00. The display is 0.64 behind reality. The consequences are real: a loss spike appears later and smaller than it actually was; a divergence looks like a "slight uptick" for hundreds of steps; and comparing two runs at "the same step" compares different effective times if their smoothing differs.

Bias correction. An EMA started at zero is dragged toward zero early in training. Dividing by (1 − βt) fixes it. Without correction, the first readings of every run look artificially good (lower loss than reality) — so two runs compared in their first hundred steps can swap places purely from an initialization artifact. TensorBoard's smoothing slider applies a debiased EMA; W&B offers several — know which your dashboard uses before comparing screenshots across tools.

Now the shape gallery — read curves like an ECG. (1) Healthy: a fast initial drop, then a smooth power-law-ish decay. (2) LR too high: large oscillations, or the loss plateauing high with sawtooth noise. (3) LR too low: painfully slow, nearly linear descent. (4) Divergence: exponential blow-up, often right after a spike — check gradient norms. (5) Overfitting: training loss keeps falling while validation loss bottoms out and rises — the gap is the signal, which is why you must always plot train and val on the same axes. (6) Data bug or repeated data: sudden cliff drops or periodic scalloping aligned with epoch boundaries.

Let us make the log-y point concrete with numbers. On a linear axis spanning 0 to 8, the drop from 4.0 to 2.0 is a vertical move of 2 units out of 8 — a quarter of the frame — while the earlier drop 8.0 to 4.0 is also 4 units, so the early phase visually dominates and the late phase looks like a flat tail. On a log axis, 8→4 spans log₂(8/4) = 1 unit and 4→2 spans log₂(4/2) = 1 unit: identical. The late competence-building phase gets equal visual weight to the early collapse, because it represents an equal fractional improvement.

Comparison discipline for curves: align runs on a meaningful x-axis (tokens or FLOPs, not wall-clock — wall-clock differences are hardware, not learning), use the same smoothing for all runs, and never crop the y-range to make one run's advantage fill the frame without annotating the range (the Chapter 0 rule: lines may zoom, but must say so).

One more consequence of lag worth internalizing: it is asymmetric in how it deceives. During improvement, the smoothed curve reads worse than reality (as in our worked example, 2.64 vs 2.00). During a spike, the smoothed curve reads better than reality — a loss that momentarily jumps to 6.0 appears on a β = 0.99 dashboard as a gentle bump to maybe 4.1, arriving dozens of steps late. That is the dangerous direction: the one place you most need to see a problem immediately (a divergence) is exactly where heavy smoothing hides it longest.

The eval connection closes the chapter. Intermediate checkpoint evals are points on these curves; a benchmark score plotted every N steps has its own sampling noise (Chapter 2's error bars), so a "dip at checkpoint 40k" that sits inside the CI band is not a regression. Curve literacy plus uncertainty literacy prevents the classic incident: "we reverted a fine run because one noisy checkpoint eval dipped."

Misconception: "Heavier smoothing just makes the curve cleaner, with no downside." An EMA is a low-pass filter with memory 1/(1−β) steps: at β = 0.999 your dashboard shows a 1000-step-delayed, amplitude-shrunken version of reality. A divergence spike appears late and small; two runs smoothed differently are not comparable at the same step. Smoothing trades noise for lag — always know the window you bought.

The viewer below plots a synthetic 2000-step run with raw loss (faint), the EMA (bold), and optional validation. Inject a pathology, drag the β slider, and watch the lag readout — the gap between what the dashboard shows and the true underlying loss. Toggle log-y and bias correction; turn correction off and watch the first 50 steps sag artificially.

The Curve Shape Gallery

Faint = raw loss, bold = EMA smoothed, optional validation. Pick a pathology; drag β and watch the smoothed curve lag the truth. The lag readout shows the current gap; the marker flags where the smoothed curve first reveals a spike vs where it truly happened.

smoothing β 0.900

Worked example: the lag of an EMA, step by step

Five consecutive loss readings: 4.0, 3.0, 2.5, 2.2, 2.0. Smooth with β = 0.9, initialized at 0, with bias correction. The update is et = 0.9 et−1 + 0.1 Lt, and the corrected value is et / (1 − 0.9t).

t=1: e = 0.9×0 + 0.1×4.0 = 0.4;   corrected = 0.4 / (1−0.9) = 0.4/0.1 = 4.0000
t=2: e = 0.9×0.4 + 0.1×3.0 = 0.66;   corrected = 0.66 / (1−0.81) = 0.66/0.19 = 3.4737
t=3: e = 0.9×0.66 + 0.1×2.5 = 0.844;   corrected = 0.844 / 0.271 = 3.1144
t=4: e = 0.9×0.844 + 0.1×2.2 = 0.9796;   corrected = 0.9796 / 0.3439 = 2.8485
t=5: e = 0.9×0.9796 + 0.1×2.0 = 1.08164;   corrected = 1.08164 / 0.40951 = 2.6413

The dashboard displays 2.64 while the true current loss is 2.00 — a lag of 0.64. That is roughly the 1/(1−β) = 10-reading memory showing through: during rapid improvement the smoothed value is systematically above the truth, because it still remembers the higher losses from up to ten readings ago.

python — from scratch
def ema(xs, beta):
    out, e = [], 0.0
    for t, x in enumerate(xs, 1):
        e = beta*e + (1-beta)*x
        out.append(e / (1 - beta**t))    # bias-corrected
    return out

print([round(v,4) for v in ema([4.0,3.0,2.5,2.2,2.0], 0.9)])
# [4.0, 3.4737, 3.1144, 2.8485, 2.6413]   -> reads 2.64 while truth is 2.00
print(1/(1-0.9))   # 10.0  effective window
python — library equivalent (pandas)
import pandas as pd
smoothed = pd.Series(loss).ewm(alpha=1-beta, adjust=True).mean()
# adjust=True IS the bias correction; plot raw at alpha=0.25, smoothed bold, ax.set_yscale('log')
Interview lens. Two candidate pretraining runs are compared via TensorBoard screenshots and run B "looks better." What must you check about the plots before believing it? Check (1) the smoothing slider — it is per-viewer state, so the screenshots may compare β = 0.6 against β = 0.99, and heavier smoothing both lags and flatters late-run noise; (2) the x-axis unit — steps vs tokens vs wall-clock; runs with different batch sizes at the same step have consumed different data; (3) the y-scale — linear y hides late multiplicative gains, and a cropped y-range without annotation manufactures separation; (4) whether validation, not just train, is shown, since B could be winning train loss by overfitting; (5) seed count — a single run each means the gap may be within run-to-run variance. The staff move is to re-pull the raw scalars and replot both runs with identical smoothing, log-y, and a tokens x-axis.

We can now read one model's learning honestly. But comparing two classifiers means decompressing an entire trade-off surface — and each of the four workhorse comparison plots is routinely read wrong. Chapter 4.

A dashboard shows smoothed loss 2.64 while the latest raw reading is 2.00 (β = 0.9). What explains the gap?

Chapter 4: Comparison Plots — ROC, PR, Calibration, and Residuals

One accuracy number compresses an entire trade-off surface into a single dot. Four plots decompress it — ROC, precision-recall, reliability diagrams, and residual plots — but each one answers exactly ONE question and each one is routinely read wrong.

A scope note first. The definitions of these metrics — what AUC, ECE, and precision mean and why — are metric-design's territory (it derives the AUC-as-ranking-probability identity and hand-computes ECE). This chapter owns reading and plotting them honestly: axes, operating-point dots, curve crossings, error bands, and picking the plot that answers the decision at hand. Running example: a camera-trap classifier flagging a rare species. A score threshold sweeps from strict to lenient; every threshold is a different operating point.

The ROC curve plots the true-positive rate (TPR) against the false-positive rate (FPR) across all thresholds. It shows the model's ranking quality and is immune to class prevalence, because both axes are rates within their own class. Read it correctly: the diagonal is coin-flipping; the closer the curve hugs the top-left corner the better; and AUC is the probability that a random positive outranks a random negative (that identity is proven in metric-design; here we just use it to read the curve).

But prevalence-immunity is a double-edged sword. With 3 rare positives per 97 negatives, an innocuous-looking FPR of 0.14 means 0.14 × 97 ≈ 14 false alarms drowning out 2 true detections. The ROC looks great; the product is unusable. ROC's blindness to rarity is precisely how imbalanced-data papers oversell.

The PR curve plots precision against recall and puts prevalence back in, because precision = TP / (TP + FP) directly feels every false alarm. In the worked example below, the same operating point (TPR 0.667, FPR 0.143) has precision 0.667 on a balanced set but only 0.126 when negatives outnumber positives 97:3. The rule: rare-positive problems — fraud, rare species, safety violations — get PR curves. ROC alone hides the collapse.

Neither curve is a single number, and models can cross: A better at low FPR, B better at high recall. So plot the curves, mark your operating point — the threshold you actually deploy — with a dot, and compare at that dot. AUC differences of 0.005 are usually within resampling noise anyway (error bands via bootstrap, machinery in eval-statistics).

Calibration / reliability diagrams answer a different question: when the model says it is 80% confident, is it right 80% of the time? Bin predictions by confidence, then plot mean confidence vs empirical accuracy per bin; the identity diagonal is perfect calibration. Points below the diagonal mean overconfidence — the modern deep-net default. Summarize the gap with ECE (expected calibration error), the prediction-weighted average gap between confidence and accuracy. Metric-design owns the ECE definition and its Brier-score cousin; here we read the diagram and note its fragility — ECE inherits the histogram's bin-count sensitivity from Chapter 1.

Why calibration plots matter for eval work: LLM-as-judge scores, uncertainty-aware routing, abstention thresholds, and any downstream system consuming probabilities all silently assume calibration. A model that "improves accuracy" while becoming wildly overconfident can make the system worse — and only the reliability diagram reveals this.

For regressors and quality scores, the workhorse is the predicted-vs-true scatter with the identity line y = x. Points on the line are perfect; the vertical gap to the line IS the error; systematic curvature away from it is bias; and points hugging a horizontal band mean the model predicts the mean and ignores its input. Never fit-and-plot a regression line instead of the identity line — the regression line always looks flattering because it is optimized to.

Residual plots (residual = predicted minus true, plotted against the prediction or an input) reveal error structure. A shapeless cloud around zero is health. A fan opening rightward is heteroscedasticity — errors growing with magnitude, like a depth estimator whose error grows with distance. A curve means a missing nonlinearity. Stripes or clusters mean a data slice with distinct behavior — which is Chapter 6's cue to go slice-level.

Read residual structure like a diagnosis, not a verdict. Suppose a depth estimator's residuals form a fan that opens rightward: near residuals cluster within ±0.1 m, but at 20 m predictions the residuals spread ±2 m. That is heteroscedasticity, and it has a concrete engineering consequence — a single RMSE number averages the tight near-field and loose far-field into one misleading figure, so you should report error as a function of distance, not one scalar. The residual plot told you which model of the noise to build; the scalar metric would have hidden it entirely.

The meta-skill: pick the plot that answers the decision. Choosing a deploy threshold → PR curve with your operating point. Trusting probabilities → reliability diagram. Debugging a regressor → residuals. Publishing ranking quality → ROC with error bands. A plot chosen because it looks impressive rather than because it answers the decision is Chapter 0's lie in a fancier costume.

Misconception: "A great ROC curve means the classifier is ready to deploy." ROC axes are within-class rates and cannot see prevalence: at 3 positives per 97 negatives, the same operating point with a healthy-looking FPR of 0.14 delivers precision 0.126 — seven false alarms for every true detection. For rare-positive problems the PR curve, which feels every false positive, is the honest view; ROC-only reporting is how imbalanced-data results get oversold.

The simulation links three panels driven by one synthetic classifier. Drag the threshold and watch the operating point move on both curves. The prevalence slider is the lesson: as positives become rare, the ROC curve does not move while the PR curve collapses. Flip to the calibration view and use the temperature slider to make the model over- or under-confident, watching ECE respond.

ROC vs PR vs Calibration — One Classifier, Three Views

Left: two score distributions with a draggable threshold. Middle: ROC with the moving operating point. Right: PR with the same point. Slide prevalence down and watch ROC hold still while PR collapses. Toggle the calibration view; the temperature slider bends confidence and the live ECE responds.

threshold 0.50
prevalence 0.50

Worked example: the prevalence collapse and a 2-bin ECE

At one threshold the camera-trap classifier gives TP = 2, FN = 1, FP = 1, TN = 6 (3 positives, 7 negatives). Compute the rates:

TPR = TP/(TP+FN) = 2/3 = 0.6667
FPR = FP/(FP+TN) = 1/7 = 0.1429
precision = TP/(TP+FP) = 2/2... = 2/(2+1) = 0.6667

Now keep the same per-class rates but change the population to 3 positives and 97 negatives. Expected TP = TPR × 3 = 2 (unchanged). Expected FP = FPR × 97 = 97/7 = 13.857. So:

precision = 2 / (2 + 13.857) = 2 / 15.857 = 0.1261

Same ROC point (0.1429, 0.6667); precision fell from 0.667 to 0.126 — an 81% relative collapse, purely from prevalence. The ROC could not show it; the PR curve does.

ECE from two bins. Bin 1 has 10 predictions, average confidence 0.90, accuracy 0.70. Bin 2 has 10 predictions, average confidence 0.60, accuracy 0.65. The per-bin gaps are |0.90 − 0.70| = 0.20 and |0.60 − 0.65| = 0.05. The prediction-weighted average:

ECE = (10 × 0.20 + 10 × 0.05) / 20 = (2.0 + 0.5) / 20 = 2.5 / 20 = 0.125

The dominant bin-1 gap is above-the-diagonal (confidence 0.90 > accuracy 0.70), so the model is overconfident overall.

python — from scratch
TP, FN, FP, TN = 2, 1, 1, 6
tpr, fpr = TP/(TP+FN), FP/(FP+TN)
fp_imb = fpr * 97                     # expected FP under 3:97
print(round(tpr,4), round(fpr,4),
      round(TP/(TP+FP),4), round(2/(2+fp_imb),4))
# 0.6667 0.1429 0.6667 0.1261

# ECE from two bins: prediction-weighted |conf - acc|
ece = (10*abs(0.9-0.7) + 10*abs(0.6-0.65)) / 20
print(ece)                            # 0.125
python — library equivalent (sklearn)
from sklearn.metrics import RocCurveDisplay, PrecisionRecallDisplay
RocCurveDisplay.from_predictions(y, scores)
PrecisionRecallDisplay.from_predictions(y, scores)
from sklearn.calibration import calibration_curve   # reliability diagram bins
Interview lens. You're evaluating a safety classifier that flags policy-violating LLM outputs, where violations are about 1% of traffic, and the vendor's report shows ROC AUC 0.98. What plots do you ask for and why? Ask for (1) the PR curve — at 1% prevalence an excellent AUC can still mean precision below 0.3 at useful recall, because FP count scales with the 99% negative class; (2) the actual deployment operating point marked on the PR curve, with a bootstrap error band — AUC is an average over thresholds you will never use; (3) a reliability diagram if the scores feed a downstream throttling policy, since overconfident scores break any probability-consuming logic; (4) per-slice PR because safety failures concentrate in subpopulations. The senior signal is translating curves into product numbers: "at recall 0.9 this precision means N false flags per million requests, costing X reviewer-hours" — the connection metrics-ladder formalizes.

We can now decompress any classifier comparison. But the plots that drive the biggest decisions — which component to keep, how big to build — are ablations and scaling sweeps, and they attract the most sophisticated lies of all. Chapter 5.

A classifier's operating point has TPR 0.667 and FPR 0.143. Moving from a balanced eval set to one with 3 positives per 97 negatives, what happens to the ROC point and the precision?

Chapter 5: Ablation and Scaling Plots — Log-Log Honesty

Ablations and scaling sweeps drive the biggest decisions in ML — which component to keep, how large to build the next model — and precisely because the stakes are highest, they attract the most sophisticated lies. This chapter arms you against them, and teaches you to fit a power law by hand.

Ablation tables become ablation plots when you need to see interaction and uncertainty at a glance: a grouped bar chart where groups are benchmarks or slices and the bars within a group are variants (full model, minus component X, minus Y), each bar wearing a 95% CI whisker from Chapter 2. Zero baseline (they are bars!), consistent variant colors across all groups, and the full-model bar always in the same position so the eye can sweep.

The ablation-plot lie catalog: sorting groups by the favored variant's advantage (cherry-ordering); dropping the benchmarks where the ablation actually helped (survivorship); whiskerless bars at n = 1 seed; and the Chapter 0 truncated baseline. An honest ablation plot shows ALL evaluated benchmarks, ordered alphabetically or by task family, with the per-bar n stated.

When one factor is dose-like — data fraction, model width, compute — lines beat bars: x = dose, y = metric, one line per variant, a CI band per line. Now curvature becomes visible. "Component X helps at small scale but washes out at large scale" is a crossing of two lines — and it is invisible in any table.

Now scaling laws. Loss versus scale follows an approximate power law, L = a × N−α. On linear axes this is a boring hyperbola. On log-log axes a power law is a perfectly straight line whose slope IS the exponent, because taking logs of both sides gives log L = log a − α log N. That is why every scaling-law figure since Kaplan 2020 is log-log: straightness is the hypothesis test your eye performs.

Work the fit by hand. Three training runs of the code-completion model at N = 1e6, 1e7, 1e8 parameters give losses 4.00, 3.17, 2.52. In log₁₀ space the x-values are (6, 7, 8) and the y-values are (0.6021, 0.5011, 0.4014). The least-squares slope comes out to −0.1003, so α = 0.100: every 10x in parameters cuts loss by the factor 100.1003 = 1.26, i.e. about 21% off. The intercept 1.2038 completes the line. (Full arithmetic in the worked example.)

Extrapolation is where scaling plots turn into fiction. Our fitted line predicts L(1e9) = 2.00 — one full order of magnitude beyond the data. The honest rendering shades the region beyond the last data point, switches the line to dashed, and widens the uncertainty band with distance. Three points spanning two decades cannot certify decade three: power laws bend when data, resolution, or task ceilings bite. The field's own history proves it — Chinchilla revised Kaplan's compute-optimal recipe in 2022, then Epoch AI's 2024 replication revised Chinchilla's own fitted constants. Both were disputes about fits and their extrapolations.

Log-log reading traps. (1) Equal visual gaps are equal ratios — a hair of daylight between two curves at the right edge can be 5% of loss, which at that scale is enormous. (2) Zero cannot appear on a log axis, so metrics that hit zero (error rates on easy tasks) get quietly clipped, or the plot silently switches to log-linear — check which. (3) Straightening by choice of variable: some papers plot against compute, some against parameters, some against data, and a curve straight in one variable can bend in another. Choosing the straightest x is a subtle form of result-shopping.

The emergence controversy is this chapter's capstone in plot literacy. "Emergent abilities" — flat-then-cliff curves of accuracy vs scale — were shown by Schaeffer et al. (NeurIPS 2023 best paper) to largely be artifacts of the metric and the plot: nonlinear, discontinuous metrics like exact-match manufacture cliffs out of smoothly improving log-likelihood. Same models, different y-variable, opposite narrative. The deepest lesson of the chapter: the choice of what to put on the axes is itself a modeling decision.

Misconception: "A tight straight-line fit on a log-log plot licenses reading the line out to any scale I like." Straightness over the observed decades says nothing about the next decade: power laws bend when data, resolution, or task ceilings bite, and the field's history — Kaplan revised by Chinchilla, Chinchilla's constants revised by a 2024 replication — is a history of extrapolations corrected. Honest scaling plots dash the line and shade the region beyond the last data point.

The fitting bench below plots loss vs parameter count. Toggle log-log and watch the hyperbola straighten. The least-squares line renders with its live slope; beyond the last point it turns dashed inside a shaded zone. Drag a data point to see fit sensitivity, add a 4th off-trend run to watch the extrapolation shift, and drag the target-scale marker out to 1e10 — the caveat badge grows with distance.

The Log-Log Fitting Bench

Loss vs parameter count with three run points. Toggle log-log to straighten the power law. The fitted line shows its slope (−α) live; past the last point it goes dashed inside a shaded extrapolation zone. Add a 4th off-trend run to see the prediction shift; drag the target marker to feel extrapolation risk grow.

target scale (log₁₀N) 9.0

Worked example: fitting a power law in log space

Three runs: N = 1e6 → loss 4.00; N = 1e7 → 3.17; N = 1e8 → 2.52. Fit L = a × N−α by least squares in log₁₀ space, then extrapolate to 1e9.

Transform. x = log₁₀N = (6, 7, 8); y = log₁₀L = (0.60206, 0.50106, 0.40140). The means:

x̄ = 7;   ȳ = (0.60206 + 0.50106 + 0.40140) / 3 = 1.50452 / 3 = 0.50151

Deviations. dx = (−1, 0, 1); dy = (0.10055, −0.00045, −0.10011). The least-squares slope is Σ(dx dy) / Σ(dx²):

slope = (−0.10055 + 0 − 0.10011) / (1 + 0 + 1) = −0.20066 / 2 = −0.10033

So α = 0.1003. The intercept is ȳ − slope × x̄:

intercept = 0.50151 + 0.10033 × 7 = 0.50151 + 0.70231 = 1.20382

Extrapolate to x = 9:

y = 1.20382 − 0.10033 × 9 = 1.20382 − 0.90297 = 0.30085
L = 100.30085 = 1.999 ≈ 2.00

Interpretation: each 10x of parameters multiplies loss by 10−0.10033 = 0.794 — a 21% reduction per decade. And the 1e9 prediction sits one full decade beyond the data, so it must be drawn dashed inside a shaded zone. It is a hypothesis, not a measurement.

python — from scratch
import numpy as np
x = np.log10([1e6, 1e7, 1e8])          # [6, 7, 8]
y = np.log10([4.00, 3.17, 2.52])

dx, dy = x - x.mean(), y - y.mean()
slope = (dx*dy).sum() / (dx**2).sum()   # -0.10033
intercept = y.mean() - slope * x.mean()   # 1.20382
L9 = 10**(intercept + slope*9)               # ~2.00
print(round(slope,5), round(intercept,5), round(L9,3), round(10**slope,4))
# -0.10033 1.20381 1.999 0.7937
python — library equivalent
slope, intercept = np.polyfit(np.log10(N), np.log10(L), 1)
ax.set_xscale('log'); ax.set_yscale('log')
ax.axvspan(N[-1], N_target, alpha=0.1)   # the honesty shade
Interview lens. Leadership wants to greenlight a 10x-larger training run based on your three-point scaling plot. How do you present it so the decision is made with honest uncertainty? Present the log-log fit with (1) the fitted slope and its sensitivity — refit dropping each point and show the spread of predictions at the target scale (with three points, leave-one-out spread is the cheapest honesty available); (2) the extrapolation region explicitly shaded and the prediction stated as a range, not a point ("roughly 1.9 to 2.1 if the trend holds"); (3) the assumptions that could bend the line at 1e9 — data-pool exhaustion, tokenizer or context ceilings, optimizer changes — as annotations, not fine print; (4) the historical base rate, citing that Chinchilla revised Kaplan and Epoch AI's 2024 replication revised Chinchilla. The staff move is to convert residual uncertainty into a cheap de-risking step: propose a 3e8 intermediate run to add a fourth point before committing the full budget.

We can now keep the highest-stakes plots honest. But there is a subtler failure that no amount of axis discipline catches: an aggregate metric that rises while a whole subgroup collapses beneath it. Chapter 6.

On a log-log plot, three runs at 1e6/1e7/1e8 parameters fall on a straight line of slope −0.10. What does the slope mean?

Chapter 6: Slice Heatmaps and Metrics Over Time

Your overall accuracy went up with the new model. You shipped. Two weeks later, support tickets from one user segment tripled. The aggregate plot never had a chance of warning you — and this chapter's plots are the ones that would have.

Start with the arithmetic of hiding. Overall accuracy is a size-weighted average of slice accuracies. Camera-trap example: daytime images (n = 900, accuracy 0.92) and nighttime images (n = 100, accuracy 0.50) give overall (900×0.92 + 100×0.50)/1000 = 0.878. A new model scores daytime 0.95 but nighttime 0.40: overall (855 + 40)/1000 = 0.895. The headline metric improved by 1.7 points while nighttime performance collapsed by 10. Nothing about the aggregate is wrong; it is answering a different question than "is anyone worse off?"

The slice heatmap is the antidote: rows are slices (device, language, image condition, input length, user segment), columns are models or versions, cell color encodes the metric, and cell text prints the value AND the n. Color encodes where to look; the printed n encodes whether to believe it. A 0.40 cell with n = 100 carries a CI of roughly ±0.10 and deserves attention; a 0.63 cell with n = 8 deserves a bigger sample before anyone panics.

Heatmap craft rules. (1) Use diverging colormaps only when there is a meaningful midpoint (like delta-vs-baseline centered at 0); use sequential colormaps for raw levels — a red-to-green map of raw accuracy invents a "neutral" point nobody chose. (2) Normalize per row when slices have wildly different baseline difficulty, else one hard slice's dark row hides all within-row structure. (3) Sort rows by delta or by n; never leave dictionary order if the plot's job is triage. (4) Print values in cells whenever the grid is smaller than about 15×10 — color perception is not precise enough for decisions.

Better than two heatmaps (old model, new model) side by side is ONE delta heatmap: each cell is new minus old, a diverging colormap centered at zero, and cells are hatched out when the change is within the slice's noise band. This single plot is the regression-review workhorse — regression-testing-ml builds its gate logic on exactly this view; here we own making it readable.

Metrics over time. The deployed model's metric plotted per day or week is a time series, and honest ones carry two garnishes. (1) An uncertainty band — with about 1000 eval examples per week at p = 0.878, the weekly standard error is about 0.0104, so a ±2 SE band of ±0.021 tells the on-call reader which wiggles are noise. (2) Event annotations — vertical lines for every deploy, data-pipeline change, and eval-set refresh. An unannotated step change is a mystery; an annotated one is a diagnosis.

Drift panels. When the metric moves, the first question is "did the model change or did the inputs change?" A drift panel pairs the metric time series with input-distribution summaries over time (feature means, class mix, input-length ECDF snapshots) as small aligned subplots sharing the x-axis. Shared-x alignment is the craft point — the reader's eye does the correlation between an input shift and a metric move.

Sparklines — word-sized metric-over-time strips embedded one per table row — combine both views: a table of slices where each row shows current value, delta, n, and a tiny 8-week trend line. This is the densest honest format for a weekly eval review. It fails only when axes differ silently per row, so fix a shared y-range per metric column and say so.

Read the heatmap's n column as a believability filter, with a number attached. A slice cell showing accuracy 0.40 on n = 100 examples has a standard error of √(0.40 × 0.60 / 100) = √0.0024 = 0.049, so its rough 95% CI is ±0.096 — nearly ±0.10, wide enough that a one-week drop from 0.50 to 0.40 might partly be noise and warrants a bigger sample before a rollback. A cell at n = 8 has SE around 0.17: essentially unreadable. Color tells you where to look; the printed n and its implied CI tell you whether to act.

A boundary note. These are all eval surfaces — is the model good, where, and since when. Once the y-axis becomes revenue, retention, or tickets, you have crossed into product dashboards, and the metrics-ladder lesson owns that layer, including how eval metrics should (and should not) be composed into exec-facing KPIs.

Misconception: "If the overall metric improved, no user group got worse — the average went up, after all." An aggregate is a size-weighted average: a 90%-weight slice gaining 3 points arithmetically buries a 10%-weight slice losing 10. In the worked example the headline rose 0.878 to 0.895 while nighttime accuracy collapsed 0.50 to 0.40. Aggregates answer "how are we doing on average," never "is anyone worse off" — only the slice view answers that.

The split view below shows the aggregate accuracy as one big number and trend line on top, and a slice heatmap (daytime / nighttime / rain / snow × weekly versions) below. Ship a new version and occasionally watch the aggregate tick up and glow green while a minority-slice cell flashes red. The delta toggle switches the heatmap to new-minus-old with noise cells hatched; the drift toggle adds an input-mix panel sharing the x-axis.

The Slice-Collapse Detector

Top: the aggregate accuracy trend. Bottom: a slice heatmap over weekly versions. Ship next version steps a week — sometimes the aggregate rises while a minority slice tanks. The share slider controls minority weight (how much the aggregate can hide). Delta view shows new−old with noise-hatched cells; drift panel adds the input mix.

minority slice share 0.10

Worked example: the average rises, the slice collapses

Old model: daytime slice n = 900 at 0.92, nighttime n = 100 at 0.50. New model: daytime 0.95, nighttime 0.40.

old overall = (900×0.92 + 100×0.50) / 1000 = (828 + 50) / 1000 = 0.878
new overall = (900×0.95 + 100×0.40) / 1000 = (855 + 40) / 1000 = 0.895

The aggregate change is +0.017 (up 1.7 points). The nighttime change is 0.40 − 0.50 = −0.10 (down 10 points, a 20% relative collapse). It is hidden because the slice holds only 10% of the weight: its −0.10 contributes just −0.01 to the aggregate, outweighed by daytime's +0.03 × 0.9 = +0.027.

The weekly band. For an overall accuracy of 0.878 measured on 1000 examples:

SE = √(0.878 × 0.122 / 1000) = √(0.0001071) = 0.01035

A ±2 SE band is ±0.0207, so weekly readings between 0.857 and 0.899 are indistinguishable from a flat 0.878. Any wiggle inside that band should never trigger a discussion — drawing the band is what stops false alarms.

python — from scratch
old = (900*0.92 + 100*0.50) / 1000     # 0.878
new = (900*0.95 + 100*0.40) / 1000     # 0.895
import numpy as np
se = np.sqrt(0.878 * 0.122 / 1000)          # 0.01035
print(old, new, round(se, 5))                 # 0.878 0.895 0.01035
python — library equivalent (seaborn heatmap)
import seaborn as sns
sns.heatmap(delta_df, annot=True, fmt='.2f', center=0, cmap='RdBu_r')
# center=0 is the honesty flag: the colormap diverges around no-change
Interview lens. Design the standard figure set for your team's weekly model-quality review. A defensible set: (1) the headline metric over time with a ±2 SE band sized from the weekly eval n, plus vertical annotations for every deploy and data change — so wiggles inside the band never trigger discussion; (2) a delta slice heatmap (this week vs last, and candidate vs production) with per-cell n printed and noise-level cells hatched, rows sorted by delta; (3) one distribution view (ECDF of per-example scores or latency) because means hide tails; (4) a drift panel sharing the x-axis with the headline series so input-mix changes are visually co-located with metric moves. Trustworthiness comes from the invariants: fixed y-ranges week over week (no silent rescaling), stated n everywhere, and a changelog discipline where every annotation line links to the change. The interviewer is probing whether you design for the on-call reader at 2am, not for the launch slide.

You now hold every lie individually. The showcase lets you stack them — and watch a true conclusion transform, toggle by toggle, into a false one. Chapter 7.

A new model raises overall accuracy from 0.878 to 0.895 on a 1000-example eval (900 daytime, 100 nighttime images). Nighttime accuracy fell from 0.50 to 0.40. How is this arithmetic possible?

Chapter 7: Showcase — The Honest Plot Builder

You now know each lie individually. The showcase lets you stack them — and watch a true conclusion ("no significant difference") transform, toggle by toggle, into a false one ("B is a breakthrough"). Every distortion from Chapters 0 through 6 composes into one machine.

Meet the fixed ground truth, baked into the simulation. Two code-completion models, A and B, each trained with 5 seeds and evaluated on the same benchmark. The true story: mean A = 0.8490, mean B = 0.8502, with per-seed scatter much larger than the 0.12-point gap. Welch's t = 0.45, p = 0.67. Any rendering that leads a reader to "B is clearly better" is, by construction, a lie about this data.

Five levers, each mapped to its chapter. (1) Axis truncation (Ch0) — the baseline slides from 0 up to 0.84. (2) Error bars (Ch2) — off / SD / SE / 95% CI. (3) Seed selection (this chapter's new lever) — "mean of 5 seeds" vs "best seed per model" vs the maximally dishonest "best B seed vs mean A." (4) Smoothing (Ch3), applied to the per-checkpoint curve view. (5) Log scale (Ch5) on the curve view's y-axis, used here to compress B's late-training noise.

Let us quantify lever 3, because it is the one this chapter owns. Cherry-picking B's best seed (0.856) against A's mean (0.849) reports a 0.7-point gap where the true mean gap is 0.12 points — a 5.8x inflation, before any axis games at all. With 5 seeds, the expected maximum sits about one standard deviation above the mean, so single-best-seed reporting builds in roughly a +1 SD bias for free. Papers reporting best-of-N runs without stating N commit this silently.

The live conclusion caption is the pedagogical heart. A rule-based reader looks at the current rendering the way a hurried reviewer would — visual gap versus whisker width versus baseline — and prints what that reviewer would conclude. Honest settings print "A and B are statistically indistinguishable (p = 0.67)." Full-lie settings print "B delivers a dramatic improvement." A running LIE SCORE from 0 to 5 counts how many levers are currently distorting.

The reverse game teaches detection. A "detective mode" button renders the plot with hidden lever settings, and you must name every active lie before revealing. This is the skill transfer that matters: from building honest plots to auditing hostile ones, which is exactly what interviews and code reviews demand.

Connect it to the real world. The 2025 "Leaderboard Illusion" analysis (Singh et al., arXiv April 2025) of Chatbot Arena documented private best-of-N variant testing with selective disclosure of only the best performer — lever 3 operating at industrial scale. Multi-seed reporting norms (mean ± CI over seeds, all seeds shown as dots) exist precisely to make this lever visible.

Codify the exit checklist — the Honest Plot Contract, five clauses: (1) zero-baseline or annotated zoom; (2) uncertainty shown and labeled (statistic + n + unit); (3) all runs / seeds / slices disclosed, with selection rules stated; (4) axes and smoothing declared; (5) the conclusion stated WITH its uncertainty. Every figure in your next eval report should pass all five.

Misconception: "Reporting your best seed is just showing the model at its best — everyone does it." With 5 seeds the expected best sits about one SD above the mean, so best-seed reporting manufactures a systematic +1 SD bias: here it turned a p = 0.67 non-difference into an advertised 0.7-point win, a 5.8x inflation of the true mean gap. If selection happened, the plot must show it — all seeds as dots, with the selection rule in the caption. Selection is the one lie that lives upstream of rendering.

Below is the full builder. Adjust all five levers, switch between bar view and curve view, and watch the conclusion caption rewrite itself and the LIE SCORE climb. Every seed is drawn as a dot beside its bar, so seed-selection visibly grays out excluded dots. Hit "detective mode" to hide the levers and quiz yourself; "reset to honest" returns to a lie score of 0.

The Honest Plot Builder

One fixed dataset (A and B, 5 seeds each, truly p = 0.67). Stack the five levers and read the live conclusion + LIE SCORE. Every seed is a dot — excluded seeds gray out. Detective hides the levers so you must name the active lies; reset returns to honest.

baseline 0.00

Worked example: the honest comparison, and the cherry-pick inflation

Seed scores. Model A: 0.850, 0.847, 0.852, 0.845, 0.851. Model B: 0.849, 0.856, 0.851, 0.842, 0.853.

mean A = 4.245 / 5 = 0.8490;   mean B = 4.251 / 5 = 0.8502;   true gap = 0.0012

SD of A. Deviations from 0.8490: (0.001, −0.002, 0.003, −0.004, 0.002). Squares sum = (1 + 4 + 9 + 16 + 4)×10−6 = 34×10−6. Variance = 34e−6 / 4 = 8.5e−6, so SDA = 0.00292.

SD of B. Deviations from 0.8502: (−0.0012, 0.0058, 0.0008, −0.0082, 0.0028). Squares sum = (1.44 + 33.64 + 0.64 + 67.24 + 7.84)×10−6 = 110.8e−6. Variance = 27.7e−6, so SDB = 0.00526.

Welch's test.

SEdiff = √(8.5e−6/5 + 27.7e−6/5) = √(7.24e−6) = 0.00269
t = 0.0012 / 0.00269 = 0.446  →  p = 0.67

Honest conclusion: indistinguishable. Now the cherry-pick. Best B seed = 0.856; reported gap vs mean A = 0.856 − 0.849 = 0.0070.

inflation = 0.0070 / 0.0012 = 5.83×

The reported 0.7-point gap is 2.6 times B's own seed SD — it advertises the luckiest draw as the typical result. Axis honesty cannot repair this; the lie lives in the data selection, upstream of any rendering.

python — from scratch (verified with scipy)
import numpy as np
from scipy import stats
A = np.array([0.850,0.847,0.852,0.845,0.851])
B = np.array([0.849,0.856,0.851,0.842,0.853])
t, p = stats.ttest_ind(B, A, equal_var=False)
print(A.mean(), round(B.mean(),4),
      round(A.std(ddof=1),5), round(B.std(ddof=1),5),
      round(t,3), round(p,2))
# 0.849 0.8502 0.00292 0.00526 0.446 0.67

gap_true, gap_cherry = B.mean()-A.mean(), B.max()-A.mean()
print(round(gap_true,4), round(gap_cherry,4), round(gap_cherry/gap_true,2))
# 0.0012 0.007 5.83
python — library equivalent (dots + honest interval, two lines)
import seaborn as sns
sns.stripplot(data=long_df, x='model', y='score', color='gray')
sns.pointplot(data=long_df, x='model', y='score', errorbar=('ci',95))
Interview lens. A paper reports its method beating the baseline by 0.9 points, single number per method, no seed information. As a reviewer, what do you ask for, and what result would change your accept/reject? Ask for (1) the number of seeds/runs per method and the selection rule — if results are best-of-N with N undisclosed, the comparison carries an uncontrolled optimization bias (the 2025 Chatbot Arena "Leaderboard Illusion" showed this at leaderboard scale); (2) mean ± CI over seeds with all seeds shown, plus the baseline re-run under the SAME budget of attempts; (3) a paired comparison if both methods share eval examples. The decision rule: if the mean-over-seeds gap with a 95% CI excludes zero under equal tuning budgets, the claim stands; if the 0.9-point gap shrinks toward the seed SD, the paper is reporting luck. A strong answer names the base rate: with seed SDs of about 0.5 points, best-of-5 selection alone fabricates roughly 0.5 points.

The showcase turned every lesson into one interactive audit. Chapter 8 turns it into reusable code — the plots-as-code module that makes honesty a library default rather than an act of memory.

In the Honest Plot Builder, which SINGLE lever setting produces a misleading conclusion even with a zero baseline, error bars on, and no smoothing?

Chapter 8: The Recipe Box — matplotlib and pandas for Every Plot

Knowledge without recipes evaporates at the terminal. This chapter is the cookbook: every plot from Chapters 0 through 7, as code you can paste, in both from-scratch matplotlib and the seaborn one-liner. The organizing idea is one convention that makes plots composable into review dashboards.

The foundational pattern is fig, ax = plt.subplots(). The object-oriented API beats the plt.* state machine for eval work: you get multiple axes, reusable styling functions that take an ax argument, and no global-state surprises when a helper function plots midway through yours. Every recipe below takes ax as its first parameter — that one convention makes plots composable.

Recipe group 1 — distributions (Ch1). The four-line ECDF (sort, arange/n, step plot, grid); the histogram with bins='fd'; and overlay discipline (alpha, zorder, and shared bins via np.histogram_bin_edges on the pooled data so two models' histograms are actually comparable rather than each auto-binned differently).

python — ax-first recipes, group 1
def ecdf(ax, x, **kw):
    xs = np.sort(x)
    ys = np.arange(1, len(x)+1) / len(x)
    ax.step(xs, ys, where='post', **kw)
    ax.grid(True, alpha=0.3)

# comparable histograms: shared edges from the POOLED data
edges = np.histogram_bin_edges(np.concatenate([a, b]), bins='fd')
ax.hist(a, bins=edges, alpha=0.5); ax.hist(b, bins=edges, alpha=0.5)

Recipe group 2 — uncertainty (Ch2). errorbar with capsize for mean+CI dots; fill_between for bands; the difference-plot recipe (a second axis with axhline(0)); and the scatter-of-seeds + pointplot combo so the raw runs are never hidden.

python — ax-first recipes, group 2
def ci_bars(ax, means, ses, labels, z=1.96):
    x = np.arange(len(means))
    ax.errorbar(x, means, yerr=z*np.array(ses), fmt='o', capsize=4)
    ax.set_xticks(x); ax.set_xticklabels(labels)
    return f'mean +/- 95% CI, n stated per point'   # caption baked in

def diff_panel(ax, ma, mb, sea, seb):
    d, sed = mb - ma, np.sqrt(sea**2 + seb**2)
    ax.errorbar([0], [d], yerr=1.96*sed, fmt='s', capsize=5)
    ax.axhline(0, ls='--')   # does the difference clear zero?

Recipe group 3 — curves (Ch3) and trade-offs (Ch4). Raw-plus-smoothed loss with pd.Series.ewm(adjust=True); set_yscale('log'); ROC/PR from-scratch threshold sweeps and their sklearn Display one-liners; the reliability diagram with an identity diagonal via ax.plot([0,1],[0,1]) and equal aspect.

python — ax-first recipes, group 3
def loss_curve(ax, raw, beta=0.9):
    ax.plot(raw, alpha=0.25)                          # raw ghosted
    sm = pd.Series(raw).ewm(alpha=1-beta, adjust=True).mean()
    ax.plot(sm, lw=2); ax.set_yscale('log')

def roc_pr(ax1, ax2, y, s):
    for thr in np.sort(s)[::-1]:
        pred = s >= thr
        tp = ((pred) & (y==1)).sum(); fp = ((pred) & (y==0)).sum()
        # append (fpr, tpr) and (recall, precision), then ax1.plot / ax2.plot

Recipe group 4 — scaling and slices (Ch5, Ch6). loglog with np.polyfit in log space, dashed extrapolation and axvspan shading; imshow + ax.text for heatmap cell annotation, and the seaborn heatmap(annot=True, center=0) equivalent; time series with event axvlines and a fill_between noise band.

python — ax-first recipes, group 4
def loglog_fit(ax, N, L, extrapolate_to):
    x, yv = np.log10(N), np.log10(L)
    slope, b = np.polyfit(x, yv, 1)
    ax.loglog(N, L, 'o')
    xe = np.array([N[-1], extrapolate_to])
    ax.plot(xe, 10**(b + slope*np.log10(xe)), ls='--')   # dashed
    ax.axvspan(N[-1], extrapolate_to, alpha=0.1)         # shade

def delta_heatmap(ax, old, new, n):
    d = new - old; vmax = np.abs(d).max()
    ax.imshow(d, cmap='RdBu_r', vmin=-vmax, vmax=vmax)   # symmetric!
    for i in range(d.shape[0]):
        for j in range(d.shape[1]):
            ax.text(j, i, f'{d[i,j]:.2f}\n n={n[i,j]}', ha='center')

Small multiples — the honest answer to "too many lines on one plot." fig, axes = plt.subplots(3, 4, sharex=True, sharey=True) gives 12 slice panels with comparable axes by construction; loop axes.flat, one slice each, one shared legend. The sharey=True is the honesty feature: per-panel auto-scaling would re-introduce Chapter 0's zoom lie one panel at a time. We work the layout arithmetic below.

Defaults that lie, and their fixes. matplotlib bar charts DO start at zero (good), but plt.plot auto-scales tightly (fine for lines — annotate the range). imshow with the wrong vmin/vmax silently saturates cells, so always set them explicitly for delta maps (symmetric around 0). Legend order defaults to plot order, not importance. And rcParams['figure.dpi'] affects only display, while savefig(dpi=) affects the artifact you ship.

Reproducibility is a plotting concern. A plotting script that takes the eval CSV path and writes the figure is an artifact of record; a notebook cell modified by hand is not. End with the save discipline — fig.savefig('fig_ablation.png', dpi=150, bbox_inches='tight') — plus writing the exact data slice next to the figure so any reviewer can regenerate it. (regression-testing-ml turns this into CI; here we just form the habit.)

Misconception: "Library defaults are safe because thousands of people use them." Defaults optimize for "renders something," not "renders honestly": plt.plot auto-zooms the y-range (fine for lines only if you annotate), imshow auto-scales vmin/vmax per figure (making two heatmaps incomparable and delta maps asymmetric), and per-panel autoscaling in subplots quietly re-introduces the zoom lie panel by panel. Honest plotting means setting limits, bins, and color ranges explicitly — defaults are a starting point, not a policy.

The gallery below renders six live recipes from one shared synthetic dataset. Tap a mini-plot to expand it with the exact recipe alongside and 2–3 knobs that live-update it, so you connect each argument to its visual effect. Shuffle the data and watch all six re-render from the same source.

The Recipe Gallery

Six miniature live plots from one shared dataset: ECDF, whiskered bars, smoothed curve, ROC/PR, log-log fit, delta heatmap. Tap one to expand with its recipe and knobs. Shuffle data re-renders all six from a fresh sample.

knob 12

Worked example: laying out honest small multiples

Lay out a 12-slice figure at 4 columns, 2.2 inches per panel, saved at dpi = 150. Also compute honest shared y-limits for slice accuracies ranging 0.61 to 0.94 with 5% padding.

rows = ⌈12 / 4⌉ = 3
figsize = (4 × 2.2, 3 × 2.2) = (8.8, 6.6) inches
saved pixels = (8.8 × 150, 6.6 × 150) = (1320, 990) px

Shared y-limits: the data range is 0.94 − 0.61 = 0.33; the pad is 0.05 × 0.33 = 0.0165; so the limits are:

(0.61 − 0.0165, 0.94 + 0.0165) = (0.5935, 0.9565)

Because the panels share these limits, a slice whose line sits visibly lower IS lower — no per-panel rescaling can fake parity. Note the deliberate choice: these are line panels, so the 0.59 baseline is legitimate (position encoding), but it gets annotated per the Chapter 0 rule. If the panels were bars, the limits would need to be (0, 1).

python — from scratch
import math
rows = math.ceil(12 / 4)                            # 3
print(rows, 4*2.2, rows*2.2, 4*2.2*150, rows*2.2*150)
# 3 8.8 6.6 1320 990

lo, hi = 0.61, 0.94
pad = 0.05 * (hi - lo)
print(round(lo-pad,4), round(hi+pad,4), round(pad,4))
# 0.5935 0.9565 0.0165
python — the composing figure (library)
fig, axes = plt.subplots(3, 4, sharex=True, sharey=True)
for ax, slice_ in zip(axes.flat, slices):
    timeseries_band(ax, slice_.t, slice_.p, slice_.n, events)
fig.savefig('weekly_review.png', dpi=150, bbox_inches='tight')
Interview lens. Your team's eval figures are generated ad hoc in notebooks and reviewers keep finding inconsistencies (different axis ranges week to week, unlabeled whiskers). Propose an engineering fix. Propose a plots-as-code module: ax-first recipe functions with the honesty defaults BAKED IN — ci_bars always labels the statistic and n in the caption string it returns; delta_heatmap always sets symmetric vmin/vmax and hatches noise-level cells; timeseries always draws the ±2 SE band and takes an events list. The weekly review figure becomes one composing script that reads the eval store and writes versioned PNGs plus the exact query used, so any figure is regenerable from its commit. This converts plot honesty from per-author diligence into reviewed library behavior — the same move as putting eval thresholds into CI. Strong candidates mention testing the plot code itself: golden-image or property tests (bars start at zero, captions contain "n=").

You can now build every honest plot in this lesson from a paste-able recipe. Chapter 9 drills the whole lesson as rapid-fire interview Q&A — the skill of auditing a hostile plot in seconds.

Why is sharey=True the "honesty flag" in a small-multiples grid of slice metrics?

Chapter 9: Interview Arsenal — Plot Literacy Under Fire

Every question in this arsenal has appeared, in some costume, in a real ML system-design or analytics interview. Here is the plot: tell me what is wrong, what is missing, and what you would draw instead. Staff-level interviews test whether you can audit a plot in seconds, quantify its distortion, and design the honest replacement.

How plot questions are actually asked: (1) the audit ("here is a figure from a report — critique it"), (2) the design ("what would you plot to answer X?"), (3) the trap ("the CIs overlap, so no difference, right?"), and (4) the judgment call ("leadership wants one number and one chart — what do you give them?"). Each maps to chapters of this lesson.

The 10-second audit routine to verbalize out loud: axes first (baseline? scale? units?), uncertainty second (whiskers? which statistic? what n?), selection third (which runs/seeds/slices made it in?), encoding fourth (does bar-length / color / position match the data type?), conclusion last (does the caption's claim survive the first four checks?).

Quantification is what separates senior from staff answers: not "the axis looks truncated" but "this truncation yields a lie factor of roughly 100"; not "overlapping CIs doesn't mean no difference" but "the whisker-tip test demands 3.92 SEs when significance needs 2.77 — and just-touching 84% CIs are the calibrated visual test" (worked below).

Work the arsenal below — each answer expands. Then the three debate prompts have no single right answer; they exist because real plot decisions are trade-offs, and interviewers push until you take a defensible position.

The Q&A bank

Q. A report shows two model accuracies as bars from a baseline of 84%, no error bars, n=500 each, values 84.7% and 85.1%. Quantify everything wrong.
Model answer

Two distortions compound. First, encoding: bars encode value as length from zero, and the truncated baseline draws heights of 0.7 and 1.1 units — a 57% visual difference for a 0.47% data difference, a Tufte lie factor of 121. Second, omission: at n=500 per arm, the SE of the difference is √(0.847×0.153/500 + 0.851×0.149/500) = 0.0226, so the 0.4-point gap is z = 0.18, p = 0.86 — indistinguishable from noise. The chart therefore visually amplifies a non-effect by two orders of magnitude while hiding the only cue (whiskers) that would reveal it. The honest render: zero baseline or an annotated zoom with point markers instead of bars, 95% CI whiskers labeled with statistic and n, and ideally the difference plotted with its own CI. As a rule I compute the lie factor out loud — putting a number on the distortion changes the conversation from taste to arithmetic.

Follow-up: At what n WOULD a 0.4-point gap be statistically detectable, roughly?

Q. When do you reach for an ECDF over a histogram, and what is the one thing a histogram can do that an ECDF can't?
Model answer

I default to the ECDF whenever the decision involves percentiles, tails, thresholds, or comparing two systems: it has zero tuning parameters (no bin width, no bandwidth), shows every data point, reads off p50/p95/p99 and SLO fractions directly, and overlays cleanly as step curves where histograms turn to alpha-blended mush. Bimodality appears as a flat plateau — un-hideable. The histogram's genuine advantage is that density SHAPE is more immediately legible to most audiences: modes look like humps rather than slope changes, and non-technical readers parse it faster. So: ECDF for analysis and engineering reviews, histogram (with bins='fd' and a stability wiggle of the width) for communication — and if the two tell different stories, believe the ECDF because only one of them has a knob.

Follow-up: How would you show 20 slices' latency distributions at once without 20 overlaid curves?

Q. Your junior engineer says "the CIs overlap, so the difference isn't significant." Correct them precisely.
Model answer

Overlap of individual 95% CIs is a conservative, miscalibrated test. Each CI extends 1.96 SE, so non-overlap demands the means differ by 3.92 SE — but the significance test of the difference uses SE_diff = √2 × SE (variances add), needing only 1.96 × 1.414 = 2.77 SE. Intervals can overlap by roughly a third of their length while p < 0.05: in this lesson's example, CIs [0.681, 0.759] and [0.741, 0.819] overlap by 0.018 yet z = 2.12, p = 0.034. Two fixes: plot the difference with its own CI and check it excludes zero, or use 84% intervals, whose just-touching condition implies z = 1.99, p = 0.047 — an actually calibrated visual test. And if the two models were scored on the same examples, none of this applies — pair the data first, which usually tightens everything.

Follow-up: Why does pairing on shared eval examples shrink the interval, intuitively?

Q. A W&B screenshot with smoothing 0.99 shows run B's loss below run A's. What are all the ways this comparison can be an artifact of the plot?
Model answer

Five checks. (1) Smoothing: beta 0.99 is a ~100-step low-pass filter; it lags fast changes and its bias handling differs across tools, so early-run comparisons and spike timing are unreliable — and if the two runs' panels used different slider values, the comparison is void. (2) x-axis: at different batch sizes, equal "steps" mean different tokens consumed; align on tokens or FLOPs. (3) y-scale and cropping: linear y hides late multiplicative differences; a tight y-range manufactures separation (annotate any zoom). (4) Train vs validation: B may simply be overfitting; demand both curves. (5) Sampling: one seed each means the visible gap may be within run-to-run variance — the Ch7 seed analysis showed 5-seed SDs comparable to typical "wins." The fix is mechanical: pull raw scalars, replot both runs with identical debiased EMA, log-y, token x-axis, and if the decision matters, more seeds.

Follow-up: Your dashboard must default to SOME smoothing. What default and why?

Q. Why can a model with ROC AUC 0.98 still be useless as a rare-event detector, and what plot exposes it?
Model answer

ROC axes — TPR and FPR — are rates within their own classes, so the curve is mathematically invariant to prevalence. The product experience is not: at 3 positives per 97 negatives, an operating point with TPR 0.667 and FPR 0.143 yields expected FP = 0.143 × 97 = 13.9 against 2 TP — precision 0.126, or seven false alarms per catch. The PR curve exposes this because precision mixes the classes and feels every false positive; under heavy imbalance the PR curve collapses while ROC doesn't move. So for fraud, safety violations, and rare species, I require the PR curve with the deployment operating point marked and a bootstrap band, plus the translation into ops load (false flags per million requests). AUC 0.98 answers "does it rank well" — a real but different question from "can we act on its flags."

Follow-up: When is ROC genuinely the right primary plot?

Q. How do you present a three-point scaling-law fit to justify (or kill) a 10x training run?
Model answer

Fit in log-log space where the power law is a straight line: with runs at 1e6/1e7/1e8 parameters and losses 4.00/3.17/2.52, the least-squares slope is −0.100, i.e. 21% loss reduction per decade, predicting 2.00 at 1e9. Then I present uncertainty honestly: the prediction sits one full decade beyond the data, so the line beyond 1e8 is dashed inside a shaded extrapolation zone; leave-one-out refits give a cheap prediction spread; and the assumptions that could bend the line (data exhaustion, context ceilings, optimizer regime changes) are written on the figure, not in a footnote. I cite the base rate — Chinchilla revised Kaplan, and the 2024 Epoch replication revised Chinchilla's own constants — and convert residual risk into an action: a 3e8 intermediate run adds a fourth point for a fraction of the cost. The plot's job is to make the bet's shape visible, not to make the decision look pre-made.

Follow-up: The 4th point lands above the fitted line. What are the competing explanations and how do you distinguish them?

Q. Design the one figure you'd put on-call engineers in front of for model-quality monitoring.
Model answer

A three-panel shared-x figure. Top: headline metric per day with a ±2 SE band computed from that day's eval n (at p = 0.878 on n=1000, SE = 0.0104, so the band is ±0.021 — wiggles inside it never page anyone), with vertical annotation lines for every deploy, data change, and eval-set refresh. Middle: a delta slice heatmap (today vs trailing week) with per-cell n printed and noise-level cells hatched, rows sorted by delta so the eye lands on the worst slice first. Bottom: an input-drift strip — class mix and input-length quantiles over time — because the first diagnostic question is always "did the model change or did the traffic change," and shared-x alignment lets the eye do that correlation. Fixed y-ranges day over day so the figure itself can't re-zoom into a false alarm. The design principle: every visual alarm the figure can raise must map to a defined action.

Follow-up: How do you set the hatching threshold for "noise-level" heatmap cells in a principled way?

Q. A paper reports single-number results, and you suspect best-of-N seed selection. What's the statistical signature, and how big is the bias?
Model answer

The signature: unstated run counts, no variance reporting, and improvements clustering just above typical seed-level SDs for that benchmark family. The bias is quantifiable: the expected maximum of N draws sits above the mean by roughly SD × expected-max-of-N-standard-normals — about 1.16 SD for N=5 — so best-of-5 reporting fabricates roughly one SD of "improvement" for free. In this lesson's worked data, best-B-vs-mean-A turned a p = 0.67 nothing into a 0.7-point headline, a 5.8x inflation. This isn't hypothetical: the 2025 Leaderboard Illusion analysis documented private best-of-N variant testing with selective disclosure on Chatbot Arena. As a reviewer I ask for all runs as dots with mean ± CI, the selection rule in the caption, and the baseline given the same number of attempts — symmetric budgets or the comparison is void.

Follow-up: The authors respond "we report best because deployment uses the best checkpoint." When is that defensible?

Q. Your VP wants "one chart" for the quarterly review of model quality. What do you send, and what do you refuse to compress?
Model answer

I send a small-multiples panel — one mini-axis per top-level capability or slice family, sharey within each metric, each showing the quarter's trend with a CI band and deploy annotations — because small multiples compress breadth without compressing honesty: shared scales prevent per-panel zoom lies, and bands keep noise from becoming narrative. I refuse to compress three things into the single chart: uncertainty (bands stay), slice structure (at least the worst-performing slice gets its own panel; aggregates provably hide collapses — 0.878 to 0.895 overall while a slice fell 10 points), and selection provenance (a one-line footnote: which evals, which n, what changed this quarter). If the VP wants a single NUMBER, that's a different artifact with its own design discipline — and that conversation belongs to the metrics-ladder layer, where eval metrics get composed into exec KPIs deliberately rather than by Photoshop.

Follow-up: The VP pushes back: "the board slide template allows exactly one line going up." Now what?

Q. What plotting decisions did the "emergent abilities" debate turn on, and what's the general lesson?
Model answer

The famous flat-then-cliff curves plotted DISCONTINUOUS metrics (exact match, multiple-choice accuracy) against scale. Schaeffer et al. (NeurIPS 2023 best paper) showed that replotting the same models with smooth metrics — token-level log-likelihood, Brier-style scores — turned most cliffs into steady lines: the "emergence" lived in the metric's nonlinearity, not (in most cases) in the model. Both plots are "honest" renders of true numbers; the choice of y-variable smuggled in the narrative. The general lesson is the deepest one in plot literacy: axis selection is a modeling decision, and a complete audit asks not just "is this rendering faithful to the data" but "is this VARIABLE the right lens for the claim." That's also the boundary line where this lesson hands off to metric-design — rendering honesty is necessary; metric honesty is a separate discipline.

Follow-up: Give another example where switching the plotted variable would flip a common ML narrative.

Rapid-fire critique deck

Practice the 10-second audit against randomly generated flawed plots. The deck deals a plot with hidden flaws and a countdown; tap the flaws you spot, then reveal the honest re-render side by side. Run it ten times and narrate your audit aloud each time within 30 seconds.

The Critique Deck

Each deal draws a flawed eval plot from eight templates. Tap the flaw(s) you spot; reveal scores your audit and shows the honest re-render. A running accuracy tracks your session.

Worked example: the 84% CI trick, derived

Why do just-touching 84% confidence intervals correspond to roughly p = 0.05 for two independent groups with similar SEs? An 84% CI leaves 16% outside, 8% in each tail, so its z multiplier is the 92nd-percentile z:

z84 = Φ−1(0.92) = 1.405

Two 84% intervals just touching means the means are separated by exactly 2 × 1.405 × SE = 2.810 SE (each interval contributes 1.405 SE toward the other). The significance test of the difference uses SEdiff = √2 × SE = 1.414 × SE, so the implied test statistic is:

z = 2.810 / 1.414 = 1.987  →  p = 0.047

Almost exactly the 1.96 threshold. Compare the naive 95%-whisker-tip test: separation 2 × 1.96 = 3.92 SE implies z = 3.92 / 1.414 = 2.77, p = 0.006 — demanding significance at the 0.6% level while claiming to test at 5%. The 84% rule is the visual test that actually calibrates to p = 0.05.

python — verified with scipy
import numpy as np
from scipy import stats
z84 = stats.norm.ppf(0.92)               # 1.405
z_impl = 2*z84 / np.sqrt(2)             # 1.987
print(round(z84,3), round(z_impl,3),
      round(2*(1-stats.norm.cdf(z_impl)),3))   # 1.405 1.987 0.047

z_naive = 2*1.96 / np.sqrt(2)          # 2.772
print(round(z_naive,3), round(2*(1-stats.norm.cdf(z_naive)),4))  # 2.772 0.0056

Debate prompts (no single right answer)

Debate 1. Resolved: every bar chart must start at zero, no exceptions — even if it makes small-but-real differences invisible.

PRO: bars encode length; a truncated bar is a broken encoding with a computable lie factor, and "important small differences" should switch to point-plus-CI markers, not bend the bar rule. CON: dogma ignores context — for metrics living in a narrow feasible band (99.0–99.99% availability), zero-based bars render ALL information invisible, which is its own dishonesty; an annotated, clearly-labeled zoom serves truth better than an empty ritual. Synthesis test: does the audience read length or position? If the visual is bars, zero; if precision matters, change the chart type rather than the baseline.

Debate 2. Resolved: dashboards should show raw unsmoothed metrics only; smoothing is the dashboard lying to you by default.

PRO: smoothing is a low-pass filter with lag 1/(1−β) — it delays incident detection, shrinks spikes, and its per-viewer slider state makes screenshots non-comparable; alerting already aggregates, so the eye should see truth. CON: raw high-frequency noise causes alarm fatigue and hides trends the human eye can't integrate; the fix is DISCLOSED smoothing (window printed on the plot) plus raw shown faintly underneath, not abstinence. Middle: raw + one declared smoother overlaid, with incident views auto-switching to raw.

Debate 3. Resolved: ECDFs should replace box plots and violins entirely in engineering reports.

PRO: ECDFs are parameter-free, show every point, read off any percentile, overlay cleanly, and cannot hide bimodality — box plots exist because computing was once expensive. CON: readability is a real cost — slopes-as-density is unintuitive for mixed audiences, and 30 groups of ECDFs is unreadable where 30 violins scan instantly; plots are communication devices, and a plot the audience misreads is dishonest in effect. Practical line: ECDF for ≤ 5 overlaid groups and any threshold/percentile decision; violins for many-group shape scans with bandwidth stated; box plots only with n printed and a bimodality check done off-stage.

Misconception: "Interview plot questions test matplotlib trivia." They test judgment under ambiguity: whether you audit axes-uncertainty-selection in order, whether you can put a NUMBER on a distortion (lie factor, inflation factor, implied p), and whether your proposed replacement plot answers the decision at hand. The candidate who says "I would need to know what decision this figure supports" before critiquing has already passed half the bar.
An interviewer asks: "What visual error-bar test IS calibrated to p = 0.05 for comparing two means with similar SEs?"

Chapter 10: Beyond — Where Plot Literacy Plugs In

You can now render — and detect — the truth. Plot literacy is the presentation layer of the whole evaluation stack: statistics feed the intervals, metric design feeds the axes, and every sibling lesson consumes these plots. Here is how it all connects, and where its limits are.

The arc, one sentence per chapter: encodings can lie 121x (Ch0); distributions before summaries, ECDF as the un-liable view (Ch1); label your whiskers and test differences directly (Ch2); smoothing buys clarity with lag (Ch3); pick the trade-off plot that answers the decision (Ch4); power laws are straight only in log-log and only within the data (Ch5); aggregates hide slices (Ch6); lies compose and so must audits (Ch7); honesty belongs in the plotting library, not the plotter's memory (Ch8).

Honest limits of plot literacy: a perfectly honest plot of a badly designed metric is still misleading (metric-design owns what SHOULD be on the y-axis); a beautiful CI is only as good as the statistical assumptions behind it (eval-statistics owns those); and an honest offline plot says nothing about causal product impact (experiment-design owns A/B machinery, metrics-ladder owns the exec translation).

Where this lesson connects

LessonWhy it connects
The Statistics of EvaluationPrerequisite: supplies the SE / CI / t-test / bootstrap machinery that every interval and p-value in this lesson consumes.
Metric DesignOwns WHAT goes on the y-axis; this lesson owns how the y-axis is rendered honestly.
Experiment Design & A/B TestingHonest plots of offline evals still can't make causal product claims — A/B machinery lives there.
Regression Testing for MLConsumes the delta slice heatmap and time-series noise band as release-gate visualizations.
Evaluating Generative Modelspass@k curves, Elo/BT intervals, and FID comparisons are rendered with exactly this lesson's rules.
Evaluating Estimators & RobotsNEES/NIS consistency plots are Ch2's uncertainty bands applied to state estimators.
The Metrics LadderOwns exec dashboards and business KPIs — the layer above this lesson's eval plots.
AI Evaluation (harness)The eval-harness process lesson where these figures land in team review workflows.
Benchmark ContaminationGSM1k's identity-line scatter — an upstream integrity issue plots can reveal but not fix.

The plot-selection cheat sheet

PlotQuestion it answersMain lie to check
ECDFFull distribution shape, percentiles, tailsNone inherent — parameter-free (that's the point)
Histogram / KDEDistribution shape for communicationBin width / bandwidth manufacturing or erasing modes
Bars + CIComparing two or more meansTruncated baseline; unlabeled whisker type; overlap fallacy
Difference + CIIs A really better than B?Missing (the honest replacement for whisker-tip comparison)
Loss curveLearning dynamics of one runLinear-y hiding late gains; smoothing lag; wall-clock x
ROCRanking quality (prevalence-blind)Hiding rarity — use PR for rare positives
PR curveUsefulness under class imbalanceReading AUC instead of the deployed operating point
Reliability diagramAre the confidences honest?ECE's bin-count sensitivity
Log-log fitScaling-law exponentExtrapolation beyond the data (dash + shade it)
Slice heatmapPer-subgroup performanceWrong colormap; missing n; dictionary row order
Time seriesDrift and regressions over timeMissing noise band; missing event annotations

The formula cheat sheet

Tufte lie factor = [(h₂−h₁)/h₁] / [(v₂−v₁)/v₁],   h = value − baseline   (honest ≈ 1)
Freedman-Diaconis width = 2 × IQR / n1/3   ·   Sturges bins = ⌈1 + log₂n⌉
ECDF(x) = fraction of points ≤ x   (no tuning parameter)
SE = SD / √n   ·   95% CI half-width = t0.975, n−1 × SE   ·   SEdiff = √(SEA² + SEB²)
overlap fallacy: non-overlap needs 3.92 SE, significance needs 2.77 SE (= 1.96√2)
84% CI trick: just-touching ⇒ z = 2×1.405/√2 = 1.99, p ≈ 0.05
EMA: et = βet−1 + (1−β)Lt, corrected et/(1−βt), window ≈ 1/(1−β)
precision = TP/(TP+FP) (feels prevalence)   TPR, FPR = within-class (prevalence-blind)
ECE = Σbins (nb/N) × |confb − accb|
power law: L = a N−α ⇒ log L = log a − α log N (straight in log-log, slope = −α)
aggregate = Σslices (ns/N) × accs   (a size-weighted average that can rise while a slice falls)

The Honest Plot Contract

1. Baseline
Zero baseline for bars, or an annotated zoom for lines
2. Uncertainty
Shown and labeled: statistic (SD/SE/CI) + n + resampled unit
3. Selection
All runs / seeds / slices disclosed; selection rules stated
4. Axes
Scales and smoothing declared
5. Conclusion
Stated WITH its uncertainty attached
Misconception: "Mastering honest plots makes my evaluation trustworthy end to end." Plotting is the LAST stage of the pipeline: a truthful rendering of a gameable metric (metric-design), an uncontrolled comparison (experiment-design), or a contaminated benchmark (cs336-12-evaluation) is a truthful rendering of a lie. Plot literacy is necessary, never sufficient — which is exactly why it lives inside a family of eight lessons.

Worked example: a farewell audit in under a minute

A news-style chart shows quarterly model quality scores 58 and 61 as bars drawn from a baseline of 56. The same computation as Chapter 0, now done in one pass. Drawn heights: 58 − 56 = 2 and 61 − 56 = 5.

visual effect = (5 − 2) / 2 = 1.5   (the second bar LOOKS 150% larger)
data effect = (61 − 58) / 58 = 3 / 58 = 0.0517   (5.2% real improvement)
lie factor = 1.5 / 0.0517 = 29.0

Clause 1 of the contract is violated, a 29x exaggeration. The honest render is bars from zero, where the 5.2% gain looks like what it is.

python — the pocket auditor, 3 lines
def lie_factor(baseline, v1, v2):
    h1, h2 = v1-baseline, v2-baseline
    return ((h2-h1)/h1) / ((v2-v1)/v1)
print(round(lie_factor(56, 58, 61), 1))   # 29.0

Drop any of this lesson's plots onto the contract wheel below and each of the five clauses lights green or red with a one-line reason. Tap a sibling node in the family map to see what it owns.

The Lie-Detector Checklist

Pick a plot; the five contract clauses light green (passes) or red (violated) with a reason. Below, the family map links this lesson to its siblings and the artifact each consumes.

Interview lens. Rank the five clauses of the Honest Plot Contract by how often they're violated in papers you read, and defend your ranking. There is no canonical answer — the question tests calibrated experience. A defensible ranking: (1) disclosed selection is violated most (seed counts and run-selection rules are rarely stated; the 2025 leaderboard analyses made this concrete); (2) labeled uncertainty next (error bars absent or unlabeled — the gap Anthropic's 2024 error-bars piece targeted); (3) conclusion-with-uncertainty (abstract claims outrunning the CIs shown); (4) declared axes/smoothing (log scales usually declared, smoothing rarely); (5) baseline violations last in papers but FIRST in industry slides — venue changes the failure distribution, and noticing that is the staff-level observation.

"The greatest value of a picture is when it forces us to notice what we never expected to see." — John Tukey
— and its greatest danger is when it forces us to conclude what the data never said.

Which claim can an honest, well-constructed eval plot NOT establish on its own?