diff --git a/.travis.yml b/.travis.yml index 666dc16b..c71c2f1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ addons: - libssl-dev cache: cargo rust: - - 1.30.0 + - 1.31.0 - stable - beta - nightly diff --git a/Cargo.toml b/Cargo.toml index d3a993c5..57795ce8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,4 @@ itertools = { version = "0.7.0", default-features = false } [dev-dependencies] quickcheck = "0.7" ndarray-rand = "0.9" +approx = "0.3" diff --git a/src/lib.rs b/src/lib.rs index 4a974d2d..fad9cef8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,15 +37,20 @@ extern crate ndarray_rand; #[cfg(test)] #[macro_use(quickcheck)] extern crate quickcheck; +#[cfg(test)] +#[macro_use(abs_diff_eq)] +extern crate approx; pub use maybe_nan::{MaybeNan, MaybeNanExt}; pub use quantile::{interpolate, QuantileExt, Quantile1dExt}; pub use sort::Sort1dExt; pub use correlation::CorrelationExt; pub use histogram::HistogramExt; +pub use summary_statistics::SummaryStatisticsExt; mod maybe_nan; mod quantile; mod sort; mod correlation; -pub mod histogram; \ No newline at end of file +mod summary_statistics; +pub mod histogram; diff --git a/src/summary_statistics/means.rs b/src/summary_statistics/means.rs new file mode 100644 index 00000000..dd0d5337 --- /dev/null +++ b/src/summary_statistics/means.rs @@ -0,0 +1,97 @@ +use ndarray::{Data, Dimension, ArrayBase}; +use num_traits::{FromPrimitive, Float, Zero}; +use std::ops::{Add, Div}; +use super::SummaryStatisticsExt; + + +impl SummaryStatisticsExt for ArrayBase +where + S: Data, + D: Dimension, +{ + fn mean(&self) -> Option + where + A: Clone + FromPrimitive + Add + Div + Zero + { + let n_elements = self.len(); + if n_elements == 0 { + None + } else { + let n_elements = A::from_usize(n_elements) + .expect("Converting number of elements to `A` must not fail."); + Some(self.sum() / n_elements) + } + } + + fn harmonic_mean(&self) -> Option + where + A: Float + FromPrimitive, + { + self.map(|x| x.recip()).mean().map(|x| x.recip()) + } + + fn geometric_mean(&self) -> Option + where + A: Float + FromPrimitive, + { + self.map(|x| x.ln()).mean().map(|x| x.exp()) + } +} + +#[cfg(test)] +mod tests { + use super::SummaryStatisticsExt; + use std::f64; + use noisy_float::types::N64; + use ndarray::Array1; + + #[test] + fn test_means_with_nan_values() { + let a = array![f64::NAN, 1.]; + assert!(a.mean().unwrap().is_nan()); + assert!(a.harmonic_mean().unwrap().is_nan()); + assert!(a.geometric_mean().unwrap().is_nan()); + } + + #[test] + fn test_means_with_empty_array_of_floats() { + let a: Array1 = array![]; + assert!(a.mean().is_none()); + assert!(a.harmonic_mean().is_none()); + assert!(a.geometric_mean().is_none()); + } + + #[test] + fn test_means_with_empty_array_of_noisy_floats() { + let a: Array1 = array![]; + assert!(a.mean().is_none()); + assert!(a.harmonic_mean().is_none()); + assert!(a.geometric_mean().is_none()); + } + + #[test] + fn test_means_with_array_of_floats() { + let a: Array1 = array![ + 0.99889651, 0.0150731 , 0.28492482, 0.83819218, 0.48413156, + 0.80710412, 0.41762936, 0.22879429, 0.43997224, 0.23831807, + 0.02416466, 0.6269962 , 0.47420614, 0.56275487, 0.78995021, + 0.16060581, 0.64635041, 0.34876609, 0.78543249, 0.19938356, + 0.34429457, 0.88072369, 0.17638164, 0.60819363, 0.250392 , + 0.69912532, 0.78855523, 0.79140914, 0.85084218, 0.31839879, + 0.63381769, 0.22421048, 0.70760302, 0.99216018, 0.80199153, + 0.19239188, 0.61356023, 0.31505352, 0.06120481, 0.66417377, + 0.63608897, 0.84959691, 0.43599069, 0.77867775, 0.88267754, + 0.83003623, 0.67016118, 0.67547638, 0.65220036, 0.68043427 + ]; + // Computed using NumPy + let expected_mean = 0.5475494059146699; + // Computed using SciPy + let expected_harmonic_mean = 0.21790094950226022; + // Computed using SciPy + let expected_geometric_mean = 0.4345897639796527; + + abs_diff_eq!(a.mean().unwrap(), expected_mean, epsilon = f64::EPSILON); + abs_diff_eq!(a.harmonic_mean().unwrap(), expected_harmonic_mean, epsilon = f64::EPSILON); + abs_diff_eq!(a.geometric_mean().unwrap(), expected_geometric_mean, epsilon = f64::EPSILON); + } +} diff --git a/src/summary_statistics/mod.rs b/src/summary_statistics/mod.rs new file mode 100644 index 00000000..ae05e709 --- /dev/null +++ b/src/summary_statistics/mod.rs @@ -0,0 +1,66 @@ +//! Summary statistics (e.g. mean, variance, etc.). +use ndarray::{Data, Dimension}; +use num_traits::{FromPrimitive, Float, Zero}; +use std::ops::{Add, Div}; + +/// Extension trait for `ArrayBase` providing methods +/// to compute several summary statistics (e.g. mean, variance, etc.). +pub trait SummaryStatisticsExt + where + S: Data, + D: Dimension, +{ + /// Returns the [`arithmetic mean`] x̅ of all elements in the array: + /// + /// ```text + /// 1 n + /// x̅ = ― ∑ xᵢ + /// n i=1 + /// ``` + /// + /// If the array is empty, `None` is returned. + /// + /// **Panics** if `A::from_usize()` fails to convert the number of elements in the array. + /// + /// [`arithmetic mean`]: https://en.wikipedia.org/wiki/Arithmetic_mean + fn mean(&self) -> Option + where + A: Clone + FromPrimitive + Add + Div + Zero; + + /// Returns the [`harmonic mean`] `HM(X)` of all elements in the array: + /// + /// ```text + /// ⎛ n ⎞⁻¹ + /// HM(X) = n ⎜ ∑ xᵢ⁻¹⎟ + /// ⎝i=1 ⎠ + /// ``` + /// + /// If the array is empty, `None` is returned. + /// + /// **Panics** if `A::from_usize()` fails to convert the number of elements in the array. + /// + /// [`harmonic mean`]: https://en.wikipedia.org/wiki/Harmonic_mean + fn harmonic_mean(&self) -> Option + where + A: Float + FromPrimitive; + + /// Returns the [`geometric mean`] `GM(X)` of all elements in the array: + /// + /// ```text + /// ⎛ n ⎞¹⁄ₙ + /// GM(X) = ⎜ ∏ xᵢ⎟ + /// ⎝i=1 ⎠ + /// ``` + /// + /// If the array is empty, `None` is returned. + /// + /// **Panics** if `A::from_usize()` fails to convert the number of elements in the array. + /// + /// [`geometric mean`]: https://en.wikipedia.org/wiki/Geometric_mean + fn geometric_mean(&self) -> Option + where + A: Float + FromPrimitive; + +} + +mod means;