Skip to main content

scirs2_stats/distributions/
lognormal.rs

1//! Lognormal distribution functions
2//!
3//! This module provides functionality for the Lognormal distribution.
4
5use crate::distributions::normal::Normal;
6use crate::error::{StatsError, StatsResult};
7use crate::sampling::SampleableDistribution;
8use scirs2_core::numeric::{Float, NumCast};
9
10/// Lognormal distribution structure
11///
12/// The lognormal distribution is the distribution of a random variable
13/// whose logarithm follows a normal distribution.
14pub struct Lognormal<F: Float> {
15    /// Mean of the underlying normal distribution (shape parameter)
16    pub mu: F,
17    /// Standard deviation of the underlying normal distribution (scale parameter)
18    pub sigma: F,
19    /// Location parameter
20    pub loc: F,
21    /// Underlying normal distribution
22    norm: Normal<F>,
23}
24
25impl<F: Float + NumCast + std::fmt::Display> Lognormal<F> {
26    /// Create a new lognormal distribution with given parameters
27    ///
28    /// # Arguments
29    ///
30    /// * `mu` - Mean of the underlying normal distribution
31    /// * `sigma` - Standard deviation of the underlying normal distribution
32    /// * `loc` - Location parameter (default: 0)
33    ///
34    /// # Returns
35    ///
36    /// * A new Lognormal distribution instance
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use scirs2_stats::distributions::lognormal::Lognormal;
42    ///
43    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
44    /// ```
45    pub fn new(mu: F, sigma: F, loc: F) -> StatsResult<Self> {
46        if sigma <= F::zero() {
47            return Err(StatsError::DomainError(
48                "Standard deviation must be positive".to_string(),
49            ));
50        }
51
52        // Create underlying normal distribution
53        match Normal::new(mu, sigma) {
54            Ok(norm) => Ok(Lognormal {
55                mu,
56                sigma,
57                loc,
58                norm,
59            }),
60            Err(e) => Err(e),
61        }
62    }
63
64    /// Calculate the probability density function (PDF) at a given point
65    ///
66    /// # Arguments
67    ///
68    /// * `x` - The point at which to evaluate the PDF
69    ///
70    /// # Returns
71    ///
72    /// * The value of the PDF at the given point
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use scirs2_stats::distributions::lognormal::Lognormal;
78    ///
79    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
80    /// let pdf_at_one = lognorm.pdf(1.0);
81    /// assert!((pdf_at_one - 0.3989423).abs() < 1e-7);
82    /// ```
83    pub fn pdf(&self, x: F) -> F {
84        // For a value <= location parameter, PDF is 0
85        if x <= self.loc {
86            return F::zero();
87        }
88
89        // Shift x by location parameter
90        let x_shifted = x - self.loc;
91
92        // For lognormal PDF:
93        // f(x) = 1/(x*sigma*sqrt(2*pi)) * exp(-(ln(x) - mu)^2/(2*sigma^2))
94        let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
95        let two = F::from(2.0).expect("Failed to convert constant to float");
96
97        let ln_x = x_shifted.ln();
98        let z = (ln_x - self.mu) / self.sigma;
99        let exponent = -z * z / two;
100
101        F::from(1.0).expect("Failed to convert constant to float")
102            / (x_shifted * self.sigma * (two * pi).sqrt())
103            * exponent.exp()
104    }
105
106    /// Calculate the cumulative distribution function (CDF) at a given point
107    ///
108    /// # Arguments
109    ///
110    /// * `x` - The point at which to evaluate the CDF
111    ///
112    /// # Returns
113    ///
114    /// * The value of the CDF at the given point
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use scirs2_stats::distributions::lognormal::Lognormal;
120    ///
121    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
122    /// let cdf_at_one = lognorm.cdf(1.0);
123    /// assert!((cdf_at_one - 0.5).abs() < 1e-10);
124    /// ```
125    pub fn cdf(&self, x: F) -> F {
126        // For a value <= location parameter, CDF is 0
127        if x <= self.loc {
128            return F::zero();
129        }
130
131        // Shift x by location parameter
132        let x_shifted = x - self.loc;
133
134        // The CDF of lognormal at x is the same as the CDF of normal at ln(x)
135        let ln_x = x_shifted.ln();
136        self.norm.cdf(ln_x)
137    }
138
139    /// Inverse of the cumulative distribution function (quantile function)
140    ///
141    /// # Arguments
142    ///
143    /// * `p` - Probability value (between 0 and 1)
144    ///
145    /// # Returns
146    ///
147    /// * The value x such that CDF(x) = p
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use scirs2_stats::distributions::lognormal::Lognormal;
153    ///
154    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
155    /// let x = lognorm.ppf(0.5).expect("Operation failed");
156    /// assert!((x - 1.0) < 1e-7);
157    /// ```
158    pub fn ppf(&self, p: F) -> StatsResult<F> {
159        if p < F::zero() || p > F::one() {
160            return Err(StatsError::DomainError(
161                "Probability must be between 0 and 1".to_string(),
162            ));
163        }
164
165        // Special cases
166        if p == F::zero() {
167            return Ok(self.loc);
168        }
169        if p == F::one() {
170            return Ok(F::infinity());
171        }
172
173        // The quantile of lognormal at p is exp(quantile of normal at p)
174        match self.norm.ppf(p) {
175            Ok(normal_quantile) => Ok(normal_quantile.exp() + self.loc),
176            Err(e) => Err(e),
177        }
178    }
179
180    /// Generate random samples from the distribution
181    ///
182    /// # Arguments
183    ///
184    /// * `size` - Number of samples to generate
185    ///
186    /// # Returns
187    ///
188    /// * Vector of random samples
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// use scirs2_stats::distributions::lognormal::Lognormal;
194    ///
195    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
196    /// let samples = lognorm.rvs(1000).expect("Operation failed");
197    /// assert_eq!(samples.len(), 1000);
198    /// ```
199    pub fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
200        // Generate samples from normal distribution
201        let normal_samples = self.norm.rvs(size)?;
202
203        // Transform the samples to lognormal by taking the exponent and adding location
204        let lognormal_samples: Vec<F> = normal_samples
205            .into_iter()
206            .map(|x| x.exp() + self.loc)
207            .collect();
208
209        Ok(lognormal_samples)
210    }
211
212    /// Calculate the mean of the distribution
213    ///
214    /// # Returns
215    ///
216    /// * The mean of the distribution
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use scirs2_stats::distributions::lognormal::Lognormal;
222    ///
223    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
224    /// let mean = lognorm.mean();
225    /// assert!((mean - 1.6487212707).abs() < 1e-7);
226    /// ```
227    pub fn mean(&self) -> F {
228        let half = F::from(0.5).expect("Failed to convert constant to float");
229        let variance = self.sigma * self.sigma;
230
231        (self.mu + variance * half).exp() + self.loc
232    }
233
234    /// Calculate the variance of the distribution
235    ///
236    /// # Returns
237    ///
238    /// * The variance of the distribution
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use scirs2_stats::distributions::lognormal::Lognormal;
244    ///
245    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
246    /// let var = lognorm.var();
247    /// assert!((var - 4.670774270471604) < 1e-7);
248    /// ```
249    pub fn var(&self) -> F {
250        let one = F::one();
251        let two = F::from(2.0).expect("Failed to convert constant to float");
252        let variance = self.sigma * self.sigma;
253
254        ((two * self.mu + variance).exp()) * (variance.exp() - one)
255    }
256
257    /// Calculate the median of the distribution
258    ///
259    /// # Returns
260    ///
261    /// * The median of the distribution
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// use scirs2_stats::distributions::lognormal::Lognormal;
267    ///
268    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
269    /// let median = lognorm.median();
270    /// assert!((median - 1.0).abs() < 1e-7);
271    /// ```
272    pub fn median(&self) -> F {
273        self.mu.exp() + self.loc
274    }
275
276    /// Calculate the mode of the distribution
277    ///
278    /// # Returns
279    ///
280    /// * The mode of the distribution
281    ///
282    /// # Examples
283    ///
284    /// ```
285    /// use scirs2_stats::distributions::lognormal::Lognormal;
286    ///
287    /// let lognorm = Lognormal::new(0.0f64, 1.0, 0.0).expect("Operation failed");
288    /// let mode = lognorm.mode();
289    /// assert!((mode - 0.36787944).abs() < 1e-7);
290    /// ```
291    pub fn mode(&self) -> F {
292        (self.mu - self.sigma * self.sigma).exp() + self.loc
293    }
294}
295
296/// Create a lognormal distribution with the given parameters.
297///
298/// This is a convenience function to create a lognormal distribution with
299/// the given mu, sigma, and loc parameters.
300///
301/// # Arguments
302///
303/// * `mu` - Mean of the underlying normal distribution
304/// * `sigma` - Standard deviation of the underlying normal distribution
305/// * `loc` - Location parameter
306///
307/// # Returns
308///
309/// * A lognormal distribution object
310///
311/// # Examples
312///
313/// ```
314/// use scirs2_stats::distributions::lognormal;
315///
316/// let lognorm = lognormal::lognormal(0.0f64, 1.0, 0.0).expect("Operation failed");
317/// let pdf_at_one = lognorm.pdf(1.0);
318/// ```
319#[allow(dead_code)]
320pub fn lognormal<F>(mu: F, sigma: F, loc: F) -> StatsResult<Lognormal<F>>
321where
322    F: Float + NumCast + std::fmt::Display,
323{
324    Lognormal::new(mu, sigma, loc)
325}
326
327/// Implementation of SampleableDistribution for Lognormal
328impl<F: Float + NumCast + std::fmt::Display> SampleableDistribution<F> for Lognormal<F> {
329    fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
330        self.rvs(size)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use approx::assert_relative_eq;
338
339    #[test]
340    fn test_lognormal_creation() {
341        // Standard lognormal
342        let lognorm = Lognormal::new(0.0, 1.0, 0.0).expect("Operation failed");
343        assert_eq!(lognorm.mu, 0.0);
344        assert_eq!(lognorm.sigma, 1.0);
345        assert_eq!(lognorm.loc, 0.0);
346
347        // Custom lognormal
348        let custom = Lognormal::new(1.0, 0.5, 1.0).expect("Operation failed");
349        assert_eq!(custom.mu, 1.0);
350        assert_eq!(custom.sigma, 0.5);
351        assert_eq!(custom.loc, 1.0);
352
353        // Error cases
354        assert!(Lognormal::<f64>::new(0.0, 0.0, 0.0).is_err());
355        assert!(Lognormal::<f64>::new(0.0, -1.0, 0.0).is_err());
356    }
357
358    #[test]
359    fn test_lognormal_pdf() {
360        // Standard lognormal PDF values
361        let lognorm = Lognormal::new(0.0, 1.0, 0.0).expect("Operation failed");
362
363        // PDF at x = 1
364        let pdf_at_one = lognorm.pdf(1.0);
365        assert_relative_eq!(pdf_at_one, 0.3989423, epsilon = 1e-7);
366
367        // PDF at x = 2
368        let pdf_at_two = lognorm.pdf(2.0);
369        assert_relative_eq!(pdf_at_two, 0.1568740192789811, epsilon = 1e-7);
370
371        // PDF at x = 0.5
372        let pdf_at_half = lognorm.pdf(0.5);
373        assert_relative_eq!(pdf_at_half, 0.6274960771159244, epsilon = 1e-7);
374
375        // PDF at x <= 0 is 0
376        assert_eq!(lognorm.pdf(0.0), 0.0);
377        assert_eq!(lognorm.pdf(-1.0), 0.0);
378
379        // Custom lognormal
380        let custom = Lognormal::new(1.0, 0.5, 1.0).expect("Operation failed");
381
382        // PDF at x = 1 (at loc) should be 0
383        assert_eq!(custom.pdf(1.0), 0.0);
384
385        // PDF at x = 2 (adjusted for loc)
386        let pdf_at_custom = custom.pdf(2.0);
387        let expected = 0.10798193302637613; // Equivalent to standard normal PDF at 0 adjusted
388        assert_relative_eq!(pdf_at_custom, expected, epsilon = 1e-7);
389    }
390
391    #[test]
392    fn test_lognormal_cdf() {
393        // Standard lognormal CDF values
394        let lognorm = Lognormal::new(0.0, 1.0, 0.0).expect("Operation failed");
395
396        // CDF at x = 1
397        let cdf_at_one = lognorm.cdf(1.0);
398        assert_relative_eq!(cdf_at_one, 0.5, epsilon = 1e-7);
399
400        // CDF at x = 2
401        let cdf_at_two = lognorm.cdf(2.0);
402        assert_relative_eq!(cdf_at_two, 0.7558914, epsilon = 1e-7);
403
404        // CDF at x = 0.5
405        let cdf_at_half = lognorm.cdf(0.5);
406        assert_relative_eq!(cdf_at_half, 0.24410852729647436, epsilon = 1e-7);
407
408        // CDF at x <= 0 is 0
409        assert_eq!(lognorm.cdf(0.0), 0.0);
410        assert_eq!(lognorm.cdf(-1.0), 0.0);
411    }
412
413    #[test]
414    fn test_lognormal_ppf() {
415        // Standard lognormal quantiles
416        let lognorm = Lognormal::new(0.0, 1.0, 0.0).expect("Operation failed");
417
418        // Median (50th percentile)
419        let median = lognorm.ppf(0.5).expect("Operation failed");
420        assert_relative_eq!(median, 1.0, epsilon = 1e-7);
421
422        // 75th percentile: exp(Normal(0,1).ppf(0.75)) = exp(0.67449) ≈ 1.96303
423        let p75 = lognorm.ppf(0.75).expect("Operation failed");
424        assert_relative_eq!(p75, 1.9630, epsilon = 1e-3);
425
426        // 25th percentile: exp(Normal(0,1).ppf(0.25)) = exp(-0.67449) ≈ 0.50942
427        let p25 = lognorm.ppf(0.25).expect("Operation failed");
428        assert_relative_eq!(p25, 0.50942, epsilon = 1e-3);
429
430        // Error cases
431        assert!(lognorm.ppf(-0.1).is_err());
432        assert!(lognorm.ppf(1.1).is_err());
433    }
434
435    #[test]
436    fn test_lognormal_statistics() {
437        // Standard lognormal statistics
438        let lognorm = Lognormal::new(0.0, 1.0, 0.0).expect("Operation failed");
439
440        // Mean = exp(μ + σ²/2)
441        let mean = lognorm.mean();
442        assert_relative_eq!(mean, 1.6487212707, epsilon = 1e-7);
443
444        // Variance = exp(2μ + σ²) * (exp(σ²) - 1)
445        let var = lognorm.var();
446        assert_relative_eq!(var, 4.670774270471604, epsilon = 1e-7);
447
448        // Median = exp(μ)
449        let median = lognorm.median();
450        assert_relative_eq!(median, 1.0, epsilon = 1e-7);
451
452        // Mode = exp(μ - σ²)
453        let mode = lognorm.mode();
454        assert_relative_eq!(mode, 0.36787944, epsilon = 1e-7);
455
456        // Custom lognormal
457        let custom = Lognormal::new(1.0, 0.5, 2.0).expect("Operation failed");
458
459        // Mean with location parameter
460        let custom_mean = custom.mean();
461        let expected_mean = (1.0 + 0.5 * 0.5 / 2.0).exp() + 2.0;
462        assert_relative_eq!(custom_mean, expected_mean, epsilon = 1e-7);
463
464        // Median with location parameter
465        let custom_median = custom.median();
466        assert_relative_eq!(custom_median, 1.0.exp() + 2.0, epsilon = 1e-7);
467    }
468
469    #[test]
470    fn test_lognormal_rvs() {
471        let lognorm = Lognormal::new(0.0, 1.0, 0.0).expect("Operation failed");
472
473        // Generate samples
474        let samples = lognorm.rvs(1000).expect("Operation failed");
475
476        // Check the number of samples
477        assert_eq!(samples.len(), 1000);
478
479        // Basic positivity check (all lognormal samples should be positive)
480        for &sample in &samples {
481            assert!(sample > 0.0);
482        }
483
484        // Basic statistical checks
485        let sum: f64 = samples.iter().sum();
486        let mean = sum / 1000.0;
487
488        // Mean should be close to true mean (within reason for random samples)
489        // True mean for standard lognormal is e^(1/2) ≈ 1.6487
490        assert!((mean - 1.6487).abs() < 0.5);
491
492        // Calculate sample median as a sanity check
493        let mut sorted_samples = samples.clone();
494        sorted_samples.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
495        let median = if samples.len() % 2 == 0 {
496            (sorted_samples[499] + sorted_samples[500]) / 2.0
497        } else {
498            sorted_samples[500]
499        };
500
501        // Median should be close to 1 (within reason for random samples)
502        assert!((median - 1.0).abs() < 0.5);
503    }
504}