1use crate::analysis::statistics::DistributionType;
10
11pub const FAMILIES: [DistributionType; 14] = [
13 DistributionType::Normal,
14 DistributionType::LogNormal,
15 DistributionType::Uniform,
16 DistributionType::Exponential,
17 DistributionType::Gamma,
18 DistributionType::ChiSquared,
19 DistributionType::Beta,
20 DistributionType::StudentsT,
21 DistributionType::Weibull,
22 DistributionType::PowerLaw,
23 DistributionType::Poisson,
24 DistributionType::Bernoulli,
25 DistributionType::Binomial,
26 DistributionType::Geometric,
27];
28
29pub const REPLICATES: usize = 199;
31
32pub const TEST_VALUES: usize = 500;
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum Fitted {
40 Normal {
41 mean: f64,
42 sd: f64,
43 },
44 LogNormal {
45 mu: f64,
46 sigma: f64,
47 },
48 Uniform {
49 low: f64,
50 high: f64,
51 },
52 Exponential {
53 rate: f64,
54 },
55 Gamma {
56 shape: f64,
57 scale: f64,
58 },
59 ChiSquared {
60 df: f64,
61 },
62 Beta {
63 alpha: f64,
64 beta: f64,
65 },
66 StudentsT {
67 df: f64,
68 location: f64,
69 scale: f64,
70 },
71 Weibull {
72 shape: f64,
73 scale: f64,
74 },
75 PowerLaw {
76 xmin: f64,
77 alpha: f64,
78 },
79 Poisson {
80 rate: f64,
81 },
82 Bernoulli {
83 p: f64,
84 },
85 Binomial {
86 trials: u64,
87 p: f64,
88 },
89 Geometric {
92 p: f64,
93 start: u64,
94 },
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub enum FitOutcome {
100 Tested(FitTest),
101 NotApplicable(&'static str),
103}
104
105impl FitOutcome {
106 pub fn p_value(&self) -> Option<f64> {
107 match self {
108 Self::Tested(test) => Some(test.p_value),
109 Self::NotApplicable(_) => None,
110 }
111 }
112
113 pub fn test(&self) -> Option<&FitTest> {
114 match self {
115 Self::Tested(test) => Some(test),
116 Self::NotApplicable(_) => None,
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq)]
123pub struct FitTest {
124 pub fitted: Fitted,
126 pub p_value: f64,
128 pub beyond: usize,
130 pub replicates: usize,
131 pub tested_on: usize,
133 pub aic: f64,
136}
137
138impl FitTest {
139 pub fn at_bound(&self) -> bool {
142 self.beyond == 0
143 }
144}
145
146impl Fitted {
147 pub fn family(&self) -> DistributionType {
148 match self {
149 Self::Normal { .. } => DistributionType::Normal,
150 Self::LogNormal { .. } => DistributionType::LogNormal,
151 Self::Uniform { .. } => DistributionType::Uniform,
152 Self::Exponential { .. } => DistributionType::Exponential,
153 Self::Gamma { .. } => DistributionType::Gamma,
154 Self::ChiSquared { .. } => DistributionType::ChiSquared,
155 Self::Beta { .. } => DistributionType::Beta,
156 Self::StudentsT { .. } => DistributionType::StudentsT,
157 Self::Weibull { .. } => DistributionType::Weibull,
158 Self::PowerLaw { .. } => DistributionType::PowerLaw,
159 Self::Poisson { .. } => DistributionType::Poisson,
160 Self::Bernoulli { .. } => DistributionType::Bernoulli,
161 Self::Binomial { .. } => DistributionType::Binomial,
162 Self::Geometric { .. } => DistributionType::Geometric,
163 }
164 }
165
166 pub fn discrete(&self) -> bool {
168 matches!(
169 self,
170 Self::Poisson { .. }
171 | Self::Bernoulli { .. }
172 | Self::Binomial { .. }
173 | Self::Geometric { .. }
174 )
175 }
176
177 fn parameters(&self) -> usize {
178 match self {
179 Self::Exponential { .. }
180 | Self::ChiSquared { .. }
181 | Self::Poisson { .. }
182 | Self::Bernoulli { .. } => 1,
183 Self::Geometric { .. } => 2,
185 Self::StudentsT { .. } => 3,
186 _ => 2,
187 }
188 }
189
190 pub fn cdf(&self, x: f64) -> f64 {
192 match *self {
193 Self::Normal { mean, sd } => normal_cdf((x - mean) / sd),
194 Self::LogNormal { mu, sigma } => {
195 if x <= 0.0 {
196 0.0
197 } else {
198 normal_cdf((x.ln() - mu) / sigma)
199 }
200 }
201 Self::Uniform { low, high } => ((x - low) / (high - low)).clamp(0.0, 1.0),
202 Self::Exponential { rate } => {
203 if x <= 0.0 {
204 0.0
205 } else {
206 -(-rate * x).exp_m1()
207 }
208 }
209 Self::Gamma { shape, scale } => {
210 if x <= 0.0 {
211 0.0
212 } else {
213 gamma_p(shape, x / scale)
214 }
215 }
216 Self::ChiSquared { df } => {
217 if x <= 0.0 {
218 0.0
219 } else {
220 gamma_p(df / 2.0, x / 2.0)
221 }
222 }
223 Self::Beta { alpha, beta } => {
224 if x <= 0.0 {
225 0.0
226 } else if x >= 1.0 {
227 1.0
228 } else {
229 beta_inc(alpha, beta, x)
230 }
231 }
232 Self::StudentsT {
233 df,
234 location,
235 scale,
236 } => t_cdf((x - location) / scale, df),
237 Self::Weibull { shape, scale } => {
238 if x <= 0.0 {
239 0.0
240 } else {
241 -(-(x / scale).powf(shape)).exp_m1()
242 }
243 }
244 Self::PowerLaw { xmin, alpha } => {
245 if x < xmin {
246 0.0
247 } else {
248 1.0 - (x / xmin).powf(1.0 - alpha)
249 }
250 }
251 Self::Poisson { rate } => {
252 if x < 0.0 {
253 0.0
254 } else {
255 gamma_q(x.floor() + 1.0, rate)
257 }
258 }
259 Self::Bernoulli { p } => {
260 if x < 0.0 {
261 0.0
262 } else if x < 1.0 {
263 1.0 - p
264 } else {
265 1.0
266 }
267 }
268 Self::Binomial { trials, p } => {
269 let k = x.floor();
270 if k < 0.0 {
271 0.0
272 } else if k >= trials as f64 {
273 1.0
274 } else {
275 beta_inc(trials as f64 - k, k + 1.0, 1.0 - p)
277 }
278 }
279 Self::Geometric { p, start } => {
280 let k = x.floor() - start as f64;
281 if k < 0.0 {
282 0.0
283 } else {
284 -((k + 1.0) * (-p).ln_1p()).exp_m1()
285 }
286 }
287 }
288 }
289
290 pub fn cdf_below(&self, x: f64) -> f64 {
292 if !self.discrete() {
293 return self.cdf(x);
294 }
295 if x == x.floor() {
296 self.cdf(x - 1.0)
297 } else {
298 self.cdf(x)
299 }
300 }
301
302 pub fn quantile(&self, p: f64) -> f64 {
304 if p.is_nan() {
305 return f64::NAN;
306 }
307 match *self {
308 Self::Normal { mean, sd } => mean + sd * normal_quantile(p),
309 Self::LogNormal { mu, sigma } => (mu + sigma * normal_quantile(p)).exp(),
310 Self::Uniform { low, high } => low + (high - low) * p.clamp(0.0, 1.0),
311 Self::Exponential { rate } => -(-p).ln_1p() / rate,
312 Self::Weibull { shape, scale } => scale * (-(-p).ln_1p()).powf(1.0 / shape),
313 Self::PowerLaw { xmin, alpha } => xmin * (1.0 - p).powf(-1.0 / (alpha - 1.0)),
314 Self::StudentsT {
315 df,
316 location,
317 scale,
318 } => location + scale * t_quantile(p, df),
319 Self::Gamma { .. } | Self::ChiSquared { .. } | Self::Beta { .. } => {
320 self.continuous_quantile_by_search(p)
321 }
322 Self::Poisson { .. }
323 | Self::Bernoulli { .. }
324 | Self::Binomial { .. }
325 | Self::Geometric { .. } => self.discrete_quantile(p),
326 }
327 }
328
329 fn continuous_quantile_by_search(&self, p: f64) -> f64 {
331 if p <= 0.0 {
332 return match self {
333 Self::Beta { .. } => 0.0,
334 _ => 0.0,
335 };
336 }
337 if p >= 1.0 {
338 return match self {
339 Self::Beta { .. } => 1.0,
340 _ => f64::INFINITY,
341 };
342 }
343 let (mut low, mut high) = match self {
344 Self::Beta { .. } => (0.0, 1.0),
345 _ => {
346 let mut high = 1.0;
347 while self.cdf(high) < p && high < 1e300 {
348 high *= 2.0;
349 }
350 (0.0, high)
351 }
352 };
353 for _ in 0..200 {
354 let mid = 0.5 * (low + high);
355 if self.cdf(mid) < p {
356 low = mid;
357 } else {
358 high = mid;
359 }
360 if high - low <= 1e-12 * high.abs().max(1e-300) {
361 break;
362 }
363 }
364 0.5 * (low + high)
365 }
366
367 fn discrete_quantile(&self, p: f64) -> f64 {
371 let start = match *self {
372 Self::Geometric { start, .. } => start as f64,
373 _ => 0.0,
374 };
375 if p <= 0.0 {
376 return start;
377 }
378 if p >= 1.0 {
379 return match *self {
380 Self::Bernoulli { .. } => 1.0,
381 Self::Binomial { trials, .. } => trials as f64,
382 _ => f64::INFINITY,
383 };
384 }
385 let (guess, spread) = match *self {
386 Self::Geometric { p: q, .. } => {
388 let k = ((-p).ln_1p() / (-q).ln_1p()).ceil() - 1.0;
389 (start + k.max(0.0), 1.0)
390 }
391 Self::Poisson { rate } => (rate + rate.sqrt() * normal_quantile(p), rate.sqrt()),
392 Self::Binomial { trials, p: q } => {
393 let n = trials as f64;
394 let sd = (n * q * (1.0 - q)).sqrt();
395 (n * q + sd * normal_quantile(p), sd)
396 }
397 _ => (start, 1.0),
398 };
399 let top = match *self {
400 Self::Bernoulli { .. } => 1.0,
401 Self::Binomial { trials, .. } => trials as f64,
402 _ => 1e15,
403 };
404 let guess = guess.floor().clamp(start, top);
405 let step = spread.max(1.0).ceil();
406 let (mut low, mut high);
409 if self.cdf(guess) >= p {
410 high = guess;
411 low = start - 1.0;
412 let mut step = step;
413 while high > start {
414 let next = (high - step).max(start);
415 if self.cdf(next) < p {
416 low = next;
417 break;
418 }
419 high = next;
420 step *= 2.0;
421 }
422 } else {
423 low = guess;
424 high = top;
425 let mut step = step;
426 while low < top {
427 let next = (low + step).min(top);
428 if self.cdf(next) >= p {
429 high = next;
430 break;
431 }
432 low = next;
433 step *= 2.0;
434 }
435 }
436 while high - low > 1.0 {
437 let mid = (low + (high - low) / 2.0).floor();
438 if self.cdf(mid) >= p {
439 high = mid;
440 } else {
441 low = mid;
442 }
443 }
444 high
445 }
446
447 fn ln_density(&self, x: f64) -> f64 {
450 match *self {
451 Self::Normal { mean, sd } => {
452 let z = (x - mean) / sd;
453 -0.5 * z * z - sd.ln() - 0.5 * LN_2PI
454 }
455 Self::LogNormal { mu, sigma } => {
456 if x <= 0.0 {
457 return f64::NEG_INFINITY;
458 }
459 let z = (x.ln() - mu) / sigma;
460 -0.5 * z * z - sigma.ln() - x.ln() - 0.5 * LN_2PI
461 }
462 Self::Uniform { low, high } => {
463 if x < low || x > high {
464 f64::NEG_INFINITY
465 } else {
466 -(high - low).ln()
467 }
468 }
469 Self::Exponential { rate } => {
470 if x < 0.0 {
471 f64::NEG_INFINITY
472 } else {
473 rate.ln() - rate * x
474 }
475 }
476 Self::Gamma { shape, scale } => gamma_ln_pdf(x, shape, scale),
477 Self::ChiSquared { df } => gamma_ln_pdf(x, df / 2.0, 2.0),
478 Self::Beta { alpha, beta } => {
479 if x <= 0.0 || x >= 1.0 {
480 return f64::NEG_INFINITY;
481 }
482 (alpha - 1.0) * x.ln() + (beta - 1.0) * (-x).ln_1p() - ln_beta(alpha, beta)
483 }
484 Self::StudentsT {
485 df,
486 location,
487 scale,
488 } => {
489 let t = (x - location) / scale;
490 ln_gamma((df + 1.0) / 2.0)
491 - ln_gamma(df / 2.0)
492 - 0.5 * (df * std::f64::consts::PI).ln()
493 - scale.ln()
494 - (df + 1.0) / 2.0 * (t * t / df).ln_1p()
495 }
496 Self::Weibull { shape, scale } => {
497 if x < 0.0 {
498 return f64::NEG_INFINITY;
499 }
500 let z = x / scale;
501 shape.ln() - scale.ln() + (shape - 1.0) * z.ln() - z.powf(shape)
502 }
503 Self::PowerLaw { xmin, alpha } => {
504 if x < xmin {
505 return f64::NEG_INFINITY;
506 }
507 (alpha - 1.0).ln() - xmin.ln() - alpha * (x / xmin).ln()
508 }
509 Self::Poisson { rate } => {
510 if x < 0.0 || x != x.floor() {
511 return f64::NEG_INFINITY;
512 }
513 x * rate.ln() - rate - ln_gamma(x + 1.0)
514 }
515 Self::Bernoulli { p } => match x {
516 0.0 => (-p).ln_1p(),
517 1.0 => p.ln(),
518 _ => f64::NEG_INFINITY,
519 },
520 Self::Binomial { trials, p } => {
521 let n = trials as f64;
522 if x < 0.0 || x > n || x != x.floor() {
523 return f64::NEG_INFINITY;
524 }
525 ln_gamma(n + 1.0) - ln_gamma(x + 1.0) - ln_gamma(n - x + 1.0)
526 + x * p.ln()
527 + (n - x) * (-p).ln_1p()
528 }
529 Self::Geometric { p, start } => {
530 let k = x - start as f64;
531 if k < 0.0 || x != x.floor() {
532 return f64::NEG_INFINITY;
533 }
534 p.ln() + k * (-p).ln_1p()
535 }
536 }
537 }
538
539 pub fn density(&self, x: f64) -> f64 {
541 self.ln_density(x).exp()
542 }
543
544 fn sample(&self, rng: &mut Rng) -> f64 {
545 match *self {
546 Self::Gamma { shape, scale } => rng.gamma(shape) * scale,
547 Self::ChiSquared { df } => rng.gamma(df / 2.0) * 2.0,
548 Self::Beta { alpha, beta } => {
549 let a = rng.gamma(alpha);
550 let b = rng.gamma(beta);
551 a / (a + b)
552 }
553 Self::StudentsT {
554 df,
555 location,
556 scale,
557 } => {
558 let z = rng.normal();
559 let v = rng.gamma(df / 2.0) * 2.0;
560 location + scale * z / (v / df).sqrt()
561 }
562 Self::Poisson { rate } if rate >= 10.0 => rng.poisson(rate),
566 Self::Binomial { trials, p } if trials as f64 * p.min(1.0 - p) >= 10.0 => {
567 rng.binomial(trials, p)
568 }
569 _ => self.quantile(rng.uniform()),
570 }
571 }
572
573 pub fn fit(family: DistributionType, values: &[f64]) -> Result<Fitted, &'static str> {
576 let n = values.len();
577 if n < 5 {
578 return Err("needs 5 or more values");
579 }
580 let nf = n as f64;
581 let mean = values.iter().sum::<f64>() / nf;
582 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nf - 1.0);
583 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
584 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
585 let counts = values.iter().all(|v| *v >= 0.0 && *v == v.floor());
586 if variance <= 0.0 || !variance.is_finite() {
587 return Err("needs values that vary");
588 }
589 let positive = || {
590 if min > 0.0 {
591 Ok(())
592 } else {
593 Err("needs positive values")
594 }
595 };
596 let fitted = match family {
597 DistributionType::Normal => Fitted::Normal {
598 mean,
599 sd: variance.sqrt(),
600 },
601 DistributionType::LogNormal => {
602 positive()?;
603 let logs: Vec<f64> = values.iter().map(|v| v.ln()).collect();
604 let mu = logs.iter().sum::<f64>() / nf;
605 let var = logs.iter().map(|v| (v - mu).powi(2)).sum::<f64>() / (nf - 1.0);
606 Fitted::LogNormal {
607 mu,
608 sigma: var.sqrt(),
609 }
610 }
611 DistributionType::Uniform => Fitted::Uniform {
612 low: min,
613 high: max,
614 },
615 DistributionType::Exponential => {
616 if min < 0.0 {
617 return Err("needs values of zero or more");
618 }
619 Fitted::Exponential { rate: 1.0 / mean }
620 }
621 DistributionType::Gamma => {
622 positive()?;
623 let mean_log = values.iter().map(|v| v.ln()).sum::<f64>() / nf;
625 let s = mean.ln() - mean_log;
626 if s <= 0.0 {
627 return Err("needs values that vary");
628 }
629 let shape = (3.0 - s + ((s - 3.0).powi(2) + 24.0 * s).sqrt()) / (12.0 * s);
630 Fitted::Gamma {
631 shape,
632 scale: mean / shape,
633 }
634 }
635 DistributionType::ChiSquared => {
636 positive()?;
637 Fitted::ChiSquared { df: mean }
638 }
639 DistributionType::Beta => {
640 if min <= 0.0 || max >= 1.0 {
641 return Err("needs values strictly between 0 and 1");
642 }
643 let common = mean * (1.0 - mean) / variance - 1.0;
644 if common <= 0.0 {
645 return Err("spread too wide for a beta");
646 }
647 beta_mle(values, mean * common, (1.0 - mean) * common)
651 }
652 DistributionType::StudentsT => {
653 let fourth = values.iter().map(|v| (v - mean).powi(4)).sum::<f64>() / nf;
654 let excess = fourth / (variance * variance) - 3.0;
655 if excess <= 0.0 {
656 return Err("tails no heavier than a normal's");
657 }
658 let df = (4.0 + 6.0 / excess).clamp(2.5, 1_000.0);
660 Fitted::StudentsT {
661 df,
662 location: mean,
663 scale: (variance * (df - 2.0) / df).sqrt(),
664 }
665 }
666 DistributionType::Weibull => {
667 positive()?;
668 weibull_mle(values).ok_or("no weibull fits these values")?
669 }
670 DistributionType::PowerLaw => {
671 positive()?;
672 let sum_log = values.iter().map(|v| (v / min).ln()).sum::<f64>();
673 if sum_log <= 0.0 {
674 return Err("needs values that vary");
675 }
676 Fitted::PowerLaw {
677 xmin: min,
678 alpha: 1.0 + nf / sum_log,
679 }
680 }
681 DistributionType::Poisson => {
682 if !counts {
683 return Err("needs counts: whole numbers, zero or more");
684 }
685 Fitted::Poisson { rate: mean }
686 }
687 DistributionType::Bernoulli => {
688 if !values.iter().all(|v| *v == 0.0 || *v == 1.0) {
689 return Err("needs only 0 and 1");
690 }
691 Fitted::Bernoulli { p: mean }
692 }
693 DistributionType::Binomial => {
694 if !counts {
695 return Err("needs counts: whole numbers, zero or more");
696 }
697 if variance >= mean {
698 return Err("spread too wide for a binomial");
699 }
700 let p = 1.0 - variance / mean;
702 let trials = ((mean / p).round() as u64).max(max as u64).max(1);
703 Fitted::Binomial {
704 trials,
705 p: (mean / trials as f64).clamp(1e-12, 1.0 - 1e-12),
706 }
707 }
708 DistributionType::Geometric => {
709 if !counts {
710 return Err("needs counts: whole numbers, zero or more");
711 }
712 let start = if min >= 1.0 { 1 } else { 0 };
715 Fitted::Geometric {
716 p: 1.0 / (1.0 + mean - start as f64),
717 start,
718 }
719 }
720 DistributionType::Constant | DistributionType::Unknown => {
721 return Err("not a distribution");
722 }
723 };
724 if fitted.valid() {
725 Ok(fitted)
726 } else {
727 Err("no fit for these values")
728 }
729 }
730
731 fn valid(&self) -> bool {
732 let finite_positive = |x: f64| x.is_finite() && x > 0.0;
733 match *self {
734 Self::Normal { mean, sd } => mean.is_finite() && finite_positive(sd),
735 Self::LogNormal { mu, sigma } => mu.is_finite() && finite_positive(sigma),
736 Self::Uniform { low, high } => low.is_finite() && high.is_finite() && high > low,
737 Self::Exponential { rate } => finite_positive(rate),
738 Self::Gamma { shape, scale } => finite_positive(shape) && finite_positive(scale),
739 Self::ChiSquared { df } => finite_positive(df),
740 Self::Beta { alpha, beta } => finite_positive(alpha) && finite_positive(beta),
741 Self::StudentsT {
742 df,
743 location,
744 scale,
745 } => finite_positive(df) && location.is_finite() && finite_positive(scale),
746 Self::Weibull { shape, scale } => finite_positive(shape) && finite_positive(scale),
747 Self::PowerLaw { xmin, alpha } => {
748 finite_positive(xmin) && alpha.is_finite() && alpha > 1.0
749 }
750 Self::Poisson { rate } => finite_positive(rate),
751 Self::Bernoulli { p } | Self::Geometric { p, .. } => p > 0.0 && p < 1.0,
752 Self::Binomial { trials, p } => trials >= 1 && p > 0.0 && p < 1.0,
753 }
754 }
755}
756
757fn beta_mle(values: &[f64], alpha: f64, beta: f64) -> Fitted {
760 let n = values.len() as f64;
761 let mean_log = values.iter().map(|v| v.ln()).sum::<f64>() / n;
762 let mean_log_rest = values.iter().map(|v| (-v).ln_1p()).sum::<f64>() / n;
763 let (mut a, mut b) = (alpha, beta);
764 for _ in 0..100 {
765 let both = digamma(a + b);
766 let g1 = digamma(a) - both - mean_log;
767 let g2 = digamma(b) - both - mean_log_rest;
768 let t = trigamma(a + b);
769 let (j11, j22, j12) = (trigamma(a) - t, trigamma(b) - t, -t);
770 let det = j11 * j22 - j12 * j12;
771 if det.abs() < 1e-300 {
772 break;
773 }
774 let da = (g1 * j22 - g2 * j12) / det;
775 let db = (g2 * j11 - g1 * j12) / det;
776 let mut step = 1.0;
778 while (a - step * da <= 0.0 || b - step * db <= 0.0) && step > 1e-6 {
779 step /= 2.0;
780 }
781 let (next_a, next_b) = (a - step * da, b - step * db);
782 let done = (next_a - a).abs() < 1e-10 * a && (next_b - b).abs() < 1e-10 * b;
783 a = next_a;
784 b = next_b;
785 if done {
786 break;
787 }
788 }
789 Fitted::Beta { alpha: a, beta: b }
790}
791
792fn digamma(mut x: f64) -> f64 {
795 let mut result = 0.0;
796 while x < 6.0 {
797 result -= 1.0 / x;
798 x += 1.0;
799 }
800 let inv = 1.0 / x;
801 let inv2 = inv * inv;
802 result + x.ln()
803 - 0.5 * inv
804 - inv2 * (1.0 / 12.0 - inv2 * (1.0 / 120.0 - inv2 * (1.0 / 252.0 - inv2 / 240.0)))
805}
806
807fn trigamma(mut x: f64) -> f64 {
809 let mut result = 0.0;
810 while x < 6.0 {
811 result += 1.0 / (x * x);
812 x += 1.0;
813 }
814 let inv = 1.0 / x;
815 let inv2 = inv * inv;
816 result
817 + inv
818 + 0.5 * inv2
819 + inv * inv2 * (1.0 / 6.0 - inv2 * (1.0 / 30.0 - inv2 * (1.0 / 42.0 - inv2 / 30.0)))
820}
821
822fn weibull_mle(values: &[f64]) -> Option<Fitted> {
824 let n = values.len() as f64;
825 let logs: Vec<f64> = values.iter().map(|v| v.ln()).collect();
826 let mean_log = logs.iter().sum::<f64>() / n;
827 let top = values.iter().copied().fold(0.0, f64::max);
829 let scaled: Vec<f64> = values.iter().map(|v| v / top).collect();
830 let scaled_logs: Vec<f64> = scaled.iter().map(|v| v.ln()).collect();
831 let mean_scaled_log = scaled_logs.iter().sum::<f64>() / n;
832 let sd_log = (logs.iter().map(|l| (l - mean_log).powi(2)).sum::<f64>() / n).sqrt();
833 let mut k = if sd_log > 0.0 { 1.2825 / sd_log } else { 1.0 };
835 for _ in 0..100 {
836 let (mut s0, mut s1, mut s2) = (0.0, 0.0, 0.0);
837 for (x, l) in scaled.iter().zip(&scaled_logs) {
838 let xk = x.powf(k);
839 s0 += xk;
840 s1 += xk * l;
841 s2 += xk * l * l;
842 }
843 if s0 <= 0.0 {
844 return None;
845 }
846 let f = s1 / s0 - 1.0 / k - mean_scaled_log;
847 let df = (s2 * s0 - s1 * s1) / (s0 * s0) + 1.0 / (k * k);
848 let step = f / df;
849 let next = (k - step).clamp(k / 2.0, k * 2.0);
850 let done = (next - k).abs() < 1e-10 * k;
851 k = next;
852 if done {
853 break;
854 }
855 }
856 let s0 = scaled.iter().map(|x| x.powf(k)).sum::<f64>();
857 let scale = top * (s0 / n).powf(1.0 / k);
858 Some(Fitted::Weibull { shape: k, scale })
859}
860
861pub fn ks_statistic(sorted: &[f64], fitted: &Fitted) -> f64 {
865 let n = sorted.len() as f64;
866 let mut d: f64 = 0.0;
867 let mut i = 0;
868 while i < sorted.len() {
869 let x = sorted[i];
870 let mut j = i;
871 while j < sorted.len() && sorted[j] == x {
872 j += 1;
873 }
874 let below = i as f64 / n;
875 let at = j as f64 / n;
876 d = d
877 .max((at - fitted.cdf(x)).abs())
878 .max((fitted.cdf_below(x) - below).abs());
879 i = j;
880 }
881 d
882}
883
884pub fn test_family(family: DistributionType, values: &[f64], seed: u64) -> FitOutcome {
886 let fitted = match Fitted::fit(family, values) {
887 Ok(fitted) => fitted,
888 Err(reason) => return FitOutcome::NotApplicable(reason),
889 };
890 let ln_likelihood: f64 = values.iter().map(|v| fitted.ln_density(*v)).sum();
891 let aic = 2.0 * fitted.parameters() as f64 - 2.0 * ln_likelihood;
892
893 let mut rng = Rng::new(seed ^ family as u64);
896 let mut subset = values.to_vec();
897 if subset.len() > TEST_VALUES {
898 for i in 0..TEST_VALUES {
899 let j = i + (rng.next_u64() % (subset.len() - i) as u64) as usize;
900 subset.swap(i, j);
901 }
902 subset.truncate(TEST_VALUES);
903 }
904 subset.sort_by(f64::total_cmp);
905 let Ok(sub_fit) = Fitted::fit(family, &subset) else {
906 return FitOutcome::NotApplicable("no fit for these values");
907 };
908 let observed = ks_statistic(&subset, &sub_fit);
909
910 let mut beyond = 0;
911 let mut simulated = vec![0.0; subset.len()];
912 for _ in 0..REPLICATES {
913 for value in simulated.iter_mut() {
914 *value = sub_fit.sample(&mut rng);
915 }
916 simulated.sort_by(f64::total_cmp);
917 let statistic = match Fitted::fit(family, &simulated) {
920 Ok(refit) => ks_statistic(&simulated, &refit),
921 Err(_) => f64::INFINITY,
922 };
923 if statistic >= observed {
924 beyond += 1;
925 }
926 }
927 FitOutcome::Tested(FitTest {
928 fitted,
929 p_value: (1 + beyond) as f64 / (1 + REPLICATES) as f64,
930 beyond,
931 replicates: REPLICATES,
932 tested_on: subset.len(),
933 aic,
934 })
935}
936
937pub fn test_all(values: &[f64], seed: u64) -> Vec<(DistributionType, FitOutcome)> {
940 std::thread::scope(|scope| {
941 let handles: Vec<_> = FAMILIES
942 .iter()
943 .map(|family| scope.spawn(move || (*family, test_family(*family, values, seed))))
944 .collect();
945 handles
946 .into_iter()
947 .filter_map(|handle| handle.join().ok())
948 .collect()
949 })
950}
951
952pub const REJECT_P: f64 = 0.01;
956
957fn nested_in(family: DistributionType) -> &'static [DistributionType] {
960 match family {
961 DistributionType::Beta => &[DistributionType::Uniform],
962 DistributionType::Gamma => &[DistributionType::Exponential, DistributionType::ChiSquared],
963 DistributionType::Weibull => &[DistributionType::Exponential],
964 DistributionType::StudentsT => &[DistributionType::Normal],
965 DistributionType::Binomial => &[DistributionType::Bernoulli, DistributionType::Poisson],
968 _ => &[],
969 }
970}
971
972pub fn select(results: &[(DistributionType, FitOutcome)], counts: bool) -> DistributionType {
976 let held: Vec<&FitTest> = results
977 .iter()
978 .filter_map(|(_, outcome)| outcome.test())
979 .filter(|test| test.p_value >= REJECT_P)
980 .collect();
981 let discrete_held = counts && held.iter().any(|test| test.fitted.discrete());
982 let best = held
983 .iter()
984 .filter(|test| test.fitted.discrete() == discrete_held)
985 .min_by(|a, b| a.aic.total_cmp(&b.aic));
986 let Some(best) = best else {
987 return DistributionType::Unknown;
988 };
989 let family = best.fitted.family();
990 nested_in(family)
994 .iter()
995 .find(|simpler| {
996 held.iter()
997 .any(|test| test.fitted.family() == **simpler && test.aic - best.aic < DECISIVE_AIC)
998 })
999 .copied()
1000 .unwrap_or(family)
1001}
1002
1003const DECISIVE_AIC: f64 = 10.0;
1005
1006pub fn listing_order(results: &[(DistributionType, FitOutcome)]) -> Vec<DistributionType> {
1009 let mut tested: Vec<(DistributionType, f64)> = results
1010 .iter()
1011 .filter_map(|(family, outcome)| outcome.p_value().map(|p| (*family, p)))
1012 .collect();
1013 tested.sort_by(|a, b| b.1.total_cmp(&a.1));
1014 let mut order: Vec<DistributionType> = tested.into_iter().map(|(family, _)| family).collect();
1015 let missing: Vec<DistributionType> = FAMILIES
1016 .iter()
1017 .filter(|family| !order.contains(family))
1018 .copied()
1019 .collect();
1020 order.extend(missing);
1021 order
1022}
1023
1024pub fn qq_quantiles(fitted: &Fitted, n: usize) -> Vec<f64> {
1027 (1..=n)
1028 .map(|i| fitted.quantile(i as f64 / (n as f64 + 1.0)))
1029 .collect()
1030}
1031
1032const LN_2PI: f64 = 1.837_877_066_409_345_5;
1033
1034pub fn ln_gamma(x: f64) -> f64 {
1037 const COEFFICIENTS: [f64; 9] = [
1038 0.999_999_999_999_809_9,
1039 676.520_368_121_885_1,
1040 -1_259.139_216_722_402_8,
1041 771.323_428_777_653_1,
1042 -176.615_029_162_140_6,
1043 12.507_343_278_686_905,
1044 -0.138_571_095_265_720_12,
1045 9.984_369_578_019_572e-6,
1046 1.505_632_735_149_311_6e-7,
1047 ];
1048 if x < 0.5 {
1049 let pi = std::f64::consts::PI;
1051 return (pi / (pi * x).sin()).ln() - ln_gamma(1.0 - x);
1052 }
1053 let x = x - 1.0;
1054 let mut sum = COEFFICIENTS[0];
1055 for (i, c) in COEFFICIENTS.iter().enumerate().skip(1) {
1056 sum += c / (x + i as f64);
1057 }
1058 let t = x + 7.5;
1059 0.5 * LN_2PI + (x + 0.5) * t.ln() - t + sum.ln()
1060}
1061
1062fn ln_beta(a: f64, b: f64) -> f64 {
1063 ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
1064}
1065
1066fn gamma_ln_pdf(x: f64, shape: f64, scale: f64) -> f64 {
1067 if x <= 0.0 {
1068 return f64::NEG_INFINITY;
1069 }
1070 (shape - 1.0) * x.ln() - x / scale - shape * scale.ln() - ln_gamma(shape)
1071}
1072
1073pub fn gamma_p(a: f64, x: f64) -> f64 {
1075 let (p, _) = incomplete_gamma(a, x);
1076 p
1077}
1078
1079pub fn gamma_q(a: f64, x: f64) -> f64 {
1082 let (_, q) = incomplete_gamma(a, x);
1083 q
1084}
1085
1086fn incomplete_gamma(a: f64, x: f64) -> (f64, f64) {
1090 if x <= 0.0 {
1091 return (0.0, 1.0);
1092 }
1093 if !x.is_finite() {
1094 return (1.0, 0.0);
1095 }
1096 let ln_prefix = a * x.ln() - x - ln_gamma(a);
1097 if x < a + 1.0 {
1098 let mut term = 1.0 / a;
1099 let mut sum = term;
1100 let mut ap = a;
1101 for _ in 0..10_000 {
1102 ap += 1.0;
1103 term *= x / ap;
1104 sum += term;
1105 if term.abs() < sum.abs() * 1e-16 {
1106 break;
1107 }
1108 }
1109 let p = (sum.ln() + ln_prefix).exp().clamp(0.0, 1.0);
1110 (p, 1.0 - p)
1111 } else {
1112 let tiny = 1e-300;
1114 let mut b = x + 1.0 - a;
1115 let mut c = 1.0 / tiny;
1116 let mut d = 1.0 / b;
1117 let mut h = d;
1118 for i in 1..10_000 {
1119 let an = -(i as f64) * (i as f64 - a);
1120 b += 2.0;
1121 d = an * d + b;
1122 if d.abs() < tiny {
1123 d = tiny;
1124 }
1125 c = b + an / c;
1126 if c.abs() < tiny {
1127 c = tiny;
1128 }
1129 d = 1.0 / d;
1130 let delta = d * c;
1131 h *= delta;
1132 if (delta - 1.0).abs() < 1e-16 {
1133 break;
1134 }
1135 }
1136 let q = (ln_prefix + h.ln()).exp().clamp(0.0, 1.0);
1137 (1.0 - q, q)
1138 }
1139}
1140
1141pub fn beta_inc(a: f64, b: f64, x: f64) -> f64 {
1143 if x <= 0.0 {
1144 return 0.0;
1145 }
1146 if x >= 1.0 {
1147 return 1.0;
1148 }
1149 let ln_front = a * x.ln() + b * (-x).ln_1p() - ln_beta(a, b);
1150 if x < (a + 1.0) / (a + b + 2.0) {
1151 (ln_front.exp() * beta_fraction(a, b, x) / a).clamp(0.0, 1.0)
1152 } else {
1153 (1.0 - ln_front.exp() * beta_fraction(b, a, 1.0 - x) / b).clamp(0.0, 1.0)
1154 }
1155}
1156
1157fn beta_fraction(a: f64, b: f64, x: f64) -> f64 {
1158 let tiny = 1e-300;
1159 let (qab, qap, qam) = (a + b, a + 1.0, a - 1.0);
1160 let mut c = 1.0;
1161 let mut d = 1.0 - qab * x / qap;
1162 if d.abs() < tiny {
1163 d = tiny;
1164 }
1165 d = 1.0 / d;
1166 let mut h = d;
1167 for m in 1..10_000 {
1168 let m = m as f64;
1169 let m2 = 2.0 * m;
1170 let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
1171 d = 1.0 + aa * d;
1172 if d.abs() < tiny {
1173 d = tiny;
1174 }
1175 c = 1.0 + aa / c;
1176 if c.abs() < tiny {
1177 c = tiny;
1178 }
1179 d = 1.0 / d;
1180 h *= d * c;
1181 let aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
1182 d = 1.0 + aa * d;
1183 if d.abs() < tiny {
1184 d = tiny;
1185 }
1186 c = 1.0 + aa / c;
1187 if c.abs() < tiny {
1188 c = tiny;
1189 }
1190 d = 1.0 / d;
1191 let delta = d * c;
1192 h *= delta;
1193 if (delta - 1.0).abs() < 1e-16 {
1194 break;
1195 }
1196 }
1197 h
1198}
1199
1200pub fn normal_cdf(z: f64) -> f64 {
1203 if z.is_nan() {
1204 return f64::NAN;
1205 }
1206 let tail = 0.5 * gamma_q(0.5, 0.5 * z * z);
1207 if z < 0.0 { tail } else { 1.0 - tail }
1208}
1209
1210pub fn normal_quantile(p: f64) -> f64 {
1214 if !(0.0..=1.0).contains(&p) {
1215 return f64::NAN;
1216 }
1217 if p == 0.0 {
1218 return f64::NEG_INFINITY;
1219 }
1220 if p == 1.0 {
1221 return f64::INFINITY;
1222 }
1223 if p == 0.5 {
1224 return 0.0;
1225 }
1226 const A: [f64; 6] = [
1227 -3.969_683_028_665_376e1,
1228 2.209_460_984_245_205e2,
1229 -2.759_285_104_469_687e2,
1230 1.383_577_518_672_69e2,
1231 -3.066_479_806_614_716e1,
1232 2.506_628_277_459_239,
1233 ];
1234 const B: [f64; 5] = [
1235 -5.447_609_879_822_406e1,
1236 1.615_858_368_580_409e2,
1237 -1.556_989_798_598_866e2,
1238 6.680_131_188_771_972e1,
1239 -1.328_068_155_288_572e1,
1240 ];
1241 const C: [f64; 6] = [
1242 -7.784_894_002_430_293e-3,
1243 -3.223_964_580_411_365e-1,
1244 -2.400_758_277_161_838,
1245 -2.549_732_539_343_734,
1246 4.374_664_141_464_968,
1247 2.938_163_982_698_783,
1248 ];
1249 const D: [f64; 4] = [
1250 7.784_695_709_041_462e-3,
1251 3.224_671_290_700_398e-1,
1252 2.445_134_137_142_996,
1253 3.754_408_661_907_416,
1254 ];
1255 let low = 0.02425;
1256 let x = if p < low {
1257 let q = (-2.0 * p.ln()).sqrt();
1258 (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
1259 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
1260 } else if p <= 1.0 - low {
1261 let q = p - 0.5;
1262 let r = q * q;
1263 (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
1264 / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
1265 } else {
1266 let q = (-2.0 * (-p).ln_1p()).sqrt();
1267 -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
1268 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
1269 };
1270 let e = normal_cdf(x) - p;
1272 let u = e * (2.0 * std::f64::consts::PI).sqrt() * (0.5 * x * x).exp();
1273 x - u / (1.0 + 0.5 * x * u)
1274}
1275
1276pub fn t_cdf(t: f64, df: f64) -> f64 {
1278 if t.is_nan() {
1279 return f64::NAN;
1280 }
1281 let tail = 0.5 * beta_inc(0.5 * df, 0.5, df / (df + t * t));
1282 if t > 0.0 { 1.0 - tail } else { tail }
1283}
1284
1285fn t_quantile(p: f64, df: f64) -> f64 {
1286 if p <= 0.0 {
1287 return f64::NEG_INFINITY;
1288 }
1289 if p >= 1.0 {
1290 return f64::INFINITY;
1291 }
1292 if p == 0.5 {
1293 return 0.0;
1294 }
1295 let mut high = 1.0;
1296 let target = p.max(1.0 - p);
1297 while t_cdf(high, df) < target && high < 1e300 {
1298 high *= 2.0;
1299 }
1300 let (mut low, mut hi) = (0.0, high);
1301 for _ in 0..200 {
1302 let mid = 0.5 * (low + hi);
1303 if t_cdf(mid, df) < target {
1304 low = mid;
1305 } else {
1306 hi = mid;
1307 }
1308 if hi - low <= 1e-13 * hi {
1309 break;
1310 }
1311 }
1312 let t = 0.5 * (low + hi);
1313 if p < 0.5 { -t } else { t }
1314}
1315
1316pub struct Rng(u64);
1319
1320impl Rng {
1321 pub fn new(seed: u64) -> Self {
1322 Self(seed)
1323 }
1324
1325 pub fn next_u64(&mut self) -> u64 {
1326 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
1327 let mut z = self.0;
1328 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1329 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1330 z ^ (z >> 31)
1331 }
1332
1333 pub fn uniform(&mut self) -> f64 {
1335 ((self.next_u64() >> 11) as f64 + 0.5) / (1u64 << 53) as f64
1336 }
1337
1338 pub fn normal(&mut self) -> f64 {
1339 normal_quantile(self.uniform())
1340 }
1341
1342 pub fn gamma(&mut self, shape: f64) -> f64 {
1344 if shape < 1.0 {
1345 let boost = self.uniform().powf(1.0 / shape);
1346 return self.gamma(shape + 1.0) * boost;
1347 }
1348 let d = shape - 1.0 / 3.0;
1349 let c = 1.0 / (9.0 * d).sqrt();
1350 loop {
1351 let x = self.normal();
1352 let v = 1.0 + c * x;
1353 if v <= 0.0 {
1354 continue;
1355 }
1356 let v = v * v * v;
1357 let u = self.uniform();
1358 if u.ln() < 0.5 * x * x + d - d * v + d * v.ln() {
1359 return d * v;
1360 }
1361 }
1362 }
1363
1364 pub fn poisson(&mut self, rate: f64) -> f64 {
1367 let ln_rate = rate.ln();
1368 let b = 0.931 + 2.53 * rate.sqrt();
1369 let a = -0.059 + 0.02483 * b;
1370 let ln_inv_alpha = (1.1239 + 1.1328 / (b - 3.4)).ln();
1371 let v_r = 0.9277 - 3.6224 / (b - 2.0);
1372 loop {
1373 let u = self.uniform() - 0.5;
1374 let v = self.uniform();
1375 let us = 0.5 - u.abs();
1376 let k = ((2.0 * a / us + b) * u + rate + 0.43).floor();
1377 if us >= 0.07 && v <= v_r {
1378 return k;
1379 }
1380 if k < 0.0 || (us < 0.013 && v > us) {
1381 continue;
1382 }
1383 if v.ln() + ln_inv_alpha - (a / (us * us) + b).ln()
1384 <= -rate + k * ln_rate - ln_gamma(k + 1.0)
1385 {
1386 return k;
1387 }
1388 }
1389 }
1390
1391 pub fn binomial(&mut self, trials: u64, p: f64) -> f64 {
1395 if p > 0.5 {
1396 return trials as f64 - self.binomial(trials, 1.0 - p);
1397 }
1398 let n = trials as f64;
1399 let q = 1.0 - p;
1400 let spq = (n * p * q).sqrt();
1401 let b = 1.15 + 2.53 * spq;
1402 let a = -0.0873 + 0.0248 * b + 0.01 * p;
1403 let c = n * p + 0.5;
1404 let v_r = 0.92 - 4.2 / b;
1405 let alpha = (2.83 + 5.1 / b) * spq;
1406 let ln_pq = (p / q).ln();
1407 let m = ((n + 1.0) * p).floor();
1408 let h = ln_gamma(m + 1.0) + ln_gamma(n - m + 1.0);
1409 loop {
1410 let u = self.uniform() - 0.5;
1411 let v = self.uniform();
1412 let us = 0.5 - u.abs();
1413 let k = ((2.0 * a / us + b) * u + c).floor();
1414 if k < 0.0 || k > n {
1415 continue;
1416 }
1417 if us >= 0.07 && v <= v_r {
1418 return k;
1419 }
1420 let v = (v * alpha / (a / (us * us) + b)).ln();
1421 if v <= h - ln_gamma(k + 1.0) - ln_gamma(n - k + 1.0) + (k - m) * ln_pq {
1422 return k;
1423 }
1424 }
1425 }
1426}
1427
1428#[cfg(test)]
1429mod tests {
1430 use super::*;
1431
1432 fn close(a: f64, b: f64, tolerance: f64) -> bool {
1433 (a - b).abs() <= tolerance * b.abs().max(1.0)
1434 }
1435
1436 #[test]
1438 fn normal_quantiles_match_reference_values() {
1439 for (p, z) in [
1440 (0.975, 1.959_963_984_540_054),
1441 (0.841_344_746_068_542_9, 1.0),
1442 (0.001, -3.090_232_306_167_813),
1443 (1e-10, -6.361_340_902_404_056),
1444 (0.3, -0.524_400_512_708_041),
1445 ] {
1446 assert!(
1447 close(normal_quantile(p), z, 1e-12),
1448 "{p}: {}",
1449 normal_quantile(p)
1450 );
1451 }
1452 assert_eq!(normal_quantile(0.5), 0.0);
1453 assert_eq!(normal_quantile(0.0), f64::NEG_INFINITY);
1454 assert_eq!(normal_quantile(1.0), f64::INFINITY);
1455 assert!(normal_quantile(1.5).is_nan());
1456 let grid: Vec<f64> = (1..1000)
1457 .map(|i| normal_quantile(i as f64 / 1000.0))
1458 .collect();
1459 assert!(grid.windows(2).all(|pair| pair[0] < pair[1]));
1460 for i in 1..500 {
1461 let p = i as f64 / 1000.0;
1462 assert!(close(normal_quantile(p), -normal_quantile(1.0 - p), 1e-12));
1463 }
1464 }
1465
1466 #[test]
1467 fn cdfs_match_reference_values() {
1468 assert!(close(normal_cdf(1.959_963_984_540_054), 0.975, 1e-14));
1469 assert!(close(normal_cdf(-8.0), 6.220_960_574_271_784e-16, 1e-9));
1470 assert!(close(t_cdf(2.015_048_372_669_157, 5.0), 0.95, 1e-10));
1472 let chi = Fitted::ChiSquared { df: 3.0 };
1474 assert!(close(chi.cdf(7.814_727_903_251_178), 0.95, 1e-10));
1475 let beta = Fitted::Beta {
1477 alpha: 2.0,
1478 beta: 5.0,
1479 };
1480 assert!(close(beta.cdf(0.5), 57.0 / 64.0, 1e-12));
1481 assert!(close(beta_inc(2.0, 3.0, 0.5), 0.6875, 1e-12));
1483 assert!(close(beta_inc(1.0, 1.0, 0.3), 0.3, 1e-12));
1484 assert!(close(t_cdf(2.5706, 5.0), 0.975, 1e-4));
1486 assert!(close(t_cdf(-2.5706, 5.0), 0.025, 1e-4));
1487 assert!(close(t_cdf(0.0, 5.0), 0.5, 1e-12));
1488 let poisson = Fitted::Poisson { rate: 3.0 };
1490 assert!(close(poisson.cdf(2.0), 8.5 * (-3.0f64).exp(), 1e-12));
1491 let binomial = Fitted::Binomial { trials: 10, p: 0.3 };
1493 assert!(close(binomial.cdf(3.0), 0.649_610_718_4, 1e-9));
1494 assert!(close(ln_gamma(0.5), 0.572_364_942_924_700_1, 1e-14));
1495 assert!(close(ln_gamma(10.0), 12.801_827_480_081_469, 1e-14));
1496 }
1497
1498 #[test]
1501 fn quantiles_invert_their_cdfs() {
1502 let fits = [
1503 Fitted::Normal { mean: 3.0, sd: 2.0 },
1504 Fitted::LogNormal {
1505 mu: 0.5,
1506 sigma: 0.8,
1507 },
1508 Fitted::Uniform {
1509 low: -1.0,
1510 high: 4.0,
1511 },
1512 Fitted::Exponential { rate: 2.0 },
1513 Fitted::Gamma {
1514 shape: 2.5,
1515 scale: 0.7,
1516 },
1517 Fitted::ChiSquared { df: 5.0 },
1518 Fitted::Beta {
1519 alpha: 2.0,
1520 beta: 5.0,
1521 },
1522 Fitted::StudentsT {
1523 df: 5.0,
1524 location: 1.0,
1525 scale: 2.0,
1526 },
1527 Fitted::Weibull {
1528 shape: 1.7,
1529 scale: 3.0,
1530 },
1531 Fitted::PowerLaw {
1532 xmin: 1.0,
1533 alpha: 2.5,
1534 },
1535 ];
1536 for fit in fits {
1537 let qs: Vec<f64> = (1..100).map(|i| fit.quantile(i as f64 / 100.0)).collect();
1538 assert!(qs.windows(2).all(|pair| pair[0] < pair[1]), "{fit:?}");
1539 for (i, q) in qs.iter().enumerate() {
1540 let p = (i + 1) as f64 / 100.0;
1541 assert!(
1542 close(fit.cdf(*q), p, 1e-8),
1543 "{fit:?} at {p}: {}",
1544 fit.cdf(*q)
1545 );
1546 }
1547 }
1548 let lognormal = Fitted::LogNormal {
1549 mu: 0.0,
1550 sigma: 1.0,
1551 };
1552 assert!(lognormal.quantile(1e-9) > 0.0);
1553 let discrete = [
1554 Fitted::Poisson { rate: 4.0 },
1555 Fitted::Binomial { trials: 20, p: 0.4 },
1556 Fitted::Geometric { p: 0.3, start: 1 },
1557 Fitted::Bernoulli { p: 0.3 },
1558 Fitted::Poisson { rate: 1e6 },
1561 Fitted::Binomial {
1562 trials: 2_000_000,
1563 p: 0.4,
1564 },
1565 Fitted::Geometric { p: 1e-6, start: 0 },
1566 ];
1567 for fit in discrete {
1568 for i in 1..100 {
1569 let p = i as f64 / 100.0;
1570 let k = fit.quantile(p);
1571 assert!(fit.cdf(k) >= p && fit.cdf(k - 1.0) < p, "{fit:?} at {p}");
1572 }
1573 }
1574 }
1575
1576 #[test]
1579 fn the_ks_statistic_is_two_sided() {
1580 let uniform = Fitted::Uniform {
1581 low: 0.0,
1582 high: 1.0,
1583 };
1584 assert!(close(ks_statistic(&[0.5, 0.6, 0.9], &uniform), 0.5, 1e-12));
1585 assert!(close(ks_statistic(&[0.1, 0.4, 0.7], &uniform), 0.3, 1e-12));
1586 let coin = Fitted::Bernoulli { p: 0.5 };
1588 assert!(close(ks_statistic(&[1.0, 1.0, 1.0], &coin), 0.5, 1e-12));
1589 }
1590
1591 fn draw(fit: Fitted, n: usize, seed: u64) -> Vec<f64> {
1592 let mut rng = Rng::new(seed);
1593 (0..n).map(|_| fit.sample(&mut rng)).collect()
1594 }
1595
1596 #[test]
1599 fn large_count_draws_follow_their_family() {
1600 let fits = [
1601 Fitted::Poisson { rate: 10.0 },
1602 Fitted::Poisson { rate: 37.5 },
1603 Fitted::Poisson { rate: 1e6 },
1604 Fitted::Binomial { trials: 40, p: 0.7 },
1605 Fitted::Binomial {
1606 trials: 2_000_000,
1607 p: 0.4,
1608 },
1609 ];
1610 for fit in fits {
1611 let mut values = draw(fit, 5_000, 9);
1612 assert!(values.iter().all(|v| *v >= 0.0 && *v == v.floor()));
1613 values.sort_by(f64::total_cmp);
1614 let d = ks_statistic(&values, &fit);
1616 assert!(d < 1.63 / (5_000f64).sqrt(), "{fit:?}: D = {d}");
1617 }
1618 }
1619
1620 #[test]
1623 fn the_bootstrap_p_value_is_calibrated() {
1624 let normal = Fitted::Normal {
1625 mean: 10.0,
1626 sd: 3.0,
1627 };
1628 let rejected = (0..40)
1629 .filter(|seed| {
1630 let values = draw(normal, 120, 1_000 + seed);
1631 test_family(DistributionType::Normal, &values, *seed)
1632 .p_value()
1633 .unwrap()
1634 < 0.05
1635 })
1636 .count();
1637 assert!(
1638 rejected <= 6,
1639 "{rejected} of 40 normal samples rejected as normal"
1640 );
1641
1642 let skewed = draw(
1643 Fitted::LogNormal {
1644 mu: 0.0,
1645 sigma: 1.0,
1646 },
1647 400,
1648 7,
1649 );
1650 let outcome = test_family(DistributionType::Normal, &skewed, 7);
1651 let test = outcome.test().unwrap();
1652 assert!(test.at_bound(), "{test:?}");
1653 assert!(test.p_value < 0.01);
1654 }
1655
1656 #[test]
1658 fn a_family_that_does_not_apply_has_no_p_value() {
1659 let values = draw(Fitted::Normal { mean: 0.0, sd: 1.0 }, 50, 3);
1660 let outcome = test_family(DistributionType::LogNormal, &values, 3);
1661 assert_eq!(outcome, FitOutcome::NotApplicable("needs positive values"));
1662 let results = vec![
1663 (DistributionType::LogNormal, outcome),
1664 (
1665 DistributionType::Normal,
1666 test_family(DistributionType::Normal, &values, 3),
1667 ),
1668 ];
1669 assert_eq!(
1670 listing_order(&results)[..2],
1671 [DistributionType::Normal, DistributionType::LogNormal]
1672 );
1673 }
1674
1675 #[test]
1678 fn selection_prefers_the_simpler_family_that_holds() {
1679 let values = draw(Fitted::Exponential { rate: 2.0 }, 2_000, 11);
1680 let results = test_all(&values, 11);
1681 assert_eq!(select(&results, false), DistributionType::Exponential);
1682
1683 let counts = draw(Fitted::Poisson { rate: 5.0 }, 2_000, 12);
1684 let results = test_all(&counts, 12);
1685 assert_eq!(select(&results, true), DistributionType::Poisson);
1686 }
1687}