1use u_numflow::distributions::{
21 BetaDistribution, Exponential, GammaDistribution, LogNormal, Normal, Sample, Weibull,
22};
23use u_numflow::special;
24use u_numflow::stats;
25
26pub fn ecdf(data: &[f64]) -> Option<(Vec<f64>, Vec<f64>)> {
47 if data.is_empty() || data.iter().any(|v| !v.is_finite()) {
48 return None;
49 }
50
51 let n = data.len() as f64;
52 let mut sorted: Vec<f64> = data.to_vec();
53 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
54
55 let mut values = Vec::new();
56 let mut probs = Vec::new();
57
58 let mut i = 0;
59 while i < sorted.len() {
60 let val = sorted[i];
61 let mut j = i;
63 while j < sorted.len() && (sorted[j] - val).abs() < 1e-300 {
64 j += 1;
65 }
66 values.push(val);
67 probs.push(j as f64 / n);
68 i = j;
69 }
70
71 Some((values, probs))
72}
73
74#[derive(Debug, Clone, Copy)]
80pub enum BinMethod {
81 Sturges,
83 Scott,
85 FreedmanDiaconis,
87 Fixed(usize),
89}
90
91#[derive(Debug, Clone)]
93pub struct HistogramBins {
94 pub n_bins: usize,
96 pub bin_width: f64,
98 pub edges: Vec<f64>,
100 pub counts: Vec<usize>,
102}
103
104pub fn histogram_bins(data: &[f64], method: BinMethod) -> Option<HistogramBins> {
122 let n = data.len();
123 if n < 2 || data.iter().any(|v| !v.is_finite()) {
124 return None;
125 }
126
127 let min_val = data.iter().cloned().reduce(f64::min)?;
128 let max_val = data.iter().cloned().reduce(f64::max)?;
129 let range = max_val - min_val;
130
131 if range < 1e-300 {
132 return None; }
134
135 let nf = n as f64;
136
137 let n_bins = match method {
138 BinMethod::Sturges => {
139 let k = (nf.log2()).ceil() as usize + 1;
140 k.max(2)
141 }
142 BinMethod::Scott => {
143 let sd = stats::std_dev(data)?;
144 if sd < 1e-300 {
145 return None;
146 }
147 let h = 3.49 * sd * nf.powf(-1.0 / 3.0);
148 ((range / h).ceil() as usize).max(2)
149 }
150 BinMethod::FreedmanDiaconis => {
151 let q1 = stats::quantile(data, 0.25)?;
152 let q3 = stats::quantile(data, 0.75)?;
153 let iqr = q3 - q1;
154 if iqr < 1e-300 {
155 let k = (nf.log2()).ceil() as usize + 1;
157 k.max(2)
158 } else {
159 let h = 2.0 * iqr * nf.powf(-1.0 / 3.0);
160 ((range / h).ceil() as usize).max(2)
161 }
162 }
163 BinMethod::Fixed(0) => return None,
166 BinMethod::Fixed(k) => k,
167 };
168
169 let bin_width = range / n_bins as f64;
170
171 let mut edges = Vec::with_capacity(n_bins + 1);
173 for i in 0..=n_bins {
174 edges.push(min_val + i as f64 * bin_width);
175 }
176
177 let mut counts = vec![0_usize; n_bins];
179 for &x in data {
180 let bin = ((x - min_val) / bin_width).floor() as usize;
181 let bin = bin.min(n_bins - 1); counts[bin] += 1;
183 }
184
185 Some(HistogramBins {
186 n_bins,
187 bin_width,
188 edges,
189 counts,
190 })
191}
192
193pub fn qq_plot_normal(data: &[f64]) -> Option<(Vec<f64>, Vec<f64>)> {
220 let n = data.len();
221 if n < 3 || data.iter().any(|v| !v.is_finite()) {
222 return None;
223 }
224
225 let mut sample: Vec<f64> = data.to_vec();
226 sample.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
227
228 let nf = n as f64;
229 let theoretical: Vec<f64> = (0..n)
230 .map(|i| {
231 let p = (i as f64 + 0.5) / nf;
232 special::inverse_normal_cdf(p)
233 })
234 .collect();
235
236 Some((theoretical, sample))
237}
238
239pub fn ks_test_normal(data: &[f64]) -> Option<(f64, f64)> {
265 let n = data.len();
266 if n < 5 || data.iter().any(|v| !v.is_finite()) {
267 return None;
268 }
269
270 let mean = stats::mean(data)?;
271 let sd = stats::std_dev(data)?;
272 if sd < 1e-300 {
273 return None;
274 }
275
276 let mut sorted: Vec<f64> = data.to_vec();
277 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
278
279 let nf = n as f64;
280 let mut d_stat = 0.0_f64;
281
282 for (i, &x) in sorted.iter().enumerate() {
283 let z = (x - mean) / sd;
284 let cdf = special::standard_normal_cdf(z);
285 let ecdf_above = (i + 1) as f64 / nf;
286 let ecdf_below = i as f64 / nf;
287 d_stat = d_stat.max((ecdf_above - cdf).abs());
288 d_stat = d_stat.max((ecdf_below - cdf).abs());
289 }
290
291 let lambda = (nf.sqrt() + 0.12 + 0.11 / nf.sqrt()) * d_stat;
294 let mut p_value = 0.0;
295 for k in 1..=100 {
296 let kf = k as f64;
297 let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
298 let term = sign * (-2.0 * kf * kf * lambda * lambda).exp();
299 p_value += term;
300 if term.abs() < 1e-15 {
301 break;
302 }
303 }
304 p_value = (2.0 * p_value).clamp(0.0, 1.0);
305
306 Some((d_stat, p_value))
307}
308
309#[derive(Debug, Clone, Copy)]
315pub enum BandwidthMethod {
316 Silverman,
322 Scott,
327 Manual(f64),
329}
330
331#[derive(Debug, Clone)]
333pub struct KdeResult {
334 pub x: Vec<f64>,
336 pub density: Vec<f64>,
338 pub bandwidth: f64,
340}
341
342pub fn kde(data: &[f64], method: BandwidthMethod, n_points: usize) -> Option<KdeResult> {
384 let n = data.len();
385 if n < 2 || n_points < 2 || data.iter().any(|v| !v.is_finite()) {
386 return None;
387 }
388
389 let bandwidth = kde_bandwidth(data, method)?;
390
391 let min_val = data.iter().cloned().reduce(f64::min)?;
393 let max_val = data.iter().cloned().reduce(f64::max)?;
394 let x_min = min_val - 3.0 * bandwidth;
395 let x_max = max_val + 3.0 * bandwidth;
396 let step = (x_max - x_min) / (n_points - 1) as f64;
397
398 let x: Vec<f64> = (0..n_points).map(|i| x_min + i as f64 * step).collect();
399
400 let inv_h = 1.0 / bandwidth;
402 let inv_nh = inv_h / n as f64;
403 let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
404
405 let density: Vec<f64> = x
406 .iter()
407 .map(|&xi| {
408 let sum: f64 = data
409 .iter()
410 .map(|&xj| {
411 let u = (xi - xj) * inv_h;
412 inv_sqrt_2pi * (-0.5 * u * u).exp()
413 })
414 .sum();
415 sum * inv_nh
416 })
417 .collect();
418
419 Some(KdeResult {
420 x,
421 density,
422 bandwidth,
423 })
424}
425
426pub fn kde_evaluate(data: &[f64], bandwidth: f64, x: f64) -> Option<f64> {
435 let n = data.len();
436 if n == 0 || bandwidth <= 0.0 || !bandwidth.is_finite() || !x.is_finite() {
437 return None;
438 }
439 if data.iter().any(|v| !v.is_finite()) {
440 return None;
441 }
442
443 let inv_h = 1.0 / bandwidth;
444 let inv_nh = inv_h / n as f64;
445 let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
446
447 let sum: f64 = data
448 .iter()
449 .map(|&xj| {
450 let u = (x - xj) * inv_h;
451 inv_sqrt_2pi * (-0.5 * u * u).exp()
452 })
453 .sum();
454
455 Some(sum * inv_nh)
456}
457
458pub fn kde_bandwidth(data: &[f64], method: BandwidthMethod) -> Option<f64> {
467 let n = data.len();
468 if n < 2 || data.iter().any(|v| !v.is_finite()) {
469 return None;
470 }
471
472 match method {
473 BandwidthMethod::Silverman => {
474 let sd = stats::std_dev(data)?;
475 if sd < 1e-300 {
476 return None;
477 }
478 let q1 = stats::quantile(data, 0.25)?;
479 let q3 = stats::quantile(data, 0.75)?;
480 let iqr = q3 - q1;
481 let spread = if iqr > 1e-300 { sd.min(iqr / 1.34) } else { sd };
482 Some(0.9 * spread * (n as f64).powf(-0.2))
483 }
484 BandwidthMethod::Scott => {
485 let sd = stats::std_dev(data)?;
486 if sd < 1e-300 {
487 return None;
488 }
489 Some(1.06 * sd * (n as f64).powf(-0.2))
490 }
491 BandwidthMethod::Manual(h) => {
492 if h <= 0.0 || !h.is_finite() {
493 None
494 } else {
495 Some(h)
496 }
497 }
498 }
499}
500
501#[derive(Debug, Clone)]
507pub struct FitResult {
508 pub distribution: String,
510 pub parameters: Vec<(String, f64)>,
512 pub log_likelihood: f64,
514 pub aic: f64,
516 pub bic: f64,
518 pub n_params: usize,
520}
521
522pub fn fit_normal(data: &[f64]) -> Option<FitResult> {
545 let n = data.len();
546 if n < 2 || data.iter().any(|v| !v.is_finite()) {
547 return None;
548 }
549
550 let nf = n as f64;
551 let mu = stats::mean(data)?;
552
553 let sum_sq: f64 = data.iter().map(|&x| (x - mu).powi(2)).sum();
555 let sigma_sq = sum_sq / nf;
556 if sigma_sq < 1e-300 {
557 return None;
558 }
559 let sigma = sigma_sq.sqrt();
560
561 let log_lik = -nf / 2.0 * (2.0 * std::f64::consts::PI).ln() - nf * sigma.ln() - nf / 2.0;
563
564 let k = 2;
565 let aic = -2.0 * log_lik + 2.0 * k as f64;
566 let bic = -2.0 * log_lik + k as f64 * nf.ln();
567
568 Some(FitResult {
569 distribution: "Normal".to_string(),
570 parameters: vec![("mu".to_string(), mu), ("sigma".to_string(), sigma)],
571 log_likelihood: log_lik,
572 aic,
573 bic,
574 n_params: k,
575 })
576}
577
578pub fn fit_exponential(data: &[f64]) -> Option<FitResult> {
599 let n = data.len();
600 if n < 2 || data.iter().any(|v| !v.is_finite()) {
601 return None;
602 }
603 if data.iter().any(|&v| v <= 0.0) {
605 return None;
606 }
607
608 let nf = n as f64;
609 let mean = stats::mean(data)?;
610 if mean < 1e-300 {
611 return None;
612 }
613
614 let lambda = 1.0 / mean;
615
616 let log_lik = nf * lambda.ln() - nf;
618
619 let k = 1;
620 let aic = -2.0 * log_lik + 2.0 * k as f64;
621 let bic = -2.0 * log_lik + k as f64 * nf.ln();
622
623 Some(FitResult {
624 distribution: "Exponential".to_string(),
625 parameters: vec![("lambda".to_string(), lambda)],
626 log_likelihood: log_lik,
627 aic,
628 bic,
629 n_params: k,
630 })
631}
632
633fn digamma(x: f64) -> f64 {
638 if x <= 0.0 {
639 return f64::NAN;
640 }
641
642 let mut val = 0.0;
644 let mut x = x;
645 while x < 8.0 {
646 val -= 1.0 / x;
647 x += 1.0;
648 }
649
650 let inv_x = 1.0 / x;
653 let inv_x2 = inv_x * inv_x;
654 val += x.ln()
655 - 0.5 * inv_x
656 - inv_x2 * (1.0 / 12.0 - inv_x2 * (1.0 / 120.0 - inv_x2 * (1.0 / 252.0)));
657
658 val
659}
660
661fn trigamma(x: f64) -> f64 {
665 if x <= 0.0 {
666 return f64::NAN;
667 }
668
669 let mut val = 0.0;
671 let mut x = x;
672 while x < 8.0 {
673 val += 1.0 / (x * x);
674 x += 1.0;
675 }
676
677 let inv_x = 1.0 / x;
680 let inv_x2 = inv_x * inv_x;
681 val += inv_x
682 + 0.5 * inv_x2
683 + inv_x2 * inv_x * (1.0 / 6.0 - inv_x2 * (1.0 / 30.0 - inv_x2 * (1.0 / 42.0)));
684
685 val
686}
687
688pub fn fit_gamma(data: &[f64]) -> Option<FitResult> {
717 let n = data.len();
718 if n < 2 || data.iter().any(|v| !v.is_finite()) {
719 return None;
720 }
721 if data.iter().any(|&v| v <= 0.0) {
723 return None;
724 }
725
726 let nf = n as f64;
727 let mean = stats::mean(data)?;
728 if mean < 1e-300 {
729 return None;
730 }
731
732 let var: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
734 if var < 1e-300 {
735 return None;
736 }
737
738 let sum_log: f64 = data.iter().map(|&x| x.ln()).sum();
739 let mean_log = sum_log / nf;
740 let target = mean.ln() - mean_log; let mut alpha = mean * mean / var;
744 if alpha < 1e-10 {
745 alpha = 0.5;
746 }
747
748 for _ in 0..200 {
750 let f = alpha.ln() - digamma(alpha) - target;
751 let f_prime = 1.0 / alpha - trigamma(alpha);
752 if f_prime.abs() < 1e-30 {
753 break;
754 }
755
756 let delta = f / f_prime;
757 alpha -= delta;
758
759 if alpha <= 0.0 {
761 alpha = 1e-10;
762 }
763
764 if delta.abs() < 1e-10 {
765 break;
766 }
767 }
768
769 if !alpha.is_finite() || alpha <= 0.0 {
770 return None;
771 }
772
773 let beta = alpha / mean; let sum_x: f64 = data.iter().sum();
777 let log_lik = nf * alpha * beta.ln() - nf * special::ln_gamma(alpha) + (alpha - 1.0) * sum_log
778 - beta * sum_x;
779
780 let k = 2;
781 let aic = -2.0 * log_lik + 2.0 * k as f64;
782 let bic = -2.0 * log_lik + k as f64 * nf.ln();
783
784 Some(FitResult {
785 distribution: "Gamma".to_string(),
786 parameters: vec![("alpha".to_string(), alpha), ("beta".to_string(), beta)],
787 log_likelihood: log_lik,
788 aic,
789 bic,
790 n_params: k,
791 })
792}
793
794pub fn fit_lognormal(data: &[f64]) -> Option<FitResult> {
818 let n = data.len();
819 if n < 2 || data.iter().any(|v| !v.is_finite()) {
820 return None;
821 }
822 if data.iter().any(|&v| v <= 0.0) {
823 return None;
824 }
825
826 let nf = n as f64;
827 let log_data: Vec<f64> = data.iter().map(|&x| x.ln()).collect();
828
829 let mu = log_data.iter().sum::<f64>() / nf;
830 let sigma_sq = log_data.iter().map(|&y| (y - mu).powi(2)).sum::<f64>() / nf;
831 if sigma_sq < 1e-300 {
832 return None;
833 }
834 let sigma = sigma_sq.sqrt();
835
836 let sum_log: f64 = log_data.iter().sum();
838 let log_lik =
839 -nf / 2.0 * (2.0 * std::f64::consts::PI).ln() - nf * sigma.ln() - nf / 2.0 - sum_log;
840
841 let k = 2;
842 let aic = -2.0 * log_lik + 2.0 * k as f64;
843 let bic = -2.0 * log_lik + k as f64 * nf.ln();
844
845 Some(FitResult {
846 distribution: "LogNormal".to_string(),
847 parameters: vec![("mu".to_string(), mu), ("sigma".to_string(), sigma)],
848 log_likelihood: log_lik,
849 aic,
850 bic,
851 n_params: k,
852 })
853}
854
855pub fn fit_poisson(data: &[f64]) -> Option<FitResult> {
879 let n = data.len();
880 if n < 2 || data.iter().any(|v| !v.is_finite()) {
881 return None;
882 }
883 if data.iter().any(|&v| v < 0.0 || v.fract() != 0.0) {
884 return None;
885 }
886
887 let nf = n as f64;
888 let lambda = stats::mean(data)?;
889 if lambda < 1e-300 {
890 return None; }
892
893 let sum_log_fact: f64 = data.iter().map(|&x| special::ln_gamma(x + 1.0)).sum();
896 let log_lik = nf * lambda * lambda.ln() - nf * lambda - sum_log_fact;
897
898 let k = 1;
899 let aic = -2.0 * log_lik + 2.0 * k as f64;
900 let bic = -2.0 * log_lik + k as f64 * nf.ln();
901
902 Some(FitResult {
903 distribution: "Poisson".to_string(),
904 parameters: vec![("lambda".to_string(), lambda)],
905 log_likelihood: log_lik,
906 aic,
907 bic,
908 n_params: k,
909 })
910}
911
912pub fn fit_beta(data: &[f64]) -> Option<FitResult> {
942 let n = data.len();
943 if n < 2 || data.iter().any(|v| !v.is_finite()) {
944 return None;
945 }
946 if data.iter().any(|&v| v <= 0.0 || v >= 1.0) {
948 return None;
949 }
950
951 let nf = n as f64;
952 let mean = stats::mean(data)?;
953 let var = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
954 if var < 1e-300 {
955 return None;
956 }
957
958 let sum_log_x: f64 = data.iter().map(|&x| x.ln()).sum();
959 let sum_log_1mx: f64 = data.iter().map(|&x| (1.0 - x).ln()).sum();
960 let mean_log_x = sum_log_x / nf;
961 let mean_log_1mx = sum_log_1mx / nf;
962
963 let factor = mean * (1.0 - mean) / var - 1.0;
965 if factor <= 0.0 {
966 return None;
967 }
968 let mut alpha = mean * factor;
969 let mut beta = (1.0 - mean) * factor;
970
971 if alpha < 0.01 {
972 alpha = 0.5;
973 }
974 if beta < 0.01 {
975 beta = 0.5;
976 }
977
978 for _ in 0..200 {
980 let psi_ab = digamma(alpha + beta);
981 let tri_ab = trigamma(alpha + beta);
982
983 let f1 = digamma(alpha) - psi_ab - mean_log_x;
984 let f2 = digamma(beta) - psi_ab - mean_log_1mx;
985
986 let j11 = trigamma(alpha) - tri_ab;
987 let j12 = -tri_ab;
988 let j21 = -tri_ab;
989 let j22 = trigamma(beta) - tri_ab;
990
991 let det = j11 * j22 - j12 * j21;
992 if det.abs() < 1e-30 {
993 break;
994 }
995
996 let da = (j22 * f1 - j12 * f2) / det;
997 let db = (j11 * f2 - j21 * f1) / det;
998
999 alpha -= da;
1000 beta -= db;
1001
1002 if alpha <= 0.0 {
1003 alpha = 1e-10;
1004 }
1005 if beta <= 0.0 {
1006 beta = 1e-10;
1007 }
1008
1009 if da.abs() < 1e-10 && db.abs() < 1e-10 {
1010 break;
1011 }
1012 }
1013
1014 if !alpha.is_finite() || !beta.is_finite() || alpha <= 0.0 || beta <= 0.0 {
1015 return None;
1016 }
1017
1018 let log_lik = nf
1021 * (special::ln_gamma(alpha + beta) - special::ln_gamma(alpha) - special::ln_gamma(beta))
1022 + (alpha - 1.0) * sum_log_x
1023 + (beta - 1.0) * sum_log_1mx;
1024
1025 let k = 2;
1026 let aic = -2.0 * log_lik + 2.0 * k as f64;
1027 let bic = -2.0 * log_lik + k as f64 * nf.ln();
1028
1029 Some(FitResult {
1030 distribution: "Beta".to_string(),
1031 parameters: vec![("alpha".to_string(), alpha), ("beta".to_string(), beta)],
1032 log_likelihood: log_lik,
1033 aic,
1034 bic,
1035 n_params: k,
1036 })
1037}
1038
1039pub fn fit_weibull(data: &[f64]) -> Option<FitResult> {
1059 if data.len() < 2 || data.iter().any(|&v| !(v.is_finite() && v > 0.0)) {
1060 return None;
1061 }
1062 let r = crate::weibull::weibull_mle(data)?;
1063 let nf = data.len() as f64;
1064 let k = 2;
1065 Some(FitResult {
1066 distribution: "Weibull".to_string(),
1067 parameters: vec![
1068 ("shape".to_string(), r.shape),
1069 ("scale".to_string(), r.scale),
1070 ],
1071 log_likelihood: r.log_likelihood,
1072 aic: -2.0 * r.log_likelihood + 2.0 * k as f64,
1073 bic: -2.0 * r.log_likelihood + k as f64 * nf.ln(),
1074 n_params: k,
1075 })
1076}
1077
1078pub fn fit_best(data: &[f64]) -> Vec<FitResult> {
1104 let mut results = Vec::new();
1105
1106 if let Some(r) = fit_normal(data) {
1107 results.push(r);
1108 }
1109 if let Some(r) = fit_exponential(data) {
1110 results.push(r);
1111 }
1112 if let Some(r) = fit_gamma(data) {
1113 results.push(r);
1114 }
1115 if let Some(r) = fit_lognormal(data) {
1116 results.push(r);
1117 }
1118 if let Some(r) = fit_weibull(data) {
1119 results.push(r);
1120 }
1121 if let Some(r) = fit_beta(data) {
1122 results.push(r);
1123 }
1124
1125 results.sort_by(|a, b| {
1126 a.aic
1127 .partial_cmp(&b.aic)
1128 .unwrap_or(std::cmp::Ordering::Equal)
1129 });
1130 results
1131}
1132
1133#[derive(Debug, Clone)]
1139enum Fitted {
1140 Normal(Normal),
1141 LogNormal(LogNormal),
1142 Weibull(Weibull),
1143 Gamma(GammaDistribution),
1144 Exponential(Exponential),
1145 Beta(BetaDistribution),
1146}
1147
1148impl Fitted {
1149 fn of(fit: &FitResult) -> Option<Self> {
1152 let param = |name: &str| {
1153 fit.parameters
1154 .iter()
1155 .find(|(n, _)| n == name)
1156 .map(|&(_, v)| v)
1157 };
1158 Some(match fit.distribution.as_str() {
1159 "Normal" => Self::Normal(Normal::new(param("mu")?, param("sigma")?).ok()?),
1160 "LogNormal" => Self::LogNormal(LogNormal::new(param("mu")?, param("sigma")?).ok()?),
1161 "Weibull" => Self::Weibull(Weibull::new(param("shape")?, param("scale")?).ok()?),
1162 "Gamma" => Self::Gamma(GammaDistribution::new(param("alpha")?, param("beta")?).ok()?),
1164 "Exponential" => Self::Exponential(Exponential::new(param("lambda")?).ok()?),
1165 "Beta" => Self::Beta(BetaDistribution::new(param("alpha")?, param("beta")?).ok()?),
1166 _ => return None,
1167 })
1168 }
1169
1170 fn refit(&self, data: &[f64]) -> Option<FitResult> {
1172 match self {
1173 Self::Normal(_) => fit_normal(data),
1174 Self::LogNormal(_) => fit_lognormal(data),
1175 Self::Weibull(_) => fit_weibull(data),
1176 Self::Gamma(_) => fit_gamma(data),
1177 Self::Exponential(_) => fit_exponential(data),
1178 Self::Beta(_) => fit_beta(data),
1179 }
1180 }
1181
1182 fn quantile(&self, p: f64) -> Option<f64> {
1183 match self {
1184 Self::Normal(d) => d.quantile(p),
1185 Self::LogNormal(d) => d.quantile(p),
1186 Self::Weibull(d) => d.quantile(p),
1187 Self::Gamma(d) => d.quantile(p),
1188 Self::Exponential(d) => d.quantile(p),
1189 Self::Beta(d) => d.quantile(p),
1190 }
1191 }
1192
1193 fn ln_tails(&self, x: f64) -> (f64, f64) {
1196 let (cdf, sf) = match self {
1197 Self::Normal(d) => {
1198 let z = (x - d.mu()) / d.sigma();
1199 (
1200 special::standard_normal_cdf(z),
1201 special::standard_normal_sf(z),
1202 )
1203 }
1204 Self::LogNormal(d) => {
1205 if x <= 0.0 {
1206 (0.0, 1.0)
1207 } else {
1208 let z = (x.ln() - d.mu()) / d.sigma();
1209 (
1210 special::standard_normal_cdf(z),
1211 special::standard_normal_sf(z),
1212 )
1213 }
1214 }
1215 Self::Weibull(d) => {
1216 if x <= 0.0 {
1217 (0.0, 1.0)
1218 } else {
1219 let h = (x / d.scale()).powf(d.shape());
1220 (-(-h).exp_m1(), (-h).exp())
1221 }
1222 }
1223 Self::Gamma(d) => {
1224 if x <= 0.0 {
1225 (0.0, 1.0)
1226 } else {
1227 let t = d.rate() * x;
1228 (
1229 special::regularized_lower_gamma(d.shape(), t),
1230 special::regularized_upper_gamma(d.shape(), t),
1231 )
1232 }
1233 }
1234 Self::Exponential(d) => (d.cdf(x), d.survival(x)),
1235 Self::Beta(d) => {
1236 if x <= 0.0 {
1237 (0.0, 1.0)
1238 } else if x >= 1.0 {
1239 (1.0, 0.0)
1240 } else {
1241 (
1242 special::regularized_incomplete_beta(x, d.alpha, d.beta),
1243 special::regularized_incomplete_beta(1.0 - x, d.beta, d.alpha),
1244 )
1245 }
1246 }
1247 };
1248 (
1249 cdf.max(f64::MIN_POSITIVE).ln(),
1250 sf.max(f64::MIN_POSITIVE).ln(),
1251 )
1252 }
1253
1254 fn sample_n(&self, n: usize, seed: u64) -> Vec<f64> {
1255 match self {
1256 Self::Normal(d) => d.sample_n(n, seed),
1257 Self::LogNormal(d) => d.sample_n(n, seed),
1258 Self::Weibull(d) => d.sample_n(n, seed),
1259 Self::Gamma(d) => d.sample_n(n, seed),
1260 Self::Exponential(d) => d.sample_n(n, seed),
1261 Self::Beta(d) => d.sample_n(n, seed),
1262 }
1263 }
1264
1265 fn anderson_darling(&self, data: &[f64]) -> f64 {
1267 let mut x = data.to_vec();
1268 x.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1269 let n = x.len();
1270 let tails: Vec<(f64, f64)> = x.iter().map(|&v| self.ln_tails(v)).collect();
1271 let s: f64 = (0..n)
1272 .map(|i| (2 * i + 1) as f64 * (tails[i].0 + tails[n - 1 - i].1))
1273 .sum();
1274 -(n as f64) - s / n as f64
1275 }
1276}
1277
1278pub(crate) fn fitted_quantile(fit: &FitResult, p: f64) -> Option<f64> {
1281 Fitted::of(fit)?.quantile(p)
1282}
1283
1284pub fn anderson_darling_fit(data: &[f64], fit: &FitResult) -> Option<f64> {
1315 if data.is_empty() || data.iter().any(|v| !v.is_finite()) {
1316 return None;
1317 }
1318 Some(Fitted::of(fit)?.anderson_darling(data))
1319}
1320
1321#[derive(Debug, Clone, Copy, PartialEq)]
1324pub struct GoodnessOfFit {
1325 pub statistic: f64,
1327 pub p_value: f64,
1332 pub replicates: usize,
1335}
1336
1337pub const DEFAULT_GOF_REPLICATES: usize = 199;
1341
1342pub fn goodness_of_fit(
1388 data: &[f64],
1389 fit: &FitResult,
1390 replicates: usize,
1391 seed: u64,
1392) -> Option<GoodnessOfFit> {
1393 if replicates == 0 || data.len() < 2 || data.iter().any(|v| !v.is_finite()) {
1394 return None;
1395 }
1396 let model = Fitted::of(fit)?;
1397 let statistic = model.anderson_darling(data);
1398 let n = data.len();
1399 let at_least = (0..replicates as u64)
1400 .filter(|&b| {
1401 let sample = model.sample_n(n, seed.wrapping_add(b));
1402 match model.refit(&sample).and_then(|f| Fitted::of(&f)) {
1405 Some(refitted) => refitted.anderson_darling(&sample) >= statistic,
1406 None => true,
1407 }
1408 })
1409 .count();
1410 Some(GoodnessOfFit {
1411 statistic,
1412 p_value: (1 + at_least) as f64 / (replicates + 1) as f64,
1413 replicates,
1414 })
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419 use super::*;
1420
1421 #[test]
1426 fn ecdf_basic() {
1427 let (vals, probs) = ecdf(&[3.0, 1.0, 2.0]).expect("should compute");
1428 assert_eq!(vals, vec![1.0, 2.0, 3.0]);
1429 assert!((probs[0] - 1.0 / 3.0).abs() < 1e-10);
1430 assert!((probs[1] - 2.0 / 3.0).abs() < 1e-10);
1431 assert!((probs[2] - 1.0).abs() < 1e-10);
1432 }
1433
1434 #[test]
1435 fn ecdf_with_ties() {
1436 let (vals, probs) = ecdf(&[1.0, 2.0, 2.0, 3.0]).expect("should compute");
1437 assert_eq!(vals, vec![1.0, 2.0, 3.0]);
1438 assert!((probs[0] - 0.25).abs() < 1e-10);
1439 assert!((probs[1] - 0.75).abs() < 1e-10); assert!((probs[2] - 1.0).abs() < 1e-10);
1441 }
1442
1443 #[test]
1444 fn ecdf_single() {
1445 let (vals, probs) = ecdf(&[5.0]).expect("should compute");
1446 assert_eq!(vals, vec![5.0]);
1447 assert!((probs[0] - 1.0).abs() < 1e-10);
1448 }
1449
1450 #[test]
1451 fn ecdf_empty() {
1452 assert!(ecdf(&[]).is_none());
1453 }
1454
1455 #[test]
1456 fn ecdf_nan() {
1457 assert!(ecdf(&[1.0, f64::NAN, 3.0]).is_none());
1458 }
1459
1460 #[test]
1465 fn hist_sturges_basic() {
1466 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1467 let r = histogram_bins(&data, BinMethod::Sturges).expect("should compute");
1468 assert!(r.n_bins >= 5);
1469 assert_eq!(r.edges.len(), r.n_bins + 1);
1470 assert_eq!(r.counts.len(), r.n_bins);
1471 let total: usize = r.counts.iter().sum();
1472 assert_eq!(total, 100);
1473 }
1474
1475 #[test]
1476 fn hist_scott() {
1477 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1478 let r = histogram_bins(&data, BinMethod::Scott).expect("should compute");
1479 assert!(r.n_bins >= 3);
1480 let total: usize = r.counts.iter().sum();
1481 assert_eq!(total, 100);
1482 }
1483
1484 #[test]
1485 fn hist_fd() {
1486 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1487 let r = histogram_bins(&data, BinMethod::FreedmanDiaconis).expect("should compute");
1488 assert!(r.n_bins >= 3);
1489 let total: usize = r.counts.iter().sum();
1490 assert_eq!(total, 100);
1491 }
1492
1493 #[test]
1494 fn hist_edge_cases() {
1495 assert!(histogram_bins(&[1.0], BinMethod::Sturges).is_none()); assert!(histogram_bins(&[5.0, 5.0, 5.0], BinMethod::Sturges).is_none());
1497 }
1499
1500 #[test]
1501 fn hist_fixed_exact_count() {
1502 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1503 let r = histogram_bins(&data, BinMethod::Fixed(5)).expect("should compute");
1504 assert_eq!(r.n_bins, 5);
1505 assert_eq!(r.edges.len(), 6);
1506 assert_eq!(r.counts.len(), 5);
1507 let total: usize = r.counts.iter().sum();
1508 assert_eq!(total, 100);
1509 }
1510
1511 #[test]
1512 fn hist_fixed_single_bin() {
1513 let data = [1.0, 2.0, 3.0];
1514 let r = histogram_bins(&data, BinMethod::Fixed(1)).expect("should compute");
1515 assert_eq!(r.n_bins, 1);
1516 assert_eq!(r.counts, vec![3]);
1517 }
1518
1519 #[test]
1520 fn hist_fixed_zero_is_invalid() {
1521 let data = [1.0, 2.0, 3.0];
1522 assert!(histogram_bins(&data, BinMethod::Fixed(0)).is_none());
1523 }
1524
1525 #[test]
1526 fn hist_fixed_shares_input_guards() {
1527 assert!(histogram_bins(&[1.0], BinMethod::Fixed(5)).is_none()); assert!(histogram_bins(&[5.0, 5.0, 5.0], BinMethod::Fixed(5)).is_none());
1529 }
1531
1532 #[test]
1537 fn qq_basic() {
1538 let data = [-1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5];
1539 let (theo, samp) = qq_plot_normal(&data).expect("should compute");
1540 assert_eq!(theo.len(), 7);
1541 assert_eq!(samp.len(), 7);
1542 assert!((samp[0] + 1.5).abs() < 1e-10);
1544 assert!((samp[6] - 1.5).abs() < 1e-10);
1545 assert!(theo[3].abs() < 0.1);
1547 }
1548
1549 #[test]
1550 fn qq_edge_cases() {
1551 assert!(qq_plot_normal(&[1.0, 2.0]).is_none()); }
1553
1554 #[test]
1559 fn ks_normal_data() {
1560 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5];
1562 let (d, p) = ks_test_normal(&data).expect("should compute");
1563 assert!(d > 0.0 && d < 1.0, "D = {d}");
1564 assert!(p > 0.05, "p = {p}");
1565 }
1566
1567 #[test]
1568 fn ks_non_normal_data() {
1569 let data: Vec<f64> = (0..50).map(|i| i as f64 * 2.0).collect();
1571 let (d, _p) = ks_test_normal(&data).expect("should compute");
1572 assert!(d > 0.0);
1573 }
1574
1575 #[test]
1576 fn ks_edge_cases() {
1577 assert!(ks_test_normal(&[1.0, 2.0, 3.0, 4.0]).is_none()); assert!(ks_test_normal(&[5.0, 5.0, 5.0, 5.0, 5.0]).is_none()); }
1580
1581 #[test]
1586 fn kde_silverman_basic() {
1587 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1588 let r = kde(&data, BandwidthMethod::Silverman, 256).expect("should compute");
1589 assert_eq!(r.x.len(), 256);
1590 assert_eq!(r.density.len(), 256);
1591 assert!(r.bandwidth > 0.0);
1592 assert!(r.density.iter().all(|&d| d >= 0.0));
1594 let dx = r.x[1] - r.x[0];
1596 let integral: f64 = r.density.iter().sum::<f64>() * dx;
1597 assert!(
1598 (integral - 1.0).abs() < 0.05,
1599 "integral = {integral}, expected ≈ 1.0"
1600 );
1601 }
1602
1603 #[test]
1604 fn kde_scott_basic() {
1605 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1606 let r = kde(&data, BandwidthMethod::Scott, 256).expect("should compute");
1607 assert!(r.bandwidth > 0.0);
1608 let dx = r.x[1] - r.x[0];
1609 let integral: f64 = r.density.iter().sum::<f64>() * dx;
1610 assert!(
1611 (integral - 1.0).abs() < 0.05,
1612 "integral = {integral}, expected ≈ 1.0"
1613 );
1614 }
1615
1616 #[test]
1617 fn kde_manual_bandwidth() {
1618 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1619 let r = kde(&data, BandwidthMethod::Manual(0.5), 128).expect("should compute");
1620 assert!((r.bandwidth - 0.5).abs() < 1e-10);
1621 assert_eq!(r.x.len(), 128);
1622 }
1623
1624 #[test]
1625 fn kde_edge_cases() {
1626 assert!(kde(&[1.0], BandwidthMethod::Silverman, 256).is_none());
1628 assert!(kde(&[], BandwidthMethod::Silverman, 256).is_none());
1630 assert!(kde(&[1.0, f64::NAN], BandwidthMethod::Silverman, 256).is_none());
1632 assert!(kde(&[5.0, 5.0, 5.0], BandwidthMethod::Silverman, 256).is_none());
1634 assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(1.0), 1).is_none());
1636 assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(0.0), 256).is_none());
1638 assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(-1.0), 256).is_none());
1639 }
1640
1641 #[test]
1642 fn kde_bimodal() {
1643 let mut data = Vec::new();
1645 for i in 0..50 {
1646 data.push(i as f64 * 0.1); }
1648 for i in 0..50 {
1649 data.push(10.0 + i as f64 * 0.1); }
1651 let r = kde(&data, BandwidthMethod::Silverman, 512).expect("should compute");
1652 let idx_cluster = r.x.iter().position(|&x| x >= 2.5).expect("grid point");
1655 let idx_valley = r.x.iter().position(|&x| x >= 7.5).expect("grid point");
1656 assert!(
1657 r.density[idx_cluster] > r.density[idx_valley],
1658 "cluster density ({}) should exceed valley density ({})",
1659 r.density[idx_cluster],
1660 r.density[idx_valley]
1661 );
1662 }
1663
1664 #[test]
1665 fn kde_evaluate_single() {
1666 let data = [0.0, 1.0, 2.0, 3.0, 4.0];
1667 let h = 1.0;
1668 let d = kde_evaluate(&data, h, 2.0).expect("should compute");
1669 assert!(d > 0.0);
1670 let d_edge = kde_evaluate(&data, h, -5.0).expect("should compute");
1672 assert!(d > d_edge, "center density ({d}) > edge density ({d_edge})");
1673 }
1674
1675 #[test]
1676 fn kde_evaluate_edge_cases() {
1677 assert!(kde_evaluate(&[], 1.0, 0.0).is_none());
1678 assert!(kde_evaluate(&[1.0], 0.0, 0.0).is_none());
1679 assert!(kde_evaluate(&[1.0], -1.0, 0.0).is_none());
1680 assert!(kde_evaluate(&[1.0], 1.0, f64::NAN).is_none());
1681 assert!(kde_evaluate(&[f64::NAN], 1.0, 0.0).is_none());
1682 }
1683
1684 #[test]
1685 fn kde_bandwidth_methods() {
1686 let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
1687 let h_silv = kde_bandwidth(&data, BandwidthMethod::Silverman).expect("silverman");
1688 let h_scott = kde_bandwidth(&data, BandwidthMethod::Scott).expect("scott");
1689 let h_manual = kde_bandwidth(&data, BandwidthMethod::Manual(2.5)).expect("manual");
1690 assert!(h_silv > 0.0);
1691 assert!(h_scott > 0.0);
1692 assert!((h_manual - 2.5).abs() < 1e-10);
1693 assert!(h_scott > h_silv);
1696 }
1697
1698 #[test]
1699 fn kde_bandwidth_edge_cases() {
1700 assert!(kde_bandwidth(&[1.0], BandwidthMethod::Silverman).is_none());
1701 assert!(kde_bandwidth(&[5.0, 5.0], BandwidthMethod::Silverman).is_none());
1702 assert!(kde_bandwidth(&[1.0, 2.0], BandwidthMethod::Manual(0.0)).is_none());
1703 }
1704
1705 #[test]
1706 fn kde_grid_extends_beyond_data() {
1707 let data = [10.0, 20.0, 30.0];
1708 let r = kde(&data, BandwidthMethod::Manual(2.0), 64).expect("should compute");
1709 assert!(r.x[0] < 10.0 - 2.0, "grid starts at {}", r.x[0]);
1711 assert!(
1712 *r.x.last().expect("non-empty") > 30.0 + 2.0,
1713 "grid ends at {}",
1714 r.x.last().expect("non-empty")
1715 );
1716 }
1717
1718 #[test]
1723 fn fit_normal_basic() {
1724 let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1725 let r = fit_normal(&data).expect("should compute");
1726 assert_eq!(r.distribution, "Normal");
1727 assert_eq!(r.n_params, 2);
1728 assert!((r.parameters[0].1 - 5.0).abs() < 1e-10, "μ̂ = 5.0");
1729 let expected_sigma = (32.0_f64 / 8.0).sqrt();
1732 assert!(
1733 (r.parameters[1].1 - expected_sigma).abs() < 1e-10,
1734 "σ̂ = {}, expected {}",
1735 r.parameters[1].1,
1736 expected_sigma
1737 );
1738 assert!(r.log_likelihood.is_finite());
1739 assert!(r.aic.is_finite());
1740 assert!(r.bic.is_finite());
1741 }
1742
1743 #[test]
1744 fn fit_normal_edge_cases() {
1745 assert!(fit_normal(&[1.0]).is_none()); assert!(fit_normal(&[]).is_none());
1747 assert!(fit_normal(&[1.0, f64::NAN]).is_none());
1748 assert!(fit_normal(&[5.0, 5.0, 5.0]).is_none()); }
1750
1751 #[test]
1752 fn fit_exponential_basic() {
1753 let data = [0.5, 1.2, 0.8, 2.1, 0.3, 1.5, 0.9, 1.8];
1754 let r = fit_exponential(&data).expect("should compute");
1755 assert_eq!(r.distribution, "Exponential");
1756 assert_eq!(r.n_params, 1);
1757 let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1758 let expected_lambda = 1.0 / mean;
1759 assert!(
1760 (r.parameters[0].1 - expected_lambda).abs() < 1e-10,
1761 "λ̂ = {}, expected {}",
1762 r.parameters[0].1,
1763 expected_lambda
1764 );
1765 }
1766
1767 #[test]
1768 fn fit_exponential_edge_cases() {
1769 assert!(fit_exponential(&[1.0]).is_none()); assert!(fit_exponential(&[1.0, -1.0]).is_none()); assert!(fit_exponential(&[0.0, 1.0]).is_none()); assert!(fit_exponential(&[1.0, f64::NAN]).is_none());
1773 }
1774
1775 #[test]
1776 fn fit_gamma_basic() {
1777 let data = [
1779 1.5, 2.1, 1.8, 2.5, 1.9, 2.3, 2.0, 1.7, 2.4, 2.2, 1.6, 2.6, 1.4, 2.8, 2.1, 1.9, 2.3,
1780 2.0, 1.8, 2.5,
1781 ];
1782 let r = fit_gamma(&data).expect("should compute");
1783 assert_eq!(r.distribution, "Gamma");
1784 assert_eq!(r.n_params, 2);
1785 assert!(r.parameters[0].1 > 0.0, "α̂ > 0");
1786 assert!(r.parameters[1].1 > 0.0, "β̂ > 0");
1787 assert!(r.log_likelihood.is_finite());
1788 }
1789
1790 #[test]
1791 fn fit_gamma_edge_cases() {
1792 assert!(fit_gamma(&[1.0]).is_none()); assert!(fit_gamma(&[1.0, -1.0]).is_none()); assert!(fit_gamma(&[0.0, 1.0]).is_none()); assert!(fit_gamma(&[1.0, f64::NAN]).is_none());
1796 assert!(fit_gamma(&[5.0, 5.0, 5.0]).is_none()); }
1798
1799 #[test]
1800 fn fit_gamma_mean_rate_relationship() {
1801 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1802 let r = fit_gamma(&data).expect("should compute");
1803 let alpha = r.parameters[0].1;
1804 let beta = r.parameters[1].1;
1805 let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1806 assert!(
1808 (alpha / beta - mean).abs() < 1e-6,
1809 "α/β = {}, mean = {}",
1810 alpha / beta,
1811 mean
1812 );
1813 }
1814
1815 #[test]
1816 fn fit_best_sorted_by_aic() {
1817 let data = [2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7];
1818 let fits = fit_best(&data);
1819 assert!(!fits.is_empty());
1820 assert!(fits.len() >= 2, "got {} fits", fits.len());
1822 for w in fits.windows(2) {
1824 assert!(
1825 w[0].aic <= w[1].aic,
1826 "AIC not sorted: {} > {}",
1827 w[0].aic,
1828 w[1].aic
1829 );
1830 }
1831 }
1832
1833 #[test]
1834 fn fit_best_mixed_sign_data() {
1835 let data = [-2.0, -1.0, 0.5, 1.0, 2.0, 3.0, 1.5, -0.5];
1837 let fits = fit_best(&data);
1838 assert_eq!(fits.len(), 1, "only Normal should fit");
1839 assert_eq!(fits[0].distribution, "Normal");
1840 }
1841
1842 #[test]
1843 fn digamma_known_values() {
1844 assert!((digamma(1.0) + 0.5772156649).abs() < 1e-8);
1846 assert!((digamma(2.0) - 0.4227843351).abs() < 1e-8);
1848 assert!((digamma(0.5) + 1.9635100260).abs() < 1e-7);
1850 }
1851
1852 #[test]
1853 fn trigamma_known_values() {
1854 assert!((trigamma(1.0) - std::f64::consts::PI.powi(2) / 6.0).abs() < 1e-7);
1856 assert!((trigamma(2.0) - (std::f64::consts::PI.powi(2) / 6.0 - 1.0)).abs() < 1e-7);
1858 }
1859
1860 #[test]
1865 fn fit_lognormal_basic() {
1866 let data = [1.2, 2.5, 1.8, 3.1, 0.9, 2.0, 1.5, 4.0];
1867 let r = fit_lognormal(&data).expect("should compute");
1868 assert_eq!(r.distribution, "LogNormal");
1869 assert_eq!(r.n_params, 2);
1870 assert!(r.parameters[1].1 > 0.0); assert!(r.log_likelihood.is_finite());
1872 }
1873
1874 #[test]
1875 fn fit_lognormal_params_match_log_transform() {
1876 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1877 let r = fit_lognormal(&data).expect("should compute");
1878 let mu = r.parameters[0].1;
1879 let sigma = r.parameters[1].1;
1880 let log_data: Vec<f64> = data.iter().map(|&x| x.ln()).collect();
1882 let expected_mu: f64 = log_data.iter().sum::<f64>() / 5.0;
1883 assert!(
1884 (mu - expected_mu).abs() < 1e-10,
1885 "μ = {}, expected {}",
1886 mu,
1887 expected_mu
1888 );
1889 assert!(sigma > 0.0);
1890 }
1891
1892 #[test]
1893 fn fit_lognormal_edge_cases() {
1894 assert!(fit_lognormal(&[1.0]).is_none()); assert!(fit_lognormal(&[1.0, -1.0]).is_none()); assert!(fit_lognormal(&[0.0, 1.0]).is_none()); }
1898
1899 #[test]
1904 fn fit_poisson_basic() {
1905 let data = [2.0, 3.0, 1.0, 4.0, 2.0, 3.0, 1.0, 2.0];
1906 let r = fit_poisson(&data).expect("should compute");
1907 assert_eq!(r.distribution, "Poisson");
1908 assert_eq!(r.n_params, 1);
1909 let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1910 assert!(
1911 (r.parameters[0].1 - mean).abs() < 1e-10,
1912 "λ̂ = {}, expected {}",
1913 r.parameters[0].1,
1914 mean
1915 );
1916 }
1917
1918 #[test]
1919 fn fit_poisson_edge_cases() {
1920 assert!(fit_poisson(&[1.0]).is_none()); assert!(fit_poisson(&[1.0, -1.0]).is_none()); assert!(fit_poisson(&[0.0, 0.0]).is_none()); assert!(fit_poisson(&[1.0, 2.5]).is_none()); }
1925
1926 #[test]
1931 fn fit_beta_basic() {
1932 let data = [0.2, 0.35, 0.5, 0.15, 0.45, 0.3, 0.6, 0.25, 0.4, 0.55];
1933 let r = fit_beta(&data).expect("should compute");
1934 assert_eq!(r.distribution, "Beta");
1935 assert_eq!(r.n_params, 2);
1936 assert!(r.parameters[0].1 > 0.0, "α̂ > 0");
1937 assert!(r.parameters[1].1 > 0.0, "β̂ > 0");
1938 assert!(r.log_likelihood.is_finite());
1939 }
1940
1941 #[test]
1942 fn fit_beta_symmetric() {
1943 let data = [0.3, 0.4, 0.5, 0.6, 0.7, 0.35, 0.45, 0.55, 0.65, 0.5];
1945 let r = fit_beta(&data).expect("should compute");
1946 let alpha = r.parameters[0].1;
1947 let beta = r.parameters[1].1;
1948 assert!(
1950 (alpha - beta).abs() / alpha < 0.3,
1951 "α = {alpha}, β = {beta}, should be roughly equal"
1952 );
1953 }
1954
1955 #[test]
1956 fn fit_beta_edge_cases() {
1957 assert!(fit_beta(&[0.5]).is_none()); assert!(fit_beta(&[0.5, 1.0]).is_none()); assert!(fit_beta(&[0.0, 0.5]).is_none()); assert!(fit_beta(&[0.5, 1.5]).is_none()); assert!(fit_beta(&[0.5, -0.5]).is_none()); }
1963
1964 #[test]
1969 fn fit_best_compares_the_continuous_families_that_apply() {
1970 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1971 let fits = fit_best(&data);
1972 let names: Vec<&str> = fits.iter().map(|f| f.distribution.as_str()).collect();
1973 for family in ["Normal", "Exponential", "Gamma", "LogNormal", "Weibull"] {
1975 assert!(
1976 names.contains(&family),
1977 "should include {family}: {names:?}"
1978 );
1979 }
1980 assert!(!names.contains(&"Beta"), "{names:?}");
1981 assert!(!names.contains(&"Poisson"), "{names:?}");
1984
1985 let proportions = [0.2, 0.35, 0.5, 0.15, 0.45, 0.3, 0.6, 0.25, 0.4, 0.55];
1986 let names: Vec<String> = fit_best(&proportions)
1987 .into_iter()
1988 .map(|f| f.distribution)
1989 .collect();
1990 assert!(names.iter().any(|n| n == "Beta"), "{names:?}");
1991 }
1992
1993 #[test]
1997 fn fit_best_ranking_does_not_depend_on_the_unit() {
1998 let data = [2.0, 3.0, 1.0, 4.0, 2.0, 3.0, 5.0, 2.0, 3.0, 4.0];
1999 let in_thousandths: Vec<f64> = data.iter().map(|v| v * 1000.0).collect();
2000 let order = |d: &[f64]| -> Vec<String> {
2001 fit_best(d).into_iter().map(|f| f.distribution).collect()
2002 };
2003 assert_eq!(order(&data), order(&in_thousandths));
2004 }
2005
2006 #[test]
2007 fn fit_weibull_matches_the_weibull_mle() {
2008 let data = [12.0, 25.0, 31.0, 44.0, 52.0, 60.0, 71.0, 85.0];
2009 let fit = fit_weibull(&data).expect("positive data");
2010 let mle = crate::weibull::weibull_mle(&data).expect("same data");
2011 assert_eq!(fit.parameters[0], ("shape".to_string(), mle.shape));
2012 assert_eq!(fit.parameters[1], ("scale".to_string(), mle.scale));
2013 assert!((fit.aic - (-2.0 * mle.log_likelihood + 4.0)).abs() < 1e-12);
2014 assert!(fit_weibull(&[1.0, 0.0, 2.0]).is_none());
2015 }
2016}
2017
2018#[cfg(test)]
2019mod proptests {
2020 use super::*;
2021 use proptest::prelude::*;
2022
2023 proptest! {
2024 #[test]
2025 fn ecdf_last_is_one(
2026 data in proptest::collection::vec(-1e3_f64..1e3, 1..=50)
2027 ) {
2028 if let Some((_, probs)) = ecdf(&data) {
2029 let last = *probs.last().expect("non-empty");
2030 prop_assert!((last - 1.0).abs() < 1e-10, "last prob = {last}");
2031 }
2032 }
2033
2034 #[test]
2035 fn ecdf_monotonic(
2036 data in proptest::collection::vec(-1e3_f64..1e3, 2..=50)
2037 ) {
2038 if let Some((_, probs)) = ecdf(&data) {
2039 for w in probs.windows(2) {
2040 prop_assert!(w[1] >= w[0] - 1e-15, "{} < {}", w[1], w[0]);
2041 }
2042 }
2043 }
2044
2045 #[test]
2046 fn hist_counts_sum_to_n(
2047 data in proptest::collection::vec(-1e3_f64..1e3, 5..=100)
2048 ) {
2049 if let Some(r) = histogram_bins(&data, BinMethod::Sturges) {
2050 let total: usize = r.counts.iter().sum();
2051 prop_assert_eq!(total, data.len());
2052 }
2053 }
2054
2055 #[test]
2056 fn kde_density_non_negative(
2057 data in proptest::collection::vec(-100.0_f64..100.0, 5..=50)
2058 ) {
2059 if let Some(r) = kde(&data, BandwidthMethod::Silverman, 128) {
2060 for &d in &r.density {
2061 prop_assert!(d >= 0.0, "negative density: {d}");
2062 }
2063 }
2064 }
2065
2066 #[test]
2067 fn kde_integral_approx_one(
2068 data in proptest::collection::vec(-100.0_f64..100.0, 10..=80)
2069 ) {
2070 if let Some(r) = kde(&data, BandwidthMethod::Silverman, 512) {
2071 let dx = r.x[1] - r.x[0];
2072 let integral: f64 = r.density.iter().sum::<f64>() * dx;
2073 prop_assert!(
2074 (integral - 1.0).abs() < 0.1,
2075 "integral = {integral}, expected ≈ 1.0"
2076 );
2077 }
2078 }
2079
2080 #[test]
2081 fn fit_normal_aic_finite(
2082 data in proptest::collection::vec(-100.0_f64..100.0, 5..=50)
2083 ) {
2084 if let Some(r) = fit_normal(&data) {
2085 prop_assert!(r.aic.is_finite(), "AIC = {}", r.aic);
2086 prop_assert!(r.bic.is_finite(), "BIC = {}", r.bic);
2087 prop_assert!(r.log_likelihood.is_finite(), "LL = {}", r.log_likelihood);
2088 }
2089 }
2090
2091 #[test]
2092 fn fit_gamma_alpha_positive(
2093 data in proptest::collection::vec(0.1_f64..100.0, 5..=50)
2094 ) {
2095 if let Some(r) = fit_gamma(&data) {
2096 let alpha = r.parameters[0].1;
2097 let beta = r.parameters[1].1;
2098 prop_assert!(alpha > 0.0, "α = {alpha}");
2099 prop_assert!(beta > 0.0, "β = {beta}");
2100 prop_assert!(r.aic.is_finite(), "AIC = {}", r.aic);
2101 }
2102 }
2103 }
2104}
2105
2106#[cfg(test)]
2113mod scipy_reference {
2114 use super::*;
2115
2116 type Want = (&'static str, &'static [f64], f64, f64, f64);
2117
2118 fn close(got: f64, want: f64) -> bool {
2119 (got - want).abs() <= 1e-9 * want.abs().max(1.0)
2120 }
2121
2122 fn check(data: &[f64], want: &[Want], what: &str) {
2123 let got = fit_best(data);
2124 let names: Vec<&str> = got.iter().map(|f| f.distribution.as_str()).collect();
2125 let wanted: Vec<&str> = want.iter().map(|w| w.0).collect();
2126 assert_eq!(names, wanted, "{what}: families in AIC order");
2127 for (f, w) in got.iter().zip(want) {
2128 let name = &f.distribution;
2129 assert_eq!(f.parameters.len(), w.1.len(), "{what} {name}: parameters");
2130 for ((pname, p), q) in f.parameters.iter().zip(w.1) {
2131 assert!(
2132 close(*p, *q),
2133 "{what} {name} {pname}: got {p}, reference {q}"
2134 );
2135 }
2136 assert!(
2137 close(f.log_likelihood, w.2),
2138 "{what} {name} log-likelihood: got {}, reference {}",
2139 f.log_likelihood,
2140 w.2
2141 );
2142 assert!(
2143 close(f.aic, w.3),
2144 "{what} {name} AIC: got {}, reference {}",
2145 f.aic,
2146 w.3
2147 );
2148 assert!(
2149 close(f.bic, w.4),
2150 "{what} {name} BIC: got {}, reference {}",
2151 f.bic,
2152 w.4
2153 );
2154 }
2155 }
2156
2157 #[test]
2158 fn data_inside_the_unit_interval_is_fitted_by_all_six_families() {
2159 check(
2160 &[
2161 0.12, 0.23, 0.31, 0.38, 0.44, 0.52, 0.57, 0.63, 0.71, 0.78, 0.86, 0.93,
2162 ],
2163 &[
2164 (
2165 "Beta",
2166 &[1.8438565181620945, 1.5583703583443078],
2167 1.035200520768992,
2168 1.929598958462016,
2169 2.8994122580380166,
2170 ),
2171 (
2172 "Weibull",
2173 &[2.391258961247938, 0.6091890535880672],
2174 0.10397825424951779,
2175 3.7920434915009644,
2176 4.761856791076966,
2177 ),
2178 (
2179 "Normal",
2180 &[0.54, 0.24348168993444524],
2181 -0.0746999829213777,
2182 4.149399965842756,
2183 5.119213265418756,
2184 ),
2185 (
2186 "Gamma",
2187 &[3.7996792156263166, 7.036442991900586],
2188 -0.4996467398561213,
2189 4.999293479712243,
2190 5.969106779288243,
2191 ),
2192 (
2193 "LogNormal",
2194 &[-0.7535094301720809, 0.5764955804748335],
2195 -1.3756979711767767,
2196 6.751395942353554,
2197 7.721209241929554,
2198 ),
2199 (
2200 "Exponential",
2201 &[1.8518518518518516],
2202 -4.605766326914196,
2203 11.211532653828392,
2204 11.696439303616392,
2205 ),
2206 ],
2207 "unit interval",
2208 );
2209 }
2210
2211 #[test]
2212 fn positive_data_is_fitted_by_the_five_positive_families() {
2213 check(
2214 &[2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7, 6.4, 9.8],
2215 &[
2216 (
2217 "LogNormal",
2218 &[1.1668644506389008, 0.5022892551220499],
2219 -22.766686368395682,
2220 49.533372736791364,
2221 50.50318603636737,
2222 ),
2223 (
2224 "Gamma",
2225 &[3.7501260541767762, 1.0158355000027386],
2226 -23.629758495985087,
2227 51.25951699197017,
2228 52.22933029154618,
2229 ),
2230 (
2231 "Weibull",
2232 &[1.8208938249614839, 4.190960652455631],
2233 -24.62416845020696,
2234 53.24833690041392,
2235 54.21815019998992,
2236 ),
2237 (
2238 "Normal",
2239 &[3.6916666666666664, 2.2220329780531065],
2240 -26.60833279104041,
2241 57.21666558208082,
2242 58.186478881656825,
2243 ),
2244 (
2245 "Exponential",
2246 &[0.2708803611738149],
2247 -27.672936327157075,
2248 57.34587265431415,
2249 57.83077930410215,
2250 ),
2251 ],
2252 "positive",
2253 );
2254 }
2255
2256 #[test]
2257 fn data_of_mixed_sign_is_fitted_by_the_normal_alone() {
2258 check(
2259 &[-1.2, 0.4, 2.3, -0.7, 1.9, 0.8, -2.1, 3.1],
2260 &[(
2261 "Normal",
2262 &[0.5625, 1.6999540434964704],
2263 -15.596318005312547,
2264 35.192636010625094,
2265 35.35151909398476,
2266 )],
2267 "mixed sign",
2268 );
2269 }
2270}
2271
2272#[cfg(test)]
2276mod goodness_of_fit_reference {
2277 use super::*;
2278
2279 const POS: &[f64] = &[
2280 3.2, 4.8, 5.1, 6.0, 6.7, 7.3, 8.1, 9.4, 10.2, 12.5, 15.8, 41.0,
2281 ];
2282 const UNIT: &[f64] = &[
2283 0.04, 0.11, 0.19, 0.23, 0.31, 0.38, 0.42, 0.55, 0.61, 0.74, 0.88, 0.97,
2284 ];
2285
2286 fn fixed(distribution: &str, parameters: &[(&str, f64)]) -> FitResult {
2287 FitResult {
2288 distribution: distribution.to_string(),
2289 parameters: parameters
2290 .iter()
2291 .map(|&(n, v)| (n.to_string(), v))
2292 .collect(),
2293 log_likelihood: 0.0,
2294 aic: 0.0,
2295 bic: 0.0,
2296 n_params: parameters.len(),
2297 }
2298 }
2299
2300 #[test]
2304 fn anderson_darling_statistic_reproduces_scipy_for_every_family() {
2305 type Case = (
2306 &'static str,
2307 &'static [(&'static str, f64)],
2308 &'static [f64],
2309 f64,
2310 );
2311 let cases: [Case; 6] = [
2312 (
2313 "Normal",
2314 &[("mu", 9.0), ("sigma", 4.0)],
2315 POS,
2316 3.0398709332150187,
2317 ),
2318 (
2319 "LogNormal",
2320 &[("mu", 2.0), ("sigma", 0.6)],
2321 POS,
2322 0.4154008172250965,
2323 ),
2324 (
2325 "Weibull",
2326 &[("shape", 1.7), ("scale", 11.0)],
2327 POS,
2328 0.8259972063661056,
2329 ),
2330 (
2331 "Gamma",
2332 &[("alpha", 2.5), ("beta", 0.25)],
2333 POS,
2334 0.5575965468003456,
2335 ),
2336 ("Exponential", &[("lambda", 0.1)], POS, 1.235635893786549),
2337 (
2338 "Beta",
2339 &[("alpha", 1.3), ("beta", 1.6)],
2340 UNIT,
2341 0.34159812117638744,
2342 ),
2343 ];
2344 for (name, params, data, want) in cases {
2345 let got = anderson_darling_fit(data, &fixed(name, params)).unwrap();
2346 assert!(
2347 ((got - want) / want).abs() < 1e-9,
2348 "{name}: A² {got}, SciPy {want}"
2349 );
2350 }
2351 }
2352
2353 #[test]
2359 fn bootstrap_p_value_agrees_with_scipy_goodness_of_fit() {
2360 let data = [
2361 3.2, 4.8, 5.1, 6.0, 6.7, 7.3, 8.1, 9.4, 10.2, 12.5, 15.8, 21.0,
2362 ];
2363 let fit = fit_exponential(&data).unwrap();
2364 let (statistic, p) = (1.4502868637902981, 0.03158);
2365 let g = goodness_of_fit(&data, &fit, 9999, 3).unwrap();
2366 assert!(
2367 ((g.statistic - statistic) / statistic).abs() < 1e-9,
2368 "A² {} vs {statistic}",
2369 g.statistic
2370 );
2371 assert!(
2372 (g.p_value - p).abs() < 0.015,
2373 "p {} vs SciPy {p}",
2374 g.p_value
2375 );
2376 }
2377
2378 #[test]
2382 fn rejects_the_best_family_for_uniform_data_and_keeps_a_true_one() {
2383 use u_numflow::distributions::Sample;
2384 let uniform: Vec<f64> = (1..=2000).map(f64::from).collect();
2385 let best = &fit_best(&uniform)[0];
2386 let g = goodness_of_fit(&uniform, best, DEFAULT_GOF_REPLICATES, 0).unwrap();
2387 assert_eq!(g.p_value, 1.0 / 200.0, "{}: {g:?}", best.distribution);
2388
2389 let weibull = Weibull::new(2.0, 10.0).unwrap().sample_n(300, 11);
2390 let fit = fit_weibull(&weibull).unwrap();
2391 let g = goodness_of_fit(&weibull, &fit, DEFAULT_GOF_REPLICATES, 0).unwrap();
2392 assert!(g.p_value > 0.05, "{g:?}");
2393 }
2394
2395 #[test]
2396 fn same_seed_same_p_value_and_refusals() {
2397 let data = [2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7];
2398 let fit = fit_gamma(&data).unwrap();
2399 let a = goodness_of_fit(&data, &fit, 99, 7).unwrap();
2400 assert_eq!(a, goodness_of_fit(&data, &fit, 99, 7).unwrap());
2401 assert_eq!(a.replicates, 99);
2402 assert!(goodness_of_fit(&data, &fit, 0, 7).is_none());
2403 assert!(goodness_of_fit(&[1.0], &fit, 9, 7).is_none());
2404 assert!(goodness_of_fit(&[1.0, f64::NAN, 2.0], &fit, 9, 7).is_none());
2405 let poisson = fit_poisson(&[1.0, 2.0, 2.0, 3.0]).unwrap();
2406 assert!(anderson_darling_fit(&data, &poisson).is_none());
2407 }
2408
2409 #[test]
2411 fn unit_invariant() {
2412 let data = [2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7, 6.3, 2.2];
2413 for scale in [1e-9, 1.0, 1e9] {
2414 let x: Vec<f64> = data.iter().map(|v| v * scale).collect();
2415 for fit in fit_best(&x) {
2416 let base: Vec<f64> = data.to_vec();
2417 let reference = fit_best(&base)
2418 .into_iter()
2419 .find(|f| f.distribution == fit.distribution);
2420 let Some(reference) = reference else { continue };
2421 let a = goodness_of_fit(&x, &fit, 49, 1).unwrap();
2422 let b = goodness_of_fit(&base, &reference, 49, 1).unwrap();
2423 assert!(
2424 (a.statistic - b.statistic).abs() < 1e-6 * b.statistic.max(1.0),
2425 "{} at ×{scale}: {} vs {}",
2426 fit.distribution,
2427 a.statistic,
2428 b.statistic
2429 );
2430 }
2431 }
2432 }
2433}