Skip to content

Add aggregate(key, agg=...) metric factory for dict-valued Score.value #3735

Description

@MattFisher

Summary

Many scorers emit dict-valued Score.value (multiple numeric fields per sample) and then need per-key metrics at the task level. Today each eval re-implements that plumbing by hand, even when it's just "take the mean of foo across samples".

inspect_evals.utils.metrics.mean_of solves this for the specific case of mean aggregation. Per the Slack discussion, a more general aggregate(key, agg=mean) factory that composes an arbitrary aggregator with a key selector is a better fit for the upstream metric API.

Motivation

Current pattern in inspect_evals:

from inspect_ai.scorer import Metric, metric
from inspect_evals.utils import mean_of

@metric
def element_accuracy() -> Metric:
    return mean_of("element_acc", on_missing="skip")

@metric
def action_f1() -> Metric:
    return mean_of("action_f1", on_missing="skip")

Extending this to other aggregators (stderr, std, percentile, harmonic mean, ...) currently requires writing a new factory for each. A single composition primitive would cover all of them and would compose cleanly with the existing accuracy, mean, stderr metrics in inspect_ai.scorer.

Proposed API

from inspect_ai.scorer import Metric, aggregate, mean, stderr

@metric
def element_accuracy() -> Metric:
    return aggregate("element_acc", agg=mean(), on_missing="skip")

@metric
def element_accuracy_stderr() -> Metric:
    return aggregate("element_acc", agg=stderr(), on_missing="skip")

Signature sketch:

def aggregate(
    key: str,
    agg: Metric,
    *,
    to_float: ValueToFloat = value_to_float(),
    on_missing: Literal["error", "skip", "zero"] = "error",
) -> Metric:
    """Apply `agg` to the `key` field extracted from each dict-valued
    Score.value."""

Semantics:

  • For each SampleScore, require score.value to be a dict.
  • Extract value[key], convert with to_float, feed the resulting SampleScores (with unwrapped values) into agg.
  • on_missing controls how samples without key are handled:
    • "error" (default): raise.
    • "skip": exclude from the aggregator.
    • "zero": treat as 0.0.

Reference implementation

Current, mean-only version: src/inspect_evals/utils/metrics.py

mean_of(
    key: str,
    to_float: ValueToFloat = value_to_float(),
    on_missing: Literal["error", "skip", "zero"] = "error",
) -> Metric

Related

Prior discussion

  • Slack thread on Inspect Community #feature-requests (2026-04-17).
  • JJ: "Or maybe aggregate(key, agg=mean)?" — preferred shape.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions