diff --git a/evals/plot.py b/evals/plot.py
index eb9e749..44f9327 100644
--- a/evals/plot.py
+++ b/evals/plot.py
@@ -1,5 +1,6 @@
"""
-Generate an interactive bar chart of skill compression vs the terse control arm.
+Generate an intuitive bar chart comparing skill output length vs the
+terse control arm — side by side, so the visual gap IS the gain.
Reads evals/snapshots/results.json and writes:
- evals/snapshots/results.html (interactive plotly)
@@ -11,7 +12,6 @@ Run: uv run --with tiktoken --with plotly --with kaleido python evals/plot.py
from __future__ import annotations
import json
-import statistics
from pathlib import Path
import plotly.graph_objects as go
@@ -32,115 +32,93 @@ def main() -> None:
arms = data["arms"]
meta = data.get("metadata", {})
- terse_tokens = [count(o) for o in arms["__terse__"]]
+ terse_total = sum(count(o) for o in arms["__terse__"])
rows = []
for skill, outputs in arms.items():
if skill in ("__baseline__", "__terse__"):
continue
- skill_tokens = [count(o) for o in outputs]
- # savings as positive percentages (bigger = more compression)
- savings = [
- (1 - (s / t)) * 100 if t else 0.0
- for s, t in zip(skill_tokens, terse_tokens)
- ]
+ skill_total = sum(count(o) for o in outputs)
+ saved = terse_total - skill_total
+ pct = (saved / terse_total) * 100 if terse_total else 0.0
rows.append(
- {
- "skill": skill,
- "median": statistics.median(savings),
- "mean": statistics.mean(savings),
- "min": min(savings),
- "max": max(savings),
- "all": savings,
- }
+ {"skill": skill, "skill_total": skill_total, "saved": saved, "pct": pct}
)
- rows.sort(key=lambda r: r["mean"]) # ascending so best is at top in horizontal bar
+ rows.sort(
+ key=lambda r: r["pct"]
+ ) # ascending → biggest gain at top in horizontal bar
names = [r["skill"] for r in rows]
- means = [r["mean"] for r in rows]
- medians = [r["median"] for r in rows]
- mins = [r["min"] for r in rows]
- maxs = [r["max"] for r in rows]
-
- # color by mean: green for compression, red for inflation
- colors = ["#2ca02c" if m > 0 else "#d62728" for m in means]
+ skill_totals = [r["skill_total"] for r in rows]
+ saved = [r["saved"] for r in rows]
+ pcts = [r["pct"] for r in rows]
fig = go.Figure()
- # range line (min → max) per skill, behind the bars
- for name, lo, hi in zip(names, mins, maxs):
- fig.add_trace(
- go.Scatter(
- x=[lo, hi],
- y=[name, name],
- mode="lines",
- line=dict(color="rgba(80,80,80,0.5)", width=2),
- showlegend=False,
- hoverinfo="skip",
- )
- )
-
- # mean bars
+ # the part the skill still uses (what you pay)
fig.add_trace(
go.Bar(
- x=means,
y=names,
+ x=skill_totals,
orientation="h",
- marker=dict(color=colors),
- text=[f"{m:+.0f}%" for m in means],
- textposition="outside",
- name="mean",
- hovertemplate="%{y}
mean: %{x:.1f}%
tokens used: %{x}
median: %{x:.1f}%
tokens saved: %{x} (%{customdata:.0f}%)
"
- f"{meta.get('model', '?')} · n={meta.get('n_prompts', '?')} prompts · "
- f"single run per arm",
+ text=f"How much shorter does each skill make Claude's answers?
"
+ f"{meta.get('model', '?')} · {meta.get('n_prompts', '?')} prompts · "
+ f"compared against a plain 'Answer concisely.' baseline",
x=0.5,
xanchor="center",
),
+ barmode="stack",
xaxis=dict(
- title="Savings (%) — positive = compressed, negative = inflated",
- ticksuffix="%",
+ title="Total output tokens across all prompts",
zeroline=False,
gridcolor="rgba(0,0,0,0.08)",
+ range=[0, terse_total * 1.15],
),
yaxis=dict(title=""),
plot_bgcolor="white",
height=420,
- width=900,
- margin=dict(l=120, r=80, t=90, b=70),
+ width=950,
+ margin=dict(l=120, r=80, t=100, b=70),
legend=dict(
- orientation="h", yanchor="bottom", y=-0.25, xanchor="center", x=0.5
+ orientation="h", yanchor="bottom", y=-0.22, xanchor="center", x=0.5
),
)
diff --git a/evals/snapshots/results.html b/evals/snapshots/results.html
index fa028cc..7537ef8 100644
--- a/evals/snapshots/results.html
+++ b/evals/snapshots/results.html
@@ -3883,6 +3883,6 @@ maplibre-gl/dist/maplibre-gl.js:
window.Plotly = Plotly;
return Plotly;
-}));