Skip to main content

u_analytics/
testing.rs

1//! Hypothesis testing.
2//!
3//! Parametric and non-parametric statistical tests: t-tests, ANOVA,
4//! chi-squared tests, and normality tests.
5//!
6//! # Examples
7//!
8//! ```
9//! use u_analytics::testing::{one_sample_t_test, TestResult};
10//!
11//! let data = [5.1, 4.9, 5.2, 5.0, 4.8, 5.3, 5.1, 4.9];
12//! let result = one_sample_t_test(&data, 5.0).unwrap();
13//! assert!(result.p_value > 0.05); // cannot reject H₀: μ = 5.0
14//! ```
15
16use u_numflow::special;
17use u_numflow::stats;
18
19/// Result of a hypothesis test.
20#[derive(Debug, Clone, Copy)]
21pub struct TestResult {
22    /// Test statistic (t, F, χ², or z depending on test).
23    pub statistic: f64,
24    /// Degrees of freedom (may be fractional for Welch).
25    pub df: f64,
26    /// Two-tailed p-value.
27    pub p_value: f64,
28}
29
30// ---------------------------------------------------------------------------
31// t-tests
32// ---------------------------------------------------------------------------
33
34/// One-sample t-test: H₀: μ = μ₀.
35///
36/// # Algorithm
37///
38/// t = (x̄ - μ₀) / (s / √n), df = n-1.
39///
40/// # Returns
41///
42/// `None` if fewer than 2 observations or non-finite values.
43///
44/// # Examples
45///
46/// ```
47/// use u_analytics::testing::one_sample_t_test;
48///
49/// let data = [2.0, 4.0, 6.0, 8.0, 10.0];
50/// let r = one_sample_t_test(&data, 6.0).unwrap();
51/// assert!(r.p_value > 0.5); // mean is 6.0
52/// ```
53pub fn one_sample_t_test(data: &[f64], mu0: f64) -> Option<TestResult> {
54    let n = data.len();
55    if n < 2 {
56        return None;
57    }
58    if data.iter().any(|v| !v.is_finite()) || !mu0.is_finite() {
59        return None;
60    }
61
62    let mean = stats::mean(data)?;
63    let sd = stats::std_dev(data)?;
64
65    if sd < 1e-300 {
66        return None; // zero variance
67    }
68
69    let t = (mean - mu0) / (sd / (n as f64).sqrt());
70    let df = (n - 1) as f64;
71    let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
72
73    Some(TestResult {
74        statistic: t,
75        df,
76        p_value,
77    })
78}
79
80/// Two-sample Welch t-test: H₀: μ₁ = μ₂ (unequal variances).
81///
82/// # Algorithm
83///
84/// t = (x̄₁ - x̄₂) / √(s₁²/n₁ + s₂²/n₂)
85/// df = Welch-Satterthwaite approximation.
86///
87/// # Returns
88///
89/// `None` if either sample has fewer than 2 observations.
90///
91/// # References
92///
93/// Welch (1947). "The generalization of Student's problem when several
94/// different population variances are involved". Biometrika, 34, 28–35.
95///
96/// # Examples
97///
98/// ```
99/// use u_analytics::testing::two_sample_t_test;
100///
101/// let a = [5.1, 4.9, 5.2, 5.0, 4.8];
102/// let b = [7.1, 6.9, 7.2, 7.0, 6.8];
103/// let r = two_sample_t_test(&a, &b).unwrap();
104/// assert!(r.p_value < 0.01); // means clearly differ
105/// ```
106pub fn two_sample_t_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
107    let n1 = a.len();
108    let n2 = b.len();
109    if n1 < 2 || n2 < 2 {
110        return None;
111    }
112    if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
113        return None;
114    }
115
116    let mean1 = stats::mean(a)?;
117    let mean2 = stats::mean(b)?;
118    let var1 = stats::variance(a)?;
119    let var2 = stats::variance(b)?;
120
121    let n1f = n1 as f64;
122    let n2f = n2 as f64;
123
124    let se_sq = var1 / n1f + var2 / n2f;
125    if se_sq < 1e-300 {
126        return None;
127    }
128
129    let t = (mean1 - mean2) / se_sq.sqrt();
130
131    // Welch-Satterthwaite degrees of freedom
132    let v1 = var1 / n1f;
133    let v2 = var2 / n2f;
134    let df = (v1 + v2).powi(2) / (v1 * v1 / (n1f - 1.0) + v2 * v2 / (n2f - 1.0));
135
136    let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
137
138    Some(TestResult {
139        statistic: t,
140        df,
141        p_value,
142    })
143}
144
145/// Paired t-test: H₀: mean difference = 0.
146///
147/// # Algorithm
148///
149/// Computes differences dᵢ = xᵢ - yᵢ, then applies one-sample t-test
150/// with μ₀ = 0.
151///
152/// # Returns
153///
154/// `None` if fewer than 2 pairs, slices differ in length, or non-finite values.
155///
156/// # Examples
157///
158/// ```
159/// use u_analytics::testing::paired_t_test;
160///
161/// let before = [5.0, 6.0, 7.0, 8.0, 9.0];
162/// let after  = [5.5, 6.2, 7.1, 8.3, 9.4];
163/// let r = paired_t_test(&before, &after).unwrap();
164/// assert!(r.statistic < 0.0); // after > before
165/// ```
166pub fn paired_t_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
167    if x.len() != y.len() || x.len() < 2 {
168        return None;
169    }
170
171    let diffs: Vec<f64> = x.iter().zip(y.iter()).map(|(&a, &b)| a - b).collect();
172    one_sample_t_test(&diffs, 0.0)
173}
174
175// ---------------------------------------------------------------------------
176// ANOVA
177// ---------------------------------------------------------------------------
178
179/// Result of one-way ANOVA.
180#[derive(Debug, Clone)]
181pub struct AnovaResult {
182    /// F-statistic.
183    pub f_statistic: f64,
184    /// Degrees of freedom between groups.
185    pub df_between: usize,
186    /// Degrees of freedom within groups.
187    pub df_within: usize,
188    /// p-value.
189    pub p_value: f64,
190    /// Sum of squares between groups.
191    pub ss_between: f64,
192    /// Sum of squares within groups.
193    pub ss_within: f64,
194    /// Mean square between.
195    pub ms_between: f64,
196    /// Mean square within.
197    pub ms_within: f64,
198    /// Group means.
199    pub group_means: Vec<f64>,
200    /// Grand mean.
201    pub grand_mean: f64,
202}
203
204/// One-way ANOVA: H₀: all group means are equal.
205///
206/// # Algorithm
207///
208/// F = MS_between / MS_within where
209/// MS_between = SS_between / (k-1),
210/// MS_within = SS_within / (N-k).
211///
212/// # Returns
213///
214/// `None` if fewer than 2 groups, any group has fewer than 2 observations,
215/// or non-finite values.
216///
217/// # References
218///
219/// Fisher (1925). "Statistical Methods for Research Workers".
220///
221/// # Examples
222///
223/// ```
224/// use u_analytics::testing::one_way_anova;
225///
226/// let group1 = [5.0, 6.0, 7.0, 5.5, 6.5];
227/// let group2 = [8.0, 9.0, 8.5, 9.5, 8.0];
228/// let group3 = [4.0, 3.0, 3.5, 4.5, 4.0];
229/// let r = one_way_anova(&[&group1, &group2, &group3]).unwrap();
230/// assert!(r.p_value < 0.01); // means clearly differ
231/// ```
232pub fn one_way_anova(groups: &[&[f64]]) -> Option<AnovaResult> {
233    let k = groups.len();
234    if k < 2 {
235        return None;
236    }
237
238    for g in groups {
239        if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
240            return None;
241        }
242    }
243
244    let total_n: usize = groups.iter().map(|g| g.len()).sum();
245
246    // Grand mean
247    let grand_sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
248    let grand_mean = grand_sum / total_n as f64;
249
250    // Group means
251    let group_means: Vec<f64> = groups
252        .iter()
253        .map(|g| g.iter().sum::<f64>() / g.len() as f64)
254        .collect();
255
256    // Sum of squares
257    let ss_between: f64 = groups
258        .iter()
259        .zip(group_means.iter())
260        .map(|(g, &gm)| g.len() as f64 * (gm - grand_mean).powi(2))
261        .sum();
262
263    let ss_within: f64 = groups
264        .iter()
265        .zip(group_means.iter())
266        .map(|(g, &gm)| g.iter().map(|&x| (x - gm).powi(2)).sum::<f64>())
267        .sum();
268
269    let df_between = k - 1;
270    let df_within = total_n - k;
271
272    if df_within == 0 {
273        return None;
274    }
275
276    let ms_between = ss_between / df_between as f64;
277    let ms_within = ss_within / df_within as f64;
278
279    let f_statistic = if ms_within > 1e-300 {
280        ms_between / ms_within
281    } else {
282        f64::INFINITY
283    };
284
285    let p_value = if f_statistic.is_infinite() {
286        0.0
287    } else {
288        1.0 - special::f_distribution_cdf(f_statistic, df_between as f64, df_within as f64)
289    };
290
291    Some(AnovaResult {
292        f_statistic,
293        df_between,
294        df_within,
295        p_value,
296        ss_between,
297        ss_within,
298        ms_between,
299        ms_within,
300        group_means,
301        grand_mean,
302    })
303}
304
305// ---------------------------------------------------------------------------
306// Chi-squared tests
307// ---------------------------------------------------------------------------
308
309/// Chi-squared goodness-of-fit test: H₀: observed matches expected distribution.
310///
311/// # Algorithm
312///
313/// χ² = Σ (Oᵢ - Eᵢ)² / Eᵢ, df = k-1.
314///
315/// # Returns
316///
317/// `None` if fewer than 2 categories, any expected frequency ≤ 0, or
318/// slices differ in length.
319///
320/// # Examples
321///
322/// ```
323/// use u_analytics::testing::chi_squared_goodness_of_fit;
324///
325/// let observed = [50.0, 30.0, 20.0];
326/// let expected = [40.0, 35.0, 25.0];
327/// let r = chi_squared_goodness_of_fit(&observed, &expected).unwrap();
328/// assert!(r.statistic > 0.0);
329/// ```
330pub fn chi_squared_goodness_of_fit(observed: &[f64], expected: &[f64]) -> Option<TestResult> {
331    let k = observed.len();
332    if k < 2 || k != expected.len() {
333        return None;
334    }
335
336    for &e in expected {
337        if e <= 0.0 || !e.is_finite() {
338            return None;
339        }
340    }
341    for &o in observed {
342        if o < 0.0 || !o.is_finite() {
343            return None;
344        }
345    }
346
347    let chi2: f64 = observed
348        .iter()
349        .zip(expected.iter())
350        .map(|(&o, &e)| (o - e).powi(2) / e)
351        .sum();
352
353    let df = (k - 1) as f64;
354    let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
355
356    Some(TestResult {
357        statistic: chi2,
358        df,
359        p_value,
360    })
361}
362
363/// Chi-squared test of independence on a contingency table.
364///
365/// # Arguments
366///
367/// * `table` — Flat row-major contingency table (rows × cols observed frequencies).
368/// * `n_rows` — Number of rows.
369/// * `n_cols` — Number of columns.
370///
371/// # Algorithm
372///
373/// Expected: Eᵢⱼ = (row_sumᵢ × col_sumⱼ) / N.
374/// χ² = Σᵢⱼ (Oᵢⱼ - Eᵢⱼ)² / Eᵢⱼ, df = (r-1)(c-1).
375///
376/// # Returns
377///
378/// `None` if fewer than 2 rows or columns, any cell is negative, or
379/// any marginal is zero.
380///
381/// # Examples
382///
383/// ```
384/// use u_analytics::testing::chi_squared_independence;
385///
386/// // 2×2 contingency table
387/// let table = [30.0, 10.0, 20.0, 40.0];
388/// let r = chi_squared_independence(&table, 2, 2).unwrap();
389/// assert!(r.p_value < 0.01);
390/// ```
391pub fn chi_squared_independence(table: &[f64], n_rows: usize, n_cols: usize) -> Option<TestResult> {
392    if n_rows < 2 || n_cols < 2 || table.len() != n_rows * n_cols {
393        return None;
394    }
395
396    for &v in table {
397        if v < 0.0 || !v.is_finite() {
398            return None;
399        }
400    }
401
402    // Row sums and column sums
403    let mut row_sums = vec![0.0; n_rows];
404    let mut col_sums = vec![0.0; n_cols];
405    let mut total = 0.0;
406
407    for i in 0..n_rows {
408        for j in 0..n_cols {
409            let val = table[i * n_cols + j];
410            row_sums[i] += val;
411            col_sums[j] += val;
412            total += val;
413        }
414    }
415
416    if total <= 0.0 {
417        return None;
418    }
419
420    // Check no zero marginals
421    for &r in &row_sums {
422        if r <= 0.0 {
423            return None;
424        }
425    }
426    for &c in &col_sums {
427        if c <= 0.0 {
428            return None;
429        }
430    }
431
432    // Compute chi-squared statistic
433    let mut chi2 = 0.0;
434    for i in 0..n_rows {
435        for j in 0..n_cols {
436            let observed = table[i * n_cols + j];
437            let expected = row_sums[i] * col_sums[j] / total;
438            chi2 += (observed - expected).powi(2) / expected;
439        }
440    }
441
442    let df = ((n_rows - 1) * (n_cols - 1)) as f64;
443    let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
444
445    Some(TestResult {
446        statistic: chi2,
447        df,
448        p_value,
449    })
450}
451
452// ---------------------------------------------------------------------------
453// Normality tests
454// ---------------------------------------------------------------------------
455
456/// Jarque-Bera normality test: H₀: data is normally distributed.
457///
458/// # Algorithm
459///
460/// JB = (n/6) · [S² + (K²/4)]
461///
462/// where S = skewness, K = excess kurtosis. JB ~ χ²(2) under H₀.
463///
464/// # Returns
465///
466/// `None` if fewer than 8 observations or non-finite values.
467///
468/// # References
469///
470/// Jarque & Bera (1987). "A test for normality of observations and
471/// regression residuals". International Statistical Review, 55(2), 163–172.
472///
473/// # Examples
474///
475/// ```
476/// use u_analytics::testing::jarque_bera_test;
477///
478/// // Near-normal data
479/// let data = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
480/// let r = jarque_bera_test(&data).unwrap();
481/// assert!(r.p_value > 0.05); // cannot reject normality
482/// ```
483pub fn jarque_bera_test(data: &[f64]) -> Option<TestResult> {
484    let n = data.len();
485    if n < 8 {
486        return None;
487    }
488    if data.iter().any(|v| !v.is_finite()) {
489        return None;
490    }
491
492    let s = stats::skewness(data)?;
493    let k = stats::kurtosis(data)?;
494
495    let nf = n as f64;
496    let jb = (nf / 6.0) * (s * s + k * k / 4.0);
497    let p_value = 1.0 - special::chi_squared_cdf(jb, 2.0);
498
499    Some(TestResult {
500        statistic: jb,
501        df: 2.0,
502        p_value,
503    })
504}
505
506/// Result of the Anderson-Darling normality test.
507#[derive(Debug, Clone, Copy)]
508pub struct AndersonDarlingResult {
509    /// The A² test statistic (raw, before sample-size correction).
510    pub statistic: f64,
511    /// The modified statistic A*² = A² × (1 + 0.75/n + 2.25/n²).
512    pub statistic_star: f64,
513    /// The p-value. Small values reject the null hypothesis of normality.
514    pub p_value: f64,
515}
516
517/// Anderson-Darling normality test: H₀: data is normally distributed.
518///
519/// More sensitive to tail deviations than Kolmogorov-Smirnov.
520///
521/// # Algorithm
522///
523/// 1. Standardize sorted data: zᵢ = (x₍ᵢ₎ - x̄) / s
524/// 2. Compute A² = -n - (1/n) Σᵢ (2i-1) [ln Φ(zᵢ) + ln(1 - Φ(z_{n+1-i}))]
525/// 3. Apply Stephens (1986) correction: A*² = A² (1 + 0.75/n + 2.25/n²)
526/// 4. Compute p-value from piecewise exponential approximation
527///
528/// # Returns
529///
530/// `None` if n < 8, all values identical, or non-finite values.
531///
532/// # References
533///
534/// - Anderson & Darling (1952). "Asymptotic theory of certain goodness of
535///   fit criteria based on stochastic processes". Annals of Mathematical
536///   Statistics, 23(2), 193–212.
537/// - Stephens (1986). "Tests based on EDF statistics". In D'Agostino &
538///   Stephens (Eds.), Goodness-of-Fit Techniques. Marcel Dekker.
539///
540/// # Examples
541///
542/// ```
543/// use u_analytics::testing::anderson_darling_test;
544///
545/// let data = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
546/// let r = anderson_darling_test(&data).unwrap();
547/// assert!(r.p_value > 0.05); // cannot reject normality
548/// ```
549pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
550    let n = data.len();
551    if n < 8 {
552        return None;
553    }
554    if data.iter().any(|v| !v.is_finite()) {
555        return None;
556    }
557
558    let mean = stats::mean(data)?;
559    let sd = stats::std_dev(data)?;
560
561    if sd < 1e-300 {
562        return None; // zero variance
563    }
564
565    // Sort data
566    let mut x: Vec<f64> = data.to_vec();
567    x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
568
569    let nf = n as f64;
570
571    // Compute A² statistic
572    let mut s = 0.0;
573    for i in 0..n {
574        let z = (x[i] - mean) / sd;
575        let phi = special::standard_normal_cdf(z);
576        // Clamp to avoid ln(0) or ln(negative)
577        let phi = phi.clamp(1e-15, 1.0 - 1e-15);
578
579        let z_rev = (x[n - 1 - i] - mean) / sd;
580        let phi_rev = special::standard_normal_cdf(z_rev);
581        let phi_rev = phi_rev.clamp(1e-15, 1.0 - 1e-15);
582
583        let coeff = (2 * (i + 1) - 1) as f64;
584        s += coeff * (phi.ln() + (1.0 - phi_rev).ln());
585    }
586
587    let a2 = -nf - s / nf;
588
589    // Stephens (1986) correction for sample size
590    let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
591
592    // P-value from piecewise approximation (D'Agostino & Stephens 1986)
593    let p = if a2_star >= 0.6 {
594        (1.2937 - 5.709 * a2_star + 0.0186 * a2_star * a2_star).exp()
595    } else if a2_star > 0.34 {
596        (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
597    } else if a2_star > 0.2 {
598        1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
599    } else {
600        1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
601    };
602
603    Some(AndersonDarlingResult {
604        statistic: a2,
605        statistic_star: a2_star,
606        p_value: p.clamp(0.0, 1.0),
607    })
608}
609
610/// Result of the Anderson-Darling normality test (Stephens 1974 variant).
611///
612/// This variant uses the `statistic_modified` field name and accepts n ≥ 3,
613/// making it suitable for small-sample normality checking before control
614/// charts and Box-Cox capability analysis.
615#[derive(Debug, Clone, Copy)]
616pub struct AdNormalityResult {
617    /// The A² test statistic (raw).
618    pub statistic: f64,
619    /// The modified statistic A²* = A² · (1 + 0.75/n + 2.25/n²).
620    pub statistic_modified: f64,
621    /// Approximate p-value from Stephens (1974) piecewise approximation.
622    pub p_value: f64,
623}
624
625/// Anderson-Darling normality test (Stephens 1974): H₀: data is normally distributed.
626///
627/// Suitable for small samples (n ≥ 3). Provides the A²* modified statistic and
628/// approximate p-values using Stephens (1974) piecewise exponential formulae.
629/// Prefer this function when you need a lightweight normality pre-check before
630/// applying control charts or Box-Cox capability analysis.
631///
632/// # Algorithm
633///
634/// 1. Sort ascending, compute mean and std_dev.
635/// 2. Standardize: zᵢ = (x_(i) − mean) / std.
636/// 3. A² = −n − (1/n) · Σᵢ₌₀ⁿ⁻¹ (2i+1) · [ln Φ(zᵢ) + ln(1 − Φ(z_{n−1−i}))].
637/// 4. A²* = A² · (1 + 0.75/n + 2.25/n²).
638/// 5. p-value from Stephens (1974) piecewise approximation.
639///
640/// # Returns
641///
642/// `None` if n < 3, any non-finite value, or std_dev < 1e-15 (degenerate data).
643///
644/// # References
645///
646/// - Stephens, M. A. (1974). "EDF statistics for goodness of fit and some
647///   comparisons". *Journal of the American Statistical Association*, 69(347), 730–737.
648///
649/// # Examples
650///
651/// ```
652/// use u_analytics::testing::anderson_darling_normality;
653///
654/// let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
655/// let r = anderson_darling_normality(&data).unwrap();
656/// assert!(r.p_value > 0.05); // cannot reject normality
657/// ```
658pub fn anderson_darling_normality(data: &[f64]) -> Option<AdNormalityResult> {
659    let n = data.len();
660    if n < 3 {
661        return None;
662    }
663    if data.iter().any(|v| !v.is_finite()) {
664        return None;
665    }
666
667    let mean = stats::mean(data)?;
668    let sd = stats::std_dev(data)?;
669
670    if sd < 1e-15 {
671        return None;
672    }
673
674    let mut x: Vec<f64> = data.to_vec();
675    x.sort_by(|a, b| a.partial_cmp(b).expect("values are finite"));
676
677    let nf = n as f64;
678
679    let mut s = 0.0;
680    for i in 0..n {
681        let z_i = (x[i] - mean) / sd;
682        let z_rev = (x[n - 1 - i] - mean) / sd;
683
684        let phi_i = special::standard_normal_cdf(z_i).clamp(1e-15, 1.0 - 1e-15);
685        let phi_rev = special::standard_normal_cdf(z_rev).clamp(1e-15, 1.0 - 1e-15);
686
687        let coeff = (2 * i + 1) as f64;
688        s += coeff * (phi_i.ln() + (1.0 - phi_rev).ln());
689    }
690
691    let a2 = -nf - s / nf;
692    let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
693
694    // Stephens (1974) piecewise p-value approximation
695    let p = if a2_star < 0.200 {
696        1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
697    } else if a2_star < 0.340 {
698        1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
699    } else if a2_star < 0.600 {
700        (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
701    } else {
702        (1.2937 - 5.709 * a2_star + 0.0186 * a2_star * a2_star).exp()
703    };
704
705    Some(AdNormalityResult {
706        statistic: a2,
707        statistic_modified: a2_star,
708        p_value: p.clamp(0.0, 1.0),
709    })
710}
711
712/// Result of the Shapiro-Wilk normality test.
713#[derive(Debug, Clone, Copy)]
714pub struct ShapiroWilkResult {
715    /// The W statistic (0 < W ≤ 1). Values close to 1 suggest normality.
716    pub w: f64,
717    /// The p-value. Small values reject the null hypothesis of normality.
718    pub p_value: f64,
719}
720
721/// Shapiro-Wilk normality test: H₀: data is normally distributed.
722///
723/// The most powerful general normality test for small to moderate samples.
724///
725/// # Algorithm
726///
727/// Uses the Royston (1992, 1995) algorithm (AS R94):
728/// 1. Compute coefficients from normal order statistics (Blom approximation)
729/// 2. Calculate W = (Σ aᵢ x₍ᵢ₎)² / Σ (xᵢ - x̄)²
730/// 3. Transform W to z-score via log-normal approximation
731/// 4. Compute p-value from standard normal distribution
732///
733/// # Supported range
734///
735/// n = 3..5000. Returns `None` outside this range.
736///
737/// # Returns
738///
739/// `None` if n < 3, n > 5000, all values identical, or non-finite values.
740///
741/// # References
742///
743/// - Shapiro & Wilk (1965). "An analysis of variance test for normality".
744///   Biometrika, 52(3–4), 591–611.
745/// - Royston (1992). "Approximating the Shapiro-Wilk W-test for
746///   non-normality". Statistics and Computing, 2, 117–119.
747/// - Royston (1995). "Remark AS R94: A remark on Algorithm AS 181".
748///   Applied Statistics, 44(4), 547–551.
749///
750/// # Examples
751///
752/// ```
753/// use u_analytics::testing::shapiro_wilk_test;
754///
755/// let data = [-1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5];
756/// let r = shapiro_wilk_test(&data).unwrap();
757/// assert!(r.w > 0.9);
758/// assert!(r.p_value > 0.05); // cannot reject normality
759/// ```
760pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
761    let n = data.len();
762    if !(3..=5000).contains(&n) {
763        return None;
764    }
765    if data.iter().any(|v| !v.is_finite()) {
766        return None;
767    }
768
769    // Sort data
770    let mut x: Vec<f64> = data.to_vec();
771    x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
772
773    if x[n - 1] - x[0] < 1e-300 {
774        return None; // all values identical
775    }
776
777    let nn2 = n / 2;
778
779    // Special case n = 3
780    if n == 3 {
781        return shapiro_wilk_n3(&x);
782    }
783
784    // Compute coefficients via Royston algorithm
785    let a = sw_coefficients(n, nn2)?;
786
787    // Compute W statistic
788    let w = sw_statistic(&x, &a, n, nn2);
789
790    if !(0.0..=1.0 + 1e-10).contains(&w) {
791        return None;
792    }
793    let w = w.min(1.0);
794
795    // Compute p-value
796    let p_value = sw_p_value(w, n);
797
798    Some(ShapiroWilkResult {
799        w,
800        p_value: p_value.clamp(0.0, 1.0),
801    })
802}
803
804// Shapiro-Wilk: n=3 exact formula
805fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
806    // For n=3: a = [sqrt(1/2), 0, -sqrt(1/2)]
807    let a1 = std::f64::consts::FRAC_1_SQRT_2; // 0.7071...
808    let mean = (x[0] + x[1] + x[2]) / 3.0;
809    let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
810    if ss < 1e-300 {
811        return None;
812    }
813
814    let numerator = a1 * (x[2] - x[0]);
815    let w = (numerator * numerator) / ss;
816    let w = w.clamp(0.75, 1.0);
817
818    // Exact p-value for n=3: p = 1 - (6/pi) * arccos(sqrt(w))
819    let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
820    let p = p.clamp(0.0, 1.0);
821
822    Some(ShapiroWilkResult { w, p_value: p })
823}
824
825// Royston polynomial coefficients (AS R94)
826const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
827const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
828const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
829const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
830const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
831const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
832const SW_G: [f64; 2] = [-2.273, 0.459];
833
834// Evaluate polynomial: c[0] + c[1]*x + c[2]*x^2 + ... (Horner's method)
835fn sw_poly(c: &[f64], x: f64) -> f64 {
836    let mut result = c[c.len() - 1];
837    for i in (0..c.len() - 1).rev() {
838        result = result * x + c[i];
839    }
840    result
841}
842
843// Compute Shapiro-Wilk coefficients using Royston's algorithm
844fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
845    let mut a = vec![0.0; nn2];
846
847    // Blom's approximation for expected normal order statistics
848    let mut m = vec![0.0; nn2];
849    let mut summ2 = 0.0;
850    for (i, mi) in m.iter_mut().enumerate() {
851        // m[i] corresponds to the (i+1)-th order statistic expectation
852        let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
853        *mi = special::inverse_normal_cdf(p);
854        summ2 += *mi * *mi;
855    }
856    summ2 *= 2.0;
857    let ssumm2 = summ2.sqrt();
858    let rsn = 1.0 / (n as f64).sqrt();
859
860    // First coefficient: polynomial correction
861    let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
862
863    if n <= 5 {
864        // For n=4,5: only a[0] is corrected
865        let fac_sq = summ2 - 2.0 * m[0] * m[0];
866        let one_minus = 1.0 - 2.0 * a1 * a1;
867        if fac_sq <= 0.0 || one_minus <= 0.0 {
868            return None;
869        }
870        let fac = (fac_sq / one_minus).sqrt();
871        a[0] = a1;
872        for i in 1..nn2 {
873            a[i] = -m[i] / fac;
874        }
875    } else {
876        // For n>5: a[0] and a[1] are corrected
877        let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
878        let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
879        let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
880        if fac_sq <= 0.0 || one_minus <= 0.0 {
881            return None;
882        }
883        let fac = (fac_sq / one_minus).sqrt();
884        a[0] = a1;
885        a[1] = a2;
886        for i in 2..nn2 {
887            a[i] = -m[i] / fac;
888        }
889    }
890
891    Some(a)
892}
893
894// Compute W statistic from sorted data, coefficients, and range
895fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
896    // Numerator: (sum a_i * (x_{n+1-i} - x_i))^2
897    let mut sa = 0.0;
898    for i in 0..nn2 {
899        sa += a[i] * (x[n - 1 - i] - x[i]);
900    }
901
902    // Denominator: sum of squares about the mean
903    let mean = x.iter().sum::<f64>() / n as f64;
904    let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
905
906    if ss < 1e-300 {
907        return 1.0; // degenerate
908    }
909
910    (sa * sa) / ss
911}
912
913// Compute p-value from W statistic using Royston's transformation
914fn sw_p_value(w: f64, n: usize) -> f64 {
915    let nf = n as f64;
916
917    if n == 3 {
918        // Should not reach here (handled separately), but just in case
919        let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
920        return p.clamp(0.0, 1.0);
921    }
922
923    let w1 = 1.0 - w;
924    if w1 <= 0.0 {
925        return 1.0; // perfectly normal
926    }
927
928    let y = w1.ln();
929
930    if n <= 11 {
931        // Small sample: gamma + log transformation
932        let gamma = sw_poly(&SW_G, nf);
933        if y >= gamma {
934            return 0.0; // extremely non-normal
935        }
936        let y2 = -(gamma - y).ln();
937        let m = sw_poly(&SW_C3, nf);
938        let s = sw_poly(&SW_C4, nf).exp();
939        if s < 1e-300 {
940            return 0.0;
941        }
942        let z = (y2 - m) / s;
943        1.0 - special::standard_normal_cdf(z)
944    } else {
945        // Large sample: log-normal transformation
946        let xx = nf.ln();
947        let m = sw_poly(&SW_C5, xx);
948        let s = sw_poly(&SW_C6, xx).exp();
949        if s < 1e-300 {
950            return 0.0;
951        }
952        let z = (y - m) / s;
953        1.0 - special::standard_normal_cdf(z)
954    }
955}
956
957// ---------------------------------------------------------------------------
958// Non-parametric tests
959// ---------------------------------------------------------------------------
960
961/// Mann-Whitney U test: H₀: the two populations have the same distribution.
962///
963/// Non-parametric alternative to the two-sample t-test. Does not assume
964/// normality.
965///
966/// # Algorithm
967///
968/// 1. Combine samples, rank all observations (average ranks for ties)
969/// 2. U₁ = R₁ - n₁(n₁+1)/2 where R₁ = sum of ranks in sample 1
970/// 3. Normal approximation: z = (U₁ - μ) / σ
971///    where μ = n₁n₂/2, σ² includes tie correction
972///
973/// # Returns
974///
975/// `None` if either sample has fewer than 2 observations or non-finite values.
976///
977/// # References
978///
979/// - Mann & Whitney (1947). "On a test of whether one of two random
980///   variables is stochastically larger than the other". Annals of
981///   Mathematical Statistics, 18(1), 50–60.
982///
983/// # Examples
984///
985/// ```
986/// use u_analytics::testing::mann_whitney_u_test;
987///
988/// let a = [1.0, 2.0, 3.0, 4.0, 5.0];
989/// let b = [6.0, 7.0, 8.0, 9.0, 10.0];
990/// let r = mann_whitney_u_test(&a, &b).unwrap();
991/// assert!(r.p_value < 0.05);
992/// ```
993pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
994    let n1 = a.len();
995    let n2 = b.len();
996    if n1 < 2 || n2 < 2 {
997        return None;
998    }
999    if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
1000        return None;
1001    }
1002
1003    let n = n1 + n2;
1004    let n1f = n1 as f64;
1005    let n2f = n2 as f64;
1006    let nf = n as f64;
1007
1008    // Combine and rank
1009    let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
1010    for &v in a {
1011        combined.push((v, 0)); // group 0 = sample a
1012    }
1013    for &v in b {
1014        combined.push((v, 1)); // group 1 = sample b
1015    }
1016    combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
1017
1018    // Assign average ranks and track ties
1019    let ranks = average_ranks(&combined);
1020
1021    // Sum of ranks for sample a
1022    let r1: f64 = combined
1023        .iter()
1024        .zip(ranks.iter())
1025        .filter(|((_, g), _)| *g == 0)
1026        .map(|(_, &r)| r)
1027        .sum();
1028
1029    // U statistic
1030    let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
1031
1032    // Tie correction
1033    let tie_correction = compute_tie_correction(&combined);
1034
1035    // Normal approximation
1036    let mu = n1f * n2f / 2.0;
1037    let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
1038
1039    if sigma_sq <= 0.0 {
1040        return None;
1041    }
1042
1043    let z = (u1 - mu) / sigma_sq.sqrt();
1044    let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
1045
1046    Some(TestResult {
1047        statistic: u1,
1048        df: 0.0, // not applicable for non-parametric
1049        p_value,
1050    })
1051}
1052
1053/// Wilcoxon signed-rank test: H₀: median of differences = 0.
1054///
1055/// Non-parametric alternative to the paired t-test. Does not assume
1056/// normality of differences.
1057///
1058/// # Algorithm
1059///
1060/// 1. Compute differences dᵢ = xᵢ - yᵢ, discard zeros
1061/// 2. Rank |dᵢ| (average ranks for ties)
1062/// 3. T⁺ = sum of ranks where dᵢ > 0
1063/// 4. Normal approximation: z = (T⁺ - μ) / σ
1064///    where μ = n(n+1)/4, σ² includes tie correction
1065///
1066/// # Returns
1067///
1068/// `None` if fewer than 2 non-zero differences, slices differ in length,
1069/// or non-finite values.
1070///
1071/// # References
1072///
1073/// - Wilcoxon (1945). "Individual comparisons by ranking methods".
1074///   Biometrics Bulletin, 1(6), 80–83.
1075///
1076/// # Examples
1077///
1078/// ```
1079/// use u_analytics::testing::wilcoxon_signed_rank_test;
1080///
1081/// let before = [5.0, 6.0, 7.0, 8.0, 9.0];
1082/// let after  = [6.0, 7.5, 8.0, 9.5, 11.0];
1083/// let r = wilcoxon_signed_rank_test(&after, &before).unwrap();
1084/// assert!(r.statistic > 0.0); // T+ sum of positive ranks
1085/// ```
1086pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1087    if x.len() != y.len() || x.len() < 2 {
1088        return None;
1089    }
1090    if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1091        return None;
1092    }
1093
1094    // Compute differences and discard zeros
1095    let diffs: Vec<f64> = x
1096        .iter()
1097        .zip(y.iter())
1098        .map(|(&a, &b)| a - b)
1099        .filter(|&d| d.abs() > 1e-300)
1100        .collect();
1101
1102    let nr = diffs.len();
1103    if nr < 2 {
1104        return None;
1105    }
1106
1107    let nf = nr as f64;
1108
1109    // Sort by absolute difference and rank
1110    let mut abs_diffs: Vec<(f64, usize)> = diffs
1111        .iter()
1112        .enumerate()
1113        .map(|(i, &d)| (d.abs(), i))
1114        .collect();
1115    abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1116
1117    // Assign average ranks
1118    let ranks = average_ranks(&abs_diffs);
1119
1120    // T+ = sum of ranks where the original difference is positive
1121    let t_plus: f64 = abs_diffs
1122        .iter()
1123        .zip(ranks.iter())
1124        .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1125        .map(|(_, &r)| r)
1126        .sum();
1127
1128    // Tie correction for variance
1129    let tie_correction_val = compute_tie_correction(&abs_diffs);
1130
1131    // Normal approximation
1132    let mu = nf * (nf + 1.0) / 4.0;
1133    let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1134
1135    if sigma_sq <= 0.0 {
1136        return None;
1137    }
1138
1139    let z = (t_plus - mu) / sigma_sq.sqrt();
1140    let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
1141
1142    Some(TestResult {
1143        statistic: t_plus,
1144        df: 0.0,
1145        p_value,
1146    })
1147}
1148
1149// Assign average ranks to sorted (value, group_or_index) pairs.
1150// Handles ties by assigning the average of the tied ranks.
1151fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1152    let n = sorted.len();
1153    let mut ranks = vec![0.0; n];
1154    let mut i = 0;
1155    while i < n {
1156        let mut j = i + 1;
1157        while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1158            j += 1;
1159        }
1160        // Positions i..j are tied; average rank = (i+1 + j) / 2
1161        let avg_rank = (i + 1 + j) as f64 / 2.0;
1162        for rank in ranks.iter_mut().take(j).skip(i) {
1163            *rank = avg_rank;
1164        }
1165        i = j;
1166    }
1167    ranks
1168}
1169
1170// Compute tie correction factor: Σ tₖ(tₖ² - 1) for all tie groups
1171fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1172    let n = sorted.len();
1173    let mut correction = 0.0;
1174    let mut i = 0;
1175    while i < n {
1176        let mut j = i + 1;
1177        while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1178            j += 1;
1179        }
1180        let t = (j - i) as f64;
1181        if t > 1.0 {
1182            correction += t * (t * t - 1.0);
1183        }
1184        i = j;
1185    }
1186    correction
1187}
1188
1189/// Kruskal-Wallis test: H₀: all groups have the same distribution.
1190///
1191/// Non-parametric alternative to one-way ANOVA. Does not assume normality.
1192///
1193/// # Algorithm
1194///
1195/// 1. Combine all groups, rank observations (average ranks for ties)
1196/// 2. H = (12 / N(N+1)) Σ nᵢ (R̄ᵢ - R̄)² with tie correction
1197/// 3. H ~ χ²(k-1) under H₀
1198///
1199/// # Returns
1200///
1201/// `None` if fewer than 2 groups, any group has fewer than 2 observations,
1202/// or non-finite values.
1203///
1204/// # References
1205///
1206/// - Kruskal & Wallis (1952). "Use of ranks in one-criterion variance
1207///   analysis". JASA, 47(260), 583–621.
1208///
1209/// # Examples
1210///
1211/// ```
1212/// use u_analytics::testing::kruskal_wallis_test;
1213///
1214/// let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
1215/// let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
1216/// let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
1217/// let r = kruskal_wallis_test(&[&g1, &g2, &g3]).unwrap();
1218/// assert!(r.p_value < 0.01);
1219/// ```
1220pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1221    let k = groups.len();
1222    if k < 2 {
1223        return None;
1224    }
1225    for g in groups {
1226        if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1227            return None;
1228        }
1229    }
1230
1231    let total_n: usize = groups.iter().map(|g| g.len()).sum();
1232    let nf = total_n as f64;
1233
1234    // Combine all observations with group labels
1235    let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1236    for (gi, g) in groups.iter().enumerate() {
1237        for &v in *g {
1238            combined.push((v, gi));
1239        }
1240    }
1241    combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1242
1243    let ranks = average_ranks(&combined);
1244
1245    // Sum of ranks per group
1246    let mut rank_sums = vec![0.0; k];
1247    for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1248        rank_sums[*gi] += r;
1249    }
1250
1251    // H statistic: H = (12 / N(N+1)) * Σ Rᵢ²/nᵢ - 3(N+1)
1252    let mean_rank = (nf + 1.0) / 2.0;
1253    let mut h = 0.0;
1254    for (gi, g) in groups.iter().enumerate() {
1255        let ni = g.len() as f64;
1256        let mean_rank_i = rank_sums[gi] / ni;
1257        h += ni * (mean_rank_i - mean_rank).powi(2);
1258    }
1259    h *= 12.0 / (nf * (nf + 1.0));
1260
1261    // Tie correction: divide by 1 - Σtₖ(tₖ²-1) / (N³ - N)
1262    let tie_corr = compute_tie_correction(&combined);
1263    let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1264    if denom > 1e-15 {
1265        h /= denom;
1266    }
1267
1268    let df = (k - 1) as f64;
1269    let p_value = 1.0 - special::chi_squared_cdf(h, df);
1270
1271    Some(TestResult {
1272        statistic: h,
1273        df,
1274        p_value,
1275    })
1276}
1277
1278// ---------------------------------------------------------------------------
1279// Variance tests
1280// ---------------------------------------------------------------------------
1281
1282/// Levene test for equality of variances: H₀: all groups have equal variance.
1283///
1284/// Robust to non-normality. Uses the **median** variant (Brown-Forsythe),
1285/// which is recommended for non-normal data.
1286///
1287/// # Algorithm
1288///
1289/// 1. Compute zᵢⱼ = |xᵢⱼ - median(groupᵢ)|
1290/// 2. Apply one-way ANOVA on the zᵢⱼ values
1291///
1292/// # Returns
1293///
1294/// `None` if fewer than 2 groups, any group < 2 observations, or non-finite values.
1295///
1296/// # References
1297///
1298/// - Levene (1960). "Robust tests for equality of variances". In
1299///   Olkin (Ed.), Contributions to Probability and Statistics.
1300/// - Brown & Forsythe (1974). "Robust tests for the equality of variances".
1301///   JASA, 69(346), 364–367.
1302///
1303/// # Examples
1304///
1305/// ```
1306/// use u_analytics::testing::levene_test;
1307///
1308/// let g1 = [4.9, 5.0, 5.0, 5.1, 5.0]; // tight cluster (low variance)
1309/// let g2 = [0.0, 3.0, 5.0, 7.0, 10.0]; // wide spread (high variance)
1310/// let r = levene_test(&[&g1, &g2]).unwrap();
1311/// assert!(r.p_value < 0.05); // clear variance difference
1312/// ```
1313pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1314    let k = groups.len();
1315    if k < 2 {
1316        return None;
1317    }
1318    for g in groups {
1319        if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1320            return None;
1321        }
1322    }
1323
1324    // Compute z-values: |x - median(group)| (Brown-Forsythe variant)
1325    let z_groups: Vec<Vec<f64>> = groups
1326        .iter()
1327        .map(|g| {
1328            let median = stats::median(g).unwrap_or(0.0);
1329            g.iter().map(|&x| (x - median).abs()).collect()
1330        })
1331        .collect();
1332
1333    // Apply ANOVA on the z-values
1334    let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1335    let anova = one_way_anova(&z_refs)?;
1336
1337    Some(TestResult {
1338        statistic: anova.f_statistic,
1339        df: anova.df_between as f64,
1340        p_value: anova.p_value,
1341    })
1342}
1343
1344// ---------------------------------------------------------------------------
1345// Multiple comparison correction
1346// ---------------------------------------------------------------------------
1347
1348/// Bonferroni correction: adjusts p-values for multiple comparisons.
1349///
1350/// adjusted_pᵢ = min(pᵢ × m, 1.0) where m = number of tests.
1351///
1352/// # Returns
1353///
1354/// `None` if the slice is empty or contains non-finite values.
1355pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1356    if p_values.is_empty() || p_values.iter().any(|v| !v.is_finite()) {
1357        return None;
1358    }
1359    let m = p_values.len() as f64;
1360    Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1361}
1362
1363/// Benjamini-Hochberg FDR correction.
1364///
1365/// Controls false discovery rate at level α.
1366///
1367/// # Algorithm
1368///
1369/// 1. Sort p-values.
1370/// 2. For rank i (1-indexed): adjusted_pᵢ = pᵢ × m / i.
1371/// 3. Enforce monotonicity (cumulative minimum from right).
1372///
1373/// # Returns
1374///
1375/// `None` if the slice is empty or contains non-finite values.
1376///
1377/// # References
1378///
1379/// Benjamini & Hochberg (1995). "Controlling the false discovery rate".
1380/// JRSS-B, 57(1), 289–300.
1381pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1382    let m = p_values.len();
1383    if m == 0 || p_values.iter().any(|v| !v.is_finite()) {
1384        return None;
1385    }
1386
1387    // Sort indices by p-value
1388    let mut indices: Vec<usize> = (0..m).collect();
1389    indices.sort_by(|&a, &b| {
1390        p_values[a]
1391            .partial_cmp(&p_values[b])
1392            .unwrap_or(std::cmp::Ordering::Equal)
1393    });
1394
1395    let mf = m as f64;
1396    let mut adjusted = vec![0.0; m];
1397
1398    // Compute adjusted p-values
1399    let mut cummin = f64::INFINITY;
1400    for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1401        let rank = rank_rev + 1; // 1-indexed
1402        let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1403        cummin = cummin.min(adj);
1404        adjusted[orig_idx] = cummin;
1405    }
1406
1407    Some(adjusted)
1408}
1409
1410// ---------------------------------------------------------------------------
1411// Bartlett test for equality of variances
1412// ---------------------------------------------------------------------------
1413
1414/// Bartlett test for equality of variances: H₀: all groups have equal variance.
1415///
1416/// Assumes data are from **normal** distributions. For non-normal data, prefer
1417/// [`levene_test`] (Brown-Forsythe variant).
1418///
1419/// # Algorithm
1420///
1421/// 1. Compute pooled variance: s²ₚ = Σ(nᵢ-1)s²ᵢ / (N-k)
1422/// 2. Numerator: (N-k) ln(s²ₚ) - Σ(nᵢ-1) ln(s²ᵢ)
1423/// 3. Correction factor: C = 1 + [1/(3(k-1))] × [Σ 1/(nᵢ-1) - 1/(N-k)]
1424/// 4. Statistic: T = numerator / C ~ χ²(k-1)
1425///
1426/// # Returns
1427///
1428/// `None` if fewer than 2 groups, any group < 2 observations, any group has
1429/// zero variance, or non-finite values.
1430///
1431/// # References
1432///
1433/// - Bartlett (1937). "Properties of sufficiency and statistical tests".
1434///   Proceedings of the Royal Society A, 160(901), 268–282.
1435///
1436/// # Examples
1437///
1438/// ```
1439/// use u_analytics::testing::bartlett_test;
1440///
1441/// let g1 = [2.0, 3.0, 4.0, 5.0, 6.0]; // variance ~2.5
1442/// let g2 = [10.0, 20.0, 30.0, 40.0, 50.0]; // variance ~250
1443/// let r = bartlett_test(&[&g1, &g2]).unwrap();
1444/// assert!(r.p_value < 0.01); // strongly different variances
1445/// ```
1446pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1447    let k = groups.len();
1448    if k < 2 {
1449        return None;
1450    }
1451
1452    let mut sizes = Vec::with_capacity(k);
1453    let mut vars = Vec::with_capacity(k);
1454    let mut n_total: usize = 0;
1455
1456    for g in groups {
1457        if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1458            return None;
1459        }
1460        let n = g.len();
1461        let v = stats::variance(g)?;
1462        if v <= 0.0 {
1463            return None; // zero variance → ln undefined
1464        }
1465        sizes.push(n);
1466        vars.push(v);
1467        n_total += n;
1468    }
1469
1470    let nk = n_total - k; // N - k
1471    if nk == 0 {
1472        return None;
1473    }
1474    let nk_f = nk as f64;
1475
1476    // Pooled variance
1477    let s2_pooled: f64 = sizes
1478        .iter()
1479        .zip(vars.iter())
1480        .map(|(&n, &v)| (n as f64 - 1.0) * v)
1481        .sum::<f64>()
1482        / nk_f;
1483
1484    if s2_pooled <= 0.0 {
1485        return None;
1486    }
1487
1488    // Numerator: (N-k) ln(s²ₚ) - Σ(nᵢ-1) ln(s²ᵢ)
1489    let num = nk_f * s2_pooled.ln()
1490        - sizes
1491            .iter()
1492            .zip(vars.iter())
1493            .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1494            .sum::<f64>();
1495
1496    // Correction factor C
1497    let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1498    let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1499
1500    let statistic = num / c;
1501    let df = (k - 1) as f64;
1502    let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
1503
1504    Some(TestResult {
1505        statistic,
1506        df,
1507        p_value,
1508    })
1509}
1510
1511// ---------------------------------------------------------------------------
1512// Fisher exact test (2×2)
1513// ---------------------------------------------------------------------------
1514
1515/// Fisher exact test for a 2×2 contingency table.
1516///
1517/// Tests H₀: the two categorical variables are independent.
1518/// Unlike the chi-squared test, this is exact and valid for small samples.
1519///
1520/// # Arguments
1521///
1522/// The 2×2 table is specified as four cell counts:
1523///
1524/// ```text
1525///          Col1   Col2
1526///   Row1 |  a   |  b  |
1527///   Row2 |  c   |  d  |
1528/// ```
1529///
1530/// # Algorithm
1531///
1532/// 1. Compute probability of observed table via hypergeometric distribution
1533///    (using log-factorials for numerical stability).
1534/// 2. Enumerate all tables with the same marginals.
1535/// 3. Two-tailed p-value = sum of probabilities ≤ P(observed).
1536///
1537/// # Returns
1538///
1539/// `None` if any marginal total is zero (degenerate table).
1540///
1541/// # References
1542///
1543/// - Fisher (1922). "On the interpretation of χ² from contingency tables,
1544///   and the calculation of P". JRSS, 85(1), 87–94.
1545///
1546/// # Examples
1547///
1548/// ```
1549/// use u_analytics::testing::fisher_exact_test;
1550///
1551/// // Tea-tasting experiment
1552/// let r = fisher_exact_test(3, 1, 1, 3).unwrap();
1553/// assert!(r.p_value > 0.05); // not significant at 5%
1554/// ```
1555pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1556    let row1 = a + b;
1557    let row2 = c + d;
1558    let col1 = a + c;
1559    let col2 = b + d;
1560    let n = a + b + c + d;
1561
1562    // Degenerate if any marginal is zero
1563    if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1564        return None;
1565    }
1566
1567    // Log-probability of a specific table given marginals
1568    let log_prob = |a_i: u64| -> f64 {
1569        let b_i = row1 - a_i;
1570        let c_i = col1 - a_i;
1571        let d_i = row2 - c_i;
1572        ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1573            - ln_factorial(a_i)
1574            - ln_factorial(b_i)
1575            - ln_factorial(c_i)
1576            - ln_factorial(d_i)
1577            - ln_factorial(n)
1578    };
1579
1580    // Range of valid values for cell a
1581    let a_min = col1.saturating_sub(row2);
1582    let a_max = row1.min(col1);
1583
1584    let log_p_obs = log_prob(a);
1585
1586    // Two-tailed: sum probabilities ≤ P(observed)
1587    let mut p_value = 0.0;
1588    for a_i in a_min..=a_max {
1589        let lp = log_prob(a_i);
1590        // Use small tolerance for floating-point comparison
1591        if lp <= log_p_obs + 1e-10 {
1592            p_value += lp.exp();
1593        }
1594    }
1595
1596    // Clamp to [0, 1]
1597    let p_value = p_value.min(1.0);
1598
1599    // Odds ratio: (a*d) / (b*c)
1600    let odds_ratio = if b > 0 && c > 0 {
1601        (a as f64 * d as f64) / (b as f64 * c as f64)
1602    } else {
1603        f64::INFINITY
1604    };
1605
1606    Some(TestResult {
1607        statistic: odds_ratio,
1608        df: 1.0,
1609        p_value,
1610    })
1611}
1612
1613// ---------------------------------------------------------------------------
1614// Mann-Kendall trend test
1615// ---------------------------------------------------------------------------
1616
1617/// Result of the Mann-Kendall trend test.
1618#[derive(Debug, Clone, Copy)]
1619pub struct MannKendallResult {
1620    /// Mann-Kendall S statistic: Σ sign(xⱼ - xᵢ) for all i < j.
1621    pub s_statistic: i64,
1622    /// Variance of S (with tie correction).
1623    pub variance: f64,
1624    /// Z statistic (with continuity correction).
1625    pub z_statistic: f64,
1626    /// Two-tailed p-value.
1627    pub p_value: f64,
1628    /// Kendall's tau: S / [n(n-1)/2]. Range [-1, 1].
1629    pub kendall_tau: f64,
1630    /// Sen's slope estimator: median of (xⱼ - xᵢ)/(j - i) for all i < j.
1631    pub sen_slope: f64,
1632}
1633
1634/// Mann-Kendall non-parametric trend test with Sen's slope estimator.
1635///
1636/// Tests H₀: no monotonic trend vs H₁: monotonic trend exists.
1637/// Assumes serially independent observations (no autocorrelation).
1638///
1639/// # Algorithm
1640///
1641/// 1. S = Σᵢ<ⱼ sign(xⱼ - xᵢ)
1642/// 2. Var(S) = \[n(n-1)(2n+5) - Σ tₖ(tₖ-1)(2tₖ+5)\] / 18 (tie-corrected)
1643/// 3. Z = (S-1)/√Var(S) if S>0, 0 if S=0, (S+1)/√Var(S) if S<0
1644/// 4. Sen's slope = median of all pairwise slopes (xⱼ - xᵢ)/(j - i)
1645///
1646/// References:
1647/// - Mann (1945), "Nonparametric tests against trend"
1648/// - Kendall (1975), "Rank Correlation Methods"
1649/// - Sen (1968), "Estimates of the regression coefficient based on Kendall's tau"
1650///
1651/// # Complexity
1652///
1653/// O(n² log n) — pairwise comparisons O(n²) plus median finding O(n² log n²).
1654///
1655/// # Returns
1656///
1657/// `None` if fewer than 4 data points, non-finite values, or zero variance.
1658///
1659/// # Examples
1660///
1661/// ```
1662/// use u_analytics::testing::mann_kendall_test;
1663///
1664/// // Clear upward trend
1665/// let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
1666/// let r = mann_kendall_test(&data).unwrap();
1667/// assert!(r.p_value < 0.01);
1668/// assert!(r.kendall_tau > 0.8);
1669/// assert!(r.sen_slope > 0.0);
1670/// ```
1671pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1672    let n = data.len();
1673    if n < 4 || data.iter().any(|v| !v.is_finite()) {
1674        return None;
1675    }
1676
1677    // Step 1: Compute S statistic
1678    let mut s: i64 = 0;
1679    for i in 0..n - 1 {
1680        for j in (i + 1)..n {
1681            let diff = data[j] - data[i];
1682            if diff > 0.0 {
1683                s += 1;
1684            } else if diff < 0.0 {
1685                s -= 1;
1686            }
1687        }
1688    }
1689
1690    // Step 2: Compute tie groups
1691    let mut sorted: Vec<f64> = data.to_vec();
1692    sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1693
1694    let mut tie_correction: f64 = 0.0;
1695    let mut current_count: usize = 1;
1696    for i in 1..sorted.len() {
1697        if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1698            current_count += 1;
1699        } else {
1700            if current_count > 1 {
1701                let t = current_count as f64;
1702                tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1703            }
1704            current_count = 1;
1705        }
1706    }
1707    if current_count > 1 {
1708        let t = current_count as f64;
1709        tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1710    }
1711
1712    // Step 3: Variance with tie correction
1713    let nf = n as f64;
1714    let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1715
1716    if variance < 1e-300 {
1717        return None; // All values identical
1718    }
1719
1720    // Step 4: Z with continuity correction
1721    let sigma = variance.sqrt();
1722    let z_statistic = if s > 0 {
1723        (s as f64 - 1.0) / sigma
1724    } else if s < 0 {
1725        (s as f64 + 1.0) / sigma
1726    } else {
1727        0.0
1728    };
1729
1730    // Step 5: Two-tailed p-value
1731    let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z_statistic.abs()));
1732    let p_value = p_value.clamp(0.0, 1.0);
1733
1734    // Step 6: Kendall's tau
1735    let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1736
1737    // Step 7: Sen's slope = median of pairwise slopes
1738    let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1739    for i in 0..n - 1 {
1740        for j in (i + 1)..n {
1741            let dx = (j - i) as f64;
1742            slopes.push((data[j] - data[i]) / dx);
1743        }
1744    }
1745    slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1746    let m = slopes.len();
1747    let sen_slope = if m % 2 == 0 {
1748        (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1749    } else {
1750        slopes[m / 2]
1751    };
1752
1753    Some(MannKendallResult {
1754        s_statistic: s,
1755        variance,
1756        z_statistic,
1757        p_value,
1758        kendall_tau,
1759        sen_slope,
1760    })
1761}
1762
1763/// Natural log of n! using Stirling/ln_gamma for large values.
1764fn ln_factorial(n: u64) -> f64 {
1765    if n <= 1 {
1766        return 0.0;
1767    }
1768    // ln(n!) = ln_gamma(n+1)
1769    special::ln_gamma(n as f64 + 1.0)
1770}
1771
1772// ---------------------------------------------------------------------------
1773// Augmented Dickey-Fuller test
1774// ---------------------------------------------------------------------------
1775
1776/// Result of the Augmented Dickey-Fuller (ADF) unit root test.
1777#[derive(Debug, Clone)]
1778pub struct AdfResult {
1779    /// ADF test statistic (t-ratio for γ̂).
1780    pub statistic: f64,
1781    /// Number of lags used.
1782    pub n_lags: usize,
1783    /// Number of observations used in the regression.
1784    pub n_obs: usize,
1785    /// Critical values at 1%, 5%, 10% significance levels.
1786    pub critical_values: [f64; 3],
1787    /// Whether the null hypothesis (unit root) is rejected at each level.
1788    pub rejected: [bool; 3],
1789}
1790
1791/// Model specification for the ADF test.
1792#[derive(Debug, Clone, Copy)]
1793pub enum AdfModel {
1794    /// No constant, no trend: Δyₜ = γyₜ₋₁ + Σδᵢ·Δyₜ₋ᵢ + εₜ
1795    None,
1796    /// Constant only (default): Δyₜ = α + γyₜ₋₁ + Σδᵢ·Δyₜ₋ᵢ + εₜ
1797    Constant,
1798    /// Constant + linear trend: Δyₜ = α + βt + γyₜ₋₁ + Σδᵢ·Δyₜ₋ᵢ + εₜ
1799    ConstantTrend,
1800}
1801
1802/// Augmented Dickey-Fuller (ADF) unit root test for stationarity.
1803///
1804/// Tests H₀: unit root (non-stationary) vs H₁: stationary.
1805///
1806/// # Algorithm
1807///
1808/// 1. Constructs Δyₜ = α + γyₜ₋₁ + Σδᵢ·Δyₜ₋ᵢ + εₜ
1809/// 2. Estimates via OLS
1810/// 3. Tests t-ratio for γ against Dickey-Fuller critical values
1811///
1812/// When `max_lags` is `None`, lag length is selected by AIC (Schwert rule
1813/// for maximum). When `Some(p)`, exactly `p` lags are used.
1814///
1815/// Reference: Dickey & Fuller (1979), "Distribution of the Estimators for
1816/// Autoregressive Time Series with a Unit Root"
1817///
1818/// # Returns
1819///
1820/// `None` if fewer than 10 data points, non-finite values, or OLS fails.
1821///
1822/// # Examples
1823///
1824/// ```
1825/// use u_analytics::testing::{adf_test, AdfModel};
1826///
1827/// // Stationary series: strong mean-reversion
1828/// let mut data = vec![0.0_f64; 40];
1829/// for i in 1..40 {
1830///     data[i] = 0.3 * data[i - 1] + [0.5, -0.8, 0.3, -0.6, 0.9,
1831///         -0.4, 0.7, -0.2, 0.1, -0.5][i % 10];
1832/// }
1833/// let r = adf_test(&data, AdfModel::Constant, None).unwrap();
1834/// assert!(r.statistic.is_finite());
1835/// assert_eq!(r.critical_values.len(), 3);
1836/// ```
1837pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1838    let n = data.len();
1839    if n < 10 || data.iter().any(|v| !v.is_finite()) {
1840        return None;
1841    }
1842
1843    // Compute differences
1844    let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1845
1846    // Determine lag count
1847    let best_lag = match max_lags {
1848        Some(p) => p, // Use exact lag count when specified
1849        None => {
1850            // Schwert (1989) rule for maximum lag
1851            let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1852            let p_max = schwert.min(n / 3);
1853            // Select optimal lag by AIC
1854            select_adf_lag(data, &dy, model, p_max)
1855        }
1856    };
1857
1858    // Run OLS regression with selected lag
1859    adf_ols(data, &dy, model, best_lag)
1860}
1861
1862/// Selects optimal lag for ADF by minimizing AIC.
1863fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
1864    let mut best_aic = f64::INFINITY;
1865    let mut best_p = 0;
1866
1867    for p in 0..=p_max {
1868        if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
1869            if aic < best_aic {
1870                best_aic = aic;
1871                best_p = p;
1872            }
1873        }
1874    }
1875
1876    best_p
1877}
1878
1879/// Builds the ADF design matrix and dependent variable.
1880///
1881/// Returns (design_matrix_row_major, y_dep, n_rows, n_cols, gamma_col_index).
1882#[allow(clippy::type_complexity)]
1883fn adf_build_matrix(
1884    data: &[f64],
1885    dy: &[f64],
1886    model: AdfModel,
1887    p: usize,
1888) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
1889    let start = p + 1;
1890    if start >= dy.len() || dy.len() - start < 5 {
1891        return None;
1892    }
1893    let m = dy.len() - start;
1894
1895    let y_dep: Vec<f64> = dy[start..].to_vec();
1896
1897    // Count columns: intercept + y_{t-1} + [trend] + p lags
1898    let has_intercept = !matches!(model, AdfModel::None);
1899    let has_trend = matches!(model, AdfModel::ConstantTrend);
1900    let ncols = has_intercept as usize + 1 + has_trend as usize + p;
1901
1902    // Build row-major design matrix
1903    let mut x_data = Vec::with_capacity(m * ncols);
1904    let mut gamma_col = 0;
1905
1906    for i in 0..m {
1907        let t = start + i;
1908        if has_intercept {
1909            x_data.push(1.0); // intercept
1910            gamma_col = 1;
1911        }
1912        x_data.push(data[t]); // y_{t-1}
1913        if has_trend {
1914            x_data.push((t + 1) as f64); // trend
1915        }
1916        for lag in 1..=p {
1917            x_data.push(dy[t - lag]);
1918        }
1919    }
1920
1921    Some((x_data, y_dep, m, ncols, gamma_col))
1922}
1923
1924/// Lightweight OLS for ADF: returns (gamma_t_stat, rss, k).
1925///
1926/// Solves X'Xβ = X'y using Cholesky-like decomposition (Gaussian elimination).
1927fn adf_ols_core(
1928    x_data: &[f64],
1929    y: &[f64],
1930    m: usize,
1931    ncols: usize,
1932    gamma_col: usize,
1933) -> Option<(f64, f64, usize)> {
1934    // Compute X'X (ncols × ncols, symmetric)
1935    let mut xtx = vec![0.0_f64; ncols * ncols];
1936    for i in 0..m {
1937        let row = &x_data[i * ncols..(i + 1) * ncols];
1938        for j in 0..ncols {
1939            for k in j..ncols {
1940                xtx[j * ncols + k] += row[j] * row[k];
1941            }
1942        }
1943    }
1944    // Mirror upper to lower
1945    for j in 0..ncols {
1946        for k in (j + 1)..ncols {
1947            xtx[k * ncols + j] = xtx[j * ncols + k];
1948        }
1949    }
1950
1951    // Compute X'y (ncols × 1)
1952    let mut xty = vec![0.0_f64; ncols];
1953    for i in 0..m {
1954        let row = &x_data[i * ncols..(i + 1) * ncols];
1955        for j in 0..ncols {
1956            xty[j] += row[j] * y[i];
1957        }
1958    }
1959
1960    // Solve via Gaussian elimination with partial pivoting
1961    let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
1962    for i in 0..ncols {
1963        for j in 0..ncols {
1964            augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
1965        }
1966        augmented[i * (ncols + 1) + ncols] = xty[i];
1967    }
1968
1969    for col in 0..ncols {
1970        // Partial pivoting
1971        let mut max_row = col;
1972        let mut max_val = augmented[col * (ncols + 1) + col].abs();
1973        for row in (col + 1)..ncols {
1974            let val = augmented[row * (ncols + 1) + col].abs();
1975            if val > max_val {
1976                max_val = val;
1977                max_row = row;
1978            }
1979        }
1980        if max_val < 1e-15 {
1981            return None; // Singular
1982        }
1983        if max_row != col {
1984            for j in 0..=ncols {
1985                let a = col * (ncols + 1) + j;
1986                let b = max_row * (ncols + 1) + j;
1987                augmented.swap(a, b);
1988            }
1989        }
1990
1991        let pivot = augmented[col * (ncols + 1) + col];
1992        for row in (col + 1)..ncols {
1993            let factor = augmented[row * (ncols + 1) + col] / pivot;
1994            for j in col..=ncols {
1995                let above = augmented[col * (ncols + 1) + j];
1996                augmented[row * (ncols + 1) + j] -= factor * above;
1997            }
1998        }
1999    }
2000
2001    // Back-substitution
2002    let mut beta = vec![0.0_f64; ncols];
2003    for i in (0..ncols).rev() {
2004        let mut sum = augmented[i * (ncols + 1) + ncols];
2005        for j in (i + 1)..ncols {
2006            sum -= augmented[i * (ncols + 1) + j] * beta[j];
2007        }
2008        beta[i] = sum / augmented[i * (ncols + 1) + i];
2009    }
2010
2011    // Compute residuals and RSS
2012    let mut rss = 0.0;
2013    for i in 0..m {
2014        let row = &x_data[i * ncols..(i + 1) * ncols];
2015        let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2016        let resid = y[i] - y_hat;
2017        rss += resid * resid;
2018    }
2019
2020    // Standard error of coefficients
2021    let df = m - ncols;
2022    if df == 0 {
2023        return None;
2024    }
2025    let mse = rss / df as f64;
2026
2027    // Compute (X'X)^{-1} via Gauss-Jordan elimination to get variance of γ̂
2028    let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; // xtx | I
2029    for i in 0..ncols {
2030        for j in 0..ncols {
2031            xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2032        }
2033        xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2034    }
2035
2036    // Gauss-Jordan elimination
2037    for col in 0..ncols {
2038        let mut max_row = col;
2039        let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2040        for row in (col + 1)..ncols {
2041            let val = xtx_aug[row * 2 * ncols + col].abs();
2042            if val > max_val {
2043                max_val = val;
2044                max_row = row;
2045            }
2046        }
2047        if max_val < 1e-15 {
2048            return None;
2049        }
2050        if max_row != col {
2051            for j in 0..(2 * ncols) {
2052                let a = col * 2 * ncols + j;
2053                let b = max_row * 2 * ncols + j;
2054                xtx_aug.swap(a, b);
2055            }
2056        }
2057
2058        let pivot = xtx_aug[col * 2 * ncols + col];
2059        for j in 0..(2 * ncols) {
2060            xtx_aug[col * 2 * ncols + j] /= pivot;
2061        }
2062        for row in 0..ncols {
2063            if row == col {
2064                continue;
2065            }
2066            let factor = xtx_aug[row * 2 * ncols + col];
2067            for j in 0..(2 * ncols) {
2068                let above = xtx_aug[col * 2 * ncols + j];
2069                xtx_aug[row * 2 * ncols + j] -= factor * above;
2070            }
2071        }
2072    }
2073
2074    // Extract diagonal element for gamma column
2075    let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2076    if var_gamma <= 0.0 {
2077        return None;
2078    }
2079    let se_gamma = var_gamma.sqrt();
2080    let t_gamma = beta[gamma_col] / se_gamma;
2081
2082    Some((t_gamma, rss, ncols))
2083}
2084
2085/// Runs ADF OLS and returns AIC + number of observations.
2086fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2087    let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2088    let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2089    let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2090    Some((aic, m))
2091}
2092
2093/// Runs the actual ADF OLS and returns the test result.
2094fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2095    let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2096    let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2097
2098    let critical_values = adf_critical_values(model, m);
2099
2100    let rejected = [
2101        gamma_t <= critical_values[0],
2102        gamma_t <= critical_values[1],
2103        gamma_t <= critical_values[2],
2104    ];
2105
2106    Some(AdfResult {
2107        statistic: gamma_t,
2108        n_lags: p,
2109        n_obs: m,
2110        critical_values,
2111        rejected,
2112    })
2113}
2114
2115/// MacKinnon (1994) critical values for ADF test.
2116///
2117/// Returns [1%, 5%, 10%] critical values based on sample size.
2118fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2119    // MacKinnon (1994) regression-based approximation:
2120    // cv(n) = τ_∞ + τ₁/n + τ₂/n²
2121    //
2122    // Coefficients from MacKinnon (2010), Table 1.
2123    let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2124        AdfModel::None => (
2125            [-2.5658, -1.9393, -1.6156],
2126            [-1.960, -0.398, -0.181],
2127            [-10.04, 0.0, 0.0],
2128        ),
2129        AdfModel::Constant => (
2130            [-3.4336, -2.8621, -2.5671],
2131            [-5.999, -2.738, -1.438],
2132            [-29.25, -8.36, -4.48],
2133        ),
2134        AdfModel::ConstantTrend => (
2135            [-3.9638, -3.4126, -3.1279],
2136            [-8.353, -4.039, -2.418],
2137            [-47.44, -17.83, -7.58],
2138        ),
2139    };
2140
2141    let nf = n as f64;
2142    let inv_n = 1.0 / nf;
2143    let inv_n2 = inv_n * inv_n;
2144
2145    [
2146        tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2147        tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2148        tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2149    ]
2150}
2151
2152#[cfg(test)]
2153mod tests {
2154    use super::*;
2155
2156    // -----------------------------------------------------------------------
2157    // One-sample t-test
2158    // -----------------------------------------------------------------------
2159
2160    #[test]
2161    fn one_sample_null_true() {
2162        let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2163        let r = one_sample_t_test(&data, 5.0).expect("should compute");
2164        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2165    }
2166
2167    #[test]
2168    fn one_sample_null_false() {
2169        let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2170        let r = one_sample_t_test(&data, 10.0).expect("should compute");
2171        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2172    }
2173
2174    #[test]
2175    fn one_sample_edge_cases() {
2176        assert!(one_sample_t_test(&[1.0], 0.0).is_none()); // n < 2
2177        assert!(one_sample_t_test(&[5.0, 5.0, 5.0], 5.0).is_none()); // zero var
2178        assert!(one_sample_t_test(&[1.0, f64::NAN, 3.0], 2.0).is_none());
2179    }
2180
2181    // -----------------------------------------------------------------------
2182    // Two-sample t-test
2183    // -----------------------------------------------------------------------
2184
2185    #[test]
2186    fn two_sample_same_mean() {
2187        let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2188        let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2189        let r = two_sample_t_test(&a, &b).expect("should compute");
2190        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2191    }
2192
2193    #[test]
2194    fn two_sample_different_means() {
2195        let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2196        let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2197        let r = two_sample_t_test(&a, &b).expect("should compute");
2198        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2199    }
2200
2201    #[test]
2202    fn two_sample_different_sizes() {
2203        let a = [1.0, 2.0, 3.0];
2204        let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2205        let r = two_sample_t_test(&a, &b).expect("should compute");
2206        assert!(r.p_value < 0.05);
2207    }
2208
2209    #[test]
2210    fn two_sample_edge_cases() {
2211        assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2212        assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2213    }
2214
2215    // -----------------------------------------------------------------------
2216    // Paired t-test
2217    // -----------------------------------------------------------------------
2218
2219    #[test]
2220    fn paired_no_difference() {
2221        let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2222        let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2223        let r = paired_t_test(&x, &y).expect("should compute");
2224        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2225    }
2226
2227    #[test]
2228    fn paired_significant_difference() {
2229        // Differences have non-zero variance
2230        let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2231        let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2232        let r = paired_t_test(&before, &after).expect("should compute");
2233        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2234        assert!(r.statistic < 0.0); // after > before
2235    }
2236
2237    #[test]
2238    fn paired_edge_cases() {
2239        assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); // length mismatch
2240        assert!(paired_t_test(&[1.0], &[2.0]).is_none()); // n < 2
2241    }
2242
2243    // -----------------------------------------------------------------------
2244    // ANOVA
2245    // -----------------------------------------------------------------------
2246
2247    #[test]
2248    fn anova_same_means() {
2249        let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2250        let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2251        let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2252        let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2253        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2254    }
2255
2256    #[test]
2257    fn anova_different_means() {
2258        let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2259        let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2260        let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2261        let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2262        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2263        assert!(r.df_between == 2);
2264        assert!(r.df_within == 12);
2265    }
2266
2267    #[test]
2268    fn anova_ss_decomposition() {
2269        let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2270        let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2271        let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2272        // SS_total = SS_between + SS_within
2273        let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2274        let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2275        assert!(
2276            (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2277            "SS decomposition: {ss_total} vs {} + {}",
2278            r.ss_between,
2279            r.ss_within
2280        );
2281    }
2282
2283    #[test]
2284    fn anova_edge_cases() {
2285        let g1 = [1.0, 2.0, 3.0];
2286        assert!(one_way_anova(&[&g1]).is_none()); // < 2 groups
2287    }
2288
2289    // -----------------------------------------------------------------------
2290    // Chi-squared goodness of fit
2291    // -----------------------------------------------------------------------
2292
2293    #[test]
2294    fn chi2_gof_uniform() {
2295        // Perfect uniform distribution
2296        let observed = [25.0, 25.0, 25.0, 25.0];
2297        let expected = [25.0, 25.0, 25.0, 25.0];
2298        let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2299        assert!((r.statistic).abs() < 1e-15);
2300        assert!((r.p_value - 1.0).abs() < 0.01);
2301    }
2302
2303    #[test]
2304    fn chi2_gof_significant() {
2305        let observed = [90.0, 10.0];
2306        let expected = [50.0, 50.0];
2307        let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2308        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2309    }
2310
2311    #[test]
2312    fn chi2_gof_edge_cases() {
2313        assert!(chi_squared_goodness_of_fit(&[10.0], &[10.0]).is_none()); // < 2
2314        assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0, 0.0]).is_none()); // expected 0
2315        assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0]).is_none()); // mismatch
2316    }
2317
2318    // -----------------------------------------------------------------------
2319    // Chi-squared independence
2320    // -----------------------------------------------------------------------
2321
2322    #[test]
2323    fn chi2_independence_significant() {
2324        // Strong association
2325        let table = [30.0, 10.0, 10.0, 50.0];
2326        let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2327        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2328        assert!((r.df - 1.0).abs() < 1e-10);
2329    }
2330
2331    #[test]
2332    fn chi2_independence_not_significant() {
2333        // No association
2334        let table = [25.0, 25.0, 25.0, 25.0];
2335        let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2336        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2337    }
2338
2339    #[test]
2340    fn chi2_independence_3x3() {
2341        let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2342        let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2343        assert!((r.df - 4.0).abs() < 1e-10);
2344        assert!(r.p_value < 0.05);
2345    }
2346
2347    #[test]
2348    fn chi2_independence_edge_cases() {
2349        assert!(chi_squared_independence(&[10.0, 20.0], 1, 2).is_none()); // 1 row
2350        assert!(chi_squared_independence(&[10.0, 20.0], 2, 1).is_none()); // 1 col
2351        assert!(chi_squared_independence(&[10.0], 2, 2).is_none()); // wrong size
2352    }
2353
2354    // -----------------------------------------------------------------------
2355    // Jarque-Bera
2356    // -----------------------------------------------------------------------
2357
2358    #[test]
2359    fn jb_normal_data() {
2360        // Symmetric, light-tailed data
2361        let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2362        let r = jarque_bera_test(&data).expect("should compute");
2363        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2364    }
2365
2366    #[test]
2367    fn jb_skewed_data() {
2368        // Highly right-skewed
2369        let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2370        let r = jarque_bera_test(&data).expect("should compute");
2371        assert!(r.p_value < 0.05, "p = {}", r.p_value);
2372    }
2373
2374    #[test]
2375    fn jb_edge_cases() {
2376        assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); // n < 8
2377    }
2378
2379    // -----------------------------------------------------------------------
2380    // Anderson-Darling
2381    // -----------------------------------------------------------------------
2382
2383    #[test]
2384    fn ad_normal_data() {
2385        let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2386        let r = anderson_darling_test(&data).expect("should compute");
2387        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2388        assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2389        assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2390    }
2391
2392    #[test]
2393    fn ad_skewed_data() {
2394        // Exponential-like data — not normal
2395        let data = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3, 2.1, 3.4, 5.5, 8.9, 14.4, 23.3];
2396        let r = anderson_darling_test(&data).expect("should compute");
2397        assert!(
2398            r.p_value < 0.05,
2399            "p = {} (should reject normality)",
2400            r.p_value
2401        );
2402    }
2403
2404    #[test]
2405    fn ad_bimodal_data() {
2406        // Bimodal data — clearly not normal
2407        let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2408        data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2409        let r = anderson_darling_test(&data).expect("should compute");
2410        assert!(
2411            r.p_value < 0.01,
2412            "p = {} (bimodal should reject normality)",
2413            r.p_value
2414        );
2415    }
2416
2417    #[test]
2418    fn ad_large_normal_sample() {
2419        let n = 100;
2420        let data: Vec<f64> = (1..=n)
2421            .map(|i| {
2422                let p = (i as f64 - 0.5) / n as f64;
2423                special::inverse_normal_cdf(p)
2424            })
2425            .collect();
2426        let r = anderson_darling_test(&data).expect("should compute");
2427        assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2428    }
2429
2430    #[test]
2431    fn ad_edge_cases() {
2432        assert!(anderson_darling_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); // n < 8
2433        assert!(anderson_darling_test(&[5.0; 10]).is_none()); // constant
2434        assert!(anderson_darling_test(&[1.0, f64::NAN, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).is_none());
2435    }
2436
2437    #[test]
2438    fn ad_p_value_ranges() {
2439        // Test different A*² ranges for p-value formula
2440        // Use datasets that produce different A*² magnitudes
2441
2442        // Near-normal → small A*² (< 0.2 range)
2443        let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2444        let r = anderson_darling_test(&near_normal).expect("should compute");
2445        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2446
2447        // Heavy-tailed → large A*² (>= 0.6 range)
2448        let heavy_tail = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3, 2.1, 3.4, 5.5, 8.9, 14.4, 23.3];
2449        let r = anderson_darling_test(&heavy_tail).expect("should compute");
2450        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2451    }
2452
2453    // -----------------------------------------------------------------------
2454    // Anderson-Darling normality (anderson_darling_normality)
2455    // -----------------------------------------------------------------------
2456
2457    #[test]
2458    fn ad_normal_data_large_p() {
2459        // Clearly normal data → cannot reject normality (p > 0.05)
2460        let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2461        let r = anderson_darling_normality(&data).unwrap();
2462        assert!(r.p_value > 0.05, "p={}", r.p_value);
2463    }
2464
2465    #[test]
2466    fn ad_exponential_data_small_p() {
2467        // Clearly non-normal (exponential) → reject normality (p < 0.05)
2468        let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2469        let r = anderson_darling_normality(&data).unwrap();
2470        assert!(r.p_value < 0.05, "p={}", r.p_value);
2471    }
2472
2473    #[test]
2474    fn ad_statistic_non_negative() {
2475        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2476        let r = anderson_darling_normality(&data).unwrap();
2477        assert!(r.statistic >= 0.0);
2478        assert!(r.statistic_modified >= r.statistic);
2479    }
2480
2481    #[test]
2482    fn ad_p_value_in_range() {
2483        let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2484        let r = anderson_darling_normality(&data).unwrap();
2485        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2486    }
2487
2488    #[test]
2489    fn ad_insufficient_data() {
2490        assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); // n < 3
2491    }
2492
2493    #[test]
2494    fn ad_degenerate_data() {
2495        // All same value → std = 0 → None
2496        assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2497    }
2498
2499    #[test]
2500    fn ad_modified_statistic_formula() {
2501        // A²* > A² for any finite n
2502        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2503        let r = anderson_darling_normality(&data).unwrap();
2504        assert!(r.statistic_modified > r.statistic - 1e-10);
2505    }
2506
2507    /// Validates A²* = A² · (1 + 0.75/n + 2.25/n²) against manual computation.
2508    ///
2509    /// Reference: Stephens (1974), "EDF statistics for goodness of fit",
2510    /// J. Amer. Statist. Assoc. 69(347), 730–737 (composite normal case).
2511    #[test]
2512    fn ad_correction_factor_formula() {
2513        let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2514        let n = data.len() as f64;
2515        let r = anderson_darling_normality(&data).unwrap();
2516
2517        // Verify the Stephens correction formula A²* = A² · (1 + 0.75/n + 2.25/n²)
2518        let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2519        assert!(
2520            (r.statistic_modified - expected_star).abs() < 1e-12,
2521            "A²* = {}, expected = {}",
2522            r.statistic_modified,
2523            expected_star
2524        );
2525
2526        // A² must be positive (it is a sum of log terms with negative coefficient)
2527        assert!(
2528            r.statistic >= 0.0,
2529            "A² = {} must be non-negative",
2530            r.statistic
2531        );
2532    }
2533
2534    /// Validates that the A² summation formula implements:
2535    /// A² = -n - (1/n)·Σᵢ₌₀ⁿ⁻¹ (2i+1)·[ln Φ(zᵢ) + ln(1 − Φ(z_{n−1−i}))]
2536    ///
2537    /// This tests the formula structure by checking that a perfectly symmetric
2538    /// dataset centered at 0 produces a smaller A² than a skewed dataset.
2539    #[test]
2540    fn ad_formula_structure_symmetric_vs_skewed() {
2541        // Symmetric around mean → smaller A²
2542        let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2543        // Right-skewed → larger A² (heavier right tail deviates from normality)
2544        let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2545
2546        let r_sym = anderson_darling_normality(&symmetric).unwrap();
2547        let r_skew = anderson_darling_normality(&skewed).unwrap();
2548
2549        assert!(
2550            r_sym.statistic < r_skew.statistic,
2551            "Symmetric A²={} should be less than skewed A²={}",
2552            r_sym.statistic,
2553            r_skew.statistic
2554        );
2555    }
2556
2557    /// Validates p-value invariants for the Anderson-Darling test.
2558    ///
2559    /// - Normal data: p > 0.05 (cannot reject normality)
2560    /// - Exponential data: p < 0.05 (reject normality)
2561    #[test]
2562    fn ad_pvalue_invariants() {
2563        // Near-normal data — tightly clustered, should not reject normality
2564        let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2565        let r_normal = anderson_darling_normality(&normal_data).unwrap();
2566        assert!(
2567            r_normal.p_value > 0.05,
2568            "Normal data: p = {} (expected > 0.05)",
2569            r_normal.p_value
2570        );
2571
2572        // Exponential data — heavy right tail, should reject normality
2573        let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2574        let r_exp = anderson_darling_normality(&exp_data).unwrap();
2575        assert!(
2576            r_exp.p_value < 0.05,
2577            "Exponential data: p = {} (expected < 0.05)",
2578            r_exp.p_value
2579        );
2580    }
2581
2582    // -----------------------------------------------------------------------
2583    // Shapiro-Wilk
2584    // -----------------------------------------------------------------------
2585
2586    #[test]
2587    fn sw_normal_data() {
2588        // Approximately normal data (symmetric, bell-shaped)
2589        let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2590        let r = shapiro_wilk_test(&data).expect("should compute");
2591        assert!(r.w > 0.9, "W = {}", r.w);
2592        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2593    }
2594
2595    #[test]
2596    fn sw_bimodal_data() {
2597        // Bimodal data — clearly not normal
2598        let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2599        data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2600        let r = shapiro_wilk_test(&data).expect("should compute");
2601        assert!(
2602            r.p_value < 0.01,
2603            "p = {} (bimodal should reject normality)",
2604            r.p_value
2605        );
2606    }
2607
2608    #[test]
2609    fn sw_n3() {
2610        let data = [1.0, 2.0, 3.0];
2611        let r = shapiro_wilk_test(&data).expect("n=3 should work");
2612        assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2613        assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2614    }
2615
2616    #[test]
2617    fn sw_n4() {
2618        let data = [1.0, 2.0, 3.0, 4.0];
2619        let r = shapiro_wilk_test(&data).expect("n=4 should work");
2620        assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2621        assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2622    }
2623
2624    #[test]
2625    fn sw_n5() {
2626        let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2627        let r = shapiro_wilk_test(&data).expect("n=5 should work");
2628        assert!(r.w > 0.9, "W = {}", r.w);
2629        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2630    }
2631
2632    #[test]
2633    fn sw_skewed_data() {
2634        // Exponential-like data — not normal
2635        let data = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3, 2.1, 3.4, 5.5, 8.9, 14.4, 23.3];
2636        let r = shapiro_wilk_test(&data).expect("should compute");
2637        assert!(
2638            r.p_value < 0.05,
2639            "p = {} (skewed data should reject normality)",
2640            r.p_value
2641        );
2642    }
2643
2644    #[test]
2645    fn sw_large_normal_sample() {
2646        // Generate pseudo-normal data via Box-Muller-like approach
2647        // Use linearly spaced quantiles from standard normal
2648        let n = 100;
2649        let data: Vec<f64> = (1..=n)
2650            .map(|i| {
2651                let p = (i as f64 - 0.5) / n as f64;
2652                special::inverse_normal_cdf(p)
2653            })
2654            .collect();
2655        let r = shapiro_wilk_test(&data).expect("should compute");
2656        assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2657        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2658    }
2659
2660    #[test]
2661    fn sw_w_bounded() {
2662        // W should be in (0, 1] for any valid data
2663        let datasets: Vec<Vec<f64>> = vec![
2664            vec![1.0, 2.0, 3.0],
2665            vec![1.0, 1.0, 2.0, 3.0, 3.0],
2666            vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2667            (0..20).map(|i| (i as f64).powi(2)).collect(),
2668        ];
2669        for (idx, data) in datasets.iter().enumerate() {
2670            let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2671            assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2672            assert!(
2673                r.p_value >= 0.0 && r.p_value <= 1.0,
2674                "dataset {idx}: p = {}",
2675                r.p_value
2676            );
2677        }
2678    }
2679
2680    #[test]
2681    fn sw_edge_cases() {
2682        assert!(shapiro_wilk_test(&[1.0, 2.0]).is_none()); // n < 3
2683        assert!(shapiro_wilk_test(&[]).is_none()); // empty
2684        assert!(shapiro_wilk_test(&[5.0, 5.0, 5.0]).is_none()); // constant
2685        assert!(shapiro_wilk_test(&[1.0, f64::NAN, 3.0]).is_none()); // NaN
2686        assert!(shapiro_wilk_test(&[1.0, f64::INFINITY, 3.0]).is_none()); // Inf
2687    }
2688
2689    #[test]
2690    fn sw_n5001_rejected() {
2691        let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2692        assert!(shapiro_wilk_test(&data).is_none()); // n > 5000
2693    }
2694
2695    // -----------------------------------------------------------------------
2696    // Mann-Whitney U
2697    // -----------------------------------------------------------------------
2698
2699    #[test]
2700    fn mw_clearly_different() {
2701        let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2702        let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2703        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2704        assert!(r.p_value < 0.05, "p = {}", r.p_value);
2705    }
2706
2707    #[test]
2708    fn mw_same_distribution() {
2709        let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2710        let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2711        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2712        assert!(
2713            r.p_value > 0.3,
2714            "p = {} (interleaved, same dist)",
2715            r.p_value
2716        );
2717    }
2718
2719    #[test]
2720    fn mw_with_ties() {
2721        let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2722        let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2723        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2724        assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2725    }
2726
2727    #[test]
2728    fn mw_different_sizes() {
2729        let a = [1.0, 2.0, 3.0];
2730        let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2731        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2732        assert!(r.p_value < 0.05);
2733    }
2734
2735    #[test]
2736    fn mw_edge_cases() {
2737        assert!(mann_whitney_u_test(&[1.0], &[2.0, 3.0]).is_none()); // n1 < 2
2738        assert!(mann_whitney_u_test(&[1.0, 2.0], &[3.0]).is_none()); // n2 < 2
2739        assert!(mann_whitney_u_test(&[1.0, f64::NAN], &[2.0, 3.0]).is_none());
2740    }
2741
2742    // -----------------------------------------------------------------------
2743    // Wilcoxon signed-rank
2744    // -----------------------------------------------------------------------
2745
2746    #[test]
2747    fn wilcoxon_significant_increase() {
2748        let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2749        let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2750        let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2751        assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2752    }
2753
2754    #[test]
2755    fn wilcoxon_no_difference() {
2756        let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2757        let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2758        let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2759        assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2760    }
2761
2762    #[test]
2763    fn wilcoxon_with_ties() {
2764        // Some differences are equal in magnitude
2765        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2766        let y = [0.0, 1.0, 2.0, 3.0, 4.0]; // constant difference = 1.0
2767        let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2768        // All differences positive with ties in magnitude
2769        assert!(r.statistic > 0.0);
2770    }
2771
2772    #[test]
2773    fn wilcoxon_with_zero_diffs() {
2774        // Some pairs are equal → zero differences discarded
2775        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2776        let y = [1.0, 2.0, 3.0, 3.0, 4.0]; // first 3 are zero diffs
2777        let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2778        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2779    }
2780
2781    #[test]
2782    fn wilcoxon_edge_cases() {
2783        assert!(wilcoxon_signed_rank_test(&[1.0, 2.0], &[3.0]).is_none()); // mismatch
2784        assert!(wilcoxon_signed_rank_test(&[1.0], &[2.0]).is_none()); // n < 2
2785                                                                      // All zero differences → fewer than 2 non-zero diffs
2786        assert!(wilcoxon_signed_rank_test(&[5.0, 5.0], &[5.0, 5.0]).is_none());
2787    }
2788
2789    // -----------------------------------------------------------------------
2790    // Kruskal-Wallis
2791    // -----------------------------------------------------------------------
2792
2793    #[test]
2794    fn kw_clearly_different() {
2795        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2796        let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2797        let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
2798        let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2799        assert!(r.p_value < 0.01, "p = {}", r.p_value);
2800        assert!((r.df - 2.0).abs() < 1e-10);
2801    }
2802
2803    #[test]
2804    fn kw_same_distribution() {
2805        let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
2806        let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
2807        let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
2808        assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
2809    }
2810
2811    #[test]
2812    fn kw_with_ties() {
2813        let g1 = [1.0, 2.0, 2.0, 3.0];
2814        let g2 = [3.0, 4.0, 4.0, 5.0];
2815        let g3 = [5.0, 6.0, 6.0, 7.0];
2816        let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2817        assert!(r.statistic > 0.0);
2818    }
2819
2820    #[test]
2821    fn kw_edge_cases() {
2822        let g1 = [1.0, 2.0, 3.0];
2823        assert!(kruskal_wallis_test(&[&g1]).is_none()); // < 2 groups
2824    }
2825
2826    // -----------------------------------------------------------------------
2827    // Levene (Brown-Forsythe)
2828    // -----------------------------------------------------------------------
2829
2830    #[test]
2831    fn levene_equal_variance() {
2832        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2833        let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2834        let r = levene_test(&[&g1, &g2]).expect("should compute");
2835        // Both have same spread, different means → equal variance
2836        assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
2837    }
2838
2839    #[test]
2840    fn levene_unequal_variance() {
2841        let g1 = [4.5, 4.8, 5.0, 5.2, 5.5]; // small spread
2842        let g2 = [0.0, 2.0, 5.0, 8.0, 10.0]; // large spread
2843        let r = levene_test(&[&g1, &g2]).expect("should compute");
2844        assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
2845    }
2846
2847    #[test]
2848    fn levene_three_groups() {
2849        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2850        let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
2851        let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
2852        let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
2853        assert!(r.df >= 2.0);
2854    }
2855
2856    #[test]
2857    fn levene_edge_cases() {
2858        let g1 = [1.0, 2.0, 3.0];
2859        assert!(levene_test(&[&g1]).is_none()); // < 2 groups
2860    }
2861
2862    // -----------------------------------------------------------------------
2863    // Multiple comparison correction
2864    // -----------------------------------------------------------------------
2865
2866    #[test]
2867    fn bonferroni_basic() {
2868        let ps = [0.01, 0.04, 0.03, 0.005];
2869        let adj = bonferroni_correction(&ps).expect("should compute");
2870        assert!((adj[0] - 0.04).abs() < 1e-10);
2871        assert!((adj[1] - 0.16).abs() < 1e-10);
2872        assert!((adj[2] - 0.12).abs() < 1e-10);
2873        assert!((adj[3] - 0.02).abs() < 1e-10);
2874    }
2875
2876    #[test]
2877    fn bonferroni_capped_at_one() {
2878        let ps = [0.5, 0.6];
2879        let adj = bonferroni_correction(&ps).expect("should compute");
2880        assert!((adj[0] - 1.0).abs() < 1e-10);
2881        assert!((adj[1] - 1.0).abs() < 1e-10);
2882    }
2883
2884    #[test]
2885    fn bh_basic() {
2886        let ps = [0.01, 0.04, 0.03, 0.005];
2887        let adj = benjamini_hochberg(&ps).expect("should compute");
2888        // All adjusted p-values should be >= original
2889        for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
2890            assert!(
2891                adjusted >= orig - 1e-15,
2892                "adj[{i}] = {adjusted} < original {orig}"
2893            );
2894        }
2895        // Adjusted p-values should still be ordered (weakly) by original order
2896        // after reordering by original p-value
2897    }
2898
2899    #[test]
2900    fn bh_all_significant() {
2901        let ps = [0.001, 0.002, 0.003];
2902        let adj = benjamini_hochberg(&ps).expect("should compute");
2903        for &a in &adj {
2904            assert!(a < 0.05);
2905        }
2906    }
2907
2908    #[test]
2909    fn correction_edge_cases() {
2910        assert!(bonferroni_correction(&[]).is_none());
2911        assert!(benjamini_hochberg(&[]).is_none());
2912        assert!(bonferroni_correction(&[f64::NAN]).is_none());
2913    }
2914
2915    // -----------------------------------------------------------------------
2916    // Bartlett test
2917    // -----------------------------------------------------------------------
2918
2919    #[test]
2920    fn bartlett_equal_variances() {
2921        let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
2922        let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
2923        let r = bartlett_test(&[&g1, &g2]).expect("should compute");
2924        assert!(
2925            r.p_value > 0.9,
2926            "equal variance → p high, got {}",
2927            r.p_value
2928        );
2929    }
2930
2931    #[test]
2932    fn bartlett_unequal_variances() {
2933        let g1 = [2.0, 3.0, 4.0, 5.0, 6.0]; // var ≈ 2.5
2934        let g2 = [10.0, 20.0, 30.0, 40.0, 50.0]; // var ≈ 250
2935        let r = bartlett_test(&[&g1, &g2]).expect("should compute");
2936        assert!(
2937            r.p_value < 0.01,
2938            "very different variances → p < 0.01, got {}",
2939            r.p_value
2940        );
2941        assert!((r.df - 1.0).abs() < 1e-10); // k-1 = 1
2942    }
2943
2944    #[test]
2945    fn bartlett_three_groups() {
2946        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2947        let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
2948        let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; // much higher variance
2949        let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
2950        assert!(r.p_value < 0.05, "one group with high variance");
2951        assert!((r.df - 2.0).abs() < 1e-10); // k-1 = 2
2952    }
2953
2954    #[test]
2955    fn bartlett_edge_cases() {
2956        let g1 = [1.0, 2.0, 3.0];
2957        assert!(bartlett_test(&[&g1]).is_none()); // < 2 groups
2958
2959        let g2 = [5.0, 5.0, 5.0]; // zero variance
2960        assert!(bartlett_test(&[&g1, &g2]).is_none());
2961
2962        let g3 = [1.0]; // group too small
2963        assert!(bartlett_test(&[&g1, &g3]).is_none());
2964    }
2965
2966    // -----------------------------------------------------------------------
2967    // Fisher exact test
2968    // -----------------------------------------------------------------------
2969
2970    #[test]
2971    fn fisher_tea_tasting() {
2972        // Classic Fisher tea-tasting: [[3,1],[1,3]]
2973        let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
2974        // Known two-tailed p ≈ 0.4857
2975        assert!(
2976            (r.p_value - 0.4857).abs() < 0.01,
2977            "p ≈ 0.4857, got {}",
2978            r.p_value
2979        );
2980    }
2981
2982    #[test]
2983    fn fisher_significant() {
2984        // Strong association: [[10, 0], [0, 10]]
2985        let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
2986        assert!(r.p_value < 0.001, "perfect association → p very small");
2987    }
2988
2989    #[test]
2990    fn fisher_no_association() {
2991        // Proportional table: [[5, 5], [5, 5]]
2992        let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
2993        assert!(
2994            r.p_value > 0.9,
2995            "no association → p ≈ 1.0, got {}",
2996            r.p_value
2997        );
2998    }
2999
3000    #[test]
3001    fn fisher_small_table() {
3002        // [[1, 0], [0, 1]]
3003        let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3004        assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3005    }
3006
3007    #[test]
3008    fn fisher_asymmetric() {
3009        // [[8, 2], [1, 5]]
3010        let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3011        assert!(r.p_value < 0.05, "significant association");
3012    }
3013
3014    #[test]
3015    fn fisher_edge_cases() {
3016        // Zero marginals → None
3017        assert!(fisher_exact_test(0, 0, 1, 2).is_none()); // row1 = 0
3018        assert!(fisher_exact_test(1, 2, 0, 0).is_none()); // row2 = 0
3019        assert!(fisher_exact_test(0, 1, 0, 2).is_none()); // col1 = 0
3020    }
3021
3022    #[test]
3023    fn fisher_odds_ratio() {
3024        let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3025        // OR = (3*3)/(1*1) = 9
3026        assert!(
3027            (r.statistic - 9.0).abs() < 1e-10,
3028            "OR = 9, got {}",
3029            r.statistic
3030        );
3031    }
3032
3033    // -----------------------------------------------------------------------
3034    // Mann-Kendall trend test
3035    // -----------------------------------------------------------------------
3036
3037    #[test]
3038    fn mk_increasing_trend() {
3039        let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3040        let r = mann_kendall_test(&data).expect("should compute");
3041        assert!(r.p_value < 0.01, "p = {}", r.p_value);
3042        assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3043        assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3044        assert!(r.s_statistic > 0);
3045    }
3046
3047    #[test]
3048    fn mk_decreasing_trend() {
3049        let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3050        let r = mann_kendall_test(&data).expect("should compute");
3051        assert!(r.p_value < 0.01, "p = {}", r.p_value);
3052        assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3053        assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3054        assert!(r.s_statistic < 0);
3055    }
3056
3057    #[test]
3058    fn mk_no_trend() {
3059        // Random-looking data with no clear trend
3060        let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3061        let r = mann_kendall_test(&data).expect("should compute");
3062        // Should not detect significant trend
3063        assert!(
3064            r.p_value > 0.05,
3065            "p = {} (should be > 0.05 for no trend)",
3066            r.p_value
3067        );
3068    }
3069
3070    #[test]
3071    fn mk_perfect_monotone() {
3072        let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3073        let r = mann_kendall_test(&data).expect("should compute");
3074        // Perfect monotone: S = n(n-1)/2 = 45, tau = 1.0
3075        assert_eq!(r.s_statistic, 45);
3076        assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3077        assert!((r.sen_slope - 1.0).abs() < 1e-10);
3078    }
3079
3080    #[test]
3081    fn mk_with_ties() {
3082        let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3083        let r = mann_kendall_test(&data).expect("should compute");
3084        assert!(r.s_statistic > 0);
3085        // Tie correction should reduce variance
3086        let n = data.len() as f64;
3087        let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3088        assert!(r.variance < base_var, "ties should reduce variance");
3089    }
3090
3091    #[test]
3092    fn mk_edge_cases() {
3093        // Too few data points
3094        assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3095        // NaN
3096        assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3097        // All identical (zero variance)
3098        assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3099    }
3100
3101    #[test]
3102    fn mk_minimum_n() {
3103        // n = 4 should work
3104        let data = [1.0, 2.0, 3.0, 4.0];
3105        let r = mann_kendall_test(&data).expect("n=4 should work");
3106        assert_eq!(r.s_statistic, 6); // C(4,2) = 6 pairs, all positive
3107        assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3108    }
3109
3110    #[test]
3111    fn mk_sen_slope_robust_to_outlier() {
3112        // Mostly linear (slope ≈ 1) with one outlier
3113        let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3114        let r = mann_kendall_test(&data).expect("should compute");
3115        // Sen's slope should be close to 1.0 despite the outlier at index 3
3116        assert!(
3117            (r.sen_slope - 1.0).abs() < 0.5,
3118            "sen slope = {}, expected ≈ 1.0",
3119            r.sen_slope
3120        );
3121    }
3122
3123    // -----------------------------------------------------------------------
3124    // ADF test
3125    // -----------------------------------------------------------------------
3126
3127    #[test]
3128    fn adf_stationary_mean_reverting() {
3129        // Strongly mean-reverting AR(1) process: y_t = 0.3*y_{t-1} + noise
3130        // This is clearly stationary (|ρ| < 1)
3131        let data = [
3132            0.5, 0.45, -0.2, 0.14, 0.54, -0.04, 0.39, -0.18, 0.35, 0.01, -0.3, 0.21, 0.47, -0.06,
3133            0.38, -0.25, 0.12, 0.44, -0.13, 0.36, -0.09, 0.27, 0.51, -0.15, 0.33, -0.22, 0.18,
3134            0.42, -0.08, 0.31, -0.19, 0.25, 0.48, -0.11, 0.37, -0.24, 0.15, 0.43, -0.07, 0.34,
3135        ];
3136        let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3137        // Should reject H₀ (series is stationary)
3138        assert!(
3139            r.rejected[2],
3140            "should reject at 10%: stat={}, cv={}",
3141            r.statistic, r.critical_values[2]
3142        );
3143    }
3144
3145    #[test]
3146    fn adf_nonstationary_random_walk() {
3147        // Cumulative sum simulates random walk (non-stationary)
3148        let increments = [
3149            0.1, -0.2, 0.15, -0.05, 0.3, -0.1, 0.2, -0.15, 0.25, -0.08, 0.12, -0.18, 0.22, -0.07,
3150            0.16, -0.11, 0.19, -0.14, 0.21, -0.09, 0.13, -0.17, 0.24, -0.06, 0.18, -0.12, 0.2,
3151            -0.13, 0.15, -0.1,
3152        ];
3153        let mut walk = Vec::with_capacity(increments.len());
3154        let mut cum = 0.0;
3155        for &inc in &increments {
3156            cum += inc;
3157            walk.push(cum);
3158        }
3159        let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3160        // Random walk should NOT reject at 1%
3161        assert!(
3162            !r.rejected[0],
3163            "should NOT reject at 1%: stat={}, cv={}",
3164            r.statistic, r.critical_values[0]
3165        );
3166    }
3167
3168    #[test]
3169    fn adf_with_fixed_lags() {
3170        // Use wider oscillation to avoid near-singular design matrix
3171        let data: Vec<f64> = (0..50)
3172            .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3173            .collect();
3174        let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3175        assert_eq!(r.n_lags, 2);
3176        assert!(r.statistic.is_finite());
3177    }
3178
3179    #[test]
3180    fn adf_constant_trend_model() {
3181        // Linear trend is unit-root-like under "constant" model
3182        // but with "constant+trend" model, it should be recognized
3183        let data: Vec<f64> = (0..30)
3184            .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3185            .collect();
3186        let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3187        assert!(r.statistic.is_finite());
3188        assert_eq!(r.critical_values.len(), 3);
3189    }
3190
3191    #[test]
3192    fn adf_edge_cases() {
3193        // Too few data points
3194        assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3195        // NaN
3196        let mut data = vec![0.0; 20];
3197        data[5] = f64::NAN;
3198        assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3199    }
3200
3201    #[test]
3202    fn adf_critical_values_constant() {
3203        // Verify critical values are reasonable for n=100
3204        let cv = adf_critical_values(AdfModel::Constant, 100);
3205        // At n=100: approximately -3.51, -2.89, -2.58
3206        assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3207        assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3208        assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3209    }
3210
3211    #[test]
3212    fn adf_critical_values_ordering() {
3213        let cv = adf_critical_values(AdfModel::Constant, 50);
3214        // 1% < 5% < 10% (more negative for stricter levels)
3215        assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3216        assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3217    }
3218}
3219
3220#[cfg(test)]
3221mod proptests {
3222    use super::*;
3223    use proptest::prelude::*;
3224
3225    proptest! {
3226        #[test]
3227        fn one_sample_p_bounded(
3228            data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3229            mu0 in -1e3_f64..1e3
3230        ) {
3231            if let Some(r) = one_sample_t_test(&data, mu0) {
3232                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3233            }
3234        }
3235
3236        #[test]
3237        fn two_sample_p_bounded(
3238            a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3239            b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3240        ) {
3241            if let Some(r) = two_sample_t_test(&a, &b) {
3242                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3243            }
3244        }
3245
3246        #[test]
3247        fn anova_p_bounded(
3248            g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3249            g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3250            g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3251        ) {
3252            if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3253                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3254                prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3255            }
3256        }
3257
3258        #[test]
3259        fn bonferroni_monotone(
3260            ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3261        ) {
3262            let adj = bonferroni_correction(&ps).expect("should compute");
3263            for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3264                prop_assert!(adjusted >= orig - 1e-15,
3265                    "adj[{i}] = {adjusted} < orig = {orig}");
3266            }
3267        }
3268
3269        #[test]
3270        fn shapiro_wilk_p_bounded(
3271            data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3272        ) {
3273            if let Some(r) = shapiro_wilk_test(&data) {
3274                prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3275                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3276            }
3277        }
3278
3279        #[test]
3280        fn anderson_darling_p_bounded(
3281            data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3282        ) {
3283            if let Some(r) = anderson_darling_test(&data) {
3284                prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3285                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3286            }
3287        }
3288
3289        #[test]
3290        fn mann_whitney_p_bounded(
3291            a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3292            b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3293        ) {
3294            if let Some(r) = mann_whitney_u_test(&a, &b) {
3295                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3296                prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3297            }
3298        }
3299
3300        #[test]
3301        fn wilcoxon_p_bounded(
3302            diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3303        ) {
3304            let zeros: Vec<f64> = vec![0.0; diffs.len()];
3305            if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3306                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3307            }
3308        }
3309
3310        #[test]
3311        fn bartlett_p_bounded(
3312            g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3313            g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3314        ) {
3315            if let Some(r) = bartlett_test(&[&g1, &g2]) {
3316                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3317                prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3318            }
3319        }
3320
3321        #[test]
3322        fn fisher_p_bounded(
3323            a in 0_u64..20,
3324            b in 0_u64..20,
3325            c in 0_u64..20,
3326            d in 0_u64..20,
3327        ) {
3328            if let Some(r) = fisher_exact_test(a, b, c, d) {
3329                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3330            }
3331        }
3332
3333        #[test]
3334        fn mann_kendall_p_bounded(
3335            data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3336        ) {
3337            if let Some(r) = mann_kendall_test(&data) {
3338                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3339                prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3340                    "tau = {}", r.kendall_tau);
3341            }
3342        }
3343
3344        #[test]
3345        fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3346            let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3347            let r = mann_kendall_test(&data).expect("should compute");
3348            prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3349                "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3350            prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3351        }
3352    }
3353}