SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
3.9 KB · 83 lines python
Raw Blame History
1"""Deterministic statistical utilities (src/countryatlas/stats.py)."""2from __future__ import annotations34import math56import pytest78from countryatlas import stats91011def test_growth_helpers():12    assert stats.yoy(110, 100) == pytest.approx(10.0)13    assert stats.yoy(90, -100) == pytest.approx(190.0)  # relative to |prev|14    assert stats.yoy(1, 0) is None and stats.yoy(None, 1) is None15    assert stats.cagr(100, 200, 10) == pytest.approx((2 ** 0.1 - 1) * 100)16    assert stats.cagr(0, 5, 3) is None and stats.cagr(5, 5, 0) is None17    assert stats.rolling_mean([1, 2, 3, None, 5], 2) == [None, 1.5, 2.5, None, None]18    assert stats.trend_slope([2000, 2001, 2002, 2003], [1, 3, 5, 7]) == pytest.approx(2.0)19    assert stats.trend_slope([2000, 2001], [1, 2]) is None20    assert stats.volatility([1, 1, 1, 1]) == pytest.approx(0.0)21    assert stats.volatility([100, 110, 121], relative=True) == pytest.approx(0.0, abs=1e-9)222324def test_ranks_and_percentiles():25    assert stats.rank([3, 1, None, 3, 2]) == [1, 4, None, 1, 3]26    assert stats.rank([3, 1, 2], descending=False) == [3, 1, 2]27    pct = stats.percentile_rank([10, 20, 30, None])28    assert pct[:3] == [0.0, 50.0, 100.0] and pct[3] is None29    tie = stats.percentile_rank([1, 2, 2, 3])30    assert tie[1] == tie[2] == pytest.approx(50.0)31    z = stats.zscore([1, 2, 3])32    assert z[1] == pytest.approx(0.0) and z[2] == pytest.approx(math.sqrt(1.5))33    assert stats.zscore([5, 5, 5]) == [None, None, None]34    rz = stats.robust_zscore([1, 2, 3, 4, 100])35    assert rz[4] > 10 and abs(rz[2]) < 1e-936    assert stats.median([3, None, 1, 2]) == 2.037    assert stats.weighted_mean([10, 20], [1, 3]) == pytest.approx(17.5)38    assert stats.weighted_mean([10, 20], [0, None]) is None394041def test_records_and_breaks():42    assert stats.is_record_high([1, 2, 3]) and not stats.is_record_high([3, 2, 1]) and not stats.is_record_high([3])43    assert stats.is_record_low([3, 2, 1]) and not stats.is_record_low([1, 2, 3])44    series = [10] * 8 + [20] * 845    b = stats.structural_break(series, min_segment=5)46    assert b is not None and b["index"] == 8 and b["gain"] == pytest.approx(1.0) and b["shift"] == pytest.approx(10.0)47    assert stats.structural_break([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) is None  # flat48    assert stats.structural_break([1, 2, 3], min_segment=5) is None  # too short49    assert stats.persistent_reversal([5, 4, 3, 2, 3, 4, 5]) == "up"50    assert stats.persistent_reversal([1, 2, 3, 4, 3, 2, 1]) == "down"51    assert stats.persistent_reversal([1, 2, 3, 4, 5, 6, 7]) is None525354def test_association():55    x = [1, 2, 3, 4, 5]56    r, n = stats.pearson(x, [2, 4, 6, 8, 10])57    assert r == pytest.approx(1.0) and n == 558    r, n = stats.pearson(x, [5, 4, 3, 2, 1])59    assert r == pytest.approx(-1.0)60    assert stats.pearson(x, [1, 1, 1, 1, 1]) == (None, 5)61    rho, _ = stats.spearman(x, [1, 10, 100, 1000, 10000])62    assert rho == pytest.approx(1.0)63    fit = stats.ols(x, [3, 5, 7, 9, 11])64    assert fit["slope"] == pytest.approx(2.0) and fit["intercept"] == pytest.approx(1.0) and fit["r2"] == pytest.approx(1.0)65    ts = stats.theil_sen(x, [3, 5, 7, 9, 100])  # one outlier does not move the robust slope66    assert ts["slope"] == pytest.approx(2.0) and ts["intercept"] == pytest.approx(1.0)67    assert stats.ols([1, 1, 1], [1, 2, 3]) is None686970def test_breaks_histogram_convergence():71    br = stats.quantile_breaks(list(range(1, 101)), 4)72    assert len(br) == 3 and br[1] == pytest.approx(50.5)73    assert stats.quantile_breaks([1], 5) == []74    h = stats.histogram([1, 2, 3, 4, 5], bins=5)75    assert sum(h["counts"]) == 5 and len(h["edges"]) == 676    hl = stats.histogram([1, 10, 100, 1000], bins=3, log=True)77    assert hl["log"] and hl["edges"][0] == pytest.approx(1.0) and hl["edges"][-1] == pytest.approx(1000.0)78    start = [10 + i for i in range(20)]79    end = [20 + i * 0.2 for i in range(20)]80    c = stats.convergence(start, end)81    assert c["direction"] == "converging" and c["n"] == 2082    assert stats.convergence([1, 2], [1, 2]) is None83