Skip to main content

alice_analytics/
streaming_ops.rs

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