Functional Weave
Code in Rust

stats.percentile@2.0.0

impl/python.py

2,331 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import math
from typing import Sequence

from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune
from .stats_percentile_types import PercentileMethod


def _finite(value: object) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)


def percentile(values: Sequence[float], p: float, method: PercentileMethod, decimals: int) -> float:
    """The p-th percentile of ``values`` by a named method.

    There is no single "95th percentile": the caller names the definition, so
    a dashboard and a billing job cannot quietly disagree.
    """
    if isinstance(values, (str, bytes)) or not isinstance(values, (list, tuple)):
        raise TypeError("values must be a list of numbers")
    if len(values) == 0:
        raise ValueError("values must not be empty")
    for v in values:
        if not _finite(v):
            raise TypeError("values must be finite numbers, received %r" % (v,))
    if not _finite(p) or p < 0 or p > 100:
        raise ValueError("p must be between 0 and 100, received %r" % (p,))
    # Checked up front as well as in round_float (same wording), so a bad
    # decimals is reported before the method is looked at, as in 1.x.
    if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0 or decimals > 12:
        raise ValueError("decimals must be a whole number from 0 to 12, received %r" % (decimals,))

    # float() first so integer inputs follow exactly the binary64 path the
    # other languages take.
    ordered = sorted(float(v) for v in values)
    n = len(ordered)
    p = float(p)

    if method == "nearest-rank":
        # The smallest value with at least p% of the sample at or below it.
        rank = math.ceil((p * n) / 100.0)
        if rank < 1:
            rank = 1
        result = ordered[rank - 1]
    elif method == "linear":
        # Hyndman & Fan type 7: zero-based position (n - 1) * p / 100.
        h = ((n - 1) * p) / 100.0
        lo = math.floor(h)
        if lo >= n - 1:
            result = ordered[n - 1]
        else:
            result = ordered[lo] + (h - lo) * (ordered[lo + 1] - ordered[lo])
    else:
        raise ValueError('unknown percentile method "%s"' % (method,))
    # math.round-float rounds on the exact value of the double: 2.675 gives 2.67.
    return round_float(result, decimals)