Skip to content

Histograms

A histogram series stores a distribution per step, not a number. There are two ways to read one, and the right choice depends on whether you want the shape or a trend.

frame = client.fetch_histogram_series("run-001", "weights/l0")

A histogram point is two-dimensional, so it cannot fold into a wide value matrix the way float points do. The frame is long — index (run, series, step), one row per point:

histogram count min max mean std
run series step
run-001 weights/l0 0 <metrana.Histogram> 4096.0 -0.412 0.398 -0.021 0.147
100 <metrana.Histogram> 4096.0 -0.207 0.211 -0.008 0.088

The histogram column holds metrana.Histogram objects — the same type the logger takes, so what you read back is what you logged. Counts and densities are indistinguishable as bare arrays, and the type settles which it is structurally:

h = frame.loc[("run-001", "weights/l0", 100), "histogram"]
h.bin_edges, h.counts, h.stats

The stat columns beside it are per-point summaries kept vectorised — frame["std"].plot() works without unpacking a single object. The set depends on the kind: counting histograms carry count/min/max/mean/std, density histograms min/max/mean/std (a density divides the sample count out, so there is none to report). They are exact when the logger passed stats=, and the ingestion service’s bin-approximation otherwise; the raw sum / sum-of-squares primitives remain available on Histogram.stats if you need to merge points yourself.

Density histograms live under their own value kind — pass kind= to fetch those instead:

from metrana.query import SeriesValueKind
client.fetch_histogram_series("run-001", "acts", kind=SeriesValueKind.DENSITY_HISTOGRAM)

include_extents=True attaches each series’ step extents as frame.attrs["extents"], keyed by (run, series). Each value is a SeriesExtent:

Field Meaning
first_step Lowest step the series has, ignoring the steps selection.
last_step Highest step the series has, ignoring the steps selection.
points_count Upper bound on the number of points — counted before deduplication, so re-logged steps inflate it.

The extent is independent of the steps you asked for, which is what makes it useful: fetch just the last point and you still learn the series’ full range, enough to render a step slider without a second scan.

from metrana.query import LastSteps
frame = client.fetch_histogram_series("run-001", "acts", steps=LastSteps(1),
include_extents=True)
extent = frame.attrs["extents"][("run-001", "acts")]
slider_range = (extent.first_step, extent.last_step)

Every series the fetch matched is reported — including one whose selected step range holds no points at all, which is precisely the case where the extent tells you where the points actually are.

To chart a histogram metric you want a line, not 512 bins per step. reduce_histogram turns each point into one float at query time, so the series flows through the ordinary float endpoints — including the bucketed one:

from metrana.query import SeriesQuery, HistogramReducer
client.fetch_float_series("run-001", SeriesQuery("grads").reduce_histogram(HistogramReducer.avg()))
client.fetch_float_series_buckets(
runs, SeriesQuery("grads").reduce_histogram(HistogramReducer.quantile(0.95)), buckets_count=500,
)
Reducer Computed from
HistogramReducer.min() / .max() / .avg() / .std() the point’s stored moments — exact if you logged stats=
HistogramReducer.quantile(p) the bins — carries the binning’s resolution
HistogramReducer.fraction_above(t) the bins — same caveat

That split matters: moment-based reducers are exact when the logger passed the raw values’ moments, whereas a quantile is only ever as precise as the bin width. It is the same trade the ingestion side describes from the writing end.

Reduction is a point-fetch feature. The summary listings reduce with their own summary function instead, so reduce_histogram has no place there.