import math from typing import Sequence from .stats_standard_deviation_types import DeviationKind POW10 = [1.0, 10.0, 100.0, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12] def _round_to(x: float, decimals: int) -> float: # Half away from zero on the binary64 value. Python's round() is # half-even and would give round(0.5) == 0; this does not. scale = POW10[decimals] y = x * scale r = float(math.floor(y)) if y - r >= 0.5: r += 1.0 return r / scale + 0.0 def standard_deviation(values: Sequence[float], kind: DeviationKind, decimals: int) -> float: """Population or sample standard deviation, two-pass. The one-pass "mean of squares minus square of mean" shortcut cancels catastrophically on large, close values; two passes do not. """ if isinstance(values, (str, bytes)) or not isinstance(values, (list, tuple)): raise TypeError("values must be a list of numbers") for v in values: if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v): raise TypeError("values must be finite numbers, received %r" % (v,)) if kind not in ("population", "sample"): raise ValueError('unknown standard deviation kind "%s"' % (kind,)) 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,)) n = len(values) if n == 0: raise ValueError("values must not be empty") if kind == "sample" and n < 2: raise ValueError("sample standard deviation needs at least 2 values") # Plain left-to-right float additions; sum() on ints would be exact and # math.fsum compensated, and either would differ from the other languages. total = 0.0 for v in values: total = total + float(v) mean = total / n squares = 0.0 for v in values: d = float(v) - mean squares = squares + d * d divisor = n - 1 if kind == "sample" else n return _round_to(math.sqrt(squares / divisor), decimals)