kestrel_chartkit/
stats.rs1pub fn rolling_sum(slice: &[f64]) -> f64 {
5 slice.iter().copied().filter(|v| v.is_finite()).sum()
6}
7
8pub fn rolling_mean(slice: &[f64]) -> f64 {
10 let finite: Vec<f64> = slice.iter().copied().filter(|v| v.is_finite()).collect();
11 if finite.is_empty() {
12 0.0
13 } else {
14 finite.iter().sum::<f64>() / finite.len() as f64
15 }
16}
17
18pub fn rolling_variance(slice: &[f64]) -> f64 {
20 let finite: Vec<f64> = slice.iter().copied().filter(|v| v.is_finite()).collect();
21 if finite.len() < 2 {
22 return 0.0;
23 }
24 let mean = finite.iter().sum::<f64>() / finite.len() as f64;
25 let var_sum: f64 = finite.iter().map(|v| (v - mean).powi(2)).sum();
26 var_sum / finite.len() as f64
27}
28
29pub fn rolling_stddev(slice: &[f64]) -> f64 {
31 rolling_variance(slice).sqrt()
32}
33
34pub fn rolling_median(slice: &[f64]) -> f64 {
36 rolling_quantile(slice, 0.5)
37}
38
39pub fn rolling_quantile(slice: &[f64], quantile: f64) -> f64 {
41 let mut finite: Vec<f64> = slice.iter().copied().filter(|v| v.is_finite()).collect();
42 if finite.is_empty() {
43 return 0.0;
44 }
45 finite.sort_by(|a, b| a.partial_cmp(b).unwrap());
46
47 let q = quantile.clamp(0.0, 1.0);
48 let idx_f = q * (finite.len() - 1) as f64;
49 let idx_lower = idx_f.floor() as usize;
50 let idx_upper = idx_f.ceil() as usize;
51
52 if idx_lower == idx_upper {
53 finite[idx_lower]
54 } else {
55 let weight = idx_f - idx_lower as f64;
56 finite[idx_lower] * (1.0 - weight) + finite[idx_upper] * weight
57 }
58}
59
60pub fn percent_rank(slice: &[f64], val: f64) -> f64 {
62 let finite: Vec<f64> = slice.iter().copied().filter(|v| v.is_finite()).collect();
63 if finite.is_empty() || !val.is_finite() {
64 return 0.0;
65 }
66 let count_below = finite.iter().filter(|&&v| v <= val).count();
67 (count_below as f64 / finite.len() as f64) * 100.0
68}
69
70pub fn correlation(left: &[f64], right: &[f64]) -> Option<f64> {
72 let pairs: Vec<(f64, f64)> = left
73 .iter()
74 .copied()
75 .zip(right.iter().copied())
76 .filter(|(x, y)| x.is_finite() && y.is_finite())
77 .collect();
78 if pairs.len() < 2 {
79 return None;
80 }
81 let count = pairs.len() as f64;
82 let mean_x = pairs.iter().map(|(x, _)| x).sum::<f64>() / count;
83 let mean_y = pairs.iter().map(|(_, y)| y).sum::<f64>() / count;
84 let covariance = pairs
85 .iter()
86 .map(|(x, y)| (x - mean_x) * (y - mean_y))
87 .sum::<f64>();
88 let variance_x = pairs.iter().map(|(x, _)| (x - mean_x).powi(2)).sum::<f64>();
89 let variance_y = pairs.iter().map(|(_, y)| (y - mean_y).powi(2)).sum::<f64>();
90 let denominator = (variance_x * variance_y).sqrt();
91 (denominator > f64::EPSILON).then_some(covariance / denominator)
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
96pub struct LinearRegressionResult {
97 pub slope: f64,
98 pub intercept: f64,
99 pub r2: f64,
100}
101
102pub fn linear_regression(slice: &[f64]) -> Option<LinearRegressionResult> {
104 let n = slice.len();
105 if n < 2 {
106 return None;
107 }
108
109 let mut sum_x = 0.0f64;
110 let mut sum_y = 0.0f64;
111 let mut sum_xy = 0.0f64;
112 let mut sum_xx = 0.0f64;
113 let mut valid_n = 0;
114
115 for (i, &y) in slice.iter().enumerate() {
116 if y.is_finite() {
117 let x = i as f64;
118 sum_x += x;
119 sum_y += y;
120 sum_xy += x * y;
121 sum_xx += x * x;
122 valid_n += 1;
123 }
124 }
125
126 if valid_n < 2 {
127 return None;
128 }
129
130 let fn_val = valid_n as f64;
131 let denom = fn_val * sum_xx - sum_x * sum_x;
132 if denom.abs() < 1e-12 {
133 return None;
134 }
135
136 let slope = (fn_val * sum_xy - sum_x * sum_y) / denom;
137 let intercept = (sum_y - slope * sum_x) / fn_val;
138
139 let y_mean = sum_y / fn_val;
140 let ss_tot: f64 = slice
141 .iter()
142 .filter(|v| v.is_finite())
143 .map(|&y| (y - y_mean).powi(2))
144 .sum();
145 let ss_res: f64 = slice
146 .iter()
147 .enumerate()
148 .filter(|(_, v)| v.is_finite())
149 .map(|(i, &y)| (y - (slope * i as f64 + intercept)).powi(2))
150 .sum();
151
152 let r2 = if ss_tot > 0.0 {
153 (1.0 - (ss_res / ss_tot)).clamp(0.0, 1.0)
154 } else {
155 1.0
156 };
157
158 Some(LinearRegressionResult {
159 slope,
160 intercept,
161 r2,
162 })
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_rolling_stats() {
171 let data = vec![10.0, 20.0, 30.0, 40.0, 50.0];
172 assert_eq!(rolling_sum(&data), 150.0);
173 assert_eq!(rolling_mean(&data), 30.0);
174 assert_eq!(rolling_median(&data), 30.0);
175 assert_eq!(percent_rank(&data, 30.0), 60.0);
176 }
177
178 #[test]
179 fn test_linear_regression() {
180 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
181 let res = linear_regression(&data).unwrap();
182 assert!((res.slope - 1.0).abs() < 1e-6);
183 assert!((res.intercept - 1.0).abs() < 1e-6);
184 assert!((res.r2 - 1.0).abs() < 1e-6);
185 }
186
187 #[test]
188 fn test_correlation() {
189 assert_eq!(correlation(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0]), Some(1.0));
190 assert_eq!(correlation(&[1.0, 1.0], &[1.0, 2.0]), None);
191 }
192}