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