Skip to main content

alice_analytics/
stats.rs

1//! Advanced Statistical Functions
2//!
3//! パーセンタイルランク、IQR、共分散/相関行列、Welford法オンライン統計。
4//! `no_std` 対応、ゼロアロケーション設計。
5
6// a no_std unit-test build links std (libtest), whose inherent methods win;
7// FloatExt only covers correctly rounded operations, so the bits are the same
8#[cfg(not(any(feature = "std", test)))]
9use crate::math::FloatExt;
10use crate::math::{f64_usize, u64_f64, usize_f64};
11
12// ============================================================================
13// Percentile Rank
14// ============================================================================
15
16/// ソート済み配列中の値のパーセンタイルランク。
17///
18/// `value` が `sorted_data` 中で何パーセント以下にあるかを返す `[0.0, 100.0]`。
19#[inline]
20#[must_use]
21pub fn percentile_rank(sorted_data: &[f64], value: f64) -> f64 {
22    if sorted_data.is_empty() {
23        return 0.0;
24    }
25    let mut count_below = 0usize;
26    let mut count_equal = 0usize;
27    for &v in sorted_data {
28        if v < value {
29            count_below += 1;
30        } else if (v - value).abs() < f64::EPSILON {
31            count_equal += 1;
32        }
33    }
34    // パーセンタイルランク = (below + 0.5 * equal) / N * 100
35    let n = usize_f64(sorted_data.len());
36    0.5f64.mul_add(usize_f64(count_equal), usize_f64(count_below)) / n * 100.0
37}
38
39// ============================================================================
40// IQR (Interquartile Range)
41// ============================================================================
42
43/// IQR結果。
44#[derive(Clone, Copy, Debug)]
45pub struct IqrResult {
46    /// 第1四分位数 (Q1)。
47    pub q1: f64,
48    /// 中央値 (Q2)。
49    pub median: f64,
50    /// 第3四分位数 (Q3)。
51    pub q3: f64,
52    /// IQR = Q3 - Q1。
53    pub iqr: f64,
54    /// 下限 = Q1 - 1.5 * IQR。
55    pub lower_fence: f64,
56    /// 上限 = Q3 + 1.5 * IQR。
57    pub upper_fence: f64,
58}
59
60/// ソート済み配列のIQRを計算。
61///
62/// 線形補間でQ1/Q3を算出。
63#[must_use]
64pub fn iqr(sorted_data: &[f64]) -> Option<IqrResult> {
65    let n = sorted_data.len();
66    if n < 4 {
67        return None;
68    }
69
70    let q1 = quantile_sorted(sorted_data, 0.25);
71    let median = quantile_sorted(sorted_data, 0.50);
72    let q3 = quantile_sorted(sorted_data, 0.75);
73    let iqr_val = q3 - q1;
74
75    Some(IqrResult {
76        q1,
77        median,
78        q3,
79        iqr: iqr_val,
80        lower_fence: 1.5f64.mul_add(-iqr_val, q1),
81        upper_fence: 1.5f64.mul_add(iqr_val, q3),
82    })
83}
84
85/// ソート済み配列の任意パーセンタイル(線形補間)。
86#[inline]
87#[must_use]
88pub fn quantile_sorted(sorted_data: &[f64], q: f64) -> f64 {
89    let n = sorted_data.len();
90    if n == 0 {
91        return 0.0;
92    }
93    if n == 1 {
94        return sorted_data[0];
95    }
96    let q = q.clamp(0.0, 1.0);
97    let pos = q * usize_f64(n - 1);
98    let lower = f64_usize(pos.floor());
99    let upper = f64_usize(pos.ceil());
100    if lower == upper {
101        return sorted_data[lower];
102    }
103    let frac = pos - usize_f64(lower);
104    sorted_data[lower].mul_add(1.0 - frac, sorted_data[upper] * frac)
105}
106
107// ============================================================================
108// Covariance & Correlation Matrix
109// ============================================================================
110
111/// 共分散行列(最大8変数)。
112///
113/// Welford法のオンライン計算。
114pub struct CovarianceMatrix<const D: usize> {
115    /// 観測数。
116    count: u64,
117    /// 各変数の平均。
118    means: [f64; D],
119    /// co-moment行列 C[i][j](上三角 + 対角)。
120    co_moments: [[f64; D]; D],
121}
122
123impl<const D: usize> CovarianceMatrix<D> {
124    /// 新しい共分散行列を初期化。
125    #[must_use]
126    pub const fn new() -> Self {
127        Self {
128            count: 0,
129            means: [0.0; D],
130            co_moments: [[0.0; D]; D],
131        }
132    }
133
134    /// 観測値ベクトルを追加(Welfordオンラインアルゴリズム)。
135    pub fn observe(&mut self, values: &[f64; D]) {
136        self.count += 1;
137        let n = u64_f64(self.count);
138
139        let mut dx = [0.0; D];
140        for (i, d) in dx.iter_mut().enumerate() {
141            *d = values[i] - self.means[i];
142        }
143
144        // 平均値更新
145        for (i, mean) in self.means.iter_mut().enumerate() {
146            *mean += dx[i] / n;
147        }
148
149        // co-moment更新
150        for (i, (&val, &mean_i)) in values.iter().zip(self.means.iter()).enumerate() {
151            let dx2_i = val - mean_i;
152            for (j, dx_j) in dx.iter().enumerate().skip(i) {
153                self.co_moments[i][j] += dx_j * dx2_i;
154            }
155        }
156    }
157
158    /// 共分散 Cov(i, j)。
159    #[must_use]
160    pub fn covariance(&self, i: usize, j: usize) -> f64 {
161        if self.count < 2 || i >= D || j >= D {
162            return 0.0;
163        }
164        let (r, c) = if i <= j { (i, j) } else { (j, i) };
165        self.co_moments[r][c] / u64_f64(self.count - 1)
166    }
167
168    /// 相関係数 r(i, j)。
169    #[must_use]
170    pub fn correlation(&self, i: usize, j: usize) -> f64 {
171        if i == j {
172            return 1.0;
173        }
174        let cov = self.covariance(i, j);
175        let var_i = self.covariance(i, i);
176        let var_j = self.covariance(j, j);
177        let denom = (var_i * var_j).sqrt();
178        if denom < f64::EPSILON {
179            return 0.0;
180        }
181        (cov / denom).clamp(-1.0, 1.0)
182    }
183
184    /// 変数iの分散。
185    #[inline]
186    #[must_use]
187    pub fn variance(&self, i: usize) -> f64 {
188        self.covariance(i, i)
189    }
190
191    /// 変数iの平均。
192    #[inline]
193    #[must_use]
194    pub const fn mean(&self, i: usize) -> f64 {
195        if i < D {
196            self.means[i]
197        } else {
198            0.0
199        }
200    }
201
202    /// 観測数。
203    #[inline]
204    #[must_use]
205    pub const fn count(&self) -> u64 {
206        self.count
207    }
208}
209
210impl<const D: usize> Default for CovarianceMatrix<D> {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216// ============================================================================
217// Streaming Stats (Welford拡張)
218// ============================================================================
219
220/// オンラインストリーミング統計。
221///
222/// Welford法で mean/variance/skewness/kurtosis をO(1)メモリで追跡。
223#[derive(Clone, Debug)]
224pub struct StreamingStats {
225    count: u64,
226    mean: f64,
227    m2: f64,
228    m3: f64,
229    m4: f64,
230    min: f64,
231    max: f64,
232}
233
234impl StreamingStats {
235    /// 新しい `StreamingStats` を作成。
236    #[must_use]
237    pub const fn new() -> Self {
238        Self {
239            count: 0,
240            mean: 0.0,
241            m2: 0.0,
242            m3: 0.0,
243            m4: 0.0,
244            min: f64::INFINITY,
245            max: f64::NEG_INFINITY,
246        }
247    }
248
249    /// 観測値を追加。
250    pub fn observe(&mut self, value: f64) {
251        let n1 = self.count;
252        self.count += 1;
253        let n = u64_f64(self.count);
254
255        let delta = value - self.mean;
256        let delta_n = delta / n;
257        let delta_n2 = delta_n * delta_n;
258        let term1 = delta * delta_n * u64_f64(n1);
259
260        self.mean += delta_n;
261
262        // 4次モーメント更新(Pebay 2008)
263        self.m4 += (4.0 * delta_n).mul_add(
264            -self.m3,
265            (term1 * delta_n2).mul_add(n.mul_add(n, -(3.0 * n)) + 3.0, 6.0 * delta_n2 * self.m2),
266        );
267        self.m3 += (term1 * delta_n).mul_add(n - 2.0, -(3.0 * delta_n * self.m2));
268        self.m2 += term1;
269
270        self.min = alice_det_math::min64(self.min, value);
271        self.max = alice_det_math::max64(self.max, value);
272    }
273
274    /// 観測数。
275    #[inline]
276    #[must_use]
277    pub const fn count(&self) -> u64 {
278        self.count
279    }
280
281    /// 平均値。
282    #[inline]
283    #[must_use]
284    pub const fn mean(&self) -> f64 {
285        self.mean
286    }
287
288    /// 母集団分散。
289    #[must_use]
290    pub fn variance(&self) -> f64 {
291        if self.count < 2 {
292            return 0.0;
293        }
294        self.m2 / u64_f64(self.count)
295    }
296
297    /// 標本分散。
298    #[must_use]
299    pub fn sample_variance(&self) -> f64 {
300        if self.count < 2 {
301            return 0.0;
302        }
303        self.m2 / u64_f64(self.count - 1)
304    }
305
306    /// 標準偏差。
307    #[inline]
308    #[must_use]
309    pub fn std_dev(&self) -> f64 {
310        self.variance().sqrt()
311    }
312
313    /// 歪度 (Skewness)。
314    #[must_use]
315    pub fn skewness(&self) -> f64 {
316        if self.count < 3 || self.m2 < f64::EPSILON {
317            return 0.0;
318        }
319        let n = u64_f64(self.count);
320        // m2^1.5 = m2 * sqrt(m2): both operations are IEEE-exact, unlike powf
321        n.sqrt() * self.m3 / (self.m2 * self.m2.sqrt())
322    }
323
324    /// 尖度 (Excess Kurtosis)。
325    #[must_use]
326    pub fn kurtosis(&self) -> f64 {
327        if self.count < 4 || self.m2 < f64::EPSILON {
328            return 0.0;
329        }
330        let n = u64_f64(self.count);
331        n * self.m4 / (self.m2 * self.m2) - 3.0
332    }
333
334    /// 最小値。
335    #[inline]
336    #[must_use]
337    pub const fn min(&self) -> f64 {
338        self.min
339    }
340
341    /// 最大値。
342    #[inline]
343    #[must_use]
344    pub const fn max(&self) -> f64 {
345        self.max
346    }
347
348    /// リセット。
349    pub const fn reset(&mut self) {
350        self.count = 0;
351        self.mean = 0.0;
352        self.m2 = 0.0;
353        self.m3 = 0.0;
354        self.m4 = 0.0;
355        self.min = f64::INFINITY;
356        self.max = f64::NEG_INFINITY;
357    }
358}
359
360impl Default for StreamingStats {
361    fn default() -> Self {
362        Self::new()
363    }
364}
365
366// ============================================================================
367// Tests
368// ============================================================================
369
370#[cfg(test)]
371#[allow(clippy::float_cmp)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn percentile_rank_basic() {
377        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
378        let rank = percentile_rank(&data, 3.0);
379        // (2 below + 0.5 * 1 equal) / 5 * 100 = 50%
380        assert!((rank - 50.0).abs() < 1e-6);
381    }
382
383    #[test]
384    fn percentile_rank_empty() {
385        assert!((percentile_rank(&[], 5.0) - 0.0).abs() < 1e-6);
386    }
387
388    #[test]
389    fn percentile_rank_below_all() {
390        let data = [10.0, 20.0, 30.0];
391        let rank = percentile_rank(&data, 1.0);
392        assert!((rank - 0.0).abs() < 1e-6);
393    }
394
395    #[test]
396    fn percentile_rank_above_all() {
397        let data = [10.0, 20.0, 30.0];
398        let rank = percentile_rank(&data, 50.0);
399        assert!((rank - 100.0).abs() < 1e-6);
400    }
401
402    #[test]
403    fn iqr_basic() {
404        // データ: 1,2,3,4,5,6,7,8
405        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
406        let result = iqr(&data).unwrap();
407        // Q1=2.75, Q2=4.5, Q3=6.25, IQR=3.5
408        assert!((result.q1 - 2.75).abs() < 1e-6);
409        assert!((result.median - 4.5).abs() < 1e-6);
410        assert!((result.q3 - 6.25).abs() < 1e-6);
411        assert!((result.iqr - 3.5).abs() < 1e-6);
412    }
413
414    #[test]
415    fn iqr_too_small() {
416        let data = [1.0, 2.0, 3.0];
417        assert!(iqr(&data).is_none());
418    }
419
420    #[test]
421    fn iqr_fences() {
422        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
423        let result = iqr(&data).unwrap();
424        assert!(result.lower_fence < result.q1);
425        assert!(result.upper_fence > result.q3);
426    }
427
428    #[test]
429    fn quantile_sorted_edges() {
430        let data = [10.0, 20.0, 30.0];
431        assert!((quantile_sorted(&data, 0.0) - 10.0).abs() < 1e-6);
432        assert!((quantile_sorted(&data, 1.0) - 30.0).abs() < 1e-6);
433        assert!((quantile_sorted(&data, 0.5) - 20.0).abs() < 1e-6);
434    }
435
436    #[test]
437    fn covariance_matrix_basic() {
438        let mut cov = CovarianceMatrix::<2>::new();
439        // 完全正相関: x=y
440        for i in 0..100 {
441            let v = f64::from(i);
442            cov.observe(&[v, v]);
443        }
444        let r = cov.correlation(0, 1);
445        assert!((r - 1.0).abs() < 1e-6);
446    }
447
448    #[test]
449    fn covariance_matrix_negative_correlation() {
450        let mut cov = CovarianceMatrix::<2>::new();
451        // 完全負相関: x = -y
452        for i in 0..100 {
453            let v = f64::from(i);
454            cov.observe(&[v, -v]);
455        }
456        let r = cov.correlation(0, 1);
457        assert!((r - (-1.0)).abs() < 1e-6);
458    }
459
460    #[test]
461    fn covariance_matrix_self_correlation() {
462        let cov = CovarianceMatrix::<3>::new();
463        // 自己相関は常に1.0
464        assert!((cov.correlation(0, 0) - 1.0).abs() < 1e-6);
465    }
466
467    #[test]
468    fn covariance_matrix_variance() {
469        let mut cov = CovarianceMatrix::<2>::new();
470        // 0,1,2,...,99 → sample variance = 833.25
471        for i in 0..100 {
472            cov.observe(&[f64::from(i), 0.0]);
473        }
474        let var = cov.variance(0);
475        // Welford co-moment / (n-1) で標本分散
476        // 0..100 の標本分散 = 100*99/12 = 833.333...
477        assert!(var > 800.0 && var < 870.0, "var = {var}");
478    }
479
480    #[test]
481    fn streaming_stats_basic() {
482        let mut s = StreamingStats::new();
483        for v in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
484            s.observe(v);
485        }
486        assert_eq!(s.count(), 8);
487        assert!((s.mean() - 5.0).abs() < 1e-6);
488        assert!((s.min() - 2.0).abs() < 1e-6);
489        assert!((s.max() - 9.0).abs() < 1e-6);
490    }
491
492    #[test]
493    fn streaming_stats_variance() {
494        let mut s = StreamingStats::new();
495        for v in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
496            s.observe(v);
497        }
498        // 母集団分散 = 4.0 (厳密値)
499        assert!((s.variance() - 4.0).abs() < 1e-6);
500    }
501
502    #[test]
503    fn streaming_stats_empty() {
504        let s = StreamingStats::new();
505        assert_eq!(s.count(), 0);
506        assert!((s.mean() - 0.0).abs() < 1e-6);
507        assert!((s.variance() - 0.0).abs() < 1e-6);
508        assert!((s.skewness() - 0.0).abs() < 1e-6);
509    }
510
511    #[test]
512    fn streaming_stats_reset() {
513        let mut s = StreamingStats::new();
514        s.observe(10.0);
515        s.observe(20.0);
516        s.reset();
517        assert_eq!(s.count(), 0);
518    }
519
520    #[test]
521    fn streaming_stats_skewness_symmetric() {
522        let mut s = StreamingStats::new();
523        // 対称分布 → skewness ≈ 0
524        for v in [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] {
525            s.observe(v);
526        }
527        assert!(s.skewness().abs() < 0.1);
528    }
529
530    #[test]
531    fn streaming_stats_kurtosis_uniform() {
532        let mut s = StreamingStats::new();
533        // 一様分布の尖度は約-1.2
534        for i in 0..1000 {
535            s.observe(f64::from(i));
536        }
537        assert!(s.kurtosis() < 0.0);
538    }
539
540    #[test]
541    fn streaming_stats_default() {
542        let s = StreamingStats::default();
543        assert_eq!(s.count(), 0);
544    }
545
546    #[test]
547    fn covariance_matrix_default() {
548        let cov = CovarianceMatrix::<4>::default();
549        assert_eq!(cov.count(), 0);
550    }
551}