chronix_analytics/forecast/
util.rs1use crate::forecast::error::ForecastError;
4
5#[inline]
8pub fn validate_input(timestamps: &[i64], values: &[f64]) -> Result<(), ForecastError> {
9 if values.is_empty() {
20 return Err(ForecastError::InvalidInput(
21 "cannot fit a model to an empty series".to_string(),
22 ));
23 }
24 if timestamps.len() != values.len() {
25 return Err(ForecastError::InvalidInput(format!(
26 "timestamps length ({}) != values length ({})",
27 timestamps.len(),
28 values.len(),
29 )));
30 }
31 if let Some(pos) = values.iter().position(|v| !v.is_finite()) {
32 return Err(ForecastError::InvalidInput(format!(
33 "values[{pos}] is not finite ({})",
34 values[pos],
35 )));
36 }
37 Ok(())
38}
39
40pub fn median_interval(timestamps: &[i64]) -> i64 {
45 if timestamps.len() < 2 {
46 return 1;
47 }
48 let mut deltas: Vec<i64> = timestamps.windows(2).map(|w| w[1] - w[0]).collect();
49 let mid = deltas.len() / 2;
50 let median = *deltas.select_nth_unstable(mid).1;
51 median.max(1)
52}
53
54#[must_use]
61pub fn normal_quantile(p: f64) -> f64 {
62 if !(0.0..=1.0).contains(&p) || p.is_nan() {
63 return f64::NAN;
64 }
65 if p == 0.0 {
66 return f64::NEG_INFINITY;
67 }
68 if p == 1.0 {
69 return f64::INFINITY;
70 }
71
72 const A: [f64; 6] = [
73 -3.969_683_028_665_376e1,
74 2.209_460_984_245_205e2,
75 -2.759_285_104_469_687e2,
76 1.383_577_518_672_69e2,
77 -3.066_479_806_614_716e1,
78 2.506_628_277_459_239e0,
79 ];
80 const B: [f64; 5] = [
81 -5.447_609_879_822_406e1,
82 1.615_858_368_580_409e2,
83 -1.556_989_798_598_866e2,
84 6.680_131_188_771_972e1,
85 -1.328_068_155_288_572e1,
86 ];
87 const C: [f64; 6] = [
88 -7.784_894_002_430_293e-3,
89 -3.223_964_580_411_365e-1,
90 -2.400_758_277_161_838e0,
91 -2.549_732_539_343_734e0,
92 4.374_664_141_464_968e0,
93 2.938_163_982_698_783e0,
94 ];
95 const D: [f64; 4] = [
96 7.784_695_709_041_462e-3,
97 3.224_671_290_700_398e-1,
98 2.445_134_137_142_996e0,
99 3.754_408_661_907_416e0,
100 ];
101 const P_LOW: f64 = 0.024_25;
103 const P_HIGH: f64 = 1.0 - P_LOW;
104
105 if p < P_LOW {
106 let q = (-2.0 * p.ln()).sqrt();
107 (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
108 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
109 } else if p <= P_HIGH {
110 let q = p - 0.5;
111 let r = q * q;
112 (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
113 / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
114 } else {
115 let q = (-2.0 * (1.0 - p).ln()).sqrt();
116 -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
117 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
118 }
119}
120
121#[must_use]
125pub fn z_for_confidence(level: f64) -> Option<f64> {
126 if !(level > 0.0 && level < 1.0) {
127 return None;
128 }
129 Some(normal_quantile(0.5 + level / 2.0))
130}