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!(
2248            a.statistic_star > 300.0,
2249            "expected large A*², got {}",
2250            a.statistic_star
2251        );
2252        assert!(
2253            a.p_value < 0.5,
2254            "test() overflowed toward normal: p={}",
2255            a.p_value
2256        );
2257        assert!(
2258            b.p_value < 0.5,
2259            "normality() overflowed toward normal: p={}",
2260            b.p_value
2261        );
2262    }
2263
2264    // -----------------------------------------------------------------------
2265    // One-sample t-test
2266    // -----------------------------------------------------------------------
2267
2268    #[test]
2269    fn one_sample_null_true() {
2270        let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2271        let r = one_sample_t_test(&data, 5.0).expect("should compute");
2272        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2273    }
2274
2275    #[test]
2276    fn one_sample_null_false() {
2277        let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2278        let r = one_sample_t_test(&data, 10.0).expect("should compute");
2279        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2280    }
2281
2282    #[test]
2283    fn one_sample_edge_cases() {
2284        assert!(one_sample_t_test(&[1.0], 0.0).is_none()); // n < 2
2285        assert!(one_sample_t_test(&[5.0, 5.0, 5.0], 5.0).is_none()); // zero var
2286        assert!(one_sample_t_test(&[1.0, f64::NAN, 3.0], 2.0).is_none());
2287    }
2288
2289    // -----------------------------------------------------------------------
2290    // Two-sample t-test
2291    // -----------------------------------------------------------------------
2292
2293    #[test]
2294    fn two_sample_same_mean() {
2295        let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2296        let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2297        let r = two_sample_t_test(&a, &b).expect("should compute");
2298        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2299    }
2300
2301    #[test]
2302    fn two_sample_different_means() {
2303        let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2304        let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2305        let r = two_sample_t_test(&a, &b).expect("should compute");
2306        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2307    }
2308
2309    #[test]
2310    fn two_sample_different_sizes() {
2311        let a = [1.0, 2.0, 3.0];
2312        let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2313        let r = two_sample_t_test(&a, &b).expect("should compute");
2314        assert!(r.p_value < 0.05);
2315    }
2316
2317    #[test]
2318    fn two_sample_edge_cases() {
2319        assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2320        assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2321    }
2322
2323    // -----------------------------------------------------------------------
2324    // Paired t-test
2325    // -----------------------------------------------------------------------
2326
2327    #[test]
2328    fn paired_no_difference() {
2329        let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2330        let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2331        let r = paired_t_test(&x, &y).expect("should compute");
2332        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2333    }
2334
2335    #[test]
2336    fn paired_significant_difference() {
2337        // Differences have non-zero variance
2338        let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2339        let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2340        let r = paired_t_test(&before, &after).expect("should compute");
2341        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2342        assert!(r.statistic < 0.0); // after > before
2343    }
2344
2345    #[test]
2346    fn paired_edge_cases() {
2347        assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); // length mismatch
2348        assert!(paired_t_test(&[1.0], &[2.0]).is_none()); // n < 2
2349    }
2350
2351    // -----------------------------------------------------------------------
2352    // ANOVA
2353    // -----------------------------------------------------------------------
2354
2355    #[test]
2356    fn anova_same_means() {
2357        let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2358        let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2359        let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2360        let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2361        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2362    }
2363
2364    #[test]
2365    fn anova_different_means() {
2366        let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2367        let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2368        let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2369        let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2370        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2371        assert!(r.df_between == 2);
2372        assert!(r.df_within == 12);
2373    }
2374
2375    #[test]
2376    fn anova_ss_decomposition() {
2377        let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2378        let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2379        let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2380        // SS_total = SS_between + SS_within
2381        let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2382        let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2383        assert!(
2384            (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2385            "SS decomposition: {ss_total} vs {} + {}",
2386            r.ss_between,
2387            r.ss_within
2388        );
2389    }
2390
2391    #[test]
2392    fn anova_edge_cases() {
2393        let g1 = [1.0, 2.0, 3.0];
2394        assert!(one_way_anova(&[&g1]).is_none()); // < 2 groups
2395    }
2396
2397    // -----------------------------------------------------------------------
2398    // Chi-squared goodness of fit
2399    // -----------------------------------------------------------------------
2400
2401    #[test]
2402    fn chi2_gof_uniform() {
2403        // Perfect uniform distribution
2404        let observed = [25.0, 25.0, 25.0, 25.0];
2405        let expected = [25.0, 25.0, 25.0, 25.0];
2406        let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2407        assert!((r.statistic).abs() < 1e-15);
2408        assert!((r.p_value - 1.0).abs() < 0.01);
2409    }
2410
2411    #[test]
2412    fn chi2_gof_significant() {
2413        let observed = [90.0, 10.0];
2414        let expected = [50.0, 50.0];
2415        let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2416        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2417    }
2418
2419    #[test]
2420    fn chi2_gof_edge_cases() {
2421        assert!(chi_squared_goodness_of_fit(&[10.0], &[10.0]).is_none()); // < 2
2422        assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0, 0.0]).is_none()); // expected 0
2423        assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0]).is_none()); // mismatch
2424    }
2425
2426    // -----------------------------------------------------------------------
2427    // Chi-squared independence
2428    // -----------------------------------------------------------------------
2429
2430    #[test]
2431    fn chi2_independence_significant() {
2432        // Strong association
2433        let table = [30.0, 10.0, 10.0, 50.0];
2434        let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2435        assert!(r.p_value < 0.001, "p = {}", r.p_value);
2436        assert!((r.df - 1.0).abs() < 1e-10);
2437    }
2438
2439    #[test]
2440    fn chi2_independence_not_significant() {
2441        // No association
2442        let table = [25.0, 25.0, 25.0, 25.0];
2443        let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2444        assert!(r.p_value > 0.3, "p = {}", r.p_value);
2445    }
2446
2447    #[test]
2448    fn chi2_independence_3x3() {
2449        let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2450        let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2451        assert!((r.df - 4.0).abs() < 1e-10);
2452        assert!(r.p_value < 0.05);
2453    }
2454
2455    #[test]
2456    fn chi2_independence_edge_cases() {
2457        assert!(chi_squared_independence(&[10.0, 20.0], 1, 2).is_none()); // 1 row
2458        assert!(chi_squared_independence(&[10.0, 20.0], 2, 1).is_none()); // 1 col
2459        assert!(chi_squared_independence(&[10.0], 2, 2).is_none()); // wrong size
2460    }
2461
2462    // -----------------------------------------------------------------------
2463    // Jarque-Bera
2464    // -----------------------------------------------------------------------
2465
2466    #[test]
2467    fn jb_normal_data() {
2468        // Symmetric, light-tailed data
2469        let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2470        let r = jarque_bera_test(&data).expect("should compute");
2471        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2472    }
2473
2474    #[test]
2475    fn jb_skewed_data() {
2476        // Highly right-skewed
2477        let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2478        let r = jarque_bera_test(&data).expect("should compute");
2479        assert!(r.p_value < 0.05, "p = {}", r.p_value);
2480    }
2481
2482    #[test]
2483    fn jb_edge_cases() {
2484        assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); // n < 8
2485    }
2486
2487    // -----------------------------------------------------------------------
2488    // Anderson-Darling
2489    // -----------------------------------------------------------------------
2490
2491    #[test]
2492    fn ad_normal_data() {
2493        let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2494        let r = anderson_darling_test(&data).expect("should compute");
2495        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2496        assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2497        assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2498    }
2499
2500    #[test]
2501    fn ad_skewed_data() {
2502        // Exponential-like data — not normal
2503        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];
2504        let r = anderson_darling_test(&data).expect("should compute");
2505        assert!(
2506            r.p_value < 0.05,
2507            "p = {} (should reject normality)",
2508            r.p_value
2509        );
2510    }
2511
2512    #[test]
2513    fn ad_bimodal_data() {
2514        // Bimodal data — clearly not normal
2515        let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2516        data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2517        let r = anderson_darling_test(&data).expect("should compute");
2518        assert!(
2519            r.p_value < 0.01,
2520            "p = {} (bimodal should reject normality)",
2521            r.p_value
2522        );
2523    }
2524
2525    #[test]
2526    fn ad_large_normal_sample() {
2527        let n = 100;
2528        let data: Vec<f64> = (1..=n)
2529            .map(|i| {
2530                let p = (i as f64 - 0.5) / n as f64;
2531                special::inverse_normal_cdf(p)
2532            })
2533            .collect();
2534        let r = anderson_darling_test(&data).expect("should compute");
2535        assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2536    }
2537
2538    #[test]
2539    fn ad_edge_cases() {
2540        assert!(anderson_darling_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); // n < 8
2541        assert!(anderson_darling_test(&[5.0; 10]).is_none()); // constant
2542        assert!(anderson_darling_test(&[1.0, f64::NAN, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).is_none());
2543    }
2544
2545    #[test]
2546    fn ad_p_value_ranges() {
2547        // Test different A*² ranges for p-value formula
2548        // Use datasets that produce different A*² magnitudes
2549
2550        // Near-normal → small A*² (< 0.2 range)
2551        let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2552        let r = anderson_darling_test(&near_normal).expect("should compute");
2553        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2554
2555        // Heavy-tailed → large A*² (>= 0.6 range)
2556        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];
2557        let r = anderson_darling_test(&heavy_tail).expect("should compute");
2558        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2559    }
2560
2561    // -----------------------------------------------------------------------
2562    // Anderson-Darling normality (anderson_darling_normality)
2563    // -----------------------------------------------------------------------
2564
2565    #[test]
2566    fn ad_normal_data_large_p() {
2567        // Clearly normal data → cannot reject normality (p > 0.05)
2568        let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2569        let r = anderson_darling_normality(&data).unwrap();
2570        assert!(r.p_value > 0.05, "p={}", r.p_value);
2571    }
2572
2573    #[test]
2574    fn ad_exponential_data_small_p() {
2575        // Clearly non-normal (exponential) → reject normality (p < 0.05)
2576        let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2577        let r = anderson_darling_normality(&data).unwrap();
2578        assert!(r.p_value < 0.05, "p={}", r.p_value);
2579    }
2580
2581    #[test]
2582    fn ad_statistic_non_negative() {
2583        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2584        let r = anderson_darling_normality(&data).unwrap();
2585        assert!(r.statistic >= 0.0);
2586        assert!(r.statistic_modified >= r.statistic);
2587    }
2588
2589    #[test]
2590    fn ad_p_value_in_range() {
2591        let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2592        let r = anderson_darling_normality(&data).unwrap();
2593        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2594    }
2595
2596    #[test]
2597    fn ad_insufficient_data() {
2598        assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); // n < 3
2599    }
2600
2601    #[test]
2602    fn ad_degenerate_data() {
2603        // All same value → std = 0 → None
2604        assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2605    }
2606
2607    #[test]
2608    fn ad_modified_statistic_formula() {
2609        // A²* > A² for any finite n
2610        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2611        let r = anderson_darling_normality(&data).unwrap();
2612        assert!(r.statistic_modified > r.statistic - 1e-10);
2613    }
2614
2615    /// Validates A²* = A² · (1 + 0.75/n + 2.25/n²) against manual computation.
2616    ///
2617    /// Reference: Stephens (1974), "EDF statistics for goodness of fit",
2618    /// J. Amer. Statist. Assoc. 69(347), 730–737 (composite normal case).
2619    #[test]
2620    fn ad_correction_factor_formula() {
2621        let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2622        let n = data.len() as f64;
2623        let r = anderson_darling_normality(&data).unwrap();
2624
2625        // Verify the Stephens correction formula A²* = A² · (1 + 0.75/n + 2.25/n²)
2626        let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2627        assert!(
2628            (r.statistic_modified - expected_star).abs() < 1e-12,
2629            "A²* = {}, expected = {}",
2630            r.statistic_modified,
2631            expected_star
2632        );
2633
2634        // A² must be positive (it is a sum of log terms with negative coefficient)
2635        assert!(
2636            r.statistic >= 0.0,
2637            "A² = {} must be non-negative",
2638            r.statistic
2639        );
2640    }
2641
2642    /// Validates that the A² summation formula implements:
2643    /// A² = -n - (1/n)·Σᵢ₌₀ⁿ⁻¹ (2i+1)·[ln Φ(zᵢ) + ln(1 − Φ(z_{n−1−i}))]
2644    ///
2645    /// This tests the formula structure by checking that a perfectly symmetric
2646    /// dataset centered at 0 produces a smaller A² than a skewed dataset.
2647    #[test]
2648    fn ad_formula_structure_symmetric_vs_skewed() {
2649        // Symmetric around mean → smaller A²
2650        let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2651        // Right-skewed → larger A² (heavier right tail deviates from normality)
2652        let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2653
2654        let r_sym = anderson_darling_normality(&symmetric).unwrap();
2655        let r_skew = anderson_darling_normality(&skewed).unwrap();
2656
2657        assert!(
2658            r_sym.statistic < r_skew.statistic,
2659            "Symmetric A²={} should be less than skewed A²={}",
2660            r_sym.statistic,
2661            r_skew.statistic
2662        );
2663    }
2664
2665    /// Validates p-value invariants for the Anderson-Darling test.
2666    ///
2667    /// - Normal data: p > 0.05 (cannot reject normality)
2668    /// - Exponential data: p < 0.05 (reject normality)
2669    #[test]
2670    fn ad_pvalue_invariants() {
2671        // Near-normal data — tightly clustered, should not reject normality
2672        let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2673        let r_normal = anderson_darling_normality(&normal_data).unwrap();
2674        assert!(
2675            r_normal.p_value > 0.05,
2676            "Normal data: p = {} (expected > 0.05)",
2677            r_normal.p_value
2678        );
2679
2680        // Exponential data — heavy right tail, should reject normality
2681        let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2682        let r_exp = anderson_darling_normality(&exp_data).unwrap();
2683        assert!(
2684            r_exp.p_value < 0.05,
2685            "Exponential data: p = {} (expected < 0.05)",
2686            r_exp.p_value
2687        );
2688    }
2689
2690    // -----------------------------------------------------------------------
2691    // Shapiro-Wilk
2692    // -----------------------------------------------------------------------
2693
2694    #[test]
2695    fn sw_normal_data() {
2696        // Approximately normal data (symmetric, bell-shaped)
2697        let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2698        let r = shapiro_wilk_test(&data).expect("should compute");
2699        assert!(r.w > 0.9, "W = {}", r.w);
2700        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2701    }
2702
2703    #[test]
2704    fn sw_bimodal_data() {
2705        // Bimodal data — clearly not normal
2706        let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2707        data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2708        let r = shapiro_wilk_test(&data).expect("should compute");
2709        assert!(
2710            r.p_value < 0.01,
2711            "p = {} (bimodal should reject normality)",
2712            r.p_value
2713        );
2714    }
2715
2716    #[test]
2717    fn sw_n3() {
2718        let data = [1.0, 2.0, 3.0];
2719        let r = shapiro_wilk_test(&data).expect("n=3 should work");
2720        assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2721        assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2722    }
2723
2724    #[test]
2725    fn sw_n4() {
2726        let data = [1.0, 2.0, 3.0, 4.0];
2727        let r = shapiro_wilk_test(&data).expect("n=4 should work");
2728        assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2729        assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2730    }
2731
2732    #[test]
2733    fn sw_n5() {
2734        let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2735        let r = shapiro_wilk_test(&data).expect("n=5 should work");
2736        assert!(r.w > 0.9, "W = {}", r.w);
2737        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2738    }
2739
2740    #[test]
2741    fn sw_skewed_data() {
2742        // Exponential-like data — not normal
2743        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];
2744        let r = shapiro_wilk_test(&data).expect("should compute");
2745        assert!(
2746            r.p_value < 0.05,
2747            "p = {} (skewed data should reject normality)",
2748            r.p_value
2749        );
2750    }
2751
2752    #[test]
2753    fn sw_large_normal_sample() {
2754        // Generate pseudo-normal data via Box-Muller-like approach
2755        // Use linearly spaced quantiles from standard normal
2756        let n = 100;
2757        let data: Vec<f64> = (1..=n)
2758            .map(|i| {
2759                let p = (i as f64 - 0.5) / n as f64;
2760                special::inverse_normal_cdf(p)
2761            })
2762            .collect();
2763        let r = shapiro_wilk_test(&data).expect("should compute");
2764        assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2765        assert!(r.p_value > 0.05, "p = {}", r.p_value);
2766    }
2767
2768    #[test]
2769    fn sw_w_bounded() {
2770        // W should be in (0, 1] for any valid data
2771        let datasets: Vec<Vec<f64>> = vec![
2772            vec![1.0, 2.0, 3.0],
2773            vec![1.0, 1.0, 2.0, 3.0, 3.0],
2774            vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2775            (0..20).map(|i| (i as f64).powi(2)).collect(),
2776        ];
2777        for (idx, data) in datasets.iter().enumerate() {
2778            let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2779            assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2780            assert!(
2781                r.p_value >= 0.0 && r.p_value <= 1.0,
2782                "dataset {idx}: p = {}",
2783                r.p_value
2784            );
2785        }
2786    }
2787
2788    #[test]
2789    fn sw_edge_cases() {
2790        assert!(shapiro_wilk_test(&[1.0, 2.0]).is_none()); // n < 3
2791        assert!(shapiro_wilk_test(&[]).is_none()); // empty
2792        assert!(shapiro_wilk_test(&[5.0, 5.0, 5.0]).is_none()); // constant
2793        assert!(shapiro_wilk_test(&[1.0, f64::NAN, 3.0]).is_none()); // NaN
2794        assert!(shapiro_wilk_test(&[1.0, f64::INFINITY, 3.0]).is_none()); // Inf
2795    }
2796
2797    #[test]
2798    fn sw_n5001_rejected() {
2799        let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2800        assert!(shapiro_wilk_test(&data).is_none()); // n > 5000
2801    }
2802
2803    // -----------------------------------------------------------------------
2804    // Mann-Whitney U
2805    // -----------------------------------------------------------------------
2806
2807    #[test]
2808    fn mw_clearly_different() {
2809        let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2810        let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2811        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2812        assert!(r.p_value < 0.05, "p = {}", r.p_value);
2813    }
2814
2815    #[test]
2816    fn mw_same_distribution() {
2817        let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2818        let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2819        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2820        assert!(
2821            r.p_value > 0.3,
2822            "p = {} (interleaved, same dist)",
2823            r.p_value
2824        );
2825    }
2826
2827    #[test]
2828    fn mw_with_ties() {
2829        let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2830        let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2831        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2832        assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2833    }
2834
2835    #[test]
2836    fn mw_different_sizes() {
2837        let a = [1.0, 2.0, 3.0];
2838        let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2839        let r = mann_whitney_u_test(&a, &b).expect("should compute");
2840        assert!(r.p_value < 0.05);
2841    }
2842
2843    #[test]
2844    fn mw_edge_cases() {
2845        assert!(mann_whitney_u_test(&[1.0], &[2.0, 3.0]).is_none()); // n1 < 2
2846        assert!(mann_whitney_u_test(&[1.0, 2.0], &[3.0]).is_none()); // n2 < 2
2847        assert!(mann_whitney_u_test(&[1.0, f64::NAN], &[2.0, 3.0]).is_none());
2848    }
2849
2850    // -----------------------------------------------------------------------
2851    // Wilcoxon signed-rank
2852    // -----------------------------------------------------------------------
2853
2854    #[test]
2855    fn wilcoxon_significant_increase() {
2856        let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2857        let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2858        let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2859        assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2860    }
2861
2862    #[test]
2863    fn wilcoxon_no_difference() {
2864        let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2865        let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2866        let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2867        assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2868    }
2869
2870    #[test]
2871    fn wilcoxon_with_ties() {
2872        // Some differences are equal in magnitude
2873        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2874        let y = [0.0, 1.0, 2.0, 3.0, 4.0]; // constant difference = 1.0
2875        let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2876        // All differences positive with ties in magnitude
2877        assert!(r.statistic > 0.0);
2878    }
2879
2880    #[test]
2881    fn wilcoxon_with_zero_diffs() {
2882        // Some pairs are equal → zero differences discarded
2883        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2884        let y = [1.0, 2.0, 3.0, 3.0, 4.0]; // first 3 are zero diffs
2885        let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2886        assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2887    }
2888
2889    #[test]
2890    fn wilcoxon_edge_cases() {
2891        assert!(wilcoxon_signed_rank_test(&[1.0, 2.0], &[3.0]).is_none()); // mismatch
2892        assert!(wilcoxon_signed_rank_test(&[1.0], &[2.0]).is_none()); // n < 2
2893                                                                      // All zero differences → fewer than 2 non-zero diffs
2894        assert!(wilcoxon_signed_rank_test(&[5.0, 5.0], &[5.0, 5.0]).is_none());
2895    }
2896
2897    // -----------------------------------------------------------------------
2898    // Kruskal-Wallis
2899    // -----------------------------------------------------------------------
2900
2901    #[test]
2902    fn kw_clearly_different() {
2903        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2904        let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2905        let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
2906        let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2907        assert!(r.p_value < 0.01, "p = {}", r.p_value);
2908        assert!((r.df - 2.0).abs() < 1e-10);
2909    }
2910
2911    #[test]
2912    fn kw_same_distribution() {
2913        let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
2914        let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
2915        let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
2916        assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
2917    }
2918
2919    #[test]
2920    fn kw_with_ties() {
2921        let g1 = [1.0, 2.0, 2.0, 3.0];
2922        let g2 = [3.0, 4.0, 4.0, 5.0];
2923        let g3 = [5.0, 6.0, 6.0, 7.0];
2924        let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2925        assert!(r.statistic > 0.0);
2926    }
2927
2928    #[test]
2929    fn kw_edge_cases() {
2930        let g1 = [1.0, 2.0, 3.0];
2931        assert!(kruskal_wallis_test(&[&g1]).is_none()); // < 2 groups
2932    }
2933
2934    // -----------------------------------------------------------------------
2935    // Levene (Brown-Forsythe)
2936    // -----------------------------------------------------------------------
2937
2938    #[test]
2939    fn levene_equal_variance() {
2940        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2941        let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2942        let r = levene_test(&[&g1, &g2]).expect("should compute");
2943        // Both have same spread, different means → equal variance
2944        assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
2945    }
2946
2947    #[test]
2948    fn levene_unequal_variance() {
2949        let g1 = [4.5, 4.8, 5.0, 5.2, 5.5]; // small spread
2950        let g2 = [0.0, 2.0, 5.0, 8.0, 10.0]; // large spread
2951        let r = levene_test(&[&g1, &g2]).expect("should compute");
2952        assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
2953    }
2954
2955    #[test]
2956    fn levene_three_groups() {
2957        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2958        let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
2959        let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
2960        let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
2961        assert!(r.df >= 2.0);
2962    }
2963
2964    #[test]
2965    fn levene_edge_cases() {
2966        let g1 = [1.0, 2.0, 3.0];
2967        assert!(levene_test(&[&g1]).is_none()); // < 2 groups
2968    }
2969
2970    // -----------------------------------------------------------------------
2971    // Multiple comparison correction
2972    // -----------------------------------------------------------------------
2973
2974    #[test]
2975    fn bonferroni_basic() {
2976        let ps = [0.01, 0.04, 0.03, 0.005];
2977        let adj = bonferroni_correction(&ps).expect("should compute");
2978        assert!((adj[0] - 0.04).abs() < 1e-10);
2979        assert!((adj[1] - 0.16).abs() < 1e-10);
2980        assert!((adj[2] - 0.12).abs() < 1e-10);
2981        assert!((adj[3] - 0.02).abs() < 1e-10);
2982    }
2983
2984    #[test]
2985    fn bonferroni_capped_at_one() {
2986        let ps = [0.5, 0.6];
2987        let adj = bonferroni_correction(&ps).expect("should compute");
2988        assert!((adj[0] - 1.0).abs() < 1e-10);
2989        assert!((adj[1] - 1.0).abs() < 1e-10);
2990    }
2991
2992    #[test]
2993    fn bh_basic() {
2994        let ps = [0.01, 0.04, 0.03, 0.005];
2995        let adj = benjamini_hochberg(&ps).expect("should compute");
2996        // All adjusted p-values should be >= original
2997        for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
2998            assert!(
2999                adjusted >= orig - 1e-15,
3000                "adj[{i}] = {adjusted} < original {orig}"
3001            );
3002        }
3003        // Adjusted p-values should still be ordered (weakly) by original order
3004        // after reordering by original p-value
3005    }
3006
3007    #[test]
3008    fn bh_all_significant() {
3009        let ps = [0.001, 0.002, 0.003];
3010        let adj = benjamini_hochberg(&ps).expect("should compute");
3011        for &a in &adj {
3012            assert!(a < 0.05);
3013        }
3014    }
3015
3016    #[test]
3017    fn correction_edge_cases() {
3018        assert!(bonferroni_correction(&[]).is_none());
3019        assert!(benjamini_hochberg(&[]).is_none());
3020        assert!(bonferroni_correction(&[f64::NAN]).is_none());
3021    }
3022
3023    // -----------------------------------------------------------------------
3024    // Bartlett test
3025    // -----------------------------------------------------------------------
3026
3027    #[test]
3028    fn bartlett_equal_variances() {
3029        let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3030        let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3031        let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3032        assert!(
3033            r.p_value > 0.9,
3034            "equal variance → p high, got {}",
3035            r.p_value
3036        );
3037    }
3038
3039    #[test]
3040    fn bartlett_unequal_variances() {
3041        let g1 = [2.0, 3.0, 4.0, 5.0, 6.0]; // var ≈ 2.5
3042        let g2 = [10.0, 20.0, 30.0, 40.0, 50.0]; // var ≈ 250
3043        let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3044        assert!(
3045            r.p_value < 0.01,
3046            "very different variances → p < 0.01, got {}",
3047            r.p_value
3048        );
3049        assert!((r.df - 1.0).abs() < 1e-10); // k-1 = 1
3050    }
3051
3052    #[test]
3053    fn bartlett_three_groups() {
3054        let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3055        let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3056        let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; // much higher variance
3057        let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3058        assert!(r.p_value < 0.05, "one group with high variance");
3059        assert!((r.df - 2.0).abs() < 1e-10); // k-1 = 2
3060    }
3061
3062    #[test]
3063    fn bartlett_edge_cases() {
3064        let g1 = [1.0, 2.0, 3.0];
3065        assert!(bartlett_test(&[&g1]).is_none()); // < 2 groups
3066
3067        let g2 = [5.0, 5.0, 5.0]; // zero variance
3068        assert!(bartlett_test(&[&g1, &g2]).is_none());
3069
3070        let g3 = [1.0]; // group too small
3071        assert!(bartlett_test(&[&g1, &g3]).is_none());
3072    }
3073
3074    // -----------------------------------------------------------------------
3075    // Fisher exact test
3076    // -----------------------------------------------------------------------
3077
3078    #[test]
3079    fn fisher_tea_tasting() {
3080        // Classic Fisher tea-tasting: [[3,1],[1,3]]
3081        let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3082        // Known two-tailed p ≈ 0.4857
3083        assert!(
3084            (r.p_value - 0.4857).abs() < 0.01,
3085            "p ≈ 0.4857, got {}",
3086            r.p_value
3087        );
3088    }
3089
3090    #[test]
3091    fn fisher_significant() {
3092        // Strong association: [[10, 0], [0, 10]]
3093        let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3094        assert!(r.p_value < 0.001, "perfect association → p very small");
3095    }
3096
3097    #[test]
3098    fn fisher_no_association() {
3099        // Proportional table: [[5, 5], [5, 5]]
3100        let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3101        assert!(
3102            r.p_value > 0.9,
3103            "no association → p ≈ 1.0, got {}",
3104            r.p_value
3105        );
3106    }
3107
3108    #[test]
3109    fn fisher_small_table() {
3110        // [[1, 0], [0, 1]]
3111        let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3112        assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3113    }
3114
3115    #[test]
3116    fn fisher_asymmetric() {
3117        // [[8, 2], [1, 5]]
3118        let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3119        assert!(r.p_value < 0.05, "significant association");
3120    }
3121
3122    #[test]
3123    fn fisher_edge_cases() {
3124        // Zero marginals → None
3125        assert!(fisher_exact_test(0, 0, 1, 2).is_none()); // row1 = 0
3126        assert!(fisher_exact_test(1, 2, 0, 0).is_none()); // row2 = 0
3127        assert!(fisher_exact_test(0, 1, 0, 2).is_none()); // col1 = 0
3128    }
3129
3130    #[test]
3131    fn fisher_odds_ratio() {
3132        let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3133        // OR = (3*3)/(1*1) = 9
3134        assert!(
3135            (r.statistic - 9.0).abs() < 1e-10,
3136            "OR = 9, got {}",
3137            r.statistic
3138        );
3139    }
3140
3141    // -----------------------------------------------------------------------
3142    // Mann-Kendall trend test
3143    // -----------------------------------------------------------------------
3144
3145    #[test]
3146    fn mk_increasing_trend() {
3147        let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3148        let r = mann_kendall_test(&data).expect("should compute");
3149        assert!(r.p_value < 0.01, "p = {}", r.p_value);
3150        assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3151        assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3152        assert!(r.s_statistic > 0);
3153    }
3154
3155    #[test]
3156    fn mk_decreasing_trend() {
3157        let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3158        let r = mann_kendall_test(&data).expect("should compute");
3159        assert!(r.p_value < 0.01, "p = {}", r.p_value);
3160        assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3161        assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3162        assert!(r.s_statistic < 0);
3163    }
3164
3165    #[test]
3166    fn mk_no_trend() {
3167        // Random-looking data with no clear trend
3168        let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3169        let r = mann_kendall_test(&data).expect("should compute");
3170        // Should not detect significant trend
3171        assert!(
3172            r.p_value > 0.05,
3173            "p = {} (should be > 0.05 for no trend)",
3174            r.p_value
3175        );
3176    }
3177
3178    #[test]
3179    fn mk_perfect_monotone() {
3180        let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3181        let r = mann_kendall_test(&data).expect("should compute");
3182        // Perfect monotone: S = n(n-1)/2 = 45, tau = 1.0
3183        assert_eq!(r.s_statistic, 45);
3184        assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3185        assert!((r.sen_slope - 1.0).abs() < 1e-10);
3186    }
3187
3188    #[test]
3189    fn mk_with_ties() {
3190        let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3191        let r = mann_kendall_test(&data).expect("should compute");
3192        assert!(r.s_statistic > 0);
3193        // Tie correction should reduce variance
3194        let n = data.len() as f64;
3195        let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3196        assert!(r.variance < base_var, "ties should reduce variance");
3197    }
3198
3199    #[test]
3200    fn mk_edge_cases() {
3201        // Too few data points
3202        assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3203        // NaN
3204        assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3205        // All identical (zero variance)
3206        assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3207    }
3208
3209    #[test]
3210    fn mk_minimum_n() {
3211        // n = 4 should work
3212        let data = [1.0, 2.0, 3.0, 4.0];
3213        let r = mann_kendall_test(&data).expect("n=4 should work");
3214        assert_eq!(r.s_statistic, 6); // C(4,2) = 6 pairs, all positive
3215        assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3216    }
3217
3218    #[test]
3219    fn mk_sen_slope_robust_to_outlier() {
3220        // Mostly linear (slope ≈ 1) with one outlier
3221        let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3222        let r = mann_kendall_test(&data).expect("should compute");
3223        // Sen's slope should be close to 1.0 despite the outlier at index 3
3224        assert!(
3225            (r.sen_slope - 1.0).abs() < 0.5,
3226            "sen slope = {}, expected ≈ 1.0",
3227            r.sen_slope
3228        );
3229    }
3230
3231    // -----------------------------------------------------------------------
3232    // ADF test
3233    // -----------------------------------------------------------------------
3234
3235    #[test]
3236    fn adf_stationary_mean_reverting() {
3237        // Strongly mean-reverting AR(1) process: y_t = 0.3*y_{t-1} + noise
3238        // This is clearly stationary (|ρ| < 1)
3239        let data = [
3240            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,
3241            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,
3242            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,
3243        ];
3244        let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3245        // Should reject H₀ (series is stationary)
3246        assert!(
3247            r.rejected[2],
3248            "should reject at 10%: stat={}, cv={}",
3249            r.statistic, r.critical_values[2]
3250        );
3251    }
3252
3253    #[test]
3254    fn adf_nonstationary_random_walk() {
3255        // Cumulative sum simulates random walk (non-stationary)
3256        let increments = [
3257            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,
3258            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,
3259            -0.13, 0.15, -0.1,
3260        ];
3261        let mut walk = Vec::with_capacity(increments.len());
3262        let mut cum = 0.0;
3263        for &inc in &increments {
3264            cum += inc;
3265            walk.push(cum);
3266        }
3267        let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3268        // Random walk should NOT reject at 1%
3269        assert!(
3270            !r.rejected[0],
3271            "should NOT reject at 1%: stat={}, cv={}",
3272            r.statistic, r.critical_values[0]
3273        );
3274    }
3275
3276    #[test]
3277    fn adf_with_fixed_lags() {
3278        // Use wider oscillation to avoid near-singular design matrix
3279        let data: Vec<f64> = (0..50)
3280            .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3281            .collect();
3282        let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3283        assert_eq!(r.n_lags, 2);
3284        assert!(r.statistic.is_finite());
3285    }
3286
3287    #[test]
3288    fn adf_constant_trend_model() {
3289        // Linear trend is unit-root-like under "constant" model
3290        // but with "constant+trend" model, it should be recognized
3291        let data: Vec<f64> = (0..30)
3292            .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3293            .collect();
3294        let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3295        assert!(r.statistic.is_finite());
3296        assert_eq!(r.critical_values.len(), 3);
3297    }
3298
3299    #[test]
3300    fn adf_edge_cases() {
3301        // Too few data points
3302        assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3303        // NaN
3304        let mut data = vec![0.0; 20];
3305        data[5] = f64::NAN;
3306        assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3307    }
3308
3309    #[test]
3310    fn adf_critical_values_constant() {
3311        // Verify critical values are reasonable for n=100
3312        let cv = adf_critical_values(AdfModel::Constant, 100);
3313        // At n=100: approximately -3.51, -2.89, -2.58
3314        assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3315        assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3316        assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3317    }
3318
3319    #[test]
3320    fn adf_critical_values_ordering() {
3321        let cv = adf_critical_values(AdfModel::Constant, 50);
3322        // 1% < 5% < 10% (more negative for stricter levels)
3323        assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3324        assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3325    }
3326}
3327
3328#[cfg(test)]
3329mod proptests {
3330    use super::*;
3331    use proptest::prelude::*;
3332
3333    proptest! {
3334        #[test]
3335        fn one_sample_p_bounded(
3336            data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3337            mu0 in -1e3_f64..1e3
3338        ) {
3339            if let Some(r) = one_sample_t_test(&data, mu0) {
3340                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3341            }
3342        }
3343
3344        #[test]
3345        fn two_sample_p_bounded(
3346            a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3347            b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3348        ) {
3349            if let Some(r) = two_sample_t_test(&a, &b) {
3350                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3351            }
3352        }
3353
3354        #[test]
3355        fn anova_p_bounded(
3356            g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3357            g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3358            g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3359        ) {
3360            if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3361                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3362                prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3363            }
3364        }
3365
3366        #[test]
3367        fn bonferroni_monotone(
3368            ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3369        ) {
3370            let adj = bonferroni_correction(&ps).expect("should compute");
3371            for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3372                prop_assert!(adjusted >= orig - 1e-15,
3373                    "adj[{i}] = {adjusted} < orig = {orig}");
3374            }
3375        }
3376
3377        #[test]
3378        fn shapiro_wilk_p_bounded(
3379            data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3380        ) {
3381            if let Some(r) = shapiro_wilk_test(&data) {
3382                prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3383                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3384            }
3385        }
3386
3387        #[test]
3388        fn anderson_darling_p_bounded(
3389            data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3390        ) {
3391            if let Some(r) = anderson_darling_test(&data) {
3392                prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3393                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3394            }
3395        }
3396
3397        #[test]
3398        fn mann_whitney_p_bounded(
3399            a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3400            b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3401        ) {
3402            if let Some(r) = mann_whitney_u_test(&a, &b) {
3403                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3404                prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3405            }
3406        }
3407
3408        #[test]
3409        fn wilcoxon_p_bounded(
3410            diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3411        ) {
3412            let zeros: Vec<f64> = vec![0.0; diffs.len()];
3413            if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3414                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3415            }
3416        }
3417
3418        #[test]
3419        fn bartlett_p_bounded(
3420            g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3421            g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3422        ) {
3423            if let Some(r) = bartlett_test(&[&g1, &g2]) {
3424                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3425                prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3426            }
3427        }
3428
3429        #[test]
3430        fn fisher_p_bounded(
3431            a in 0_u64..20,
3432            b in 0_u64..20,
3433            c in 0_u64..20,
3434            d in 0_u64..20,
3435        ) {
3436            if let Some(r) = fisher_exact_test(a, b, c, d) {
3437                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3438            }
3439        }
3440
3441        #[test]
3442        fn mann_kendall_p_bounded(
3443            data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3444        ) {
3445            if let Some(r) = mann_kendall_test(&data) {
3446                prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3447                prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3448                    "tau = {}", r.kendall_tau);
3449            }
3450        }
3451
3452        #[test]
3453        fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3454            let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3455            let r = mann_kendall_test(&data).expect("should compute");
3456            prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3457                "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3458            prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3459        }
3460    }
3461}