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