use crate::foundation::{Provenance, Stats, Uncertain};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DistributionShape {
Normal,
Triangular,
LogNormal,
}
pub fn characterise(values: &[f64], shape: DistributionShape, provenance: Provenance) -> Uncertain {
let stats = Stats::of(values);
if stats.count < 2 {
return Uncertain {
value: stats.mean,
distribution: crate::foundation::Distribution::Deterministic,
provenance,
};
}
match shape {
DistributionShape::Normal => Uncertain::from_stats(&stats, provenance),
DistributionShape::Triangular => {
Uncertain::triangular(stats.p10, stats.p50, stats.p90).with_provenance(provenance)
}
DistributionShape::LogNormal => {
let logs: Vec<f64> = values
.iter()
.filter(|v| v.is_finite() && **v > 0.0)
.map(|v| v.ln())
.collect();
if logs.len() < 2 {
return Uncertain::defaulted(stats.mean).with_provenance(provenance);
}
let ls = Stats::of(&logs);
Uncertain::lognormal(ls.mean, ls.std).with_provenance(provenance)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::foundation::Distribution;
use approx::assert_relative_eq;
#[test]
fn normal_fit_carries_mean_and_provenance() {
let u = characterise(
&[1.0, 2.0, 3.0],
DistributionShape::Normal,
Provenance::Interpolated,
);
assert_relative_eq!(u.value, 2.0);
assert!(matches!(u.distribution, Distribution::Normal { .. }));
assert_eq!(u.provenance, Provenance::Interpolated);
}
#[test]
fn triangular_uses_p10_p50_p90() {
let u = characterise(
&[1.0, 2.0, 3.0, 4.0, 5.0],
DistributionShape::Triangular,
Provenance::HardData,
);
assert_relative_eq!(u.value, 3.0); match u.distribution {
Distribution::Triangular { lo, mode, hi } => {
assert_relative_eq!(lo, 1.4, epsilon = 1e-9);
assert_relative_eq!(mode, 3.0, epsilon = 1e-9);
assert_relative_eq!(hi, 4.6, epsilon = 1e-9);
}
other => panic!("expected Triangular, got {other:?}"),
}
assert_eq!(u.provenance, Provenance::HardData);
}
#[test]
fn lognormal_fits_on_log_and_returns_geometric_mean() {
let u = characterise(
&[1.0, std::f64::consts::E, std::f64::consts::E.powi(2)],
DistributionShape::LogNormal,
Provenance::Interpolated,
);
assert_relative_eq!(u.value, std::f64::consts::E, epsilon = 1e-9);
match u.distribution {
Distribution::LogNormal { mu, .. } => assert_relative_eq!(mu, 1.0, epsilon = 1e-9),
other => panic!("expected LogNormal, got {other:?}"),
}
}
#[test]
fn collapses_to_deterministic_below_two() {
let u = characterise(&[7.0], DistributionShape::Normal, Provenance::HardData);
assert_eq!(u.distribution, Distribution::Deterministic);
assert_relative_eq!(u.value, 7.0);
}
}