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