Skip to main content

sklears_utils/
statistical.rs

1//! Statistical Utilities
2//!
3//! This module provides comprehensive statistical analysis utilities for machine learning,
4//! including statistical tests, confidence intervals, correlation analysis, hypothesis testing,
5//! and distribution fitting utilities.
6
7use crate::{math_utils::SpecialFunctions, UtilsError};
8use scirs2_core::ndarray::{Array1, Array2};
9use scirs2_core::numeric::Float;
10use std::cmp::Ordering;
11
12/// Helper function to safely compare floats, treating NaN as greater than all other values
13#[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/// Helper function to safely compute mean of an array
30#[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/// Statistical test results
40#[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/// Confidence interval
67#[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
94/// Statistical tests implementation
95pub struct StatisticalTests;
96
97impl StatisticalTests {
98    /// One-sample t-test
99    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        // Approximate p-value using t-distribution (simplified)
122        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    /// Two-sample t-test (assuming equal variances)
133    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        // Pooled standard deviation
150        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    /// Welch's t-test (unequal variances)
173    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        // Welch-Satterthwaite degrees of freedom
199        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    /// Chi-square goodness of fit test
213    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    /// Kolmogorov-Smirnov test for normality
251    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        // Approximate p-value using Kolmogorov distribution
277        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    /// Anderson-Darling test for normality
288    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        // Adjust for finite sample size
317        ad_statistic *= 1.0 + 0.75 / n + 2.25 / n.powi(2);
318
319        // Approximate p-value
320        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    // Helper functions for statistical distributions
348
349    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        // Compute mean manually to avoid Result handling in internal helper
359        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    /// Approximate normal CDF using error function
365    fn normal_cdf(x: f64) -> f64 {
366        0.5 * (1.0 + SpecialFunctions::erf(x / 2.0_f64.sqrt()))
367    }
368
369    /// Approximate t-distribution CDF (simplified)
370    fn t_distribution_cdf(t: f64, df: f64) -> f64 {
371        if df >= 30.0 {
372            // For large df, t-distribution approaches normal
373            return Self::normal_cdf(t);
374        }
375
376        // Simplified approximation for t-distribution
377        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    /// Approximate chi-square CDF
383    fn chi_square_cdf(x: f64, df: f64) -> f64 {
384        if x <= 0.0 {
385            return 0.0;
386        }
387
388        // Use incomplete gamma function
389        SpecialFunctions::gamma_inc(df / 2.0, x / 2.0) / SpecialFunctions::gamma(df / 2.0)
390    }
391
392    /// Approximate Kolmogorov-Smirnov p-value
393    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
410/// Confidence interval computation utilities
411pub struct ConfidenceIntervals;
412
413impl ConfidenceIntervals {
414    /// Confidence interval for mean (t-distribution)
415    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        // Approximate t-critical value (simplified)
438        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    /// Confidence interval for proportion
450    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        // Use normal approximation for large samples
472        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            // Use Wilson score interval for small samples
485            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    /// Confidence interval for variance
500    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        // Chi-square critical values (approximated)
517        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    // Helper functions for critical values
532
533    fn normal_critical_value(alpha: f64) -> f64 {
534        // Approximation for normal critical values
535        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        // Simplified t-critical value approximation
558        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        // Simplified chi-square critical value approximation
568        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
574/// Correlation analysis utilities
575pub struct CorrelationAnalysis;
576
577impl CorrelationAnalysis {
578    /// Pearson correlation coefficient
579    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    /// Spearman rank correlation coefficient
616    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    /// Kendall's tau correlation coefficient
631    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    /// Correlation matrix for multiple variables
665    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    /// Test correlation significance
689    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        // Handle ties by averaging ranks
723        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                // There are ties
735                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
748/// Distribution fitting utilities
749pub struct DistributionFitting;
750
751impl DistributionFitting {
752    /// Fit normal distribution parameters
753    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    /// Fit exponential distribution parameter
765    pub fn fit_exponential(data: &Array1<f64>) -> Result<f64, UtilsError> {
766        if data.is_empty() {
767            return Err(UtilsError::EmptyInput);
768        }
769
770        // Check for positive values
771        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) // Lambda parameter
779    }
780
781    /// Fit uniform distribution parameters
782    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    /// Goodness of fit test using chi-square
794    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        // Count observed frequencies
820        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        // Calculate expected frequencies
827        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]; // y = x^2
882        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)); // Should contain the sample mean
897    }
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)); // Should contain the sample proportion
907    }
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        // Diagonal should be 1.0
930        for i in 0..3 {
931            assert_abs_diff_eq!(corr_matrix[(i, i)], 1.0, epsilon = 1e-10);
932        }
933
934        // Matrix should be symmetric
935        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}