src/mine_sim/stats.py

← Back to submission · View raw on GitHub

"""Aggregation and Student-t 95% confidence interval helpers."""

from __future__ import annotations

import math
from typing import Iterable, List, Tuple

from scipy import stats as sstats


def mean_ci(values: Iterable[float], confidence: float = 0.95) -> Tuple[float, float, float]:
    """Return ``(mean, ci95_low, ci95_high)`` using Student-t with ``n-1`` df.

    For ``n <= 1`` the half-width is undefined; we return ``(mean, mean, mean)``
    so downstream consumers don't hit NaNs.
    """
    vals = list(values)
    n = len(vals)
    if n == 0:
        return (0.0, 0.0, 0.0)
    mean = sum(vals) / n
    if n == 1:
        return (mean, mean, mean)
    sd = math.sqrt(sum((v - mean) ** 2 for v in vals) / (n - 1))
    t_crit = float(sstats.t.ppf(0.5 + confidence / 2.0, df=n - 1))
    half = t_crit * sd / math.sqrt(n)
    return (mean, mean - half, mean + half)


def busy_minutes(intervals: Iterable[Tuple[float, float]], shift_min: float) -> float:
    """Sum of clamped busy intervals within ``[0, shift_min]``."""
    total = 0.0
    for s, e in intervals:
        s_c = max(0.0, s)
        e_c = min(shift_min, e)
        if e_c > s_c:
            total += e_c - s_c
    return total


def utilisation(intervals: Iterable[Tuple[float, float]], shift_min: float) -> float:
    """Fraction of shift the resource was busy, in [0, 1]."""
    if shift_min <= 0:
        return 0.0
    return min(1.0, busy_minutes(intervals, shift_min) / shift_min)


def mean_or_zero(values: Iterable[float]) -> float:
    vals = list(values)
    if not vals:
        return 0.0
    return sum(vals) / len(vals)