#[derive(Debug, Clone)]
pub struct LinearTrendResult {
pub slope: f64,
pub intercept: f64,
pub r_squared: f64,
pub stderr: f64,
pub p_value: f64,
}
pub fn linear_trend(series: &[f64]) -> LinearTrendResult {
if series.len() < 2 {
return LinearTrendResult {
slope: f64::NAN,
intercept: f64::NAN,
r_squared: f64::NAN,
stderr: f64::NAN,
p_value: f64::NAN,
};
}
let n = series.len() as f64;
let sum_x: f64 = (0..series.len()).map(|i| i as f64).sum();
let sum_y: f64 = series.iter().sum();
let sum_xy: f64 = series.iter().enumerate().map(|(i, &y)| i as f64 * y).sum();
let sum_x2: f64 = (0..series.len()).map(|i| (i * i) as f64).sum();
let mean_x = sum_x / n;
let mean_y = sum_y / n;
let ss_xx = sum_x2 - n * mean_x * mean_x;
let ss_xy = sum_xy - n * mean_x * mean_y;
if ss_xx.abs() < 1e-10 {
return LinearTrendResult {
slope: 0.0,
intercept: mean_y,
r_squared: 0.0,
stderr: f64::NAN,
p_value: 1.0,
};
}
let slope = ss_xy / ss_xx;
let intercept = mean_y - slope * mean_x;
let ss_yy: f64 = series.iter().map(|&y| (y - mean_y).powi(2)).sum();
let ss_res: f64 = series
.iter()
.enumerate()
.map(|(i, &y)| {
let y_pred = slope * i as f64 + intercept;
(y - y_pred).powi(2)
})
.sum();
let r_squared = if ss_yy.abs() < 1e-10 {
1.0 } else {
1.0 - ss_res / ss_yy
};
let mse = if n > 2.0 { ss_res / (n - 2.0) } else { 0.0 };
let stderr = if ss_xx > 0.0 {
(mse / ss_xx).sqrt()
} else {
f64::NAN
};
let t_stat = if stderr > 1e-10 {
slope / stderr
} else {
f64::INFINITY
};
let p_value = 2.0 * (1.0 - normal_cdf(t_stat.abs()));
LinearTrendResult {
slope,
intercept,
r_squared,
stderr,
p_value,
}
}
pub fn agg_linear_trend(series: &[f64], chunk_len: usize, agg_func: &str, attribute: &str) -> f64 {
if series.is_empty() || chunk_len == 0 || chunk_len > series.len() {
return f64::NAN;
}
let chunks: Vec<&[f64]> = series.chunks(chunk_len).collect();
let aggregated: Vec<f64> = chunks
.iter()
.map(|chunk| crate::utils::stats::aggregate(chunk, agg_func))
.filter(|v| !v.is_nan())
.collect();
if aggregated.len() < 2 {
return f64::NAN;
}
let x: Vec<f64> = (0..aggregated.len()).map(|i| i as f64).collect();
let trend = linear_regression(&x, &aggregated);
match attribute {
"slope" => trend.slope,
"intercept" => trend.intercept,
"rvalue" => trend.r_squared.sqrt(),
"r_squared" => trend.r_squared,
"pvalue" => trend.p_value,
"stderr" => trend.stderr,
_ => f64::NAN,
}
}
fn linear_regression(x: &[f64], y: &[f64]) -> LinearTrendResult {
if x.len() != y.len() || x.len() < 2 {
return LinearTrendResult {
slope: f64::NAN,
intercept: f64::NAN,
r_squared: f64::NAN,
stderr: f64::NAN,
p_value: f64::NAN,
};
}
let n = x.len() as f64;
let sum_x: f64 = x.iter().sum();
let sum_y: f64 = y.iter().sum();
let sum_xy: f64 = x.iter().zip(y.iter()).map(|(&xi, &yi)| xi * yi).sum();
let sum_x2: f64 = x.iter().map(|&xi| xi * xi).sum();
let mean_x = sum_x / n;
let mean_y = sum_y / n;
let ss_xx = sum_x2 - n * mean_x * mean_x;
let ss_xy = sum_xy - n * mean_x * mean_y;
if ss_xx.abs() < 1e-10 {
return LinearTrendResult {
slope: 0.0,
intercept: mean_y,
r_squared: 0.0,
stderr: f64::NAN,
p_value: 1.0,
};
}
let slope = ss_xy / ss_xx;
let intercept = mean_y - slope * mean_x;
let ss_yy: f64 = y.iter().map(|&yi| (yi - mean_y).powi(2)).sum();
let ss_res: f64 = x
.iter()
.zip(y.iter())
.map(|(&xi, &yi)| {
let y_pred = slope * xi + intercept;
(yi - y_pred).powi(2)
})
.sum();
let r_squared = if ss_yy.abs() < 1e-10 {
1.0
} else {
1.0 - ss_res / ss_yy
};
let mse = if n > 2.0 { ss_res / (n - 2.0) } else { 0.0 };
let stderr = if ss_xx > 0.0 {
(mse / ss_xx).sqrt()
} else {
f64::NAN
};
let t_stat = if stderr > 1e-10 {
slope / stderr
} else {
f64::INFINITY
};
let p_value = 2.0 * (1.0 - normal_cdf(t_stat.abs()));
LinearTrendResult {
slope,
intercept,
r_squared,
stderr,
p_value,
}
}
pub fn ar_coefficient(series: &[f64], k: usize, coeff: usize) -> f64 {
if series.len() <= k || k == 0 || coeff > k {
return f64::NAN;
}
let n = series.len();
let n_obs = n - k;
if n_obs < k + 2 {
return f64::NAN;
}
let (xtx, xty) = build_ar_normal_equations(series, k, n);
match solve_linear_system(&xtx, &xty) {
Some(p) if coeff < p.len() => p[coeff],
_ => f64::NAN,
}
}
#[inline]
fn build_ar_normal_equations(series: &[f64], k: usize, n: usize) -> (Vec<Vec<f64>>, Vec<f64>) {
let n_params = k + 1;
let mut xtx = vec![vec![0.0; n_params]; n_params];
let mut xty = vec![0.0; n_params];
for t in k..n {
let y_t = series[t];
xtx[0][0] += 1.0;
xty[0] += y_t;
for i in 1..n_params {
let xi = series[t - i];
xtx[0][i] += xi;
xtx[i][0] += xi;
xty[i] += xi * y_t;
for j in 1..n_params {
xtx[i][j] += xi * series[t - j];
}
}
}
(xtx, xty)
}
fn solve_linear_system(a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
let n = b.len();
if n == 0 || a.len() != n {
return None;
}
let mut aug = build_augmented_matrix(a, b, n);
if !gaussian_eliminate(&mut aug, n) {
return None;
}
Some(back_substitute_augmented(&aug, n))
}
#[inline]
fn build_augmented_matrix(a: &[Vec<f64>], b: &[f64], n: usize) -> Vec<Vec<f64>> {
a.iter()
.enumerate()
.map(|(i, row)| {
let mut r = Vec::with_capacity(n + 1);
r.extend_from_slice(row);
r.push(b[i]);
r
})
.collect()
}
#[inline]
fn gaussian_eliminate(aug: &mut [Vec<f64>], n: usize) -> bool {
for col in 0..n {
let (max_row, max_val) = (col..n).fold((col, aug[col][col].abs()), |(mr, mv), row| {
let v = aug[row][col].abs();
if v > mv {
(row, v)
} else {
(mr, mv)
}
});
if max_val < 1e-14 {
return false;
}
aug.swap(col, max_row);
for row in (col + 1)..n {
let factor = aug[row][col] / aug[col][col];
for j in col..=n {
aug[row][j] -= factor * aug[col][j];
}
}
}
true
}
#[inline]
fn back_substitute_augmented(aug: &[Vec<f64>], n: usize) -> Vec<f64> {
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut sum = aug[i][n];
for j in (i + 1)..n {
sum -= aug[i][j] * x[j];
}
x[i] = sum / aug[i][i];
}
x
}
pub fn ar_coefficient_yule_walker(series: &[f64], k: usize) -> f64 {
if series.len() <= k || k == 0 {
return f64::NAN;
}
let mean: f64 = series.iter().sum::<f64>() / series.len() as f64;
let var: f64 = series.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / series.len() as f64;
if var < 1e-10 {
return 0.0;
}
let acf = compute_acf_for_yule_walker(series, k, mean, var);
durbin_levinson(&acf, k)
}
#[inline]
fn compute_acf_for_yule_walker(series: &[f64], k: usize, mean: f64, var: f64) -> Vec<f64> {
(0..=k)
.map(|lag| {
if lag == 0 {
1.0
} else {
let cov: f64 = series[lag..]
.iter()
.zip(series.iter())
.map(|(&x1, &x2)| (x1 - mean) * (x2 - mean))
.sum::<f64>()
/ series.len() as f64;
cov / var
}
})
.collect()
}
#[inline]
fn durbin_levinson(acf: &[f64], k: usize) -> f64 {
let mut phi = vec![0.0; k + 1];
phi[1] = acf[1];
for m in 2..=k {
let num: f64 = acf[m] - (1..m).map(|j| phi[j] * acf[m - j]).sum::<f64>();
let denom: f64 = 1.0 - (1..m).map(|j| phi[j] * acf[j]).sum::<f64>();
if denom.abs() < 1e-10 {
return f64::NAN;
}
let new_phi = num / denom;
let mut new_coeffs = vec![0.0; k + 1];
new_coeffs[m] = new_phi;
for j in 1..m {
new_coeffs[j] = phi[j] - new_phi * phi[m - j];
}
phi = new_coeffs;
}
phi[k]
}
pub fn augmented_dickey_fuller(series: &[f64]) -> f64 {
if series.len() < 4 {
return f64::NAN;
}
let diff: Vec<f64> = series.windows(2).map(|w| w[1] - w[0]).collect();
let n = diff.len();
let y_lag: Vec<f64> = series[..n].to_vec();
let mean_y_lag: f64 = y_lag.iter().sum::<f64>() / n as f64;
let mean_diff: f64 = diff.iter().sum::<f64>() / n as f64;
let ss_yy: f64 = y_lag.iter().map(|&y| (y - mean_y_lag).powi(2)).sum();
let ss_xy: f64 = diff
.iter()
.zip(y_lag.iter())
.map(|(&d, &y)| (d - mean_diff) * (y - mean_y_lag))
.sum();
if ss_yy.abs() < 1e-10 {
return f64::NAN;
}
let beta = ss_xy / ss_yy;
let alpha = mean_diff - beta * mean_y_lag;
let residuals: Vec<f64> = diff
.iter()
.zip(y_lag.iter())
.map(|(&d, &y)| d - alpha - beta * y)
.collect();
let sse: f64 = residuals.iter().map(|r| r * r).sum();
let mse = sse / (n - 2) as f64;
let se_beta = (mse / ss_yy).sqrt();
if se_beta < 1e-10 {
return f64::NAN;
}
beta / se_beta
}
fn normal_cdf(x: f64) -> f64 {
0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
}
fn erf(x: f64) -> f64 {
let a1 = 0.254829592;
let a2 = -0.284496736;
let a3 = 1.421413741;
let a4 = -1.453152027;
let a5 = 1.061405429;
let p = 0.3275911;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + p * x);
let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
sign * y
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn linear_trend_perfect_line() {
let series: Vec<f64> = (0..10).map(|i| 2.0 * i as f64 + 1.0).collect();
let trend = linear_trend(&series);
assert_relative_eq!(trend.slope, 2.0, epsilon = 1e-10);
assert_relative_eq!(trend.intercept, 1.0, epsilon = 1e-10);
assert_relative_eq!(trend.r_squared, 1.0, epsilon = 1e-10);
}
#[test]
fn linear_trend_no_trend() {
let series = vec![5.0; 10];
let trend = linear_trend(&series);
assert_relative_eq!(trend.slope, 0.0, epsilon = 1e-10);
assert_relative_eq!(trend.intercept, 5.0, epsilon = 1e-10);
}
#[test]
fn linear_trend_negative_slope() {
let series: Vec<f64> = (0..10).map(|i| -1.5 * i as f64 + 10.0).collect();
let trend = linear_trend(&series);
assert_relative_eq!(trend.slope, -1.5, epsilon = 1e-10);
assert_relative_eq!(trend.intercept, 10.0, epsilon = 1e-10);
}
#[test]
fn linear_trend_with_noise() {
let series = vec![0.1, 1.2, 1.9, 3.1, 4.0, 5.2, 5.9, 7.1, 8.0, 9.1];
let trend = linear_trend(&series);
assert!(trend.slope > 0.9 && trend.slope < 1.1);
assert!(trend.r_squared > 0.99);
}
#[test]
fn linear_trend_short() {
assert!(linear_trend(&[]).slope.is_nan());
assert!(linear_trend(&[1.0]).slope.is_nan());
}
#[test]
fn linear_trend_two_points() {
let series = vec![0.0, 10.0];
let trend = linear_trend(&series);
assert_relative_eq!(trend.slope, 10.0, epsilon = 1e-10);
assert_relative_eq!(trend.intercept, 0.0, epsilon = 1e-10);
}
#[test]
fn agg_linear_trend_mean_slope() {
let series: Vec<f64> = (0..100).map(|i| i as f64).collect();
let agg = agg_linear_trend(&series, 10, "mean", "slope");
assert_relative_eq!(agg, 10.0, epsilon = 0.1);
}
#[test]
fn agg_linear_trend_rvalue() {
let series: Vec<f64> = (0..100).map(|i| i as f64).collect();
let agg = agg_linear_trend(&series, 10, "mean", "rvalue");
assert_relative_eq!(agg, 1.0, epsilon = 1e-10);
}
#[test]
fn agg_linear_trend_empty() {
assert!(agg_linear_trend(&[], 5, "mean", "slope").is_nan());
}
#[test]
fn agg_linear_trend_zero_chunk_len() {
let series = vec![1.0, 2.0, 3.0];
assert!(agg_linear_trend(&series, 0, "mean", "slope").is_nan());
}
#[test]
fn agg_linear_trend_chunk_len_too_large() {
let series = vec![1.0, 2.0, 3.0];
assert!(agg_linear_trend(&series, 10, "mean", "slope").is_nan());
}
#[test]
fn ar_coefficient_ar1_process() {
let mut series = vec![0.0; 200];
for i in 1..200 {
series[i] = 0.8 * series[i - 1] + (i as f64 * 0.1).sin() * 0.1;
}
let coef = ar_coefficient(&series, 1, 1);
assert!(
coef > 0.6 && coef < 1.0,
"AR(1) coef should be ~0.8, got {}",
coef
);
}
#[test]
fn ar_coefficient_intercept() {
let series: Vec<f64> = (0..100).map(|i| i as f64 * 0.5 + 10.0).collect();
let intercept = ar_coefficient(&series, 1, 0);
assert!(!intercept.is_nan());
}
#[test]
fn ar_coefficient_constant() {
let series = vec![5.0; 50];
let intercept = ar_coefficient(&series, 1, 0);
let ar1 = ar_coefficient(&series, 1, 1);
let _ = intercept;
let _ = ar1;
}
#[test]
fn ar_coefficient_short() {
assert!(ar_coefficient(&[], 1, 1).is_nan());
assert!(ar_coefficient(&[1.0], 1, 1).is_nan());
assert!(ar_coefficient(&[1.0, 2.0], 3, 1).is_nan());
}
#[test]
fn ar_coefficient_zero_k() {
let series = vec![1.0, 2.0, 3.0, 4.0];
assert!(ar_coefficient(&series, 0, 0).is_nan());
}
#[test]
fn ar_coefficient_coeff_out_of_range() {
let series: Vec<f64> = (0..50).map(|i| i as f64).collect();
assert!(ar_coefficient(&series, 2, 3).is_nan());
}
#[test]
fn ar_coefficient_yule_walker_works() {
let mut series = vec![0.0; 200];
for i in 1..200 {
series[i] = 0.8 * series[i - 1] + (i as f64 * 0.1).sin() * 0.1;
}
let coef = ar_coefficient_yule_walker(&series, 1);
assert!(coef > 0.6 && coef < 1.0);
}
#[test]
fn adf_stationary() {
let series: Vec<f64> = (0..100).map(|i| (i as f64 * 0.5).sin()).collect();
let adf = augmented_dickey_fuller(&series);
assert!(!adf.is_nan());
}
#[test]
fn adf_unit_root() {
let mut series = vec![0.0; 100];
for i in 1..100 {
series[i] = series[i - 1] + ((i * 7) % 11) as f64 - 5.0;
}
let adf = augmented_dickey_fuller(&series);
assert!(!adf.is_nan());
}
#[test]
fn adf_trending() {
let series: Vec<f64> = (0..100)
.map(|i| i as f64 * 2.0 + ((i * 7) % 5) as f64 * 0.1)
.collect();
let adf = augmented_dickey_fuller(&series);
assert!(!adf.is_nan());
}
#[test]
fn adf_short() {
assert!(augmented_dickey_fuller(&[]).is_nan());
assert!(augmented_dickey_fuller(&[1.0, 2.0]).is_nan());
}
#[test]
fn adf_constant() {
let series = vec![5.0; 50];
let adf = augmented_dickey_fuller(&series);
assert!(adf.is_nan() || adf.abs() < 1e-10);
}
}