1use crate::{math_utils::SpecialFunctions, UtilsError};
8use scirs2_core::ndarray::{Array1, Array2};
9use scirs2_core::numeric::Float;
10use std::cmp::Ordering;
11
12#[inline]
14fn compare_floats<T: Float>(a: &T, b: &T) -> Ordering {
15 match a.partial_cmp(b) {
16 Some(ord) => ord,
17 None => {
18 if a.is_nan() && b.is_nan() {
19 Ordering::Equal
20 } else if a.is_nan() {
21 Ordering::Greater
22 } else {
23 Ordering::Less
24 }
25 }
26 }
27}
28
29#[inline]
31fn safe_mean<T>(arr: &Array1<T>) -> Result<T, UtilsError>
32where
33 T: Float + std::iter::Sum + scirs2_core::numeric::FromPrimitive,
34{
35 arr.mean()
36 .ok_or_else(|| UtilsError::InvalidParameter("Failed to compute mean of array".to_string()))
37}
38
39#[derive(Debug, Clone)]
41pub struct TestResult {
42 pub statistic: f64,
43 pub p_value: f64,
44 pub critical_value: Option<f64>,
45 pub test_name: String,
46 pub significant: bool,
47}
48
49impl TestResult {
50 pub fn new(statistic: f64, p_value: f64, test_name: String, alpha: f64) -> Self {
51 Self {
52 statistic,
53 p_value,
54 critical_value: None,
55 test_name,
56 significant: p_value < alpha,
57 }
58 }
59
60 pub fn with_critical_value(mut self, critical_value: f64) -> Self {
61 self.critical_value = Some(critical_value);
62 self
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct ConfidenceInterval {
69 pub lower: f64,
70 pub upper: f64,
71 pub confidence_level: f64,
72 pub parameter: String,
73}
74
75impl ConfidenceInterval {
76 pub fn new(lower: f64, upper: f64, confidence_level: f64, parameter: String) -> Self {
77 Self {
78 lower,
79 upper,
80 confidence_level,
81 parameter,
82 }
83 }
84
85 pub fn width(&self) -> f64 {
86 self.upper - self.lower
87 }
88
89 pub fn contains(&self, value: f64) -> bool {
90 value >= self.lower && value <= self.upper
91 }
92}
93
94pub struct StatisticalTests;
96
97impl StatisticalTests {
98 pub fn one_sample_ttest(
100 data: &Array1<f64>,
101 population_mean: f64,
102 alpha: f64,
103 ) -> Result<TestResult, UtilsError> {
104 if data.is_empty() {
105 return Err(UtilsError::EmptyInput);
106 }
107
108 let n = data.len() as f64;
109 let sample_mean = safe_mean(data)?;
110 let sample_std = Self::standard_deviation(data);
111
112 if sample_std < f64::EPSILON {
113 return Err(UtilsError::InvalidParameter(
114 "Standard deviation is zero".to_string(),
115 ));
116 }
117
118 let t_statistic = (sample_mean - population_mean) / (sample_std / n.sqrt());
119 let degrees_of_freedom = n - 1.0;
120
121 let p_value = Self::t_distribution_cdf(-t_statistic.abs(), degrees_of_freedom) * 2.0;
123
124 Ok(TestResult::new(
125 t_statistic,
126 p_value,
127 "One-sample t-test".to_string(),
128 alpha,
129 ))
130 }
131
132 pub fn two_sample_ttest(
134 data1: &Array1<f64>,
135 data2: &Array1<f64>,
136 alpha: f64,
137 ) -> Result<TestResult, UtilsError> {
138 if data1.is_empty() || data2.is_empty() {
139 return Err(UtilsError::EmptyInput);
140 }
141
142 let n1 = data1.len() as f64;
143 let n2 = data2.len() as f64;
144 let mean1 = safe_mean(data1)?;
145 let mean2 = safe_mean(data2)?;
146 let std1 = Self::standard_deviation(data1);
147 let std2 = Self::standard_deviation(data2);
148
149 let pooled_std =
151 ((std1.powi(2) * (n1 - 1.0) + std2.powi(2) * (n2 - 1.0)) / (n1 + n2 - 2.0)).sqrt();
152
153 if pooled_std < f64::EPSILON {
154 return Err(UtilsError::InvalidParameter(
155 "Pooled standard deviation is zero".to_string(),
156 ));
157 }
158
159 let t_statistic = (mean1 - mean2) / (pooled_std * (1.0 / n1 + 1.0 / n2).sqrt());
160 let degrees_of_freedom = n1 + n2 - 2.0;
161
162 let p_value = Self::t_distribution_cdf(-t_statistic.abs(), degrees_of_freedom) * 2.0;
163
164 Ok(TestResult::new(
165 t_statistic,
166 p_value,
167 "Two-sample t-test".to_string(),
168 alpha,
169 ))
170 }
171
172 pub fn welch_ttest(
174 data1: &Array1<f64>,
175 data2: &Array1<f64>,
176 alpha: f64,
177 ) -> Result<TestResult, UtilsError> {
178 if data1.is_empty() || data2.is_empty() {
179 return Err(UtilsError::EmptyInput);
180 }
181
182 let n1 = data1.len() as f64;
183 let n2 = data2.len() as f64;
184 let mean1 = safe_mean(data1)?;
185 let mean2 = safe_mean(data2)?;
186 let var1 = Self::variance(data1);
187 let var2 = Self::variance(data2);
188
189 let se = (var1 / n1 + var2 / n2).sqrt();
190 if se < f64::EPSILON {
191 return Err(UtilsError::InvalidParameter(
192 "Standard error is zero".to_string(),
193 ));
194 }
195
196 let t_statistic = (mean1 - mean2) / se;
197
198 let degrees_of_freedom = (var1 / n1 + var2 / n2).powi(2)
200 / ((var1 / n1).powi(2) / (n1 - 1.0) + (var2 / n2).powi(2) / (n2 - 1.0));
201
202 let p_value = Self::t_distribution_cdf(-t_statistic.abs(), degrees_of_freedom) * 2.0;
203
204 Ok(TestResult::new(
205 t_statistic,
206 p_value,
207 "Welch's t-test".to_string(),
208 alpha,
209 ))
210 }
211
212 pub fn chi_square_goodness_of_fit(
214 observed: &Array1<f64>,
215 expected: &Array1<f64>,
216 alpha: f64,
217 ) -> Result<TestResult, UtilsError> {
218 if observed.len() != expected.len() {
219 return Err(UtilsError::ShapeMismatch {
220 expected: vec![expected.len()],
221 actual: vec![observed.len()],
222 });
223 }
224
225 if observed.is_empty() {
226 return Err(UtilsError::EmptyInput);
227 }
228
229 let mut chi_square = 0.0;
230 for (obs, exp) in observed.iter().zip(expected.iter()) {
231 if *exp <= 0.0 {
232 return Err(UtilsError::InvalidParameter(
233 "Expected frequencies must be positive".to_string(),
234 ));
235 }
236 chi_square += (obs - exp).powi(2) / exp;
237 }
238
239 let degrees_of_freedom = (observed.len() - 1) as f64;
240 let p_value = 1.0 - Self::chi_square_cdf(chi_square, degrees_of_freedom);
241
242 Ok(TestResult::new(
243 chi_square,
244 p_value,
245 "Chi-square goodness of fit".to_string(),
246 alpha,
247 ))
248 }
249
250 pub fn ks_test_normality(data: &Array1<f64>, alpha: f64) -> Result<TestResult, UtilsError> {
252 if data.is_empty() {
253 return Err(UtilsError::EmptyInput);
254 }
255
256 let n = data.len() as f64;
257 let mean = safe_mean(data)?;
258 let std = Self::standard_deviation(data);
259
260 let mut sorted_data = data.to_vec();
261 sorted_data.sort_by(compare_floats);
262
263 let mut d_plus = 0.0;
264 let mut d_minus = 0.0;
265
266 for (i, &value) in sorted_data.iter().enumerate() {
267 let empirical_cdf = (i + 1) as f64 / n;
268 let theoretical_cdf = Self::normal_cdf((value - mean) / std);
269
270 d_plus = d_plus.max(empirical_cdf - theoretical_cdf);
271 d_minus = d_minus.max(theoretical_cdf - empirical_cdf);
272 }
273
274 let ks_statistic = d_plus.max(d_minus);
275
276 let p_value = Self::kolmogorov_smirnov_p_value(ks_statistic, n);
278
279 Ok(TestResult::new(
280 ks_statistic,
281 p_value,
282 "Kolmogorov-Smirnov normality test".to_string(),
283 alpha,
284 ))
285 }
286
287 pub fn anderson_darling_test(data: &Array1<f64>, alpha: f64) -> Result<TestResult, UtilsError> {
289 if data.is_empty() {
290 return Err(UtilsError::EmptyInput);
291 }
292
293 let n = data.len() as f64;
294 let mean = safe_mean(data)?;
295 let std = Self::standard_deviation(data);
296
297 let mut sorted_data = data.to_vec();
298 sorted_data.sort_by(compare_floats);
299
300 let mut ad_statistic = 0.0;
301
302 for (i, &value) in sorted_data.iter().enumerate() {
303 let z = (value - mean) / std;
304 let phi = Self::normal_cdf(z);
305 let phi_complement = 1.0 - phi;
306
307 if phi > 0.0 && phi < 1.0 && phi_complement > 0.0 {
308 let j = i + 1;
309 ad_statistic +=
310 ((2 * j - 1) as f64) * (phi.ln() + sorted_data[n as usize - j].ln());
311 }
312 }
313
314 ad_statistic = -n - ad_statistic / n;
315
316 ad_statistic *= 1.0 + 0.75 / n + 2.25 / n.powi(2);
318
319 let p_value = if ad_statistic >= 0.6 {
321 (-1.2337141 / ad_statistic).exp()
322 * (2.00012
323 + (ad_statistic
324 * (-3.00021
325 + ad_statistic
326 * (12.24425
327 + ad_statistic
328 * (-17.2385
329 + ad_statistic * (12.79 - ad_statistic * 5.27)))))
330 .exp())
331 } else if ad_statistic >= 0.34 {
332 (-0.9177 - 2.0637 * ad_statistic).exp()
333 } else if ad_statistic >= 0.2 {
334 1.0 - (-8.318 + 42.796 * ad_statistic - 59.938 * ad_statistic.powi(2)).exp()
335 } else {
336 1.0 - (-13.436 + 101.14 * ad_statistic - 223.73 * ad_statistic.powi(2)).exp()
337 };
338
339 Ok(TestResult::new(
340 ad_statistic,
341 p_value,
342 "Anderson-Darling normality test".to_string(),
343 alpha,
344 ))
345 }
346
347 fn standard_deviation(data: &Array1<f64>) -> f64 {
350 Self::variance(data).sqrt()
351 }
352
353 fn variance(data: &Array1<f64>) -> f64 {
354 if data.len() <= 1 {
355 return 0.0;
356 }
357
358 let mean = data.iter().sum::<f64>() / data.len() as f64;
360 let sum_squares = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
361 sum_squares / (data.len() - 1) as f64
362 }
363
364 fn normal_cdf(x: f64) -> f64 {
366 0.5 * (1.0 + SpecialFunctions::erf(x / 2.0_f64.sqrt()))
367 }
368
369 fn t_distribution_cdf(t: f64, df: f64) -> f64 {
371 if df >= 30.0 {
372 return Self::normal_cdf(t);
374 }
375
376 let x = t / (t.powi(2) + df).sqrt();
378 0.5 + 0.5 * x * SpecialFunctions::gamma((df + 1.0) / 2.0)
379 / ((df * std::f64::consts::PI).sqrt() * SpecialFunctions::gamma(df / 2.0))
380 }
381
382 fn chi_square_cdf(x: f64, df: f64) -> f64 {
384 if x <= 0.0 {
385 return 0.0;
386 }
387
388 SpecialFunctions::gamma_inc(df / 2.0, x / 2.0) / SpecialFunctions::gamma(df / 2.0)
390 }
391
392 fn kolmogorov_smirnov_p_value(d: f64, n: f64) -> f64 {
394 let lambda = d * n.sqrt();
395 let mut p_value = 0.0;
396
397 for i in 1..=10 {
398 let term = (-2.0 * (i as f64).powi(2) * lambda.powi(2)).exp();
399 if i % 2 == 1 {
400 p_value += term;
401 } else {
402 p_value -= term;
403 }
404 }
405
406 2.0 * p_value
407 }
408}
409
410pub struct ConfidenceIntervals;
412
413impl ConfidenceIntervals {
414 pub fn mean_ci(
416 data: &Array1<f64>,
417 confidence_level: f64,
418 ) -> Result<ConfidenceInterval, UtilsError> {
419 if data.is_empty() {
420 return Err(UtilsError::EmptyInput);
421 }
422
423 if !(0.0..1.0).contains(&confidence_level) {
424 return Err(UtilsError::InvalidParameter(
425 "Confidence level must be between 0 and 1".to_string(),
426 ));
427 }
428
429 let n = data.len() as f64;
430 let mean = safe_mean(data)?;
431 let std = StatisticalTests::standard_deviation(data);
432 let se = std / n.sqrt();
433
434 let alpha = 1.0 - confidence_level;
435 let df = n - 1.0;
436
437 let t_critical = Self::t_critical_value(alpha / 2.0, df);
439 let margin_of_error = t_critical * se;
440
441 Ok(ConfidenceInterval::new(
442 mean - margin_of_error,
443 mean + margin_of_error,
444 confidence_level,
445 "Mean".to_string(),
446 ))
447 }
448
449 pub fn proportion_ci(
451 successes: usize,
452 trials: usize,
453 confidence_level: f64,
454 ) -> Result<ConfidenceInterval, UtilsError> {
455 if trials == 0 {
456 return Err(UtilsError::InvalidParameter(
457 "Number of trials must be positive".to_string(),
458 ));
459 }
460
461 if successes > trials {
462 return Err(UtilsError::InvalidParameter(
463 "Successes cannot exceed trials".to_string(),
464 ));
465 }
466
467 let p = successes as f64 / trials as f64;
468 let n = trials as f64;
469 let alpha = 1.0 - confidence_level;
470
471 if n * p >= 5.0 && n * (1.0 - p) >= 5.0 {
473 let z_critical = Self::normal_critical_value(alpha / 2.0);
474 let se = (p * (1.0 - p) / n).sqrt();
475 let margin_of_error = z_critical * se;
476
477 Ok(ConfidenceInterval::new(
478 (p - margin_of_error).max(0.0),
479 (p + margin_of_error).min(1.0),
480 confidence_level,
481 "Proportion".to_string(),
482 ))
483 } else {
484 let z = Self::normal_critical_value(alpha / 2.0);
486 let z2 = z * z;
487 let center = (p + z2 / (2.0 * n)) / (1.0 + z2 / n);
488 let width = z / (1.0 + z2 / n) * (p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt();
489
490 Ok(ConfidenceInterval::new(
491 (center - width).max(0.0),
492 (center + width).min(1.0),
493 confidence_level,
494 "Proportion (Wilson)".to_string(),
495 ))
496 }
497 }
498
499 pub fn variance_ci(
501 data: &Array1<f64>,
502 confidence_level: f64,
503 ) -> Result<ConfidenceInterval, UtilsError> {
504 if data.len() <= 1 {
505 return Err(UtilsError::InsufficientData {
506 min: 2,
507 actual: data.len(),
508 });
509 }
510
511 let n = data.len() as f64;
512 let variance = StatisticalTests::variance(data);
513 let df = n - 1.0;
514 let alpha = 1.0 - confidence_level;
515
516 let chi2_lower = Self::chi_square_critical_value(1.0 - alpha / 2.0, df);
518 let chi2_upper = Self::chi_square_critical_value(alpha / 2.0, df);
519
520 let lower = df * variance / chi2_upper;
521 let upper = df * variance / chi2_lower;
522
523 Ok(ConfidenceInterval::new(
524 lower,
525 upper,
526 confidence_level,
527 "Variance".to_string(),
528 ))
529 }
530
531 fn normal_critical_value(alpha: f64) -> f64 {
534 if alpha <= 0.001 {
536 3.291
537 } else if alpha <= 0.005 {
538 2.807
539 } else if alpha <= 0.01 {
540 2.576
541 } else if alpha <= 0.025 {
542 1.960
543 } else if alpha <= 0.05 {
544 1.645
545 } else if alpha <= 0.1 {
546 1.282
547 } else {
548 0.674
549 }
550 }
551
552 fn t_critical_value(alpha: f64, df: f64) -> f64 {
553 if df >= 30.0 {
554 return Self::normal_critical_value(alpha);
555 }
556
557 let normal_val = Self::normal_critical_value(alpha);
559 let correction = (1.0 + (normal_val.powi(2) + 1.0) / (4.0 * df))
560 * (1.0
561 + (5.0 * normal_val.powi(4) + 16.0 * normal_val.powi(2) + 3.0)
562 / (96.0 * df.powi(2)));
563 normal_val * correction
564 }
565
566 fn chi_square_critical_value(alpha: f64, df: f64) -> f64 {
567 let h = 2.0 / (9.0 * df);
569 let normal_val = Self::normal_critical_value(alpha);
570 df * (1.0 - h + normal_val * (h * 2.0).sqrt()).powi(3)
571 }
572}
573
574pub struct CorrelationAnalysis;
576
577impl CorrelationAnalysis {
578 pub fn pearson_correlation(x: &Array1<f64>, y: &Array1<f64>) -> Result<f64, UtilsError> {
580 if x.len() != y.len() {
581 return Err(UtilsError::ShapeMismatch {
582 expected: vec![x.len()],
583 actual: vec![y.len()],
584 });
585 }
586
587 if x.is_empty() {
588 return Err(UtilsError::EmptyInput);
589 }
590
591 let _n = x.len() as f64;
592 let mean_x = safe_mean(x)?;
593 let mean_y = safe_mean(y)?;
594
595 let mut numerator = 0.0;
596 let mut sum_sq_x = 0.0;
597 let mut sum_sq_y = 0.0;
598
599 for (xi, yi) in x.iter().zip(y.iter()) {
600 let dx = xi - mean_x;
601 let dy = yi - mean_y;
602 numerator += dx * dy;
603 sum_sq_x += dx * dx;
604 sum_sq_y += dy * dy;
605 }
606
607 let denominator = (sum_sq_x * sum_sq_y).sqrt();
608 if denominator < f64::EPSILON {
609 return Ok(0.0);
610 }
611
612 Ok(numerator / denominator)
613 }
614
615 pub fn spearman_correlation(x: &Array1<f64>, y: &Array1<f64>) -> Result<f64, UtilsError> {
617 if x.len() != y.len() {
618 return Err(UtilsError::ShapeMismatch {
619 expected: vec![x.len()],
620 actual: vec![y.len()],
621 });
622 }
623
624 let ranks_x = Self::compute_ranks(x);
625 let ranks_y = Self::compute_ranks(y);
626
627 Self::pearson_correlation(&ranks_x, &ranks_y)
628 }
629
630 pub fn kendall_tau(x: &Array1<f64>, y: &Array1<f64>) -> Result<f64, UtilsError> {
632 if x.len() != y.len() {
633 return Err(UtilsError::ShapeMismatch {
634 expected: vec![x.len()],
635 actual: vec![y.len()],
636 });
637 }
638
639 if x.is_empty() {
640 return Err(UtilsError::EmptyInput);
641 }
642
643 let n = x.len();
644 let mut concordant = 0;
645 let mut discordant = 0;
646
647 for i in 0..n {
648 for j in (i + 1)..n {
649 let sign_x = (x[j] - x[i]).signum();
650 let sign_y = (y[j] - y[i]).signum();
651
652 if sign_x * sign_y > 0.0 {
653 concordant += 1;
654 } else if sign_x * sign_y < 0.0 {
655 discordant += 1;
656 }
657 }
658 }
659
660 let total_pairs = n * (n - 1) / 2;
661 Ok((concordant - discordant) as f64 / total_pairs as f64)
662 }
663
664 pub fn correlation_matrix(data: &Array2<f64>) -> Result<Array2<f64>, UtilsError> {
666 let (n_samples, n_features) = data.dim();
667 if n_samples == 0 || n_features == 0 {
668 return Err(UtilsError::EmptyInput);
669 }
670
671 let mut corr_matrix = Array2::zeros((n_features, n_features));
672
673 for i in 0..n_features {
674 for j in 0..n_features {
675 if i == j {
676 corr_matrix[(i, j)] = 1.0;
677 } else {
678 let col_i = data.column(i).to_owned();
679 let col_j = data.column(j).to_owned();
680 corr_matrix[(i, j)] = Self::pearson_correlation(&col_i, &col_j)?;
681 }
682 }
683 }
684
685 Ok(corr_matrix)
686 }
687
688 pub fn correlation_test(
690 correlation: f64,
691 n: usize,
692 alpha: f64,
693 ) -> Result<TestResult, UtilsError> {
694 if n < 3 {
695 return Err(UtilsError::InsufficientData { min: 3, actual: n });
696 }
697
698 let df = (n - 2) as f64;
699 let t_statistic = correlation * (df / (1.0 - correlation.powi(2))).sqrt();
700
701 let p_value = 2.0 * StatisticalTests::t_distribution_cdf(-t_statistic.abs(), df);
702
703 Ok(TestResult::new(
704 t_statistic,
705 p_value,
706 "Correlation significance test".to_string(),
707 alpha,
708 ))
709 }
710
711 fn compute_ranks(data: &Array1<f64>) -> Array1<f64> {
712 let mut indexed_data: Vec<(usize, f64)> =
713 data.iter().enumerate().map(|(i, &x)| (i, x)).collect();
714 indexed_data.sort_by(|a, b| compare_floats(&a.1, &b.1));
715
716 let mut ranks = Array1::zeros(data.len());
717
718 for (rank, (original_index, _)) in indexed_data.iter().enumerate() {
719 ranks[*original_index] = (rank + 1) as f64;
720 }
721
722 let mut i = 0;
724 while i < indexed_data.len() {
725 let current_value = indexed_data[i].1;
726 let mut j = i + 1;
727
728 while j < indexed_data.len() && (indexed_data[j].1 - current_value).abs() < f64::EPSILON
729 {
730 j += 1;
731 }
732
733 if j > i + 1 {
734 let average_rank = ((i + 1) + j) as f64 / 2.0;
736 for k in i..j {
737 ranks[indexed_data[k].0] = average_rank;
738 }
739 }
740
741 i = j;
742 }
743
744 ranks
745 }
746}
747
748pub struct DistributionFitting;
750
751impl DistributionFitting {
752 pub fn fit_normal(data: &Array1<f64>) -> Result<(f64, f64), UtilsError> {
754 if data.is_empty() {
755 return Err(UtilsError::EmptyInput);
756 }
757
758 let mean = safe_mean(data)?;
759 let std = StatisticalTests::standard_deviation(data);
760
761 Ok((mean, std))
762 }
763
764 pub fn fit_exponential(data: &Array1<f64>) -> Result<f64, UtilsError> {
766 if data.is_empty() {
767 return Err(UtilsError::EmptyInput);
768 }
769
770 if data.iter().any(|&x| x <= 0.0) {
772 return Err(UtilsError::InvalidParameter(
773 "Exponential distribution requires positive values".to_string(),
774 ));
775 }
776
777 let mean = safe_mean(data)?;
778 Ok(1.0 / mean) }
780
781 pub fn fit_uniform(data: &Array1<f64>) -> Result<(f64, f64), UtilsError> {
783 if data.is_empty() {
784 return Err(UtilsError::EmptyInput);
785 }
786
787 let min = data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
788 let max = data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
789
790 Ok((min, max))
791 }
792
793 pub fn goodness_of_fit_test(
795 data: &Array1<f64>,
796 expected_cdf: fn(f64, &[f64]) -> f64,
797 parameters: &[f64],
798 bins: usize,
799 alpha: f64,
800 ) -> Result<TestResult, UtilsError> {
801 if data.is_empty() {
802 return Err(UtilsError::EmptyInput);
803 }
804
805 if bins < 2 {
806 return Err(UtilsError::InvalidParameter(
807 "Number of bins must be at least 2".to_string(),
808 ));
809 }
810
811 let n = data.len() as f64;
812 let min_val = data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
813 let max_val = data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
814
815 let bin_width = (max_val - min_val) / bins as f64;
816 let mut observed = Array1::zeros(bins);
817 let mut expected = Array1::zeros(bins);
818
819 for &value in data.iter() {
821 let bin_index = ((value - min_val) / bin_width).floor() as usize;
822 let bin_index = bin_index.min(bins - 1);
823 observed[bin_index] += 1.0;
824 }
825
826 for i in 0..bins {
828 let lower = min_val + i as f64 * bin_width;
829 let upper = min_val + (i + 1) as f64 * bin_width;
830 let prob = expected_cdf(upper, parameters) - expected_cdf(lower, parameters);
831 expected[i] = n * prob;
832 }
833
834 StatisticalTests::chi_square_goodness_of_fit(&observed, &expected, alpha)
835 }
836}
837
838#[allow(non_snake_case)]
839#[cfg(test)]
840mod tests {
841 use super::*;
842 use approx::assert_abs_diff_eq;
843 use scirs2_core::ndarray::array;
844
845 #[test]
846 fn test_one_sample_ttest() {
847 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
848 let result =
849 StatisticalTests::one_sample_ttest(&data, 3.0, 0.05).expect("operation should succeed");
850
851 assert_eq!(result.test_name, "One-sample t-test");
852 assert!(!result.statistic.is_nan());
853 assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
854 }
855
856 #[test]
857 fn test_two_sample_ttest() {
858 let data1 = array![1.0, 2.0, 3.0, 4.0, 5.0];
859 let data2 = array![2.0, 3.0, 4.0, 5.0, 6.0];
860 let result = StatisticalTests::two_sample_ttest(&data1, &data2, 0.05)
861 .expect("operation should succeed");
862
863 assert_eq!(result.test_name, "Two-sample t-test");
864 assert!(!result.statistic.is_nan());
865 assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
866 }
867
868 #[test]
869 fn test_pearson_correlation() {
870 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
871 let y = array![2.0, 4.0, 6.0, 8.0, 10.0];
872 let correlation =
873 CorrelationAnalysis::pearson_correlation(&x, &y).expect("operation should succeed");
874
875 assert_abs_diff_eq!(correlation, 1.0, epsilon = 1e-10);
876 }
877
878 #[test]
879 fn test_spearman_correlation() {
880 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
881 let y = array![1.0, 4.0, 9.0, 16.0, 25.0]; let correlation =
883 CorrelationAnalysis::spearman_correlation(&x, &y).expect("operation should succeed");
884
885 assert_abs_diff_eq!(correlation, 1.0, epsilon = 1e-10);
886 }
887
888 #[test]
889 fn test_confidence_interval_mean() {
890 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
891 let ci = ConfidenceIntervals::mean_ci(&data, 0.95).expect("operation should succeed");
892
893 assert_eq!(ci.parameter, "Mean");
894 assert_eq!(ci.confidence_level, 0.95);
895 assert!(ci.lower < ci.upper);
896 assert!(ci.contains(3.0)); }
898
899 #[test]
900 fn test_confidence_interval_proportion() {
901 let ci =
902 ConfidenceIntervals::proportion_ci(30, 100, 0.95).expect("operation should succeed");
903
904 assert_eq!(ci.parameter, "Proportion");
905 assert!(ci.lower >= 0.0 && ci.upper <= 1.0);
906 assert!(ci.contains(0.3)); }
908
909 #[test]
910 fn test_chi_square_goodness_of_fit() {
911 let observed = array![10.0, 15.0, 8.0, 12.0];
912 let expected = array![11.25, 11.25, 11.25, 11.25];
913 let result = StatisticalTests::chi_square_goodness_of_fit(&observed, &expected, 0.05)
914 .expect("operation should succeed");
915
916 assert_eq!(result.test_name, "Chi-square goodness of fit");
917 assert!(result.statistic >= 0.0);
918 assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
919 }
920
921 #[test]
922 fn test_correlation_matrix() {
923 let data = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
924 let corr_matrix =
925 CorrelationAnalysis::correlation_matrix(&data).expect("operation should succeed");
926
927 assert_eq!(corr_matrix.shape(), &[3, 3]);
928
929 for i in 0..3 {
931 assert_abs_diff_eq!(corr_matrix[(i, i)], 1.0, epsilon = 1e-10);
932 }
933
934 for i in 0..3 {
936 for j in 0..3 {
937 assert_abs_diff_eq!(corr_matrix[(i, j)], corr_matrix[(j, i)], epsilon = 1e-10);
938 }
939 }
940 }
941
942 #[test]
943 fn test_distribution_fitting() {
944 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
945
946 let (mean, std) = DistributionFitting::fit_normal(&data).expect("operation should succeed");
947 assert_abs_diff_eq!(mean, 3.0, epsilon = 1e-10);
948 assert!(std > 0.0);
949
950 let (min, max) = DistributionFitting::fit_uniform(&data).expect("operation should succeed");
951 assert_eq!(min, 1.0);
952 assert_eq!(max, 5.0);
953 }
954
955 #[test]
956 fn test_kendall_tau() {
957 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
958 let y = array![1.0, 2.0, 3.0, 4.0, 5.0];
959 let tau = CorrelationAnalysis::kendall_tau(&x, &y).expect("operation should succeed");
960
961 assert_abs_diff_eq!(tau, 1.0, epsilon = 1e-10);
962 }
963}