use crate::prelude::*;
use statrs::distribution::Triangular as StatrsTriangular;
#[derive(Debug, Clone)]
pub struct Triangular {
inner: StatrsTriangular,
}
impl Triangular {
pub fn new(min: f64, likely: f64, max: f64) -> Result<Self> {
if min >= max {
return Err(Error::InvalidRange { min, max });
}
if likely < min || likely > max {
return Err(Error::InvalidMode { min, likely, max });
}
Ok(Triangular {
inner: StatrsTriangular::new(min, max, likely)?,
})
}
}
impl EstimationDistribution for Triangular {}
impl Distribution<f64> for Triangular {
fn mean(&self) -> Option<f64> {
self.inner.mean()
}
fn variance(&self) -> Option<f64> {
self.inner.variance()
}
fn skewness(&self) -> Option<f64> {
self.inner.skewness()
}
fn entropy(&self) -> Option<f64> {
self.inner.entropy()
}
}
impl Median<f64> for Triangular {
fn median(&self) -> f64 {
self.inner.median()
}
}
impl Mode<f64> for Triangular {
fn mode(&self) -> f64 {
self.inner.mode().unwrap()
}
}
impl Continuous<f64, f64> for Triangular {
fn pdf(&self, x: f64) -> f64 {
self.inner.pdf(x)
}
fn ln_pdf(&self, x: f64) -> f64 {
self.inner.ln_pdf(x)
}
}
impl ContinuousCDF<f64, f64> for Triangular {
fn cdf(&self, x: f64) -> f64 {
self.inner.cdf(x)
}
fn inverse_cdf(&self, p: f64) -> f64 {
self.inner.inverse_cdf(p)
}
}
impl Min<f64> for Triangular {
fn min(&self) -> f64 {
self.inner.min()
}
}
impl Max<f64> for Triangular {
fn max(&self) -> f64 {
self.inner.max()
}
}
impl RandDistribution<f64> for Triangular {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> f64 {
self.inner.sample(rng)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use rand::distributions::Distribution as RandDistribution;
use rand::rngs::StdRng;
use rand::SeedableRng;
use statrs::statistics::{Data, Distribution, OrderStatistics};
#[test]
fn test_new() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_eq!(triangular.min(), 1.0);
assert_eq!(triangular.mode(), 2.0);
assert_eq!(triangular.max(), 3.0);
}
#[test]
fn test_min() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_eq!(triangular.min(), 1.0);
}
#[test]
fn test_max() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_eq!(triangular.max(), 3.0);
}
#[test]
fn test_mean() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_relative_eq!(triangular.mean().unwrap(), 2.0, epsilon = 1e-6);
}
#[test]
fn test_median() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_relative_eq!(triangular.median(), 2.0, epsilon = 1e-6);
}
#[test]
fn test_mode() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_eq!(triangular.mode(), 2.0);
}
#[test]
fn test_variance() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_relative_eq!(triangular.variance().unwrap(), 0.1666667, epsilon = 1e-6);
}
#[test]
fn test_skewness() {
let triangular = Triangular::new(1.0, 2.0, 3.0).unwrap();
assert_relative_eq!(triangular.skewness().unwrap(), 0.0, epsilon = 1e-6);
}
#[test]
fn test_asymmetric_distribution() {
let triangular = Triangular::new(0.0, 1.0, 5.0).unwrap();
assert_relative_eq!(triangular.mean().unwrap(), 2.0, epsilon = 1e-6);
assert_relative_eq!(triangular.median(), 1.83772234, epsilon = 1e-6);
assert_relative_eq!(triangular.variance().unwrap(), 1.1666667, epsilon = 1e-6);
assert_relative_eq!(triangular.skewness().unwrap(), 0.47613605, epsilon = 1e-6);
}
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: InvalidRange { min: 3.0, max: 1.0 }"
)]
fn test_invalid_parameters() {
Triangular::new(3.0, 2.0, 1.0).unwrap(); }
fn sample_statistics(dist: &Triangular, n: usize) -> (f64, f64, f64, f64) {
let mut rng = StdRng::seed_from_u64(42); let samples: Vec<f64> = (0..n).map(|_| dist.sample(&mut rng)).collect();
let mean = samples.iter().sum::<f64>() / n as f64;
let variance = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
let std_dev = variance.sqrt();
let mut sorted_samples = samples.clone();
sorted_samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = if n % 2 == 0 {
(sorted_samples[n / 2 - 1] + sorted_samples[n / 2]) / 2.0
} else {
sorted_samples[n / 2]
};
(mean, variance, std_dev, median)
}
#[test]
fn test_triangular_distribution_statistics() {
let min = 1.0;
let mode = 2.0;
let max = 5.0;
let dist = Triangular::new(min, mode, max).unwrap();
let n = 100_000; let epsilon = 0.01;
let (sample_mean, sample_variance, sample_std_dev, sample_median) =
sample_statistics(&dist, n);
assert_relative_eq!(
sample_mean,
dist.mean().unwrap(),
epsilon = epsilon,
max_relative = epsilon
);
assert_relative_eq!(
sample_variance,
dist.variance().unwrap(),
epsilon = epsilon,
max_relative = epsilon
);
assert_relative_eq!(
sample_std_dev,
dist.std_dev().unwrap(),
epsilon = epsilon,
max_relative = epsilon
);
assert_relative_eq!(
sample_median,
dist.median(),
epsilon = epsilon,
max_relative = epsilon
);
}
#[test]
fn test_triangular_distribution_bounds() {
let min = 1.0;
let mode = 2.0;
let max = 3.0;
let dist = Triangular::new(min, mode, max).unwrap();
let n = 100_000;
let mut rng = StdRng::seed_from_u64(42);
let samples: Vec<f64> = (0..n).map(|_| dist.sample(&mut rng)).collect();
assert!(samples.iter().all(|&x| x >= min && x <= max));
assert!(samples.iter().any(|&x| x < mode));
assert!(samples.iter().any(|&x| x > mode));
}
fn sample_skewness(data: &[f64]) -> f64 {
let n = data.len() as f64;
let mean = data.iter().sum::<f64>() / n;
let m2 = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
let m3 = data.iter().map(|x| (x - mean).powi(3)).sum::<f64>() / n;
m3 / m2.powf(1.5)
}
#[test]
fn test_distribution_statistics() {
let test_cases = vec![
(0.0, 5.0, 10.0), (0.0, 2.0, 10.0), (0.0, 9.0, 10.0), (-10.0, 0.0, 10.0), (100.0, 400.0, 1000.0), ];
for (min, mode, max) in test_cases {
let triangular = Triangular::new(min, mode, max).unwrap();
let n = 25_000; let mut rng = StdRng::seed_from_u64(42);
let samples: Vec<f64> = (0..n).map(|_| triangular.sample(&mut rng)).collect();
let mut data = Data::new(samples.clone());
let sample_mean = data.mean().unwrap();
let theoretical_mean = triangular.mean().unwrap();
assert_relative_eq!(
sample_mean,
theoretical_mean,
epsilon = 0.1,
max_relative = 0.1
);
let sample_variance = data.variance().unwrap();
let theoretical_variance = triangular.variance().unwrap();
assert_relative_eq!(
sample_variance,
theoretical_variance,
epsilon = 0.05,
max_relative = 0.05
);
let sample_std_dev = data.std_dev().unwrap();
let theoretical_std_dev = triangular.std_dev().unwrap();
assert_relative_eq!(
sample_std_dev,
theoretical_std_dev,
epsilon = 0.1,
max_relative = 0.1
);
let sample_skewness = sample_skewness(&samples);
let theoretical_skewness = triangular.skewness().unwrap();
assert_relative_eq!(
sample_skewness,
theoretical_skewness,
epsilon = 0.1,
max_relative = 0.1
);
let sample_median = data.median();
let theoretical_median = triangular.median();
assert_relative_eq!(
sample_median,
theoretical_median,
epsilon = 0.1,
max_relative = 0.1
);
assert!(triangular.mode() >= min && triangular.mode() <= max);
let sample_min = data.min();
let sample_max = data.max();
assert!(sample_min >= triangular.min());
assert!(sample_max <= triangular.max());
let percentiles = [0.1, 0.25, 0.5, 0.75, 0.9];
for p in percentiles.iter() {
let sample_percentile = data.percentile((100.0 * p) as usize);
let theoretical_percentile = triangular.inverse_cdf(*p);
assert_relative_eq!(
sample_percentile,
theoretical_percentile,
epsilon = 0.05,
max_relative = 0.05
);
let sample_cdf =
samples.iter().filter(|&x| x <= &sample_percentile).count() as f64 / n as f64;
let theoretical_cdf = triangular.cdf(sample_percentile);
assert_relative_eq!(
sample_cdf,
theoretical_cdf,
epsilon = 0.01,
max_relative = 0.01
);
}
println!("Test passed for Triangular({}, {}, {})", min, mode, max);
}
}
}