Skip to content

Series summaries

A summary reduces a whole series to one number: its last value, its minimum, its mean. Summaries are pre-computed server-side, so asking for “the final loss of 500 runs” costs one cheap query rather than 500 series fetches.

As columns of the runs table, when you want them alongside attributes:

client.fetch_runs(series_summaries=[SeriesSummary("loss", "last")])

Or on their own, in bulk, when the runs table is not what you’re after:

frame = client.fetch_series_summaries(run_names, ["loss", "accuracy"], functions=["last", "min", "max"])

The bulk form returns index (run, series), one float64 column per function requested anywhere in the call (NaN for a series whose selector did not ask for it), plus last_step (a nullable UInt64, so steps stay exact) and last_timestamp of that series’ most recent point:

last min max last_step last_timestamp
run series
run-001 accuracy 0.9412 0.1030 0.9412 4999 2026-07-01 09:14:22.114
loss 0.0921 0.0921 2.4012 4999 2026-07-01 09:14:22.114
frame["last"] # every run × series, one function
frame.xs("loss", level="series")
Whole-series FIRST, LAST, MIN, MAX, SUM, AVG, COUNT, STD
Of the first point’s group FIRST_COUNT, FIRST_MIN, FIRST_MAX, FIRST_AVG, FIRST_STD
Of the last point’s group LAST_COUNT, LAST_SUM, LAST_MIN, LAST_MAX, LAST_AVG, LAST_STD

Plain strings work wherever a SummaryFunction does — "last" and SummaryFunction.LAST are the same thing.

All of them fold finite values only: a series that logged NaN/±inf keeps meaningful statistics (COUNT counts its finite points), and the specials stay visible in the raw points and in bucketed reads.

functions= applies the same list to every series, which is the common case and the compact one. When different series want different functions, that full mesh is wasteful — so pass a SeriesSummariesQuery, which carries its own:

from metrana.query import SeriesSummariesQuery
client.fetch_series_summaries(
run_names,
[
SeriesSummariesQuery("loss", "last", "min"),
SeriesSummariesQuery("throughput", "avg"),
],
)

Mixed lists are fine: bare names and SeriesQuery objects take the functions= default, while a SeriesSummariesQuery overrides it for itself.

The same summaries drive SeriesFilter and SeriesOrderBy, so “runs whose final loss beat 0.1, best first” is one query:

from metrana.query import SeriesFilter, SeriesOrderBy
client.fetch_runs(
filter=SeriesFilter("loss").last() < 0.1,
order_by=SeriesOrderBy("accuracy", "max").desc(),
)

A summary column, filter or ordering always produces one value per run. Which series that value comes from is up to the labels you pin:

SeriesSummary("loss", "avg").with_label("worker", "w0") # that one series
SeriesSummary("loss", "avg").with_label_matches("worker", "^gpu.*") # across the matching ones
SeriesSummary("loss", "avg") # across every series of the metric

Leave a label key out, or use any operator other than equality, and the function is computed across every matching series of the run — the same operator surface as a point fetch. The combination is exact, not an average of averages: MIN/MAX/SUM/COUNT combine the obvious way, and AVG/STD are recomputed from the summed moments of each series.

A run whose series match nothing contributes nothing: it is filtered out, sorts in natural order, and its summary column is empty.

This is not the same as fetch_series_summaries, which takes the same partial labels but keeps the series apart — one row per (run, series) instead of one aggregated value per run.