Functional Weave
Code in Python

stats.mean-median@1.0.0

impl/python.py

2,829 bytes · the Python implementation · view raw

from math import gcd
from typing import List, Sequence

from .stats_mean_median_types import CentralTendency, Fraction


MAX_SAFE = 9007199254740991


def _fraction(numerator: int, denominator: int) -> Fraction:
    g = gcd(numerator, denominator) or 1
    return Fraction(numerator=numerator // g, denominator=denominator // g)


def mean_median_mode(values: Sequence[int]) -> CentralTendency:
    """Mean, median and mode of a list of integers.

    Mean and median are exact fractions; their float forms come from a single
    correctly rounded division, so every language returns the same double.
    """
    if isinstance(values, (str, bytes)) or not isinstance(values, (list, tuple)):
        raise TypeError("values must be a list of integers")
    if len(values) == 0:
        raise ValueError("values must not be empty")
    for v in values:
        # bool is an int in Python; every other language rejects it, so do too.
        if isinstance(v, bool) or not isinstance(v, int):
            raise TypeError("values must be integers, received %r" % (v,))
        if abs(v) > MAX_SAFE:
            raise ValueError("values must be safe integers (magnitude at most 9007199254740991), received %d" % v)

    total = sum(values)
    if abs(total) > MAX_SAFE:
        raise ValueError("sum exceeds the safe integer range")
    count = len(values)

    ordered = sorted(values)
    if count % 2 == 1:
        median = Fraction(numerator=ordered[(count - 1) // 2], denominator=1)
    else:
        pair = ordered[count // 2 - 1] + ordered[count // 2]
        if pair % 2 == 0:
            median = Fraction(numerator=pair // 2, denominator=1)
        else:
            if abs(pair) > MAX_SAFE:
                raise ValueError("median exceeds the safe integer range")
            median = Fraction(numerator=pair, denominator=2)

    # Runs of equal values in the sorted copy give frequencies in ascending
    # value order, so the modes come out sorted without a second sort.
    modes: List[int] = []
    mode_frequency = 0
    i = 0
    while i < count:
        j = i
        while j < count and ordered[j] == ordered[i]:
            j += 1
        run = j - i
        if run > mode_frequency:
            mode_frequency = run
            modes = [ordered[i]]
        elif run == mode_frequency:
            modes.append(ordered[i])
        i = j

    # float(a) / float(b) with both exact (|a| <= 2^53) is one correctly
    # rounded IEEE division, the same double TypeScript and Rust produce.
    return CentralTendency(
        count=count,
        sum=total,
        mean=_fraction(total, count),
        mean_value=float(total) / float(count) + 0.0,
        median=median,
        median_value=float(median.numerator) / float(median.denominator) + 0.0,
        modes=modes,
        mode_frequency=mode_frequency,
    )