1use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24pub struct FitnessEstimate {
25 pub mean: f64,
27 pub variance: f64,
29 pub ci_lower: f64,
31 pub ci_upper: f64,
33 pub observation_count: usize,
35}
36
37impl FitnessEstimate {
38 const Z_95: f64 = 1.96;
40
41 pub fn new(mean: f64, variance: f64, observation_count: usize) -> Self {
51 let std_err = variance.sqrt().max(0.0);
52 Self {
53 mean,
54 variance,
55 ci_lower: mean - Self::Z_95 * std_err,
56 ci_upper: mean + Self::Z_95 * std_err,
57 observation_count,
58 }
59 }
60
61 pub fn with_confidence(
70 mean: f64,
71 variance: f64,
72 observation_count: usize,
73 z_score: f64,
74 ) -> Self {
75 let std_err = variance.sqrt().max(0.0);
76 Self {
77 mean,
78 variance,
79 ci_lower: mean - z_score * std_err,
80 ci_upper: mean + z_score * std_err,
81 observation_count,
82 }
83 }
84
85 pub fn uninformative(default_mean: f64) -> Self {
89 Self {
90 mean: default_mean,
91 variance: f64::INFINITY,
92 ci_lower: f64::NEG_INFINITY,
93 ci_upper: f64::INFINITY,
94 observation_count: 0,
95 }
96 }
97
98 pub fn std_error(&self) -> f64 {
100 self.variance.sqrt()
101 }
102
103 pub fn ci_width(&self) -> f64 {
105 self.ci_upper - self.ci_lower
106 }
107
108 pub fn ci_contains(&self, value: f64) -> bool {
110 value >= self.ci_lower && value <= self.ci_upper
111 }
112
113 pub fn ci_overlaps(&self, other: &FitnessEstimate) -> bool {
115 self.ci_lower <= other.ci_upper && self.ci_upper >= other.ci_lower
116 }
117
118 pub fn significantly_better_than(&self, other: &FitnessEstimate) -> bool {
123 self.ci_lower > other.ci_upper
124 }
125
126 pub fn is_uncertain(&self, min_observations: usize) -> bool {
130 self.variance.is_infinite() || self.observation_count < min_observations
131 }
132
133 pub fn coefficient_of_variation(&self) -> Option<f64> {
137 if self.mean.abs() < f64::EPSILON {
138 None
139 } else {
140 Some(self.std_error() / self.mean.abs())
141 }
142 }
143
144 pub fn merge(&self, other: &FitnessEstimate) -> FitnessEstimate {
149 if self.variance.is_infinite() {
151 return other.clone();
152 }
153 if other.variance.is_infinite() {
154 return self.clone();
155 }
156 if self.variance == 0.0 && other.variance == 0.0 {
157 return FitnessEstimate::new(
159 (self.mean + other.mean) / 2.0,
160 0.0,
161 self.observation_count + other.observation_count,
162 );
163 }
164
165 let w1 = 1.0 / self.variance;
167 let w2 = 1.0 / other.variance;
168 let w_total = w1 + w2;
169
170 let merged_mean = (w1 * self.mean + w2 * other.mean) / w_total;
171 let merged_variance = 1.0 / w_total;
172 let merged_count = self.observation_count + other.observation_count;
173
174 FitnessEstimate::new(merged_mean, merged_variance, merged_count)
175 }
176}
177
178impl Default for FitnessEstimate {
179 fn default() -> Self {
180 Self::uninformative(0.0)
181 }
182}
183
184#[derive(Clone, Debug, Default, Serialize, Deserialize)]
188pub struct WelfordVariance {
189 count: usize,
190 mean: f64,
191 m2: f64, }
193
194impl WelfordVariance {
195 pub fn new() -> Self {
197 Self::default()
198 }
199
200 pub fn update(&mut self, value: f64) {
202 self.count += 1;
203 let delta = value - self.mean;
204 self.mean += delta / self.count as f64;
205 let delta2 = value - self.mean;
206 self.m2 += delta * delta2;
207 }
208
209 pub fn count(&self) -> usize {
211 self.count
212 }
213
214 pub fn mean(&self) -> f64 {
216 self.mean
217 }
218
219 pub fn sample_variance(&self) -> f64 {
221 if self.count < 2 {
222 f64::INFINITY
223 } else {
224 self.m2 / (self.count - 1) as f64
225 }
226 }
227
228 pub fn population_variance(&self) -> f64 {
230 if self.count == 0 {
231 f64::INFINITY
232 } else {
233 self.m2 / self.count as f64
234 }
235 }
236
237 pub fn variance_of_mean(&self) -> f64 {
239 if self.count == 0 {
240 f64::INFINITY
241 } else {
242 self.sample_variance() / self.count as f64
243 }
244 }
245
246 pub fn to_estimate(&self) -> FitnessEstimate {
248 FitnessEstimate::new(self.mean, self.variance_of_mean(), self.count)
249 }
250
251 pub fn merge(&self, other: &WelfordVariance) -> WelfordVariance {
253 if self.count == 0 {
254 return other.clone();
255 }
256 if other.count == 0 {
257 return self.clone();
258 }
259
260 let count = self.count + other.count;
261 let delta = other.mean - self.mean;
262 let mean = self.mean + delta * other.count as f64 / count as f64;
263 let m2 = self.m2
264 + other.m2
265 + delta * delta * self.count as f64 * other.count as f64 / count as f64;
266
267 WelfordVariance { count, mean, m2 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_fitness_estimate_creation() {
277 let est = FitnessEstimate::new(5.0, 0.25, 10);
278 assert_eq!(est.mean, 5.0);
279 assert_eq!(est.variance, 0.25);
280 assert_eq!(est.observation_count, 10);
281 assert!((est.std_error() - 0.5).abs() < 1e-9);
282 }
283
284 #[test]
285 fn test_confidence_interval() {
286 let est = FitnessEstimate::new(10.0, 1.0, 100);
287 assert!((est.ci_lower - 8.04).abs() < 0.01);
289 assert!((est.ci_upper - 11.96).abs() < 0.01);
290 assert!((est.ci_width() - 3.92).abs() < 0.01);
291 }
292
293 #[test]
294 fn test_ci_contains() {
295 let est = FitnessEstimate::new(10.0, 1.0, 100);
296 assert!(est.ci_contains(10.0));
297 assert!(est.ci_contains(9.0));
298 assert!(est.ci_contains(11.0));
299 assert!(!est.ci_contains(5.0));
300 assert!(!est.ci_contains(15.0));
301 }
302
303 #[test]
304 fn test_ci_overlaps() {
305 let est1 = FitnessEstimate::new(10.0, 1.0, 100);
306 let est2 = FitnessEstimate::new(11.0, 1.0, 100);
307 let est3 = FitnessEstimate::new(20.0, 1.0, 100);
308
309 assert!(est1.ci_overlaps(&est2)); assert!(!est1.ci_overlaps(&est3)); }
312
313 #[test]
314 fn test_significantly_better_than() {
315 let good = FitnessEstimate::new(20.0, 0.1, 100);
316 let bad = FitnessEstimate::new(10.0, 0.1, 100);
317 let uncertain = FitnessEstimate::new(15.0, 100.0, 5);
318
319 assert!(good.significantly_better_than(&bad));
320 assert!(!bad.significantly_better_than(&good));
321 assert!(!good.significantly_better_than(&uncertain)); }
323
324 #[test]
325 fn test_uninformative_estimate() {
326 let est = FitnessEstimate::uninformative(5.0);
327 assert_eq!(est.mean, 5.0);
328 assert!(est.variance.is_infinite());
329 assert!(est.is_uncertain(1));
330 }
331
332 #[test]
333 fn test_merge_estimates() {
334 let est1 = FitnessEstimate::new(10.0, 1.0, 10);
335 let est2 = FitnessEstimate::new(12.0, 1.0, 10);
336
337 let merged = est1.merge(&est2);
338 assert_eq!(merged.mean, 11.0); assert!(merged.variance < est1.variance); assert_eq!(merged.observation_count, 20);
341 }
342
343 #[test]
344 fn test_merge_with_uninformative() {
345 let est = FitnessEstimate::new(10.0, 1.0, 10);
346 let uninf = FitnessEstimate::uninformative(5.0);
347
348 let merged = est.merge(&uninf);
349 assert_eq!(merged.mean, est.mean);
350 assert_eq!(merged.variance, est.variance);
351 }
352
353 #[test]
354 fn test_welford_variance() {
355 let mut welford = WelfordVariance::new();
356 let values = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
357
358 for v in values {
359 welford.update(v);
360 }
361
362 assert_eq!(welford.count(), 8);
363 assert!((welford.mean() - 5.0).abs() < 1e-9);
364 assert!((welford.sample_variance() - 32.0 / 7.0).abs() < 1e-9);
366 }
367
368 #[test]
369 fn test_welford_merge() {
370 let mut w1 = WelfordVariance::new();
371 let mut w2 = WelfordVariance::new();
372
373 for v in [1.0, 2.0, 3.0] {
374 w1.update(v);
375 }
376 for v in [4.0, 5.0, 6.0] {
377 w2.update(v);
378 }
379
380 let merged = w1.merge(&w2);
381 assert_eq!(merged.count(), 6);
382 assert!((merged.mean() - 3.5).abs() < 1e-9);
383 }
384
385 #[test]
386 fn test_welford_to_estimate() {
387 let mut welford = WelfordVariance::new();
388 for v in [10.0, 11.0, 9.0, 10.0, 10.0] {
389 welford.update(v);
390 }
391
392 let est = welford.to_estimate();
393 assert_eq!(est.mean, welford.mean());
394 assert_eq!(est.observation_count, 5);
395 assert_eq!(est.variance, welford.variance_of_mean());
396 }
397
398 #[test]
399 fn test_coefficient_of_variation() {
400 let est = FitnessEstimate::new(10.0, 1.0, 100);
401 let cv = est.coefficient_of_variation().unwrap();
402 assert!((cv - 0.1).abs() < 1e-9); let zero_mean = FitnessEstimate::new(0.0, 1.0, 100);
405 assert!(zero_mean.coefficient_of_variation().is_none());
406 }
407}