sklears_preprocessing/temporal/
trend.rs

1//! Trend detection and analysis for time series
2//!
3//! This module provides various methods for detecting and quantifying trends
4//! in time series data.
5
6use scirs2_core::ndarray::{s, Array1};
7use sklears_core::{
8    error::Result,
9    traits::{Fit, Trained, Transform, Untrained},
10    types::Float,
11};
12use std::marker::PhantomData;
13
14/// Configuration for TrendDetector
15#[derive(Debug, Clone)]
16pub struct TrendDetectorConfig {
17    /// Trend detection method
18    pub method: TrendMethod,
19    /// Window size for local trend calculation
20    pub window_size: usize,
21    /// Polynomial degree for polynomial trend fitting
22    pub polynomial_degree: usize,
23}
24
25/// Trend detection methods
26#[derive(Debug, Clone, Copy)]
27pub enum TrendMethod {
28    /// Linear trend using least squares
29    Linear,
30    /// Polynomial trend fitting
31    Polynomial,
32    /// Local linear trends in sliding windows
33    LocalLinear,
34    /// Mann-Kendall trend test
35    MannKendall,
36}
37
38impl Default for TrendDetectorConfig {
39    fn default() -> Self {
40        Self {
41            method: TrendMethod::Linear,
42            window_size: 10,
43            polynomial_degree: 2,
44        }
45    }
46}
47
48/// TrendDetector for detecting and quantifying trends in time series
49#[derive(Debug, Clone)]
50pub struct TrendDetector<S> {
51    config: TrendDetectorConfig,
52    _phantom: PhantomData<S>,
53}
54
55impl TrendDetector<Untrained> {
56    /// Create a new TrendDetector
57    pub fn new() -> Self {
58        Self {
59            config: TrendDetectorConfig::default(),
60            _phantom: PhantomData,
61        }
62    }
63
64    /// Set the trend detection method
65    pub fn method(mut self, method: TrendMethod) -> Self {
66        self.config.method = method;
67        self
68    }
69
70    /// Set the window size for local trend calculation
71    pub fn window_size(mut self, window_size: usize) -> Self {
72        self.config.window_size = window_size;
73        self
74    }
75
76    /// Set the polynomial degree
77    pub fn polynomial_degree(mut self, polynomial_degree: usize) -> Self {
78        self.config.polynomial_degree = polynomial_degree;
79        self
80    }
81}
82
83impl TrendDetector<Trained> {
84    /// Calculate linear trend slope using least squares
85    fn calculate_linear_trend(&self, data: &Array1<Float>) -> Float {
86        let n = data.len() as Float;
87        let x_mean = (n - 1.0) / 2.0;
88        let y_mean = data.mean().unwrap_or(0.0);
89
90        let mut numerator = 0.0;
91        let mut denominator = 0.0;
92
93        for (i, &y) in data.iter().enumerate() {
94            let x = i as Float;
95            numerator += (x - x_mean) * (y - y_mean);
96            denominator += (x - x_mean).powi(2);
97        }
98
99        if denominator.abs() > 1e-10 {
100            numerator / denominator
101        } else {
102            0.0
103        }
104    }
105
106    /// Calculate Mann-Kendall trend statistic
107    fn calculate_mann_kendall(&self, data: &Array1<Float>) -> Float {
108        let n = data.len();
109        let mut s = 0i32;
110
111        for i in 0..n {
112            for j in (i + 1)..n {
113                if data[j] > data[i] {
114                    s += 1;
115                } else if data[j] < data[i] {
116                    s -= 1;
117                }
118            }
119        }
120
121        // Normalize by maximum possible value
122        let max_s = (n * (n - 1) / 2) as i32;
123        if max_s > 0 {
124            s as Float / max_s as Float
125        } else {
126            0.0
127        }
128    }
129
130    /// Calculate local linear trends in sliding windows
131    fn calculate_local_trends(&self, data: &Array1<Float>) -> Array1<Float> {
132        let n = data.len();
133        let window_size = self.config.window_size.min(n);
134        let mut trends = Array1::<Float>::zeros(n);
135
136        for i in 0..n {
137            let start = if i >= window_size / 2 {
138                (i - window_size / 2).max(0)
139            } else {
140                0
141            };
142            let end = (start + window_size).min(n);
143            let window = data.slice(s![start..end]);
144            trends[i] = self.calculate_linear_trend(&window.to_owned());
145        }
146
147        trends
148    }
149}
150
151impl Default for TrendDetector<Untrained> {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157impl Fit<Array1<Float>, ()> for TrendDetector<Untrained> {
158    type Fitted = TrendDetector<Trained>;
159
160    fn fit(self, _x: &Array1<Float>, _y: &()) -> Result<Self::Fitted> {
161        Ok(TrendDetector {
162            config: self.config,
163            _phantom: PhantomData,
164        })
165    }
166}
167
168impl Transform<Array1<Float>, Array1<Float>> for TrendDetector<Trained> {
169    fn transform(&self, x: &Array1<Float>) -> Result<Array1<Float>> {
170        match self.config.method {
171            TrendMethod::Linear => {
172                let slope = self.calculate_linear_trend(x);
173                Ok(Array1::from_elem(x.len(), slope))
174            }
175            TrendMethod::MannKendall => {
176                let mk_stat = self.calculate_mann_kendall(x);
177                Ok(Array1::from_elem(x.len(), mk_stat))
178            }
179            TrendMethod::LocalLinear => Ok(self.calculate_local_trends(x)),
180            TrendMethod::Polynomial => {
181                // Simplified polynomial trend - just return linear for now
182                let slope = self.calculate_linear_trend(x);
183                Ok(Array1::from_elem(x.len(), slope))
184            }
185        }
186    }
187}