Skip to main content

alice_analytics/
stats.rs

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