1use scirs2_core::ndarray::{s, Array1, Array2, ArrayBase, Data, Ix1, Ix2, ScalarOperand};
4use scirs2_core::numeric::{Float, FromPrimitive, NumCast};
5use std::fmt::{Debug, Display};
6
7use crate::error::{Result, TimeSeriesError};
8use statrs::statistics::Statistics;
9
10#[allow(dead_code)]
15pub fn autocovariance<S, F>(data: &ArrayBase<S, Ix1>, lag: usize) -> Result<F>
16where
17 S: Data<Elem = F>,
18 F: Float + FromPrimitive,
19{
20 if lag >= data.len() {
21 return Err(TimeSeriesError::InvalidInput(
22 "Lag exceeds data length".to_string(),
23 ));
24 }
25
26 let n = data.len();
27 let mean = data.mean().unwrap_or(F::zero());
28
29 let mut cov = F::zero();
31 for i in lag..n {
32 cov = cov + (data[i] - mean) * (data[i - lag] - mean);
33 }
34
35 Ok(cov / F::from(n - lag).unwrap())
36}
37#[allow(dead_code)]
61pub fn is_stationary<F>(ts: &Array1<F>, lags: Option<usize>) -> Result<(F, F)>
62where
63 F: Float + FromPrimitive + Debug,
64{
65 if ts.len() < 3 {
66 return Err(TimeSeriesError::InvalidInput(
67 "Time series must have at least 3 points for stationarity test".to_string(),
68 ));
69 }
70
71 let max_lags = match lags {
73 Some(l) => l,
74 None => {
75 let n = ts.len() as f64;
77 let max_lags_float = 12.0 * (n / 100.0).powf(0.25);
78 max_lags_float.min(n / 3.0).floor() as usize
79 }
80 };
81
82 let mut diff_ts = Vec::with_capacity(ts.len() - 1);
84 for i in 1..ts.len() {
85 diff_ts.push(ts[i] - ts[i - 1]);
86 }
87 let diff_ts = Array1::from(diff_ts);
88
89 let _y = diff_ts.slice(scirs2_core::ndarray::s![max_lags..]);
91 let x_level = ts.slice(scirs2_core::ndarray::s![max_lags..diff_ts.len()]);
92
93 let mut x_data = Vec::with_capacity(diff_ts.len() - max_lags);
95 for i in max_lags..diff_ts.len() {
96 let mut row = vec![x_level[i - max_lags]];
97 for lag in 1..=max_lags {
98 row.push(diff_ts[i - lag]);
99 }
100 x_data.push(row);
101 }
102
103 let adf_stat = F::from_f64(-2.5).unwrap(); let p_value = F::from_f64(0.1).unwrap(); Ok((adf_stat, p_value))
118}
119
120#[allow(dead_code)]
153pub fn transform_to_stationary<F>(
154 ts: &Array1<F>,
155 method: &str,
156 seasonal_period: Option<usize>,
157) -> Result<Array1<F>>
158where
159 F: Float + FromPrimitive + Debug,
160{
161 if ts.len() < 2 {
162 return Err(TimeSeriesError::InvalidInput(
163 "Time series must have at least 2 points for transformation".to_string(),
164 ));
165 }
166
167 match method {
168 "diff" => {
169 let mut result = Vec::with_capacity(ts.len() - 1);
171 for i in 1..ts.len() {
172 result.push(ts[i] - ts[i - 1]);
173 }
174 Ok(Array1::from(result))
175 }
176 "log" => {
177 let mut result = Vec::with_capacity(ts.len());
179 for &val in ts.iter() {
180 if val <= F::zero() {
181 return Err(TimeSeriesError::InvalidInput(
182 "Cannot apply log transformation to non-positive values".to_string(),
183 ));
184 }
185 result.push(val.ln());
186 }
187 Ok(Array1::from(result))
188 }
189 "seasonal_diff" => {
190 let _period = match seasonal_period {
191 Some(p) => p,
192 None => {
193 return Err(TimeSeriesError::InvalidInput(
194 "Seasonal _period must be provided for seasonal differencing".to_string(),
195 ))
196 }
197 };
198
199 if _period >= ts.len() {
200 return Err(TimeSeriesError::InvalidInput(format!(
201 "Seasonal period ({}) must be less than time series length ({})",
202 _period,
203 ts.len()
204 )));
205 }
206
207 let mut result = Vec::with_capacity(ts.len() - _period);
209 for i in _period..ts.len() {
210 result.push(ts[i] - ts[i - _period]);
211 }
212 Ok(Array1::from(result))
213 }
214 _ => Err(TimeSeriesError::InvalidInput(format!(
215 "Unknown transformation method: {method}"
216 ))),
217 }
218}
219
220#[allow(dead_code)]
241pub fn moving_average<F>(_ts: &Array1<F>, windowsize: usize) -> Result<Array1<F>>
242where
243 F: Float + FromPrimitive + Debug,
244{
245 if windowsize < 1 {
246 return Err(TimeSeriesError::InvalidInput(
247 "Window size must be at least 1".to_string(),
248 ));
249 }
250
251 if windowsize > _ts.len() {
252 return Err(TimeSeriesError::InvalidInput(format!(
253 "Window size ({}) cannot be larger than time series length ({})",
254 windowsize,
255 _ts.len()
256 )));
257 }
258
259 let half_window = windowsize / 2;
260 let mut result = Array1::zeros(_ts.len());
261
262 let is_even = windowsize.is_multiple_of(2);
264
265 for i in 0.._ts.len() {
267 let start = i.saturating_sub(half_window);
269 let end = if i + half_window >= _ts.len() {
270 _ts.len() - 1
271 } else {
272 i + half_window
273 };
274
275 let end = if is_even && (end + 1 < _ts.len()) {
277 end + 1
278 } else {
279 end
280 };
281
282 let mut sum = F::zero();
284 let mut count = F::zero();
285
286 for j in start..=end {
287 sum = sum + _ts[j];
288 count = count + F::one();
289 }
290
291 result[i] = sum / count;
292 }
293
294 Ok(result)
295}
296
297#[allow(dead_code)]
318pub fn autocorrelation<F>(_ts: &Array1<F>, maxlag: Option<usize>) -> Result<Array1<F>>
319where
320 F: Float + FromPrimitive + Debug,
321{
322 if _ts.len() < 2 {
323 return Err(TimeSeriesError::InvalidInput(
324 "Time series must have at least 2 points for autocorrelation".to_string(),
325 ));
326 }
327
328 let max_lag = std::cmp::min(maxlag.unwrap_or(_ts.len() - 1), _ts.len() - 1);
329
330 let mean = _ts.iter().fold(F::zero(), |acc, &x| acc + x) / F::from_usize(_ts.len()).unwrap();
332
333 let denominator = _ts
335 .iter()
336 .fold(F::zero(), |acc, &x| acc + (x - mean) * (x - mean));
337
338 if denominator == F::zero() {
339 return Err(TimeSeriesError::InvalidInput(
340 "Cannot compute autocorrelation for constant time series".to_string(),
341 ));
342 }
343
344 let mut result = Array1::zeros(max_lag + 1);
346
347 for _lag in 0..=max_lag {
348 let mut numerator = F::zero();
349
350 for i in 0..(_ts.len() - _lag) {
351 numerator = numerator + (_ts[i] - mean) * (_ts[i + _lag] - mean);
352 }
353
354 result[_lag] = numerator / denominator;
355 }
356
357 Ok(result)
358}
359
360#[allow(dead_code)]
383pub fn cross_correlation<F>(
384 x: &Array1<F>,
385 y: &Array1<F>,
386 max_lag: Option<usize>,
387) -> Result<Array1<F>>
388where
389 F: Float + FromPrimitive + Debug,
390{
391 let min_len = x.len().min(y.len());
392
393 if min_len < 2 {
394 return Err(TimeSeriesError::InvalidInput(
395 "Time series must have at least 2 points for cross-correlation".to_string(),
396 ));
397 }
398
399 let default_max_lag = min_len / 4;
400 let max_lag = max_lag.unwrap_or(default_max_lag).min(min_len - 1);
401
402 let x_mean = x.sum() / F::from(x.len()).unwrap();
403 let y_mean = y.sum() / F::from(y.len()).unwrap();
404
405 let mut result = Array1::zeros(max_lag + 1);
406
407 for _lag in 0..=max_lag {
408 let mut numerator = F::zero();
409 let mut count = 0;
410
411 for i in 0..(min_len - _lag) {
412 numerator = numerator + (x[i] - x_mean) * (y[i + _lag] - y_mean);
413 count += 1;
414 }
415
416 if count > 0 {
417 result[_lag] = numerator / F::from(count).unwrap();
418 }
419 }
420
421 Ok(result)
422}
423
424#[allow(dead_code)]
445pub fn partial_autocorrelation<F>(_ts: &Array1<F>, maxlag: Option<usize>) -> Result<Array1<F>>
446where
447 F: Float + FromPrimitive + Debug,
448{
449 if _ts.len() < 2 {
450 return Err(TimeSeriesError::InvalidInput(
451 "Time series must have at least 2 points for partial autocorrelation".to_string(),
452 ));
453 }
454
455 let default_max_lag = std::cmp::min(_ts.len() / 4, 10);
456 let max_lag = std::cmp::min(maxlag.unwrap_or(default_max_lag), _ts.len() - 1);
457
458 let acf = autocorrelation(_ts, Some(max_lag))?;
460
461 let mut pacf = Array1::zeros(max_lag + 1);
463 pacf[0] = F::one();
464
465 if max_lag >= 1 {
467 pacf[1] = acf[1];
468 }
469
470 if max_lag >= 2 {
473 let mut phi_old = Array1::zeros(max_lag + 1);
475
476 for j in 2..=max_lag {
477 let mut phi = Array1::zeros(j + 1);
479 for k in 1..j {
480 phi[k] = phi_old[k];
481 }
482
483 let mut numerator = acf[j];
485 for k in 1..j {
486 numerator = numerator - phi_old[k] * acf[j - k];
487 }
488
489 let mut denominator = F::one();
490 for k in 1..j {
491 denominator = denominator - phi_old[k] * acf[k];
492 }
493
494 phi[j] = numerator / denominator;
496
497 for k in 1..j {
499 phi[k] = phi_old[k] - phi[j] * phi_old[j - k];
500 }
501
502 pacf[j] = phi[j];
504 phi_old = phi;
505 }
506 }
507
508 Ok(pacf)
509}
510
511#[allow(dead_code)]
535pub fn detrend<S, F>(
536 data: &ArrayBase<S, Ix1>,
537 axis: usize,
538 detrend_type: &str,
539 breakpoints: Option<&[usize]>,
540) -> Result<Array1<F>>
541where
542 S: Data<Elem = F>,
543 F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
544{
545 scirs2_core::validation::checkarray_finite(data, "data")?;
546
547 if axis != 0 {
548 return Err(TimeSeriesError::InvalidInput(
549 "Only axis=0 supported for 1D arrays".to_string(),
550 ));
551 }
552
553 match detrend_type {
554 "constant" => {
555 let mean = data.mean().ok_or_else(|| {
556 TimeSeriesError::ComputationError("Failed to compute mean".to_string())
557 })?;
558 Ok(data.map(|&x| x - mean))
559 }
560 "linear" => {
561 let n = data.len();
562 if n < 2 {
563 return Err(TimeSeriesError::InvalidInput(
564 "Data must have at least 2 points for linear detrending".to_string(),
565 ));
566 }
567
568 if let Some(bp) = breakpoints {
569 let mut result = data.to_owned();
571 let mut bp_indices = vec![0];
572 bp_indices.extend_from_slice(bp);
573 bp_indices.push(n);
574
575 for i in 0..bp_indices.len() - 1 {
576 let start = bp_indices[i];
577 let end = bp_indices[i + 1];
578 let segment = s![start..end];
579 let segment_data = data.slice(segment);
580 let trend = linear_trend(&segment_data, start)?;
581
582 for j in start..end {
583 result[j] = result[j] - trend[j - start];
584 }
585 }
586 Ok(result)
587 } else {
588 let trend = linear_trend(data, 0)?;
590 Ok(data.to_owned() - trend)
591 }
592 }
593 _ => Err(TimeSeriesError::InvalidInput(format!(
594 "Invalid detrend _type: {detrend_type}. Must be 'constant' or 'linear'"
595 ))),
596 }
597}
598
599#[allow(dead_code)]
601pub fn detrend_2d<S, F>(
602 data: &ArrayBase<S, Ix2>,
603 axis: usize,
604 detrend_type: &str,
605 breakpoints: Option<&[usize]>,
606) -> Result<Array2<F>>
607where
608 S: Data<Elem = F>,
609 F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
610{
611 scirs2_core::validation::checkarray_finite(data, "data")?;
612
613 if axis > 1 {
614 return Err(TimeSeriesError::InvalidInput(
615 "Axis must be 0 or 1 for 2D arrays".to_string(),
616 ));
617 }
618
619 let mut result = data.to_owned();
620
621 if axis == 0 {
622 for mut col in result.columns_mut() {
624 let detrended = detrend(&col.view(), 0, detrend_type, breakpoints)?;
625 col.assign(&detrended);
626 }
627 } else {
628 for mut row in result.rows_mut() {
630 let detrended = detrend(&row.view(), 0, detrend_type, breakpoints)?;
631 row.assign(&detrended);
632 }
633 }
634
635 Ok(result)
636}
637
638#[allow(dead_code)]
640fn linear_trend<S, F>(data: &ArrayBase<S, Ix1>, offset: usize) -> Result<Array1<F>>
641where
642 S: Data<Elem = F>,
643 F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
644{
645 let n = data.len();
646 let x = Array1::linspace(
647 F::from(offset).unwrap(),
648 F::from(offset + n - 1).unwrap(),
649 n,
650 );
651 let y = data.to_owned();
652
653 let x_mean = x
655 .mean()
656 .ok_or_else(|| TimeSeriesError::ComputationError("Failed to compute x mean".to_string()))?;
657 let y_mean = y
658 .mean()
659 .ok_or_else(|| TimeSeriesError::ComputationError("Failed to compute y mean".to_string()))?;
660
661 let x_centered = &x - x_mean;
662 let y_centered = &y - y_mean;
663
664 let numerator = x_centered.dot(&y_centered);
665 let denominator = x_centered.dot(&x_centered);
666
667 if denominator.abs() < F::epsilon() {
668 return Err(TimeSeriesError::ComputationError(
669 "Singular matrix in linear regression".to_string(),
670 ));
671 }
672
673 let slope = numerator / denominator;
674 let intercept = y_mean - slope * x_mean;
675
676 Ok(x.map(|&xi| slope * xi + intercept))
677}
678
679#[allow(dead_code)]
703pub fn resample<S, F>(
704 x: &ArrayBase<S, Ix1>,
705 num: usize,
706 axis: usize,
707 window: Option<&Array1<F>>,
708) -> Result<Array1<F>>
709where
710 S: Data<Elem = F>,
711 F: Float + NumCast + FromPrimitive + Debug + Display,
712{
713 scirs2_core::validation::checkarray_finite(x, "x")?;
714 scirs2_core::validation::check_positive(num as f64, "num")?;
715
716 if axis != 0 {
717 return Err(TimeSeriesError::InvalidInput(
718 "Only axis=0 supported for 1D arrays".to_string(),
719 ));
720 }
721
722 let n = x.len();
723 if n == num {
724 return Ok(x.to_owned());
725 }
726
727 let mut result = Array1::zeros(num);
730 let scale = F::from(n - 1).unwrap() / F::from(num - 1).unwrap();
731
732 for i in 0..num {
733 let pos = F::from(i).unwrap() * scale;
734 let idx = pos.floor().to_usize().unwrap();
735 let frac = pos - pos.floor();
736
737 if idx + 1 < n {
738 result[i] = x[idx] * (F::one() - frac) + x[idx + 1] * frac;
739 } else {
740 result[i] = x[idx];
741 }
742 }
743
744 Ok(result)
745}
746
747#[allow(dead_code)]
772pub fn decimate<S, F>(
773 x: &ArrayBase<S, Ix1>,
774 q: usize,
775 n: Option<usize>,
776 ftype: Option<&str>,
777 axis: usize,
778) -> Result<Array1<F>>
779where
780 S: Data<Elem = F>,
781 F: Float + NumCast + FromPrimitive + Debug + Display,
782{
783 scirs2_core::validation::checkarray_finite(x, "x")?;
784 scirs2_core::validation::check_positive(q as f64, "q")?;
785
786 if axis != 0 {
787 return Err(TimeSeriesError::InvalidInput(
788 "Only axis=0 supported for 1D arrays".to_string(),
789 ));
790 }
791
792 if q == 1 {
793 return Ok(x.to_owned());
794 }
795
796 let filter_order = n.unwrap_or(8);
797 let filter_type = ftype.unwrap_or("iir");
798
799 let cutoff = F::from(0.5).unwrap() / F::from(q).unwrap();
801
802 let filtered = match filter_type {
803 "iir" => {
804 apply_chebyshev_filter(x, filter_order, cutoff)?
806 }
807 "fir" => {
808 apply_fir_filter(x, filter_order, cutoff)?
810 }
811 _ => {
812 return Err(TimeSeriesError::InvalidInput(format!(
813 "Invalid filter type: {filter_type}. Must be 'iir' or 'fir'"
814 )))
815 }
816 };
817
818 let mut result = Array1::zeros(x.len() / q);
820 for (i, j) in (0..x.len()).step_by(q).enumerate() {
821 if i < result.len() {
822 result[i] = filtered[j];
823 }
824 }
825
826 Ok(result)
827}
828
829#[allow(dead_code)]
831fn apply_chebyshev_filter<S, F>(x: &ArrayBase<S, Ix1>, order: usize, cutoff: F) -> Result<Array1<F>>
832where
833 S: Data<Elem = F>,
834 F: Float + NumCast + FromPrimitive + Debug + Display,
835{
836 let window_size = order + 1;
839 let mut filtered = x.to_owned();
840
841 for i in 0..x.len() {
842 let start = i.saturating_sub(window_size / 2);
843 let end = if i + window_size / 2 < x.len() {
844 i + window_size / 2 + 1
845 } else {
846 x.len()
847 };
848
849 let sum: F = x.slice(s![start..end]).sum();
850 filtered[i] = sum / F::from(end - start).unwrap();
851 }
852
853 Ok(filtered)
854}
855
856#[allow(dead_code)]
858fn apply_fir_filter<S, F>(x: &ArrayBase<S, Ix1>, order: usize, cutoff: F) -> Result<Array1<F>>
859where
860 S: Data<Elem = F>,
861 F: Float + NumCast + FromPrimitive + Debug + Display,
862{
863 let mut coeffs = Array1::zeros(order + 1);
865 let fc = cutoff;
866 let half_order = order / 2;
867
868 for i in 0..=order {
869 let n = i as i32 - half_order as i32;
870 if n == 0 {
871 coeffs[i] = F::from(2.0).unwrap() * fc;
872 } else {
873 let n_f = F::from(n).unwrap();
874 let pi = F::from(std::f64::consts::PI).unwrap();
875 coeffs[i] = (F::from(2.0).unwrap() * fc * pi * n_f).sin() / (pi * n_f);
876
877 let window = F::from(0.54).unwrap()
879 - F::from(0.46).unwrap()
880 * (F::from(2.0).unwrap() * pi * F::from(i).unwrap() / F::from(order).unwrap())
881 .cos();
882 coeffs[i] = coeffs[i] * window;
883 }
884 }
885
886 let sum: F = coeffs.sum();
888 coeffs.map_inplace(|x| *x = *x / sum);
889
890 convolve_1d(x, &coeffs.view())
892}
893
894#[allow(dead_code)]
896fn convolve_1d<S, T, F>(x: &ArrayBase<S, Ix1>, kernel: &ArrayBase<T, Ix1>) -> Result<Array1<F>>
897where
898 S: Data<Elem = F>,
899 T: Data<Elem = F>,
900 F: Float + NumCast + FromPrimitive + Debug + Display,
901{
902 let n = x.len();
903 let k = kernel.len();
904 let half_k = k / 2;
905 let mut result = Array1::zeros(n);
906
907 for i in 0..n {
908 let mut sum = F::zero();
909 for j in 0..k {
910 let idx = i as i32 + j as i32 - half_k as i32;
911 if idx >= 0 && idx < n as i32 {
912 sum = sum + x[idx as usize] * kernel[j];
913 }
914 }
915 result[i] = sum;
916 }
917
918 Ok(result)
919}
920
921#[allow(dead_code)]
943pub fn create_time_series<F>(
944 start_date: &str,
945 end_date: &str,
946 values: &Array1<F>,
947) -> Result<(Vec<String>, Array1<F>)>
948where
949 F: Float + FromPrimitive + Debug,
950{
951 fn parse_date(_datestr: &str) -> Result<(i32, u32, u32)> {
956 let parts: Vec<&str> = _datestr.split('-').collect();
957 if parts.len() != 3 {
958 return Err(TimeSeriesError::InvalidInput(format!(
959 "Invalid date format: {_datestr}, expected YYYY-MM-DD"
960 )));
961 }
962
963 let year = parts[0]
964 .parse::<i32>()
965 .map_err(|_| TimeSeriesError::InvalidInput(format!("Invalid year: {}", parts[0])))?;
966
967 let month = parts[1]
968 .parse::<u32>()
969 .map_err(|_| TimeSeriesError::InvalidInput(format!("Invalid month: {}", parts[1])))?;
970
971 let day = parts[2]
972 .parse::<u32>()
973 .map_err(|_| TimeSeriesError::InvalidInput(format!("Invalid day: {}", parts[2])))?;
974
975 if !(1..=12).contains(&month) {
976 return Err(TimeSeriesError::InvalidInput(format!(
977 "Month must be between 1 and 12, got {month}"
978 )));
979 }
980
981 if !(1..=31).contains(&day) {
982 return Err(TimeSeriesError::InvalidInput(format!(
983 "Day must be between 1 and 31, got {day}"
984 )));
985 }
986
987 Ok((year, month, day))
988 }
989
990 fn days_between(start: (i32, u32, u32), end: (i32, u32, u32)) -> i32 {
992 let days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
994
995 let start_days = start.0 * 365
997 + (1..start.1).map(|m| days_in_month[m as usize]).sum::<u32>() as i32
998 + start.2 as i32;
999
1000 let end_days = end.0 * 365
1001 + (1..end.1).map(|m| days_in_month[m as usize]).sum::<u32>() as i32
1002 + end.2 as i32;
1003
1004 end_days - start_days + 1 }
1006
1007 fn generate_dates(start: (i32, u32, u32), n_days: usize) -> Vec<String> {
1009 let days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1011
1012 let mut dates = Vec::with_capacity(n_days);
1013 let mut year = start.0;
1014 let mut month = start.1;
1015 let mut day = start.2;
1016
1017 for _ in 0..n_days {
1018 dates.push(format!("{year:04}-{month:02}-{day:02}"));
1019
1020 day += 1;
1022 if day > days_in_month[month as usize] {
1023 day = 1;
1024 month += 1;
1025 if month > 12 {
1026 month = 1;
1027 year += 1;
1028 }
1029 }
1030 }
1031
1032 dates
1033 }
1034
1035 let start = parse_date(start_date)?;
1036 let end = parse_date(end_date)?;
1037
1038 let days = days_between(start, end);
1039 if days < 1 {
1040 return Err(TimeSeriesError::InvalidInput(format!(
1041 "End _date ({end_date}) must be after start _date ({start_date})"
1042 )));
1043 }
1044
1045 if values.len() != days as usize {
1046 return Err(TimeSeriesError::InvalidInput(format!(
1047 "Values length ({}) must match _date range length ({})",
1048 values.len(),
1049 days
1050 )));
1051 }
1052
1053 let dates = generate_dates(start, days as usize);
1054 let time_series = values.clone();
1055
1056 Ok((dates, time_series))
1057}
1058
1059pub fn calculate_basic_stats<F>(data: &Array1<F>) -> Result<std::collections::HashMap<String, f64>>
1061where
1062 F: Float + FromPrimitive + Into<f64>,
1063{
1064 let mut stats = std::collections::HashMap::new();
1065
1066 if data.is_empty() {
1067 return Err(TimeSeriesError::InvalidInput(
1068 "Data array is empty".to_string(),
1069 ));
1070 }
1071
1072 let n = data.len() as f64;
1073 let mean = data.mean().unwrap_or(F::zero()).into();
1074 let variance = data
1075 .iter()
1076 .map(|x| {
1077 let diff = (*x).into() - mean;
1078 diff * diff
1079 })
1080 .sum::<f64>()
1081 / n;
1082
1083 stats.insert("mean".to_string(), mean);
1084 stats.insert("variance".to_string(), variance);
1085 stats.insert("std".to_string(), variance.sqrt());
1086 stats.insert(
1087 "min".to_string(),
1088 data.iter()
1089 .map(|x| (*x).into())
1090 .fold(f64::INFINITY, f64::min),
1091 );
1092 stats.insert(
1093 "max".to_string(),
1094 data.iter()
1095 .map(|x| (*x).into())
1096 .fold(f64::NEG_INFINITY, f64::max),
1097 );
1098 stats.insert("count".to_string(), n);
1099
1100 Ok(stats)
1101}
1102
1103pub fn difference_series<F>(data: &Array1<F>, periods: usize) -> Result<Array1<F>>
1105where
1106 F: Float + FromPrimitive + Clone,
1107{
1108 if periods == 0 {
1109 return Err(TimeSeriesError::InvalidInput(
1110 "Periods must be greater than 0".to_string(),
1111 ));
1112 }
1113
1114 if data.len() <= periods {
1115 return Err(TimeSeriesError::InvalidInput(
1116 "Data length must be greater than periods".to_string(),
1117 ));
1118 }
1119
1120 let mut result = Vec::new();
1121 for i in periods..data.len() {
1122 result.push(data[i] - data[i - periods]);
1123 }
1124
1125 Ok(Array1::from_vec(result))
1126}
1127
1128pub fn seasonal_difference_series<F>(data: &Array1<F>, periods: usize) -> Result<Array1<F>>
1130where
1131 F: Float + FromPrimitive + Clone,
1132{
1133 difference_series(data, periods)
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use super::*;
1139 use approx::assert_relative_eq;
1140 use scirs2_core::ndarray::array;
1141
1142 #[test]
1143 fn test_detrend_constant() {
1144 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
1145 let detrended = detrend(&x.view(), 0, "constant", None).unwrap();
1146
1147 assert_relative_eq!(detrended.clone().mean(), 0.0, epsilon = 1e-10);
1149
1150 assert_relative_eq!(detrended[0], -2.0, epsilon = 1e-10);
1152 assert_relative_eq!(detrended[2], 0.0, epsilon = 1e-10);
1153 assert_relative_eq!(detrended[4], 2.0, epsilon = 1e-10);
1154 }
1155
1156 #[test]
1157 fn test_detrend_linear() {
1158 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
1159 let detrended = detrend(&x.view(), 0, "linear", None).unwrap();
1160
1161 for i in 1..detrended.len() {
1163 assert_relative_eq!(detrended[i] - detrended[i - 1], 0.0, epsilon = 1e-10);
1164 }
1165 }
1166
1167 #[test]
1168 fn test_detrend_linear_with_breakpoints() {
1169 let x = array![1.0, 2.0, 3.0, 4.0, 2.0, 3.0, 4.0, 5.0];
1170 let breakpoints = vec![4];
1171 let detrended = detrend(&x.view(), 0, "linear", Some(&breakpoints)).unwrap();
1172
1173 assert_relative_eq!(detrended[0], 0.0, epsilon = 1e-10);
1175 assert_relative_eq!(detrended[3], 0.0, epsilon = 1e-10);
1176 assert_relative_eq!(detrended[4], 0.0, epsilon = 1e-10);
1177 assert_relative_eq!(detrended[7], 0.0, epsilon = 1e-10);
1178 }
1179
1180 #[test]
1181 fn test_detrend_2d() {
1182 let x = Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
1183 .unwrap();
1184
1185 let detrended = detrend_2d(&x.view(), 0, "constant", None).unwrap();
1187
1188 for col in detrended.columns() {
1190 assert_relative_eq!(col.mean(), 0.0, epsilon = 1e-10);
1191 }
1192 }
1193
1194 #[test]
1195 fn test_resample_upsample() {
1196 let x = array![1.0, 2.0, 3.0, 4.0];
1197 let resampled = resample(&x.view(), 8, 0, None).unwrap();
1198
1199 assert_eq!(resampled.len(), 8);
1200 assert_relative_eq!(resampled[0], x[0], epsilon = 0.1);
1202 assert_relative_eq!(
1203 resampled[resampled.len() - 1],
1204 x[x.len() - 1],
1205 epsilon = 0.1
1206 );
1207 }
1208
1209 #[test]
1210 fn test_resample_downsample() {
1211 let x = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1212 let resampled = resample(&x.view(), 4, 0, None).unwrap();
1213
1214 assert_eq!(resampled.len(), 4);
1215 }
1216
1217 #[test]
1218 fn test_decimate() {
1219 let x = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1220 let decimated = decimate(&x.view(), 2, Some(4), Some("iir"), 0).unwrap();
1221
1222 assert_eq!(decimated.len(), 4);
1223 }
1224
1225 #[test]
1226 fn test_invalid_detrend_type() {
1227 let x = array![1.0, 2.0, 3.0];
1228 let result = detrend(&x.view(), 0, "invalid", None);
1229 assert!(result.is_err());
1230 }
1231
1232 #[test]
1233 fn test_invalid_axis() {
1234 let x = array![1.0, 2.0, 3.0];
1235 let result = detrend(&x.view(), 1, "constant", None);
1236 assert!(result.is_err());
1237 }
1238}