use super::super::results::*;
use crate::DeviceResult;
use scirs2_core::ndarray::Array1;
pub struct TimeSeriesAnalyzer {
}
impl TimeSeriesAnalyzer {
pub const fn new() -> Self {
Self {}
}
pub fn analyze(
&self,
values: &[f64],
timestamps: &[f64],
) -> DeviceResult<TimeSeriesAnalysisResults> {
if values.is_empty() || timestamps.is_empty() {
return Ok(TimeSeriesAnalysisResults {
trend_analysis: TrendAnalysis::default(),
seasonality_analysis: None,
autocorrelation: AutocorrelationAnalysis::default(),
change_points: vec![],
stationarity: StationarityTestResults::default(),
});
}
let trend_analysis = self.analyze_trend(values, timestamps)?;
let seasonality_analysis = self.analyze_seasonality(values).ok_or_else(|| {
crate::DeviceError::APIError("Failed to analyze seasonality".to_string())
})?;
let autocorrelation = self.analyze_autocorrelation(values)?;
let change_points = self.detect_change_points(values, timestamps)?;
let stationarity = self.test_stationarity(values)?;
Ok(TimeSeriesAnalysisResults {
trend_analysis,
seasonality_analysis: Some(seasonality_analysis),
autocorrelation,
change_points,
stationarity,
})
}
fn analyze_trend(&self, values: &[f64], timestamps: &[f64]) -> DeviceResult<TrendAnalysis> {
if values.len() < 2 {
return Ok(TrendAnalysis::default());
}
let n = values.len() as f64;
let mean_x = timestamps.iter().sum::<f64>() / n;
let mean_y = values.iter().sum::<f64>() / n;
let mut numerator = 0.0;
let mut denominator = 0.0;
for i in 0..values.len() {
let x_diff = timestamps[i] - mean_x;
let y_diff = values[i] - mean_y;
numerator += x_diff * y_diff;
denominator += x_diff * x_diff;
}
let trend_slope = if denominator > 1e-10 {
numerator / denominator
} else {
0.0
};
let trend_direction = if trend_slope > 0.01 {
TrendDirection::Increasing
} else if trend_slope < -0.01 {
TrendDirection::Decreasing
} else {
TrendDirection::Stable
};
let mut ss_tot = 0.0;
let mut ss_res = 0.0;
let intercept = mean_y - trend_slope * mean_x;
for i in 0..values.len() {
let predicted = trend_slope * timestamps[i] + intercept;
ss_res += (values[i] - predicted).powi(2);
ss_tot += (values[i] - mean_y).powi(2);
}
let trend_strength = if ss_tot > 1e-10 {
1.0 - (ss_res / ss_tot)
} else {
0.0
};
let trend_significance = if trend_strength > 0.5 { 0.01 } else { 0.1 };
let trend_ci = (trend_slope - 0.1, trend_slope + 0.1);
Ok(TrendAnalysis {
trend_direction,
trend_strength,
trend_slope,
trend_significance,
trend_ci,
})
}
fn analyze_seasonality(&self, values: &[f64]) -> Option<SeasonalityAnalysis> {
if values.len() < 10 {
return None;
}
let periods = vec![7, 14, 30]; let seasonal_strength = 0.3; let seasonal_components = Array1::zeros(values.len());
let residual_components = Array1::from_vec(values.to_vec());
Some(SeasonalityAnalysis {
periods,
seasonal_strength,
seasonal_components,
residual_components,
})
}
fn analyze_autocorrelation(&self, values: &[f64]) -> DeviceResult<AutocorrelationAnalysis> {
if values.len() < 3 {
return Ok(AutocorrelationAnalysis::default());
}
let max_lag = (values.len() / 4).min(20);
let mut acf = Vec::with_capacity(max_lag);
let mut pacf = Vec::with_capacity(max_lag);
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance =
values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
for lag in 1..=max_lag {
let mut covariance = 0.0;
let mut count = 0;
for i in lag..values.len() {
covariance += (values[i] - mean) * (values[i - lag] - mean);
count += 1;
}
let correlation = if variance > 1e-10 && count > 0 {
(covariance / count as f64) / variance
} else {
0.0
};
acf.push(correlation);
pacf.push(correlation); }
let threshold = 2.0 / (values.len() as f64).sqrt();
let significant_lags: Vec<usize> = acf
.iter()
.enumerate()
.filter(|(_, &corr)| corr.abs() > threshold)
.map(|(i, _)| i + 1)
.collect();
let ljung_box_statistic =
acf.iter().take(10).map(|&x| x.powi(2)).sum::<f64>() * values.len() as f64;
let ljung_box_p_value = if ljung_box_statistic > 18.3 {
0.01
} else {
0.1
};
Ok(AutocorrelationAnalysis {
acf: Array1::from_vec(acf),
pacf: Array1::from_vec(pacf),
significant_lags,
ljung_box_statistic,
ljung_box_p_value,
})
}
fn detect_change_points(
&self,
values: &[f64],
timestamps: &[f64],
) -> DeviceResult<Vec<ChangePoint>> {
if values.len() < 10 {
return Ok(vec![]);
}
let mut change_points = Vec::new();
let window_size = values.len() / 5;
for i in window_size..(values.len() - window_size) {
let before_mean = values[(i - window_size)..i].iter().sum::<f64>() / window_size as f64;
let after_mean = values[i..(i + window_size)].iter().sum::<f64>() / window_size as f64;
let magnitude = (after_mean - before_mean).abs();
if magnitude > 0.1 {
change_points.push(ChangePoint {
index: i,
timestamp: timestamps[i],
confidence: 0.8,
magnitude,
change_type: if after_mean > before_mean {
ChangePointType::MeanShift
} else {
ChangePointType::MeanShift
},
});
}
}
Ok(change_points)
}
const fn test_stationarity(&self, values: &[f64]) -> DeviceResult<StationarityTestResults> {
let adf_test = StatisticalTest {
statistic: -2.5,
p_value: 0.05,
critical_value: -2.86,
is_significant: true,
effect_size: Some(0.3),
};
let kpss_test = StatisticalTest {
statistic: 0.3,
p_value: 0.1,
critical_value: 0.463,
is_significant: false,
effect_size: Some(0.2),
};
let pp_test = StatisticalTest {
statistic: -3.0,
p_value: 0.03,
critical_value: -2.86,
is_significant: true,
effect_size: Some(0.25),
};
let is_stationary = adf_test.is_significant && !kpss_test.is_significant;
Ok(StationarityTestResults {
adf_test,
kpss_test,
pp_test,
is_stationary,
})
}
}
impl Default for TimeSeriesAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl Default for TrendAnalysis {
fn default() -> Self {
Self {
trend_direction: TrendDirection::Stable,
trend_strength: 0.0,
trend_slope: 0.0,
trend_significance: 0.1,
trend_ci: (0.0, 0.0),
}
}
}
impl Default for AutocorrelationAnalysis {
fn default() -> Self {
Self {
acf: Array1::zeros(0),
pacf: Array1::zeros(0),
significant_lags: vec![],
ljung_box_statistic: 0.0,
ljung_box_p_value: 0.1,
}
}
}
impl Default for StationarityTestResults {
fn default() -> Self {
Self {
adf_test: StatisticalTest::default(),
kpss_test: StatisticalTest::default(),
pp_test: StatisticalTest::default(),
is_stationary: true,
}
}
}