Skip to main content

rustymath_core/
stats.rs

1//! Statistics through the shared reductions and matrix products. Means and norms use the ordinary parallel folds. Variance makes two passes, without an array-sized buffer of deviations. No compensated sums or reproducibility rules are imposed.
2use crate::agreement::Mapping;
3use crate::buffer::Element;
4use crate::error::{Error, Kind};
5use crate::gather::{self, Steps};
6use crate::layout::{Layout, count};
7use crate::linalg::{Inner, Orientation, PlusTimes, Scalar};
8use crate::reduce::{self, Reduction, Sum};
9use crate::{Float, Native};
10use num_traits::{Float as _, One as _, Zero as _};
11use std::borrow::Cow;
12use std::ops::Range;
13
14/// Groups the selected axes into one reduction axis, and keeps the other axes' metadata. One axis, or consecutive axes, needs no copy. Nonconsecutive axes gather through the ordinary transpose plan.
15pub struct Axes { pub dims: [usize; 3], pub layout: Layout, shape: Vec<usize>, steps: Option<Vec<Steps>> }
16
17impl Axes {
18    pub fn new(layout: &Layout, axes: &[usize]) -> Result<Self, Error> {
19        let shape = layout.shape();
20        let mut axes = axes.to_vec();
21        axes.sort_unstable();
22        axes.dedup();
23        if axes.last().is_some_and(|&a| a >= shape.len()) { return Err(Error::new(Kind::Index, "reduction axis is out of range")); }
24        let remaining: Vec<_> = (0..shape.len()).filter(|a| !axes.contains(a)).collect();
25        let result = layout.axes(remaining.iter().copied());
26        let (first, end) = axes.first().zip(axes.last()).map_or((0, 0), |(&a, &b)| (a, b + 1));
27        if end - first == axes.len() {
28            return Ok(Self { dims: [count(&shape[..first]), count(&shape[first..end]), count(&shape[end..])], layout: result, shape: shape.to_vec(), steps: None
29            });
30        }
31        let mut destinations = vec![0; shape.len()];
32        for (to, &from) in remaining.iter().chain(&axes).enumerate() { destinations[from] = to; }
33        let plan = crate::structure::transpose(layout, &destinations)?;
34        Ok(Self { dims: [remaining.iter().map(|&a| shape[a]).product(), axes.iter().map(|&a| shape[a]).product(), 1], layout: result, shape: shape.to_vec(),
35            steps: Some(plan.steps) })
36    }
37
38    pub fn read<'a, T: Element>(&self, data: &'a [T]) -> Cow<'a, [T]> {
39        let Some(steps) = &self.steps else { return Cow::Borrowed(data) };
40        let mut out = Vec::with_capacity(data.len());
41        gather::walk(&mut out, data, 0, steps, T::FILL, gather::Same);
42        Cow::Owned(out)
43    }
44
45    /// The walk that reads what `mapping` reads, in the order the reduction reads its items ([`Mapping::to_walk`]).
46    fn plan(&self, mapping: &Mapping) -> (usize, Vec<Steps>) {
47        let frame: Vec<usize> = self.steps.as_ref().map_or_else(|| self.shape.clone(), |steps| steps.iter().map(Steps::len).collect());
48        let ordered = match &self.steps {
49            Some(steps) => {
50                mapping.composed(&Mapping::Steps { base: 0, steps: steps.clone().into() }, &self.shape, &frame, true).expect("a table always composes")
51            }
52            None => mapping.clone(),
53        };
54        let (base, steps) = ordered.to_walk(&frame);
55        (base, steps.into_owned())
56    }
57}
58
59/// Means along the middle axis of `[outer, len, inner]`. Empty lanes give NaN.
60pub fn mean<T: Scalar>(data: &[T], dims: [usize; 3]) -> Vec<T> where Sum: Reduction<T, Total = T> {
61    let mut out = reduce::along::<T, Sum>(data, dims).0;
62    let count = T::Real::narrow(dims[1] as f64);
63    for v in &mut out { *v = v.div_real(count); }
64    out
65}
66
67/// Sums a mapped array through the same parallel axis traversal as ordinary reductions, materializing only cache-sized chunks.
68#[inline]
69fn mapped<F: Copy, R: Reduction<F>>(dims: [usize; 3], f: impl Fn(Range<usize>, &mut Vec<F>) + Sync) -> Vec<R::Total> {
70    let mut totals = vec![R::unit(); dims[0] * dims[2]];
71    if dims[1] == 0 || totals.is_empty() { return totals; }
72    reduce::fold_pieces::<F, R>(&mut totals, dims, &|range, fold| {
73        let mut work = Vec::with_capacity(crate::CHUNK);
74        for start in (range.start..range.end).step_by(crate::CHUNK) {
75            let end = range.end.min(start + crate::CHUNK);
76            work.clear();
77            f(start..end, &mut work);
78            fold(start, reduce::Part::Items(&work));
79        }
80    });
81    totals
82}
83
84#[inline]
85fn values<T: Copy + Sync, F: Copy, R: Reduction<F>>(data: &[T], dims: [usize; 3], f: impl Fn(T) -> F + Sync) -> Vec<R::Total> {
86    mapped::<F, R>(dims, |range, out| out.extend(data[range].iter().map(|&x| f(x))))
87}
88
89#[inline(always)]
90fn square<T: Scalar>(x: T) -> T::Real { x.re().algebraic_mul(x.re()).algebraic_add(x.im().algebraic_mul(x.im())) }
91
92/// Two-pass variances, real for complex data, with denominator `len - ddof`. Empty lanes and nonpositive denominators give NaN.
93pub fn variance<T: Scalar>(data: &[T], dims: [usize; 3], ddof: usize) -> Vec<T::Real> where Sum: Reduction<T, Total = T> {
94    let means = mean(data, dims);
95    let [_, len, inner] = dims;
96    let mut out = mapped::<T::Real, Sum>(dims, |range, out| {
97        let mut start = range.start;
98        while start < range.end {
99            if inner == 1 {
100                let m = means[start / len];
101                let end = range.end.min((start / len + 1) * len);
102                out.extend(data[start..end].iter().map(#[inline(always)] |&x| square(x - m)));
103                start = end;
104            } else {
105                let m = start / (len * inner) * inner + start % inner;
106                let end = range.end.min((start / inner + 1) * inner);
107                out.extend(data[start..end].iter().enumerate().map(#[inline(always)] |(k, &x)| square(x - means[m + k])));
108                start = end;
109            }
110        }
111    });
112    let count = T::Real::narrow(len.saturating_sub(ddof) as f64);
113    for v in &mut out { *v = if ddof >= len { T::Real::nan() } else { *v / count }; }
114    out
115}
116
117/// Standard deviations, the square roots of [`variance`].
118pub fn stddev<T: Scalar>(data: &[T], dims: [usize; 3], ddof: usize) -> Vec<T::Real> where Sum: Reduction<T, Total = T> {
119    let mut out = variance(data, dims, ddof);
120    for v in &mut out { *v = v.sqrt(); }
121    out
122}
123
124/// The elementwise p-norm along the selected axes: 0 counts nonzero values, ±infinity selects the greatest/least magnitude, and a finite p gives `(sum |x|^p)^(1/p)`. A pair of axes still gives this norm, not a matrix-induced norm. For p = 2, each square is computed and summed in f64, where an f32's square neither overflows nor underflows.
125pub fn norm<T: Scalar>(data: &[T], dims: [usize; 3], p: f64) -> Vec<T::Real> {
126    if p == 1.0 { return values::<_, _, Sum>(data, dims, #[inline(always)] |x| x.modulus()); }
127    if p == 0.0 {
128        return values::<_, _, Sum>(data, dims, #[inline(always)] |x| if x == T::zero() { T::Real::zero() } else { T::Real::one() });
129    }
130    if p == f64::INFINITY { return values::<_, _, reduce::Max>(data, dims, #[inline(always)] |x| x.modulus()); }
131    if p == f64::NEG_INFINITY { return values::<_, _, reduce::Min>(data, dims, #[inline(always)] |x| x.modulus()); }
132    if p == 2.0 { return reduce::along::<T, reduce::Squares>(data, dims).0.into_iter().map(|v| T::Real::narrow(v.sqrt())).collect(); }
133    let mut out = values::<_, _, Sum>(data, dims, #[inline(always)] |x| crate::pow(x.modulus(), T::Real::narrow(p)));
134    for v in &mut out { *v = crate::pow(*v, T::Real::narrow(1.0 / p)); }
135    out
136}
137
138/// Weighted means. The two mappings broadcast `data` and `weights` into an array of `dims`. Weights may be real or complex; a zero total weight gives the ordinary floating-point quotient.
139pub fn average<T: Scalar + Element>(data: &[T], weights: &[T], axes: &Axes, mappings: [&Mapping; 2]) -> Vec<T> where Sum: Reduction<T, Total = T> {
140    let [(xbase, xsteps), (wbase, wsteps)] = mappings.map(|mapping| axes.plan(mapping));
141    let [(xbase, xplan), (wbase, wplan)] = [Steps::simplify(xbase, &xsteps), Steps::simplify(wbase, &wsteps)];
142    let read = |x: &[T], base, plan: &[Cow<Steps>], range: Range<usize>, out: &mut Vec<T>| {
143        gather::walk_plan(out, x, base, plan, T::FILL, range.start, range.len(), gather::Same)
144    };
145    let mut sums = mapped::<_, Sum>(axes.dims, |range, out| {
146        read(data, xbase, &xplan, range.clone(), out);
147        let mut w = Vec::with_capacity(range.len());
148        read(weights, wbase, &wplan, range, &mut w);
149        for (x, w) in out.iter_mut().zip(w) { *x = *x * w; }
150    });
151    let totals = mapped::<_, Sum>(axes.dims, |range, out| read(weights, wbase, &wplan, range, out));
152    for (sum, weight) in sums.iter_mut().zip(totals) { *sum = sum.quotient(weight); }
153    sums
154}
155
156/// The covariance or correlation of a row-major `variables × observations` matrix. The centered Gram product uses the same native/Accelerate product as linear algebra. Complex covariance conjugates the second variable.
157pub fn covariance<T: Scalar>(data: &[T], [rows, cols]: [usize; 2], rowvar: bool, ddof: usize, correlation: bool) -> Vec<T> where Sum: Reduction<T, Total = T>,
158    PlusTimes: Inner<T, Out = T> {
159    let (variables, observations, dims) = if rowvar { (rows, cols, [rows, cols, 1]) } else { (cols, rows, [1, rows, cols]) };
160    let means = mean(data, dims);
161    let mut centered = Vec::with_capacity(data.len());
162    if rowvar { centered.extend_from_slice(data); } else {
163        centered.resize(data.len(), T::zero());
164        crate::linalg::transpose_into(data, cols, &mut centered, rows, [rows, cols], |x| x);
165    }
166    if observations > 0 { for (row, &m) in centered.chunks_mut(observations).zip(&means) { for x in row { *x = *x - m; } } }
167    let conjugate = if !T::COMPLEX { Cow::Borrowed(&centered[..]) } else { Cow::Owned(centered.iter().map(|&x| x.conj()).collect()) };
168    let mut out = PlusTimes.inner_ops(&centered, &conjugate, [Orientation::Normal, Orientation::Transpose], [variables, observations, variables]);
169    let count = T::Real::narrow(observations.saturating_sub(ddof) as f64);
170    if correlation {
171        let scales: Vec<_> = (0..variables).map(|j| out[j * (variables + 1)].re().sqrt()).collect();
172        for (ij, x) in out.iter_mut().enumerate() { *x = x.div_real(scales[ij / variables] * scales[ij % variables]); }
173    } else { for x in &mut out { *x = if ddof >= observations { T::from_real(T::Real::nan()) } else { x.div_real(count) }; } }
174    out
175}
176
177/// Equally spaced histogram edges, from the data's extrema where no range is given. Constant data expands by 0.5 on each side; empty data uses 0 to 1.
178pub fn histogram_edges<F: Native>(data: &[F], bins: usize, range: Option<(f64, f64)>) -> Result<Vec<F>, Error> {
179    if bins == 0 { return Err(Error::new(Kind::Domain, "histogram needs a positive bin count")); }
180    let (mut lo, mut hi) = range.unwrap_or_else(|| if data.is_empty() { (0.0, 1.0) } else { (reduce::min(data).wide(), reduce::max(data).wide()) });
181    if lo == hi {
182        lo -= 0.5;
183        hi += 0.5;
184    }
185    if !lo.is_finite() || !hi.is_finite() || lo >= hi { return Err(Error::new(Kind::Domain, "histogram needs a finite increasing range")); }
186    Ok((0..=bins).map(|j| F::narrow(lo + (hi - lo) * (j as f64 / bins as f64))).collect())
187}
188
189/// Counts in increasing bin edges. Bins include their lower edge and exclude their upper edge, except that the last bin includes both. Values outside the edges and NaNs aren't counted.
190pub fn histogram<F: Native>(data: &[F], edges: &[F]) -> Result<Vec<i64>, Error> {
191    if edges.len() < 2 || edges.windows(2).any(|p| p[0].is_nan() || p[0] >= p[1]) || edges.last().is_some_and(|v| v.is_nan()) {
192        return Err(Error::new(Kind::Domain, "histogram needs at least two strictly increasing bin edges"));
193    }
194    let bins = crate::search::bins(&crate::tolerant::order_keys(edges), &crate::tolerant::order_keys(data));
195    let n = edges.len() - 1;
196    let mut out = vec![0; n];
197    for (&x, b) in data.iter().zip(bins) { let b = b as usize; if b > 0 && b <= n { out[b - 1] += 1; } else if x == edges[n] { out[n - 1] += 1; } }
198    Ok(out)
199}