Summary
The accuracy/metric layer conflates several distinct outcomes — a sample that errored, a scorer that returned NaN/abstained, and a scorer that returned None — with "this sample does not exist," silently removing them from the metric denominator. Separately, value_to_float output is never range-checked, so a headline accuracy can exceed 1.0 / go negative / be inf; and mean() and accuracy() use incompatible value-to-float rules so mean() raises on the framework's own C/I/P labels. Net effect: a half-broken or abstained eval can report a clean, high accuracy with status="success" — the worst direction for a safety/capability eval.
1. Errored samples vanish from the accuracy denominator
When a sample errors and the error is not severe enough to trip fail_on_error, its score is dropped (run.py ~L1742-1745 returns None; ~L742-746 keeps only dict results; eval_results is called with samples=all but scores=completed; accuracy.py:33 divides by len(scores)). The denominator becomes "samples that happened to succeed."
from inspect_ai import Task, eval
from inspect_ai.dataset import Sample
from inspect_ai.scorer import Score, Target, scorer, accuracy, CORRECT
from inspect_ai.solver import TaskState, solver, Generate
@solver
def passthrough():
async def solve(state, generate): return state
return solve
@scorer(metrics=[accuracy()])
def flaky_scorer():
async def score(state: TaskState, target: Target) -> Score:
if int(state.metadata["idx"]) >= 5: # the "hard"/adversarial half
raise RuntimeError("judge/parse/sandbox error")
return Score(value=CORRECT)
return score
task = Task(
dataset=[Sample(input=f"q{i}", target="x", metadata={"idx": i}) for i in range(10)],
solver=passthrough(), scorer=flaky_scorer(),
)
log = eval(task, model="mockllm/model", fail_on_error=0.9, display="none")[0]
print(log.status, log.results.scores[0].metrics["accuracy"].value) # success 1.0
Actual: status="success", accuracy=1.0 over the 5 survivors.
Expected (for discussion): errored samples should be visible in the headline — counted against the denominator, or the metric reported alongside an explicit "N errored / inconclusive" so a reader cannot mistake a half-failed run for 100%.
2. A scorer that returns NaN ("abstain/inconclusive") is dropped too
results.py ~L300-307 filters out any Score whose value is a float NaN as "unscored" — but NaN is reachable from the public API (Score(value=float("nan"))), the natural way a judge says "I couldn't decide." Those abstentions leave the denominator and inflate accuracy (3 correct, 2 incorrect, 5 abstained -> reported 3/5 = 0.6, not 0.3).
3. accuracy()/mean() never clamp -> headline > 100% / negative / inf
value_to_float passes numbers straight through (_metric.py:228-229) and accuracy.py:29-33 just sums and divides — no [0,1] clamp, no finiteness check. A scorer on a non-0..1 scale (a 0..10 rubric, a count) makes accuracy exceed 1; a single inf makes it inf (it also escapes the math.isnan-only unscored filter).
# per-sample values [8, 5, 10] -> accuracy = 7.67 (767%); [-1,-1,1] -> -0.33
Expected (for discussion): clamp value_to_float to [0,1] (or document that custom numeric scorers must define their own metric), and treat non-finite as unscored rather than a contributor.
4. mean() and accuracy() disagree; mean() raises on C/I/P
accuracy() uses value_to_float() ("C"->1.0); mean() uses Score.as_float() -> float(raw), and float("C") raises ValueError (mean.py:15, _metric.py:157). A scorer emitting the framework's own CORRECT constant is 1.0 under accuracy() but crashes mean(). Tasks that attach [accuracy(), mean(), stderr()] to a label-emitting scorer throw at metric time — the whole run's scores are lost.
from inspect_ai.scorer._metric import Score, value_to_float
vtf = value_to_float()
for v in ["C", "I", "P"]:
try: m = Score(value=v).as_float()
except Exception as e: m = f"{type(e).__name__}"
print(v, "accuracy-path", vtf(v), "| mean-path", m) # C 1.0 | mean-path ValueError
Expected: mean() should understand the same label vocabulary as accuracy() (or raise a clear error at construction when paired with a label scorer). This sub-item is an unambiguous bug and could ship as a small standalone PR independent of items 1-3.
Proposed direction (one coherent change)
Items 1, 2, and the inf part of 3 are one design question: inspect has no concept of an inconclusive outcome. Erroring / abstaining / out-of-range samples all collapse into "absent," silently. A single direction fixes all of them: keep inconclusive samples in the denominator (or report a first-class "inconclusive / errored" count next to every metric), clamp value_to_float to [0,1] and treat non-finite as inconclusive, and make mean()/accuracy() agree on the label vocabulary. Happy to implement once the intended semantics are agreed — and to split into focused PRs (item 4 first as a clear bug).
Environment
inspect_ai @ 8915598; Python 3.12+; reproduced with mockllm (no model provider required).
Summary
The accuracy/metric layer conflates several distinct outcomes — a sample that errored, a scorer that returned NaN/abstained, and a scorer that returned
None— with "this sample does not exist," silently removing them from the metric denominator. Separately,value_to_floatoutput is never range-checked, so a headlineaccuracycan exceed 1.0 / go negative / beinf; andmean()andaccuracy()use incompatible value-to-float rules somean()raises on the framework's ownC/I/Plabels. Net effect: a half-broken or abstained eval can report a clean, highaccuracywithstatus="success"— the worst direction for a safety/capability eval.1. Errored samples vanish from the accuracy denominator
When a sample errors and the error is not severe enough to trip
fail_on_error, its score is dropped (run.py~L1742-1745 returnsNone; ~L742-746 keeps only dict results;eval_resultsis called withsamples=allbutscores=completed;accuracy.py:33divides bylen(scores)). The denominator becomes "samples that happened to succeed."Actual:
status="success",accuracy=1.0over the 5 survivors.Expected (for discussion): errored samples should be visible in the headline — counted against the denominator, or the metric reported alongside an explicit "N errored / inconclusive" so a reader cannot mistake a half-failed run for 100%.
2. A scorer that returns NaN ("abstain/inconclusive") is dropped too
results.py~L300-307 filters out anyScorewhosevalueis a float NaN as "unscored" — but NaN is reachable from the public API (Score(value=float("nan"))), the natural way a judge says "I couldn't decide." Those abstentions leave the denominator and inflate accuracy (3 correct, 2 incorrect, 5 abstained -> reported3/5 = 0.6, not0.3).3.
accuracy()/mean()never clamp -> headline > 100% / negative / infvalue_to_floatpasses numbers straight through (_metric.py:228-229) andaccuracy.py:29-33just sums and divides — no[0,1]clamp, no finiteness check. A scorer on a non-0..1 scale (a 0..10 rubric, a count) makesaccuracyexceed 1; a singleinfmakes itinf(it also escapes themath.isnan-only unscored filter).# per-sample values [8, 5, 10] -> accuracy = 7.67 (767%); [-1,-1,1] -> -0.33Expected (for discussion): clamp
value_to_floatto[0,1](or document that custom numeric scorers must define their own metric), and treat non-finite as unscored rather than a contributor.4.
mean()andaccuracy()disagree;mean()raises onC/I/Paccuracy()usesvalue_to_float()("C"->1.0);mean()usesScore.as_float()->float(raw), andfloat("C")raises ValueError (mean.py:15,_metric.py:157). A scorer emitting the framework's ownCORRECTconstant is1.0underaccuracy()but crashesmean(). Tasks that attach[accuracy(), mean(), stderr()]to a label-emitting scorer throw at metric time — the whole run's scores are lost.Expected:
mean()should understand the same label vocabulary asaccuracy()(or raise a clear error at construction when paired with a label scorer). This sub-item is an unambiguous bug and could ship as a small standalone PR independent of items 1-3.Proposed direction (one coherent change)
Items 1, 2, and the inf part of 3 are one design question: inspect has no concept of an inconclusive outcome. Erroring / abstaining / out-of-range samples all collapse into "absent," silently. A single direction fixes all of them: keep inconclusive samples in the denominator (or report a first-class "inconclusive / errored" count next to every metric), clamp
value_to_floatto[0,1]and treat non-finite as inconclusive, and makemean()/accuracy()agree on the label vocabulary. Happy to implement once the intended semantics are agreed — and to split into focused PRs (item 4 first as a clear bug).Environment
inspect_ai @
8915598; Python 3.12+; reproduced withmockllm(no model provider required).