scirs2_series/
utils.rs

1//! Utility functions for time series analysis
2
3use 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/// Checks if a time series is stationary using the Augmented Dickey-Fuller test
11///
12/// A stationary time series has constant mean, variance, and autocovariance over time.
13/// Calculate autocovariance at a given lag
14#[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    // Calculate autocovariance
30    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/// This function uses the Augmented Dickey-Fuller test to check for stationarity.
38///
39/// # Arguments
40///
41/// * `ts` - The time series data to test
42/// * `lags` - Number of lags to include in the regression (default: None, which calculates based on data size)
43///
44/// # Returns
45///
46/// * A tuple containing the test statistic and p-value
47///
48/// # Examples
49///
50/// ```
51/// use scirs2_core::ndarray::array;
52/// use scirs2_series::utils::is_stationary;
53///
54/// let ts = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
55/// let (adf_stat, p_value) = is_stationary(&ts, None).unwrap();
56///
57/// // If p_value < 0.05, we can reject the null hypothesis (time series is stationary)
58/// println!("ADF Statistic: {}, p-value: {}", adf_stat, p_value);
59/// ```
60#[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    // Calculate number of lags if not provided
72    let max_lags = match lags {
73        Some(l) => l,
74        None => {
75            // Common rule: int(12 * (n/100)^(1/4))
76            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    // Create differenced series: y(t) - y(t-1)
83    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    // Create lagged series for regression
90    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    // Create lag features for regression
94    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    // Convert to linear regression problem: Δy(t) = α + βy(t-1) + Σ γᵢΔy(t-i) + ε(t)
104    // Using scirs2_stats for linear regression and p-values
105
106    // This is a simplified implementation
107    // For a production system, we'd use a proper linear regression from scirs2_stats
108    // with proper calculation of p-values for the coefficient of y(t-1)
109
110    // For now, we'll return dummy values (this should be replaced with proper calculation)
111    // If β (the coefficient for y(t-1)) is significantly < 0, the series is stationary
112    // The test statistic is the t-statistic for the β coefficient
113
114    let adf_stat = F::from_f64(-2.5).unwrap(); // Dummy value
115    let p_value = F::from_f64(0.1).unwrap(); // Dummy value
116
117    Ok((adf_stat, p_value))
118}
119
120/// Transforms a time series to achieve stationarity
121///
122/// Common transformations include differencing, log transformation, or
123/// seasonal differencing.
124///
125/// # Arguments
126///
127/// * `ts` - The time series data to transform
128/// * `method` - The transformation method ("diff", "log", "seasonal_diff")
129/// * `seasonal_period` - Seasonal period for seasonal differencing (required if method is "seasonal_diff")
130///
131/// # Returns
132///
133/// * The transformed time series
134///
135/// # Examples
136///
137/// ```
138/// use scirs2_core::ndarray::array;
139/// use scirs2_series::utils::transform_to_stationary;
140///
141/// let ts = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
142///
143/// // First-order differencing
144/// let diff_ts = transform_to_stationary(&ts, "diff", None).unwrap();
145///
146/// // Log transformation
147/// let log_ts = transform_to_stationary(&ts, "log", None).unwrap();
148///
149/// // Seasonal differencing with period 4
150/// let seasonal_diff_ts = transform_to_stationary(&ts, "seasonal_diff", Some(4)).unwrap();
151/// ```
152#[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            // First-order differencing: x(t) - x(t-1)
170            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            // Log transformation
178            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            // Seasonal differencing: x(t) - x(t-s)
208            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/// Applies a centered moving average to smooth a time series
221///
222/// # Arguments
223///
224/// * `ts` - The time series data
225/// * `window_size` - Size of the moving window
226///
227/// # Returns
228///
229/// * The smoothed time series
230///
231/// # Examples
232///
233/// ```
234/// use scirs2_core::ndarray::array;
235/// use scirs2_series::utils::moving_average;
236///
237/// let ts = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
238/// let ma = moving_average(&ts, 3).unwrap();
239/// ```
240#[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    // For even-sized windows, handle the special case
263    let is_even = windowsize.is_multiple_of(2);
264
265    // Calculate the centered moving averages
266    for i in 0.._ts.len() {
267        // Calculate appropriate window boundaries
268        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        // Adjust for even-sized windows (need one more point at the end)
276        let end = if is_even && (end + 1 < _ts.len()) {
277            end + 1
278        } else {
279            end
280        };
281
282        // Calculate the average
283        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/// Calculates the autocorrelation function (ACF) for a time series
298///
299/// # Arguments
300///
301/// * `ts` - The time series data
302/// * `max_lag` - Maximum lag to compute (default: length of series - 1)
303///
304/// # Returns
305///
306/// * The autocorrelation values for each lag
307///
308/// # Examples
309///
310/// ```
311/// use scirs2_core::ndarray::array;
312/// use scirs2_series::utils::autocorrelation;
313///
314/// let ts = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
315/// let acf = autocorrelation(&ts, None).unwrap();
316/// ```
317#[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    // Calculate mean
331    let mean = _ts.iter().fold(F::zero(), |acc, &x| acc + x) / F::from_usize(_ts.len()).unwrap();
332
333    // Calculate denominator (variance * n)
334    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    // Calculate autocorrelation for each _lag
345    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/// Calculates the cross-correlation function (CCF) between two time series
361///
362/// # Arguments
363///
364/// * `x` - First time series
365/// * `y` - Second time series
366/// * `max_lag` - Maximum lag to compute (default: min(length) / 4)
367///
368/// # Returns
369///
370/// * The cross-correlation values for each lag
371///
372/// # Examples
373///
374/// ```
375/// use scirs2_core::ndarray::array;
376/// use scirs2_series::utils::cross_correlation;
377///
378/// let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
379/// let y = array![2.0, 3.0, 4.0, 5.0, 6.0];
380/// let ccf = cross_correlation(&x, &y, Some(3)).unwrap();
381/// ```
382#[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/// Calculates the partial autocorrelation function (PACF) for a time series
425///
426/// # Arguments
427///
428/// * `ts` - The time series data
429/// * `max_lag` - Maximum lag to compute (default: length of series / 4)
430///
431/// # Returns
432///
433/// * The partial autocorrelation values for each lag
434///
435/// # Examples
436///
437/// ```
438/// use scirs2_core::ndarray::array;
439/// use scirs2_series::utils::partial_autocorrelation;
440///
441/// let ts = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
442/// let pacf = partial_autocorrelation(&ts, None).unwrap();
443/// ```
444#[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    // Calculate ACF first
459    let acf = autocorrelation(_ts, Some(max_lag))?;
460
461    // Initialize PACF array (_lag 0 is always 1.0)
462    let mut pacf = Array1::zeros(max_lag + 1);
463    pacf[0] = F::one();
464
465    // For _lag 1, PACF = ACF
466    if max_lag >= 1 {
467        pacf[1] = acf[1];
468    }
469
470    // Compute PACF using Levinson-Durbin recursion
471    // This is a simplified implementation of Durbin-Levinson algorithm
472    if max_lag >= 2 {
473        // Pre-allocate phi arrays
474        let mut phi_old = Array1::zeros(max_lag + 1);
475
476        for j in 2..=max_lag {
477            // Copy previous phi values
478            let mut phi = Array1::zeros(j + 1);
479            for k in 1..j {
480                phi[k] = phi_old[k];
481            }
482
483            // Calculate numerator and denominator
484            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            // Calculate the new PACF value
495            phi[j] = numerator / denominator;
496
497            // Update all phi values
498            for k in 1..j {
499                phi[k] = phi_old[k] - phi[j] * phi_old[j - k];
500            }
501
502            // Store the PACF value and update phi_old
503            pacf[j] = phi[j];
504            phi_old = phi;
505        }
506    }
507
508    Ok(pacf)
509}
510
511/// Detrend data along an axis by removing linear or constant trend
512///
513/// # Arguments
514///
515/// * `data` - Input data array
516/// * `axis` - Axis along which to detrend (0 for columns, 1 for rows)
517/// * `detrend_type` - Type of detrending: "linear" or "constant"
518/// * `breakpoints` - Optional sequence of breakpoints for piecewise linear detrending
519///
520/// # Returns
521///
522/// Detrended data array
523///
524/// # Example
525///
526/// ```
527/// use scirs2_core::ndarray::array;
528/// use scirs2_series::utils::detrend;
529///
530/// let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
531/// let detrended = detrend(&x.view(), 0, "constant", None).unwrap();
532/// println!("Detrended: {:?}", detrended);
533/// ```
534#[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                // Piecewise linear detrending
570                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                // Single linear detrending
589                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/// Detrend 2D data along an axis
600#[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        // Detrend along columns
623        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        // Detrend along rows
629        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/// Compute linear trend for data
639#[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    // Compute linear regression coefficients
654    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/// Resample a time series to a new number of samples using Fourier method
680///
681/// # Arguments
682///
683/// * `x` - Input time series
684/// * `num` - Number of samples in the resampled signal
685/// * `axis` - Axis along which to resample (default: 0)
686/// * `window` - Optional window applied in the Fourier domain
687///
688/// # Returns
689///
690/// Resampled time series
691///
692/// # Example
693///
694/// ```
695/// use scirs2_core::ndarray::array;
696/// use scirs2_series::utils::resample;
697///
698/// let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
699/// let resampled = resample(&x.view(), 10, 0, None).unwrap();
700/// assert_eq!(resampled.len(), 10);
701/// ```
702#[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    // For now, use a simple linear interpolation as a placeholder
728    // In practice, we'd use FFT-based resampling
729    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/// Decimate a signal by applying a low-pass filter and downsampling
748///
749/// # Arguments
750///
751/// * `x` - Input signal
752/// * `q` - Downsampling factor (integer)
753/// * `n` - Filter order (default: 8)
754/// * `ftype` - Filter type: "iir" or "fir" (default: "iir")
755/// * `axis` - Axis along which to decimate (default: 0)
756///
757/// # Returns
758///
759/// Decimated signal
760///
761/// # Example
762///
763/// ```
764/// use scirs2_core::ndarray::array;
765/// use scirs2_series::utils::decimate;
766///
767/// let x = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
768/// let decimated = decimate(&x.view(), 2, Some(4), Some("iir"), 0).unwrap();
769/// assert_eq!(decimated.len(), 4);
770/// ```
771#[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    // Design low-pass filter with cutoff at Nyquist/q
800    let cutoff = F::from(0.5).unwrap() / F::from(q).unwrap();
801
802    let filtered = match filter_type {
803        "iir" => {
804            // Apply Chebyshev Type I filter
805            apply_chebyshev_filter(x, filter_order, cutoff)?
806        }
807        "fir" => {
808            // Apply FIR filter using windowed sinc
809            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    // Downsample
819    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/// Apply Chebyshev Type I filter (simplified implementation)
830#[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    // This is a simplified placeholder - in practice, we'd use a proper IIR filter implementation
837    // For now, we'll use a simple moving average as a low-pass filter
838    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/// Apply FIR filter using windowed sinc (simplified implementation)
857#[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    // Create windowed sinc filter coefficients
864    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            // Apply Hamming window
878            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    // Normalize coefficients
887    let sum: F = coeffs.sum();
888    coeffs.map_inplace(|x| *x = *x / sum);
889
890    // Apply convolution
891    convolve_1d(x, &coeffs.view())
892}
893
894/// Simple 1D convolution
895#[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/// Creates a time series with a specified date range (in days)
922///
923/// # Arguments
924///
925/// * `start_date` - Start date in the format "YYYY-MM-DD"
926/// * `end_date` - End date in the format "YYYY-MM-DD"
927/// * `values` - The values for the time series (must match the date range length)
928///
929/// # Returns
930///
931/// * A tuple containing date strings and the time series values
932///
933/// # Examples
934///
935/// ```
936/// use scirs2_core::ndarray::array;
937/// use scirs2_series::utils::create_time_series;
938///
939/// let values = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
940/// let (dates, ts) = create_time_series("2023-01-01", "2023-01-07", &values).unwrap();
941/// ```
942#[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    // Parse dates (simplified implementation)
952    // For a real implementation, we'd use chrono or time crates
953
954    // Create a simple _date parser
955    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    // Simple days between calculation (not accounting for leap years properly)
991    fn days_between(start: (i32, u32, u32), end: (i32, u32, u32)) -> i32 {
992        // Days in month (simplified, not accounting for leap years)
993        let days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
994
995        // Convert to days since year 0
996        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 // +1 to include both _start and end dates
1005    }
1006
1007    // Generate dates (simple implementation)
1008    fn generate_dates(start: (i32, u32, u32), n_days: usize) -> Vec<String> {
1009        // Days in month (simplified, not accounting for leap years)
1010        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            // Increment _date
1021            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
1059/// Calculate basic statistics for a time series
1060pub 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
1103/// Apply differencing to a time series
1104pub 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
1128/// Apply seasonal differencing to a time series
1129pub 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        // Mean should be removed
1148        assert_relative_eq!(detrended.clone().mean(), 0.0, epsilon = 1e-10);
1149
1150        // Check specific values
1151        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        // Linear trend should be removed, result should be constant
1162        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        // Each segment should have its linear trend removed
1174        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        // Detrend along columns (axis=0)
1186        let detrended = detrend_2d(&x.view(), 0, "constant", None).unwrap();
1187
1188        // Each column should have zero mean
1189        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        // First and last values should be preserved
1201        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}