use crate::error::{Result, TimeSeriesError};
use scirs2_core::ndarray::ArrayStatCompat;
use scirs2_core::ndarray::{s, Array1, Array2, ArrayBase, Data, Ix1, Ix2, ScalarOperand};
use scirs2_core::numeric::{Float, FromPrimitive, NumCast};
use std::fmt::{Debug, Display};
#[allow(dead_code)]
pub fn autocovariance<S, F>(data: &ArrayBase<S, Ix1>, lag: usize) -> Result<F>
where
S: Data<Elem = F>,
F: Float + FromPrimitive,
{
if lag >= data.len() {
return Err(TimeSeriesError::InvalidInput(
"Lag exceeds data length".to_string(),
));
}
let n = data.len();
let mean = data.mean_or(F::zero());
let mut cov = F::zero();
for i in lag..n {
cov = cov + (data[i] - mean) * (data[i - lag] - mean);
}
Ok(cov / F::from(n - lag).expect("Failed to convert to float"))
}
#[allow(dead_code)]
pub fn is_stationary<F>(ts: &Array1<F>, lags: Option<usize>) -> Result<(F, F)>
where
F: Float + FromPrimitive + Debug,
{
if ts.len() < 3 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 3 points for stationarity test".to_string(),
));
}
let max_lags = match lags {
Some(l) => l,
None => {
let n = ts.len() as f64;
let max_lags_float = 12.0 * (n / 100.0).powf(0.25);
max_lags_float.min(n / 3.0).floor() as usize
}
};
let mut diff_ts = Vec::with_capacity(ts.len() - 1);
for i in 1..ts.len() {
diff_ts.push(ts[i] - ts[i - 1]);
}
let diff_ts = Array1::from(diff_ts);
let n_diff = diff_ts.len();
let n_obs = n_diff - max_lags;
let n_params = 2 + max_lags;
if n_obs <= n_params {
return Err(TimeSeriesError::InvalidInput(format!(
"Insufficient observations ({n_obs}) for ADF regression with {n_params} parameters; \
provide a longer series or fewer lags"
)));
}
let mut x = Array2::<F>::zeros((n_obs, n_params));
let mut y = Array1::<F>::zeros(n_obs);
for (row, k) in (max_lags..n_diff).enumerate() {
y[row] = diff_ts[k];
x[[row, 0]] = F::one(); x[[row, 1]] = ts[k]; for j in 1..=max_lags {
x[[row, 1 + j]] = diff_ts[k - j];
}
}
let mut xt_x = Array2::<F>::zeros((n_params, n_params));
for a in 0..n_params {
for b in a..n_params {
let mut acc = F::zero();
for row in 0..n_obs {
acc = acc + x[[row, a]] * x[[row, b]];
}
xt_x[[a, b]] = acc;
xt_x[[b, a]] = acc; }
}
let mut xt_y = Array1::<F>::zeros(n_params);
for a in 0..n_params {
let mut acc = F::zero();
for row in 0..n_obs {
acc = acc + x[[row, a]] * y[row];
}
xt_y[a] = acc;
}
let xt_x_inv = pseudo_inverse_spd(&xt_x);
let mut beta = Array1::<F>::zeros(n_params);
for a in 0..n_params {
let mut acc = F::zero();
for b in 0..n_params {
acc = acc + xt_x_inv[[a, b]] * xt_y[b];
}
beta[a] = acc;
}
let mut rss = F::zero();
for row in 0..n_obs {
let mut fitted = F::zero();
for a in 0..n_params {
fitted = fitted + x[[row, a]] * beta[a];
}
let resid = y[row] - fitted;
rss = rss + resid * resid;
}
let dof = F::from_usize(n_obs - n_params)
.ok_or_else(|| TimeSeriesError::InvalidInput("degrees of freedom overflow".to_string()))?;
let sigma2 = rss / dof;
let var_beta = sigma2 * xt_x_inv[[1, 1]];
if var_beta > F::zero() {
let se_beta = var_beta.sqrt();
let mut adf_stat = beta[1] / se_beta;
if !adf_stat.is_finite() {
adf_stat = F::zero();
}
let p_value = mackinnon_p_value(adf_stat);
return Ok((adf_stat, p_value));
}
let n_input = F::from_usize(ts.len()).unwrap_or_else(F::one);
let mut sum = F::zero();
for &val in ts.iter() {
sum = sum + val;
}
let mean = sum / n_input;
let mut sse = F::zero();
for &val in ts.iter() {
let centered = val - mean;
sse = sse + centered * centered;
}
let input_var = sse / n_input;
let var_tol = F::from_f64(1e-12).unwrap_or_else(F::epsilon);
if input_var <= var_tol {
let stat = F::from_f64(-1e6).unwrap_or_else(F::min_value);
return Ok((stat, F::zero()));
}
let stat = F::zero();
Ok((stat, mackinnon_p_value(stat)))
}
#[allow(dead_code)]
fn jacobi_eigen_symmetric<F>(a: &Array2<F>) -> (Array1<F>, Array2<F>)
where
F: Float + FromPrimitive + Debug,
{
let n = a.nrows();
let mut v = Array2::<F>::zeros((n, n));
for i in 0..n {
v[[i, i]] = F::one();
}
if n == 0 {
return (Array1::<F>::zeros(0), v);
}
let mut m = a.clone();
if n == 1 {
let mut eig = Array1::<F>::zeros(1);
eig[0] = m[[0, 0]];
return (eig, v);
}
let half = F::from_f64(0.5).unwrap_or_else(|| F::one() / (F::one() + F::one()));
let hundred = F::from_f64(100.0).unwrap_or_else(F::one);
let frac = F::from_f64(0.2).unwrap_or_else(F::zero);
let eps = F::epsilon();
const MAX_SWEEPS: usize = 100;
for sweep in 0..MAX_SWEEPS {
let mut off = F::zero();
let mut diag = F::zero();
for p in 0..n {
diag = diag + m[[p, p]].abs();
for q in (p + 1)..n {
off = off + m[[p, q]].abs();
}
}
if off <= eps * diag {
break;
}
let n_sq = F::from_usize(n * n).unwrap_or_else(F::one);
let thresh = if sweep < 3 {
frac * off / n_sq
} else {
F::zero()
};
for p in 0..n {
for q in (p + 1)..n {
let apq = m[[p, q]];
let g = hundred * apq.abs();
let app = m[[p, p]];
let aqq = m[[q, q]];
if sweep > 4 && (app.abs() + g == app.abs()) && (aqq.abs() + g == aqq.abs()) {
m[[p, q]] = F::zero();
continue;
}
if apq.abs() <= thresh {
continue;
}
let h = aqq - app;
let t = if h.abs() + g == h.abs() {
apq / h
} else {
let theta = half * h / apq;
let denom = theta.abs() + (F::one() + theta * theta).sqrt();
let magnitude = if denom > F::zero() {
F::one() / denom
} else {
F::zero()
};
if theta < F::zero() {
-magnitude
} else {
magnitude
}
};
let c = F::one() / (F::one() + t * t).sqrt();
let s = t * c;
let tau = s / (F::one() + c);
let delta = t * apq;
m[[p, p]] = app - delta;
m[[q, q]] = aqq + delta;
m[[p, q]] = F::zero();
for j in 0..p {
let g1 = m[[j, p]];
let h1 = m[[j, q]];
m[[j, p]] = g1 - s * (h1 + g1 * tau);
m[[j, q]] = h1 + s * (g1 - h1 * tau);
}
for j in (p + 1)..q {
let g1 = m[[p, j]];
let h1 = m[[j, q]];
m[[p, j]] = g1 - s * (h1 + g1 * tau);
m[[j, q]] = h1 + s * (g1 - h1 * tau);
}
for j in (q + 1)..n {
let g1 = m[[p, j]];
let h1 = m[[q, j]];
m[[p, j]] = g1 - s * (h1 + g1 * tau);
m[[q, j]] = h1 + s * (g1 - h1 * tau);
}
for j in 0..n {
let g1 = v[[j, p]];
let h1 = v[[j, q]];
v[[j, p]] = g1 - s * (h1 + g1 * tau);
v[[j, q]] = h1 + s * (g1 - h1 * tau);
}
}
}
}
let mut eig = Array1::<F>::zeros(n);
for (i, value) in eig.iter_mut().enumerate() {
*value = m[[i, i]];
}
(eig, v)
}
#[allow(dead_code)]
fn pseudo_inverse_spd<F>(a: &Array2<F>) -> Array2<F>
where
F: Float + FromPrimitive + Debug,
{
let (eig, v) = jacobi_eigen_symmetric(a);
let n = eig.len();
let mut lambda_max = F::zero();
for &lam in eig.iter() {
let mag = lam.abs();
if mag > lambda_max {
lambda_max = mag;
}
}
let rcond = F::from_f64(1e-12).unwrap_or_else(F::epsilon);
let cutoff = rcond * lambda_max;
let inv_eig = eig.mapv(|lam| {
if lam > cutoff {
F::one() / lam
} else {
F::zero()
}
});
let mut pinv = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in i..n {
let mut acc = F::zero();
for k in 0..n {
acc = acc + v[[i, k]] * inv_eig[k] * v[[j, k]];
}
pinv[[i, j]] = acc;
pinv[[j, i]] = acc;
}
}
pinv
}
#[allow(dead_code)]
pub(super) fn invert_matrix<F>(a: &Array2<F>) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = a.nrows();
if n != a.ncols() {
return Err(TimeSeriesError::InvalidInput(
"Matrix must be square to invert".to_string(),
));
}
let mut aug = Array2::<F>::zeros((n, 2 * n));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n + i]] = F::one();
}
let eps = F::from_f64(1e-12).unwrap_or_else(F::epsilon);
for col in 0..n {
let mut pivot_row = col;
let mut pivot_val = aug[[col, col]].abs();
for row in (col + 1)..n {
let val = aug[[row, col]].abs();
if val > pivot_val {
pivot_val = val;
pivot_row = row;
}
}
if pivot_val <= eps {
return Err(TimeSeriesError::NumericalInstability(
"Singular matrix encountered while inverting ADF normal equations".to_string(),
));
}
if pivot_row != col {
for j in 0..(2 * n) {
let tmp = aug[[col, j]];
aug[[col, j]] = aug[[pivot_row, j]];
aug[[pivot_row, j]] = tmp;
}
}
let pivot = aug[[col, col]];
for j in 0..(2 * n) {
aug[[col, j]] = aug[[col, j]] / pivot;
}
for row in 0..n {
if row == col {
continue;
}
let factor = aug[[row, col]];
if factor != F::zero() {
for j in 0..(2 * n) {
aug[[row, j]] = aug[[row, j]] - factor * aug[[col, j]];
}
}
}
}
let mut inv = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in 0..n {
inv[[i, j]] = aug[[i, n + j]];
}
}
Ok(inv)
}
#[allow(dead_code)]
fn mackinnon_p_value<F>(stat: F) -> F
where
F: Float + FromPrimitive + Debug,
{
let t = stat.to_f64().unwrap_or(0.0);
const TABLE: [(f64, f64); 7] = [
(0.01, -3.43),
(0.025, -3.12),
(0.05, -2.86),
(0.10, -2.57),
(0.50, -1.95),
(0.90, -1.14),
(0.99, -0.44),
];
let p = if t <= TABLE[0].1 {
let excess = TABLE[0].1 - t; (0.01 * (-1.2 * excess).exp()).max(1e-6)
} else if t >= TABLE[TABLE.len() - 1].1 {
let excess = t - TABLE[TABLE.len() - 1].1; (1.0 - 0.01 * (-1.0 * excess).exp()).min(1.0 - 1e-6)
} else {
let mut result = 0.5;
for w in TABLE.windows(2) {
let (p_lo, c_lo) = w[0];
let (p_hi, c_hi) = w[1];
if t >= c_lo && t <= c_hi {
let frac = (t - c_lo) / (c_hi - c_lo);
result = p_lo + frac * (p_hi - p_lo);
break;
}
}
result
};
F::from_f64(p).unwrap_or_else(|| F::from_f64(0.5).unwrap_or_else(F::zero))
}
#[allow(dead_code)]
pub fn transform_to_stationary<F>(
ts: &Array1<F>,
method: &str,
seasonal_period: Option<usize>,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if ts.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for transformation".to_string(),
));
}
match method {
"diff" => {
let mut result = Vec::with_capacity(ts.len() - 1);
for i in 1..ts.len() {
result.push(ts[i] - ts[i - 1]);
}
Ok(Array1::from(result))
}
"log" => {
let mut result = Vec::with_capacity(ts.len());
for &val in ts.iter() {
if val <= F::zero() {
return Err(TimeSeriesError::InvalidInput(
"Cannot apply log transformation to non-positive values".to_string(),
));
}
result.push(val.ln());
}
Ok(Array1::from(result))
}
"seasonal_diff" => {
let _period = match seasonal_period {
Some(p) => p,
None => {
return Err(TimeSeriesError::InvalidInput(
"Seasonal _period must be provided for seasonal differencing".to_string(),
))
}
};
if _period >= ts.len() {
return Err(TimeSeriesError::InvalidInput(format!(
"Seasonal period ({}) must be less than time series length ({})",
_period,
ts.len()
)));
}
let mut result = Vec::with_capacity(ts.len() - _period);
for i in _period..ts.len() {
result.push(ts[i] - ts[i - _period]);
}
Ok(Array1::from(result))
}
_ => Err(TimeSeriesError::InvalidInput(format!(
"Unknown transformation method: {method}"
))),
}
}
#[allow(dead_code)]
pub fn moving_average<F>(_ts: &Array1<F>, windowsize: usize) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if windowsize < 1 {
return Err(TimeSeriesError::InvalidInput(
"Window size must be at least 1".to_string(),
));
}
if windowsize > _ts.len() {
return Err(TimeSeriesError::InvalidInput(format!(
"Window size ({}) cannot be larger than time series length ({})",
windowsize,
_ts.len()
)));
}
let half_window = windowsize / 2;
let mut result = Array1::zeros(_ts.len());
let is_even = windowsize.is_multiple_of(2);
for i in 0.._ts.len() {
let start = i.saturating_sub(half_window);
let end = if i + half_window >= _ts.len() {
_ts.len() - 1
} else {
i + half_window
};
let end = if is_even && (end + 1 < _ts.len()) {
end + 1
} else {
end
};
let mut sum = F::zero();
let mut count = F::zero();
for j in start..=end {
sum = sum + _ts[j];
count = count + F::one();
}
result[i] = sum / count;
}
Ok(result)
}
#[allow(dead_code)]
pub fn autocorrelation<F>(_ts: &Array1<F>, maxlag: Option<usize>) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if _ts.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for autocorrelation".to_string(),
));
}
let max_lag = std::cmp::min(maxlag.unwrap_or(_ts.len() - 1), _ts.len() - 1);
let mean = _ts.iter().fold(F::zero(), |acc, &x| acc + x)
/ F::from_usize(_ts.len()).expect("Operation failed");
let denominator = _ts
.iter()
.fold(F::zero(), |acc, &x| acc + (x - mean) * (x - mean));
if denominator == F::zero() {
return Err(TimeSeriesError::InvalidInput(
"Cannot compute autocorrelation for constant time series".to_string(),
));
}
let mut result = Array1::zeros(max_lag + 1);
for _lag in 0..=max_lag {
let mut numerator = F::zero();
for i in 0..(_ts.len() - _lag) {
numerator = numerator + (_ts[i] - mean) * (_ts[i + _lag] - mean);
}
result[_lag] = numerator / denominator;
}
Ok(result)
}
#[allow(dead_code)]
pub fn cross_correlation<F>(
x: &Array1<F>,
y: &Array1<F>,
max_lag: Option<usize>,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
let min_len = x.len().min(y.len());
if min_len < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for cross-correlation".to_string(),
));
}
let default_max_lag = min_len / 4;
let max_lag = max_lag.unwrap_or(default_max_lag).min(min_len - 1);
let x_mean = x.sum() / F::from(x.len()).expect("Operation failed");
let y_mean = y.sum() / F::from(y.len()).expect("Operation failed");
let mut result = Array1::zeros(max_lag + 1);
for _lag in 0..=max_lag {
let mut numerator = F::zero();
let mut count = 0;
for i in 0..(min_len - _lag) {
numerator = numerator + (x[i] - x_mean) * (y[i + _lag] - y_mean);
count += 1;
}
if count > 0 {
result[_lag] = numerator / F::from(count).expect("Failed to convert to float");
}
}
Ok(result)
}
#[allow(dead_code)]
pub fn partial_autocorrelation<F>(_ts: &Array1<F>, maxlag: Option<usize>) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if _ts.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for partial autocorrelation".to_string(),
));
}
let default_max_lag = std::cmp::min(_ts.len() / 4, 10);
let max_lag = std::cmp::min(maxlag.unwrap_or(default_max_lag), _ts.len() - 1);
let acf = autocorrelation(_ts, Some(max_lag))?;
let mut pacf = Array1::zeros(max_lag + 1);
pacf[0] = F::one();
if max_lag >= 1 {
pacf[1] = acf[1];
}
if max_lag >= 2 {
let mut phi_old = Array1::zeros(max_lag + 1);
for j in 2..=max_lag {
let mut phi = Array1::zeros(j + 1);
for k in 1..j {
phi[k] = phi_old[k];
}
let mut numerator = acf[j];
for k in 1..j {
numerator = numerator - phi_old[k] * acf[j - k];
}
let mut denominator = F::one();
for k in 1..j {
denominator = denominator - phi_old[k] * acf[k];
}
phi[j] = numerator / denominator;
for k in 1..j {
phi[k] = phi_old[k] - phi[j] * phi_old[j - k];
}
pacf[j] = phi[j];
phi_old = phi;
}
}
Ok(pacf)
}
#[allow(dead_code)]
pub fn detrend<S, F>(
data: &ArrayBase<S, Ix1>,
axis: usize,
detrend_type: &str,
breakpoints: Option<&[usize]>,
) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
{
scirs2_core::validation::checkarray_finite(data, "data")?;
if axis != 0 {
return Err(TimeSeriesError::InvalidInput(
"Only axis=0 supported for 1D arrays".to_string(),
));
}
match detrend_type {
"constant" => {
let mean = data.mean().ok_or_else(|| {
TimeSeriesError::ComputationError("Failed to compute mean".to_string())
})?;
Ok(data.map(|&x| x - mean))
}
"linear" => {
let n = data.len();
if n < 2 {
return Err(TimeSeriesError::InvalidInput(
"Data must have at least 2 points for linear detrending".to_string(),
));
}
if let Some(bp) = breakpoints {
let mut result = data.to_owned();
let mut bp_indices = vec![0];
bp_indices.extend_from_slice(bp);
bp_indices.push(n);
for i in 0..bp_indices.len() - 1 {
let start = bp_indices[i];
let end = bp_indices[i + 1];
let segment = s![start..end];
let segment_data = data.slice(segment);
let trend = linear_trend(&segment_data, start)?;
for j in start..end {
result[j] = result[j] - trend[j - start];
}
}
Ok(result)
} else {
let trend = linear_trend(data, 0)?;
Ok(data.to_owned() - trend)
}
}
_ => Err(TimeSeriesError::InvalidInput(format!(
"Invalid detrend _type: {detrend_type}. Must be 'constant' or 'linear'"
))),
}
}
#[allow(dead_code)]
pub fn detrend_2d<S, F>(
data: &ArrayBase<S, Ix2>,
axis: usize,
detrend_type: &str,
breakpoints: Option<&[usize]>,
) -> Result<Array2<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
{
scirs2_core::validation::checkarray_finite(data, "data")?;
if axis > 1 {
return Err(TimeSeriesError::InvalidInput(
"Axis must be 0 or 1 for 2D arrays".to_string(),
));
}
let mut result = data.to_owned();
if axis == 0 {
for mut col in result.columns_mut() {
let detrended = detrend(&col.view(), 0, detrend_type, breakpoints)?;
col.assign(&detrended);
}
} else {
for mut row in result.rows_mut() {
let detrended = detrend(&row.view(), 0, detrend_type, breakpoints)?;
row.assign(&detrended);
}
}
Ok(result)
}
#[allow(dead_code)]
fn linear_trend<S, F>(data: &ArrayBase<S, Ix1>, offset: usize) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
{
let n = data.len();
let x = Array1::linspace(
F::from(offset).expect("Failed to convert to float"),
F::from(offset + n - 1).expect("Failed to convert to float"),
n,
);
let y = data.to_owned();
let x_mean = x
.mean()
.ok_or_else(|| TimeSeriesError::ComputationError("Failed to compute x mean".to_string()))?;
let y_mean = y
.mean()
.ok_or_else(|| TimeSeriesError::ComputationError("Failed to compute y mean".to_string()))?;
let x_centered = &x - x_mean;
let y_centered = &y - y_mean;
let numerator = x_centered.dot(&y_centered);
let denominator = x_centered.dot(&x_centered);
if denominator.abs() < F::epsilon() {
return Err(TimeSeriesError::ComputationError(
"Singular matrix in linear regression".to_string(),
));
}
let slope = numerator / denominator;
let intercept = y_mean - slope * x_mean;
Ok(x.map(|&xi| slope * xi + intercept))
}