// Numerically stable reductions (FP64 reference implementations). // sum_naive is provided only to *measure* error; library code uses sum_neumaier or sum_pairwise. #pragma once #include #include #include #include #include "raid/core/types.hpp" #include "raid/core/error.hpp" namespace raid::stats { template T sum_naive(std::span x) { T s = 0; for (T v : x) s += v; return s; } /// Neumaier's improved Kahan–Babuška summation. template T sum_neumaier(std::span x) { T s = 0, c = 0; for (T v : x) { const T t = s + v; if (std::abs(s) >= std::abs(v)) c += (s - t) + v; else c += (v - t) + s; s = t; } return s + c; } /// Pairwise (cascade) summation with a base-case block of 128; error O(log n) ulp. template T sum_pairwise(std::span x) { if (x.size() <= 128) { T s = 0; for (T v : x) s += v; return s; } const usize m = x.size() / 2; return sum_pairwise(x.subspan(0, m)) + sum_pairwise(x.subspan(m)); } template T dot_neumaier(std::span a, std::span b) { RAID_REQUIRE(a.size() == b.size(), invalid_argument, "dot: size mismatch"); T s = 0, c = 0; for (usize i = 0; i < a.size(); ++i) { const T v = a[i] * b[i], t = s + v; if (std::abs(s) >= std::abs(v)) c += (s - t) + v; else c += (v - t) + s; s = t; } return s + c; } template T sum_squares(std::span x) { return dot_neumaier(x, x); } template T mean(std::span x) { RAID_REQUIRE(!x.empty(), insufficient_samples, "mean of empty span"); return sum_neumaier(x) / static_cast(x.size()); } /// Welford one-pass mean/variance (ddof = 1 by default). struct Welford { usize n{0}; f64 mean{0}; f64 m2{0}; void add(f64 x) { ++n; const f64 d = x - mean; mean += d / static_cast(n); m2 += d * (x - mean); } f64 variance(int ddof = 1) const { return n > static_cast(ddof) ? m2 / static_cast(n - static_cast(ddof)) : std::numeric_limits::quiet_NaN(); } /// Chan–Golub–LeVeque parallel combine. void merge(const Welford& o) { if (o.n == 0) return; if (n == 0) { *this = o; return; } const f64 d = o.mean - mean; const usize nn = n + o.n; mean += d * static_cast(o.n) / static_cast(nn); m2 += o.m2 + d * d * static_cast(n) * static_cast(o.n) / static_cast(nn); n = nn; } }; template T variance(std::span x, int ddof = 1) { Welford w; for (T v : x) w.add(static_cast(v)); return static_cast(w.variance(ddof)); } struct Moments { usize n; f64 mean, variance, skewness, kurtosis /* excess */, min, max; }; /// Two-pass moments; skewness/kurtosis use the biased (population) normalisation g1, g2 − 3 (as scipy with bias=True). Moments moments(std::span x); /// Autocovariance at lag k with the 1/n normalisation (as statsmodels acovf(adjusted=False)). f64 autocovariance(std::span x, usize k, bool demean = true); std::vector autocorrelation(std::span x, usize max_lag); /// Maximum drawdown of a cumulative-return path (fractional, ≥ 0), and the indices of the peak and trough. struct Drawdown { f64 max_drawdown; usize peak_index; usize trough_index; }; Drawdown max_drawdown(std::span cumulative); } // namespace raid::stats