#[derive(Debug, Clone)]
pub struct Dispersion {
pub mean: f64,
pub variance: f64,
pub std_dev: f64,
pub effective_n: f64,
pub weight_sum: f64,
}
impl Dispersion {
pub(crate) fn from_weighted_pairs(pairs: &[(f64, f64)]) -> Self {
debug_assert!(
!pairs.is_empty(),
"Dispersion::from_weighted_pairs requires at least one pair"
);
let weight_sum: f64 = pairs.iter().map(|(w, _)| w).sum();
if weight_sum <= 0.0 {
return Dispersion {
mean: pairs[0].1,
variance: 0.0,
std_dev: 0.0,
effective_n: 1.0,
weight_sum,
};
}
let mean = pairs.iter().map(|(w, o)| w * o).sum::<f64>() / weight_sum;
let variance = pairs
.iter()
.map(|(w, o)| w * (o - mean) * (o - mean))
.sum::<f64>()
/ weight_sum;
let max_w = pairs.iter().fold(0.0_f64, |acc, (w, _)| acc.max(*w));
let effective_n = if max_w > 0.0 {
let scaled_sum: f64 = pairs.iter().map(|(w, _)| w / max_w).sum();
let scaled_sum_w2: f64 = pairs.iter().map(|(w, _)| (w / max_w).powi(2)).sum();
if scaled_sum_w2 > 0.0 {
scaled_sum * scaled_sum / scaled_sum_w2
} else {
0.0
}
} else {
0.0
};
Dispersion {
mean,
variance,
std_dev: variance.sqrt(),
effective_n,
weight_sum,
}
}
}