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}