1use crate::compute::{ComputeEngine, CpuEngine};
4
5use crate::multivariate::context::MultiSeriesContext;
6use crate::multivariate::error::MultivariateError;
7
8#[derive(Debug, Clone)]
10pub struct MultivariateForecastResult {
11 pub timestamps: Vec<i64>,
13 pub predictions: Vec<Vec<f64>>,
15 pub predictor_importance: Vec<(String, f64)>,
17}
18
19pub trait MultivariateForecastModel: Send + Sync {
21 fn fit(&mut self, ctx: &MultiSeriesContext, target_idx: usize)
26 -> Result<(), MultivariateError>;
27
28 fn predict(&self, horizon: usize) -> Result<MultivariateForecastResult, MultivariateError>;
30
31 fn supports_online_learning(&self) -> bool;
33}
34
35pub struct MultiLinearRegression {
39 coefficients: Vec<f64>,
40 intercept: f64,
41 r_squared: f64,
42 target_idx: usize,
43 predictor_names: Vec<String>,
44 last_predictors: Vec<f64>,
46 last_ts: i64,
47 interval_ns: i64,
48 fitted: bool,
49}
50
51impl MultiLinearRegression {
52 pub fn new() -> Self {
54 Self {
55 coefficients: Vec::new(),
56 intercept: 0.0,
57 r_squared: 0.0,
58 target_idx: 0,
59 predictor_names: Vec::new(),
60 last_predictors: Vec::new(),
61 last_ts: 0,
62 interval_ns: 1_000_000_000,
63 fitted: false,
64 }
65 }
66}
67
68impl Default for MultiLinearRegression {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl MultivariateForecastModel for MultiLinearRegression {
75 fn fit(
76 &mut self,
77 ctx: &MultiSeriesContext,
78 target_idx: usize,
79 ) -> Result<(), MultivariateError> {
80 let k = ctx.matrix.n_series();
81 let n = ctx.matrix.n_timestamps();
82 if target_idx >= k {
83 return Err(MultivariateError::InvalidParameter(format!(
84 "target_idx {target_idx} >= n_series {k}"
85 )));
86 }
87 let n_pred = k - 1;
88 if n < n_pred + 2 {
89 return Err(MultivariateError::InsufficientData {
90 min: n_pred + 2,
91 got: n,
92 });
93 }
94
95 self.target_idx = target_idx;
96 self.predictor_names = ctx
97 .matrix
98 .series_ids
99 .iter()
100 .enumerate()
101 .filter(|(i, _)| *i != target_idx)
102 .map(|(_, name)| name.clone())
103 .collect();
104
105 let y = &ctx.matrix.data[target_idx];
106 let p = n_pred + 1; let engine = CpuEngine::default();
110
111 let mut xtx = vec![0.0; p * p];
113 let mut xty = vec![0.0; p];
115
116 #[allow(clippy::needless_range_loop)]
117 for t in 0..n {
118 let mut row = Vec::with_capacity(p);
119 row.push(1.0); for s in 0..k {
121 if s != target_idx {
122 row.push(ctx.matrix.data[s][t]);
123 }
124 }
125 for i in 0..p {
126 for j in 0..p {
127 xtx[i * p + j] += row[i] * row[j];
128 }
129 xty[i] += row[i] * y[t];
130 }
131 }
132
133 for i in 0..p {
135 xtx[i * p + i] += 1e-8;
136 }
137
138 let beta = engine
140 .batch_matrix_solve(&xtx, &xty, p)
141 .map_err(|e: crate::compute::ComputeError| MultivariateError::Compute(e.to_string()))?;
142
143 self.intercept = beta[0];
144 self.coefficients = beta[1..].to_vec();
145
146 let y_mean = y.iter().sum::<f64>() / n as f64;
148 let ss_tot: f64 = y.iter().map(|&v| (v - y_mean).powi(2)).sum();
149 let mut ss_res = 0.0;
150 #[allow(clippy::needless_range_loop)]
151 for t in 0..n {
152 let mut pred = self.intercept;
153 let mut pidx = 0;
154 for s in 0..k {
155 if s != target_idx {
156 pred += self.coefficients[pidx] * ctx.matrix.data[s][t];
157 pidx += 1;
158 }
159 }
160 ss_res += (y[t] - pred).powi(2);
161 }
162 self.r_squared = if ss_tot > 1e-15 {
163 1.0 - ss_res / ss_tot
164 } else {
165 1.0
166 };
167
168 self.last_predictors = Vec::with_capacity(n_pred);
170 let last_t = n - 1;
171 for s in 0..k {
172 if s != target_idx {
173 self.last_predictors.push(ctx.matrix.data[s][last_t]);
174 }
175 }
176
177 self.last_ts = ctx
179 .matrix
180 .timestamps
181 .last()
182 .copied()
183 .ok_or(MultivariateError::InsufficientData { min: 1, got: 0 })?;
184 if ctx.matrix.timestamps.len() >= 2 {
185 let ts = &ctx.matrix.timestamps;
186 let mut deltas: Vec<i64> = ts.windows(2).map(|w| w[1] - w[0]).collect();
187 deltas.sort_unstable();
188 self.interval_ns = deltas[deltas.len() / 2];
189 }
190
191 self.fitted = true;
192 Ok(())
193 }
194
195 fn predict(&self, horizon: usize) -> Result<MultivariateForecastResult, MultivariateError> {
196 if !self.fitted {
197 return Err(MultivariateError::NotFitted);
198 }
199 let timestamps: Vec<i64> = (1..=horizon as i64)
202 .map(|h| {
203 self.last_ts
204 .saturating_add(h.saturating_mul(self.interval_ns))
205 })
206 .collect();
207
208 let pred_val = self.intercept
210 + self
211 .coefficients
212 .iter()
213 .zip(self.last_predictors.iter())
214 .map(|(c, x)| c * x)
215 .sum::<f64>();
216 let predictions = vec![vec![pred_val; horizon]];
217
218 let importance: Vec<(String, f64)> = self
219 .predictor_names
220 .iter()
221 .zip(self.coefficients.iter())
222 .map(|(name, &coef)| (name.clone(), coef.abs()))
223 .collect();
224
225 Ok(MultivariateForecastResult {
226 timestamps,
227 predictions,
228 predictor_importance: importance,
229 })
230 }
231
232 fn supports_online_learning(&self) -> bool {
233 true
234 }
235}
236
237fn hessenberg_reduce(h: &mut [f64], n: usize) {
245 for col in 0..n.saturating_sub(2) {
246 let m = n - col - 1;
248 let mut x = vec![0.0; m];
249 for i in 0..m {
250 x[i] = h[(col + 1 + i) * n + col];
251 }
252
253 let x_norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
254 if x_norm < 1e-15 {
255 continue;
256 }
257
258 let sign = if x[0] >= 0.0 { 1.0 } else { -1.0 };
259 let alpha = -sign * x_norm;
260 x[0] -= alpha;
261 let v_norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
262 if v_norm < 1e-15 {
263 continue;
264 }
265 for v in &mut x {
266 *v /= v_norm;
267 }
268
269 for j in 0..n {
271 let mut dot = 0.0;
272 for i in 0..m {
273 dot += x[i] * h[(col + 1 + i) * n + j];
274 }
275 for i in 0..m {
276 h[(col + 1 + i) * n + j] -= 2.0 * x[i] * dot;
277 }
278 }
279
280 for i in 0..n {
282 let mut dot = 0.0;
283 for j in 0..m {
284 dot += h[i * n + col + 1 + j] * x[j];
285 }
286 for j in 0..m {
287 h[i * n + col + 1 + j] -= 2.0 * dot * x[j];
288 }
289 }
290 }
291}
292
293fn qr_francis(h: &mut [f64], n: usize) {
296 if n <= 1 {
297 return;
298 }
299
300 let max_iter = 100 * n;
301 let mut iter_count = 0;
302 let mut nn = n;
303
304 while nn > 1 && iter_count < max_iter {
305 iter_count += 1;
306
307 let sub = h[(nn - 1) * n + nn - 2];
309 let diag_sum = h[(nn - 2) * n + nn - 2].abs() + h[(nn - 1) * n + nn - 1].abs();
310 let tol = 1e-14 * diag_sum.max(1e-15);
311
312 if sub.abs() <= tol {
313 nn -= 1;
314 continue;
315 }
316
317 if nn > 2 {
319 let sub2 = h[(nn - 2) * n + nn - 3];
320 let diag_sum2 = h[(nn - 3) * n + nn - 3].abs() + h[(nn - 2) * n + nn - 2].abs();
321 let tol2 = 1e-14 * diag_sum2.max(1e-15);
322 if sub2.abs() <= tol2 {
323 nn -= 2;
325 continue;
326 }
327 } else {
328 break;
330 }
331
332 let a11 = h[(nn - 2) * n + nn - 2];
334 let a12 = h[(nn - 2) * n + nn - 1];
335 let a21 = h[(nn - 1) * n + nn - 2];
336 let a22 = h[(nn - 1) * n + nn - 1];
337 let trace = a11 + a22;
338 let det = a11 * a22 - a12 * a21;
339
340 let shift = a22;
342 h[0] -= shift;
343 for i in 0..n {
344 if i < nn {
345 h[i * n + i] -= shift;
346 }
347 }
348 for i in 0..n {
353 if i < nn {
354 h[i * n + i] += shift;
355 }
356 }
357 h[0] += shift;
358
359 let _ = trace; let _ = det;
362 qr_step_givens(h, n, nn, a22);
363 }
364}
365
366fn qr_step_givens(h: &mut [f64], n: usize, nn: usize, shift: f64) {
370 for i in 0..nn {
372 h[i * n + i] -= shift;
373 }
374
375 let mut cs = vec![0.0; nn - 1];
377 let mut sn = vec![0.0; nn - 1];
378
379 for i in 0..nn - 1 {
380 let a = h[i * n + i];
381 let b = h[(i + 1) * n + i];
382 let r = a.hypot(b);
383 if r < 1e-30 {
384 cs[i] = 1.0;
385 sn[i] = 0.0;
386 continue;
387 }
388 cs[i] = a / r;
389 sn[i] = b / r;
390
391 for j in 0..n {
393 let t1 = h[i * n + j];
394 let t2 = h[(i + 1) * n + j];
395 h[i * n + j] = cs[i] * t1 + sn[i] * t2;
396 h[(i + 1) * n + j] = -sn[i] * t1 + cs[i] * t2;
397 }
398 }
399
400 for i in 0..nn - 1 {
402 for j in 0..n {
403 let t1 = h[j * n + i];
404 let t2 = h[j * n + i + 1];
405 h[j * n + i] = cs[i] * t1 + sn[i] * t2;
406 h[j * n + i + 1] = -sn[i] * t1 + cs[i] * t2;
407 }
408 }
409
410 for i in 0..nn {
412 h[i * n + i] += shift;
413 }
414}
415
416fn spectral_radius_from_schur(h: &[f64], n: usize) -> f64 {
419 let mut max_mag = 0.0f64;
420 let mut i = 0;
421
422 while i < n {
423 if i + 1 < n
424 && h[(i + 1) * n + i].abs()
425 > 1e-14 * (h[i * n + i].abs() + h[(i + 1) * n + i + 1].abs()).max(1e-15)
426 {
427 let a = h[i * n + i];
429 let b = h[i * n + i + 1];
430 let c = h[(i + 1) * n + i];
431 let d = h[(i + 1) * n + i + 1];
432 let det = a * d - b * c;
437 let trace = a + d;
438 let disc = trace * trace - 4.0 * det;
439 let mag = if disc < 0.0 {
440 det.abs().sqrt()
442 } else {
443 let sq = disc.sqrt();
445 let e1 = (trace + sq) / 2.0;
446 let e2 = (trace - sq) / 2.0;
447 e1.abs().max(e2.abs())
448 };
449 max_mag = max_mag.max(mag);
450 i += 2;
451 } else {
452 max_mag = max_mag.max(h[i * n + i].abs());
454 i += 1;
455 }
456 }
457
458 max_mag
459}
460
461pub struct VarModel {
470 lag_order: usize,
471 coefficients: Vec<Vec<f64>>,
473 intercepts: Vec<f64>,
474 k: usize,
475 last_values: Vec<Vec<f64>>, last_ts: i64,
477 interval_ns: i64,
478 series_names: Vec<String>,
479 fitted: bool,
480 stability_threshold: f64,
482}
483
484impl VarModel {
485 pub fn new(lag_order: Option<usize>) -> Self {
487 Self::new_with_stability_threshold(lag_order, None)
488 }
489
490 pub fn new_with_stability_threshold(
497 lag_order: Option<usize>,
498 stability_threshold: Option<f64>,
499 ) -> Self {
500 Self {
501 lag_order: lag_order.unwrap_or(1),
502 coefficients: Vec::new(),
503 intercepts: Vec::new(),
504 k: 0,
505 last_values: Vec::new(),
506 last_ts: 0,
507 interval_ns: 1_000_000_000,
508 series_names: Vec::new(),
509 fitted: false,
510 stability_threshold: stability_threshold.unwrap_or(1.05),
511 }
512 }
513
514 fn compute_spectral_radius(&self) -> f64 {
524 let k = self.k;
525 let p = self.lag_order;
526 let n = p * k;
527 if n == 0 {
528 return 0.0;
529 }
530
531 let mut h = vec![0.0; n * n];
540
541 for i in 0..k {
543 for l in 0..p {
544 for j in 0..k {
545 h[i * n + l * k + j] = self.coefficients[i][l * k + j];
546 }
547 }
548 }
549
550 for i in k..n {
552 h[i * n + (i - k)] = 1.0;
553 }
554
555 hessenberg_reduce(&mut h, n);
558
559 qr_francis(&mut h, n);
562
563 spectral_radius_from_schur(&h, n)
565 }
566}
567
568impl MultivariateForecastModel for VarModel {
569 fn fit(
570 &mut self,
571 ctx: &MultiSeriesContext,
572 _target_idx: usize,
573 ) -> Result<(), MultivariateError> {
574 let k = ctx.matrix.n_series();
575 let n = ctx.matrix.n_timestamps();
576 let p = self.lag_order;
577
578 if n <= p + 1 {
579 return Err(MultivariateError::InsufficientData { min: p + 2, got: n });
580 }
581
582 self.k = k;
583 self.series_names = ctx.matrix.series_ids.clone();
584
585 let engine = CpuEngine::default();
586
587 let dim = 1 + p * k; self.intercepts = vec![0.0; k];
591 self.coefficients = vec![vec![0.0; p * k]; k]; for eq in 0..k {
594 let mut xtx = vec![0.0; dim * dim];
595 let mut xty = vec![0.0; dim];
596
597 for t in p..n {
598 let mut row = Vec::with_capacity(dim);
599 row.push(1.0); for l in 1..=p {
601 for s in 0..k {
602 row.push(ctx.matrix.data[s][t - l]);
603 }
604 }
605 let y = ctx.matrix.data[eq][t];
606 for i in 0..dim {
607 for j in 0..dim {
608 xtx[i * dim + j] += row[i] * row[j];
609 }
610 xty[i] += row[i] * y;
611 }
612 }
613
614 for i in 0..dim {
616 xtx[i * dim + i] += 1e-8;
617 }
618
619 let beta = engine.batch_matrix_solve(&xtx, &xty, dim).map_err(
620 |e: crate::compute::ComputeError| MultivariateError::Compute(e.to_string()),
621 )?;
622
623 self.intercepts[eq] = beta[0];
624 self.coefficients[eq] = beta[1..].to_vec();
625 }
626
627 self.last_values = Vec::with_capacity(p);
629 for l in 0..p {
630 let t = n - p + l;
631 let obs: Vec<f64> = (0..k).map(|s| ctx.matrix.data[s][t]).collect();
632 self.last_values.push(obs);
633 }
634
635 self.last_ts = ctx
637 .matrix
638 .timestamps
639 .last()
640 .copied()
641 .ok_or(MultivariateError::InsufficientData { min: 1, got: 0 })?;
642 if ctx.matrix.timestamps.len() >= 2 {
643 let ts = &ctx.matrix.timestamps;
644 let mut deltas: Vec<i64> = ts.windows(2).map(|w| w[1] - w[0]).collect();
645 deltas.sort_unstable();
646 self.interval_ns = deltas[deltas.len() / 2];
647 }
648
649 self.fitted = true;
650
651 let stability_threshold = self.stability_threshold;
657 let spectral_radius = self.compute_spectral_radius();
658 if spectral_radius >= stability_threshold {
659 tracing::warn!(
660 spectral_radius = spectral_radius,
661 "VAR model is unstable (spectral radius >= {stability_threshold}) — forecasts may diverge"
662 );
663 return Err(MultivariateError::InvalidParameter(format!(
664 "VAR model is unstable: spectral radius {spectral_radius:.4} >= {stability_threshold}; \
665 try increasing regularization, reducing lag order, or differencing the data"
666 )));
667 } else if spectral_radius >= 1.0 {
668 tracing::warn!(
669 spectral_radius = spectral_radius,
670 "VAR model is near unit-root (spectral radius close to 1.0) — \
671 forecasts may drift; consider differencing"
672 );
673 }
674
675 Ok(())
676 }
677
678 fn predict(&self, horizon: usize) -> Result<MultivariateForecastResult, MultivariateError> {
679 if !self.fitted {
680 return Err(MultivariateError::NotFitted);
681 }
682
683 let k = self.k;
684 let p = self.lag_order;
685 let mut history = self.last_values.clone();
686 let mut predictions = vec![Vec::with_capacity(horizon); k];
687 let timestamps: Vec<i64> = (1..=horizon as i64)
688 .map(|h| {
689 self.last_ts
690 .saturating_add(h.saturating_mul(self.interval_ns))
691 })
692 .collect();
693
694 for _ in 0..horizon {
695 let mut new_obs = vec![0.0; k];
696 #[allow(clippy::needless_range_loop)]
697 for eq in 0..k {
698 let mut val = self.intercepts[eq];
699 #[allow(clippy::needless_range_loop)]
700 for l in 0..p {
701 let lag_idx = history.len() - 1 - l;
702 if lag_idx < history.len() {
703 for s in 0..k {
704 val += self.coefficients[eq][l * k + s] * history[lag_idx][s];
705 }
706 }
707 }
708 new_obs[eq] = val;
709 }
710 for eq in 0..k {
711 predictions[eq].push(new_obs[eq]);
712 }
713 history.push(new_obs);
714 }
715
716 let mut importance = Vec::with_capacity(k);
718 for s in 0..k {
719 let avg: f64 = (0..k)
720 .flat_map(|eq| (0..p).map(move |l| self.coefficients[eq][l * k + s].abs()))
721 .sum::<f64>()
722 / (k * p) as f64;
723 importance.push((self.series_names[s].clone(), avg));
724 }
725
726 Ok(MultivariateForecastResult {
727 timestamps,
728 predictions,
729 predictor_importance: importance,
730 })
731 }
732
733 fn supports_online_learning(&self) -> bool {
734 false
735 }
736}
737
738#[derive(Debug, Clone)]
742pub struct GrangerCausalityResult {
743 pub cause: String,
745 pub effect: String,
747 pub lag_order: usize,
749 pub f_statistic: f64,
751 pub p_value: f64,
753 pub adjusted_p_value: f64,
756 pub significant: bool,
758 pub rss_unrestricted: f64,
760 pub rss_restricted: f64,
762}
763
764impl VarModel {
765 pub fn granger_causality(
781 &self,
782 ctx: &MultiSeriesContext,
783 ) -> Result<Vec<GrangerCausalityResult>, MultivariateError> {
784 if !self.fitted {
785 return Err(MultivariateError::NotFitted);
786 }
787
788 let k = self.k;
789 let p = self.lag_order;
790 let n = ctx.matrix.n_timestamps();
791 let t = n - p; if t <= 1 + p * k {
794 return Err(MultivariateError::InsufficientData {
795 min: 2 + p * k,
796 got: t,
797 });
798 }
799
800 let engine = CpuEngine::default();
801 let dim_full = 1 + p * k; let dim_restricted = 1 + p * (k - 1); let mut results = Vec::with_capacity(k * (k - 1));
805
806 for effect_eq in 0..k {
807 for cause_s in 0..k {
808 if cause_s == effect_eq {
809 continue;
810 }
811
812 let rss_u = self.ols_rss(ctx, &engine, effect_eq, None, dim_full, p, k)?;
814
815 let rss_r =
817 self.ols_rss(ctx, &engine, effect_eq, Some(cause_s), dim_restricted, p, k)?;
818
819 let df1 = p as f64; let df2 = (t - dim_full) as f64; let f_stat = if rss_u.abs() < 1e-30 {
823 0.0
824 } else {
825 ((rss_r - rss_u) / df1) / (rss_u / df2)
826 };
827
828 let p_value = f_distribution_sf(f_stat, df1, df2);
832
833 results.push(GrangerCausalityResult {
834 cause: self.series_names[cause_s].clone(),
835 effect: self.series_names[effect_eq].clone(),
836 lag_order: p,
837 f_statistic: f_stat,
838 p_value,
839 adjusted_p_value: p_value, significant: false, rss_unrestricted: rss_u,
842 rss_restricted: rss_r,
843 });
844 }
845 }
846
847 let m = results.len();
851 if m > 0 {
852 let alpha = 0.05;
853 let mut indices: Vec<usize> = (0..m).collect();
854 indices.sort_by(|&a, &b| {
855 results[a]
856 .p_value
857 .partial_cmp(&results[b].p_value)
858 .unwrap_or(std::cmp::Ordering::Equal)
859 });
860
861 let mut adj_p = vec![0.0; m];
864 for (rank, &idx) in indices.iter().enumerate() {
865 adj_p[idx] = (results[idx].p_value * m as f64 / (rank + 1) as f64).min(1.0);
866 }
867 let mut running_min = 1.0;
869 for &idx in indices.iter().rev() {
870 adj_p[idx] = adj_p[idx].min(running_min);
871 running_min = adj_p[idx];
872 }
873 for (i, p_adj) in adj_p.into_iter().enumerate() {
874 results[i].adjusted_p_value = p_adj;
875 results[i].significant = p_adj < alpha;
876 }
877 }
878
879 Ok(results)
880 }
881
882 pub fn granger_causality_robust(
897 &self,
898 ctx: &MultiSeriesContext,
899 ) -> Result<Vec<GrangerCausalityResult>, MultivariateError> {
900 if !self.fitted {
901 return Err(MultivariateError::NotFitted);
902 }
903
904 let k = self.k;
905 let p = self.lag_order;
906 let n = ctx.matrix.n_timestamps();
907 let t = n - p;
908
909 if t <= 1 + p * k {
910 return Err(MultivariateError::InsufficientData {
911 min: 2 + p * k,
912 got: t,
913 });
914 }
915
916 let engine = CpuEngine::default();
917 let dim = 1 + p * k; let df_den = t as f64 - dim as f64;
919
920 if df_den <= 0.0 {
921 return Err(MultivariateError::InsufficientData {
922 min: dim + p + 1,
923 got: t,
924 });
925 }
926
927 let mut results = Vec::with_capacity(k * (k - 1));
928
929 for effect_eq in 0..k {
930 let mut x_mat = vec![0.0_f64; t * dim];
932 let mut y_vec = vec![0.0_f64; t];
933
934 for (row, obs) in (p..n).enumerate() {
935 y_vec[row] = ctx.matrix.data[effect_eq][obs];
936 x_mat[row * dim] = 1.0;
937 let mut col = 1;
938 for l in 1..=p {
939 for s in 0..k {
940 x_mat[row * dim + col] = ctx.matrix.data[s][obs - l];
941 col += 1;
942 }
943 }
944 }
945
946 let mut xtx = vec![0.0_f64; dim * dim];
948 for row in 0..t {
949 for i in 0..dim {
950 let xi = x_mat[row * dim + i];
951 for j in i..dim {
952 let v = xi * x_mat[row * dim + j];
953 xtx[i * dim + j] += v;
954 if i != j {
955 xtx[j * dim + i] += v;
956 }
957 }
958 }
959 }
960 let mut xty = vec![0.0_f64; dim];
962 for row in 0..t {
963 let y = y_vec[row];
964 for i in 0..dim {
965 xty[i] += x_mat[row * dim + i] * y;
966 }
967 }
968 for i in 0..dim {
970 xtx[i * dim + i] += 1e-8;
971 }
972
973 let beta = engine
975 .batch_matrix_solve(&xtx, &xty, dim)
976 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
977
978 let mut residuals = vec![0.0_f64; t];
980 for row in 0..t {
981 let mut fitted = 0.0;
982 for j in 0..dim {
983 fitted += x_mat[row * dim + j] * beta[j];
984 }
985 residuals[row] = y_vec[row] - fitted;
986 }
987
988 let mut xtx_inv = vec![0.0_f64; dim * dim];
990 for c in 0..dim {
991 let mut rhs = vec![0.0_f64; dim];
992 rhs[c] = 1.0;
993 let sol = engine
994 .batch_matrix_solve(&xtx, &rhs, dim)
995 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
996 for r in 0..dim {
997 xtx_inv[r * dim + c] = sol[r];
998 }
999 }
1000
1001 let mut hat_diag = vec![0.0_f64; t];
1003 for row in 0..t {
1004 let mut h = 0.0;
1005 for i in 0..dim {
1006 let mut inner = 0.0;
1007 for j in 0..dim {
1008 inner += xtx_inv[i * dim + j] * x_mat[row * dim + j];
1009 }
1010 h += x_mat[row * dim + i] * inner;
1011 }
1012 hat_diag[row] = h.clamp(0.0, 1.0 - 1e-12);
1013 }
1014
1015 let mut meat = vec![0.0_f64; dim * dim];
1017 for row in 0..t {
1018 let denom = (1.0 - hat_diag[row]).powi(2);
1019 let w = residuals[row].powi(2) / denom;
1020 for i in 0..dim {
1021 let xi_w = x_mat[row * dim + i] * w;
1022 for j in i..dim {
1023 let v = xi_w * x_mat[row * dim + j];
1024 meat[i * dim + j] += v;
1025 if i != j {
1026 meat[j * dim + i] += v;
1027 }
1028 }
1029 }
1030 }
1031
1032 let mut tmp = vec![0.0_f64; dim * dim];
1034 for i in 0..dim {
1035 for j in 0..dim {
1036 let mut s = 0.0;
1037 for c in 0..dim {
1038 s += xtx_inv[i * dim + c] * meat[c * dim + j];
1039 }
1040 tmp[i * dim + j] = s;
1041 }
1042 }
1043 let mut v_hc3 = vec![0.0_f64; dim * dim];
1044 for i in 0..dim {
1045 for j in 0..dim {
1046 let mut s = 0.0;
1047 for c in 0..dim {
1048 s += tmp[i * dim + c] * xtx_inv[c * dim + j];
1049 }
1050 v_hc3[i * dim + j] = s;
1051 }
1052 }
1053
1054 let rss_u: f64 = residuals.iter().map(|e| e * e).sum();
1056
1057 for cause_s in 0..k {
1059 if cause_s == effect_eq {
1060 continue;
1061 }
1062
1063 let cause_cols: Vec<usize> = (1..=p).map(|l| 1 + (l - 1) * k + cause_s).collect();
1066
1067 let r_beta: Vec<f64> = cause_cols.iter().map(|&c| beta[c]).collect();
1069
1070 let mut r_v_rt = vec![0.0_f64; p * p];
1072 for (i, &ci) in cause_cols.iter().enumerate() {
1073 for (j, &cj) in cause_cols.iter().enumerate() {
1074 r_v_rt[i * p + j] = v_hc3[ci * dim + cj];
1075 }
1076 }
1077 for i in 0..p {
1078 r_v_rt[i * p + i] += 1e-12;
1079 }
1080
1081 let inv_r_beta = engine
1083 .batch_matrix_solve(&r_v_rt, &r_beta, p)
1084 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
1085
1086 let wald: f64 = r_beta
1087 .iter()
1088 .zip(inv_r_beta.iter())
1089 .map(|(a, b)| a * b)
1090 .sum();
1091 let f_stat = (wald / p as f64).max(0.0);
1092 let p_value = f_distribution_sf(f_stat, p as f64, df_den);
1093
1094 let dim_r = 1 + p * (k - 1);
1096 let rss_r = self.ols_rss(ctx, &engine, effect_eq, Some(cause_s), dim_r, p, k)?;
1097
1098 results.push(GrangerCausalityResult {
1099 cause: self.series_names[cause_s].clone(),
1100 effect: self.series_names[effect_eq].clone(),
1101 lag_order: p,
1102 f_statistic: f_stat,
1103 p_value,
1104 adjusted_p_value: p_value,
1105 significant: false,
1106 rss_unrestricted: rss_u,
1107 rss_restricted: rss_r,
1108 });
1109 }
1110 }
1111
1112 let m = results.len();
1114 if m > 0 {
1115 let alpha = 0.05;
1116 let mut indices: Vec<usize> = (0..m).collect();
1117 indices.sort_by(|&a, &b| {
1118 results[a]
1119 .p_value
1120 .partial_cmp(&results[b].p_value)
1121 .unwrap_or(std::cmp::Ordering::Equal)
1122 });
1123 let mut adj_p = vec![0.0; m];
1124 for (rank, &idx) in indices.iter().enumerate() {
1125 adj_p[idx] = (results[idx].p_value * m as f64 / (rank + 1) as f64).min(1.0);
1126 }
1127 let mut running_min = 1.0;
1128 for &idx in indices.iter().rev() {
1129 adj_p[idx] = adj_p[idx].min(running_min);
1130 running_min = adj_p[idx];
1131 }
1132 for (i, p_adj) in adj_p.into_iter().enumerate() {
1133 results[i].adjusted_p_value = p_adj;
1134 results[i].significant = p_adj < alpha;
1135 }
1136 }
1137
1138 Ok(results)
1139 }
1140
1141 fn ols_rss(
1146 &self,
1147 ctx: &MultiSeriesContext,
1148 engine: &CpuEngine,
1149 effect_eq: usize,
1150 exclude_series: Option<usize>,
1151 dim: usize,
1152 p: usize,
1153 k: usize,
1154 ) -> Result<f64, MultivariateError> {
1155 let n = ctx.matrix.n_timestamps();
1156 let mut xtx = vec![0.0; dim * dim];
1157 let mut xty = vec![0.0; dim];
1158
1159 for obs in p..n {
1160 let mut row = Vec::with_capacity(dim);
1161 row.push(1.0); for l in 1..=p {
1163 for s in 0..k {
1164 if exclude_series == Some(s) {
1165 continue;
1166 }
1167 row.push(ctx.matrix.data[s][obs - l]);
1168 }
1169 }
1170 let y = ctx.matrix.data[effect_eq][obs];
1171 for i in 0..dim {
1172 for j in 0..dim {
1173 xtx[i * dim + j] += row[i] * row[j];
1174 }
1175 xty[i] += row[i] * y;
1176 }
1177 }
1178
1179 for i in 0..dim {
1181 xtx[i * dim + i] += 1e-8;
1182 }
1183
1184 let beta = engine
1185 .batch_matrix_solve(&xtx, &xty, dim)
1186 .map_err(|e| MultivariateError::Compute(e.to_string()))?;
1187
1188 let mut rss = 0.0;
1190 for obs in p..n {
1191 let mut pred = beta[0]; let mut col = 1;
1193 for l in 1..=p {
1194 for s in 0..k {
1195 if exclude_series == Some(s) {
1196 continue;
1197 }
1198 pred += beta[col] * ctx.matrix.data[s][obs - l];
1199 col += 1;
1200 }
1201 }
1202 let residual = ctx.matrix.data[effect_eq][obs] - pred;
1203 rss += residual * residual;
1204 }
1205
1206 Ok(rss)
1207 }
1208}
1209
1210fn f_distribution_sf(f: f64, d1: f64, d2: f64) -> f64 {
1215 if f <= 0.0 || d1 <= 0.0 || d2 <= 0.0 {
1216 return 1.0;
1217 }
1218 let x = d2 / (d2 + d1 * f);
1219 regularized_incomplete_beta(d2 / 2.0, d1 / 2.0, x)
1220}
1221
1222fn regularized_incomplete_beta(a: f64, b: f64, x: f64) -> f64 {
1225 if x <= 0.0 {
1226 return 0.0;
1227 }
1228 if x >= 1.0 {
1229 return 1.0;
1230 }
1231
1232 if x > (a + 1.0) / (a + b + 2.0) {
1234 return 1.0 - regularized_incomplete_beta(b, a, 1.0 - x);
1235 }
1236
1237 let ln_prefix = a * x.ln() + b * (1.0 - x).ln() - ln_beta(a, b) - a.ln();
1238 let prefix = ln_prefix.exp();
1239
1240 let max_iter = 200;
1242 let eps = 1e-14;
1243 let tiny = 1e-30;
1244
1245 let mut c = 1.0;
1246 let mut d = 1.0 - (a + b) * x / (a + 1.0);
1247 if d.abs() < tiny {
1248 d = tiny;
1249 }
1250 d = 1.0 / d;
1251 let mut h = d;
1252
1253 for m in 1..=max_iter {
1254 let m_f = m as f64;
1255
1256 let num_even = m_f * (b - m_f) * x / ((a + 2.0 * m_f - 1.0) * (a + 2.0 * m_f));
1258 d = 1.0 + num_even * d;
1259 if d.abs() < tiny {
1260 d = tiny;
1261 }
1262 d = 1.0 / d;
1263 c = 1.0 + num_even / c;
1264 if c.abs() < tiny {
1265 c = tiny;
1266 }
1267 h *= d * c;
1268
1269 let num_odd = -((a + m_f) * (a + b + m_f)) * x / ((a + 2.0 * m_f) * (a + 2.0 * m_f + 1.0));
1271 d = 1.0 + num_odd * d;
1272 if d.abs() < tiny {
1273 d = tiny;
1274 }
1275 d = 1.0 / d;
1276 c = 1.0 + num_odd / c;
1277 if c.abs() < tiny {
1278 c = tiny;
1279 }
1280 let delta = d * c;
1281 h *= delta;
1282
1283 if (delta - 1.0).abs() < eps {
1284 break;
1285 }
1286 }
1287
1288 prefix * h
1289}
1290
1291fn ln_beta(a: f64, b: f64) -> f64 {
1293 ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
1294}
1295
1296fn ln_gamma(x: f64) -> f64 {
1298 const COEFFS: [f64; 7] = [
1299 0.999_999_999_999_809_9,
1300 676.5203681218851,
1301 -1259.1392167224028,
1302 771.323_428_777_653_1,
1303 -176.615_029_162_140_6,
1304 12.507343278686905,
1305 -0.13857109526572012,
1306 ];
1307 let x = x - 1.0;
1308 let mut y = COEFFS[0];
1309 for (i, &c) in COEFFS.iter().enumerate().skip(1) {
1310 y += c / (x + i as f64);
1311 }
1312 let t = x + 6.5;
1313 0.5 * (2.0 * std::f64::consts::PI).ln() + (t).ln() * (x + 0.5) - t + y.ln()
1314}
1315
1316#[cfg(test)]
1317mod tests {
1318 use super::*;
1319
1320 fn simple_ctx() -> MultiSeriesContext {
1321 let n = 100;
1322 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1323 let x1: Vec<f64> = (0..n).map(|i| i as f64).collect();
1325 let x2: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin() * 10.0).collect();
1326 let y: Vec<f64> = (0..n)
1328 .map(|i| 2.0 * (i as f64) + 3.0 * (i as f64 * 0.1).sin() * 10.0 + 5.0)
1329 .collect();
1330 MultiSeriesContext::build(
1331 vec![
1332 ("y".to_string(), ts.clone(), y),
1333 ("x1".to_string(), ts.clone(), x1),
1334 ("x2".to_string(), ts, x2),
1335 ],
1336 None,
1337 )
1338 .unwrap()
1339 }
1340
1341 #[test]
1342 fn multi_linear_regression_fit() {
1343 let ctx = simple_ctx();
1344 let mut model = MultiLinearRegression::new();
1345 model.fit(&ctx, 0).unwrap();
1346 assert!(model.r_squared > 0.99, "R²={}", model.r_squared);
1347 assert!((model.coefficients[0] - 2.0).abs() < 0.5);
1349 assert!((model.coefficients[1] - 3.0).abs() < 0.5);
1350 }
1351
1352 #[test]
1353 fn multi_linear_regression_predict() {
1354 let ctx = simple_ctx();
1355 let mut model = MultiLinearRegression::new();
1356 model.fit(&ctx, 0).unwrap();
1357 let result = model.predict(5).unwrap();
1358 assert_eq!(result.predictions[0].len(), 5);
1359 assert_eq!(result.timestamps.len(), 5);
1360 let pred = result.predictions[0][0];
1363 assert!(
1364 pred > 100.0,
1365 "prediction {} should reflect learned coefficients, not just intercept",
1366 pred
1367 );
1368 }
1369
1370 #[test]
1371 fn var_fit_and_predict() {
1372 let n = 200;
1373 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1374 let s1: Vec<f64> = (0..n)
1375 .map(|i| (i as f64 * 0.05).sin() * 10.0 + 50.0)
1376 .collect();
1377 let s2: Vec<f64> = (0..n)
1378 .map(|i| (i as f64 * 0.05).cos() * 10.0 + 50.0)
1379 .collect();
1380 let ctx = MultiSeriesContext::build(
1381 vec![
1382 ("s1".to_string(), ts.clone(), s1),
1383 ("s2".to_string(), ts, s2),
1384 ],
1385 None,
1386 )
1387 .unwrap();
1388
1389 let mut model = VarModel::new(Some(2));
1390 model.fit(&ctx, 0).unwrap();
1391 let result = model.predict(10).unwrap();
1392 assert_eq!(result.predictions.len(), 2); assert_eq!(result.predictions[0].len(), 10);
1394 assert_eq!(result.predictions[1].len(), 10);
1395 }
1396
1397 #[test]
1398 fn var_predictor_importance() {
1399 let n = 200;
1400 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1401 let s1: Vec<f64> = (0..n).map(|i| i as f64).collect();
1402 let s2: Vec<f64> = (0..n).map(|i| i as f64 * 2.0).collect();
1403 let ctx = MultiSeriesContext::build(
1404 vec![
1405 ("s1".to_string(), ts.clone(), s1),
1406 ("s2".to_string(), ts, s2),
1407 ],
1408 None,
1409 )
1410 .unwrap();
1411
1412 let mut model = VarModel::new(Some(1));
1413 model.fit(&ctx, 0).unwrap();
1414 let result = model.predict(5).unwrap();
1415 assert_eq!(result.predictor_importance.len(), 2);
1416 }
1417
1418 #[test]
1419 fn granger_causality_detects_cause() {
1420 let n = 300;
1422 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1423 let mut rng_state: u64 = 42;
1425 let mut pseudo_rand = || -> f64 {
1426 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
1427 ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.5
1428 };
1429
1430 let s1: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin() * 5.0).collect();
1431 let mut s2 = vec![0.0; n];
1432 for i in 1..n {
1433 s2[i] = 0.8 * s1[i - 1] + pseudo_rand();
1434 }
1435
1436 let ctx = MultiSeriesContext::build(
1437 vec![
1438 ("s1".to_string(), ts.clone(), s1),
1439 ("s2".to_string(), ts, s2),
1440 ],
1441 None,
1442 )
1443 .unwrap();
1444
1445 let mut model = VarModel::new(Some(2));
1446 model.fit(&ctx, 0).unwrap();
1447
1448 let results = model.granger_causality(&ctx).unwrap();
1449 assert_eq!(results.len(), 2); let s1_causes_s2 = results
1452 .iter()
1453 .find(|r| r.cause == "s1" && r.effect == "s2")
1454 .unwrap();
1455 assert!(
1456 s1_causes_s2.significant,
1457 "s1 should Granger-cause s2: F={:.3}, p={:.4}",
1458 s1_causes_s2.f_statistic, s1_causes_s2.p_value
1459 );
1460 assert!(s1_causes_s2.f_statistic > 1.0);
1461 assert!(s1_causes_s2.p_value < 0.05);
1462 }
1463
1464 #[test]
1465 fn granger_requires_fitted_model() {
1466 let n = 100;
1467 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1468 let s1: Vec<f64> = (0..n).map(|i| i as f64).collect();
1469 let s2: Vec<f64> = (0..n).map(|i| (i as f64).sqrt()).collect();
1470 let ctx = MultiSeriesContext::build(
1471 vec![
1472 ("s1".to_string(), ts.clone(), s1),
1473 ("s2".to_string(), ts, s2),
1474 ],
1475 None,
1476 )
1477 .unwrap();
1478
1479 let model = VarModel::new(Some(1));
1480 let err = model.granger_causality(&ctx);
1481 assert!(err.is_err());
1482 }
1483
1484 #[test]
1485 fn granger_robust_detects_cause() {
1486 let n = 200;
1488 let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
1489 let mut s1 = vec![0.0; n];
1490 let mut s2 = vec![0.0; n];
1491 let mut rng_state: u64 = 12345;
1492 for i in 1..n {
1493 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
1494 let noise1 = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.5;
1495 s1[i] = 0.7 * s1[i - 1] + noise1;
1496 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
1497 let noise2 = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.2;
1498 s2[i] = 0.8 * s1[i - 1] + noise2;
1500 }
1501 let ctx = MultiSeriesContext::build(
1502 vec![
1503 ("s1".to_string(), ts.clone(), s1),
1504 ("s2".to_string(), ts, s2),
1505 ],
1506 None,
1507 )
1508 .unwrap();
1509
1510 let mut model = VarModel::new(Some(1));
1511 model.fit(&ctx, 1).unwrap();
1512 let results = model.granger_causality_robust(&ctx).unwrap();
1513 let s1_causes_s2 = results
1515 .iter()
1516 .find(|r| r.cause == "s1" && r.effect == "s2")
1517 .unwrap();
1518 assert!(
1519 s1_causes_s2.significant,
1520 "HC3: s1 should Granger-cause s2: F={:.3}, p={:.4}, adj_p={:.4}",
1521 s1_causes_s2.f_statistic, s1_causes_s2.p_value, s1_causes_s2.adjusted_p_value,
1522 );
1523 }
1524}