1use ferray_core::error::{FerrayError, FerrayResult};
10use ferray_core::{Array, Dimension, Element, Ix2};
11
12fn betainc(a: f64, b: f64, x: f64) -> f64 {
22 if !(0.0..=1.0).contains(&x) || a <= 0.0 || b <= 0.0 {
23 return f64::NAN;
24 }
25 if x == 0.0 {
26 return 0.0;
27 }
28 if x == 1.0 {
29 return 1.0;
30 }
31 let bt = (ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b) + a * x.ln() + b * (1.0 - x).ln()).exp();
34 if x < (a + 1.0) / (a + b + 2.0) {
35 bt * betacf(a, b, x) / a
36 } else {
37 1.0 - bt * betacf(b, a, 1.0 - x) / b
38 }
39}
40
41fn ln_gamma(x: f64) -> f64 {
43 const G: f64 = 5.0;
45 const COEFFS: [f64; 7] = [
46 1.000_000_000_190_015,
47 76.180_091_729_471_46,
48 -86.505_320_329_416_77,
49 24.014_098_240_830_91,
50 -1.231_739_572_450_155,
51 0.001_208_650_973_866_179,
52 -0.000_005_395_239_384_953,
53 ];
54 let mut sum = COEFFS[0];
55 for (i, &c) in COEFFS.iter().enumerate().skip(1) {
56 sum += c / (x + i as f64);
57 }
58 let half_log_2pi = 0.918_938_533_204_672_8;
59 half_log_2pi + (x + 0.5) * (x + G + 0.5).ln() - (x + G + 0.5) + (sum / x).ln()
60}
61
62fn betacf(a: f64, b: f64, x: f64) -> f64 {
64 const MAX_ITER: usize = 200;
65 const EPS: f64 = 3e-16;
66 let qab = a + b;
67 let qap = a + 1.0;
68 let qam = a - 1.0;
69 let mut c = 1.0;
70 let mut d = 1.0 - qab * x / qap;
71 if d.abs() < 1e-30 {
72 d = 1e-30;
73 }
74 d = 1.0 / d;
75 let mut h = d;
76 for m in 1..=MAX_ITER {
77 let mf = m as f64;
78 let m2 = 2.0 * mf;
79 let aa = mf * (b - mf) * x / ((qam + m2) * (a + m2));
81 d = 1.0 + aa * d;
82 if d.abs() < 1e-30 {
83 d = 1e-30;
84 }
85 c = 1.0 + aa / c;
86 if c.abs() < 1e-30 {
87 c = 1e-30;
88 }
89 d = 1.0 / d;
90 h *= d * c;
91 let aa = -(a + mf) * (qab + mf) * x / ((a + m2) * (qap + m2));
93 d = 1.0 + aa * d;
94 if d.abs() < 1e-30 {
95 d = 1e-30;
96 }
97 c = 1.0 + aa / c;
98 if c.abs() < 1e-30 {
99 c = 1e-30;
100 }
101 d = 1.0 / d;
102 let del = d * c;
103 h *= del;
104 if (del - 1.0).abs() < EPS {
105 return h;
106 }
107 }
108 h
109}
110
111fn t_two_sided_p(t: f64, df: f64) -> f64 {
115 if !t.is_finite() || df <= 0.0 {
116 return f64::NAN;
117 }
118 betainc(df / 2.0, 0.5, df / (df + t * t))
119}
120
121pub fn pearsonr<T, D>(x: &Array<T, D>, y: &Array<T, D>) -> FerrayResult<(f64, f64)>
139where
140 T: Element + Copy + Into<f64>,
141 D: Dimension,
142{
143 let xs: Vec<f64> = x.iter().copied().map(Into::into).collect();
144 let ys: Vec<f64> = y.iter().copied().map(Into::into).collect();
145 if xs.len() != ys.len() {
146 return Err(FerrayError::shape_mismatch(format!(
147 "pearsonr: arrays have different lengths ({} vs {})",
148 xs.len(),
149 ys.len()
150 )));
151 }
152 let n = xs.len();
153 if n < 2 {
154 return Err(FerrayError::invalid_value(
155 "pearsonr: at least 2 observations required",
156 ));
157 }
158 let nf = n as f64;
159 let mx = xs.iter().sum::<f64>() / nf;
160 let my = ys.iter().sum::<f64>() / nf;
161 let mut sxx = 0.0_f64;
162 let mut syy = 0.0_f64;
163 let mut sxy = 0.0_f64;
164 for (a, b) in xs.iter().zip(ys.iter()) {
165 let dx = a - mx;
166 let dy = b - my;
167 sxx += dx * dx;
168 syy += dy * dy;
169 sxy += dx * dy;
170 }
171 let denom = (sxx * syy).sqrt();
172 if denom == 0.0 {
173 return Ok((f64::NAN, f64::NAN));
175 }
176 let r = (sxy / denom).clamp(-1.0, 1.0);
177 let df = nf - 2.0;
178 let t = r * (df / (1.0 - r * r).max(f64::MIN_POSITIVE)).sqrt();
179 let p = t_two_sided_p(t, df);
180 Ok((r, p))
181}
182
183pub fn spearmanr<T, D>(x: &Array<T, D>, y: &Array<T, D>) -> FerrayResult<(f64, f64)>
193where
194 T: Element + Copy + PartialOrd + Into<f64>,
195 D: Dimension,
196{
197 let xs: Vec<f64> = x.iter().copied().map(Into::into).collect();
198 let ys: Vec<f64> = y.iter().copied().map(Into::into).collect();
199 if xs.len() != ys.len() {
200 return Err(FerrayError::shape_mismatch(format!(
201 "spearmanr: arrays have different lengths ({} vs {})",
202 xs.len(),
203 ys.len()
204 )));
205 }
206 if xs.len() < 2 {
207 return Err(FerrayError::invalid_value(
208 "spearmanr: at least 2 observations required",
209 ));
210 }
211 let rx = average_ranks(&xs);
212 let ry = average_ranks(&ys);
213 pearsonr_f64_slices(&rx, &ry)
214}
215
216fn pearsonr_f64_slices(xs: &[f64], ys: &[f64]) -> FerrayResult<(f64, f64)> {
219 let n = xs.len();
220 if n < 2 {
221 return Err(FerrayError::invalid_value(
222 "pearsonr: at least 2 observations required",
223 ));
224 }
225 let nf = n as f64;
226 let mx = xs.iter().sum::<f64>() / nf;
227 let my = ys.iter().sum::<f64>() / nf;
228 let mut sxx = 0.0_f64;
229 let mut syy = 0.0_f64;
230 let mut sxy = 0.0_f64;
231 for (a, b) in xs.iter().zip(ys.iter()) {
232 let dx = a - mx;
233 let dy = b - my;
234 sxx += dx * dx;
235 syy += dy * dy;
236 sxy += dx * dy;
237 }
238 let denom = (sxx * syy).sqrt();
239 if denom == 0.0 {
240 return Ok((f64::NAN, f64::NAN));
241 }
242 let r = (sxy / denom).clamp(-1.0, 1.0);
243 let df = nf - 2.0;
244 let t = r * (df / (1.0 - r * r).max(f64::MIN_POSITIVE)).sqrt();
245 let p = t_two_sided_p(t, df);
246 Ok((r, p))
247}
248
249fn average_ranks(xs: &[f64]) -> Vec<f64> {
251 let n = xs.len();
252 let mut idx: Vec<usize> = (0..n).collect();
253 idx.sort_by(|&a, &b| {
254 xs[a]
255 .partial_cmp(&xs[b])
256 .unwrap_or(std::cmp::Ordering::Equal)
257 });
258 let mut ranks = vec![0.0_f64; n];
259 let mut i = 0;
260 while i < n {
261 let mut j = i;
262 while j + 1 < n && xs[idx[j + 1]] == xs[idx[i]] {
263 j += 1;
264 }
265 let avg = (i as f64 + j as f64) / 2.0 + 1.0;
267 for k in i..=j {
268 ranks[idx[k]] = avg;
269 }
270 i = j + 1;
271 }
272 ranks
273}
274
275pub fn ttest_1samp<T, D>(a: &Array<T, D>, popmean: f64) -> FerrayResult<(f64, f64)>
285where
286 T: Element + Copy + Into<f64>,
287 D: Dimension,
288{
289 let xs: Vec<f64> = a.iter().copied().map(Into::into).collect();
290 let n = xs.len();
291 if n < 2 {
292 return Err(FerrayError::invalid_value(
293 "ttest_1samp: at least 2 observations required",
294 ));
295 }
296 let nf = n as f64;
297 let mean = xs.iter().sum::<f64>() / nf;
298 let var = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (nf - 1.0);
299 if var == 0.0 {
300 return Ok((f64::NAN, f64::NAN));
301 }
302 let std = var.sqrt();
303 let se = std / nf.sqrt();
304 let t = (mean - popmean) / se;
305 let p = t_two_sided_p(t, nf - 1.0);
306 Ok((t, p))
307}
308
309pub fn ttest_ind<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<(f64, f64)>
319where
320 T: Element + Copy + Into<f64>,
321 D: Dimension,
322{
323 let xs: Vec<f64> = a.iter().copied().map(Into::into).collect();
324 let ys: Vec<f64> = b.iter().copied().map(Into::into).collect();
325 if xs.len() < 2 || ys.len() < 2 {
326 return Err(FerrayError::invalid_value(
327 "ttest_ind: each sample needs at least 2 observations",
328 ));
329 }
330 let n1 = xs.len() as f64;
331 let n2 = ys.len() as f64;
332 let m1 = xs.iter().sum::<f64>() / n1;
333 let m2 = ys.iter().sum::<f64>() / n2;
334 let v1 = xs.iter().map(|x| (x - m1).powi(2)).sum::<f64>() / (n1 - 1.0);
335 let v2 = ys.iter().map(|x| (x - m2).powi(2)).sum::<f64>() / (n2 - 1.0);
336 if v1 == 0.0 && v2 == 0.0 {
337 return Ok((f64::NAN, f64::NAN));
338 }
339 let se = (v1 / n1 + v2 / n2).sqrt();
340 let t = (m1 - m2) / se;
341 let num = (v1 / n1 + v2 / n2).powi(2);
342 let denom = (v1 / n1).powi(2) / (n1 - 1.0) + (v2 / n2).powi(2) / (n2 - 1.0);
343 let df = num / denom;
344 let p = t_two_sided_p(t, df);
345 Ok((t, p))
346}
347
348#[derive(Debug, Clone)]
350pub struct Chi2ContingencyResult {
351 pub statistic: f64,
353 pub p_value: f64,
355 pub dof: usize,
357 pub expected: Array<f64, Ix2>,
359}
360
361pub fn chi2_contingency<T>(observed: &Array<T, Ix2>) -> FerrayResult<Chi2ContingencyResult>
372where
373 T: Element + Copy + Into<f64>,
374{
375 let shape = observed.shape();
376 let nr = shape[0];
377 let nc = shape[1];
378 if nr == 0 || nc == 0 {
379 return Err(FerrayError::invalid_value(
380 "chi2_contingency: table must be non-empty along both axes",
381 ));
382 }
383 let data: Vec<f64> = observed.iter().copied().map(Into::into).collect();
384 let mut row_sums = vec![0.0_f64; nr];
386 let mut col_sums = vec![0.0_f64; nc];
387 let mut total = 0.0_f64;
388 for i in 0..nr {
389 for j in 0..nc {
390 let v = data[i * nc + j];
391 row_sums[i] += v;
392 col_sums[j] += v;
393 total += v;
394 }
395 }
396 if total == 0.0 {
397 return Err(FerrayError::invalid_value(
398 "chi2_contingency: total frequency is zero",
399 ));
400 }
401 if row_sums.contains(&0.0) || col_sums.contains(&0.0) {
402 return Err(FerrayError::invalid_value(
403 "chi2_contingency: a row or column has zero total — independence model is degenerate",
404 ));
405 }
406 let mut expected = vec![0.0_f64; nr * nc];
407 let mut chi2 = 0.0_f64;
408 for i in 0..nr {
409 for j in 0..nc {
410 let e = row_sums[i] * col_sums[j] / total;
411 expected[i * nc + j] = e;
412 let o = data[i * nc + j];
413 let d = o - e;
415 chi2 += d * d / e;
416 }
417 }
418 let dof = (nr - 1) * (nc - 1);
419 let p = chi2_sf(chi2, dof as f64);
420 Ok(Chi2ContingencyResult {
421 statistic: chi2,
422 p_value: p,
423 dof,
424 expected: Array::<f64, Ix2>::from_vec(Ix2::new([nr, nc]), expected)?,
425 })
426}
427
428fn chi2_sf(x: f64, df: f64) -> f64 {
432 if !x.is_finite() || x < 0.0 || df <= 0.0 {
433 return f64::NAN;
434 }
435 if x == 0.0 {
436 return 1.0;
437 }
438 gamma_q(df / 2.0, x / 2.0)
439}
440
441fn gamma_q(a: f64, x: f64) -> f64 {
446 if a <= 0.0 || x < 0.0 || !x.is_finite() {
447 return f64::NAN;
448 }
449 if x == 0.0 {
450 return 1.0;
451 }
452 if x < a + 1.0 {
453 1.0 - gamma_p_series(a, x)
454 } else {
455 gamma_q_cf(a, x)
456 }
457}
458
459fn gamma_p_series(a: f64, x: f64) -> f64 {
461 const MAX_ITER: usize = 200;
462 const EPS: f64 = 3e-16;
463 let mut ap = a;
464 let mut sum = 1.0_f64 / a;
465 let mut term = sum;
466 for _ in 0..MAX_ITER {
467 ap += 1.0;
468 term *= x / ap;
469 sum += term;
470 if term.abs() < sum.abs() * EPS {
471 break;
472 }
473 }
474 sum * (-x + a * x.ln() - ln_gamma(a)).exp()
475}
476
477fn gamma_q_cf(a: f64, x: f64) -> f64 {
480 const MAX_ITER: usize = 200;
481 const EPS: f64 = 3e-16;
482 let mut b = x + 1.0 - a;
483 let mut c = 1.0 / 1e-30;
484 let mut d = 1.0 / b;
485 let mut h = d;
486 for i in 1..=MAX_ITER {
487 let an = -(i as f64) * (i as f64 - a);
488 b += 2.0;
489 d = an * d + b;
490 if d.abs() < 1e-30 {
491 d = 1e-30;
492 }
493 c = b + an / c;
494 if c.abs() < 1e-30 {
495 c = 1e-30;
496 }
497 d = 1.0 / d;
498 let del = d * c;
499 h *= del;
500 if (del - 1.0).abs() < EPS {
501 break;
502 }
503 }
504 h * (-x + a * x.ln() - ln_gamma(a)).exp()
505}
506
507pub fn ks_2samp<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<(f64, f64)>
517where
518 T: Element + Copy + PartialOrd + Into<f64>,
519 D: Dimension,
520{
521 let mut xs: Vec<f64> = a.iter().copied().map(Into::into).collect();
522 let mut ys: Vec<f64> = b.iter().copied().map(Into::into).collect();
523 if xs.is_empty() || ys.is_empty() {
524 return Err(FerrayError::invalid_value(
525 "ks_2samp: both samples must be non-empty",
526 ));
527 }
528 xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
529 ys.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
530
531 let n1 = xs.len() as f64;
536 let n2 = ys.len() as f64;
537 let mut union: Vec<f64> = Vec::with_capacity(xs.len() + ys.len());
538 union.extend_from_slice(&xs);
539 union.extend_from_slice(&ys);
540 union.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
541 let mut d = 0.0_f64;
542 for &v in &union {
543 let i = xs.partition_point(|x| *x <= v);
544 let j = ys.partition_point(|y| *y <= v);
545 let here = (i as f64 / n1 - j as f64 / n2).abs();
546 if here > d {
547 d = here;
548 }
549 }
550
551 let en = (n1 * n2 / (n1 + n2)).sqrt();
553 let lambda = (en + 0.12 + 0.11 / en) * d;
554 let p = ks_p_kolmogorov(lambda).clamp(0.0, 1.0);
555 Ok((d, p))
556}
557
558fn ks_p_kolmogorov(lambda: f64) -> f64 {
561 if lambda <= 0.0 {
562 return 1.0;
563 }
564 let mut sum = 0.0_f64;
565 let mut sign = 1.0_f64;
566 let mut prev = 0.0_f64;
567 for k in 1..=200 {
568 let kf = k as f64;
569 let term = sign * (-2.0 * kf * kf * lambda * lambda).exp();
570 sum += term;
571 if (sum - prev).abs() < 1e-15 {
572 break;
573 }
574 prev = sum;
575 sign = -sign;
576 }
577 2.0 * sum
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use ferray_core::Ix1;
584
585 fn arr1(v: Vec<f64>) -> Array<f64, Ix1> {
586 let n = v.len();
587 Array::from_vec(Ix1::new([n]), v).unwrap()
588 }
589
590 #[test]
591 fn pearsonr_perfect_positive() {
592 let x = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
593 let y = arr1(vec![2.0, 4.0, 6.0, 8.0, 10.0]);
594 let (r, p) = pearsonr(&x, &y).unwrap();
595 assert!((r - 1.0).abs() < 1e-12);
596 assert!(p < 1e-6);
598 }
599
600 #[test]
601 fn pearsonr_perfect_negative() {
602 let x = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
603 let y = arr1(vec![5.0, 4.0, 3.0, 2.0, 1.0]);
604 let (r, p) = pearsonr(&x, &y).unwrap();
605 assert!((r + 1.0).abs() < 1e-12);
606 assert!(p < 1e-6);
607 }
608
609 #[test]
610 fn pearsonr_uncorrelated_high_p() {
611 let x = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
613 let y = arr1(vec![7.5, -1.0, 2.0, 5.5, -3.0, 6.0, 0.5, 1.5]);
614 let (r, p) = pearsonr(&x, &y).unwrap();
615 assert!(r.abs() < 1.0);
616 assert!(p > 0.0 && p <= 1.0);
617 }
618
619 #[test]
620 fn pearsonr_constant_returns_nan() {
621 let x = arr1(vec![3.0, 3.0, 3.0, 3.0]);
622 let y = arr1(vec![1.0, 2.0, 3.0, 4.0]);
623 let (r, p) = pearsonr(&x, &y).unwrap();
624 assert!(r.is_nan());
625 assert!(p.is_nan());
626 }
627
628 #[test]
629 fn pearsonr_length_mismatch_errors() {
630 let x = arr1(vec![1.0, 2.0]);
631 let y = arr1(vec![1.0, 2.0, 3.0]);
632 assert!(pearsonr(&x, &y).is_err());
633 }
634
635 #[test]
636 fn spearmanr_monotonic_nonlinear_is_one() {
637 let x = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
639 let y = arr1(vec![1.0, 4.0, 9.0, 16.0, 25.0]);
640 let (rho, _) = spearmanr(&x, &y).unwrap();
641 assert!((rho - 1.0).abs() < 1e-12);
642 }
643
644 #[test]
645 fn ttest_1samp_known() {
646 let a = arr1(vec![4.0, 5.0, 6.0]);
649 let (t, p) = ttest_1samp(&a, 5.0).unwrap();
650 assert!(t.abs() < 1e-12);
651 assert!((p - 1.0).abs() < 1e-12);
653 }
654
655 #[test]
656 fn ttest_1samp_strong_signal() {
657 let a = arr1(vec![10.0, 11.0, 9.0, 10.5, 9.5, 10.0]);
659 let (_, p) = ttest_1samp(&a, 0.0).unwrap();
660 assert!(p < 1e-6);
661 }
662
663 #[test]
664 fn ttest_ind_identical_samples_t_zero() {
665 let a = arr1(vec![1.0, 2.0, 3.0, 4.0]);
666 let b = arr1(vec![1.0, 2.0, 3.0, 4.0]);
667 let (t, p) = ttest_ind(&a, &b).unwrap();
668 assert!(t.abs() < 1e-12);
669 assert!((p - 1.0).abs() < 1e-12);
670 }
671
672 #[test]
673 fn ttest_ind_strong_difference() {
674 let a = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
677 let b = arr1(vec![10.0, 11.0, 12.0, 13.0, 14.0]);
678 let (_, p) = ttest_ind(&a, &b).unwrap();
679 assert!(p < 1e-3, "p too large: {p}");
680 }
681
682 #[test]
683 fn ks_2samp_identical_samples_d_zero() {
684 let a = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
685 let b = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
686 let (d, p) = ks_2samp(&a, &b).unwrap();
687 assert!(d < 1e-12);
688 assert!(p > 0.99);
689 }
690
691 #[test]
692 fn ks_2samp_clearly_different_distributions() {
693 let a = arr1(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]);
694 let b = arr1(vec![
695 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0,
696 ]);
697 let (d, p) = ks_2samp(&a, &b).unwrap();
698 assert!((d - 1.0).abs() < 1e-12);
700 assert!(p < 0.01);
701 }
702
703 #[test]
704 fn ks_2samp_partial_overlap() {
705 let a = arr1(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
706 let b = arr1(vec![3.0, 4.0, 5.0, 6.0, 7.0]);
707 let (d, _p) = ks_2samp(&a, &b).unwrap();
708 assert!(d > 0.0 && d < 1.0);
710 }
711
712 #[test]
713 fn empty_inputs_error() {
714 let empty = arr1(vec![]);
715 let one = arr1(vec![1.0]);
716 let two = arr1(vec![1.0, 2.0]);
717 assert!(pearsonr(&one, &one).is_err());
718 assert!(spearmanr(&one, &one).is_err());
719 assert!(ttest_1samp(&one, 0.0).is_err());
720 assert!(ttest_ind(&one, &two).is_err());
721 assert!(ks_2samp(&empty, &two).is_err());
722 }
723
724 #[test]
727 fn chi2_independent_table_yields_high_p() {
728 let obs = Array::<f64, ferray_core::Ix2>::from_vec(
731 ferray_core::Ix2::new([2, 2]),
732 vec![10.0, 10.0, 20.0, 20.0],
733 )
734 .unwrap();
735 let r = chi2_contingency(&obs).unwrap();
736 assert!(r.statistic.abs() < 1e-12);
737 assert!((r.p_value - 1.0).abs() < 1e-12);
738 assert_eq!(r.dof, 1);
739 }
740
741 #[test]
742 fn chi2_strong_dependence_low_p() {
743 let obs = Array::<f64, ferray_core::Ix2>::from_vec(
745 ferray_core::Ix2::new([2, 2]),
746 vec![100.0, 0.0, 0.0, 100.0],
747 )
748 .unwrap();
749 let r = chi2_contingency(&obs).unwrap();
750 assert!(r.statistic > 100.0);
752 assert!(r.p_value < 1e-6);
753 assert_eq!(r.dof, 1);
754 }
755
756 #[test]
757 fn chi2_known_value_2x3() {
758 let obs = Array::<f64, ferray_core::Ix2>::from_vec(
764 ferray_core::Ix2::new([2, 3]),
765 vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0],
766 )
767 .unwrap();
768 let r = chi2_contingency(&obs).unwrap();
769 assert_eq!(r.dof, 2);
771 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
772 let e00 = r.expected.as_slice().unwrap()[0];
774 assert!((e00 - 60.0 * 50.0 / 210.0).abs() < 1e-10);
775 }
776
777 #[test]
778 fn chi2_zero_row_total_errors() {
779 let obs = Array::<f64, ferray_core::Ix2>::from_vec(
780 ferray_core::Ix2::new([2, 2]),
781 vec![0.0, 0.0, 1.0, 1.0],
782 )
783 .unwrap();
784 assert!(chi2_contingency(&obs).is_err());
785 }
786
787 #[test]
788 fn chi2_empty_table_errors() {
789 let obs = Array::<f64, ferray_core::Ix2>::from_vec(ferray_core::Ix2::new([0, 0]), vec![])
790 .unwrap();
791 assert!(chi2_contingency(&obs).is_err());
792 }
793
794 #[test]
795 fn gamma_q_known_values() {
796 assert!((gamma_q(1.0, 1.0) - (-1.0_f64).exp()).abs() < 1e-12);
798 assert!((gamma_q(1.0, 2.0) - (-2.0_f64).exp()).abs() < 1e-12);
799 assert!((gamma_q(2.5, 0.0) - 1.0).abs() < 1e-12);
801 }
802
803 #[test]
804 fn betainc_known_values() {
805 assert!((betainc(1.0, 1.0, 0.5) - 0.5).abs() < 1e-10);
807 assert_eq!(betainc(2.0, 3.0, 0.0), 0.0);
809 assert_eq!(betainc(2.0, 3.0, 1.0), 1.0);
810 }
811}