Skip to content

Metrics

Overview

MASA metrics are designed to make it easy to record and report learning signals at different granularities: single scalars, streaming summary statistics, and approximate distributions. Keeping logging lightweight and backend-agnostic.

  • Scalars are plain numeric values (e.g., reward, loss, episode length). Loggers keep a rolling window of recent scalar values and typically report a smoothed mean over that window.
  • Summary statistics are handled by masa.common.metrics.Stats, which performs streaming aggregation over batches of values. It tracks running moments (mean and mean-square) and extrema (min/max/magnitude), and exposes derived quantities like standard deviation: \(\sigma = \sqrt{\max(0, \mathbb{E}[X^2] - \mathbb{E}[X]^2)}\). Calling Stats.get returns a flat dictionary of scalars suitable for logging.
  • Distributions are handled by masa.common.metrics.Dist, which maintains a fixed-size reservoir sample of a stream. This gives a compact approximation of the underlying distribution that can be plotted or logged as a histogram.
  • Logging is performed by logger implementations (see the next page), which accept mixtures of scalars, Stats, and Dist objects. They aggregate values over a configurable window and can emit to stdout and/or TensorBoard with consistent key prefixing.

API Reference

Metrics Core

masa.common.metrics.Stats

Stats(prefix: str = '')

Streaming scalar summary statistics for an arbitrary batch of values.

This class maintains simple running aggregates over a stream of scalar observations:

  • Mean: \(\mu\)
  • Mean of squares: \(\mathbb{E}[X^2]\)
  • Derived standard deviation: \(\sigma = \sqrt{\max(0, \mathbb{E}[X^2] - \mu^2)}\)
  • Extremes: min/max
  • Magnitude: \(\max |x|\)

The update method accepts any array-like input, flattens it to a 1D vector, and updates the aggregates. The get method returns a dict of scalars suitable for logging; if prefix is non-empty, keys are prefixed with "{prefix}_".

Parameters:

Name Type Description Default
prefix str

Optional key prefix (without trailing underscore). If provided, keys returned by get are of the form "{prefix}_{name}".

''

Attributes:

Name Type Description
n int

Total number of scalar samples seen so far.

prefix str

Prefix used to namespace emitted keys.

stats Optional[Dict[str, float]]

Internal aggregate state or None if no data has been observed. When populated, it contains keys mean, mean_squares, max, min, and mag.

Source code in masa/common/metrics.py
def __init__(self, prefix: str = ""):
    self.n: int = 0
    self.prefix: str = prefix
    self.stats: Optional[Dict[str, float]] = None

n instance-attribute

n: int = 0

prefix instance-attribute

prefix: str = prefix

stats instance-attribute

stats: Optional[Dict[str, float]] = None

update

update(values: Union[ndarray, Iterable[float], float])

Update aggregates with a batch of scalar values.

The input is converted to np.float32 and flattened. If the resulting array is empty, the call is a no-op.

Parameters:

Name Type Description Default
values Union[ndarray, Iterable[float], float]

A scalar or array-like collection of numeric values.

required
Source code in masa/common/metrics.py
def update(self, values: Union[np.ndarray, Iterable[float], float]):
    """Update aggregates with a batch of scalar values.

    The input is converted to ``np.float32`` and flattened. If the resulting
    array is empty, the call is a no-op.

    Args:
        values: A scalar or array-like collection of numeric values.
    """
    v = np.asarray(values, dtype=np.float32).ravel()
    m = int(v.size)
    if m == 0:
        return

    if self.stats is None:
        self.n = m
        self.stats = {
            "mean": float(np.mean(v)),
            "mean_squares": float(np.mean(v**2)),
            "max": float(np.max(v)),
            "min": float(np.min(v)),
            "mag": float(np.max(np.abs(v))),
        }
        return

    # Weighted update of first and second moments.
    n_prev = self.n
    self.n += m
    w_new = m / self.n
    w_old = (self.n - m) / self.n

    self.stats = {
        "mean": float(np.mean(v)) * w_new + float(self.stats["mean"]) * w_old,
        "mean_squares": float(np.mean(v**2)) * w_new
        + float(self.stats["mean_squares"]) * w_old,
        "max": float(max(float(np.max(v)), float(self.stats["max"]))),
        "min": float(min(float(np.min(v)), float(self.stats["min"]))),
        "mag": float(max(float(np.max(np.abs(v))), float(self.stats["mag"]))),
    }

get

get() -> Dict[str, float]

Return the current statistics as a flat dict of scalars.

Returns:

Type Description
Dict[str, float]

A mapping containing:

Dict[str, float]
  • mean: \(\mu\)
Dict[str, float]
  • std: \(\sigma = \sqrt{\max(0, \mathbb{E}[X^2] - \mu^2)}\)
Dict[str, float]
  • max: \(\max x\)
Dict[str, float]
  • min: \(\min x\)
Dict[str, float]
  • mag: \(\max |x|\)
Dict[str, float]

If prefix is non-empty, keys are prefixed with

Dict[str, float]

"{prefix}_".

Raises:

Type Description
RuntimeError

If called before any data has been observed.

Source code in masa/common/metrics.py
def get(self) -> Dict[str, float]:
    r"""Return the current statistics as a flat dict of scalars.

    Returns:
        A mapping containing:

        - ``mean``: :math:`\mu`
        - ``std``: :math:`\sigma = \sqrt{\max(0, \mathbb{E}[X^2] - \mu^2)}`
        - ``max``: :math:`\max x`
        - ``min``: :math:`\min x`
        - ``mag``: :math:`\max |x|`

        If :attr:`prefix` is non-empty, keys are prefixed with
        ``"{prefix}_"``.

    Raises:
        RuntimeError: If called before any data has been observed.
    """
    if self.stats is None:
        raise RuntimeError("Stats.get() called before any update().")

    mean = float(self.stats["mean"])
    mean_sq = float(self.stats["mean_squares"])
    std = float(np.sqrt(np.maximum(0.0, mean_sq - mean**2)))

    out: Dict[str, float] = {
        "mean": mean,
        "std": std,
        "max": float(self.stats["max"]),
        "min": float(self.stats["min"]),
        "mag": float(self.stats["mag"]),
    }
    if self.prefix:
        out = {f"{self.prefix}_{k}": v for k, v in out.items()}
    return out

__add__

__add__(other: 'Stats') -> 'Stats'

Combine two Stats objects into a new aggregate.

This is useful when you have two independent streams and want the same summary you would have obtained if you had processed all samples in one stream. The combination is exact for the tracked aggregates (first/second moments and extrema).

Parameters:

Name Type Description Default
other 'Stats'

Another Stats instance with the same prefix.

required

Returns:

Type Description
'Stats'

A new Stats instance representing the combined aggregate.

Raises:

Type Description
AssertionError

If prefix differs.

RuntimeError

If either object has not observed any data.

Source code in masa/common/metrics.py
def __add__(self, other: "Stats") -> "Stats":
    """Combine two :class:`Stats` objects into a new aggregate.

    This is useful when you have two independent streams and want the same
    summary you would have obtained if you had processed all samples in one
    stream. The combination is exact for the tracked aggregates
    (first/second moments and extrema).

    Args:
        other: Another :class:`Stats` instance with the same :attr:`prefix`.

    Returns:
        A new :class:`Stats` instance representing the combined aggregate.

    Raises:
        AssertionError: If :attr:`prefix` differs.
        RuntimeError: If either object has not observed any data.
    """
    assert isinstance(other, Stats)
    assert self.prefix == other.prefix, "can't add two Stats objects with different prefixes"
    if self.stats is None or other.stats is None:
        raise RuntimeError("Cannot add Stats objects before both have been updated at least once.")

    new = Stats(prefix=self.prefix)
    new.n = int(self.n + other.n)

    w_self = self.n / new.n
    w_other = other.n / new.n
    new.stats = {
        "mean": float(self.stats["mean"]) * w_self + float(other.stats["mean"]) * w_other,
        "mean_squares": float(self.stats["mean_squares"]) * w_self
        + float(other.stats["mean_squares"]) * w_other,
        "max": float(max(float(self.stats["max"]), float(other.stats["max"]))),
        "min": float(min(float(self.stats["min"]), float(other.stats["min"]))),
        "mag": float(max(float(self.stats["mag"]), float(other.stats["mag"]))),
    }
    return new

masa.common.metrics.Dist

Dist(prefix: str = '', reservoir_size: int = 2048, rng: Optional[Union[int, Generator, BitGenerator]] = None)

Reservoir-sampled distribution summary.

This class maintains a fixed-size reservoir sample of a stream of scalar values using reservoir sampling. After processing \(n\) total samples, a reservoir of size \(k\) contains a uniform sample (without replacement) from the observed stream (in expectation).

Parameters:

Name Type Description Default
prefix str

Optional logical prefix for this distribution (used by loggers).

''
reservoir_size int

Maximum number of samples retained in the reservoir.

2048
rng Optional[Union[int, Generator, BitGenerator]]

Seed (or seed-like) passed to numpy.random.default_rng.

None

Attributes:

Name Type Description
n int

Total number of scalar samples seen so far.

prefix str

Prefix used by StatsLogger when naming histogram keys.

reservoir_size int

Maximum reservoir capacity.

res ndarray

Current reservoir buffer of shape (<=reservoir_size,).

rng Generator

Numpy random generator used for reservoir sampling.

Source code in masa/common/metrics.py
def __init__(
    self,
    prefix: str = "",
    reservoir_size: int = 2048,
    rng: Optional[Union[int, np.random.Generator, np.random.BitGenerator]] = None,
):
    self.n: int = 0
    self.prefix: str = prefix
    self.reservoir_size: int = int(reservoir_size)
    self.res: np.ndarray = np.empty((0,), dtype=np.float32)
    self.rng: np.random.Generator = np.random.default_rng(rng)

n instance-attribute

n: int = 0

prefix instance-attribute

prefix: str = prefix

reservoir_size instance-attribute

reservoir_size: int = int(reservoir_size)

res instance-attribute

res: ndarray = np.empty((0,), dtype=np.float32)

rng instance-attribute

rng: Generator = np.random.default_rng(rng)

update

update(values: Union[ndarray, Iterable[float], float])

Update the reservoir with a batch of values.

Parameters:

Name Type Description Default
values Union[ndarray, Iterable[float], float]

A scalar or array-like collection of numeric values. The input is flattened into a 1D array.

required
Source code in masa/common/metrics.py
def update(self, values: Union[np.ndarray, Iterable[float], float]):
    """Update the reservoir with a batch of values.

    Args:
        values: A scalar or array-like collection of numeric values. The
            input is flattened into a 1D array.
    """
    v = np.asarray(values, dtype=np.float32).ravel()
    m = int(v.size)
    if m == 0:
        return

    # Fill reservoir initially.
    if self.n < self.reservoir_size:
        take = min(self.reservoir_size - self.n, m)
        if take > 0:
            self.res = np.concatenate([self.res, v[:take]])
            self.n += int(take)
            v = v[take:]

    # Reservoir sampling for the remainder.
    for x in v:
        self.n += 1
        j = int(self.rng.integers(0, self.n))
        if j < self.reservoir_size:
            self.res[j] = x

get

get() -> np.ndarray

Return a copy of the reservoir buffer.

Returns:

Type Description
ndarray

A copy of the internal reservoir array (dtype np.float32).

Source code in masa/common/metrics.py
def get(self) -> np.ndarray:
    """Return a copy of the reservoir buffer.

    Returns:
        A copy of the internal reservoir array (dtype ``np.float32``).
    """
    return self.res.copy()

__add__

__add__(other: 'Dist') -> 'Dist'

Merge two reservoirs into a new reservoir.

The merged reservoir is produced by sampling (without replacement) from the concatenation of both reservoirs, capped at reservoir_size. This is a pragmatic merge for logging/visualisation; it does not exactly reproduce a single-pass reservoir over the full underlying streams.

Parameters:

Name Type Description Default
other 'Dist'

Another Dist with the same prefix and reservoir_size.

required

Returns:

Type Description
'Dist'

A new Dist containing a merged reservoir sample.

Raises:

Type Description
AssertionError

If prefix differs.

Source code in masa/common/metrics.py
def __add__(self, other: "Dist") -> "Dist":
    """Merge two reservoirs into a new reservoir.

    The merged reservoir is produced by sampling (without replacement) from
    the concatenation of both reservoirs, capped at :attr:`reservoir_size`.
    This is a pragmatic merge for logging/visualisation; it does not exactly
    reproduce a single-pass reservoir over the full underlying streams.

    Args:
        other: Another :class:`Dist` with the same :attr:`prefix` and
            :attr:`reservoir_size`.

    Returns:
        A new :class:`Dist` containing a merged reservoir sample.

    Raises:
        AssertionError: If :attr:`prefix` differs.
    """
    assert isinstance(other, Dist)
    assert self.prefix == other.prefix, "can't add two Dist objects with different prefixes"
    assert (
        self.reservoir_size == other.reservoir_size
    ), "can't add two Dist objects with different reservoir sizes"

    new = Dist(prefix=self.prefix, reservoir_size=self.reservoir_size, rng=0)
    new.n = int(self.n + other.n)

    both = np.concatenate([self.res, other.res])
    if both.size <= new.reservoir_size:
        new.res = both.astype(np.float32, copy=False)
    else:
        idx = np.random.default_rng(0).choice(
            both.size, size=new.reservoir_size, replace=False
        )
        new.res = both[idx].astype(np.float32, copy=False)
    return new

Next Steps

  • Logging - Learn how logging is handled automatically in MASA.