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}