use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FitnessEstimate {
pub mean: f64,
pub variance: f64,
pub ci_lower: f64,
pub ci_upper: f64,
pub observation_count: usize,
}
impl FitnessEstimate {
const Z_95: f64 = 1.96;
pub fn new(mean: f64, variance: f64, observation_count: usize) -> Self {
let std_err = variance.sqrt().max(0.0);
Self {
mean,
variance,
ci_lower: mean - Self::Z_95 * std_err,
ci_upper: mean + Self::Z_95 * std_err,
observation_count,
}
}
pub fn with_confidence(
mean: f64,
variance: f64,
observation_count: usize,
z_score: f64,
) -> Self {
let std_err = variance.sqrt().max(0.0);
Self {
mean,
variance,
ci_lower: mean - z_score * std_err,
ci_upper: mean + z_score * std_err,
observation_count,
}
}
pub fn uninformative(default_mean: f64) -> Self {
Self {
mean: default_mean,
variance: f64::INFINITY,
ci_lower: f64::NEG_INFINITY,
ci_upper: f64::INFINITY,
observation_count: 0,
}
}
pub fn std_error(&self) -> f64 {
self.variance.sqrt()
}
pub fn ci_width(&self) -> f64 {
self.ci_upper - self.ci_lower
}
pub fn ci_contains(&self, value: f64) -> bool {
value >= self.ci_lower && value <= self.ci_upper
}
pub fn ci_overlaps(&self, other: &FitnessEstimate) -> bool {
self.ci_lower <= other.ci_upper && self.ci_upper >= other.ci_lower
}
pub fn significantly_better_than(&self, other: &FitnessEstimate) -> bool {
self.ci_lower > other.ci_upper
}
pub fn is_uncertain(&self, min_observations: usize) -> bool {
self.variance.is_infinite() || self.observation_count < min_observations
}
pub fn coefficient_of_variation(&self) -> Option<f64> {
if self.mean.abs() < f64::EPSILON {
None
} else {
Some(self.std_error() / self.mean.abs())
}
}
pub fn merge(&self, other: &FitnessEstimate) -> FitnessEstimate {
if self.variance.is_infinite() {
return other.clone();
}
if other.variance.is_infinite() {
return self.clone();
}
if self.variance == 0.0 && other.variance == 0.0 {
return FitnessEstimate::new(
(self.mean + other.mean) / 2.0,
0.0,
self.observation_count + other.observation_count,
);
}
let w1 = 1.0 / self.variance;
let w2 = 1.0 / other.variance;
let w_total = w1 + w2;
let merged_mean = (w1 * self.mean + w2 * other.mean) / w_total;
let merged_variance = 1.0 / w_total;
let merged_count = self.observation_count + other.observation_count;
FitnessEstimate::new(merged_mean, merged_variance, merged_count)
}
}
impl Default for FitnessEstimate {
fn default() -> Self {
Self::uninformative(0.0)
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct WelfordVariance {
count: usize,
mean: f64,
m2: f64, }
impl WelfordVariance {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, value: f64) {
self.count += 1;
let delta = value - self.mean;
self.mean += delta / self.count as f64;
let delta2 = value - self.mean;
self.m2 += delta * delta2;
}
pub fn count(&self) -> usize {
self.count
}
pub fn mean(&self) -> f64 {
self.mean
}
pub fn sample_variance(&self) -> f64 {
if self.count < 2 {
f64::INFINITY
} else {
self.m2 / (self.count - 1) as f64
}
}
pub fn population_variance(&self) -> f64 {
if self.count == 0 {
f64::INFINITY
} else {
self.m2 / self.count as f64
}
}
pub fn variance_of_mean(&self) -> f64 {
if self.count == 0 {
f64::INFINITY
} else {
self.sample_variance() / self.count as f64
}
}
pub fn to_estimate(&self) -> FitnessEstimate {
FitnessEstimate::new(self.mean, self.variance_of_mean(), self.count)
}
pub fn merge(&self, other: &WelfordVariance) -> WelfordVariance {
if self.count == 0 {
return other.clone();
}
if other.count == 0 {
return self.clone();
}
let count = self.count + other.count;
let delta = other.mean - self.mean;
let mean = self.mean + delta * other.count as f64 / count as f64;
let m2 = self.m2
+ other.m2
+ delta * delta * self.count as f64 * other.count as f64 / count as f64;
WelfordVariance { count, mean, m2 }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fitness_estimate_creation() {
let est = FitnessEstimate::new(5.0, 0.25, 10);
assert_eq!(est.mean, 5.0);
assert_eq!(est.variance, 0.25);
assert_eq!(est.observation_count, 10);
assert!((est.std_error() - 0.5).abs() < 1e-9);
}
#[test]
fn test_confidence_interval() {
let est = FitnessEstimate::new(10.0, 1.0, 100);
assert!((est.ci_lower - 8.04).abs() < 0.01);
assert!((est.ci_upper - 11.96).abs() < 0.01);
assert!((est.ci_width() - 3.92).abs() < 0.01);
}
#[test]
fn test_ci_contains() {
let est = FitnessEstimate::new(10.0, 1.0, 100);
assert!(est.ci_contains(10.0));
assert!(est.ci_contains(9.0));
assert!(est.ci_contains(11.0));
assert!(!est.ci_contains(5.0));
assert!(!est.ci_contains(15.0));
}
#[test]
fn test_ci_overlaps() {
let est1 = FitnessEstimate::new(10.0, 1.0, 100);
let est2 = FitnessEstimate::new(11.0, 1.0, 100);
let est3 = FitnessEstimate::new(20.0, 1.0, 100);
assert!(est1.ci_overlaps(&est2)); assert!(!est1.ci_overlaps(&est3)); }
#[test]
fn test_significantly_better_than() {
let good = FitnessEstimate::new(20.0, 0.1, 100);
let bad = FitnessEstimate::new(10.0, 0.1, 100);
let uncertain = FitnessEstimate::new(15.0, 100.0, 5);
assert!(good.significantly_better_than(&bad));
assert!(!bad.significantly_better_than(&good));
assert!(!good.significantly_better_than(&uncertain)); }
#[test]
fn test_uninformative_estimate() {
let est = FitnessEstimate::uninformative(5.0);
assert_eq!(est.mean, 5.0);
assert!(est.variance.is_infinite());
assert!(est.is_uncertain(1));
}
#[test]
fn test_merge_estimates() {
let est1 = FitnessEstimate::new(10.0, 1.0, 10);
let est2 = FitnessEstimate::new(12.0, 1.0, 10);
let merged = est1.merge(&est2);
assert_eq!(merged.mean, 11.0); assert!(merged.variance < est1.variance); assert_eq!(merged.observation_count, 20);
}
#[test]
fn test_merge_with_uninformative() {
let est = FitnessEstimate::new(10.0, 1.0, 10);
let uninf = FitnessEstimate::uninformative(5.0);
let merged = est.merge(&uninf);
assert_eq!(merged.mean, est.mean);
assert_eq!(merged.variance, est.variance);
}
#[test]
fn test_welford_variance() {
let mut welford = WelfordVariance::new();
let values = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
for v in values {
welford.update(v);
}
assert_eq!(welford.count(), 8);
assert!((welford.mean() - 5.0).abs() < 1e-9);
assert!((welford.sample_variance() - 32.0 / 7.0).abs() < 1e-9);
}
#[test]
fn test_welford_merge() {
let mut w1 = WelfordVariance::new();
let mut w2 = WelfordVariance::new();
for v in [1.0, 2.0, 3.0] {
w1.update(v);
}
for v in [4.0, 5.0, 6.0] {
w2.update(v);
}
let merged = w1.merge(&w2);
assert_eq!(merged.count(), 6);
assert!((merged.mean() - 3.5).abs() < 1e-9);
}
#[test]
fn test_welford_to_estimate() {
let mut welford = WelfordVariance::new();
for v in [10.0, 11.0, 9.0, 10.0, 10.0] {
welford.update(v);
}
let est = welford.to_estimate();
assert_eq!(est.mean, welford.mean());
assert_eq!(est.observation_count, 5);
assert_eq!(est.variance, welford.variance_of_mean());
}
#[test]
fn test_coefficient_of_variation() {
let est = FitnessEstimate::new(10.0, 1.0, 100);
let cv = est.coefficient_of_variation().unwrap();
assert!((cv - 0.1).abs() < 1e-9);
let zero_mean = FitnessEstimate::new(0.0, 1.0, 100);
assert!(zero_mean.coefficient_of_variation().is_none());
}
}