scirs2_stats/distributions/negative_binomial.rs
1//! Negative Binomial distribution functions
2//!
3//! This module provides functionality for the Negative Binomial 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;
10use statrs::function::gamma::ln_gamma;
11
12/// Negative Binomial distribution structure
13///
14/// The Negative Binomial distribution models the number of failures before r successes
15/// in a sequence of independent Bernoulli trials, each with success probability p.
16/// It is a generalization of the Geometric distribution for r > 1.
17///
18/// In this implementation, we use the convention where the random variable X
19/// represents the number of failures (not the number of trials) before the r-th success.
20pub struct NegativeBinomial<F: Float> {
21 /// Number of successes to achieve
22 pub r: F,
23 /// Success probability (0 < p ≤ 1)
24 pub p: F,
25}
26
27impl<F: Float + NumCast + std::fmt::Display> NegativeBinomial<F> {
28 /// Create a new Negative Binomial distribution with given parameters
29 ///
30 /// # Arguments
31 ///
32 /// * `r` - Number of successes to achieve (r > 0)
33 /// * `p` - Success probability (0 < p ≤ 1)
34 ///
35 /// # Returns
36 ///
37 /// * A new Negative Binomial distribution instance
38 ///
39 /// # Examples
40 ///
41 /// ```
42 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
43 ///
44 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
45 /// ```
46 pub fn new(r: F, p: F) -> StatsResult<Self> {
47 // Validate parameters
48 if r <= F::zero() {
49 return Err(StatsError::DomainError(
50 "Number of successes must be positive".to_string(),
51 ));
52 }
53
54 if p <= F::zero() || p > F::one() {
55 return Err(StatsError::DomainError(
56 "Success probability must be between 0 and 1, exclusive of 0".to_string(),
57 ));
58 }
59
60 Ok(NegativeBinomial { r, p })
61 }
62
63 /// Calculate the probability mass function (PMF) at a given point
64 ///
65 /// # Arguments
66 ///
67 /// * `k` - The point at which to evaluate the PMF (number of failures before r successes)
68 ///
69 /// # Returns
70 ///
71 /// * The value of the PMF at the given point
72 ///
73 /// # Examples
74 ///
75 /// ```
76 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
77 ///
78 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
79 /// let pmf_at_7 = nb.pmf(7.0);
80 /// assert!((pmf_at_7 - 0.0660).abs() < 1e-4);
81 /// ```
82 pub fn pmf(&self, k: F) -> F {
83 let zero = F::zero();
84 let one = F::one();
85
86 // Check if k is a non-negative integer
87 if k < zero || !Self::is_integer(k) {
88 return zero;
89 }
90
91 let k_usize = k.to_usize().unwrap_or(0);
92 let k_f = F::from(k_usize).unwrap_or_else(|| F::zero());
93
94 // Handle special cases
95 if self.p == one {
96 return if k_f == zero { one } else { zero };
97 }
98
99 // For integer r, we can use the binomial coefficient formula:
100 // PMF(k) = C(k+r-1, k) * p^r * (1-p)^k
101 if Self::is_integer(self.r) {
102 let r_usize = self.r.to_usize().unwrap_or(1);
103 if r_usize > 0 {
104 return self.binomial_pmf(k_usize);
105 }
106 }
107
108 // For non-integer r, we use the gamma function approach:
109 // PMF(k) = Gamma(r+k)/(Gamma(r)*k!) * p^r * (1-p)^k
110 self.gamma_pmf(k_f)
111 }
112
113 // Helper method for PMF calculation for integer r using binomial coefficient
114 fn binomial_pmf(&self, k: usize) -> F {
115 let one = F::one();
116 let k_f = F::from(k).unwrap_or_else(|| F::zero());
117
118 let r_usize = self.r.to_usize().unwrap_or(1);
119 let r_f = F::from(r_usize).unwrap_or_else(|| F::one());
120
121 // Calculate binomial coefficient C(k+r-1, k)
122 let binom_coef = self.binom_coef(k + r_usize - 1, k);
123
124 // Calculate p^r * (1-p)^k
125 let p_pow_r = self.p.powf(r_f);
126 let q_pow_k = (one - self.p).powf(k_f);
127
128 binom_coef * p_pow_r * q_pow_k
129 }
130
131 // Helper method for PMF calculation for non-integer r using gamma function
132 fn gamma_pmf(&self, k: F) -> F {
133 // Use F::one() directly in the code
134
135 // Convert k to f64 for gamma function
136 let k_f64 = <f64 as scirs2_core::numeric::NumCast>::from(k).unwrap_or(0.0);
137 let r_f64 = <f64 as scirs2_core::numeric::NumCast>::from(self.r).unwrap_or(1.0);
138
139 // Calculate ln(Gamma(r+k)/(Gamma(r)*k!))
140 let ln_coef = ln_gamma(r_f64 + k_f64) - ln_gamma(r_f64) - ln_gamma(k_f64 + 1.0);
141
142 // Calculate ln(p^r * (1-p)^k)
143 let p_f64 = <f64 as scirs2_core::numeric::NumCast>::from(self.p).unwrap_or(0.5);
144 let ln_prob = r_f64 * p_f64.ln() + k_f64 * (1.0 - p_f64).ln();
145
146 // Combine and convert back to F
147 F::from((ln_coef + ln_prob).exp()).unwrap_or_else(|| F::zero())
148 }
149
150 /// Calculate the log of the probability mass function (log-PMF) at a given point
151 ///
152 /// # Arguments
153 ///
154 /// * `k` - The point at which to evaluate the log-PMF (number of failures before r successes)
155 ///
156 /// # Returns
157 ///
158 /// * The value of the log-PMF at the given point
159 ///
160 /// # Examples
161 ///
162 /// ```
163 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
164 ///
165 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
166 /// let log_pmf_at_7 = nb.log_pmf(7.0);
167 /// assert!((log_pmf_at_7 - (-2.717)).abs() < 1e-3);
168 /// ```
169 pub fn log_pmf(&self, k: F) -> F {
170 let zero = F::zero();
171 let one = F::one();
172 let neg_infinity = F::neg_infinity();
173
174 // Check if k is a non-negative integer
175 if k < zero || !Self::is_integer(k) {
176 return neg_infinity;
177 }
178
179 let k_usize = k.to_usize().expect("Operation failed");
180 let k_f = F::from(k_usize).expect("Failed to convert to float");
181
182 // Handle special cases
183 if self.p == one {
184 return if k_f == zero { zero } else { neg_infinity };
185 }
186
187 // For integer r, we can use the binomial coefficient formula
188 if Self::is_integer(self.r) {
189 let r_usize = self.r.to_usize().expect("Operation failed");
190 if r_usize > 0 {
191 return self.log_binomial_pmf(k_usize);
192 }
193 }
194
195 // For non-integer r, we use the gamma function approach
196 self.log_gamma_pmf(k_f)
197 }
198
199 // Helper method for log-PMF calculation for integer r
200 fn log_binomial_pmf(&self, k: usize) -> F {
201 let one = F::one();
202 let k_f = F::from(k).expect("Failed to convert to float");
203
204 let r_usize = self.r.to_usize().expect("Operation failed");
205 let r_f = F::from(r_usize).expect("Failed to convert to float");
206
207 // Calculate ln(C(k+r-1, k))
208 let ln_binom_coef = self.ln_binom_coef(k + r_usize - 1, k);
209
210 // Calculate ln(p^r * (1-p)^k)
211 let ln_p_pow_r = r_f * self.p.ln();
212 let ln_q_pow_k = k_f * (one - self.p).ln();
213
214 ln_binom_coef + ln_p_pow_r + ln_q_pow_k
215 }
216
217 // Helper method for log-PMF calculation for non-integer r
218 fn log_gamma_pmf(&self, k: F) -> F {
219 // Use F::one() directly in the code
220
221 // Convert k to f64 for gamma function
222 let k_f64 = <f64 as scirs2_core::numeric::NumCast>::from(k).unwrap_or(0.0);
223 let r_f64 = <f64 as scirs2_core::numeric::NumCast>::from(self.r).unwrap_or(1.0);
224
225 // Calculate ln(Gamma(r+k)/(Gamma(r)*k!))
226 let ln_coef = ln_gamma(r_f64 + k_f64) - ln_gamma(r_f64) - ln_gamma(k_f64 + 1.0);
227
228 // Calculate ln(p^r * (1-p)^k)
229 let p_f64 = <f64 as scirs2_core::numeric::NumCast>::from(self.p).unwrap_or(0.5);
230 let ln_prob = r_f64 * p_f64.ln() + k_f64 * (1.0 - p_f64).ln();
231
232 // Convert back to F
233 F::from(ln_coef + ln_prob).expect("Failed to convert to float")
234 }
235
236 /// Calculate the cumulative distribution function (CDF) at a given point
237 ///
238 /// # Arguments
239 ///
240 /// * `k` - The point at which to evaluate the CDF
241 ///
242 /// # Returns
243 ///
244 /// * The value of the CDF at the given point
245 ///
246 /// # Examples
247 ///
248 /// ```
249 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
250 ///
251 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
252 /// let cdf_at_7 = nb.cdf(7.0);
253 /// assert!((cdf_at_7 - 0.2763).abs() < 1e-4);
254 /// ```
255 pub fn cdf(&self, k: F) -> F {
256 let zero = F::zero();
257 let one = F::one();
258
259 // CDF = 0 for k < 0
260 if k < zero {
261 return zero;
262 }
263
264 // Handle special cases
265 if self.p == one {
266 return one; // Always 1 for p=1
267 }
268
269 // Floor k to handle non-integer values
270 let k_floor = k.floor();
271 let k_int = k_floor.to_usize().expect("Operation failed");
272
273 // Calculate CDF as sum of PMFs from 0 to k_int
274 let mut sum = zero;
275 for i in 0..=k_int {
276 sum = sum + self.pmf(F::from(i).expect("Failed to convert to float"));
277 }
278
279 sum
280 }
281
282 /// Inverse of the cumulative distribution function (quantile function)
283 ///
284 /// # Arguments
285 ///
286 /// * `p` - Probability value (between 0 and 1)
287 ///
288 /// # Returns
289 ///
290 /// * The value k such that CDF(k) = p
291 ///
292 /// # Examples
293 ///
294 /// ```
295 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
296 ///
297 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
298 /// let quant = nb.ppf(0.5).expect("Operation failed");
299 /// assert_eq!(quant, 11.0);
300 /// ```
301 pub fn ppf(&self, pval: F) -> StatsResult<F> {
302 let zero = F::zero();
303 let one = F::one();
304
305 if pval < zero || pval > one {
306 return Err(StatsError::DomainError(
307 "Probability must be between 0 and 1".to_string(),
308 ));
309 }
310
311 // Special cases
312 if pval <= zero {
313 return Ok(zero);
314 }
315 if pval >= one {
316 return Ok(F::infinity());
317 }
318
319 // Handle case when p = 1.0 separately
320 if self.p == one {
321 return Ok(zero); // When p=1, X is always 0
322 }
323
324 // For actual calculation, we need to find the smallest k where CDF(k) >= p_val
325 // Use an iterative approach
326 let mut k = zero;
327
328 // Maximum number of iterations to avoid infinite loops
329 let max_iter = 1000;
330 let mut iter = 0;
331
332 while self.cdf(k) < pval && iter < max_iter {
333 k = k + one;
334 iter += 1;
335 }
336
337 if iter == max_iter {
338 return Err(StatsError::ComputationError(
339 "Failed to converge in PPF calculation".to_string(),
340 ));
341 }
342
343 Ok(k)
344 }
345
346 /// Generate random samples from the distribution
347 ///
348 /// # Arguments
349 ///
350 /// * `size` - Number of samples to generate
351 ///
352 /// # Returns
353 ///
354 /// * Vector of random samples
355 ///
356 /// # Examples
357 ///
358 /// ```
359 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
360 ///
361 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
362 /// let samples = nb.rvs(10).expect("Operation failed");
363 /// assert_eq!(samples.len(), 10);
364 /// ```
365 pub fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
366 let mut rng = thread_rng();
367 let mut samples = Vec::with_capacity(size);
368
369 // For integer r, we can use a sum of geometric variables
370 if Self::is_integer(self.r) {
371 let r_usize = self.r.to_usize().expect("Operation failed");
372
373 for _ in 0..size {
374 // Generate r geometric variables and sum them
375 let mut sum = 0;
376 for _ in 0..r_usize {
377 // Generate geometric random variable (# failures before first success)
378 let u: f64 = rng.random_range(0.0..1.0);
379 let p_f64 = <f64 as scirs2_core::numeric::NumCast>::from(self.p).unwrap_or(0.5);
380 let geom_sample = (u.ln()
381 / (F::from(1.0)
382 .expect("Failed to convert constant to float")
383 .to_f64()
384 .expect("Operation failed")
385 - p_f64)
386 .ln())
387 .floor() as usize;
388 sum += geom_sample;
389 }
390 samples.push(F::from(sum).expect("Failed to convert to float"));
391 }
392 } else {
393 // For non-integer r..use gamma-Poisson mixture
394 let r_f64 = <f64 as scirs2_core::numeric::NumCast>::from(self.r).unwrap_or(1.0);
395 let p_f64 = <f64 as scirs2_core::numeric::NumCast>::from(self.p).unwrap_or(0.5);
396
397 for _ in 0..size {
398 // Generate gamma random variable with shape r and scale (1-p)/p
399 let gamma_distr = scirs2_core::random::Gamma::new(r_f64, (1.0 - p_f64) / p_f64)
400 .map_err(|_| {
401 StatsError::ComputationError(
402 "Failed to create gamma distribution".to_string(),
403 )
404 })?;
405 let gamma_sample: f64 = gamma_distr.sample(&mut rng);
406
407 // Generate Poisson random variable with mean gamma_sample
408 let poisson_distr =
409 scirs2_core::random::Poisson::new(gamma_sample).map_err(|_| {
410 StatsError::ComputationError(
411 "Failed to create Poisson distribution".to_string(),
412 )
413 })?;
414 let poisson_sample = poisson_distr.sample(&mut rng);
415
416 samples.push(F::from(poisson_sample).expect("Failed to convert to float"));
417 }
418 }
419
420 Ok(samples)
421 }
422
423 /// Calculate the mean of the distribution
424 ///
425 /// # Returns
426 ///
427 /// * The mean of the distribution
428 ///
429 /// # Examples
430 ///
431 /// ```
432 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
433 ///
434 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
435 /// let mean = nb.mean();
436 /// assert!((mean - 11.666667).abs() < 1e-6);
437 /// ```
438 pub fn mean(&self) -> F {
439 // Mean = r * (1-p) / p
440 let one = F::one();
441 let q = one - self.p; // q = 1 - p
442 self.r * q / self.p
443 }
444
445 /// Calculate the variance of the distribution
446 ///
447 /// # Returns
448 ///
449 /// * The variance of the distribution
450 ///
451 /// # Examples
452 ///
453 /// ```
454 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
455 ///
456 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
457 /// let variance = nb.var();
458 /// assert!((variance - 38.888889).abs() < 1e-6);
459 /// ```
460 pub fn var(&self) -> F {
461 // Variance = r * (1-p) / p^2
462 let one = F::one();
463 let q = one - self.p; // q = 1 - p
464 self.r * q / (self.p * self.p)
465 }
466
467 /// Calculate the standard deviation of the distribution
468 ///
469 /// # Returns
470 ///
471 /// * The standard deviation of the distribution
472 ///
473 /// # Examples
474 ///
475 /// ```
476 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
477 ///
478 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
479 /// let std_dev = nb.std();
480 /// assert!((std_dev - 6.236).abs() < 1e-3);
481 /// ```
482 pub fn std(&self) -> F {
483 // Std = sqrt(variance)
484 self.var().sqrt()
485 }
486
487 /// Calculate the skewness of the distribution
488 ///
489 /// # Returns
490 ///
491 /// * The skewness of the distribution
492 ///
493 /// # Examples
494 ///
495 /// ```
496 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
497 ///
498 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
499 /// let skewness = nb.skewness();
500 /// assert!((skewness - 0.909).abs() < 1e-3);
501 /// ```
502 pub fn skewness(&self) -> F {
503 // Skewness = (2-p) / sqrt(r * (1-p))
504 let one = F::one();
505 let two = F::from(2.0).expect("Failed to convert constant to float");
506
507 let q = one - self.p; // q = 1 - p
508 (two - self.p) / (self.r * q).sqrt()
509 }
510
511 /// Calculate the kurtosis of the distribution
512 ///
513 /// # Returns
514 ///
515 /// * The excess kurtosis of the distribution
516 ///
517 /// # Examples
518 ///
519 /// ```
520 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
521 ///
522 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
523 /// let kurtosis = nb.kurtosis();
524 /// assert!((kurtosis - 1.226).abs() < 1e-3);
525 /// ```
526 pub fn kurtosis(&self) -> F {
527 // Excess Kurtosis = 6/r + (p^2)/(r*(1-p))
528 let one = F::one();
529 let six = F::from(6.0).expect("Failed to convert constant to float");
530
531 let q = one - self.p; // q = 1 - p
532 six / self.r + (self.p * self.p) / (self.r * q)
533 }
534
535 /// Calculate the entropy of the distribution
536 ///
537 /// # Returns
538 ///
539 /// * The entropy value (approximation for non-integer r)
540 ///
541 /// # Examples
542 ///
543 /// ```
544 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
545 ///
546 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
547 /// let entropy = nb.entropy();
548 /// assert!(entropy > 0.0);
549 /// ```
550 pub fn entropy(&self) -> F {
551 // For integer r, approximate using the mean and variance
552 // This is a reasonable approximation for large r
553 let half = F::from(0.5).expect("Failed to convert constant to float");
554 let two_pi_e = F::from(2.0 * std::f64::consts::PI * std::f64::consts::E)
555 .expect("Failed to convert to float");
556
557 // If r is large, use normal approximation
558 if self.r >= F::from(10.0).expect("Failed to convert constant to float") {
559 let variance = self.var();
560 return half * (two_pi_e * variance).ln();
561 }
562
563 // For smaller r, compute entropy directly for some values
564 // (This is an approximation for the general case)
565 let one = F::one();
566 let q = one - self.p; // q = 1 - p
567 let mean = self.mean();
568
569 if Self::is_integer(self.r) {
570 // r * ln(1/p) - mean * ln(q)
571 return self.r * (-self.p.ln()) - mean * q.ln();
572 }
573
574 // For non-integer r, use mean and variance to approximate
575 half * (two_pi_e * self.var()).ln()
576 }
577
578 /// Calculate the mode of the distribution
579 ///
580 /// # Returns
581 ///
582 /// * The mode of the distribution (floor of (r-1)*(1-p)/p for r>1, 0 for r≤1)
583 ///
584 /// # Examples
585 ///
586 /// ```
587 /// use scirs2_stats::distributions::negative_binomial::NegativeBinomial;
588 ///
589 /// let nb = NegativeBinomial::new(5.0f64, 0.3).expect("Operation failed");
590 /// let mode = nb.mode();
591 /// assert_eq!(mode, 9.0);
592 /// ```
593 pub fn mode(&self) -> F {
594 let zero = F::zero();
595 let one = F::one();
596
597 // For r ≤ 1, the mode is 0
598 if self.r <= one {
599 return zero;
600 }
601
602 // For r > 1, mode = floor((r-1)*(1-p)/p)
603 let q = one - self.p; // q = 1 - p
604 ((self.r - one) * q / self.p).floor()
605 }
606
607 // Helper method to check if a value is an integer
608 fn is_integer(value: F) -> bool {
609 value == value.floor()
610 }
611
612 // Helper method to calculate binomial coefficient C(n,k)
613 fn binom_coef(&self, n: usize, k: usize) -> F {
614 // For small values, calculate directly
615 if n <= 20 {
616 let mut result: u64 = 1;
617 for i in 0..k {
618 result = result * (n - i) as u64 / (i + 1) as u64;
619 }
620 F::from(result).expect("Failed to convert to float")
621 } else {
622 // For larger values, use ln_gamma for numerical stability
623 self.ln_binom_coef(n, k).exp()
624 }
625 }
626
627 // Helper method to calculate log of binomial coefficient ln(C(n,k))
628 fn ln_binom_coef(&self, n: usize, k: usize) -> F {
629 let n_f64 = n as f64;
630 let k_f64 = k as f64;
631
632 // ln(C(n,k)) = ln_gamma(n+1) - ln_gamma(k+1) - ln_gamma(n-k+1)
633 let result = ln_gamma(n_f64 + 1.0) - ln_gamma(k_f64 + 1.0) - ln_gamma(n_f64 - k_f64 + 1.0);
634
635 F::from(result).expect("Failed to convert to float")
636 }
637}
638
639/// Create a Negative Binomial distribution with the given parameters.
640///
641/// This is a convenience function to create a Negative Binomial distribution with
642/// the given number of successes and success probability.
643///
644/// # Arguments
645///
646/// * `r` - Number of successes to achieve (r > 0)
647/// * `p` - Success probability (0 < p ≤ 1)
648///
649/// # Returns
650///
651/// * A Negative Binomial distribution object
652///
653/// # Examples
654///
655/// ```
656/// use scirs2_stats::distributions;
657///
658/// let nb = distributions::nbinom(5.0f64, 0.3).expect("Operation failed");
659/// let pmf_at_7 = nb.pmf(7.0);
660/// assert!((pmf_at_7 - 0.0660).abs() < 1e-4);
661/// ```
662#[allow(dead_code)]
663pub fn nbinom<F>(r: F, p: F) -> StatsResult<NegativeBinomial<F>>
664where
665 F: Float + NumCast + std::fmt::Display,
666{
667 NegativeBinomial::new(r, p)
668}
669
670/// Implementation of SampleableDistribution for NegativeBinomial
671impl<F: Float + NumCast + std::fmt::Display> SampleableDistribution<F> for NegativeBinomial<F> {
672 fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
673 self.rvs(size)
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use approx::assert_relative_eq;
681
682 #[test]
683 fn test_negative_binomial_creation() {
684 // Valid parameters
685 let nb1 = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
686 assert_eq!(nb1.r, 5.0);
687 assert_eq!(nb1.p, 0.3);
688
689 let nb2 = NegativeBinomial::new(1.0, 1.0).expect("Operation failed");
690 assert_eq!(nb2.r, 1.0);
691 assert_eq!(nb2.p, 1.0);
692
693 // Invalid r values
694 assert!(NegativeBinomial::<f64>::new(0.0, 0.3).is_err());
695 assert!(NegativeBinomial::<f64>::new(-1.0, 0.3).is_err());
696
697 // Invalid p values
698 assert!(NegativeBinomial::<f64>::new(5.0, 0.0).is_err());
699 assert!(NegativeBinomial::<f64>::new(5.0, -0.1).is_err());
700 assert!(NegativeBinomial::<f64>::new(5.0, 1.1).is_err());
701 }
702
703 #[test]
704 fn test_negative_binomial_pmf() {
705 // NegativeBinomial(5, 0.3) - integer r
706 let nb = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
707
708 // PMF values for different k
709 assert_relative_eq!(nb.pmf(0.0), 0.00243, epsilon = 1e-5);
710 assert_relative_eq!(nb.pmf(3.0), 0.02917, epsilon = 1e-5);
711 assert_relative_eq!(nb.pmf(7.0), 0.06604, epsilon = 1e-5);
712 assert_relative_eq!(nb.pmf(12.0), 0.06121, epsilon = 1e-5);
713
714 // PMF should be 0 for negative k
715 assert_eq!(nb.pmf(-1.0), 0.0);
716
717 // PMF should be 0 for non-integer k
718 assert_eq!(nb.pmf(3.5), 0.0);
719
720 // Special case: NegativeBinomial(1, 0.3) - same as geometric
721 let nb_geom = NegativeBinomial::new(1.0, 0.3).expect("Operation failed");
722 assert_relative_eq!(nb_geom.pmf(0.0), 0.3, epsilon = 1e-10);
723 assert_relative_eq!(nb_geom.pmf(1.0), 0.3 * 0.7, epsilon = 1e-10);
724 assert_relative_eq!(nb_geom.pmf(2.0), 0.3 * 0.7 * 0.7, epsilon = 1e-10);
725
726 // Special case: NegativeBinomial(r, 1.0)
727 let nb_p1 = NegativeBinomial::new(5.0, 1.0).expect("Operation failed");
728 assert_eq!(nb_p1.pmf(0.0), 1.0);
729 assert_eq!(nb_p1.pmf(1.0), 0.0);
730
731 // Test with non-integer r
732 let nb_non_int = NegativeBinomial::new(2.5, 0.3).expect("Operation failed");
733 assert!(nb_non_int.pmf(0.0) > 0.0);
734 assert!(nb_non_int.pmf(3.0) > 0.0);
735 }
736
737 #[test]
738 fn test_negative_binomial_log_pmf() {
739 // NegativeBinomial(5, 0.3)
740 let nb = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
741
742 // Check log_pmf against pmf
743 for k in 0..15 {
744 let k_f = k as f64;
745 let pmf = nb.pmf(k_f);
746 let log_pmf = nb.log_pmf(k_f);
747
748 if pmf > 0.0 {
749 assert_relative_eq!(log_pmf, pmf.ln(), epsilon = 1e-5);
750 } else {
751 assert!(log_pmf.is_infinite() && log_pmf.is_sign_negative());
752 }
753 }
754
755 // log_pmf should be -infinity for negative k or non-integer k
756 assert!(nb.log_pmf(-1.0).is_infinite() && nb.log_pmf(-1.0).is_sign_negative());
757 assert!(nb.log_pmf(3.5).is_infinite() && nb.log_pmf(3.5).is_sign_negative());
758 }
759
760 #[test]
761 fn test_negative_binomial_cdf() {
762 // NegativeBinomial(5, 0.3)
763 let nb = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
764
765 // CDF for k < 0 should be 0
766 assert_eq!(nb.cdf(-1.0), 0.0);
767
768 // CDF values for different k
769 assert_relative_eq!(nb.cdf(0.0), 0.00243, epsilon = 1e-5);
770 assert_relative_eq!(nb.cdf(3.0), 0.05796, epsilon = 1e-5);
771 assert_relative_eq!(nb.cdf(7.0), 0.27634, epsilon = 1e-5);
772 assert_relative_eq!(nb.cdf(12.0), 0.61131, epsilon = 1e-5);
773
774 // Non-integer k should use floor of k
775 assert_relative_eq!(nb.cdf(7.5), nb.cdf(7.0), epsilon = 1e-10);
776
777 // Special case: NegativeBinomial(r, 1.0)
778 let nb_p1 = NegativeBinomial::new(5.0, 1.0).expect("Operation failed");
779 assert_eq!(nb_p1.cdf(0.0), 1.0);
780 assert_eq!(nb_p1.cdf(1.0), 1.0);
781 }
782
783 #[test]
784 fn test_negative_binomial_ppf() {
785 // NegativeBinomial(5, 0.3)
786 let nb = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
787
788 // Verify that ppf is the inverse of cdf for some test points
789 let test_points = vec![0.1, 0.3, 0.5, 0.7, 0.9];
790 for &p in &test_points {
791 let k = nb.ppf(p).expect("Operation failed");
792
793 // For discrete distributions, we check that CDF(k-1) < p ≤ CDF(k)
794 if k > 0.0 {
795 let k_minus_1 = k - 1.0;
796 let cdf_k_minus_1 = nb.cdf(k_minus_1);
797 let cdf_k = nb.cdf(k);
798 assert!(cdf_k_minus_1 < p && p <= cdf_k);
799 } else {
800 // For k = 0, just check that p ≤ CDF(0)
801 let cdf_k = nb.cdf(k);
802 assert!(p <= cdf_k);
803 }
804 }
805
806 // Test edge cases
807 assert_eq!(nb.ppf(0.0).expect("Operation failed"), 0.0);
808 assert!(nb.ppf(1.0).expect("Operation failed").is_infinite());
809
810 // Invalid probability values
811 assert!(nb.ppf(-0.1).is_err());
812 assert!(nb.ppf(1.1).is_err());
813 }
814
815 #[test]
816 fn test_negative_binomial_rvs() {
817 // NegativeBinomial(5, 0.3)
818 let nb = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
819
820 // Generate samples
821 let samples = nb.rvs(100).expect("Operation failed");
822
823 // Check number of samples
824 assert_eq!(samples.len(), 100);
825
826 // All samples should be non-negative integers
827 for &s in &samples {
828 assert!(s >= 0.0);
829 assert_eq!(s, s.floor());
830 }
831
832 // Mean should be approximately r(1-p)/p = 5*0.7/0.3 = 11.67
833 let sum: f64 = samples.iter().sum();
834 let mean = sum / samples.len() as f64;
835
836 // The mean could deviate somewhat due to randomness
837 assert!(mean > 8.0 && mean < 15.0);
838 }
839
840 #[test]
841 fn test_negative_binomial_moments() {
842 // Test with r = 5, p = 0.3
843 let nb = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
844
845 // Mean = r(1-p)/p = 5*0.7/0.3 = 11.67
846 assert_relative_eq!(nb.mean(), 11.666667, epsilon = 1e-6);
847
848 // Variance = r(1-p)/p^2 = 5*0.7/0.3^2 = 38.89
849 assert_relative_eq!(nb.var(), 38.888889, epsilon = 1e-6);
850
851 // Standard deviation = sqrt(variance) = sqrt(38.89) ≈ 6.236
852 assert_relative_eq!(nb.std(), 6.236095, epsilon = 1e-6);
853
854 // Skewness = (2-p)/sqrt(r(1-p)) = (2-0.3)/sqrt(5*0.7) ≈ 0.909
855 assert_relative_eq!(nb.skewness(), 0.908688, epsilon = 1e-6);
856
857 // Kurtosis = 6/r + p^2/(r(1-p)) = 6/5 + 0.3^2/(5*0.7) ≈ 1.226
858 assert_relative_eq!(nb.kurtosis(), 1.225714, epsilon = 1e-6);
859
860 // Mode is integer for these parameters: floor((r-1)(1-p)/p) = floor(4*0.7/0.3) = 9
861 assert_eq!(nb.mode(), 9.0);
862 }
863
864 #[test]
865 fn test_negative_binomial_edge_cases() {
866 // Test with r = 1, p = 0.3 (should be equivalent to Geometric(0.3))
867 let nb_geom = NegativeBinomial::new(1.0, 0.3).expect("Operation failed");
868
869 // Mean = (1-p)/p = 0.7/0.3 = 2.33...
870 assert_relative_eq!(nb_geom.mean(), 2.333333, epsilon = 1e-6);
871
872 // Variance = (1-p)/p^2 = 0.7/0.09 = 7.77...
873 assert_relative_eq!(nb_geom.var(), 7.777778, epsilon = 1e-6);
874
875 // Mode should be 0 for r = 1
876 assert_eq!(nb_geom.mode(), 0.0);
877
878 // Test with large r
879 let nb_large = NegativeBinomial::new(100.0, 0.3).expect("Operation failed");
880
881 // Mean = r(1-p)/p = 100*0.7/0.3 = 233.33...
882 assert_relative_eq!(nb_large.mean(), 233.33333, epsilon = 1e-5);
883
884 // Test with p close to 1
885 let nb_p_high = NegativeBinomial::new(5.0, 0.99).expect("Operation failed");
886
887 // Mean = r(1-p)/p = 5*0.01/0.99 = 0.0505...
888 assert_relative_eq!(nb_p_high.mean(), 0.050505, epsilon = 1e-6);
889 }
890
891 #[test]
892 fn test_negative_binomial_compare_with_binomial() {
893 // NegativeBinomial with integer r should use binomial coefficient calculation
894 let nb_int = NegativeBinomial::new(5.0, 0.3).expect("Operation failed");
895
896 // PMF values for different k
897 let pmf_0 = nb_int.pmf(0.0);
898 let pmf_3 = nb_int.pmf(3.0);
899 let pmf_7 = nb_int.pmf(7.0);
900
901 // Theoretical values
902 // For k=0: C(5+0-1, 0) * 0.3^5 * 0.7^0 = C(4, 0) * 0.3^5 = 0.3^5 ≈ 0.00243
903 // For k=3: C(5+3-1, 3) * 0.3^5 * 0.7^3 = C(7, 3) * 0.3^5 * 0.7^3 ≈ 0.02917
904 // For k=7: C(5+7-1, 7) * 0.3^5 * 0.7^7 = C(11, 7) * 0.3^5 * 0.7^7 ≈ 0.06604
905 assert_relative_eq!(pmf_0, 0.00243, epsilon = 1e-5);
906 assert_relative_eq!(pmf_3, 0.02917, epsilon = 1e-5);
907 assert_relative_eq!(pmf_7, 0.06604, epsilon = 1e-5);
908 }
909
910 #[test]
911 fn test_non_integer_r() {
912 // Test with non-integer r
913 let nb_non_int = NegativeBinomial::new(2.5, 0.3).expect("Operation failed");
914
915 // PMF values for different k
916 // These values are approximated using gamma function approach
917 let pmf_0 = nb_non_int.pmf(0.0);
918 let pmf_3 = nb_non_int.pmf(3.0);
919 let pmf_7 = nb_non_int.pmf(7.0);
920
921 // Since we're using gamma approximation, just check the values are positive
922 assert!(pmf_0 > 0.0);
923 assert!(pmf_3 > 0.0);
924 assert!(pmf_7 > 0.0);
925
926 // Check that the distribution properties are consistent
927 let mean = nb_non_int.mean();
928 let var = nb_non_int.var();
929
930 // Mean = r(1-p)/p = 2.5*0.7/0.3 = 5.83...
931 assert_relative_eq!(mean, 5.833333, epsilon = 1e-6);
932
933 // Variance = r(1-p)/p^2 = 2.5*0.7/0.3^2 = 19.44...
934 assert_relative_eq!(var, 19.444444, epsilon = 1e-6);
935 }
936}