pub fn mean(data: &[f64]) -> Option<f64> {
if data.is_empty() {
return None;
}
if !data.iter().all(|x| x.is_finite()) {
return None;
}
Some(kahan_sum(data) / data.len() as f64)
}
pub fn variance(data: &[f64]) -> Option<f64> {
if data.len() < 2 {
return None;
}
if !data.iter().all(|x| x.is_finite()) {
return None;
}
let mut acc = WelfordAccumulator::new();
for &x in data {
acc.update(x);
}
acc.sample_variance()
}
pub fn population_variance(data: &[f64]) -> Option<f64> {
if data.is_empty() {
return None;
}
if !data.iter().all(|x| x.is_finite()) {
return None;
}
let mut acc = WelfordAccumulator::new();
for &x in data {
acc.update(x);
}
acc.population_variance()
}
pub fn std_dev(data: &[f64]) -> Option<f64> {
variance(data).map(f64::sqrt)
}
pub fn population_std_dev(data: &[f64]) -> Option<f64> {
population_variance(data).map(f64::sqrt)
}
pub fn min(data: &[f64]) -> Option<f64> {
if data.is_empty() {
return None;
}
data.iter().copied().try_fold(f64::INFINITY, |acc, x| {
if x.is_nan() {
None
} else {
Some(acc.min(x))
}
})
}
pub fn max(data: &[f64]) -> Option<f64> {
if data.is_empty() {
return None;
}
data.iter().copied().try_fold(f64::NEG_INFINITY, |acc, x| {
if x.is_nan() {
None
} else {
Some(acc.max(x))
}
})
}
pub fn median(data: &[f64]) -> Option<f64> {
if data.is_empty() {
return None;
}
if data.iter().any(|x| x.is_nan()) {
return None;
}
let mut sorted = data.to_vec();
sorted.sort_unstable_by(|a, b| a.partial_cmp(b).expect("NaN filtered above"));
let n = sorted.len();
if n % 2 == 1 {
Some(sorted[n / 2])
} else {
Some((sorted[n / 2 - 1] + sorted[n / 2]) / 2.0)
}
}
pub fn quantile(data: &[f64], p: f64) -> Option<f64> {
if data.is_empty() || !(0.0..=1.0).contains(&p) {
return None;
}
if data.iter().any(|x| x.is_nan()) {
return None;
}
let mut sorted = data.to_vec();
sorted.sort_unstable_by(|a, b| a.partial_cmp(b).expect("NaN filtered above"));
quantile_sorted(&sorted, p)
}
pub fn quantile_sorted(sorted_data: &[f64], p: f64) -> Option<f64> {
let n = sorted_data.len();
if n == 0 || !(0.0..=1.0).contains(&p) {
return None;
}
if n == 1 {
return Some(sorted_data[0]);
}
let h = (n - 1) as f64 * p;
let j = h.floor() as usize;
let g = h - h.floor();
if j + 1 >= n {
Some(sorted_data[n - 1])
} else {
Some((1.0 - g) * sorted_data[j] + g * sorted_data[j + 1])
}
}
pub fn skewness(data: &[f64]) -> Option<f64> {
let n = data.len();
if n < 3 {
return None;
}
if !data.iter().all(|x| x.is_finite()) {
return None;
}
let nf = n as f64;
let m = kahan_sum(data) / nf;
let mut sum2 = 0.0;
let mut sum3 = 0.0;
for &x in data {
let d = x - m;
let d2 = d * d;
sum2 += d2;
sum3 += d2 * d;
}
let m2 = sum2 / nf;
if m2 == 0.0 {
return None;
}
let m3 = sum3 / nf;
let g1 = m3 / m2.powf(1.5);
let correction = (nf * (nf - 1.0)).sqrt() / (nf - 2.0);
Some(correction * g1)
}
pub fn kurtosis(data: &[f64]) -> Option<f64> {
let n = data.len();
if n < 4 {
return None;
}
if !data.iter().all(|x| x.is_finite()) {
return None;
}
let nf = n as f64;
let m = kahan_sum(data) / nf;
let mut sum2 = 0.0;
let mut sum4 = 0.0;
for &x in data {
let d = x - m;
let d2 = d * d;
sum2 += d2;
sum4 += d2 * d2;
}
let s2 = sum2 / (nf - 1.0);
if s2 == 0.0 {
return None;
}
let s4 = s2 * s2;
let sum_z4 = sum4 / s4;
let a = nf * (nf + 1.0) / ((nf - 1.0) * (nf - 2.0) * (nf - 3.0));
let b = 3.0 * (nf - 1.0) * (nf - 1.0) / ((nf - 2.0) * (nf - 3.0));
Some(a * sum_z4 - b)
}
pub fn covariance(x: &[f64], y: &[f64]) -> Option<f64> {
let n = x.len();
if n != y.len() || n < 2 {
return None;
}
if !x.iter().chain(y.iter()).all(|v| v.is_finite()) {
return None;
}
let nf = n as f64;
let mean_x = kahan_sum(x) / nf;
let mean_y = kahan_sum(y) / nf;
let mut sum = 0.0;
for i in 0..n {
sum += (x[i] - mean_x) * (y[i] - mean_y);
}
Some(sum / (nf - 1.0))
}
pub fn kahan_sum(data: &[f64]) -> f64 {
let mut sum = 0.0_f64;
let mut c = 0.0_f64;
for &x in data {
let t = sum + x;
if sum.abs() >= x.abs() {
c += (sum - t) + x;
} else {
c += (x - t) + sum;
}
sum = t;
}
sum + c
}
#[derive(Debug, Clone)]
pub struct WelfordAccumulator {
count: u64,
mean_acc: f64,
m2: f64,
m3: f64,
m4: f64,
}
impl WelfordAccumulator {
pub fn new() -> Self {
Self {
count: 0,
mean_acc: 0.0,
m2: 0.0,
m3: 0.0,
m4: 0.0,
}
}
pub fn update(&mut self, value: f64) {
let n1 = self.count;
self.count += 1;
if n1 == 0 {
self.mean_acc = value;
return;
}
let n = self.count as f64;
let delta = value - self.mean_acc;
let delta_n = delta / n;
let delta_n2 = delta_n * delta_n;
let term1 = delta * delta_n * n1 as f64;
self.m4 += term1 * delta_n2 * (n * n - 3.0 * n + 3.0) + 6.0 * delta_n2 * self.m2
- 4.0 * delta_n * self.m3;
self.m3 += term1 * delta_n * (n - 2.0) - 3.0 * delta_n * self.m2;
self.m2 += term1;
self.mean_acc += delta_n;
}
pub fn count(&self) -> u64 {
self.count
}
pub fn mean(&self) -> Option<f64> {
if self.count == 0 {
None
} else {
Some(self.mean_acc)
}
}
pub fn sample_variance(&self) -> Option<f64> {
if self.count < 2 {
None
} else {
Some(self.m2 / (self.count - 1) as f64)
}
}
pub fn population_variance(&self) -> Option<f64> {
if self.count == 0 {
None
} else {
Some(self.m2 / self.count as f64)
}
}
pub fn sample_std_dev(&self) -> Option<f64> {
self.sample_variance().map(f64::sqrt)
}
pub fn population_std_dev(&self) -> Option<f64> {
self.population_variance().map(f64::sqrt)
}
pub fn skewness(&self) -> Option<f64> {
if self.count < 3 {
return None;
}
let n = self.count as f64;
if self.m2 == 0.0 {
return None;
}
let g1 = n.sqrt() * self.m3 / self.m2.powf(1.5);
let correction = (n * (n - 1.0)).sqrt() / (n - 2.0);
Some(correction * g1)
}
pub fn kurtosis(&self) -> Option<f64> {
if self.count < 4 {
return None;
}
let n = self.count as f64;
if self.m2 == 0.0 {
return None;
}
let g2 = n * self.m4 / (self.m2 * self.m2) - 3.0;
let correction = (n - 1.0) / ((n - 2.0) * (n - 3.0));
Some(correction * ((n + 1.0) * g2 + 6.0))
}
pub fn merge(&mut self, other: &WelfordAccumulator) {
if other.count == 0 {
return;
}
if self.count == 0 {
*self = other.clone();
return;
}
let na = self.count as f64;
let nb = other.count as f64;
let total = self.count + other.count;
let n = total as f64;
let delta = other.mean_acc - self.mean_acc;
let delta2 = delta * delta;
let delta3 = delta2 * delta;
let delta4 = delta2 * delta2;
let new_mean = self.mean_acc + delta * (nb / n);
let new_m2 = self.m2 + other.m2 + delta2 * na * nb / n;
let new_m3 = self.m3
+ other.m3
+ delta3 * na * nb * (na - nb) / (n * n)
+ 3.0 * delta * (na * other.m2 - nb * self.m2) / n;
let new_m4 = self.m4
+ other.m4
+ delta4 * na * nb * (na * na - na * nb + nb * nb) / (n * n * n)
+ 6.0 * delta2 * (na * na * other.m2 + nb * nb * self.m2) / (n * n)
+ 4.0 * delta * (na * other.m3 - nb * self.m3) / n;
self.count = total;
self.mean_acc = new_mean;
self.m2 = new_m2;
self.m3 = new_m3;
self.m4 = new_m4;
}
}
impl Default for WelfordAccumulator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mean_basic() {
assert_eq!(mean(&[1.0, 2.0, 3.0, 4.0, 5.0]), Some(3.0));
}
#[test]
fn test_mean_single() {
assert_eq!(mean(&[42.0]), Some(42.0));
}
#[test]
fn test_mean_empty() {
assert_eq!(mean(&[]), None);
}
#[test]
fn test_mean_nan() {
assert_eq!(mean(&[1.0, f64::NAN, 3.0]), None);
}
#[test]
fn test_mean_inf() {
assert_eq!(mean(&[1.0, f64::INFINITY, 3.0]), None);
}
#[test]
fn test_variance_basic() {
let v = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let var = variance(&v).unwrap();
assert!((var - 4.571428571428571).abs() < 1e-10);
}
#[test]
fn test_variance_constant() {
let v = [5.0; 100];
assert!((variance(&v).unwrap()).abs() < 1e-15);
}
#[test]
fn test_variance_single() {
assert_eq!(variance(&[1.0]), None);
}
#[test]
fn test_variance_empty() {
assert_eq!(variance(&[]), None);
}
#[test]
fn test_population_variance() {
let v = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let var = population_variance(&v).unwrap();
assert!((var - 4.0).abs() < 1e-10);
}
#[test]
fn test_std_dev() {
let v = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let sd = std_dev(&v).unwrap();
let expected = 4.571428571428571_f64.sqrt();
assert!((sd - expected).abs() < 1e-10);
}
#[test]
fn test_min_max() {
let v = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
assert_eq!(min(&v), Some(1.0));
assert_eq!(max(&v), Some(9.0));
}
#[test]
fn test_min_max_empty() {
assert_eq!(min(&[]), None);
assert_eq!(max(&[]), None);
}
#[test]
fn test_min_max_nan() {
assert_eq!(min(&[1.0, f64::NAN]), None);
assert_eq!(max(&[1.0, f64::NAN]), None);
}
#[test]
fn test_median_odd() {
assert_eq!(median(&[3.0, 1.0, 2.0]), Some(2.0));
}
#[test]
fn test_median_even() {
assert_eq!(median(&[4.0, 1.0, 3.0, 2.0]), Some(2.5));
}
#[test]
fn test_median_single() {
assert_eq!(median(&[7.0]), Some(7.0));
}
#[test]
fn test_median_empty() {
assert_eq!(median(&[]), None);
}
#[test]
fn test_quantile_extremes() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
assert_eq!(quantile(&data, 0.0), Some(1.0));
assert_eq!(quantile(&data, 1.0), Some(5.0));
}
#[test]
fn test_quantile_median() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
assert_eq!(quantile(&data, 0.5), Some(3.0));
}
#[test]
fn test_quantile_interpolation() {
let data = [1.0, 2.0, 3.0, 4.0];
let q = quantile(&data, 0.25).unwrap();
assert!((q - 1.75).abs() < 1e-15);
}
#[test]
fn test_quantile_invalid_p() {
assert_eq!(quantile(&[1.0, 2.0], -0.1), None);
assert_eq!(quantile(&[1.0, 2.0], 1.1), None);
}
#[test]
fn test_quantile_empty() {
assert_eq!(quantile(&[], 0.5), None);
}
#[test]
fn test_quantile_single() {
assert_eq!(quantile(&[42.0], 0.0), Some(42.0));
assert_eq!(quantile(&[42.0], 0.5), Some(42.0));
assert_eq!(quantile(&[42.0], 1.0), Some(42.0));
}
#[test]
fn test_kahan_sum_basic() {
let v = [1.0, 2.0, 3.0];
assert!((kahan_sum(&v) - 6.0).abs() < 1e-15);
}
#[test]
fn test_kahan_sum_precision() {
let v = [1e16, 1.0, -1e16];
let result = kahan_sum(&v);
assert!(
(result - 1.0).abs() < 1e-10,
"Kahan sum should preserve the 1.0: got {result}"
);
}
#[test]
fn test_welford_empty() {
let acc = WelfordAccumulator::new();
assert_eq!(acc.count(), 0);
assert_eq!(acc.mean(), None);
assert_eq!(acc.sample_variance(), None);
}
#[test]
fn test_welford_single() {
let mut acc = WelfordAccumulator::new();
acc.update(5.0);
assert_eq!(acc.mean(), Some(5.0));
assert_eq!(acc.sample_variance(), None);
assert_eq!(acc.population_variance(), Some(0.0));
}
#[test]
fn test_welford_matches_batch() {
let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let mut acc = WelfordAccumulator::new();
for &x in &data {
acc.update(x);
}
let batch_mean = mean(&data).unwrap();
let batch_var = variance(&data).unwrap();
assert!((acc.mean().unwrap() - batch_mean).abs() < 1e-14);
assert!((acc.sample_variance().unwrap() - batch_var).abs() < 1e-10);
}
#[test]
fn test_welford_merge() {
let data_a = [1.0, 2.0, 3.0, 4.0];
let data_b = [5.0, 6.0, 7.0, 8.0];
let data_all: Vec<f64> = data_a.iter().chain(data_b.iter()).copied().collect();
let mut acc_a = WelfordAccumulator::new();
for &x in &data_a {
acc_a.update(x);
}
let mut acc_b = WelfordAccumulator::new();
for &x in &data_b {
acc_b.update(x);
}
acc_a.merge(&acc_b);
let expected_mean = mean(&data_all).unwrap();
let expected_var = variance(&data_all).unwrap();
assert!((acc_a.mean().unwrap() - expected_mean).abs() < 1e-14);
assert!((acc_a.sample_variance().unwrap() - expected_var).abs() < 1e-10);
}
#[test]
fn test_skewness_symmetric() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
assert!(skewness(&data).unwrap().abs() < 1e-14);
}
#[test]
fn test_skewness_right_skewed() {
let data = [1.0, 2.0, 3.0, 4.0, 50.0];
let s = skewness(&data).unwrap();
assert!(s > 0.0, "expected positive skewness, got {s}");
}
#[test]
fn test_skewness_left_skewed() {
let data = [-50.0, 1.0, 2.0, 3.0, 4.0];
let s = skewness(&data).unwrap();
assert!(s < 0.0, "expected negative skewness, got {s}");
}
#[test]
fn test_skewness_edge_cases() {
assert_eq!(skewness(&[]), None);
assert_eq!(skewness(&[1.0]), None);
assert_eq!(skewness(&[1.0, 2.0]), None);
assert_eq!(skewness(&[5.0, 5.0, 5.0]), None);
assert_eq!(skewness(&[1.0, f64::NAN, 3.0]), None);
}
#[test]
fn test_skewness_known_value() {
let data = [1.0, 2.0, 3.0, 4.0, 8.0];
let s = skewness(&data).unwrap();
assert!(
(s - 1.339).abs() < 0.01,
"expected skewness ≈ 1.34, got {s}"
);
}
#[test]
fn test_kurtosis_uniform_negative() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let k = kurtosis(&data).unwrap();
assert!(k < 0.0, "uniform data should be platykurtic, got {k}");
}
#[test]
fn test_kurtosis_edge_cases() {
assert_eq!(kurtosis(&[]), None);
assert_eq!(kurtosis(&[1.0]), None);
assert_eq!(kurtosis(&[1.0, 2.0]), None);
assert_eq!(kurtosis(&[1.0, 2.0, 3.0]), None);
assert_eq!(kurtosis(&[5.0, 5.0, 5.0, 5.0]), None);
assert_eq!(kurtosis(&[1.0, f64::NAN, 3.0, 4.0]), None);
}
#[test]
fn test_kurtosis_heavy_tails() {
let data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0];
let k = kurtosis(&data).unwrap();
assert!(k > 0.0, "heavy-tailed data should be leptokurtic, got {k}");
}
#[test]
fn test_covariance_perfect_positive() {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let y = [2.0, 4.0, 6.0, 8.0, 10.0];
let cov = covariance(&x, &y).unwrap();
assert!((cov - 5.0).abs() < 1e-14);
}
#[test]
fn test_covariance_perfect_negative() {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let y = [10.0, 8.0, 6.0, 4.0, 2.0];
let cov = covariance(&x, &y).unwrap();
assert!((cov - (-5.0)).abs() < 1e-14);
}
#[test]
fn test_covariance_zero() {
let x = [1.0, 2.0, 3.0, 4.0];
let y = [5.0, 5.0, 5.0, 5.0]; let cov = covariance(&x, &y).unwrap();
assert!(cov.abs() < 1e-14);
}
#[test]
fn test_covariance_self_is_variance() {
let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let cov_xx = covariance(&data, &data).unwrap();
let var = variance(&data).unwrap();
assert!((cov_xx - var).abs() < 1e-10);
}
#[test]
fn test_covariance_edge_cases() {
assert_eq!(covariance(&[], &[]), None);
assert_eq!(covariance(&[1.0], &[2.0]), None);
assert_eq!(covariance(&[1.0, 2.0], &[1.0]), None); assert_eq!(covariance(&[1.0, f64::NAN], &[1.0, 2.0]), None);
}
#[test]
fn test_welford_skewness_matches_batch() {
let data = [1.0, 2.0, 3.0, 4.0, 50.0];
let mut acc = WelfordAccumulator::new();
for &x in &data {
acc.update(x);
}
let batch = skewness(&data).unwrap();
let stream = acc.skewness().unwrap();
assert!(
(batch - stream).abs() < 1e-10,
"batch={batch}, stream={stream}"
);
}
#[test]
fn test_welford_kurtosis_matches_batch() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 100.0];
let mut acc = WelfordAccumulator::new();
for &x in &data {
acc.update(x);
}
let batch = kurtosis(&data).unwrap();
let stream = acc.kurtosis().unwrap();
assert!(
(batch - stream).abs() < 1e-8,
"batch={batch}, stream={stream}"
);
}
#[test]
fn test_welford_skewness_kurtosis_edge_cases() {
let mut acc = WelfordAccumulator::new();
assert_eq!(acc.skewness(), None);
assert_eq!(acc.kurtosis(), None);
acc.update(1.0);
assert_eq!(acc.skewness(), None);
assert_eq!(acc.kurtosis(), None);
acc.update(2.0);
assert_eq!(acc.skewness(), None);
assert_eq!(acc.kurtosis(), None);
acc.update(3.0);
assert!(acc.skewness().is_some());
assert_eq!(acc.kurtosis(), None);
acc.update(4.0);
assert!(acc.kurtosis().is_some());
}
#[test]
fn test_welford_merge_skewness_kurtosis() {
let data_a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0];
let data_b = [2.0, 50.0, 4.0, 6.0, 8.0, 100.0];
let all: Vec<f64> = data_a.iter().chain(data_b.iter()).copied().collect();
let mut acc_a = WelfordAccumulator::new();
for &x in &data_a {
acc_a.update(x);
}
let mut acc_b = WelfordAccumulator::new();
for &x in &data_b {
acc_b.update(x);
}
acc_a.merge(&acc_b);
let expected_skew = skewness(&all).unwrap();
let expected_kurt = kurtosis(&all).unwrap();
let merged_skew = acc_a.skewness().unwrap();
let merged_kurt = acc_a.kurtosis().unwrap();
assert!(
(expected_skew - merged_skew).abs() < 1e-8,
"skewness: expected={expected_skew}, merged={merged_skew}"
);
assert!(
(expected_kurt - merged_kurt).abs() < 1e-6,
"kurtosis: expected={expected_kurt}, merged={merged_kurt}"
);
}
#[test]
fn test_variance_large_offset() {
let data: Vec<f64> = (1..=5).map(|i| 1e9 + i as f64).collect();
let var = variance(&data).unwrap();
assert!(
(var - 2.5).abs() < 1e-5,
"Variance of offset data should be ~2.5, got {var}"
);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn finite_vec(min_len: usize, max_len: usize) -> impl Strategy<Value = Vec<f64>> {
proptest::collection::vec(
prop::num::f64::NORMAL.prop_filter("finite", |x| x.is_finite() && x.abs() < 1e12),
min_len..=max_len,
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(500))]
#[test]
fn variance_non_negative(data in finite_vec(2, 100)) {
let var = variance(&data).unwrap();
prop_assert!(var >= 0.0, "variance must be >= 0, got {}", var);
}
#[test]
fn variance_of_constant_is_zero(
value in prop::num::f64::NORMAL.prop_filter("finite", |x| x.is_finite()),
n in 2_usize..50,
) {
let data = vec![value; n];
let var = variance(&data).unwrap();
prop_assert!(var.abs() < 1e-10, "variance of constant should be ~0, got {}", var);
}
#[test]
fn std_dev_is_sqrt_of_variance(data in finite_vec(2, 100)) {
let var = variance(&data).unwrap();
let sd = std_dev(&data).unwrap();
let diff = (sd * sd - var).abs();
prop_assert!(diff < 1e-10 * var.max(1.0), "sd² should equal variance");
}
#[test]
fn mean_linearity(
data in finite_vec(1, 100),
a in -100.0_f64..100.0,
b in -100.0_f64..100.0,
) {
prop_assume!(a.is_finite() && b.is_finite());
let m = mean(&data).unwrap();
let transformed: Vec<f64> = data.iter().map(|&x| a * x + b).collect();
if let Some(mt) = mean(&transformed) {
let expected = a * m + b;
let tol = 1e-8 * expected.abs().max(1.0);
prop_assert!(
(mt - expected).abs() < tol,
"mean(a*x+b)={} != a*mean(x)+b={}",
mt, expected
);
}
}
#[test]
fn quantile_extremes_are_min_max(data in finite_vec(1, 100)) {
let q0 = quantile(&data, 0.0).unwrap();
let q1 = quantile(&data, 1.0).unwrap();
let mn = min(&data).unwrap();
let mx = max(&data).unwrap();
prop_assert!((q0 - mn).abs() < 1e-15, "quantile(0) should be min");
prop_assert!((q1 - mx).abs() < 1e-15, "quantile(1) should be max");
}
#[test]
fn quantiles_monotonic(
data in finite_vec(2, 100),
p1 in 0.0_f64..=1.0,
p2 in 0.0_f64..=1.0,
) {
let (lo, hi) = if p1 <= p2 { (p1, p2) } else { (p2, p1) };
let q_lo = quantile(&data, lo).unwrap();
let q_hi = quantile(&data, hi).unwrap();
prop_assert!(q_lo <= q_hi + 1e-15, "quantiles should be monotonic");
}
#[test]
fn median_equals_quantile_half(data in finite_vec(1, 100)) {
let med = median(&data).unwrap();
let q50 = quantile(&data, 0.5).unwrap();
prop_assert!(
(med - q50).abs() < 1e-14,
"median={} != quantile(0.5)={}",
med, q50
);
}
#[test]
fn welford_merge_equals_sequential(
data_a in finite_vec(1, 50),
data_b in finite_vec(1, 50),
) {
let mut sequential = WelfordAccumulator::new();
for &x in data_a.iter().chain(data_b.iter()) {
sequential.update(x);
}
let mut acc_a = WelfordAccumulator::new();
for &x in &data_a { acc_a.update(x); }
let mut acc_b = WelfordAccumulator::new();
for &x in &data_b { acc_b.update(x); }
acc_a.merge(&acc_b);
let seq_mean = sequential.mean().unwrap();
let mrg_mean = acc_a.mean().unwrap();
prop_assert!(
(seq_mean - mrg_mean).abs() < 1e-10 * seq_mean.abs().max(1.0),
"merged mean should match sequential"
);
if sequential.count() >= 2 {
let seq_var = sequential.sample_variance().unwrap();
let mrg_var = acc_a.sample_variance().unwrap();
prop_assert!(
(seq_var - mrg_var).abs() < 1e-8 * seq_var.max(1.0),
"merged variance should match sequential"
);
}
}
#[test]
fn skewness_of_symmetric_is_zero(
half in proptest::collection::vec(-1e6_f64..1e6, 2..=50),
) {
prop_assume!(half.iter().all(|x| x.is_finite()));
let mut data: Vec<f64> = half.clone();
data.extend(half.iter().map(|x| -x));
if let Some(s) = skewness(&data) {
prop_assert!(
s.abs() < 1e-8,
"symmetric data should have ~0 skewness, got {}",
s
);
}
}
#[test]
fn welford_skewness_matches_batch(
data in proptest::collection::vec(-1e6_f64..1e6, 3..=100)
) {
let mut acc = WelfordAccumulator::new();
for &x in &data { acc.update(x); }
match (skewness(&data), acc.skewness()) {
(Some(batch), Some(stream)) if batch.is_finite() && stream.is_finite() => {
let tol = 1e-8 * batch.abs().max(1.0);
prop_assert!(
(batch - stream).abs() < tol,
"batch={} stream={}", batch, stream
);
}
_ => {} }
}
#[test]
fn welford_kurtosis_matches_batch(
data in proptest::collection::vec(-1e6_f64..1e6, 4..=100)
) {
let mut acc = WelfordAccumulator::new();
for &x in &data { acc.update(x); }
match (kurtosis(&data), acc.kurtosis()) {
(Some(batch), Some(stream)) if batch.is_finite() && stream.is_finite() => {
let tol = 1e-6 * batch.abs().max(1.0);
prop_assert!(
(batch - stream).abs() < tol,
"batch={} stream={}", batch, stream
);
}
_ => {} }
}
#[test]
fn covariance_self_is_variance(data in finite_vec(2, 100)) {
let cov = covariance(&data, &data).unwrap();
let var = variance(&data).unwrap();
let tol = 1e-10 * var.max(1.0);
prop_assert!(
(cov - var).abs() < tol,
"Cov(x,x)={} != Var(x)={}", cov, var
);
}
#[test]
fn covariance_symmetric(
x in finite_vec(2, 50),
y in finite_vec(2, 50),
) {
let n = x.len().min(y.len());
if n >= 2 {
let x_slice = &x[..n];
let y_slice = &y[..n];
let cov_xy = covariance(x_slice, y_slice).unwrap();
let cov_yx = covariance(y_slice, x_slice).unwrap();
prop_assert!(
(cov_xy - cov_yx).abs() < 1e-10 * cov_xy.abs().max(1.0),
"Cov(x,y)={} != Cov(y,x)={}", cov_xy, cov_yx
);
}
}
}
}