Functional Weave
Code in Rust

charts.histogram-bins@1.0.0

impl/python.py

3,254 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 List, Optional, Sequence

from .charts_histogram_bins_types import Bin, BinMethod
from .charts_ticks_tick_step import tick_spec
from .math_pow import pow as pow_float  ← from math.pow ^1.0.0 · built alongside by fune
from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune
from .stats_percentile import percentile  ← from stats.percentile ^1.0.0 · built alongside by fune

_MAX_BINS = 10000


def histogram_bins(values: Sequence[float], method: BinMethod, bin_width: Optional[float]) -> List[Bin]:
    """Histogram bins on round edges, as d3.bin lays them out. Each bin holds
    x0 <= v < x1; the last also holds its right edge. See the README."""
    if method == "fixed-width":
        if bin_width is None or isinstance(bin_width, bool) or not isinstance(bin_width, (int, float)) or not math.isfinite(bin_width) or bin_width <= 0:
            raise ValueError(f"fixed-width needs a binWidth greater than 0; got {bin_width}")
    elif method in ("sturges", "freedman-diaconis"):
        if bin_width is not None:
            raise ValueError(f"binWidth is only for fixed-width; pass null for {method}")
    else:
        raise ValueError(f'unknown bin method "{method}"')
    n = len(values)
    if n == 0:
        return []
    for v in values:
        if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v):
            raise ValueError(f"values must be finite numbers; got {v}")
    lo = float(min(values))
    hi = float(max(values))

    if method == "fixed-width":
        mul, div = float(bin_width), 1.0
    else:
        if lo == hi:
            return [Bin(x0=lo + 0.0, x1=hi + 0.0, count=n)]
        if method == "sturges":
            # ceil(log2 n) + 1, by doubling rather than a logarithm.
            bits, power = 0, 1
            while power < n:
                power *= 2
                bits += 1
            count = bits + 1
        else:
            # Freedman and Diaconis: bin width 2 IQR n^(-1/3); d3 falls back
            # to one bin when the interquartile range is zero.
            iqr = percentile(values, 75, "linear", 12) - percentile(values, 25, "linear", 12)
            width = 2 * iqr * pow_float(float(n), -1 / 3)
            bins = math.ceil((hi - lo) / width) if width > 0 else 1
            if bins > _MAX_BINS:
                raise ValueError(f"too many bins: more than {_MAX_BINS}")
            count = max(1, bins)
        mul, div = tick_spec(lo, hi, count)

    def edge(i: int) -> float:
        # One exact operation on a whole number, cleaned at 12 places.
        return round_float(i / div if div > 1 else i * mul, 12)

    first = math.floor(lo * div if div > 1 else lo / mul)
    while edge(first) > lo:
        first -= 1
    while edge(first + 1) <= lo:
        first += 1
    last = first + 1
    while edge(last) < hi:
        last += 1
        if last - first > _MAX_BINS:
            raise ValueError(f"too many bins: more than {_MAX_BINS}")

    edges = [edge(i) for i in range(first, last + 1)]
    counts = [0] * (len(edges) - 1)
    for v in values:
        a, b = 0, len(counts) - 1
        while a < b:
            mid = (a + b + 1) // 2
            if edges[mid] <= v:
                a = mid
            else:
                b = mid - 1
        counts[a] += 1
    return [Bin(x0=edges[i], x1=edges[i + 1], count=c) for i, c in enumerate(counts)]