1use scirs2_core::ndarray::{Array1, Array2, Axis};
11use sklears_core::{error::Result, error::SklearsError, types::Float};
12
13#[derive(Debug, Clone)]
15pub struct SsaConfig {
16 pub window_length: usize,
18 pub n_components: Option<usize>,
20 pub grouping: bool,
22 pub reconstruction_method: ReconstructionMethod,
24}
25
26#[derive(Debug, Clone, Copy)]
28pub enum ReconstructionMethod {
29 DiagonalAveraging,
31 WeightedAveraging,
33 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
48pub struct SingularSpectrumAnalysis {
67 config: SsaConfig,
68 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 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 pub fn window_length(mut self, window_length: usize) -> Self {
95 self.config.window_length = window_length;
96 self
97 }
98
99 pub fn n_components(mut self, n_components: usize) -> Self {
101 self.config.n_components = Some(n_components);
102 self
103 }
104
105 pub fn grouping(mut self, grouping: bool) -> Self {
107 self.config.grouping = grouping;
108 self
109 }
110
111 pub fn reconstruction_method(mut self, method: ReconstructionMethod) -> Self {
113 self.config.reconstruction_method = method;
114 self
115 }
116
117 pub fn fit_transform(&mut self, time_series: &Array1<Float>) -> Result<SsaResult> {
119 self.fit(time_series)?;
120 self.transform()
121 }
122
123 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 let trajectory_matrix = self.create_trajectory_matrix(time_series)?;
142 self.trajectory_matrix_ = Some(trajectory_matrix.clone());
143
144 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 let eigenvalues = s.mapv(|x| x * x);
153 self.eigenvalues_ = Some(eigenvalues);
154
155 let n_components = self
157 .config
158 .n_components
159 .unwrap_or(s.len().min(window_length));
160
161 let reconstructed_components = self.reconstruct_components(n_components)?;
163 self.components_ = Some(reconstructed_components.clone());
164
165 let reconstructed_series = self.diagonal_averaging(&reconstructed_components)?;
167 self.reconstructed_ = Some(reconstructed_series);
168
169 Ok(())
170 }
171
172 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 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 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 let cov_matrix = if m <= n {
230 matrix.dot(&matrix.t())
231 } else {
232 matrix.t().dot(matrix)
233 };
234
235 let (eigenvalues, eigenvectors) = self.eigendecomposition(&cov_matrix)?;
237
238 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 let u = if m <= n {
256 sorted_eigenvectors.clone()
257 } else {
258 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 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 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 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 let residual = matrix.dot(&v) - eigenvalue * &v;
325 if (residual.dot(&residual)).sqrt() < tolerance {
326 break;
327 }
328 }
329
330 for i in 1..n {
332 eigenvalues[i] = eigenvalues[0] * ((i + 1) as Float).recip();
333 }
334
335 Ok((eigenvalues, eigenvectors))
336 }
337
338 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 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 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 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 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 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 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 pub fn trajectory_matrix(&self) -> Option<&Array2<Float>> {
438 self.trajectory_matrix_.as_ref()
439 }
440
441 pub fn singular_values(&self) -> Option<&Array1<Float>> {
443 self.singular_values_.as_ref()
444 }
445
446 pub fn eigenvalues(&self) -> Option<&Array1<Float>> {
448 self.eigenvalues_.as_ref()
449 }
450
451 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 let (_n_components, series_length) = components.dim();
461 let mut forecast = Array1::zeros(n_ahead);
462
463 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#[derive(Debug, Clone)]
490pub struct SsaResult {
491 pub components: Array2<Float>,
493 pub reconstructed_series: Array2<Float>,
495 pub singular_values: Array1<Float>,
497 pub eigenvalues: Array1<Float>,
499 pub explained_variance_ratio: Array1<Float>,
501}
502
503impl SsaResult {
504 pub fn total_reconstruction(&self) -> Array1<Float> {
506 self.reconstructed_series.sum_axis(Axis(0))
507 }
508
509 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 pub fn trend(&self) -> Option<Array1<Float>> {
520 self.component(0)
521 }
522
523 pub fn periodic_components(&self, n_periodic: usize) -> Array2<Float> {
525 let start_idx = 1; 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 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
549pub struct MultiChannelSSA {
552 config: SsaConfig,
553 n_channels: usize,
554 channel_results: Vec<SsaResult>,
556}
557
558impl MultiChannelSSA {
559 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 pub fn window_length(mut self, window_length: usize) -> Self {
570 self.config.window_length = window_length;
571 self
572 }
573
574 pub fn n_components(mut self, n_components: usize) -> Self {
576 self.config.n_components = Some(n_components);
577 self
578 }
579
580 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 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 pub fn channel_result(&self, channel: usize) -> Option<&SsaResult> {
613 self.channel_results.get(channel)
614 }
615
616 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 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#[derive(Debug, Clone)]
674pub struct SeasonalDecomposition {
675 pub period: usize,
677 pub model: SeasonalModel,
679 pub trend_method: TrendMethod,
681}
682
683#[derive(Debug, Clone, Copy)]
685pub enum SeasonalModel {
686 Additive,
688 Multiplicative,
690}
691
692#[derive(Debug, Clone, Copy)]
694pub enum TrendMethod {
695 MovingAverage,
697 LinearRegression,
699 Lowess,
701}
702
703impl SeasonalDecomposition {
704 pub fn new(period: usize) -> Self {
706 Self {
707 period,
708 model: SeasonalModel::Additive,
709 trend_method: TrendMethod::MovingAverage,
710 }
711 }
712
713 pub fn model(mut self, model: SeasonalModel) -> Self {
715 self.model = model;
716 self
717 }
718
719 pub fn trend_method(mut self, method: TrendMethod) -> Self {
721 self.trend_method = method;
722 self
723 }
724
725 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 let trend = self.estimate_trend(time_series)?;
737
738 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 let seasonal = self.estimate_seasonal(&detrended)?;
756
757 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 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 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 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 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 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 fn lowess_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
850 self.moving_average_trend(time_series)
853 }
854
855 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 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 for i in 0..period {
873 if period_counts[i] > 0.0 {
874 period_averages[i] /= period_counts[i];
875 }
876 }
877
878 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 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#[derive(Debug, Clone)]
896pub struct SeasonalDecompositionResult {
897 pub trend: Array1<Float>,
899 pub seasonal: Array1<Float>,
901 pub residual: Array1<Float>,
903 pub model: SeasonalModel,
905}
906
907impl SeasonalDecompositionResult {
908 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 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 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#[derive(Debug, Clone)]
942pub struct ChangePointDetection {
943 pub method: ChangePointMethod,
945 pub window_size: usize,
947 pub threshold: Float,
949 pub min_distance: usize,
951}
952
953#[derive(Debug, Clone, Copy)]
955pub enum ChangePointMethod {
956 Variance,
958 Mean,
960 SSABased,
962 Spectral,
964}
965
966impl ChangePointDetection {
967 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 pub fn method(mut self, method: ChangePointMethod) -> Self {
979 self.method = method;
980 self
981 }
982
983 pub fn window_size(mut self, window_size: usize) -> Self {
985 self.window_size = window_size;
986 self
987 }
988
989 pub fn threshold(mut self, threshold: Float) -> Self {
991 self.threshold = threshold;
992 self
993 }
994
995 pub fn min_distance(mut self, min_distance: usize) -> Self {
997 self.min_distance = min_distance;
998 self
999 }
1000
1001 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 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 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 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 scores[i] = (left_mean - right_mean).abs();
1081 }
1082 }
1083
1084 Ok(scores)
1085 }
1086
1087 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 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 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 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 let left_psd = self.compute_psd(&left_segment);
1155 let right_psd = self.compute_psd(&right_segment);
1156
1157 let spectral_distance = self.spectral_distance(&left_psd, &right_psd);
1159 scores[i] = spectral_distance;
1160 }
1161 }
1162
1163 Ok(scores)
1164 }
1165
1166 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 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 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 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 fn find_peaks(&self, scores: &Array1<Float>) -> Vec<usize> {
1217 let n = scores.len();
1218 let mut peaks = Vec::new();
1219
1220 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 self.filter_peaks_by_distance(peaks)
1230 }
1231
1232 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#[derive(Debug, Clone)]
1263pub struct ChangePointResult {
1264 pub change_points: Vec<usize>,
1266 pub change_scores: Array1<Float>,
1268 pub method: ChangePointMethod,
1270}
1271
1272impl ChangePointResult {
1273 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 if start < series_length {
1287 segments.push((start, series_length));
1288 }
1289
1290 segments
1291 }
1292
1293 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#[derive(Debug, Clone)]
1309pub struct TrendExtraction {
1310 pub method: TrendExtractionMethod,
1312 pub smoothing_parameter: Float,
1314 pub window_size: usize,
1316}
1317
1318#[derive(Debug, Clone, Copy)]
1320pub enum TrendExtractionMethod {
1321 MovingAverage,
1323 ExponentialSmoothing,
1325 PolynomialFit,
1327 Lowess,
1329 HodrickPrescott,
1331 SSATrend,
1333}
1334
1335impl TrendExtraction {
1336 pub fn new() -> Self {
1338 Self {
1339 method: TrendExtractionMethod::MovingAverage,
1340 smoothing_parameter: 0.1,
1341 window_size: 10,
1342 }
1343 }
1344
1345 pub fn method(mut self, method: TrendExtractionMethod) -> Self {
1347 self.method = method;
1348 self
1349 }
1350
1351 pub fn smoothing_parameter(mut self, smoothing_parameter: Float) -> Self {
1353 self.smoothing_parameter = smoothing_parameter;
1354 self
1355 }
1356
1357 pub fn window_size(mut self, window_size: usize) -> Self {
1359 self.window_size = window_size;
1360 self
1361 }
1362
1363 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 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 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 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; 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 fn lowess_trend(&self, time_series: &Array1<Float>) -> Result<Array1<Float>> {
1459 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 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 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; let mut trend = time_series.clone();
1503
1504 for _ in 0..10 {
1506 let mut new_trend = Array1::zeros(n);
1507
1508 new_trend[0] =
1510 (time_series[0] + lambda * 2.0 * trend[1] - lambda * trend[2]) / (1.0 + lambda);
1511
1512 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 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 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); let result = ssa.fit_transform(time_series)?;
1538
1539 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#[derive(Debug, Clone)]
1556pub struct TrendExtractionResult {
1557 pub trend: Array1<Float>,
1559 pub detrended: Array1<Float>,
1561 pub method: TrendExtractionMethod,
1563}
1564
1565impl TrendExtractionResult {
1566 pub fn reconstruct(&self) -> Array1<Float> {
1568 &self.trend + &self.detrended
1569 }
1570
1571 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 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 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}