Skip to main content

alice_analytics/
streaming_ops.rs

1//! Streaming Operators — 変化率・移動平均・線形回帰
2//!
3//! ストリーミングデータに対するオンライン演算子。
4//! ゼロアロケーション、`no_std` 対応。
5//!
6//! Author: Moroya Sakamoto
7
8// a no_std unit-test build links std (libtest), whose inherent methods win;
9// FloatExt only covers correctly rounded operations, so the bits are the same
10#[cfg(not(any(feature = "std", test)))]
11use crate::math::FloatExt;
12use crate::math::{u64_f64, usize_f64};
13
14// ============================================================================
15// Change Rate — 変化率
16// ============================================================================
17
18/// 変化率の計算(差分 / 時間間隔)。
19///
20/// 連続する観測値間の瞬時変化率を追跡。
21/// 単位時間あたりの変化量を返す。
22#[derive(Clone, Debug)]
23pub struct ChangeRate {
24    /// 前回の値。
25    prev_value: f64,
26    /// 前回のタイムスタンプ(ミリ秒)。
27    prev_ts: u64,
28    /// 最新の変化率。
29    rate: f64,
30    /// 初回観測済みフラグ。
31    initialized: bool,
32}
33
34impl ChangeRate {
35    /// 新しい `ChangeRate` を作成。
36    #[must_use]
37    pub const fn new() -> Self {
38        Self {
39            prev_value: 0.0,
40            prev_ts: 0,
41            rate: 0.0,
42            initialized: false,
43        }
44    }
45
46    /// 観測値を追加し、変化率を更新。
47    ///
48    /// 戻り値: 変化率が計算できた場合 `Some(rate)`。
49    /// 初回観測時は `None`。
50    pub fn observe(&mut self, value: f64, timestamp_ms: u64) -> Option<f64> {
51        if !self.initialized {
52            self.prev_value = value;
53            self.prev_ts = timestamp_ms;
54            self.initialized = true;
55            return None;
56        }
57
58        let dt = timestamp_ms.saturating_sub(self.prev_ts);
59        if dt == 0 {
60            return Some(self.rate);
61        }
62
63        self.rate = (value - self.prev_value) / u64_f64(dt);
64        self.prev_value = value;
65        self.prev_ts = timestamp_ms;
66        Some(self.rate)
67    }
68
69    /// 最新の変化率。
70    #[inline]
71    #[must_use]
72    pub const fn rate(&self) -> f64 {
73        self.rate
74    }
75
76    /// 初期化済みかどうか。
77    #[inline]
78    #[must_use]
79    pub const fn is_initialized(&self) -> bool {
80        self.initialized
81    }
82
83    /// リセット。
84    pub const fn reset(&mut self) {
85        self.prev_value = 0.0;
86        self.prev_ts = 0;
87        self.rate = 0.0;
88        self.initialized = false;
89    }
90}
91
92impl Default for ChangeRate {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98// ============================================================================
99// Exponential Moving Average (EMA)
100// ============================================================================
101
102/// 指数移動平均 (EMA)。
103///
104/// `alpha` が大きいほど最新の値に重みが偏る。
105/// `EMA_t = alpha * value + (1 - alpha) * EMA_{t-1}`
106#[derive(Clone, Debug)]
107pub struct ExponentialMovingAverage {
108    /// 平滑化係数 (0.0 < alpha <= 1.0)。
109    alpha: f64,
110    /// 現在のEMA値。
111    value: f64,
112    /// 観測数。
113    count: u64,
114}
115
116impl ExponentialMovingAverage {
117    /// 指定の `alpha` 値で EMA を作成。
118    ///
119    /// # Panics
120    ///
121    /// `alpha` が 0.0 以下または 1.0 より大きい場合。
122    #[must_use]
123    pub fn new(alpha: f64) -> Self {
124        assert!(alpha > 0.0 && alpha <= 1.0, "alpha must be in (0.0, 1.0]");
125        Self {
126            alpha,
127            value: 0.0,
128            count: 0,
129        }
130    }
131
132    /// スパン(期間数)からEMAを作成。
133    ///
134    /// `alpha = 2 / (span + 1)`
135    ///
136    /// # Panics
137    ///
138    /// `span` が 0 の場合。
139    #[must_use]
140    pub fn from_span(span: u64) -> Self {
141        assert!(span > 0, "span must be > 0");
142        Self::new(2.0 / (u64_f64(span) + 1.0))
143    }
144
145    /// 観測値を追加。
146    pub fn observe(&mut self, value: f64) {
147        if self.count == 0 {
148            self.value = value;
149        } else {
150            self.value = self.alpha.mul_add(value, (1.0 - self.alpha) * self.value);
151        }
152        self.count += 1;
153    }
154
155    /// 現在のEMA値。
156    #[inline]
157    #[must_use]
158    pub const fn value(&self) -> f64 {
159        self.value
160    }
161
162    /// 観測数。
163    #[inline]
164    #[must_use]
165    pub const fn count(&self) -> u64 {
166        self.count
167    }
168
169    /// alpha値。
170    #[inline]
171    #[must_use]
172    pub const fn alpha(&self) -> f64 {
173        self.alpha
174    }
175
176    /// リセット。
177    pub const fn reset(&mut self) {
178        self.value = 0.0;
179        self.count = 0;
180    }
181}
182
183// ============================================================================
184// Simple Moving Average (SMA) — 固定窓
185// ============================================================================
186
187/// 固定窓の単純移動平均 (SMA)。
188///
189/// リングバッファで最新N個の観測値を保持。
190pub struct SimpleMovingAverage<const N: usize> {
191    /// リングバッファ。
192    buffer: [f64; N],
193    /// 書き込み位置。
194    write_pos: usize,
195    /// 有効な要素数。
196    count: usize,
197    /// 合計値。
198    sum: f64,
199}
200
201impl<const N: usize> SimpleMovingAverage<N> {
202    /// 新しいSMAを作成。
203    #[must_use]
204    pub const fn new() -> Self {
205        Self {
206            buffer: [0.0; N],
207            write_pos: 0,
208            count: 0,
209            sum: 0.0,
210        }
211    }
212
213    /// 観測値を追加。
214    ///
215    /// # Panics
216    ///
217    /// `N == 0` の場合 (幅 0 の移動平均は定義できない) 型引数の前提違反なので
218    /// `Result` にせず panic する `SimpleMovingAverage::<0>::new()` 自体は成功し、
219    /// 最初の `observe` で panic する 契約は `tests/panic_contract.rs` が pin する
220    pub fn observe(&mut self, value: f64) {
221        assert!(
222            N > 0,
223            "SimpleMovingAverage needs N > 0 (got a zero-width window)"
224        );
225        if self.count >= N {
226            self.sum -= self.buffer[self.write_pos];
227        }
228        self.buffer[self.write_pos] = value;
229        self.sum += value;
230        self.write_pos = (self.write_pos + 1) % N;
231        if self.count < N {
232            self.count += 1;
233        }
234    }
235
236    /// 現在の平均値。
237    #[inline]
238    #[must_use]
239    pub fn value(&self) -> f64 {
240        if self.count == 0 {
241            return 0.0;
242        }
243        self.sum / usize_f64(self.count)
244    }
245
246    /// 有効な要素数。
247    #[inline]
248    #[must_use]
249    pub const fn len(&self) -> usize {
250        self.count
251    }
252
253    /// 空かどうか。
254    #[inline]
255    #[must_use]
256    pub const fn is_empty(&self) -> bool {
257        self.count == 0
258    }
259
260    /// ウィンドウが満杯かどうか。
261    #[inline]
262    #[must_use]
263    pub const fn is_full(&self) -> bool {
264        self.count >= N
265    }
266
267    /// リセット。
268    pub const fn reset(&mut self) {
269        self.write_pos = 0;
270        self.count = 0;
271        self.sum = 0.0;
272    }
273}
274
275impl<const N: usize> Default for SimpleMovingAverage<N> {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281// ============================================================================
282// Online Linear Regression
283// ============================================================================
284
285/// オンライン線形回帰 (y = slope * x + intercept)。
286///
287/// Welford法ベースの逐次更新で、O(1) メモリ。
288/// x, y の平均・分散・共分散から最小二乗法のパラメータを算出。
289#[derive(Clone, Debug)]
290pub struct LinearRegression {
291    /// 観測数。
292    count: u64,
293    /// xの平均。
294    mean_x: f64,
295    /// yの平均。
296    mean_y: f64,
297    /// co-moment: `Σ(x_i - mean_x)(y_i - mean_y)`.
298    co_moment_xy: f64,
299    /// x の二次モーメント: `Σ(x_i - mean_x)^2`.
300    m2_x: f64,
301}
302
303impl LinearRegression {
304    /// 新しい線形回帰を作成。
305    #[must_use]
306    pub const fn new() -> Self {
307        Self {
308            count: 0,
309            mean_x: 0.0,
310            mean_y: 0.0,
311            co_moment_xy: 0.0,
312            m2_x: 0.0,
313        }
314    }
315
316    /// 観測値 (x, y) を追加。
317    pub fn observe(&mut self, x: f64, y: f64) {
318        self.count += 1;
319        let n = u64_f64(self.count);
320
321        let dx = x - self.mean_x;
322        let dy = y - self.mean_y;
323
324        self.mean_x += dx / n;
325        self.mean_y += dy / n;
326
327        // 更新後のdx'
328        let dx2 = x - self.mean_x;
329        self.co_moment_xy += dx * (y - self.mean_y);
330        self.m2_x += dx * dx2;
331    }
332
333    /// 傾き (slope)。
334    #[must_use]
335    pub fn slope(&self) -> f64 {
336        if self.count < 2 || self.m2_x.abs() < f64::EPSILON {
337            return 0.0;
338        }
339        self.co_moment_xy / self.m2_x
340    }
341
342    /// 切片 (intercept)。
343    #[must_use]
344    pub fn intercept(&self) -> f64 {
345        self.slope().mul_add(-self.mean_x, self.mean_y)
346    }
347
348    /// 予測値: y = slope * x + intercept。
349    #[must_use]
350    pub fn predict(&self, x: f64) -> f64 {
351        self.slope().mul_add(x, self.intercept())
352    }
353
354    /// 決定係数 R²。
355    #[must_use]
356    pub const fn r_squared(&self) -> f64 {
357        if self.count < 2 {
358            return 0.0;
359        }
360        // R² = (co_moment_xy)² / (m2_x * m2_y)
361        // m2_y を別途計算する必要があるため、相関係数の二乗で近似
362        // ここでは slope * co_moment / m2_y で計算
363        // 簡易版: R² = slope * Sxy / Syy だが Syy を持たないため
364        // slope² * Var(x) / Var(y) を使う
365        // → co_moment_xy² / (m2_x * m2_y) が必要
366        // m2_y を持たないので、追跡を追加せずに slope² * m2_x / m2_y は不可
367        // 代替: 常に slope * co_moment_xy / m2_x * co_moment_xy / (m2_y) ...
368        // → 簡潔さのため m2_y も追跡する
369        0.0 // r_squared_full() を使用すること
370    }
371
372    /// 観測数。
373    #[inline]
374    #[must_use]
375    pub const fn count(&self) -> u64 {
376        self.count
377    }
378
379    /// リセット。
380    pub const fn reset(&mut self) {
381        self.count = 0;
382        self.mean_x = 0.0;
383        self.mean_y = 0.0;
384        self.co_moment_xy = 0.0;
385        self.m2_x = 0.0;
386    }
387}
388
389impl Default for LinearRegression {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395/// R²も追跡するオンライン線形回帰。
396#[derive(Clone, Debug)]
397pub struct LinearRegressionFull {
398    /// 基本回帰。
399    inner: LinearRegression,
400    /// y の二次モーメント: `Σ(y_i - mean_y)^2`.
401    m2_y: f64,
402}
403
404impl LinearRegressionFull {
405    /// 新しい線形回帰を作成。
406    #[must_use]
407    pub const fn new() -> Self {
408        Self {
409            inner: LinearRegression::new(),
410            m2_y: 0.0,
411        }
412    }
413
414    /// 観測値 (x, y) を追加。
415    pub fn observe(&mut self, x: f64, y: f64) {
416        let dy = y - self.inner.mean_y;
417        self.inner.observe(x, y);
418        let dy2 = y - self.inner.mean_y;
419        self.m2_y += dy * dy2;
420    }
421
422    /// 傾き。
423    #[must_use]
424    pub fn slope(&self) -> f64 {
425        self.inner.slope()
426    }
427
428    /// 切片。
429    #[must_use]
430    pub fn intercept(&self) -> f64 {
431        self.inner.intercept()
432    }
433
434    /// 予測値。
435    #[must_use]
436    pub fn predict(&self, x: f64) -> f64 {
437        self.inner.predict(x)
438    }
439
440    /// 決定係数 R²。
441    #[must_use]
442    pub fn r_squared(&self) -> f64 {
443        if self.inner.count < 2
444            || self.inner.m2_x.abs() < f64::EPSILON
445            || self.m2_y.abs() < f64::EPSILON
446        {
447            return 0.0;
448        }
449        let r = self.inner.co_moment_xy / (self.inner.m2_x * self.m2_y).sqrt();
450        (r * r).clamp(0.0, 1.0)
451    }
452
453    /// 観測数。
454    #[inline]
455    #[must_use]
456    pub const fn count(&self) -> u64 {
457        self.inner.count
458    }
459
460    /// リセット。
461    pub const fn reset(&mut self) {
462        self.inner.reset();
463        self.m2_y = 0.0;
464    }
465}
466
467impl Default for LinearRegressionFull {
468    fn default() -> Self {
469        Self::new()
470    }
471}
472
473// ============================================================================
474// Tests
475// ============================================================================
476
477#[cfg(test)]
478#[allow(clippy::float_cmp)]
479mod tests {
480    use super::*;
481
482    // --- ChangeRate ---
483
484    #[test]
485    fn change_rate_first_observation() {
486        let mut cr = ChangeRate::new();
487        assert!(cr.observe(10.0, 1000).is_none());
488        assert!(cr.is_initialized());
489    }
490
491    #[test]
492    fn change_rate_constant() {
493        let mut cr = ChangeRate::new();
494        cr.observe(10.0, 0);
495        let rate = cr.observe(10.0, 100).unwrap();
496        assert!((rate - 0.0).abs() < 1e-10);
497    }
498
499    #[test]
500    fn change_rate_linear() {
501        let mut cr = ChangeRate::new();
502        cr.observe(0.0, 0);
503        // 100ms で 50 増加 → rate = 0.5/ms
504        let rate = cr.observe(50.0, 100).unwrap();
505        assert!((rate - 0.5).abs() < 1e-10);
506    }
507
508    #[test]
509    fn change_rate_negative() {
510        let mut cr = ChangeRate::new();
511        cr.observe(100.0, 0);
512        let rate = cr.observe(50.0, 100).unwrap();
513        assert!((rate - (-0.5)).abs() < 1e-10);
514    }
515
516    #[test]
517    fn change_rate_same_timestamp() {
518        let mut cr = ChangeRate::new();
519        cr.observe(10.0, 100);
520        let rate = cr.observe(20.0, 100).unwrap();
521        // dt=0 → 前回のrateを返す
522        assert!((rate - 0.0).abs() < 1e-10);
523    }
524
525    #[test]
526    fn change_rate_reset() {
527        let mut cr = ChangeRate::new();
528        cr.observe(10.0, 0);
529        cr.observe(20.0, 100);
530        cr.reset();
531        assert!(!cr.is_initialized());
532        assert!((cr.rate() - 0.0).abs() < 1e-10);
533    }
534
535    #[test]
536    fn change_rate_default() {
537        let cr = ChangeRate::default();
538        assert!(!cr.is_initialized());
539    }
540
541    // --- ExponentialMovingAverage ---
542
543    #[test]
544    fn ema_first_value() {
545        let mut ema = ExponentialMovingAverage::new(0.5);
546        ema.observe(10.0);
547        assert!((ema.value() - 10.0).abs() < 1e-10);
548        assert_eq!(ema.count(), 1);
549    }
550
551    #[test]
552    fn ema_convergence() {
553        let mut ema = ExponentialMovingAverage::new(0.1);
554        // 全て同じ値 → EMAはその値に収束
555        for _ in 0..100 {
556            ema.observe(50.0);
557        }
558        assert!((ema.value() - 50.0).abs() < 1e-6);
559    }
560
561    #[test]
562    fn ema_from_span() {
563        let ema = ExponentialMovingAverage::from_span(9);
564        // alpha = 2/(9+1) = 0.2
565        assert!((ema.alpha() - 0.2).abs() < 1e-10);
566    }
567
568    #[test]
569    fn ema_step_response() {
570        let mut ema = ExponentialMovingAverage::new(0.5);
571        ema.observe(0.0);
572        ema.observe(100.0);
573        // EMA = 0.5 * 100 + 0.5 * 0 = 50
574        assert!((ema.value() - 50.0).abs() < 1e-10);
575    }
576
577    #[test]
578    fn ema_reset() {
579        let mut ema = ExponentialMovingAverage::new(0.3);
580        ema.observe(10.0);
581        ema.reset();
582        assert_eq!(ema.count(), 0);
583        assert!((ema.value() - 0.0).abs() < 1e-10);
584    }
585
586    // --- SimpleMovingAverage ---
587
588    #[test]
589    fn sma_basic() {
590        let mut sma = SimpleMovingAverage::<3>::new();
591        sma.observe(10.0);
592        sma.observe(20.0);
593        sma.observe(30.0);
594        assert!((sma.value() - 20.0).abs() < 1e-10);
595        assert_eq!(sma.len(), 3);
596        assert!(sma.is_full());
597    }
598
599    #[test]
600    fn sma_overflow() {
601        let mut sma = SimpleMovingAverage::<3>::new();
602        sma.observe(10.0);
603        sma.observe(20.0);
604        sma.observe(30.0);
605        sma.observe(40.0); // 10が押し出される
606                           // (20+30+40)/3 = 30
607        assert!((sma.value() - 30.0).abs() < 1e-10);
608    }
609
610    #[test]
611    fn sma_empty() {
612        let sma = SimpleMovingAverage::<8>::new();
613        assert!(sma.is_empty());
614        assert!((sma.value() - 0.0).abs() < 1e-10);
615    }
616
617    #[test]
618    fn sma_reset() {
619        let mut sma = SimpleMovingAverage::<4>::new();
620        sma.observe(10.0);
621        sma.reset();
622        assert!(sma.is_empty());
623    }
624
625    #[test]
626    fn sma_default() {
627        let sma = SimpleMovingAverage::<16>::default();
628        assert!(sma.is_empty());
629    }
630
631    // --- LinearRegression ---
632
633    #[test]
634    fn linreg_perfect_line() {
635        let mut lr = LinearRegression::new();
636        // y = 2x + 1
637        for i in 0..100 {
638            let x = f64::from(i);
639            lr.observe(x, 2.0f64.mul_add(x, 1.0));
640        }
641        assert!((lr.slope() - 2.0).abs() < 1e-6, "slope = {}", lr.slope());
642        assert!(
643            (lr.intercept() - 1.0).abs() < 1e-6,
644            "intercept = {}",
645            lr.intercept()
646        );
647    }
648
649    #[test]
650    fn linreg_predict() {
651        let mut lr = LinearRegression::new();
652        // y = 3x - 2
653        for i in 0..50 {
654            let x = f64::from(i);
655            lr.observe(x, 3.0f64.mul_add(x, -2.0));
656        }
657        let pred = lr.predict(100.0);
658        assert!((pred - 298.0).abs() < 1e-4, "pred = {pred}");
659    }
660
661    #[test]
662    fn linreg_constant() {
663        let mut lr = LinearRegression::new();
664        for i in 0..50 {
665            lr.observe(f64::from(i), 42.0);
666        }
667        assert!((lr.slope() - 0.0).abs() < 1e-10);
668        assert!((lr.intercept() - 42.0).abs() < 1e-6);
669    }
670
671    #[test]
672    fn linreg_single_point() {
673        let mut lr = LinearRegression::new();
674        lr.observe(5.0, 10.0);
675        assert!((lr.slope() - 0.0).abs() < 1e-10);
676        assert_eq!(lr.count(), 1);
677    }
678
679    #[test]
680    fn linreg_reset() {
681        let mut lr = LinearRegression::new();
682        lr.observe(1.0, 2.0);
683        lr.observe(2.0, 4.0);
684        lr.reset();
685        assert_eq!(lr.count(), 0);
686    }
687
688    #[test]
689    fn linreg_default() {
690        let lr = LinearRegression::default();
691        assert_eq!(lr.count(), 0);
692    }
693
694    // --- LinearRegressionFull ---
695
696    #[test]
697    fn linreg_full_r_squared_perfect() {
698        let mut lr = LinearRegressionFull::new();
699        // 完全な線形関係 → R²≈1.0
700        for i in 0..100 {
701            let x = f64::from(i);
702            lr.observe(x, 2.0f64.mul_add(x, 1.0));
703        }
704        assert!(
705            (lr.r_squared() - 1.0).abs() < 1e-6,
706            "R² = {}",
707            lr.r_squared()
708        );
709    }
710
711    #[test]
712    fn linreg_full_r_squared_no_correlation() {
713        let mut lr = LinearRegressionFull::new();
714        // x増加、yは定数 → R²≈0
715        for i in 0..100 {
716            lr.observe(f64::from(i), 42.0);
717        }
718        assert!(lr.r_squared() < 0.01, "R² = {}", lr.r_squared());
719    }
720
721    #[test]
722    fn linreg_full_predict() {
723        let mut lr = LinearRegressionFull::new();
724        for i in 0..50 {
725            let x = f64::from(i);
726            lr.observe(x, x.mul_add(0.5, 10.0));
727        }
728        let pred = lr.predict(100.0);
729        assert!((pred - 60.0).abs() < 1e-4, "pred = {pred}");
730    }
731
732    #[test]
733    fn linreg_full_reset() {
734        let mut lr = LinearRegressionFull::new();
735        lr.observe(1.0, 2.0);
736        lr.reset();
737        assert_eq!(lr.count(), 0);
738    }
739
740    #[test]
741    fn linreg_full_default() {
742        let lr = LinearRegressionFull::default();
743        assert_eq!(lr.count(), 0);
744    }
745}