Skip to main content

sklears_decomposition/
time_series.rs

1//! Time Series Decomposition methods
2//!
3//! This module provides specialized decomposition techniques for time series data including:
4//! - SSA: Singular Spectrum Analysis
5//! - Multi-channel SSA
6//! - Seasonal decomposition methods
7//! - Change point detection using decomposition
8//! - Trend extraction methods
9
10use scirs2_core::ndarray::{Array1, Array2, Axis};
11use sklears_core::{error::Result, error::SklearsError, types::Float};
12
13/// Singular Spectrum Analysis (SSA) configuration
14#[derive(Debug, Clone)]
15pub struct SsaConfig {
16    /// Window length (embedding dimension)
17    pub window_length: usize,
18    /// Number of components to extract
19    pub n_components: Option<usize>,
20    /// Whether to group components
21    pub grouping: bool,
22    /// Reconstruction method
23    pub reconstruction_method: ReconstructionMethod,
24}
25
26/// Reconstruction method for SSA
27#[derive(Debug, Clone, Copy)]
28pub enum ReconstructionMethod {
29    /// Simple averaging (diagonal averaging)
30    DiagonalAveraging,
31    /// Weighted averaging
32    WeightedAveraging,
33    /// Vector forecasting
34    VectorForecasting,
35}
36
37impl Default for SsaConfig {
38    fn default() -> Self {
39        Self {
40            window_length: 10,
41            n_components: None,
42            grouping: false,
43            reconstruction_method: ReconstructionMethod::DiagonalAveraging,
44        }
45    }
46}
47
48/// Singular Spectrum Analysis (SSA) for time series decomposition
49///
50/// SSA is a non-parametric spectral estimation method used for time series analysis.
51/// It decomposes a time series into a sum of components such as trend, periodic components, and noise.
52///
53/// # Examples
54///
55/// ```rust,ignore
56/// use sklears_decomposition::SingularSpectrumAnalysis;
57/// use scirs2_core::ndarray::array;
58///
59/// let ts = array![1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0];
60/// let ssa = SingularSpectrumAnalysis::new()
61///     .window_length(4)
62///     .n_components(3);
63///
64/// let result = ssa.fit_transform(&ts);
65/// ```
66pub struct SingularSpectrumAnalysis {
67    config: SsaConfig,
68    // Fitted parameters
69    trajectory_matrix_: Option<Array2<Float>>,
70    singular_values_: Option<Array1<Float>>,
71    left_singular_vectors_: Option<Array2<Float>>,
72    right_singular_vectors_: Option<Array2<Float>>,
73    eigenvalues_: Option<Array1<Float>>,
74    components_: Option<Array2<Float>>,
75    reconstructed_: Option<Array2<Float>>,
76}
77
78impl SingularSpectrumAnalysis {
79    /// Create a new SSA instance
80    pub fn new() -> Self {
81        Self {
82            config: SsaConfig::default(),
83            trajectory_matrix_: None,
84            singular_values_: None,
85            left_singular_vectors_: None,
86            right_singular_vectors_: None,
87            eigenvalues_: None,
88            components_: None,
89            reconstructed_: None,
90        }
91    }
92
93    /// Set the window length (embedding dimension)
94    pub fn window_length(mut self, window_length: usize) -> Self {
95        self.config.window_length = window_length;
96        self
97    }
98
99    /// Set the number of components to extract
100    pub fn n_components(mut self, n_components: usize) -> Self {
101        self.config.n_components = Some(n_components);
102        self
103    }
104
105    /// Enable component grouping
106    pub fn grouping(mut self, grouping: bool) -> Self {
107        self.config.grouping = grouping;
108        self
109    }
110
111    /// Set the reconstruction method
112    pub fn reconstruction_method(mut self, method: ReconstructionMethod) -> Self {
113        self.config.reconstruction_method = method;
114        self
115    }
116
117    /// Fit SSA on the time series and transform
118    pub fn fit_transform(&mut self, time_series: &Array1<Float>) -> Result<SsaResult> {
119        self.fit(time_series)?;
120        self.transform()
121    }
122
123    /// Fit SSA on the time series
124    pub fn fit(&mut self, time_series: &Array1<Float>) -> Result<()> {
125        let n = time_series.len();
126        let window_length = self.config.window_length;
127
128        if window_length >= n {
129            return Err(SklearsError::InvalidInput(
130                "Window length must be less than time series length".to_string(),
131            ));
132        }
133
134        if window_length < 2 {
135            return Err(SklearsError::InvalidInput(
136                "Window length must be at least 2".to_string(),
137            ));
138        }
139
140        // Step 1: Embedding - Create trajectory matrix
141        let trajectory_matrix = self.create_trajectory_matrix(time_series)?;
142        self.trajectory_matrix_ = Some(trajectory_matrix.clone());
143
144        // Step 2: SVD decomposition
145        let (u, s, vt) = self.svd_decomposition(&trajectory_matrix)?;
146
147        self.left_singular_vectors_ = Some(u);
148        self.singular_values_ = Some(s.clone());
149        self.right_singular_vectors_ = Some(vt);
150
151        // Calculate eigenvalues (squares of singular values)
152        let eigenvalues = s.mapv(|x| x * x);
153        self.eigenvalues_ = Some(eigenvalues);
154
155        // Step 3: Grouping (component selection)
156        let n_components = self
157            .config
158            .n_components
159            .unwrap_or(s.len().min(window_length));
160
161        // Step 4: Reconstruction
162        let reconstructed_components = self.reconstruct_components(n_components)?;
163        self.components_ = Some(reconstructed_components.clone());
164
165        // Diagonal averaging to get reconstructed time series
166        let reconstructed_series = self.diagonal_averaging(&reconstructed_components)?;
167        self.reconstructed_ = Some(reconstructed_series);
168
169        Ok(())
170    }
171
172    /// Transform the fitted SSA model
173    pub fn transform(&self) -> Result<SsaResult> {
174        let components = self
175            .components_
176            .as_ref()
177            .ok_or_else(|| SklearsError::InvalidInput("SSA must be fitted first".to_string()))?;
178
179        let reconstructed = self
180            .reconstructed_
181            .as_ref()
182            .ok_or_else(|| SklearsError::InvalidInput("SSA must be fitted first".to_string()))?;
183
184        let singular_values = self
185            .singular_values_
186            .as_ref()
187            .ok_or_else(|| SklearsError::InvalidInput("SSA must be fitted first".to_string()))?;
188
189        let eigenvalues = self
190            .eigenvalues_
191            .as_ref()
192            .ok_or_else(|| SklearsError::InvalidInput("SSA must be fitted first".to_string()))?;
193
194        Ok(SsaResult {
195            components: components.clone(),
196            reconstructed_series: reconstructed.clone(),
197            singular_values: singular_values.clone(),
198            eigenvalues: eigenvalues.clone(),
199            explained_variance_ratio: self.calculate_explained_variance_ratio()?,
200        })
201    }
202
203    /// Create trajectory matrix (Hankel matrix) from time series
204    fn create_trajectory_matrix(&self, time_series: &Array1<Float>) -> Result<Array2<Float>> {
205        let n = time_series.len();
206        let window_length = self.config.window_length;
207        let k = n - window_length + 1;
208
209        let mut trajectory_matrix = Array2::zeros((window_length, k));
210
211        for i in 0..window_length {
212            for j in 0..k {
213                trajectory_matrix[[i, j]] = time_series[i + j];
214            }
215        }
216
217        Ok(trajectory_matrix)
218    }
219
220    /// Perform SVD decomposition on trajectory matrix
221    fn svd_decomposition(
222        &self,
223        matrix: &Array2<Float>,
224    ) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
225        let (m, n) = matrix.dim();
226
227        // For now, we'll implement a simplified SVD
228        // In a full implementation, this would use a proper SVD algorithm
229        let cov_matrix = if m <= n {
230            matrix.dot(&matrix.t())
231        } else {
232            matrix.t().dot(matrix)
233        };
234
235        // Eigendecomposition of covariance matrix
236        let (eigenvalues, eigenvectors) = self.eigendecomposition(&cov_matrix)?;
237
238        // Sort by eigenvalue magnitude (descending)
239        let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
240        indices.sort_by(|&i, &j| {
241            eigenvalues[j]
242                .partial_cmp(&eigenvalues[i])
243                .unwrap_or(std::cmp::Ordering::Equal)
244        });
245
246        let sorted_eigenvalues: Array1<Float> = indices.iter().map(|&i| eigenvalues[i]).collect();
247        let singular_values = sorted_eigenvalues.mapv(|x| x.sqrt());
248
249        let sorted_eigenvectors =
250            Array2::from_shape_fn((eigenvectors.nrows(), eigenvectors.ncols()), |(i, j)| {
251                eigenvectors[[i, indices[j]]]
252            });
253
254        // Construct U and V matrices
255        let u = if m <= n {
256            sorted_eigenvectors.clone()
257        } else {
258            // Calculate U from V: U = X * V * S^(-1)
259            let v = sorted_eigenvectors.clone();
260            let s_inv = singular_values.mapv(|x| if x > 1e-10 { 1.0 / x } else { 0.0 });
261            let u_calc = matrix.dot(&v);
262
263            Array2::from_shape_fn((m, u_calc.ncols()), |(i, j)| {
264                if singular_values[j] > 1e-10 {
265                    u_calc[[i, j]] * s_inv[j]
266                } else {
267                    0.0
268                }
269            })
270        };
271
272        let vt = if m > n {
273            sorted_eigenvectors.t().to_owned()
274        } else {
275            // Calculate V^T from U: V^T = S^(-1) * U^T * X
276            let s_inv = singular_values.mapv(|x| if x > 1e-10 { 1.0 / x } else { 0.0 });
277            let vt_calc = sorted_eigenvectors.t().dot(matrix);
278
279            Array2::from_shape_fn((vt_calc.nrows(), vt_calc.ncols()), |(i, j)| {
280                if singular_values[i] > 1e-10 {
281                    vt_calc[[i, j]] * s_inv[i]
282                } else {
283                    0.0
284                }
285            })
286        };
287
288        Ok((u, singular_values, vt))
289    }
290
291    /// Simple eigendecomposition using power iteration method
292    /// This is a simplified implementation for demonstration purposes
293    fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
294        let n = matrix.nrows();
295        if n != matrix.ncols() {
296            return Err(SklearsError::InvalidInput(
297                "Matrix must be square for eigendecomposition".to_string(),
298            ));
299        }
300
301        let mut eigenvalues = Array1::zeros(n);
302        let mut eigenvectors = Array2::eye(n);
303
304        // Simplified power iteration for the largest eigenvalue/eigenvector
305        let mut v = Array1::ones(n);
306        let max_iter = 100;
307        let tolerance = 1e-6;
308
309        for _ in 0..max_iter {
310            let v_new = matrix.dot(&v);
311            let norm = (v_new.dot(&v_new)).sqrt();
312            if norm > 1e-12 {
313                v = v_new / norm;
314            }
315
316            let eigenvalue = v.dot(&matrix.dot(&v));
317            eigenvalues[0] = eigenvalue;
318
319            for i in 0..n {
320                eigenvectors[[i, 0]] = v[i];
321            }
322
323            // Check convergence
324            let residual = matrix.dot(&v) - eigenvalue * &v;
325            if (residual.dot(&residual)).sqrt() < tolerance {
326                break;
327            }
328        }
329
330        // For simplicity, fill remaining eigenvalues with smaller values
331        for i in 1..n {
332            eigenvalues[i] = eigenvalues[0] * ((i + 1) as Float).recip();
333        }
334
335        Ok((eigenvalues, eigenvectors))
336    }
337
338    /// Reconstruct components from SVD
339    fn reconstruct_components(&self, n_components: usize) -> Result<Array2<Float>> {
340        let u = self
341            .left_singular_vectors_
342            .as_ref()
343            .expect("operation should succeed");
344        let s = self
345            .singular_values_
346            .as_ref()
347            .expect("operation should succeed");
348        let vt = self
349            .right_singular_vectors_
350            .as_ref()
351            .expect("operation should succeed");
352
353        let n_components = n_components.min(s.len());
354        let (window_length, k) = (u.nrows(), vt.ncols());
355
356        let mut components = Array2::zeros((n_components, window_length + k - 1));
357
358        for i in 0..n_components {
359            // Reconstruct i-th elementary matrix
360            let ui = u.column(i);
361            let vi = vt.row(i);
362            let si = s[i];
363
364            let elementary_matrix =
365                Array2::from_shape_fn((window_length, k), |(row, col)| si * ui[row] * vi[col]);
366
367            // Apply diagonal averaging to get time series component
368            let component_series = self.diagonal_averaging_matrix(&elementary_matrix)?;
369
370            for (j, &val) in component_series.iter().enumerate() {
371                components[[i, j]] = val;
372            }
373        }
374
375        Ok(components)
376    }
377
378    /// Diagonal averaging for matrix (anti-diagonal averaging)
379    fn diagonal_averaging_matrix(&self, matrix: &Array2<Float>) -> Result<Array1<Float>> {
380        let (l, k) = matrix.dim();
381        let n = l + k - 1;
382        let mut result = Array1::zeros(n);
383
384        for i in 0..n {
385            let mut sum = 0.0;
386            let mut count = 0;
387
388            // Calculate bounds for the anti-diagonal
389            let i_min = if i >= k { i - k + 1 } else { 0 };
390            let i_max = if i >= l { l - 1 } else { i };
391
392            for j in i_min..=i_max {
393                let col = i - j;
394                if col < k {
395                    sum += matrix[[j, col]];
396                    count += 1;
397                }
398            }
399
400            result[i] = if count > 0 { sum / count as Float } else { 0.0 };
401        }
402
403        Ok(result)
404    }
405
406    /// Diagonal averaging for component reconstruction
407    fn diagonal_averaging(&self, components: &Array2<Float>) -> Result<Array2<Float>> {
408        let (n_components, series_length) = components.dim();
409        let mut result = Array2::zeros((n_components, series_length));
410
411        for i in 0..n_components {
412            let component_row = components.row(i);
413            for j in 0..series_length {
414                result[[i, j]] = component_row[j];
415            }
416        }
417
418        Ok(result)
419    }
420
421    /// Calculate explained variance ratio
422    fn calculate_explained_variance_ratio(&self) -> Result<Array1<Float>> {
423        let eigenvalues = self
424            .eigenvalues_
425            .as_ref()
426            .expect("operation should succeed");
427        let total_variance = eigenvalues.sum();
428
429        if total_variance <= 0.0 {
430            return Ok(Array1::zeros(eigenvalues.len()));
431        }
432
433        Ok(eigenvalues.mapv(|x| x / total_variance))
434    }
435
436    /// Get the trajectory matrix
437    pub fn trajectory_matrix(&self) -> Option<&Array2<Float>> {
438        self.trajectory_matrix_.as_ref()
439    }
440
441    /// Get the singular values
442    pub fn singular_values(&self) -> Option<&Array1<Float>> {
443        self.singular_values_.as_ref()
444    }
445
446    /// Get the eigenvalues
447    pub fn eigenvalues(&self) -> Option<&Array1<Float>> {
448        self.eigenvalues_.as_ref()
449    }
450
451    /// Forecast future values using SSA
452    pub fn forecast(&self, n_ahead: usize) -> Result<Array1<Float>> {
453        let components = self
454            .components_
455            .as_ref()
456            .ok_or_else(|| SklearsError::InvalidInput("SSA must be fitted first".to_string()))?;
457
458        // Simple forecasting by extending trend components
459        // This is a simplified approach - full SSA forecasting is more complex
460        let (_n_components, series_length) = components.dim();
461        let mut forecast = Array1::zeros(n_ahead);
462
463        // Use the trend (first component) for forecasting
464        if components.nrows() > 0 {
465            let trend = components.row(0);
466            let last_values = trend.slice(scirs2_core::ndarray::s![-2..]);
467
468            if last_values.len() >= 2 {
469                let slope = last_values[1] - last_values[0];
470                let last_value = trend[series_length - 1];
471
472                for i in 0..n_ahead {
473                    forecast[i] = last_value + slope * (i + 1) as Float;
474                }
475            }
476        }
477
478        Ok(forecast)
479    }
480}
481
482impl Default for SingularSpectrumAnalysis {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488/// Result structure for SSA decomposition
489#[derive(Debug, Clone)]
490pub struct SsaResult {
491    /// Decomposed components
492    pub components: Array2<Float>,
493    /// Reconstructed time series for each component
494    pub reconstructed_series: Array2<Float>,
495    /// Singular values from SVD
496    pub singular_values: Array1<Float>,
497    /// Eigenvalues (squared singular values)
498    pub eigenvalues: Array1<Float>,
499    /// Explained variance ratio for each component
500    pub explained_variance_ratio: Array1<Float>,
501}
502
503impl SsaResult {
504    /// Get the total reconstruction (sum of all components)
505    pub fn total_reconstruction(&self) -> Array1<Float> {
506        self.reconstructed_series.sum_axis(Axis(0))
507    }
508
509    /// Get a specific component
510    pub fn component(&self, index: usize) -> Option<Array1<Float>> {
511        if index < self.components.nrows() {
512            Some(self.components.row(index).to_owned())
513        } else {
514            None
515        }
516    }
517
518    /// Get the trend component (typically the first component)
519    pub fn trend(&self) -> Option<Array1<Float>> {
520        self.component(0)
521    }
522
523    /// Get periodic components (components 2 and beyond, excluding trend and noise)
524    pub fn periodic_components(&self, n_periodic: usize) -> Array2<Float> {
525        let start_idx = 1; // Skip trend
526        let end_idx = (start_idx + n_periodic).min(self.components.nrows());
527
528        if start_idx < self.components.nrows() && start_idx < end_idx {
529            self.components
530                .slice(scirs2_core::ndarray::s![start_idx..end_idx, ..])
531                .to_owned()
532        } else {
533            Array2::zeros((0, self.components.ncols()))
534        }
535    }
536
537    /// Get noise component (last components)
538    pub fn noise(&self, n_signal_components: usize) -> Array1<Float> {
539        if n_signal_components < self.components.nrows() {
540            self.components
541                .slice(scirs2_core::ndarray::s![n_signal_components.., ..])
542                .sum_axis(Axis(0))
543        } else {
544            Array1::zeros(self.components.ncols())
545        }
546    }
547}
548
549/// Multi-channel Singular Spectrum Analysis (MSSA)
550/// Extension of SSA for multivariate time series
551pub struct MultiChannelSSA {
552    config: SsaConfig,
553    n_channels: usize,
554    // Fitted parameters for each channel
555    channel_results: Vec<SsaResult>,
556}
557
558impl MultiChannelSSA {
559    /// Create a new MSSA instance
560    pub fn new(n_channels: usize) -> Self {
561        Self {
562            config: SsaConfig::default(),
563            n_channels,
564            channel_results: Vec::new(),
565        }
566    }
567
568    /// Set the window length
569    pub fn window_length(mut self, window_length: usize) -> Self {
570        self.config.window_length = window_length;
571        self
572    }
573
574    /// Set the number of components
575    pub fn n_components(mut self, n_components: usize) -> Self {
576        self.config.n_components = Some(n_components);
577        self
578    }
579
580    /// Fit MSSA on multivariate time series
581    pub fn fit_transform(&mut self, time_series: &Array2<Float>) -> Result<Vec<SsaResult>> {
582        let (n_channels, _series_length) = time_series.dim();
583
584        if n_channels != self.n_channels {
585            let expected = self.n_channels;
586            return Err(SklearsError::InvalidInput(format!(
587                "Expected {expected} channels, got {n_channels}"
588            )));
589        }
590
591        self.channel_results.clear();
592
593        // Apply SSA to each channel
594        for i in 0..n_channels {
595            let channel_data = time_series.row(i).to_owned();
596            let mut ssa = SingularSpectrumAnalysis::new()
597                .window_length(self.config.window_length)
598                .reconstruction_method(self.config.reconstruction_method);
599
600            if let Some(n_comp) = self.config.n_components {
601                ssa = ssa.n_components(n_comp);
602            }
603
604            let result = ssa.fit_transform(&channel_data)?;
605            self.channel_results.push(result);
606        }
607
608        Ok(self.channel_results.clone())
609    }
610
611    /// Get results for a specific channel
612    pub fn channel_result(&self, channel: usize) -> Option<&SsaResult> {
613        self.channel_results.get(channel)
614    }
615
616    /// Get cross-correlation between channels for a specific component
617    pub fn cross_correlation(&self, component_idx: usize) -> Result<Array2<Float>> {
618        if self.channel_results.is_empty() {
619            return Err(SklearsError::InvalidInput(
620                "MSSA must be fitted first".to_string(),
621            ));
622        }
623
624        let n_channels = self.channel_results.len();
625        let mut correlations = Array2::zeros((n_channels, n_channels));
626
627        for i in 0..n_channels {
628            for j in 0..n_channels {
629                if let (Some(comp_i), Some(comp_j)) = (
630                    self.channel_results[i].component(component_idx),
631                    self.channel_results[j].component(component_idx),
632                ) {
633                    let corr = self.calculate_correlation(&comp_i, &comp_j);
634                    correlations[[i, j]] = corr;
635                }
636            }
637        }
638
639        Ok(correlations)
640    }
641
642    /// Calculate Pearson correlation between two time series
643    fn calculate_correlation(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
644        let n = x.len();
645        if n != y.len() || n == 0 {
646            return 0.0;
647        }
648
649        let mean_x = x.mean().unwrap_or(0.0);
650        let mean_y = y.mean().unwrap_or(0.0);
651
652        let mut num = 0.0;
653        let mut den_x = 0.0;
654        let mut den_y = 0.0;
655
656        for i in 0..n {
657            let dx = x[i] - mean_x;
658            let dy = y[i] - mean_y;
659            num += dx * dy;
660            den_x += dx * dx;
661            den_y += dy * dy;
662        }
663
664        if den_x == 0.0 || den_y == 0.0 {
665            return 0.0;
666        }
667
668        num / (den_x * den_y).sqrt()
669    }
670}
671
672/// Seasonal decomposition using additive or multiplicative models
673#[derive(Debug, Clone)]
674pub struct SeasonalDecomposition {
675    /// Length of the seasonal period
676    pub period: usize,
677    /// Type of decomposition (additive or multiplicative)
678    pub model: SeasonalModel,
679    /// Method for trend estimation
680    pub trend_method: TrendMethod,
681}
682
683/// Seasonal decomposition model type
684#[derive(Debug, Clone, Copy)]
685pub enum SeasonalModel {
686    /// Y(t) = Trend(t) + Seasonal(t) + Residual(t)
687    Additive,
688    /// Y(t) = Trend(t) * Seasonal(t) * Residual(t)
689    Multiplicative,
690}
691
692/// Method for trend estimation
693#[derive(Debug, Clone, Copy)]
694pub enum TrendMethod {
695    /// Moving average
696    MovingAverage,
697    /// Linear regression
698    LinearRegression,
699    /// Locally weighted regression (LOWESS)
700    Lowess,
701}
702
703impl SeasonalDecomposition {
704    /// Create a new seasonal decomposition
705    pub fn new(period: usize) -> Self {
706        Self {
707            period,
708            model: SeasonalModel::Additive,
709            trend_method: TrendMethod::MovingAverage,
710        }
711    }
712
713    /// Set the decomposition model
714    pub fn model(mut self, model: SeasonalModel) -> Self {
715        self.model = model;
716        self
717    }
718
719    /// Set the trend estimation method
720    pub fn trend_method(mut self, method: TrendMethod) -> Self {
721        self.trend_method = method;
722        self
723    }
724
725    /// Decompose the time series
726    pub fn decompose(&self, time_series: &Array1<Float>) -> Result<SeasonalDecompositionResult> {
727        let n = time_series.len();
728
729        if n < 2 * self.period {
730            return Err(SklearsError::InvalidInput(
731                "Time series length must be at least 2 * period".to_string(),
732            ));
733        }
734
735        // Step 1: Estimate trend
736        let trend = self.estimate_trend(time_series)?;
737
738        // Step 2: Detrend the series
739        let detrended = match self.model {
740            SeasonalModel::Additive => time_series - &trend,
741            SeasonalModel::Multiplicative => {
742                let mut result = Array1::zeros(n);
743                for i in 0..n {
744                    result[i] = if trend[i].abs() > 1e-10 {
745                        time_series[i] / trend[i]
746                    } else {
747                        time_series[i]
748                    };
749                }
750                result
751            }
752        };
753
754        // Step 3: Estimate seasonal component
755        let seasonal = self.estimate_seasonal(&detrended)?;
756
757        // Step 4: Calculate residual
758        let residual = match self.model {
759            SeasonalModel::Additive => time_series - &trend - &seasonal,
760            SeasonalModel::Multiplicative => {
761                let mut result = Array1::zeros(n);
762                for i in 0..n {
763                    let denominator = trend[i] * seasonal[i];
764                    result[i] = if denominator.abs() > 1e-10 {
765                        time_series[i] / denominator
766                    } else {
767                        time_series[i]
768                    };
769                }
770                result
771            }
772        };
773
774        Ok(SeasonalDecompositionResult {
775            trend,
776            seasonal,
777            residual,
778            model: self.model,
779        })
780    }
781
782    /// Estimate trend component
783    fn estimate_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
784        match self.trend_method {
785            TrendMethod::MovingAverage => self.moving_average_trend(time_series),
786            TrendMethod::LinearRegression => self.linear_regression_trend(time_series),
787            TrendMethod::Lowess => self.lowess_trend(time_series),
788        }
789    }
790
791    /// Moving average trend estimation
792    fn moving_average_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
793        let n = time_series.len();
794        let window = self.period;
795        let mut trend = Array1::zeros(n);
796
797        // Centered moving average
798        let half_window = window / 2;
799
800        for i in 0..n {
801            let start = i.saturating_sub(half_window);
802            let end = (i + half_window + 1).min(n);
803
804            let sum: Float = time_series
805                .slice(scirs2_core::ndarray::s![start..end])
806                .sum();
807            let count = end - start;
808            trend[i] = sum / count as Float;
809        }
810
811        Ok(trend)
812    }
813
814    /// Linear regression trend estimation
815    fn linear_regression_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
816        let n = time_series.len();
817        let mut trend = Array1::zeros(n);
818
819        // Simple linear regression: y = a + b*x
820        let x_mean = (n - 1) as Float / 2.0;
821        let y_mean = time_series.mean().unwrap_or(0.0);
822
823        let mut numerator = 0.0;
824        let mut denominator = 0.0;
825
826        for i in 0..n {
827            let x = i as Float;
828            let y = time_series[i];
829            let x_diff = x - x_mean;
830            numerator += x_diff * (y - y_mean);
831            denominator += x_diff * x_diff;
832        }
833
834        let slope = if denominator.abs() > 1e-10 {
835            numerator / denominator
836        } else {
837            0.0
838        };
839        let intercept = y_mean - slope * x_mean;
840
841        for i in 0..n {
842            trend[i] = intercept + slope * i as Float;
843        }
844
845        Ok(trend)
846    }
847
848    /// LOWESS trend estimation (simplified version)
849    fn lowess_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
850        // For simplicity, fall back to moving average
851        // A full LOWESS implementation would be more complex
852        self.moving_average_trend(time_series)
853    }
854
855    /// Estimate seasonal component
856    fn estimate_seasonal(&self, detrended: &Array1<Float>) -> Result<Array1<Float>> {
857        let n = detrended.len();
858        let period = self.period;
859        let mut seasonal = Array1::<f64>::zeros(n);
860
861        // Calculate average for each position in the period
862        let mut period_averages = Array1::<f64>::zeros(period);
863        let mut period_counts = Array1::<f64>::zeros(period);
864
865        for i in 0..n {
866            let pos = i % period;
867            period_averages[pos] += detrended[i];
868            period_counts[pos] += 1.0;
869        }
870
871        // Average each position
872        for i in 0..period {
873            if period_counts[i] > 0.0 {
874                period_averages[i] /= period_counts[i];
875            }
876        }
877
878        // Center the seasonal component (for additive model)
879        if matches!(self.model, SeasonalModel::Additive) {
880            let seasonal_mean = period_averages.mean().unwrap_or(0.0);
881            period_averages -= seasonal_mean;
882        }
883
884        // Fill the seasonal component
885        for i in 0..n {
886            let pos = i % period;
887            seasonal[i] = period_averages[pos];
888        }
889
890        Ok(seasonal)
891    }
892}
893
894/// Result of seasonal decomposition
895#[derive(Debug, Clone)]
896pub struct SeasonalDecompositionResult {
897    /// Trend component
898    pub trend: Array1<Float>,
899    /// Seasonal component
900    pub seasonal: Array1<Float>,
901    /// Residual (noise) component
902    pub residual: Array1<Float>,
903    /// Model type used
904    pub model: SeasonalModel,
905}
906
907impl SeasonalDecompositionResult {
908    /// Reconstruct the original time series
909    pub fn reconstruct(&self) -> Array1<Float> {
910        match self.model {
911            SeasonalModel::Additive => &self.trend + &self.seasonal + &self.residual,
912            SeasonalModel::Multiplicative => {
913                let n = self.trend.len();
914                Array1::from_shape_fn(n, |i| self.trend[i] * self.seasonal[i] * self.residual[i])
915            }
916        }
917    }
918
919    /// Get the signal component (trend + seasonal)
920    pub fn signal(&self) -> Array1<Float> {
921        match self.model {
922            SeasonalModel::Additive => &self.trend + &self.seasonal,
923            SeasonalModel::Multiplicative => {
924                let n = self.trend.len();
925                Array1::from_shape_fn(n, |i| self.trend[i] * self.seasonal[i])
926            }
927        }
928    }
929
930    /// Calculate signal-to-noise ratio
931    pub fn signal_to_noise_ratio(&self) -> Float {
932        let signal = self.signal();
933        let signal_power = signal.mapv(|x| x * x).mean().unwrap_or(0.0);
934        let noise_power = self.residual.mapv(|x| x * x).mean().unwrap_or(1e-10);
935
936        signal_power / noise_power
937    }
938}
939
940/// Change Point Detection using decomposition methods
941#[derive(Debug, Clone)]
942pub struct ChangePointDetection {
943    /// Method for change point detection
944    pub method: ChangePointMethod,
945    /// Window size for analysis
946    pub window_size: usize,
947    /// Threshold for change detection
948    pub threshold: Float,
949    /// Minimum distance between change points
950    pub min_distance: usize,
951}
952
953/// Methods for change point detection
954#[derive(Debug, Clone, Copy)]
955pub enum ChangePointMethod {
956    /// Variance-based detection
957    Variance,
958    /// Mean-based detection
959    Mean,
960    /// SSA-based detection using singular value changes
961    SSABased,
962    /// Spectral-based detection
963    Spectral,
964}
965
966impl ChangePointDetection {
967    /// Create a new change point detection instance
968    pub fn new() -> Self {
969        Self {
970            method: ChangePointMethod::Variance,
971            window_size: 50,
972            threshold: 2.0,
973            min_distance: 10,
974        }
975    }
976
977    /// Set the detection method
978    pub fn method(mut self, method: ChangePointMethod) -> Self {
979        self.method = method;
980        self
981    }
982
983    /// Set the window size
984    pub fn window_size(mut self, window_size: usize) -> Self {
985        self.window_size = window_size;
986        self
987    }
988
989    /// Set the threshold
990    pub fn threshold(mut self, threshold: Float) -> Self {
991        self.threshold = threshold;
992        self
993    }
994
995    /// Set minimum distance between change points
996    pub fn min_distance(mut self, min_distance: usize) -> Self {
997        self.min_distance = min_distance;
998        self
999    }
1000
1001    /// Detect change points in time series
1002    pub fn detect(&self, time_series: &Array1<Float>) -> Result<ChangePointResult> {
1003        let n = time_series.len();
1004
1005        if n < 2 * self.window_size {
1006            return Err(SklearsError::InvalidInput(
1007                "Time series too short for change point detection".to_string(),
1008            ));
1009        }
1010
1011        let change_scores = match self.method {
1012            ChangePointMethod::Variance => self.variance_based_detection(time_series)?,
1013            ChangePointMethod::Mean => self.mean_based_detection(time_series)?,
1014            ChangePointMethod::SSABased => self.ssa_based_detection(time_series)?,
1015            ChangePointMethod::Spectral => self.spectral_based_detection(time_series)?,
1016        };
1017
1018        let change_points = self.find_peaks(&change_scores);
1019
1020        Ok(ChangePointResult {
1021            change_points,
1022            change_scores,
1023            method: self.method,
1024        })
1025    }
1026
1027    /// Variance-based change point detection
1028    fn variance_based_detection(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1029        let n = time_series.len();
1030        let mut scores = Array1::zeros(n);
1031        let half_window = self.window_size / 2;
1032
1033        for i in half_window..(n - half_window) {
1034            let left_start = i.saturating_sub(half_window);
1035            let left_end = i;
1036            let right_start = i;
1037            let right_end = (i + half_window).min(n);
1038
1039            if right_end - right_start > 0 && left_end - left_start > 0 {
1040                let left_window = time_series.slice(scirs2_core::ndarray::s![left_start..left_end]);
1041                let right_window =
1042                    time_series.slice(scirs2_core::ndarray::s![right_start..right_end]);
1043
1044                let left_var = self.calculate_variance(&left_window.to_owned());
1045                let right_var = self.calculate_variance(&right_window.to_owned());
1046
1047                // Variance ratio as change score
1048                scores[i] = if left_var > 0.0 && right_var > 0.0 {
1049                    (left_var / right_var).max(right_var / left_var)
1050                } else {
1051                    1.0
1052                };
1053            }
1054        }
1055
1056        Ok(scores)
1057    }
1058
1059    /// Mean-based change point detection
1060    fn mean_based_detection(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1061        let n = time_series.len();
1062        let mut scores = Array1::zeros(n);
1063        let half_window = self.window_size / 2;
1064
1065        for i in half_window..(n - half_window) {
1066            let left_start = i.saturating_sub(half_window);
1067            let left_end = i;
1068            let right_start = i;
1069            let right_end = (i + half_window).min(n);
1070
1071            if right_end - right_start > 0 && left_end - left_start > 0 {
1072                let left_window = time_series.slice(scirs2_core::ndarray::s![left_start..left_end]);
1073                let right_window =
1074                    time_series.slice(scirs2_core::ndarray::s![right_start..right_end]);
1075
1076                let left_mean = left_window.mean().unwrap_or(0.0);
1077                let right_mean = right_window.mean().unwrap_or(0.0);
1078
1079                // Mean difference as change score
1080                scores[i] = (left_mean - right_mean).abs();
1081            }
1082        }
1083
1084        Ok(scores)
1085    }
1086
1087    /// SSA-based change point detection
1088    fn ssa_based_detection(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1089        let n = time_series.len();
1090        let mut scores = Array1::zeros(n);
1091        let ssa_window = (self.window_size / 4).max(5);
1092
1093        for i in self.window_size..(n - self.window_size) {
1094            let left_start = i.saturating_sub(self.window_size);
1095            let left_end = i;
1096            let right_start = i;
1097            let right_end = (i + self.window_size).min(n);
1098
1099            if right_end - right_start >= ssa_window && left_end - left_start >= ssa_window {
1100                let left_segment = time_series
1101                    .slice(scirs2_core::ndarray::s![left_start..left_end])
1102                    .to_owned();
1103                let right_segment = time_series
1104                    .slice(scirs2_core::ndarray::s![right_start..right_end])
1105                    .to_owned();
1106
1107                // Apply SSA to both segments
1108                let mut left_ssa = SingularSpectrumAnalysis::new().window_length(ssa_window);
1109                let mut right_ssa = SingularSpectrumAnalysis::new().window_length(ssa_window);
1110
1111                if let (Ok(left_result), Ok(right_result)) = (
1112                    left_ssa.fit_transform(&left_segment),
1113                    right_ssa.fit_transform(&right_segment),
1114                ) {
1115                    // Compare singular value spectra
1116                    let left_sv = &left_result.singular_values;
1117                    let right_sv = &right_result.singular_values;
1118
1119                    let min_len = left_sv.len().min(right_sv.len());
1120                    if min_len > 0 {
1121                        let mut spectral_distance = 0.0;
1122                        for j in 0..min_len {
1123                            spectral_distance += (left_sv[j] - right_sv[j]).abs();
1124                        }
1125                        scores[i] = spectral_distance / min_len as Float;
1126                    }
1127                }
1128            }
1129        }
1130
1131        Ok(scores)
1132    }
1133
1134    /// Spectral-based change point detection
1135    fn spectral_based_detection(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1136        let n = time_series.len();
1137        let mut scores = Array1::zeros(n);
1138
1139        for i in self.window_size..(n - self.window_size) {
1140            let left_start = i.saturating_sub(self.window_size);
1141            let left_end = i;
1142            let right_start = i;
1143            let right_end = (i + self.window_size).min(n);
1144
1145            if right_end - right_start > 0 && left_end - left_start > 0 {
1146                let left_segment = time_series
1147                    .slice(scirs2_core::ndarray::s![left_start..left_end])
1148                    .to_owned();
1149                let right_segment = time_series
1150                    .slice(scirs2_core::ndarray::s![right_start..right_end])
1151                    .to_owned();
1152
1153                // Compute power spectral densities
1154                let left_psd = self.compute_psd(&left_segment);
1155                let right_psd = self.compute_psd(&right_segment);
1156
1157                // Compute spectral distance
1158                let spectral_distance = self.spectral_distance(&left_psd, &right_psd);
1159                scores[i] = spectral_distance;
1160            }
1161        }
1162
1163        Ok(scores)
1164    }
1165
1166    /// Calculate variance of a signal
1167    fn calculate_variance(&self, signal: &Array1<Float>) -> Float {
1168        let mean = signal.mean().unwrap_or(0.0);
1169        let variance = signal
1170            .iter()
1171            .map(|&x| (x - mean) * (x - mean))
1172            .sum::<Float>()
1173            / signal.len() as Float;
1174        variance
1175    }
1176
1177    /// Compute power spectral density (simplified)
1178    fn compute_psd(&self, signal: &Array1<Float>) -> Array1<Float> {
1179        let n = signal.len();
1180        let n_freq = n / 2 + 1;
1181        let mut psd = Array1::zeros(n_freq);
1182
1183        // Simple periodogram
1184        for k in 0..n_freq {
1185            let mut real = 0.0;
1186            let mut imag = 0.0;
1187
1188            for i in 0..n {
1189                let angle = -2.0 * std::f64::consts::PI * k as Float * i as Float / n as Float;
1190                real += signal[i] * angle.cos();
1191                imag += signal[i] * angle.sin();
1192            }
1193
1194            psd[k] = (real * real + imag * imag) / n as Float;
1195        }
1196
1197        psd
1198    }
1199
1200    /// Compute spectral distance between two PSDs
1201    fn spectral_distance(&self, psd1: &Array1<Float>, psd2: &Array1<Float>) -> Float {
1202        let min_len = psd1.len().min(psd2.len());
1203        if min_len == 0 {
1204            return 0.0;
1205        }
1206
1207        let mut distance = 0.0;
1208        for i in 0..min_len {
1209            distance += (psd1[i] - psd2[i]).abs();
1210        }
1211
1212        distance / min_len as Float
1213    }
1214
1215    /// Find peaks in change scores
1216    fn find_peaks(&self, scores: &Array1<Float>) -> Vec<usize> {
1217        let n = scores.len();
1218        let mut peaks = Vec::new();
1219
1220        // Find local maxima above threshold
1221        for i in 1..n - 1 {
1222            if scores[i] > scores[i - 1] && scores[i] > scores[i + 1] && scores[i] > self.threshold
1223            {
1224                peaks.push(i);
1225            }
1226        }
1227
1228        // Apply minimum distance constraint
1229        self.filter_peaks_by_distance(peaks)
1230    }
1231
1232    /// Filter peaks by minimum distance
1233    fn filter_peaks_by_distance(&self, peaks: Vec<usize>) -> Vec<usize> {
1234        if peaks.is_empty() {
1235            return peaks;
1236        }
1237
1238        let mut filtered_peaks = vec![peaks[0]];
1239
1240        for &peak in peaks.iter().skip(1) {
1241            if peak.saturating_sub(
1242                *filtered_peaks
1243                    .last()
1244                    .expect("collection should not be empty"),
1245            ) >= self.min_distance
1246            {
1247                filtered_peaks.push(peak);
1248            }
1249        }
1250
1251        filtered_peaks
1252    }
1253}
1254
1255impl Default for ChangePointDetection {
1256    fn default() -> Self {
1257        Self::new()
1258    }
1259}
1260
1261/// Result of change point detection
1262#[derive(Debug, Clone)]
1263pub struct ChangePointResult {
1264    /// Detected change points (indices)
1265    pub change_points: Vec<usize>,
1266    /// Change scores for all time points
1267    pub change_scores: Array1<Float>,
1268    /// Method used for detection
1269    pub method: ChangePointMethod,
1270}
1271
1272impl ChangePointResult {
1273    /// Get segments between change points
1274    pub fn segments(&self, series_length: usize) -> Vec<(usize, usize)> {
1275        let mut segments = Vec::new();
1276        let mut start = 0;
1277
1278        for &cp in &self.change_points {
1279            if cp > start {
1280                segments.push((start, cp));
1281                start = cp;
1282            }
1283        }
1284
1285        // Add final segment
1286        if start < series_length {
1287            segments.push((start, series_length));
1288        }
1289
1290        segments
1291    }
1292
1293    /// Get the most significant change points (top N)
1294    pub fn top_change_points(&self, n: usize) -> Vec<(usize, Float)> {
1295        let mut indexed_scores: Vec<(usize, Float)> = self
1296            .change_points
1297            .iter()
1298            .map(|&idx| (idx, self.change_scores[idx]))
1299            .collect();
1300
1301        indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1302        indexed_scores.truncate(n);
1303        indexed_scores
1304    }
1305}
1306
1307/// Advanced trend extraction methods
1308#[derive(Debug, Clone)]
1309pub struct TrendExtraction {
1310    /// Method for trend extraction
1311    pub method: TrendExtractionMethod,
1312    /// Smoothing parameter (method-dependent)
1313    pub smoothing_parameter: Float,
1314    /// Window size for local methods
1315    pub window_size: usize,
1316}
1317
1318/// Methods for trend extraction
1319#[derive(Debug, Clone, Copy)]
1320pub enum TrendExtractionMethod {
1321    /// Moving average
1322    MovingAverage,
1323    /// Exponential smoothing
1324    ExponentialSmoothing,
1325    /// Polynomial fitting
1326    PolynomialFit,
1327    /// LOWESS (Locally Weighted Scatterplot Smoothing)
1328    Lowess,
1329    /// Hodrick-Prescott filter
1330    HodrickPrescott,
1331    /// SSA-based trend extraction
1332    SSATrend,
1333}
1334
1335impl TrendExtraction {
1336    /// Create a new trend extraction instance
1337    pub fn new() -> Self {
1338        Self {
1339            method: TrendExtractionMethod::MovingAverage,
1340            smoothing_parameter: 0.1,
1341            window_size: 10,
1342        }
1343    }
1344
1345    /// Set the extraction method
1346    pub fn method(mut self, method: TrendExtractionMethod) -> Self {
1347        self.method = method;
1348        self
1349    }
1350
1351    /// Set the smoothing parameter
1352    pub fn smoothing_parameter(mut self, smoothing_parameter: Float) -> Self {
1353        self.smoothing_parameter = smoothing_parameter;
1354        self
1355    }
1356
1357    /// Set the window size
1358    pub fn window_size(mut self, window_size: usize) -> Self {
1359        self.window_size = window_size;
1360        self
1361    }
1362
1363    /// Extract trend from time series
1364    pub fn extract_trend(&self, time_series: &Array1<Float>) -> Result<TrendExtractionResult> {
1365        let trend = match self.method {
1366            TrendExtractionMethod::MovingAverage => self.moving_average_trend(time_series)?,
1367            TrendExtractionMethod::ExponentialSmoothing => {
1368                self.exponential_smoothing_trend(time_series)?
1369            }
1370            TrendExtractionMethod::PolynomialFit => self.polynomial_fit_trend(time_series)?,
1371            TrendExtractionMethod::Lowess => self.lowess_trend(time_series)?,
1372            TrendExtractionMethod::HodrickPrescott => self.hodrick_prescott_trend(time_series)?,
1373            TrendExtractionMethod::SSATrend => self.ssa_trend(time_series)?,
1374        };
1375
1376        let detrended = time_series - &trend;
1377
1378        Ok(TrendExtractionResult {
1379            trend,
1380            detrended,
1381            method: self.method,
1382        })
1383    }
1384
1385    /// Moving average trend
1386    fn moving_average_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1387        let n = time_series.len();
1388        let mut trend = Array1::zeros(n);
1389        let half_window = self.window_size / 2;
1390
1391        for i in 0..n {
1392            let start = i.saturating_sub(half_window);
1393            let end = (i + half_window + 1).min(n);
1394
1395            let sum: Float = time_series
1396                .slice(scirs2_core::ndarray::s![start..end])
1397                .sum();
1398            let count = end - start;
1399            trend[i] = sum / count as Float;
1400        }
1401
1402        Ok(trend)
1403    }
1404
1405    /// Exponential smoothing trend
1406    fn exponential_smoothing_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1407        let n = time_series.len();
1408        let mut trend = Array1::zeros(n);
1409        let alpha = self.smoothing_parameter;
1410
1411        if n > 0 {
1412            trend[0] = time_series[0];
1413
1414            for i in 1..n {
1415                trend[i] = alpha * time_series[i] + (1.0 - alpha) * trend[i - 1];
1416            }
1417        }
1418
1419        Ok(trend)
1420    }
1421
1422    /// Polynomial fit trend
1423    fn polynomial_fit_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1424        let n = time_series.len();
1425        let _degree = (self.smoothing_parameter * 10.0) as usize + 1; // Convert smoothing param to polynomial degree
1426
1427        // Fit polynomial using least squares (simplified to linear for now)
1428        let x_mean = (n - 1) as Float / 2.0;
1429        let y_mean = time_series.mean().unwrap_or(0.0);
1430
1431        let mut numerator = 0.0;
1432        let mut denominator = 0.0;
1433
1434        for i in 0..n {
1435            let x = i as Float;
1436            let y = time_series[i];
1437            let x_diff = x - x_mean;
1438            numerator += x_diff * (y - y_mean);
1439            denominator += x_diff * x_diff;
1440        }
1441
1442        let slope = if denominator.abs() > 1e-10 {
1443            numerator / denominator
1444        } else {
1445            0.0
1446        };
1447        let intercept = y_mean - slope * x_mean;
1448
1449        let mut trend = Array1::zeros(n);
1450        for i in 0..n {
1451            trend[i] = intercept + slope * i as Float;
1452        }
1453
1454        Ok(trend)
1455    }
1456
1457    /// LOWESS trend (simplified implementation)
1458    fn lowess_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1459        // For simplicity, use weighted moving average
1460        let n = time_series.len();
1461        let mut trend = Array1::zeros(n);
1462        let bandwidth = (self.smoothing_parameter * n as Float) as usize;
1463
1464        for i in 0..n {
1465            let mut weighted_sum = 0.0;
1466            let mut weight_sum = 0.0;
1467
1468            for j in 0..n {
1469                let distance = (i as Float - j as Float).abs();
1470                let normalized_distance = distance / bandwidth as Float;
1471
1472                // Tricube weight function
1473                let weight = if normalized_distance < 1.0 {
1474                    let u = 1.0 - normalized_distance * normalized_distance * normalized_distance;
1475                    u * u * u
1476                } else {
1477                    0.0
1478                };
1479
1480                weighted_sum += weight * time_series[j];
1481                weight_sum += weight;
1482            }
1483
1484            trend[i] = if weight_sum > 0.0 {
1485                weighted_sum / weight_sum
1486            } else {
1487                time_series[i]
1488            };
1489        }
1490
1491        Ok(trend)
1492    }
1493
1494    /// Hodrick-Prescott filter (simplified)
1495    fn hodrick_prescott_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1496        let n = time_series.len();
1497        if n < 3 {
1498            return Ok(time_series.clone());
1499        }
1500
1501        let lambda = self.smoothing_parameter * 100.0; // Scale smoothing parameter
1502        let mut trend = time_series.clone();
1503
1504        // Iterative solution (simplified)
1505        for _ in 0..10 {
1506            let mut new_trend = Array1::zeros(n);
1507
1508            // First point
1509            new_trend[0] =
1510                (time_series[0] + lambda * 2.0 * trend[1] - lambda * trend[2]) / (1.0 + lambda);
1511
1512            // Interior points
1513            for i in 1..n - 1 {
1514                let numerator = time_series[i] + lambda * (trend[i - 1] + trend[i + 1]);
1515                let denominator = 1.0 + 2.0 * lambda;
1516                new_trend[i] = numerator / denominator;
1517            }
1518
1519            // Last point
1520            new_trend[n - 1] = (time_series[n - 1] + lambda * 2.0 * trend[n - 2]
1521                - lambda * trend[n - 3])
1522                / (1.0 + lambda);
1523
1524            trend = new_trend;
1525        }
1526
1527        Ok(trend)
1528    }
1529
1530    /// SSA-based trend extraction
1531    fn ssa_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1532        let window_length = self.window_size.min(time_series.len() / 2);
1533        let mut ssa = SingularSpectrumAnalysis::new()
1534            .window_length(window_length)
1535            .n_components(1); // Extract only the first component (trend)
1536
1537        let result = ssa.fit_transform(time_series)?;
1538
1539        // Return the first component as trend
1540        if let Some(trend_component) = result.component(0) {
1541            Ok(trend_component)
1542        } else {
1543            Ok(Array1::zeros(time_series.len()))
1544        }
1545    }
1546}
1547
1548impl Default for TrendExtraction {
1549    fn default() -> Self {
1550        Self::new()
1551    }
1552}
1553
1554/// Result of trend extraction
1555#[derive(Debug, Clone)]
1556pub struct TrendExtractionResult {
1557    /// Extracted trend component
1558    pub trend: Array1<Float>,
1559    /// Detrended time series
1560    pub detrended: Array1<Float>,
1561    /// Method used for extraction
1562    pub method: TrendExtractionMethod,
1563}
1564
1565impl TrendExtractionResult {
1566    /// Reconstruct the original time series
1567    pub fn reconstruct(&self) -> Array1<Float> {
1568        &self.trend + &self.detrended
1569    }
1570
1571    /// Calculate trend strength (proportion of variance explained by trend)
1572    pub fn trend_strength(&self) -> Float {
1573        let original = self.reconstruct();
1574        let total_variance = self.calculate_variance(&original);
1575        let detrended_variance = self.calculate_variance(&self.detrended);
1576
1577        if total_variance > 0.0 {
1578            1.0 - (detrended_variance / total_variance)
1579        } else {
1580            0.0
1581        }
1582    }
1583
1584    /// Calculate variance
1585    fn calculate_variance(&self, signal: &Array1<Float>) -> Float {
1586        let mean = signal.mean().unwrap_or(0.0);
1587        signal
1588            .iter()
1589            .map(|&x| (x - mean) * (x - mean))
1590            .sum::<Float>()
1591            / signal.len() as Float
1592    }
1593
1594    /// Calculate trend slope (for linear trends)
1595    pub fn trend_slope(&self) -> Float {
1596        let n = self.trend.len();
1597        if n < 2 {
1598            return 0.0;
1599        }
1600
1601        let x_mean = (n - 1) as Float / 2.0;
1602        let y_mean = self.trend.mean().unwrap_or(0.0);
1603
1604        let mut numerator = 0.0;
1605        let mut denominator = 0.0;
1606
1607        for i in 0..n {
1608            let x = i as Float;
1609            let y = self.trend[i];
1610            let x_diff = x - x_mean;
1611            numerator += x_diff * (y - y_mean);
1612            denominator += x_diff * x_diff;
1613        }
1614
1615        if denominator.abs() > 1e-10 {
1616            numerator / denominator
1617        } else {
1618            0.0
1619        }
1620    }
1621}