Zum Inhalt

touchstone/stats.py

"""Distribution statistics. Pure, dependency-free, unit-tested.

The brief is explicit: report distribution, not the mean. TTFT as P50 + P95,
decode/prefill as median, and TTFT consistency as CV% (stdev/mean * 100).
"""

from __future__ import annotations

import math
from statistics import median as _median


def percentile(values: list[float], pct: float) -> float:
    """Linear-interpolation percentile (numpy default / "type 7").

    pct is in [0, 100]. Returns nan for an empty input so callers can decide how
    to render a missing cell rather than crashing the whole report.
    """
    if not values:
        return math.nan
    if len(values) == 1:
        return values[0]
    if not 0.0 <= pct <= 100.0:
        raise ValueError(f"percentile pct out of range: {pct}")
    ordered = sorted(values)
    rank = (pct / 100.0) * (len(ordered) - 1)
    lo = math.floor(rank)
    hi = math.ceil(rank)
    if lo == hi:
        return ordered[int(rank)]
    frac = rank - lo
    return ordered[lo] * (1.0 - frac) + ordered[hi] * frac


def median(values: list[float]) -> float:
    if not values:
        return math.nan
    return float(_median(values))


def cv_percent(values: list[float]) -> float:
    """Coefficient of variation in percent = stdev / mean * 100.

    Uses the sample stdev (n-1). Returns nan for <2 samples or a ~zero mean.
    """
    if len(values) < 2:
        return math.nan
    mean = sum(values) / len(values)
    if abs(mean) < 1e-12:
        return math.nan
    var = sum((x - mean) ** 2 for x in values) / (len(values) - 1)
    return math.sqrt(var) / mean * 100.0


def mean(values: list[float]) -> float:
    if not values:
        return math.nan
    return sum(values) / len(values)