Skip to main content

scirs2_stats/distributions/
logistic.rs

1//! Logistic distribution functions
2//!
3//! This module provides functionality for the Logistic distribution.
4
5use crate::error::{StatsError, StatsResult};
6use crate::sampling::SampleableDistribution;
7use scirs2_core::numeric::{Float, NumCast};
8use scirs2_core::random::prelude::*;
9use scirs2_core::random::{Distribution, Uniform as RandUniform};
10
11/// Logistic distribution structure
12///
13/// The Logistic distribution is a continuous probability distribution that
14/// has applications in growth models, neural networks, and logistic regression.
15/// It resembles the normal distribution but has heavier tails.
16pub struct Logistic<F: Float> {
17    /// Location parameter (mean, median, and mode of the distribution)
18    pub loc: F,
19    /// Scale parameter (diversity) > 0
20    pub scale: F,
21    /// Random number generator for uniform distribution
22    rand_distr: RandUniform<f64>,
23}
24
25impl<F: Float + NumCast + std::fmt::Display> Logistic<F> {
26    /// Create a new Logistic distribution with given parameters
27    ///
28    /// # Arguments
29    ///
30    /// * `loc` - Location parameter (mean, median, and mode of the distribution)
31    /// * `scale` - Scale parameter (diversity) > 0
32    ///
33    /// # Returns
34    ///
35    /// * A new Logistic distribution instance
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// use scirs2_stats::distributions::logistic::Logistic;
41    ///
42    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
43    /// ```
44    pub fn new(loc: F, scale: F) -> StatsResult<Self> {
45        // Validate parameters
46        if scale <= F::zero() {
47            return Err(StatsError::DomainError(
48                "Scale parameter must be positive".to_string(),
49            ));
50        }
51
52        // Create RNG for uniform distribution in [0, 1)
53        let rand_distr = match RandUniform::new(0.0, 1.0) {
54            Ok(distr) => distr,
55            Err(_) => {
56                return Err(StatsError::ComputationError(
57                    "Failed to create uniform distribution for sampling".to_string(),
58                ))
59            }
60        };
61
62        Ok(Logistic {
63            loc,
64            scale,
65            rand_distr,
66        })
67    }
68
69    /// Calculate the probability density function (PDF) at a given point
70    ///
71    /// # Arguments
72    ///
73    /// * `x` - The point at which to evaluate the PDF
74    ///
75    /// # Returns
76    ///
77    /// * The value of the PDF at the given point
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use scirs2_stats::distributions::logistic::Logistic;
83    ///
84    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
85    /// let pdf_at_zero = logistic.pdf(0.0);
86    /// assert!((pdf_at_zero - 0.25).abs() < 1e-7);
87    /// ```
88    pub fn pdf(&self, x: F) -> F {
89        let z = (x - self.loc) / self.scale;
90        let exp_neg_z = (-z).exp();
91
92        // PDF = exp(-z) / (scale * (1 + exp(-z))^2)
93        exp_neg_z / (self.scale * (F::one() + exp_neg_z).powi(2))
94    }
95
96    /// Calculate the cumulative distribution function (CDF) at a given point
97    ///
98    /// # Arguments
99    ///
100    /// * `x` - The point at which to evaluate the CDF
101    ///
102    /// # Returns
103    ///
104    /// * The value of the CDF at the given point
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use scirs2_stats::distributions::logistic::Logistic;
110    ///
111    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
112    /// let cdf_at_zero = logistic.cdf(0.0);
113    /// assert!((cdf_at_zero - 0.5).abs() < 1e-7);
114    /// ```
115    pub fn cdf(&self, x: F) -> F {
116        let z = (x - self.loc) / self.scale;
117
118        // CDF = 1 / (1 + exp(-z))
119        F::one() / (F::one() + (-z).exp())
120    }
121
122    /// Inverse of the cumulative distribution function (quantile function)
123    ///
124    /// # Arguments
125    ///
126    /// * `p` - Probability value (between 0 and 1)
127    ///
128    /// # Returns
129    ///
130    /// * The value x such that CDF(x) = p
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use scirs2_stats::distributions::logistic::Logistic;
136    ///
137    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
138    /// let x = logistic.ppf(0.75).expect("Operation failed");
139    /// assert!((x - 1.0986123).abs() < 1e-6);
140    /// ```
141    pub fn ppf(&self, p: F) -> StatsResult<F> {
142        if p < F::zero() || p > F::one() {
143            return Err(StatsError::DomainError(
144                "Probability must be between 0 and 1".to_string(),
145            ));
146        }
147
148        // Special cases
149        if p == F::zero() {
150            return Ok(F::neg_infinity());
151        }
152        if p == F::one() {
153            return Ok(F::infinity());
154        }
155
156        // Quantile = loc + scale * ln(p / (1 - p))
157        let quantile = self.loc + self.scale * (p / (F::one() - p)).ln();
158        Ok(quantile)
159    }
160
161    /// Generate random samples from the distribution
162    ///
163    /// # Arguments
164    ///
165    /// * `size` - Number of samples to generate
166    ///
167    /// # Returns
168    ///
169    /// * Vector of random samples
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use scirs2_stats::distributions::logistic::Logistic;
175    ///
176    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
177    /// let samples = logistic.rvs(10).expect("Operation failed");
178    /// assert_eq!(samples.len(), 10);
179    /// ```
180    pub fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
181        let mut rng = thread_rng();
182        let mut samples = Vec::with_capacity(size);
183
184        for _ in 0..size {
185            // Generate uniform random number in (0, 1)
186            // Avoid exactly 0 or 1 to prevent infinite values
187            let mut u = self.rand_distr.sample(&mut rng);
188            while u <= 0.0 || u >= 1.0 {
189                u = self.rand_distr.sample(&mut rng);
190            }
191
192            let u_f = F::from(u).expect("Failed to convert to float");
193
194            // Apply inverse CDF transform
195            let sample = match self.ppf(u_f) {
196                Ok(s) => s,
197                Err(_) => continue, // Skip invalid samples
198            };
199
200            samples.push(sample);
201        }
202
203        // Ensure we have exactly 'size' samples
204        while samples.len() < size {
205            let mut u = self.rand_distr.sample(&mut rng);
206            while u <= 0.0 || u >= 1.0 {
207                u = self.rand_distr.sample(&mut rng);
208            }
209
210            let u_f = F::from(u).expect("Failed to convert to float");
211
212            let sample = match self.ppf(u_f) {
213                Ok(s) => s,
214                Err(_) => continue,
215            };
216
217            samples.push(sample);
218        }
219
220        Ok(samples)
221    }
222
223    /// Calculate the mean of the distribution
224    ///
225    /// # Returns
226    ///
227    /// * The mean of the distribution
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use scirs2_stats::distributions::logistic::Logistic;
233    ///
234    /// let logistic = Logistic::new(2.0f64, 1.0).expect("Operation failed");
235    /// let mean = logistic.mean();
236    /// assert_eq!(mean, 2.0);
237    /// ```
238    pub fn mean(&self) -> F {
239        // Mean = loc
240        self.loc
241    }
242
243    /// Calculate the variance of the distribution
244    ///
245    /// # Returns
246    ///
247    /// * The variance of the distribution
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use scirs2_stats::distributions::logistic::Logistic;
253    ///
254    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
255    /// let variance = logistic.var();
256    /// assert!((variance - 3.28986).abs() < 1e-5);
257    /// ```
258    pub fn var(&self) -> F {
259        // Variance = (π^2/3) * scale^2
260        let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
261        let pi_squared = pi * pi;
262        let one_third = F::from(1.0 / 3.0).expect("Failed to convert to float");
263
264        pi_squared * one_third * self.scale * self.scale
265    }
266
267    /// Calculate the standard deviation of the distribution
268    ///
269    /// # Returns
270    ///
271    /// * The standard deviation of the distribution
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use scirs2_stats::distributions::logistic::Logistic;
277    ///
278    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
279    /// let std_dev = logistic.std();
280    /// assert!((std_dev - 1.81379).abs() < 1e-5);
281    /// ```
282    pub fn std(&self) -> F {
283        // Std = sqrt(var)
284        self.var().sqrt()
285    }
286
287    /// Calculate the median of the distribution
288    ///
289    /// # Returns
290    ///
291    /// * The median of the distribution
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// use scirs2_stats::distributions::logistic::Logistic;
297    ///
298    /// let logistic = Logistic::new(2.0f64, 1.0).expect("Operation failed");
299    /// let median = logistic.median();
300    /// assert_eq!(median, 2.0);
301    /// ```
302    pub fn median(&self) -> F {
303        // Median = loc
304        self.loc
305    }
306
307    /// Calculate the mode of the distribution
308    ///
309    /// # Returns
310    ///
311    /// * The mode of the distribution
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use scirs2_stats::distributions::logistic::Logistic;
317    ///
318    /// let logistic = Logistic::new(2.0f64, 1.0).expect("Operation failed");
319    /// let mode = logistic.mode();
320    /// assert_eq!(mode, 2.0);
321    /// ```
322    pub fn mode(&self) -> F {
323        // Mode = loc
324        self.loc
325    }
326
327    /// Calculate the skewness of the distribution
328    ///
329    /// # Returns
330    ///
331    /// * The skewness (which is always 0 for the logistic distribution)
332    ///
333    /// # Examples
334    ///
335    /// ```
336    /// use scirs2_stats::distributions::logistic::Logistic;
337    ///
338    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
339    /// let skewness = logistic.skewness();
340    /// assert_eq!(skewness, 0.0);
341    /// ```
342    pub fn skewness(&self) -> F {
343        // Skewness = 0 (symmetric distribution)
344        F::zero()
345    }
346
347    /// Calculate the kurtosis of the distribution
348    ///
349    /// # Returns
350    ///
351    /// * The excess kurtosis of the distribution
352    ///
353    /// # Examples
354    ///
355    /// ```
356    /// use scirs2_stats::distributions::logistic::Logistic;
357    ///
358    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
359    /// let kurtosis = logistic.kurtosis();
360    /// assert!((kurtosis - 1.2).abs() < 1e-7);
361    /// ```
362    pub fn kurtosis(&self) -> F {
363        // Excess kurtosis = 1.2
364        F::from(1.2).expect("Failed to convert constant to float")
365    }
366
367    /// Calculate the entropy of the distribution
368    ///
369    /// # Returns
370    ///
371    /// * The entropy value
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use scirs2_stats::distributions::logistic::Logistic;
377    ///
378    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
379    /// let entropy = logistic.entropy();
380    /// assert!((entropy - 2.0).abs() < 1e-7);
381    /// ```
382    pub fn entropy(&self) -> F {
383        // Entropy = 2 + ln(scale)
384        let two = F::from(2.0).expect("Failed to convert constant to float");
385        two + self.scale.ln()
386    }
387
388    /// Calculate the interquartile range (IQR) of the distribution
389    ///
390    /// # Returns
391    ///
392    /// * The interquartile range
393    ///
394    /// # Examples
395    ///
396    /// ```
397    /// use scirs2_stats::distributions::logistic::Logistic;
398    ///
399    /// let logistic = Logistic::new(0.0f64, 1.0).expect("Operation failed");
400    /// let iqr = logistic.iqr();
401    /// assert!((iqr - 2.1972245) < 1e-6);
402    /// ```
403    pub fn iqr(&self) -> F {
404        // IQR = scale * ln(3) ≈ scale * 2.1972245...
405        let three = F::from(3.0).expect("Failed to convert constant to float");
406        self.scale * three.ln()
407    }
408}
409
410/// Create a Logistic distribution with the given parameters.
411///
412/// This is a convenience function to create a Logistic distribution with
413/// the given location and scale parameters.
414///
415/// # Arguments
416///
417/// * `loc` - Location parameter (mean, median, and mode of the distribution)
418/// * `scale` - Scale parameter (diversity) > 0
419///
420/// # Returns
421///
422/// * A Logistic distribution object
423///
424/// # Examples
425///
426/// ```
427/// use scirs2_stats::distributions::logistic;
428///
429/// let l = logistic::logistic(0.0f64, 1.0).expect("Operation failed");
430/// let pdf_at_zero = l.pdf(0.0);
431/// assert!((pdf_at_zero - 0.25).abs() < 1e-7);
432/// ```
433#[allow(dead_code)]
434pub fn logistic<F>(loc: F, scale: F) -> StatsResult<Logistic<F>>
435where
436    F: Float + NumCast + std::fmt::Display,
437{
438    Logistic::new(loc, scale)
439}
440
441/// Implementation of SampleableDistribution for Logistic
442impl<F: Float + NumCast + std::fmt::Display> SampleableDistribution<F> for Logistic<F> {
443    fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
444        self.rvs(size)
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use approx::assert_relative_eq;
452
453    #[test]
454    fn test_logistic_creation() {
455        // Standard Logistic (loc=0, scale=1)
456        let logistic = Logistic::new(0.0, 1.0).expect("Operation failed");
457        assert_eq!(logistic.loc, 0.0);
458        assert_eq!(logistic.scale, 1.0);
459
460        // Custom Logistic
461        let custom = Logistic::new(-2.0, 0.5).expect("Operation failed");
462        assert_eq!(custom.loc, -2.0);
463        assert_eq!(custom.scale, 0.5);
464
465        // Error case: non-positive scale
466        assert!(Logistic::<f64>::new(0.0, 0.0).is_err());
467        assert!(Logistic::<f64>::new(0.0, -1.0).is_err());
468    }
469
470    #[test]
471    fn test_logistic_pdf() {
472        // Standard Logistic (loc=0, scale=1)
473        let logistic = Logistic::new(0.0, 1.0).expect("Operation failed");
474
475        // PDF at x = 0 should be 1/4 = 0.25
476        let pdf_at_zero = logistic.pdf(0.0);
477        assert_relative_eq!(pdf_at_zero, 0.25, epsilon = 1e-7);
478
479        // PDF at x = 1
480        let pdf_at_one = logistic.pdf(1.0);
481        assert_relative_eq!(pdf_at_one, 0.196612, epsilon = 1e-6);
482
483        // PDF at x = -1 should same as x = 1 due to symmetry
484        let pdf_at_neg_one = logistic.pdf(-1.0);
485        assert_relative_eq!(pdf_at_neg_one, pdf_at_one, epsilon = 1e-10);
486
487        // Custom Logistic with loc=-2, scale=0.5
488        let custom = Logistic::new(-2.0, 0.5).expect("Operation failed");
489
490        // PDF at x = -2 should be 1/(4*0.5) = 0.5
491        let pdf_at_loc = custom.pdf(-2.0);
492        assert_relative_eq!(pdf_at_loc, 0.5, epsilon = 1e-7);
493    }
494
495    #[test]
496    fn test_logistic_cdf() {
497        // Standard Logistic (loc=0, scale=1)
498        let logistic = Logistic::new(0.0, 1.0).expect("Operation failed");
499
500        // CDF at x = 0 should be 0.5
501        let cdf_at_zero = logistic.cdf(0.0);
502        assert_relative_eq!(cdf_at_zero, 0.5, epsilon = 1e-10);
503
504        // CDF at x = 1 should be 1/(1+exp(-1)) ≈ 0.7310586
505        let cdf_at_one = logistic.cdf(1.0);
506        assert_relative_eq!(cdf_at_one, 0.7310586, epsilon = 1e-7);
507
508        // CDF at x = -1 should be 1-CDF(1) ≈ 0.2689414 due to symmetry
509        let cdf_at_neg_one = logistic.cdf(-1.0);
510        assert_relative_eq!(cdf_at_neg_one, 0.2689414, epsilon = 1e-7);
511
512        // Custom Logistic with loc=-2, scale=0.5
513        let custom = Logistic::new(-2.0, 0.5).expect("Operation failed");
514
515        // CDF at x = -2 should be 0.5
516        let cdf_at_loc = custom.cdf(-2.0);
517        assert_relative_eq!(cdf_at_loc, 0.5, epsilon = 1e-10);
518
519        // CDF at x = -1.5 should be 1/(1+exp(-(-1.5-(-2))/0.5)) = 1/(1+exp(-1)) ≈ 0.7310586
520        let cdf_at_custom = custom.cdf(-1.5);
521        assert_relative_eq!(cdf_at_custom, 0.7310586, epsilon = 1e-7);
522    }
523
524    #[test]
525    fn test_logistic_ppf() {
526        // Standard Logistic (loc=0, scale=1)
527        let logistic = Logistic::new(0.0, 1.0).expect("Operation failed");
528
529        // PPF at p = 0.5 should be 0
530        let ppf_at_half = logistic.ppf(0.5).expect("Operation failed");
531        assert_relative_eq!(ppf_at_half, 0.0, epsilon = 1e-10);
532
533        // PPF at p = 0.75 should be ln(3) ≈ 1.0986123
534        let ppf_at_75 = logistic.ppf(0.75).expect("Operation failed");
535        assert_relative_eq!(ppf_at_75, 1.0986123, epsilon = 1e-6);
536
537        // PPF at p = 0.25 should be -ln(3) ≈ -1.0986123
538        let ppf_at_25 = logistic.ppf(0.25).expect("Operation failed");
539        assert_relative_eq!(ppf_at_25, -1.0986123, epsilon = 1e-6);
540
541        // Custom Logistic with loc=-2, scale=0.5
542        let custom = Logistic::new(-2.0, 0.5).expect("Operation failed");
543
544        // PPF at p = 0.5 should be -2.0
545        let ppf_at_half_custom = custom.ppf(0.5).expect("Operation failed");
546        assert_relative_eq!(ppf_at_half_custom, -2.0, epsilon = 1e-10);
547
548        // PPF at p = 0.75 should be -2.0 + 0.5*ln(3) ≈ -2.0 + 0.5493062 ≈ -1.4506938
549        let ppf_at_75_custom = custom.ppf(0.75).expect("Operation failed");
550        assert_relative_eq!(ppf_at_75_custom, -1.4506938, epsilon = 1e-6);
551
552        // Error cases
553        assert!(logistic.ppf(-0.1).is_err());
554        assert!(logistic.ppf(1.1).is_err());
555    }
556
557    #[test]
558    fn test_logistic_properties() {
559        // Standard Logistic (loc=0, scale=1)
560        let logistic = Logistic::new(0.0, 1.0).expect("Operation failed");
561
562        // Mean = loc = 0
563        let mean = logistic.mean();
564        assert_eq!(mean, 0.0);
565
566        // Variance = (π^2/3) * scale^2 ≈ 3.28986...
567        let variance = logistic.var();
568        assert_relative_eq!(variance, 3.28986, epsilon = 1e-5);
569
570        // Std = sqrt(variance) ≈ 1.81379...
571        let std_dev = logistic.std();
572        assert_relative_eq!(std_dev, 1.81379, epsilon = 1e-5);
573
574        // Median = loc = 0
575        let median = logistic.median();
576        assert_eq!(median, 0.0);
577
578        // Mode = loc = 0
579        let mode = logistic.mode();
580        assert_eq!(mode, 0.0);
581
582        // Skewness = 0 (symmetric)
583        let skewness = logistic.skewness();
584        assert_eq!(skewness, 0.0);
585
586        // Kurtosis = 1.2
587        let kurtosis = logistic.kurtosis();
588        assert_eq!(kurtosis, 1.2);
589
590        // Entropy = 2 + ln(scale) = 2 + ln(1) = 2
591        let entropy = logistic.entropy();
592        assert_eq!(entropy, 2.0);
593
594        // IQR = scale * ln(3) ≈ 1 * 1.0986... ≈ 1.0986...
595        let iqr = logistic.iqr();
596        assert_relative_eq!(iqr, 1.0986123, epsilon = 1e-6);
597
598        // Custom Logistic with loc=-2, scale=0.5
599        let custom = Logistic::new(-2.0, 0.5).expect("Operation failed");
600
601        // Mean = loc = -2
602        let mean_custom = custom.mean();
603        assert_eq!(mean_custom, -2.0);
604
605        // Variance = (π^2/3) * scale^2 = (π^2/3) * 0.5^2 ≈ 0.82247...
606        let variance_custom = custom.var();
607        assert_relative_eq!(variance_custom, 0.82247, epsilon = 1e-5);
608
609        // Entropy = 2 + ln(scale) = 2 + ln(0.5) ≈ 2 - 0.693147... ≈ 1.30685...
610        let entropy_custom = custom.entropy();
611        assert_relative_eq!(entropy_custom, 1.30685, epsilon = 1e-5);
612    }
613
614    #[test]
615    fn test_logistic_rvs() {
616        let logistic = Logistic::new(0.0, 1.0).expect("Operation failed");
617
618        // Generate samples (larger sample size for better statistical stability)
619        let samples = logistic.rvs(1000).expect("Operation failed");
620
621        // Check the number of samples
622        assert_eq!(samples.len(), 1000);
623
624        // Calculate sample mean and check it's reasonably close to loc = 0
625        let sum: f64 = samples.iter().sum();
626        let mean = sum / samples.len() as f64;
627
628        // For logistic(0,1): variance = π²/3 ≈ 3.29, so std ≈ 1.81
629        // With n=1000, standard error ≈ 0.057
630        // Using 5 standard errors gives tolerance ≈ 0.29, use 0.5 for safety
631        assert!(mean.abs() < 0.5);
632    }
633
634    #[test]
635    fn test_logistic_inverse_cdf() {
636        // Test that cdf(ppf(p)) == p and ppf(cdf(x)) == x
637        let logistic = Logistic::new(0.0, 1.0).expect("Operation failed");
638
639        // Test various probability values
640        let probabilities = [0.1, 0.25, 0.5, 0.75, 0.9];
641        for &p in &probabilities {
642            let x = logistic.ppf(p).expect("Operation failed");
643            let p_back = logistic.cdf(x);
644            assert_relative_eq!(p_back, p, epsilon = 1e-7);
645        }
646
647        // Test various x values
648        let x_values = [-3.0, -1.0, 0.0, 1.0, 3.0];
649        for &x in &x_values {
650            let p = logistic.cdf(x);
651            let x_back = logistic.ppf(p).expect("Operation failed");
652            assert_relative_eq!(x_back, x, epsilon = 1e-7);
653        }
654    }
655}