Skip to main content

u_analytics/
correlation.rs

1//! Correlation analysis.
2//!
3//! Pearson, Spearman, and Kendall correlation coefficients with p-values,
4//! correlation matrices, and Fisher z-transformation confidence intervals.
5//!
6//! # Examples
7//!
8//! ```
9//! use u_analytics::correlation::{pearson, spearman, kendall_tau_b};
10//!
11//! let x = [1.0, 2.0, 3.0, 4.0, 5.0];
12//! let y = [2.0, 4.0, 5.0, 4.0, 5.0];
13//!
14//! let p = pearson(&x, &y).unwrap();
15//! assert!(p.r > 0.7);
16//! assert!(p.p_value < 0.2);
17//!
18//! let s = spearman(&x, &y).unwrap();
19//! assert!(s.r > 0.7);
20//!
21//! let k = kendall_tau_b(&x, &y).unwrap();
22//! assert!(k.r > 0.5);
23//! ```
24
25use crate::error::{check_option, check_series, TimeSeriesError};
26use std::ops::RangeInclusive;
27use u_numflow::matrix::Matrix;
28use u_numflow::special;
29use u_numflow::stats;
30
31/// Result of a correlation computation.
32#[derive(Debug, Clone, Copy)]
33pub struct CorrelationResult {
34    /// Correlation coefficient in [-1, 1].
35    pub r: f64,
36    /// Two-tailed p-value for testing H₀: ρ = 0.
37    pub p_value: f64,
38    /// Sample size.
39    pub n: usize,
40}
41
42/// Confidence interval for a correlation coefficient.
43#[derive(Debug, Clone, Copy)]
44pub struct CorrelationCI {
45    /// Lower bound of the confidence interval.
46    pub lower: f64,
47    /// Upper bound of the confidence interval.
48    pub upper: f64,
49    /// Confidence level (e.g. 0.95).
50    pub confidence: f64,
51}
52
53// ---------------------------------------------------------------------------
54// Pearson
55// ---------------------------------------------------------------------------
56
57/// Computes Pearson product-moment correlation coefficient and p-value.
58///
59/// # Algorithm
60///
61/// r = cov(x,y) / (σ_x · σ_y)
62///
63/// p-value via t-test: t = r·√(n-2) / √(1-r²), df = n-2.
64///
65/// # Returns
66///
67/// `None` if either slice has fewer than 3 elements, the slices differ in
68/// length, or either variable has zero variance.
69///
70/// # References
71///
72/// Pearson (1895). "Note on regression and inheritance in the case of
73/// two parents". Proceedings of the Royal Society of London, 58, 240–242.
74///
75/// # Examples
76///
77/// ```
78/// use u_analytics::correlation::pearson;
79///
80/// let x = [1.0, 2.0, 3.0, 4.0, 5.0];
81/// let y = [2.0, 4.0, 6.0, 8.0, 10.0];
82/// let result = pearson(&x, &y).unwrap();
83/// assert!((result.r - 1.0).abs() < 1e-10);
84/// ```
85pub fn pearson(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
86    let n = x.len();
87    if n < 3 || n != y.len() {
88        return None;
89    }
90
91    if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
92        return None;
93    }
94
95    let cov = stats::covariance(x, y)?;
96    let sx = stats::std_dev(x)?;
97    let sy = stats::std_dev(y)?;
98
99    if sx < 1e-300 || sy < 1e-300 {
100        return None; // zero variance
101    }
102
103    let r = (cov / (sx * sy)).clamp(-1.0, 1.0);
104    let p_value = correlation_p_value(r, n);
105
106    Some(CorrelationResult { r, p_value, n })
107}
108
109// ---------------------------------------------------------------------------
110// Spearman
111// ---------------------------------------------------------------------------
112
113/// Computes Spearman rank correlation coefficient and p-value.
114///
115/// # Algorithm
116///
117/// Ranks both variables using the mid-rank method for ties, then computes
118/// Pearson correlation on the ranks. P-value uses the same t-test
119/// approximation as Pearson.
120///
121/// # Returns
122///
123/// `None` if fewer than 3 observations, slices differ in length, or inputs
124/// contain non-finite values.
125///
126/// # References
127///
128/// Spearman (1904). "The proof and measurement of association between two
129/// things". The American Journal of Psychology, 15(1), 72–101.
130///
131/// # Examples
132///
133/// ```
134/// use u_analytics::correlation::spearman;
135///
136/// let x = [1.0, 2.0, 3.0, 4.0, 5.0];
137/// let y = [5.0, 6.0, 7.0, 8.0, 7.0];
138/// let result = spearman(&x, &y).unwrap();
139/// assert!(result.r > 0.5);
140/// ```
141pub fn spearman(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
142    let n = x.len();
143    if n < 3 || n != y.len() {
144        return None;
145    }
146
147    if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
148        return None;
149    }
150
151    let rx = rank_data(x);
152    let ry = rank_data(y);
153
154    // Pearson on ranks
155    let cov = stats::covariance(&rx, &ry)?;
156    let srx = stats::std_dev(&rx)?;
157    let sry = stats::std_dev(&ry)?;
158
159    if srx < 1e-300 || sry < 1e-300 {
160        return None;
161    }
162
163    let r = (cov / (srx * sry)).clamp(-1.0, 1.0);
164    let p_value = correlation_p_value(r, n);
165
166    Some(CorrelationResult { r, p_value, n })
167}
168
169// ---------------------------------------------------------------------------
170// Kendall tau-b
171// ---------------------------------------------------------------------------
172
173/// Computes Kendall's tau-b correlation coefficient with tie correction.
174///
175/// # Algorithm
176///
177/// τ_b = (C - D) / √[(n₀ - n₁)(n₀ - n₂)]
178///
179/// where C = concordant pairs, D = discordant pairs,
180/// n₀ = n(n-1)/2, n₁ = Σ tᵢ(tᵢ-1)/2 (ties in x), n₂ = Σ uⱼ(uⱼ-1)/2 (ties in y).
181///
182/// # p-value
183///
184/// With no tie in either variable and at most 33 observations -- or more, when
185/// at most one pair is on the minority side -- the two-sided p-value is exact
186/// (Kendall 1970): twice the probability that a random permutation has no more
187/// discordant pairs than observed. Otherwise it is the normal approximation of
188/// S = C − D with the tie terms of the variance. These are the cases in which
189/// SciPy's `kendalltau(method="auto")` is exact too.
190///
191/// # Complexity
192///
193/// O(n²) naive enumeration. For n > 10,000 consider O(n log n) Knight's
194/// algorithm (not implemented — sufficient for u-insight's typical data sizes).
195/// The exact p-value costs O(n · n²) once, for n ≤ 33.
196///
197/// # Returns
198///
199/// `None` if fewer than 3 observations, slices differ in length, or inputs
200/// contain non-finite values.
201///
202/// # References
203///
204/// Kendall (1938). "A new measure of rank correlation".
205/// Biometrika, 30(1/2), 81–93.
206///
207/// Kendall (1970). *Rank Correlation Methods*, 4th ed. Griffin.
208///
209/// # Examples
210///
211/// ```
212/// use u_analytics::correlation::kendall_tau_b;
213///
214/// let x = [1.0, 2.0, 3.0, 4.0, 5.0];
215/// let y = [1.0, 2.0, 3.0, 4.0, 5.0];
216/// let result = kendall_tau_b(&x, &y).unwrap();
217/// assert!((result.r - 1.0).abs() < 1e-10);
218/// ```
219pub fn kendall_tau_b(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
220    let n = x.len();
221    if n < 3 || n != y.len() {
222        return None;
223    }
224
225    if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
226        return None;
227    }
228
229    let mut concordant: i64 = 0;
230    let mut discordant: i64 = 0;
231    let mut ties_x: i64 = 0;
232    let mut ties_y: i64 = 0;
233    let mut _ties_xy: i64 = 0;
234
235    for i in 0..n {
236        for j in (i + 1)..n {
237            let dx = x[i] - x[j];
238            let dy = y[i] - y[j];
239            let product = dx * dy;
240
241            if dx == 0.0 && dy == 0.0 {
242                _ties_xy += 1;
243                ties_x += 1;
244                ties_y += 1;
245            } else if dx == 0.0 {
246                ties_x += 1;
247            } else if dy == 0.0 {
248                ties_y += 1;
249            } else if product > 0.0 {
250                concordant += 1;
251            } else {
252                discordant += 1;
253            }
254        }
255    }
256
257    let n0 = (n as i64) * (n as i64 - 1) / 2;
258    let denom_sq = (n0 - ties_x) as f64 * (n0 - ties_y) as f64;
259
260    if denom_sq <= 0.0 {
261        return None; // all values tied in x or y
262    }
263
264    let tau = (concordant - discordant) as f64 / denom_sq.sqrt();
265    let tau = tau.clamp(-1.0, 1.0);
266
267    let p_value = if ties_x == 0
268        && ties_y == 0
269        && (n <= KENDALL_EXACT_MAX_N || concordant.min(discordant) <= 1)
270    {
271        // Nothing tied: the null distribution of the discordant pairs is the
272        // inversion count of a random permutation, tabulated exactly.
273        kendall_exact_p(n, discordant)
274    } else {
275        // Normal approximation of S = C - D with the variance of Kendall
276        // (1970) for tied data: the tie-free term less the tied groups, plus
277        // the two terms the groups of x and of y share.
278        let m = n as f64 * (n as f64 - 1.0);
279        let (pairs_x, triples_x, weighted_x) = tie_sums(x);
280        let (pairs_y, triples_y, weighted_y) = tie_sums(y);
281        let var_s = (m * (2.0 * n as f64 + 5.0) - weighted_x - weighted_y) / 18.0
282            + 2.0 * pairs_x * pairs_y / m
283            + triples_x * triples_y / (9.0 * m * (n as f64 - 2.0));
284
285        if var_s > 0.0 {
286            let s = (concordant - discordant) as f64;
287            let z = s / var_s.sqrt();
288            2.0 * special::standard_normal_sf(z.abs())
289        } else {
290            1.0
291        }
292    };
293
294    Some(CorrelationResult { r: tau, p_value, n })
295}
296
297/// The largest `n` whose permutations can be counted in `u128` (33! < 2^128):
298/// SciPy's `kendalltau(method="auto")` takes the exact distribution up to the
299/// same size.
300const KENDALL_EXACT_MAX_N: usize = 33;
301
302/// The exact two-sided p-value of Kendall's S for `n` observations with no
303/// ties, `discordant` of the n(n-1)/2 pairs discordant: twice the probability
304/// that a random permutation has at most `min(D, total - D)` inversions
305/// (Kendall 1970, ch. 5), capped at 1.
306///
307/// The counts of permutations by inversions are built row by row
308/// (Mahonian numbers: adding the j-th element adds 0..j-1 inversions) in
309/// integer arithmetic, so the probability is one correctly rounded division.
310/// The two smallest tails have closed forms -- 2/n! and 2/(n-1)! -- that hold
311/// for any `n`; beyond `KENDALL_EXACT_MAX_N` only those are reached.
312fn kendall_exact_p(n: usize, discordant: i64) -> f64 {
313    let total = (n * (n - 1) / 2) as i64;
314    let tail = discordant.min(total - discordant);
315    if 4 * tail == (n * (n - 1)) as i64 {
316        return 1.0; // the middle of a symmetric distribution
317    }
318    // 2 / m!, with m! left to underflow rather than overflow.
319    let two_over_factorial = |m: usize| (2..=m).fold(2.0_f64, |p, k| p / k as f64);
320    match tail {
321        0 => return two_over_factorial(n),
322        1 => return two_over_factorial(n - 1),
323        _ => {}
324    }
325    debug_assert!(n <= KENDALL_EXACT_MAX_N, "counts would overflow u128");
326    let c = tail as usize;
327    // counts[k]: the permutations of the first j elements with k inversions.
328    let mut counts = vec![0u128; c + 1];
329    counts[0] = 1;
330    for j in 2..=n {
331        // new[k] = old[k] + ... + old[k - (j - 1)], from running sums.
332        let mut next = vec![0u128; c + 1];
333        let mut window = 0u128;
334        for k in 0..=c {
335            window += counts[k];
336            if k >= j {
337                window -= counts[k - j];
338            }
339            next[k] = window;
340        }
341        counts = next;
342    }
343    let at_most: u128 = counts.iter().sum();
344    let permutations: u128 = (2..=n as u128).product();
345    (2.0 * at_most as f64 / permutations as f64).min(1.0)
346}
347
348// ---------------------------------------------------------------------------
349// Fisher z-transformation
350// ---------------------------------------------------------------------------
351
352/// Computes Fisher z-transformation: z = arctanh(r).
353///
354/// Transforms r ∈ (-1, 1) to z ∈ (-∞, +∞) where z is approximately normal
355/// with variance 1/(n-3).
356///
357/// # Returns
358///
359/// `None` if r is not in (-1, 1).
360///
361/// # References
362///
363/// Fisher (1921). "On the probable error of a coefficient of correlation".
364/// Metron, 1, 3–32.
365pub fn fisher_z(r: f64) -> Option<f64> {
366    if r <= -1.0 || r >= 1.0 || !r.is_finite() {
367        return None;
368    }
369    Some(r.atanh())
370}
371
372/// Inverse Fisher z-transformation: r = tanh(z).
373pub fn fisher_z_inv(z: f64) -> f64 {
374    z.tanh()
375}
376
377/// Computes confidence interval for a Pearson correlation coefficient
378/// using Fisher z-transformation.
379///
380/// # Arguments
381///
382/// * `r` — Pearson correlation coefficient
383/// * `n` — Sample size
384/// * `confidence` — Confidence level (e.g. 0.95)
385///
386/// # Returns
387///
388/// `None` if n < 4, r is not in (-1, 1), or confidence is not in (0, 1).
389///
390/// # Examples
391///
392/// ```
393/// use u_analytics::correlation::correlation_ci;
394///
395/// let ci = correlation_ci(0.8, 30, 0.95).unwrap();
396/// assert!(ci.lower < 0.8);
397/// assert!(ci.upper > 0.8);
398/// assert!(ci.lower > 0.0);
399/// assert!(ci.upper < 1.0);
400/// ```
401pub fn correlation_ci(r: f64, n: usize, confidence: f64) -> Option<CorrelationCI> {
402    if n < 4 || r <= -1.0 || r >= 1.0 || !r.is_finite() {
403        return None;
404    }
405    if confidence <= 0.0 || confidence >= 1.0 {
406        return None;
407    }
408
409    let z = r.atanh();
410    let se = 1.0 / ((n as f64 - 3.0).sqrt());
411    let alpha = 1.0 - confidence;
412    let z_crit = special::inverse_normal_cdf(1.0 - alpha / 2.0);
413
414    let z_lower = z - z_crit * se;
415    let z_upper = z + z_crit * se;
416
417    Some(CorrelationCI {
418        lower: z_lower.tanh(),
419        upper: z_upper.tanh(),
420        confidence,
421    })
422}
423
424// ---------------------------------------------------------------------------
425// Correlation Matrix
426// ---------------------------------------------------------------------------
427
428/// Computes a pairwise Pearson correlation matrix.
429///
430/// # Arguments
431///
432/// * `variables` — Slice of variable data. Each inner slice is one variable's
433///   observations. All must have the same length.
434///
435/// # Returns
436///
437/// A symmetric n×n `Matrix` where entry (i,j) is the Pearson r between
438/// variables i and j. Diagonal is 1.0. Returns `None` if fewer than 2
439/// variables, observations < 3, or variable lengths differ.
440///
441/// # Examples
442///
443/// ```
444/// use u_analytics::correlation::correlation_matrix;
445///
446/// let x = [1.0, 2.0, 3.0, 4.0, 5.0];
447/// let y = [2.0, 4.0, 6.0, 8.0, 10.0];
448/// let z = [5.0, 4.0, 3.0, 2.0, 1.0];
449/// let mat = correlation_matrix(&[&x, &y, &z]).unwrap();
450/// assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);   // x,y perfectly correlated
451/// assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);   // x,z perfectly anti-correlated
452/// ```
453pub fn correlation_matrix(variables: &[&[f64]]) -> Option<Matrix> {
454    let p = variables.len();
455    if p < 2 {
456        return None;
457    }
458    let n = variables[0].len();
459    if n < 3 {
460        return None;
461    }
462    for v in variables {
463        if v.len() != n {
464            return None;
465        }
466    }
467
468    // Pre-compute means and std devs
469    let mut sds = Vec::with_capacity(p);
470    for v in variables {
471        let m = stats::mean(v)?;
472        if !m.is_finite() {
473            return None;
474        }
475        let s = stats::std_dev(v)?;
476        if !s.is_finite() || s < 1e-300 {
477            return None;
478        }
479        sds.push(s);
480    }
481
482    let mut data = vec![0.0; p * p];
483
484    for i in 0..p {
485        data[i * p + i] = 1.0; // diagonal
486        for j in (i + 1)..p {
487            let cov = stats::covariance(variables[i], variables[j])?;
488            let r = (cov / (sds[i] * sds[j])).clamp(-1.0, 1.0);
489            data[i * p + j] = r;
490            data[j * p + i] = r;
491        }
492    }
493
494    Matrix::new(p, p, data).ok()
495}
496
497/// Computes a pairwise Spearman rank correlation matrix.
498///
499/// Same as [`correlation_matrix`] but uses rank-transformed data.
500pub fn spearman_matrix(variables: &[&[f64]]) -> Option<Matrix> {
501    let p = variables.len();
502    if p < 2 {
503        return None;
504    }
505    let n = variables[0].len();
506    if n < 3 {
507        return None;
508    }
509    for v in variables {
510        if v.len() != n || v.iter().any(|x| !x.is_finite()) {
511            return None;
512        }
513    }
514
515    // Rank-transform all variables
516    let ranked: Vec<Vec<f64>> = variables.iter().map(|v| rank_data(v)).collect();
517    let ranked_refs: Vec<&[f64]> = ranked.iter().map(|v| v.as_slice()).collect();
518
519    correlation_matrix(&ranked_refs)
520}
521
522/// Computes a Kendall tau-b correlation matrix for the given variables.
523///
524/// Each off-diagonal entry is the result of [`kendall_tau_b`] for the
525/// corresponding pair of variables; diagonal entries are 1.0.
526///
527/// # Returns
528///
529/// `None` if there are fewer than 2 variables, fewer than 3 observations,
530/// any variable has differing length, contains a non-finite value, or any
531/// pairwise computation returns `None`.
532///
533/// # References
534///
535/// Kendall (1938). "A new measure of rank correlation".
536/// Knight (1966). "A computer method for calculating Kendall's tau with
537/// ungrouped data" (used internally by [`kendall_tau_b`] for O(n log n)).
538///
539/// # Examples
540///
541/// ```
542/// use u_analytics::correlation::kendall_matrix;
543///
544/// let x = [1.0, 2.0, 3.0, 4.0, 5.0];
545/// let y = [2.0, 4.0, 6.0, 8.0, 10.0];      // perfectly monotonic with x
546/// let z = [5.0, 4.0, 3.0, 2.0, 1.0];        // perfectly anti-monotonic with x
547/// let mat = kendall_matrix(&[&x, &y, &z]).unwrap();
548/// assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
549/// assert!((mat.get(0, 2) - (-1.0)).abs() < 1e-10);
550/// ```
551pub fn kendall_matrix(variables: &[&[f64]]) -> Option<Matrix> {
552    let p = variables.len();
553    if p < 2 {
554        return None;
555    }
556    let n = variables[0].len();
557    if n < 3 {
558        return None;
559    }
560    for v in variables {
561        if v.len() != n || v.iter().any(|x| !x.is_finite()) {
562            return None;
563        }
564    }
565
566    let mut data = vec![0.0_f64; p * p];
567    for i in 0..p {
568        data[i * p + i] = 1.0;
569        for j in (i + 1)..p {
570            let r = kendall_tau_b(variables[i], variables[j])?.r;
571            data[i * p + j] = r;
572            data[j * p + i] = r;
573        }
574    }
575    Matrix::new(p, p, data).ok()
576}
577
578// ---------------------------------------------------------------------------
579// Internal helpers
580// ---------------------------------------------------------------------------
581
582/// Computes two-tailed p-value for a correlation coefficient via t-test.
583///
584/// t = r·√(n-2) / √(1-r²), df = n-2.
585fn correlation_p_value(r: f64, n: usize) -> f64 {
586    if n < 3 {
587        return 1.0;
588    }
589    let df = (n - 2) as f64;
590    let r2 = r * r;
591
592    // Handle r ≈ ±1.0 (denominator → 0)
593    if r2 >= 1.0 - 1e-15 {
594        return 0.0; // perfect correlation
595    }
596
597    let t = r * (df / (1.0 - r2)).sqrt();
598    2.0 * special::t_distribution_sf(t.abs(), df)
599}
600
601/// Ranks data using the mid-rank method for ties.
602///
603/// Returns a Vec of ranks (1-based). Tied values receive the average rank.
604fn rank_data(data: &[f64]) -> Vec<f64> {
605    let n = data.len();
606    let mut indexed: Vec<(usize, f64)> = data.iter().copied().enumerate().collect();
607    indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
608
609    let mut ranks = vec![0.0; n];
610    let mut i = 0;
611    while i < n {
612        let mut j = i;
613        // Find all tied elements
614        while j < n && (indexed[j].1 - indexed[i].1).abs() < 1e-300 {
615            j += 1;
616        }
617        // Average rank for the tied group (1-based)
618        let avg_rank = (i + j) as f64 / 2.0 + 0.5;
619        for item in indexed.iter().take(j).skip(i) {
620            ranks[item.0] = avg_rank;
621        }
622        i = j;
623    }
624
625    ranks
626}
627
628/// The sums over the groups of equal values (sizes `t`) that the variance of
629/// Kendall's S needs: Σ t(t-1)/2, Σ t(t-1)(t-2) and Σ t(t-1)(2t+5).
630fn tie_sums(data: &[f64]) -> (f64, f64, f64) {
631    let mut sorted: Vec<f64> = data.to_vec();
632    sorted.sort_by(f64::total_cmp);
633
634    let (mut pairs, mut triples, mut weighted) = (0.0, 0.0, 0.0);
635    let mut i = 0;
636    while i < sorted.len() {
637        let mut j = i;
638        while j < sorted.len() && sorted[j] == sorted[i] {
639            j += 1;
640        }
641        let t = (j - i) as f64;
642        pairs += t * (t - 1.0) / 2.0;
643        triples += t * (t - 1.0) * (t - 2.0);
644        weighted += t * (t - 1.0) * (2.0 * t + 5.0);
645        i = j;
646    }
647    (pairs, triples, weighted)
648}
649
650// ---------------------------------------------------------------------------
651// Autocorrelation (ACF) and Partial Autocorrelation (PACF)
652// ---------------------------------------------------------------------------
653
654/// Result of autocorrelation analysis.
655#[derive(Debug, Clone)]
656pub struct AcfResult {
657    /// Autocorrelation values for lags 0, 1, ..., max_lag.
658    /// `acf[0]` is always 1.0.
659    pub acf: Vec<f64>,
660    /// 95% white-noise threshold = z₀.₉₇₅ / √n: under the hypothesis that the
661    /// series is white noise, |acf\[k\]| above it (k > 0) is significant.
662    pub confidence_threshold: f64,
663    /// 95% limits for lags 1, ..., max_lag by Bartlett's formula,
664    /// z₀.₉₇₅ · √((1 + 2 Σⱼ₌₁ᵏ⁻¹ r(j)²) / n): for lag k, under the hypothesis
665    /// that the autocorrelation is zero beyond lag k − 1 (an MA(k − 1)
666    /// process). The bands an ACF plot draws to choose an MA order.
667    pub bartlett_limits: Vec<f64>,
668}
669
670/// Result of partial autocorrelation analysis.
671#[derive(Debug, Clone)]
672pub struct PacfResult {
673    /// Partial autocorrelation values for lags 1, ..., max_lag.
674    pub pacf: Vec<f64>,
675    /// 95% threshold = z₀.₉₇₅ / √n (Quenouille): for lag k, under the
676    /// hypothesis of an AR(k − 1) process, |pacf| above it is significant.
677    pub confidence_threshold: f64,
678}
679
680/// z₀.₉₇₅, the two-sided 95 % standard normal point.
681const Z_975: f64 = 1.959_963_984_540_054;
682
683/// The fewest values [`acf`] takes: one lag needs a pair.
684pub const ACF_MIN_OBSERVATIONS: usize = 2;
685
686/// The fewest values [`pacf`] takes: lags below `n / 2` leave lag 1 from 4 on.
687pub const PACF_MIN_OBSERVATIONS: usize = 4;
688
689/// The largest lag of an autocorrelation request: `max_lag` within
690/// `domain`, or, when it is `None`, `min(⌊10·log₁₀ n⌋, highest)`
691/// (statsmodels' default), never below 1.
692fn lag_choice(
693    max_lag: Option<usize>,
694    n: usize,
695    domain: RangeInclusive<usize>,
696) -> Result<usize, TimeSeriesError> {
697    match max_lag {
698        None => Ok((((n as f64).log10() * 10.0).floor() as usize).clamp(1, *domain.end())),
699        Some(k) => check_option("max_lag", k, &domain),
700    }
701}
702
703/// The values [`acf`] takes for `max_lag` on `data`: `1..=n − 1`.
704///
705/// # Errors
706///
707/// The series' own refusals, as [`acf`] gives them: a non-finite value or
708/// fewer than 2 values.
709///
710/// # Examples
711///
712/// ```
713/// use u_analytics::correlation::acf_max_lag_domain;
714///
715/// assert_eq!(acf_max_lag_domain(&[1.0, 2.0, 4.0, 3.0]).unwrap(), 1..=3);
716/// ```
717pub fn acf_max_lag_domain(data: &[f64]) -> Result<RangeInclusive<usize>, TimeSeriesError> {
718    check_series(data, ACF_MIN_OBSERVATIONS)?;
719    Ok(1..=data.len() - 1)
720}
721
722/// The values [`pacf`] takes for `max_lag` on `data`: `1..=n / 2 − 1`.
723///
724/// # Errors
725///
726/// The series' own refusals, as [`pacf`] gives them: a non-finite value or
727/// fewer than 4 values.
728pub fn pacf_max_lag_domain(data: &[f64]) -> Result<RangeInclusive<usize>, TimeSeriesError> {
729    check_series(data, PACF_MIN_OBSERVATIONS)?;
730    Ok(1..=data.len() / 2 - 1)
731}
732
733/// Compute the sample autocorrelation function (ACF).
734///
735/// Uses the biased estimator (denominator N, not N-k) which guarantees a
736/// positive-semidefinite autocovariance matrix.
737///
738/// # Arguments
739///
740/// * `data` — time series observations (at least 2, all finite).
741/// * `max_lag` — the largest lag, in `[1, n − 1]`; `None` for statsmodels'
742///   default `min(⌊10·log₁₀ n⌋, n − 1)`.
743///
744/// # Errors
745///
746/// [`TimeSeriesError`]: a non-finite value, fewer than 2 values, `max_lag`
747/// outside `[1, n − 1]`, or a series that does not vary.
748///
749/// # References
750///
751/// - Box & Jenkins (1976). *Time Series Analysis: Forecasting and Control*.
752///
753/// # Examples
754///
755/// ```
756/// use u_analytics::correlation::acf;
757///
758/// let data = [1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0, 2.0];
759/// let r = acf(&data, Some(5)).unwrap();
760/// assert!((r.acf[0] - 1.0).abs() < 1e-10); // lag 0 is always 1
761/// assert!(r.acf.len() == 6); // lags 0..=5
762/// ```
763pub fn acf(data: &[f64], max_lag: Option<usize>) -> Result<AcfResult, TimeSeriesError> {
764    let domain = acf_max_lag_domain(data)?;
765    let n = data.len();
766    let max_lag = lag_choice(max_lag, n, domain)?;
767    let nf = n as f64;
768
769    let mean = data.iter().sum::<f64>() / nf;
770
771    // Lag-0 autocovariance (biased variance)
772    let c0: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
773
774    // A constant series: no variation beyond the rounding of its mean, judged
775    // against the series' own size (a fixed floor would misjudge small units).
776    let mean_square: f64 = data.iter().map(|&x| x * x).sum::<f64>() / nf;
777    if c0 <= 1e-20 * mean_square {
778        return Err(TimeSeriesError::NoVariation);
779    }
780
781    let mut acf_vals = Vec::with_capacity(max_lag + 1);
782    acf_vals.push(1.0); // r(0) = 1
783
784    for lag in 1..=max_lag {
785        let ck: f64 = data[..n - lag]
786            .iter()
787            .zip(&data[lag..])
788            .map(|(&xt, &xt_h)| (xt - mean) * (xt_h - mean))
789            .sum::<f64>()
790            / nf;
791        acf_vals.push(ck / c0);
792    }
793
794    let threshold = Z_975 / nf.sqrt();
795    let mut cumulative = 0.0;
796    let bartlett_limits = acf_vals[1..]
797        .iter()
798        .map(|r| {
799            let limit = Z_975 * ((1.0 + 2.0 * cumulative) / nf).sqrt();
800            cumulative += r * r;
801            limit
802        })
803        .collect();
804
805    Ok(AcfResult {
806        acf: acf_vals,
807        confidence_threshold: threshold,
808        bartlett_limits,
809    })
810}
811
812/// Compute the sample partial autocorrelation function (PACF) via
813/// Durbin-Levinson recursion on the biased sample ACF (statsmodels'
814/// `pacf(method="ldb")`).
815///
816/// PACF at lag *h* measures the correlation between xₜ and xₜ₊ₕ after
817/// removing the linear effect of intermediate lags.
818///
819/// # Arguments
820///
821/// * `data` — time series observations (at least 4, all finite).
822/// * `max_lag` — the largest lag, in `[1, n / 2 − 1]` (statsmodels' limit:
823///   beyond `n / 2` the recursion runs on autocorrelations from too few
824///   pairs); `None` for `min(⌊10·log₁₀ n⌋, n / 2 − 1)`.
825///
826/// # Errors
827///
828/// [`TimeSeriesError`]: a non-finite value, fewer than 4 values, `max_lag`
829/// outside `[1, n / 2 − 1]`, or a series that does not vary.
830///
831/// # References
832///
833/// - Durbin (1960). "The fitting of time-series models". Revue de l'Institut
834///   International de Statistique, 28(3), 233–244.
835///
836/// # Examples
837///
838/// ```
839/// use u_analytics::correlation::pacf;
840///
841/// let data = [1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0, 6.0, 5.0, 7.0];
842/// let r = pacf(&data, Some(4)).unwrap();
843/// assert!(r.pacf.len() == 4); // lags 1..=4
844/// // Lag 1 PACF equals lag 1 ACF
845/// ```
846pub fn pacf(data: &[f64], max_lag: Option<usize>) -> Result<PacfResult, TimeSeriesError> {
847    let domain = pacf_max_lag_domain(data)?;
848    let n = data.len();
849    let max_lag = lag_choice(max_lag, n, domain)?;
850
851    // The autocorrelations it recurses on (which refuse a series that does
852    // not vary).
853    let acf_result = acf(data, Some(max_lag))?;
854    let rho = &acf_result.acf;
855
856    let mut pacf_vals = Vec::with_capacity(max_lag);
857
858    // φ₁₁ = ρ(1)
859    pacf_vals.push(rho[1]);
860
861    if max_lag == 1 {
862        return Ok(PacfResult {
863            pacf: pacf_vals,
864            confidence_threshold: Z_975 / (n as f64).sqrt(),
865        });
866    }
867
868    // Durbin-Levinson recursion
869    let mut phi_prev = vec![rho[1]];
870
871    for h in 2..=max_lag {
872        // Numerator: ρ(h) - Σⱼ₌₁ᴴ⁻¹ φ_{h-1,j} × ρ(h-j)
873        let mut num = rho[h];
874        for j in 0..h - 1 {
875            num -= phi_prev[j] * rho[h - 1 - j];
876        }
877
878        // Denominator: 1 - Σⱼ₌₁ᴴ⁻¹ φ_{h-1,j} × ρ(j+1)
879        let mut den = 1.0;
880        for j in 0..h - 1 {
881            den -= phi_prev[j] * rho[j + 1];
882        }
883
884        let phi_hh = if den.abs() > 1e-14 { num / den } else { 0.0 };
885
886        // Update: φₕⱼ = φ_{h-1,j} - φₕₕ × φ_{h-1,h-1-j}
887        let mut phi_new = Vec::with_capacity(h);
888        for j in 0..h - 1 {
889            phi_new.push(phi_prev[j] - phi_hh * phi_prev[h - 2 - j]);
890        }
891        phi_new.push(phi_hh);
892
893        pacf_vals.push(phi_hh);
894        phi_prev = phi_new;
895    }
896
897    Ok(PacfResult {
898        pacf: pacf_vals,
899        confidence_threshold: Z_975 / (n as f64).sqrt(),
900    })
901}
902
903/// ACF (with Bartlett's limits) and PACF against statsmodels 0.15.0
904/// (`acf(x, nlags=8, fft=False, alpha=0.05)`, `pacf(x, nlags=8, method="ldb")`).
905#[cfg(test)]
906mod statsmodels_reference {
907    use super::*;
908
909    const X: [f64; 24] = [
910        2.3, 3.1, 4.0, 3.6, 2.9, 2.2, 2.8, 3.9, 4.6, 4.1, 3.3, 2.5, 2.7, 3.8, 4.9, 4.4, 3.5, 2.6,
911        3.0, 4.2, 5.1, 4.5, 3.4, 2.9,
912    ];
913
914    fn close(got: &[f64], want: &[f64], what: &str) {
915        assert_eq!(got.len(), want.len(), "{what}");
916        for (k, (g, w)) in got.iter().zip(want).enumerate() {
917            assert!((g - w).abs() < 1e-12, "{what}[{k}]: {g} vs {w}");
918        }
919    }
920
921    #[test]
922    fn acf_and_bartlett_limits_reproduce_statsmodels() {
923        let r = acf(&X, Some(8)).unwrap();
924        close(
925            &r.acf,
926            &[
927                1.0,
928                0.486403576681892,
929                -0.36186808082372074,
930                -0.6951594797553611,
931                -0.2634125570953007,
932                0.42164395757528844,
933                0.6593055663079663,
934                0.26106100487729356,
935                -0.3171014941549895,
936            ],
937            "acf",
938        );
939        close(
940            &r.bartlett_limits,
941            &[
942                0.40007597302960923,
943                0.48559020426940086,
944                0.5269888881332575,
945                0.6575826795727397,
946                0.6742603293351266,
947                0.7152198020271394,
948                0.80665422242141,
949                0.8200659919957791,
950            ],
951            "bartlett",
952        );
953        assert!((r.confidence_threshold - 0.40007597302960923).abs() < 1e-12);
954    }
955
956    #[test]
957    fn pacf_reproduces_statsmodels_ldb() {
958        let r = pacf(&X, Some(8)).unwrap();
959        close(
960            &r.pacf,
961            &[
962                0.486403576681892,
963                -0.7839238375815395,
964                0.00047345408884359627,
965                0.1902495235598503,
966                0.19973005383776718,
967                0.1225767860916582,
968                0.05567632845800997,
969                0.03539504608148146,
970            ],
971            "pacf",
972        );
973    }
974
975    #[test]
976    fn a_small_unit_is_not_a_constant_series() {
977        let small: Vec<f64> = X.iter().map(|v| v * 1e-150).collect();
978        let r = acf(&small, Some(8)).unwrap();
979        assert!((r.acf[1] - 0.486403576681892).abs() < 1e-12);
980        assert_eq!(
981            acf(&[3.0; 10], Some(3)).unwrap_err(),
982            TimeSeriesError::NoVariation
983        );
984    }
985}
986
987#[cfg(test)]
988mod tests {
989    use super::*;
990
991    // -----------------------------------------------------------------------
992    // Pearson tests
993    // -----------------------------------------------------------------------
994
995    #[test]
996    fn pearson_perfect_positive() {
997        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
998        let y = [2.0, 4.0, 6.0, 8.0, 10.0];
999        let result = pearson(&x, &y).expect("should compute");
1000        assert!((result.r - 1.0).abs() < 1e-10);
1001        assert!(result.p_value < 1e-10);
1002    }
1003
1004    #[test]
1005    fn pearson_perfect_negative() {
1006        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1007        let y = [10.0, 8.0, 6.0, 4.0, 2.0];
1008        let result = pearson(&x, &y).expect("should compute");
1009        assert!((result.r + 1.0).abs() < 1e-10);
1010        assert!(result.p_value < 1e-10);
1011    }
1012
1013    #[test]
1014    fn pearson_uncorrelated() {
1015        // Designed to have r = 0
1016        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1017        let y = [3.0, 1.0, 5.0, 1.0, 5.0];
1018        let result = pearson(&x, &y).expect("should compute");
1019        assert!(result.r.abs() < 0.5);
1020    }
1021
1022    #[test]
1023    fn pearson_known_value() {
1024        // Height (inches) vs GPA example
1025        let x = [68.0, 71.0, 62.0, 75.0, 58.0, 60.0, 67.0, 68.0, 71.0, 69.0];
1026        let y = [4.1, 4.6, 3.8, 4.4, 3.2, 3.1, 3.8, 4.1, 4.3, 3.7];
1027        let result = pearson(&x, &y).expect("should compute");
1028        // Computed: r ≈ 0.8816
1029        assert!((result.r - 0.8816).abs() < 0.01, "r = {}", result.r);
1030    }
1031
1032    /// Verifies exact Pearson r numeric reference from the spec.
1033    ///
1034    /// x = [1,2,3,4,5], y = [2,4,5,4,5]
1035    /// Σ(xᵢ-x̄)(yᵢ-ȳ) = 6
1036    /// Σ(xᵢ-x̄)² = 10, Σ(yᵢ-ȳ)² = 6
1037    /// r = 6 / √(10·6) = 6/√60 = 6/7.7460 ≈ 0.7746
1038    ///
1039    /// Reference: Pearson (1895), Proc. Royal Society London, 58, 240–242.
1040    #[test]
1041    fn pearson_numeric_reference() {
1042        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1043        let y = [2.0, 4.0, 5.0, 4.0, 5.0];
1044        let result = pearson(&x, &y).expect("should compute");
1045
1046        let expected_r = 6.0_f64 / (60.0_f64).sqrt(); // = 0.774596...
1047        assert!(
1048            (result.r - expected_r).abs() < 1e-3,
1049            "r expected {:.6}, got {}",
1050            expected_r,
1051            result.r
1052        );
1053        assert!(
1054            (result.r - 0.7746).abs() < 1e-3,
1055            "r expected ≈0.7746, got {}",
1056            result.r
1057        );
1058    }
1059
1060    #[test]
1061    fn pearson_insufficient_data() {
1062        assert!(pearson(&[1.0, 2.0], &[3.0, 4.0]).is_none());
1063        assert!(pearson(&[1.0], &[2.0]).is_none());
1064    }
1065
1066    #[test]
1067    fn pearson_length_mismatch() {
1068        assert!(pearson(&[1.0, 2.0, 3.0], &[4.0, 5.0]).is_none());
1069    }
1070
1071    #[test]
1072    fn pearson_zero_variance() {
1073        assert!(pearson(&[5.0, 5.0, 5.0], &[1.0, 2.0, 3.0]).is_none());
1074    }
1075
1076    #[test]
1077    fn pearson_nan_input() {
1078        assert!(pearson(&[1.0, f64::NAN, 3.0], &[4.0, 5.0, 6.0]).is_none());
1079    }
1080
1081    // -----------------------------------------------------------------------
1082    // Spearman tests
1083    // -----------------------------------------------------------------------
1084
1085    #[test]
1086    fn spearman_perfect_monotone() {
1087        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1088        let y = [1.0, 4.0, 9.0, 16.0, 25.0]; // monotone increasing
1089        let result = spearman(&x, &y).expect("should compute");
1090        assert!((result.r - 1.0).abs() < 1e-10);
1091    }
1092
1093    #[test]
1094    fn spearman_perfect_negative() {
1095        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1096        let y = [5.0, 4.0, 3.0, 2.0, 1.0];
1097        let result = spearman(&x, &y).expect("should compute");
1098        assert!((result.r + 1.0).abs() < 1e-10);
1099    }
1100
1101    #[test]
1102    fn spearman_with_ties() {
1103        let x = [1.0, 2.0, 2.0, 4.0, 5.0];
1104        let y = [1.0, 3.0, 3.0, 4.0, 5.0];
1105        let result = spearman(&x, &y).expect("should compute");
1106        assert!(result.r > 0.9); // strong positive
1107    }
1108
1109    #[test]
1110    fn spearman_nonlinear_monotone() {
1111        // Spearman should be 1.0 for any monotone function
1112        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1113        let y: Vec<f64> = x.iter().map(|&v: &f64| v.powi(3)).collect();
1114        let result = spearman(&x, &y).expect("should compute");
1115        assert!((result.r - 1.0).abs() < 1e-10);
1116    }
1117
1118    // -----------------------------------------------------------------------
1119    // Kendall tests
1120    // -----------------------------------------------------------------------
1121
1122    #[test]
1123    fn kendall_perfect_concordance() {
1124        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1125        let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1126        let result = kendall_tau_b(&x, &y).expect("should compute");
1127        assert!((result.r - 1.0).abs() < 1e-10);
1128    }
1129
1130    #[test]
1131    fn kendall_perfect_discordance() {
1132        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1133        let y = [5.0, 4.0, 3.0, 2.0, 1.0];
1134        let result = kendall_tau_b(&x, &y).expect("should compute");
1135        assert!((result.r + 1.0).abs() < 1e-10);
1136    }
1137
1138    #[test]
1139    fn kendall_known_value() {
1140        // Known example: tau ≈ 0.733
1141        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1142        let y = [3.0, 4.0, 1.0, 2.0, 5.0];
1143        let result = kendall_tau_b(&x, &y).expect("should compute");
1144        assert!((result.r - 0.2).abs() < 0.01);
1145    }
1146
1147    #[test]
1148    fn kendall_with_ties() {
1149        let x = [1.0, 2.0, 2.0, 4.0, 5.0];
1150        let y = [1.0, 3.0, 3.0, 4.0, 5.0];
1151        let result = kendall_tau_b(&x, &y).expect("should compute");
1152        assert!(result.r > 0.8);
1153    }
1154
1155    #[test]
1156    fn kendall_all_ties() {
1157        let x = [1.0, 1.0, 1.0];
1158        let y = [2.0, 2.0, 2.0];
1159        assert!(kendall_tau_b(&x, &y).is_none()); // denom = 0
1160    }
1161
1162    // -----------------------------------------------------------------------
1163    // Fisher z tests
1164    // -----------------------------------------------------------------------
1165
1166    #[test]
1167    fn fisher_z_zero() {
1168        let z = fisher_z(0.0).expect("should compute");
1169        assert!(z.abs() < 1e-15);
1170    }
1171
1172    #[test]
1173    fn fisher_z_roundtrip() {
1174        for &r in &[0.0, 0.3, 0.5, 0.8, 0.95, -0.5, -0.95] {
1175            let z = fisher_z(r).expect("should compute");
1176            let r_back = fisher_z_inv(z);
1177            assert!((r - r_back).abs() < 1e-10, "Roundtrip failed for r={r}");
1178        }
1179    }
1180
1181    #[test]
1182    fn fisher_z_boundary() {
1183        assert!(fisher_z(1.0).is_none());
1184        assert!(fisher_z(-1.0).is_none());
1185        assert!(fisher_z(1.5).is_none());
1186        assert!(fisher_z(f64::NAN).is_none());
1187    }
1188
1189    // -----------------------------------------------------------------------
1190    // Confidence interval tests
1191    // -----------------------------------------------------------------------
1192
1193    #[test]
1194    fn ci_contains_r() {
1195        let ci = correlation_ci(0.6, 50, 0.95).expect("should compute");
1196        assert!(ci.lower < 0.6);
1197        assert!(ci.upper > 0.6);
1198        assert!(ci.lower > -1.0);
1199        assert!(ci.upper < 1.0);
1200    }
1201
1202    #[test]
1203    fn ci_wider_at_higher_confidence() {
1204        let ci_95 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1205        let ci_99 = correlation_ci(0.5, 30, 0.99).expect("should compute");
1206        assert!(ci_99.upper - ci_99.lower > ci_95.upper - ci_95.lower);
1207    }
1208
1209    #[test]
1210    fn ci_narrower_with_more_data() {
1211        let ci_30 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1212        let ci_100 = correlation_ci(0.5, 100, 0.95).expect("should compute");
1213        assert!(ci_100.upper - ci_100.lower < ci_30.upper - ci_30.lower);
1214    }
1215
1216    #[test]
1217    fn ci_edge_cases() {
1218        assert!(correlation_ci(0.5, 3, 0.95).is_none()); // n too small
1219        assert!(correlation_ci(1.0, 30, 0.95).is_none()); // r = 1
1220        assert!(correlation_ci(0.5, 30, 0.0).is_none()); // invalid confidence
1221        assert!(correlation_ci(0.5, 30, 1.0).is_none());
1222    }
1223
1224    // -----------------------------------------------------------------------
1225    // Correlation matrix tests
1226    // -----------------------------------------------------------------------
1227
1228    #[test]
1229    fn corr_matrix_identity() {
1230        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1231        let y = [2.0, 4.0, 6.0, 8.0, 10.0];
1232        let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1233        let mat = correlation_matrix(&[&x, &y, &z]).expect("should compute");
1234
1235        // Diagonal = 1
1236        assert!((mat.get(0, 0) - 1.0).abs() < 1e-10);
1237        assert!((mat.get(1, 1) - 1.0).abs() < 1e-10);
1238        assert!((mat.get(2, 2) - 1.0).abs() < 1e-10);
1239
1240        // x,y perfectly correlated
1241        assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1242
1243        // x,z perfectly anti-correlated
1244        assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1245
1246        // Symmetric
1247        assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1248        assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1249    }
1250
1251    #[test]
1252    fn corr_matrix_insufficient_variables() {
1253        let x = [1.0, 2.0, 3.0];
1254        assert!(correlation_matrix(&[&x]).is_none());
1255    }
1256
1257    #[test]
1258    fn corr_matrix_length_mismatch() {
1259        let x = [1.0, 2.0, 3.0];
1260        let y = [4.0, 5.0];
1261        assert!(correlation_matrix(&[&x, &y]).is_none());
1262    }
1263
1264    #[test]
1265    fn spearman_matrix_basic() {
1266        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1267        let y = [1.0, 4.0, 9.0, 16.0, 25.0]; // monotone
1268        let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1269        let mat = spearman_matrix(&[&x, &y, &z]).expect("should compute");
1270
1271        // x and y have perfect monotone relationship
1272        assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1273        // x and z are perfectly anti-monotone
1274        assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1275    }
1276
1277    #[test]
1278    fn kendall_matrix_basic() {
1279        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1280        let y = [2.0, 4.0, 6.0, 8.0, 10.0]; // perfectly monotone with x
1281        let z = [5.0, 4.0, 3.0, 2.0, 1.0]; // perfectly anti-monotone with x
1282        let mat = kendall_matrix(&[&x, &y, &z]).expect("should compute");
1283
1284        assert_eq!(mat.rows(), 3);
1285        assert!((mat.get(0, 0) - 1.0).abs() < 1e-15);
1286        assert!((mat.get(1, 1) - 1.0).abs() < 1e-15);
1287        assert!((mat.get(2, 2) - 1.0).abs() < 1e-15);
1288
1289        assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1290        assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1291
1292        // Symmetric
1293        assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1294        assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1295    }
1296
1297    #[test]
1298    fn kendall_matrix_insufficient_variables() {
1299        let x = [1.0, 2.0, 3.0];
1300        assert!(kendall_matrix(&[&x]).is_none());
1301    }
1302
1303    #[test]
1304    fn kendall_matrix_length_mismatch() {
1305        let x = [1.0, 2.0, 3.0, 4.0];
1306        let y = [4.0, 5.0];
1307        assert!(kendall_matrix(&[&x, &y]).is_none());
1308    }
1309
1310    #[test]
1311    fn kendall_matrix_with_ties() {
1312        let x = [1.0, 2.0, 2.0, 3.0, 4.0];
1313        let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1314        let mat = kendall_matrix(&[&x, &y]).expect("should compute");
1315        assert!(mat.get(0, 1) > 0.7);
1316    }
1317
1318    // -----------------------------------------------------------------------
1319    // Rank data tests
1320    // -----------------------------------------------------------------------
1321
1322    #[test]
1323    fn rank_no_ties() {
1324        let ranks = rank_data(&[3.0, 1.0, 2.0]);
1325        assert!((ranks[0] - 3.0).abs() < 1e-10); // 3.0 → rank 3
1326        assert!((ranks[1] - 1.0).abs() < 1e-10); // 1.0 → rank 1
1327        assert!((ranks[2] - 2.0).abs() < 1e-10); // 2.0 → rank 2
1328    }
1329
1330    #[test]
1331    fn rank_with_ties() {
1332        let ranks = rank_data(&[1.0, 2.0, 2.0, 4.0]);
1333        assert!((ranks[0] - 1.0).abs() < 1e-10);
1334        assert!((ranks[1] - 2.5).abs() < 1e-10); // average of ranks 2,3
1335        assert!((ranks[2] - 2.5).abs() < 1e-10);
1336        assert!((ranks[3] - 4.0).abs() < 1e-10);
1337    }
1338
1339    #[test]
1340    fn rank_all_same() {
1341        let ranks = rank_data(&[5.0, 5.0, 5.0]);
1342        assert!((ranks[0] - 2.0).abs() < 1e-10); // average of 1,2,3
1343        assert!((ranks[1] - 2.0).abs() < 1e-10);
1344        assert!((ranks[2] - 2.0).abs() < 1e-10);
1345    }
1346
1347    // -----------------------------------------------------------------------
1348    // P-value tests
1349    // -----------------------------------------------------------------------
1350
1351    #[test]
1352    fn pvalue_significant() {
1353        // Large sample, strong correlation → very small p-value
1354        let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
1355        let y: Vec<f64> = x.iter().map(|&v| v * 2.0 + 1.0).collect();
1356        let result = pearson(&x, &y).expect("should compute");
1357        assert!(result.p_value < 1e-10);
1358    }
1359
1360    #[test]
1361    fn pvalue_not_significant() {
1362        // Carefully constructed to have very low correlation
1363        let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1364        let y = [5.0, 1.0, 3.0, 5.0, 1.0];
1365        let result = pearson(&x, &y).expect("should compute");
1366        assert!(result.p_value > 0.3); // not significant
1367    }
1368
1369    // -----------------------------------------------------------------------
1370    // ACF tests
1371    // -----------------------------------------------------------------------
1372
1373    #[test]
1374    fn acf_lag_zero_is_one() {
1375        let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0];
1376        let r = acf(&data, Some(3)).expect("should compute");
1377        assert!((r.acf[0] - 1.0).abs() < 1e-10);
1378    }
1379
1380    #[test]
1381    fn acf_linear_trend() {
1382        // Linear trend → strong positive autocorrelation at low lags
1383        let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1384        let r = acf(&data, Some(5)).expect("should compute");
1385        assert!(r.acf[1] > 0.8, "lag-1 ACF for linear trend should be high");
1386        assert!(r.acf.len() == 6);
1387    }
1388
1389    #[test]
1390    fn acf_alternating() {
1391        // Alternating series → negative lag-1 autocorrelation
1392        let data: Vec<f64> = (0..30)
1393            .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
1394            .collect();
1395        let r = acf(&data, Some(5)).expect("should compute");
1396        assert!(r.acf[1] < -0.8, "alternating → negative lag-1 ACF");
1397        assert!(r.acf[2] > 0.8, "alternating → positive lag-2 ACF");
1398    }
1399
1400    #[test]
1401    fn acf_white_noise_threshold() {
1402        let data: Vec<f64> = (0..100).map(|i| ((i * 7 + 3) % 13) as f64).collect();
1403        let r = acf(&data, Some(10)).expect("should compute");
1404        // Threshold should be ~0.196 for n=100
1405        assert!((r.confidence_threshold - 1.96 / 10.0).abs() < 0.01);
1406    }
1407
1408    #[test]
1409    fn acf_edge_cases() {
1410        assert_eq!(
1411            acf(&[1.0], Some(3)).unwrap_err(),
1412            TimeSeriesError::TooFewObservations { needed: 2, got: 1 }
1413        );
1414        assert_eq!(
1415            acf(&[1.0, 2.0], Some(0)).unwrap_err(),
1416            TimeSeriesError::OptionOutOfRange {
1417                option: "max_lag",
1418                got: 0.0,
1419                min: 1.0,
1420                max: 1.0
1421            }
1422        );
1423        assert_eq!(
1424            acf(&[5.0, 5.0, 5.0, 5.0], Some(2)).unwrap_err(),
1425            TimeSeriesError::NoVariation
1426        );
1427        // A non-finite value is named before the length is judged.
1428        assert_eq!(
1429            acf(&[f64::NAN], Some(1)).unwrap_err(),
1430            TimeSeriesError::ValueNotFinite { index: 0 }
1431        );
1432    }
1433
1434    #[test]
1435    fn acf_max_lag_is_refused_past_n_minus_1_and_defaults_to_ten_log_n() {
1436        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1437        assert_eq!(
1438            acf(&data, Some(100)).unwrap_err(),
1439            TimeSeriesError::OptionOutOfRange {
1440                option: "max_lag",
1441                got: 100.0,
1442                min: 1.0,
1443                max: 4.0
1444            }
1445        );
1446        // min(floor(10 log10 5), 4) = 4.
1447        assert_eq!(acf(&data, None).expect("defined").acf.len(), 5);
1448        let long: Vec<f64> = (0..100).map(|i| ((i * 37) % 11) as f64).collect();
1449        assert_eq!(acf(&long, None).expect("defined").acf.len(), 21);
1450    }
1451
1452    // -----------------------------------------------------------------------
1453    // PACF tests
1454    // -----------------------------------------------------------------------
1455
1456    #[test]
1457    fn pacf_lag1_equals_acf_lag1() {
1458        let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0, 5.0, 3.0];
1459        let acf_r = acf(&data, Some(4)).expect("should compute ACF");
1460        let pacf_r = pacf(&data, Some(4)).expect("should compute PACF");
1461        assert!(
1462            (pacf_r.pacf[0] - acf_r.acf[1]).abs() < 1e-10,
1463            "PACF[1] should equal ACF[1]: {} vs {}",
1464            pacf_r.pacf[0],
1465            acf_r.acf[1]
1466        );
1467    }
1468
1469    #[test]
1470    fn pacf_linear_trend() {
1471        let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1472        let r = pacf(&data, Some(5)).expect("should compute");
1473        // AR(1)-like: strong lag-1 PACF, weaker at higher lags
1474        assert!(r.pacf[0].abs() > 0.5, "lag-1 PACF should be strong");
1475    }
1476
1477    #[test]
1478    fn pacf_bounded() {
1479        let data: Vec<f64> = (0..30).map(|i| (i as f64 * 0.5).sin()).collect();
1480        let r = pacf(&data, Some(10)).expect("should compute");
1481        for (i, &p) in r.pacf.iter().enumerate() {
1482            assert!(
1483                (-1.0..=1.0).contains(&p),
1484                "PACF[{}] = {} out of [-1, 1]",
1485                i + 1,
1486                p
1487            );
1488        }
1489    }
1490
1491    #[test]
1492    fn pacf_edge_cases() {
1493        assert_eq!(
1494            pacf(&[1.0, 2.0, 3.0], Some(1)).unwrap_err(),
1495            TimeSeriesError::TooFewObservations { needed: 4, got: 3 }
1496        );
1497        // Lags below n / 2: 1 to 4 for 10 values.
1498        let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0, 5.0, 3.0];
1499        for k in [0, 5] {
1500            assert_eq!(
1501                pacf(&data, Some(k)).unwrap_err(),
1502                TimeSeriesError::OptionOutOfRange {
1503                    option: "max_lag",
1504                    got: k as f64,
1505                    min: 1.0,
1506                    max: 4.0
1507                }
1508            );
1509        }
1510        assert_eq!(pacf(&data, None).expect("defined").pacf.len(), 4);
1511    }
1512}
1513
1514#[cfg(test)]
1515mod proptests {
1516    use super::*;
1517    use proptest::prelude::*;
1518
1519    fn bounded_vec(min_len: usize, max_len: usize) -> BoxedStrategy<Vec<f64>> {
1520        proptest::collection::vec(-1e6_f64..1e6, min_len..=max_len).boxed()
1521    }
1522
1523    proptest! {
1524        #[test]
1525        fn pearson_bounded(
1526            data in bounded_vec(5, 50).prop_flat_map(|x| {
1527                let n = x.len();
1528                (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1529            })
1530        ) {
1531            let (x, y) = data;
1532            if let Some(result) = pearson(&x, &y) {
1533                prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1534                prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1535            }
1536        }
1537
1538        #[test]
1539        fn spearman_bounded(
1540            data in bounded_vec(5, 50).prop_flat_map(|x| {
1541                let n = x.len();
1542                (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1543            })
1544        ) {
1545            let (x, y) = data;
1546            if let Some(result) = spearman(&x, &y) {
1547                prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1548                prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1549            }
1550        }
1551
1552        #[test]
1553        fn kendall_bounded(
1554            data in bounded_vec(5, 30).prop_flat_map(|x| {
1555                let n = x.len();
1556                (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1557            })
1558        ) {
1559            let (x, y) = data;
1560            if let Some(result) = kendall_tau_b(&x, &y) {
1561                prop_assert!(result.r >= -1.0 && result.r <= 1.0, "tau out of bounds: {}", result.r);
1562                prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1563            }
1564        }
1565
1566        #[test]
1567        fn pearson_symmetric(
1568            data in bounded_vec(5, 50).prop_flat_map(|x| {
1569                let n = x.len();
1570                (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1571            })
1572        ) {
1573            let (x, y) = data;
1574            let r_xy = pearson(&x, &y);
1575            let r_yx = pearson(&y, &x);
1576            match (r_xy, r_yx) {
1577                (Some(a), Some(b)) => {
1578                    prop_assert!((a.r - b.r).abs() < 1e-10, "not symmetric: {} vs {}", a.r, b.r);
1579                }
1580                (None, None) => {}
1581                _ => prop_assert!(false, "one is None but not the other"),
1582            }
1583        }
1584
1585        #[test]
1586        fn fisher_z_roundtrip_prop(r in -0.99_f64..0.99) {
1587            let z = fisher_z(r).expect("should compute");
1588            let r_back = fisher_z_inv(z);
1589            prop_assert!((r - r_back).abs() < 1e-10);
1590        }
1591
1592        #[test]
1593        fn ci_contains_true_r(
1594            r in -0.99_f64..0.99,
1595            n in 10_usize..200
1596        ) {
1597            let ci = correlation_ci(r, n, 0.95).expect("should compute");
1598            prop_assert!(ci.lower < ci.upper, "CI inverted");
1599            prop_assert!(ci.lower >= -1.0 && ci.upper <= 1.0, "CI out of bounds");
1600        }
1601
1602        #[test]
1603        fn acf_bounded_prop(
1604            data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1605        ) {
1606            if let Ok(r) = acf(&data, Some(10)) {
1607                prop_assert!((r.acf[0] - 1.0).abs() < 1e-10, "ACF[0] must be 1.0");
1608                for (i, &v) in r.acf.iter().enumerate() {
1609                    prop_assert!((-1.0..=1.0).contains(&v), "ACF[{i}] = {v} out of [-1,1]");
1610                }
1611            }
1612        }
1613
1614        #[test]
1615        fn pacf_bounded_prop(
1616            data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1617        ) {
1618            if let Ok(r) = pacf(&data, Some(5)) {
1619                for (i, &v) in r.pacf.iter().enumerate() {
1620                    prop_assert!((-1.0 - 1e-10..=1.0 + 1e-10).contains(&v), "PACF[{i}] = {v} out of bounds");
1621                }
1622            }
1623        }
1624    }
1625}
1626
1627/// A p-value far in the tail against SciPy 1.17.1 (`scipy.stats.pearsonr`).
1628#[cfg(test)]
1629mod scipy_tail_reference {
1630    use super::*;
1631
1632    #[test]
1633    fn pearson_p_value_keeps_its_tail() {
1634        let x: Vec<f64> = (1..=20).map(f64::from).collect();
1635        let y = [
1636            5.3999999999999995,
1637            7.0,
1638            9.4,
1639            11.8,
1640            13.2,
1641            15.8,
1642            17.6,
1643            19.8,
1644            22.250000000000004,
1645            23.75,
1646            26.25,
1647            28.150000000000002,
1648            30.55,
1649            32.050000000000004,
1650            34.55,
1651            36.7,
1652            38.550000000000004,
1653            41.1,
1654            42.699999999999996,
1655            45.0,
1656        ];
1657        let got = pearson(&x, &y).expect("valid").p_value;
1658        let want = 8.20335692044105e-33;
1659        assert!(
1660            (got - want).abs() <= 1e-9 * want,
1661            "p: got {got:e}, SciPy {want:e}"
1662        );
1663    }
1664}
1665
1666/// References for the Kendall p-values: SciPy 1.17.1 `kendalltau`, and the
1667/// definition (enumeration of all permutations).
1668#[cfg(test)]
1669mod kendall_exact_tests {
1670    use super::*;
1671
1672    // (permutation of 0..n, tau, p) -- SciPy 1.17.1 scipy.stats.kendalltau(range(n), perm), method='auto'
1673    const EXACT_CASES: &[(&[u8], f64, f64)] = &[
1674        (&[2, 1, 0], -1.0, 0.3333333333333333),
1675        (&[2, 1, 0], -1.0, 0.3333333333333333),
1676        (&[2, 1, 0, 3], 0.0, 1.0),
1677        (&[2, 0, 3, 1], 0.0, 1.0),
1678        (&[4, 0, 3, 1, 2], -0.19999999999999998, 0.8166666666666667),
1679        (&[2, 0, 3, 4, 1], 0.19999999999999998, 0.8166666666666667),
1680        (&[4, 1, 3, 2, 5, 0], -0.2, 0.7194444444444444),
1681        (&[3, 2, 1, 5, 0, 4], -0.06666666666666665, 1.0),
1682        (
1683            &[0, 5, 3, 1, 4, 2, 6],
1684            0.33333333333333337,
1685            0.3813492063492063,
1686        ),
1687        (
1688            &[5, 2, 4, 1, 3, 0, 6],
1689            -0.14285714285714288,
1690            0.7726190476190476,
1691        ),
1692        (
1693            &[1, 4, 3, 0, 7, 5, 6, 2],
1694            0.21428571428571427,
1695            0.5484126984126985,
1696        ),
1697        (
1698            &[5, 2, 3, 6, 1, 0, 4, 7],
1699            0.07142857142857142,
1700            0.9048611111111111,
1701        ),
1702        (
1703            &[2, 3, 6, 7, 1, 4, 8, 5, 0],
1704            0.05555555555555555,
1705            0.919455467372134,
1706        ),
1707        (
1708            &[7, 8, 1, 6, 0, 3, 4, 5, 2],
1709            -0.2777777777777778,
1710            0.35848765432098767,
1711        ),
1712        (
1713            &[7, 4, 2, 5, 9, 0, 8, 3, 6, 1],
1714            -0.15555555555555553,
1715            0.6006536596119929,
1716        ),
1717        (
1718            &[3, 4, 6, 0, 1, 9, 8, 5, 2, 7],
1719            0.19999999999999998,
1720            0.4843127204585538,
1721        ),
1722        (
1723            &[10, 7, 6, 4, 5, 3, 1, 2, 11, 0, 9, 8],
1724            -0.2121212121212121,
1725            0.38070480349126185,
1726        ),
1727        (
1728            &[7, 4, 3, 9, 10, 8, 2, 1, 6, 5, 0, 11],
1729            -0.1212121212121212,
1730            0.6383612539081289,
1731        ),
1732        (
1733            &[6, 10, 0, 13, 1, 4, 5, 2, 9, 3, 12, 7, 14, 8, 11],
1734            0.29523809523809524,
1735            0.13946285911906778,
1736        ),
1737        (
1738            &[9, 11, 12, 1, 4, 3, 6, 8, 7, 14, 13, 0, 2, 5, 10],
1739            -0.028571428571428574,
1740            0.9225700904951897,
1741        ),
1742        (
1743            &[11, 17, 13, 1, 6, 4, 2, 5, 10, 7, 12, 9, 14, 16, 15, 3, 0, 8],
1744            0.006535947712418302,
1745            1.0,
1746        ),
1747        (
1748            &[8, 16, 12, 4, 13, 6, 10, 17, 2, 5, 1, 15, 9, 14, 7, 11, 0, 3],
1749            -0.21568627450980396,
1750            0.22906043853787195,
1751        ),
1752        (
1753            &[
1754                15, 3, 0, 13, 11, 8, 1, 7, 17, 19, 10, 14, 12, 2, 5, 4, 6, 18, 9, 16,
1755            ],
1756            0.09473684210526316,
1757            0.5858584459782121,
1758        ),
1759        (
1760            &[
1761                7, 13, 17, 12, 11, 19, 2, 16, 8, 3, 10, 9, 6, 15, 18, 14, 0, 4, 5, 1,
1762            ],
1763            -0.2736842105263158,
1764            0.0983302187347563,
1765        ),
1766        (
1767            &[
1768                9, 21, 11, 3, 17, 12, 8, 1, 5, 13, 15, 22, 6, 4, 18, 2, 0, 14, 16, 23, 19, 7, 10,
1769                20,
1770            ],
1771            0.11594202898550725,
1772            0.44607885611402504,
1773        ),
1774        (
1775            &[
1776                17, 2, 18, 7, 10, 21, 15, 14, 13, 23, 1, 4, 6, 9, 3, 8, 22, 0, 20, 5, 11, 19, 12,
1777                16,
1778            ],
1779            -0.007246376811594203,
1780            0.9804334461613707,
1781        ),
1782        (
1783            &[
1784                20, 13, 24, 18, 14, 2, 9, 5, 10, 15, 11, 26, 12, 21, 25, 19, 16, 22, 27, 3, 1, 17,
1785                8, 7, 23, 4, 6, 0,
1786            ],
1787            -0.15873015873015872,
1788            0.24626955123962418,
1789        ),
1790        (
1791            &[
1792                3, 11, 14, 0, 6, 7, 21, 24, 19, 10, 8, 2, 27, 16, 26, 20, 23, 15, 9, 5, 13, 1, 25,
1793                4, 17, 12, 22, 18,
1794            ],
1795            0.13227513227513227,
1796            0.3362383470773132,
1797        ),
1798        (
1799            &[
1800                29, 16, 28, 19, 18, 12, 13, 27, 1, 25, 4, 17, 22, 15, 3, 2, 10, 8, 9, 23, 11, 7,
1801                21, 0, 26, 6, 5, 14, 24, 20,
1802            ],
1803            -0.18160919540229886,
1804            0.1653935231257671,
1805        ),
1806        (
1807            &[
1808                17, 10, 9, 22, 19, 18, 8, 5, 21, 29, 13, 28, 14, 20, 27, 25, 0, 23, 24, 7, 16, 12,
1809                11, 4, 2, 26, 6, 1, 3, 15,
1810            ],
1811            -0.20459770114942533,
1812            0.11698808645983594,
1813        ),
1814        (
1815            &[
1816                31, 24, 14, 11, 29, 12, 28, 16, 10, 7, 6, 3, 8, 17, 25, 20, 26, 0, 13, 21, 5, 9,
1817                18, 23, 27, 1, 22, 19, 2, 30, 15, 4, 32,
1818            ],
1819            -0.041666666666666664,
1820            0.7468360016830153,
1821        ),
1822        (
1823            &[
1824                0, 27, 13, 32, 19, 21, 24, 25, 30, 16, 23, 8, 26, 7, 5, 14, 29, 20, 31, 22, 28, 6,
1825                15, 2, 4, 18, 9, 10, 1, 17, 11, 12, 3,
1826            ],
1827            -0.27272727272727276,
1828            0.025883100486831147,
1829        ),
1830        (
1831            &[
1832                0, 1, 2, 4, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
1833            ],
1834            0.9894736842105263,
1835            1.644127049324866e-17,
1836        ),
1837        (
1838            &[
1839                0, 1, 2, 4, 3, 5, 6, 7, 8, 9, 11, 10, 12, 13, 14, 15, 16, 17, 18, 19,
1840            ],
1841            0.9789473684210527,
1842            1.7181127665444849e-16,
1843        ),
1844        (
1845            &[
1846                19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
1847            ],
1848            -1.0,
1849            8.22063524662433e-19,
1850        ),
1851        (
1852            &[
1853                19, 18, 17, 16, 15, 13, 14, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
1854            ],
1855            -0.9894736842105263,
1856            1.644127049324866e-17,
1857        ),
1858        (
1859            &[
1860                0, 1, 2, 4, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1861                23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
1862            ],
1863            0.9962121212121212,
1864            7.600781509709488e-36,
1865        ),
1866        (
1867            &[
1868                0, 1, 2, 4, 3, 5, 6, 7, 8, 9, 11, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1869                23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
1870            ],
1871            0.9924242424242423,
1872            1.2898295895264586e-34,
1873        ),
1874        (
1875            &[
1876                32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12,
1877                11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
1878            ],
1879            -1.0,
1880            2.30326712415439e-37,
1881        ),
1882        (
1883            &[
1884                32, 31, 30, 29, 28, 26, 27, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12,
1885                11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
1886            ],
1887            -0.9962121212121212,
1888            7.600781509709488e-36,
1889        ),
1890        (
1891            &[
1892                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1893                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
1894            ],
1895            1.0,
1896            6.774315071042324e-39,
1897        ),
1898        (
1899            &[
1900                0, 1, 2, 3, 4, 5, 6, 8, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1901                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
1902            ],
1903            0.9964349376114083,
1904            2.30326712415439e-37,
1905        ),
1906        (
1907            &[
1908                33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13,
1909                12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
1910            ],
1911            -1.0,
1912            6.774315071042324e-39,
1913        ),
1914        (
1915            &[
1916                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1917                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
1918                44, 45, 46, 47, 48, 49,
1919            ],
1920            1.0,
1921            6.575898833266317e-65,
1922        ),
1923        (
1924            &[
1925                0, 1, 2, 3, 4, 5, 6, 8, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1926                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
1927                44, 45, 46, 47, 48, 49,
1928            ],
1929            0.9983673469387755,
1930            3.287949416633158e-63,
1931        ),
1932        (
1933            &[
1934                49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29,
1935                28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8,
1936                7, 6, 5, 4, 3, 2, 1, 0,
1937            ],
1938            -1.0,
1939            6.575898833266317e-65,
1940        ),
1941        (
1942            &[
1943                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1944                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
1945                44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
1946                65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85,
1947                86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
1948            ],
1949            1.0,
1950            2.143020576250934e-158,
1951        ),
1952        (
1953            &[
1954                0, 1, 2, 3, 4, 5, 6, 8, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1955                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
1956                44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
1957                65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85,
1958                86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
1959            ],
1960            0.9995959595959597,
1961            2.1430205762509337e-156,
1962        ),
1963        (
1964            &[
1965                99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79,
1966                78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58,
1967                57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37,
1968                36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16,
1969                15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
1970            ],
1971            -1.0,
1972            2.143020576250934e-158,
1973        ),
1974        (
1975            &[
1976                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1977                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
1978                44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
1979                65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85,
1980                86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104,
1981                105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
1982                121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136,
1983                137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152,
1984                153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168,
1985                169,
1986            ],
1987            1.0,
1988            2.755801935583541e-307,
1989        ),
1990        (
1991            &[
1992                0, 1, 2, 3, 4, 5, 6, 8, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1993                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
1994                44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
1995                65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85,
1996                86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104,
1997                105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
1998                121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136,
1999                137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152,
2000                153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168,
2001                169,
2002            ],
2003            0.9998607727114516,
2004            4.68486329049202e-305,
2005        ),
2006        (
2007            &[
2008                169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154,
2009                153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138,
2010                137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122,
2011                121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106,
2012                105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87,
2013                86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66,
2014                65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45,
2015                44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24,
2016                23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,
2017                0,
2018            ],
2019            -1.0,
2020            2.755801935583541e-307,
2021        ),
2022        (
2023            &[
2024                7, 11, 20, 1, 33, 32, 29, 18, 3, 5, 27, 10, 8, 0, 31, 23, 9, 13, 19, 17, 28, 24, 6,
2025                4, 2, 26, 15, 16, 12, 14, 25, 21, 22, 30,
2026            ],
2027            0.07664884135472372,
2028            0.5238310765052258,
2029        ),
2030        (
2031            &[
2032                29, 22, 18, 16, 38, 31, 2, 24, 11, 23, 30, 5, 4, 34, 15, 33, 36, 6, 32, 27, 12, 14,
2033                19, 0, 37, 8, 35, 20, 26, 13, 21, 28, 7, 3, 17, 1, 9, 25, 39, 10,
2034            ],
2035            -0.10512820512820513,
2036            0.33938312065834597,
2037        ),
2038        (
2039            &[
2040                6, 51, 13, 41, 17, 52, 24, 35, 53, 2, 49, 22, 43, 39, 30, 58, 10, 59, 8, 21, 5, 20,
2041                50, 4, 44, 47, 38, 9, 36, 32, 42, 15, 26, 18, 37, 45, 23, 55, 25, 40, 28, 54, 11,
2042                7, 48, 16, 3, 27, 0, 34, 14, 46, 33, 56, 12, 1, 31, 57, 19, 29,
2043            ],
2044            -0.05536723163841807,
2045            0.5319468085548157,
2046        ),
2047    ];
2048
2049    // (x, y, tau, p) with ties -- SciPy 1.17.1 scipy.stats.kendalltau(x, y), method='auto' (asymptotic)
2050    const TIE_CASES: &[(&[f64], &[f64], f64, f64)] = &[
2051        (
2052            &[0.0, 2.0, 1.0, 1.0, 2.0, 0.0, 2.0, 1.0],
2053            &[3.0, 2.2, 1.6, 1.6, 2.2, 3.0, 1.2, 0.6],
2054            -0.30550504633038933,
2055            0.3441320967322945,
2056        ),
2057        (
2058            &[3.0, 1.0, 0.0, 3.0, 2.0, 3.0, 2.0, 2.0, 3.0, 0.0, 3.0, 1.0],
2059            &[
2060                9.8,
2061                10.6,
2062                5.0,
2063                12.8,
2064                3.2,
2065                9.8,
2066                2.2,
2067                2.2,
2068                1.7999999999999998,
2069                10.0,
2070                3.8,
2071                1.6,
2072            ],
2073            0.05251050315105037,
2074            0.827163859987499,
2075        ),
2076        (
2077            &[5.0, 4.0, 3.0, 4.0, 3.0, 4.0, 7.0, 1.0, 6.0, 2.0, 6.0, 9.0],
2078            &[
2079                4.0,
2080                2.4,
2081                3.8,
2082                3.4,
2083                2.8,
2084                2.4,
2085                6.2,
2086                1.6,
2087                5.6,
2088                1.2,
2089                4.6,
2090                5.3999999999999995,
2091            ],
2092            0.7146459189048197,
2093            0.001720169176312472,
2094        ),
2095        (
2096            &[
2097                3.0, 4.0, 1.0, 1.0, 0.0, 4.0, 4.0, 1.0, 2.0, 4.0, 2.0, 2.0, 4.0, 3.0, 3.0, 4.0,
2098                4.0, 1.0, 3.0, 3.0, 0.0, 0.0, 3.0, 2.0, 4.0,
2099            ],
2100            &[
2101                1.7999999999999998,
2102                5.4,
2103                4.6,
2104                0.6,
2105                0.0,
2106                3.4,
2107                4.4,
2108                1.6,
2109                1.2,
2110                4.4,
2111                3.2,
2112                2.2,
2113                7.4,
2114                6.8,
2115                5.8,
2116                2.4,
2117                4.4,
2118                4.6,
2119                6.8,
2120                2.8,
2121                2.0,
2122                0.0,
2123                2.8,
2124                2.2,
2125                2.4,
2126            ],
2127            0.4371231544800122,
2128            0.005038720516121261,
2129        ),
2130        (
2131            &[
2132                4.0, 6.0, 4.0, 1.0, 6.0, 5.0, 7.0, 6.0, 7.0, 2.0, 4.0, 6.0, 4.0, 0.0, 5.0, 0.0,
2133                7.0, 5.0, 2.0, 3.0, 7.0, 7.0, 4.0, 7.0, 0.0, 6.0, 6.0, 6.0, 1.0, 5.0, 1.0, 6.0,
2134                6.0, 2.0, 7.0, 7.0, 2.0, 6.0, 6.0, 6.0,
2135            ],
2136            &[
2137                9.4,
2138                3.5999999999999996,
2139                3.4,
2140                1.6,
2141                9.6,
2142                5.0,
2143                10.2,
2144                10.6,
2145                8.2,
2146                5.2,
2147                5.4,
2148                8.6,
2149                2.4,
2150                4.0,
2151                7.0,
2152                3.0,
2153                10.2,
2154                6.0,
2155                3.2,
2156                5.8,
2157                5.2,
2158                10.2,
2159                3.4,
2160                10.2,
2161                1.0,
2162                10.6,
2163                9.6,
2164                9.6,
2165                4.6,
2166                3.0,
2167                5.6,
2168                5.6,
2169                8.6,
2170                3.2,
2171                10.2,
2172                4.2,
2173                3.2,
2174                10.6,
2175                9.6,
2176                9.6,
2177            ],
2178            0.5598074769339775,
2179            2.6671536744057452e-06,
2180        ),
2181        (
2182            &[
2183                1.0, 4.0, 2.0, 0.0, 7.0, 3.0, 9.0, 2.0, 3.0, 3.0, 7.0, 4.0, 8.0, 9.0, 2.0, 4.0,
2184                2.0, 0.0, 2.0, 3.0, 6.0, 4.0, 2.0, 3.0, 0.0, 0.0, 3.0, 9.0, 6.0, 0.0, 7.0, 7.0,
2185                4.0, 5.0, 4.0, 8.0, 4.0, 6.0, 0.0, 0.0, 1.0, 0.0, 4.0, 0.0, 6.0, 6.0, 9.0, 1.0,
2186                7.0, 1.0, 5.0, 3.0, 1.0, 0.0, 1.0, 9.0, 4.0, 7.0, 3.0, 6.0, 3.0, 7.0, 5.0, 9.0,
2187                1.0, 1.0, 1.0, 7.0, 0.0, 8.0, 6.0, 5.0, 0.0, 2.0, 9.0, 7.0, 9.0, 1.0, 0.0, 9.0,
2188                8.0, 2.0, 7.0, 8.0, 7.0, 6.0, 8.0, 6.0, 0.0, 6.0, 0.0, 0.0, 0.0, 0.0, 6.0, 6.0,
2189                0.0, 6.0, 6.0, 2.0,
2190            ],
2191            &[
2192                2.6,
2193                8.4,
2194                3.2,
2195                6.0,
2196                8.2,
2197                6.8,
2198                6.3999999999999995,
2199                1.2,
2200                5.8,
2201                7.8,
2202                7.2,
2203                4.4,
2204                6.8,
2205                10.399999999999999,
2206                4.2,
2207                7.4,
2208                3.2,
2209                4.0,
2210                7.2,
2211                4.8,
2212                4.6,
2213                5.4,
2214                4.2,
2215                4.8,
2216                5.0,
2217                3.0,
2218                4.8,
2219                11.399999999999999,
2220                5.6,
2221                0.0,
2222                10.2,
2223                9.2,
2224                6.4,
2225                9.0,
2226                2.4,
2227                9.8,
2228                2.4,
2229                9.6,
2230                2.0,
2231                3.0,
2232                2.6,
2233                4.0,
2234                7.4,
2235                3.0,
2236                3.5999999999999996,
2237                5.6,
2238                10.399999999999999,
2239                1.6,
2240                5.2,
2241                6.6,
2242                5.0,
2243                1.7999999999999998,
2244                4.6,
2245                5.0,
2246                2.6,
2247                11.399999999999999,
2248                6.4,
2249                7.2,
2250                5.8,
2251                4.6,
2252                4.8,
2253                8.2,
2254                8.0,
2255                9.399999999999999,
2256                1.6,
2257                2.6,
2258                2.6,
2259                9.2,
2260                6.0,
2261                9.8,
2262                9.6,
2263                8.0,
2264                1.0,
2265                1.2,
2266                10.399999999999999,
2267                10.2,
2268                8.399999999999999,
2269                2.6,
2270                2.0,
2271                11.399999999999999,
2272                8.8,
2273                3.2,
2274                10.2,
2275                8.8,
2276                8.2,
2277                5.6,
2278                10.8,
2279                7.6,
2280                3.0,
2281                8.6,
2282                3.0,
2283                5.0,
2284                0.0,
2285                4.0,
2286                7.6,
2287                7.6,
2288                1.0,
2289                6.6,
2290                7.6,
2291                5.2,
2292            ],
2293            0.6273681127435055,
2294            2.3395873885354134e-18,
2295        ),
2296        (
2297            &[
2298                2.0, 81.0, 93.0, 5.0, 23.0, 26.0, 30.0, 18.0, 96.0, 39.0, 2.0, 30.0, 60.0, 40.0,
2299                90.0, 32.0, 63.0, 92.0, 6.0, 28.0, 85.0, 70.0, 35.0, 39.0, 58.0, 21.0, 23.0, 50.0,
2300                79.0, 79.0, 35.0, 15.0, 24.0, 94.0, 52.0, 3.0, 55.0, 72.0, 45.0, 76.0, 16.0, 93.0,
2301                42.0, 61.0, 75.0, 57.0, 87.0, 25.0, 54.0, 88.0, 46.0, 71.0, 57.0, 11.0, 74.0, 56.0,
2302                68.0, 56.0, 29.0, 88.0, 4.0, 19.0, 75.0, 38.0, 85.0, 86.0, 25.0, 96.0, 42.0, 10.0,
2303                34.0, 82.0, 27.0, 9.0, 26.0, 32.0, 24.0, 82.0, 7.0, 62.0, 90.0, 10.0, 30.0, 52.0,
2304                21.0, 32.0, 37.0, 0.0, 76.0, 26.0, 24.0, 50.0, 23.0, 41.0, 58.0, 32.0, 51.0, 0.0,
2305                19.0, 49.0,
2306            ],
2307            &[
2308                1.2,
2309                51.6,
2310                58.8,
2311                6.0,
2312                14.799999999999999,
2313                15.6,
2314                21.0,
2315                10.799999999999999,
2316                59.599999999999994,
2317                24.4,
2318                4.2,
2319                19.0,
2320                36.0,
2321                25.0,
2322                56.0,
2323                20.2,
2324                40.8,
2325                57.199999999999996,
2326                3.5999999999999996,
2327                19.8,
2328                51.0,
2329                44.0,
2330                22.0,
2331                24.4,
2332                35.8,
2333                14.6,
2334                16.799999999999997,
2335                32.0,
2336                47.4,
2337                47.4,
2338                24.0,
2339                9.0,
2340                17.4,
2341                59.4,
2342                34.2,
2343                1.7999999999999998,
2344                33.0,
2345                43.199999999999996,
2346                29.0,
2347                47.6,
2348                10.6,
2349                58.8,
2350                26.2,
2351                36.6,
2352                47.0,
2353                37.199999999999996,
2354                53.199999999999996,
2355                15.0,
2356                32.4,
2357                52.8,
2358                27.599999999999998,
2359                43.6,
2360                36.199999999999996,
2361                8.6,
2362                47.4,
2363                36.6,
2364                43.8,
2365                33.6,
2366                19.4,
2367                53.8,
2368                4.4,
2369                12.4,
2370                47.0,
2371                23.8,
2372                54.0,
2373                51.6,
2374                18.0,
2375                60.599999999999994,
2376                27.2,
2377                9.0,
2378                22.4,
2379                49.199999999999996,
2380                17.2,
2381                6.3999999999999995,
2382                18.6,
2383                22.2,
2384                17.4,
2385                49.199999999999996,
2386                6.2,
2387                40.199999999999996,
2388                55.0,
2389                7.0,
2390                18.0,
2391                32.2,
2392                12.6,
2393                20.2,
2394                23.2,
2395                0.0,
2396                45.6,
2397                16.6,
2398                15.399999999999999,
2399                31.0,
2400                16.799999999999997,
2401                26.599999999999998,
2402                34.8,
2403                19.2,
2404                33.599999999999994,
2405                0.0,
2406                14.4,
2407                32.4,
2408            ],
2409            0.9619049618044789,
2410            3.673981239914972e-45,
2411        ),
2412    ];
2413
2414    fn assert_close(got: f64, want: f64, rel: f64, what: &str) {
2415        let scale = want.abs().max(1e-300);
2416        assert!(
2417            (got - want).abs() <= rel * scale,
2418            "{what}: got {got:e}, want {want:e}"
2419        );
2420    }
2421
2422    /// The exact p-value and tau equal SciPy's `kendalltau` (method "auto":
2423    /// the exact distribution up to 33 observations without ties, and at any
2424    /// size when at most one pair is on the minority side; the normal
2425    /// approximation otherwise).
2426    #[test]
2427    fn kendall_p_values_reproduce_scipy_without_ties() {
2428        for (perm, tau, p) in EXACT_CASES {
2429            let x: Vec<f64> = (0..perm.len()).map(|i| i as f64).collect();
2430            let y: Vec<f64> = perm.iter().map(|&v| f64::from(v)).collect();
2431            let r = kendall_tau_b(&x, &y).expect("computes");
2432            let what = format!("n = {}", perm.len());
2433            assert!((r.r - tau).abs() < 1e-12, "{what}: tau {} vs {tau}", r.r);
2434            assert_close(r.p_value, *p, 1e-12, &what);
2435        }
2436    }
2437
2438    /// With ties SciPy takes the normal approximation with the variance of
2439    /// Kendall (1970); so does the crate, terms for tied groups included.
2440    #[test]
2441    fn kendall_p_values_reproduce_scipy_with_ties() {
2442        assert!(!TIE_CASES.is_empty());
2443        for (x, y, tau, p) in TIE_CASES {
2444            let r = kendall_tau_b(x, y).expect("computes");
2445            let what = format!("n = {} with ties", x.len());
2446            assert!((r.r - tau).abs() < 1e-12, "{what}: tau {} vs {tau}", r.r);
2447            assert_close(r.p_value, *p, 1e-10, &what);
2448        }
2449    }
2450
2451    /// The definition: over all n! orderings of y against x, the p-value of an
2452    /// outcome is twice the share with no more discordant pairs than the
2453    /// smaller side of the outcome (capped at 1), 4 * tail = n(n-1) giving 1.
2454    #[test]
2455    fn exact_p_is_the_share_of_permutations_with_fewer_inversions() {
2456        fn permutations(
2457            n: usize,
2458            out: &mut Vec<Vec<usize>>,
2459            cur: &mut Vec<usize>,
2460            used: &mut [bool],
2461        ) {
2462            if cur.len() == n {
2463                out.push(cur.clone());
2464                return;
2465            }
2466            for v in 0..n {
2467                if !used[v] {
2468                    used[v] = true;
2469                    cur.push(v);
2470                    permutations(n, out, cur, used);
2471                    cur.pop();
2472                    used[v] = false;
2473                }
2474            }
2475        }
2476        for n in 3..=8usize {
2477            let mut all = Vec::new();
2478            permutations(n, &mut all, &mut Vec::new(), &mut vec![false; n]);
2479            let inversions: Vec<i64> = all
2480                .iter()
2481                .map(|p| {
2482                    (0..n)
2483                        .flat_map(|i| (i + 1..n).map(move |j| (i, j)))
2484                        .filter(|&(i, j)| p[i] > p[j])
2485                        .count() as i64
2486                })
2487                .collect();
2488            let total = (n * (n - 1) / 2) as i64;
2489            for d in 0..=total {
2490                let tail = d.min(total - d);
2491                let share =
2492                    inversions.iter().filter(|&&k| k <= tail).count() as f64 / all.len() as f64;
2493                let want = if 4 * tail == (n * (n - 1)) as i64 {
2494                    1.0
2495                } else {
2496                    (2.0 * share).min(1.0)
2497                };
2498                assert_close(
2499                    kendall_exact_p(n, d),
2500                    want,
2501                    1e-14,
2502                    &format!("n = {n}, D = {d}"),
2503                );
2504            }
2505        }
2506    }
2507
2508    /// The tail sums to one over the whole distribution and is symmetric.
2509    #[test]
2510    fn exact_p_is_symmetric_and_a_probability() {
2511        for n in [3usize, 10, 33] {
2512            let total = (n * (n - 1) / 2) as i64;
2513            for d in 0..=total {
2514                let p = kendall_exact_p(n, d);
2515                assert!((0.0..=1.0).contains(&p), "n = {n}, D = {d}: {p}");
2516                assert_eq!(p, kendall_exact_p(n, total - d), "n = {n}, D = {d}");
2517            }
2518        }
2519    }
2520
2521    /// Past 33 observations, only the two smallest tails are exact -- 2/n!
2522    /// and 2/(n-1)! -- and the rest stays the normal approximation.
2523    #[test]
2524    fn beyond_33_only_the_smallest_tails_are_exact() {
2525        let n = 40;
2526        let x: Vec<f64> = (0..n).map(f64::from).collect();
2527        // One pair swapped: tail 1, exact 2 / 39!.
2528        let mut y = x.clone();
2529        y.swap(7, 8);
2530        let r = kendall_tau_b(&x, &y).expect("computes");
2531        let factorial_39: f64 = (2..=39).map(f64::from).product();
2532        assert_close(r.p_value, 2.0 / factorial_39, 1e-12, "tail 1");
2533        // Two pairs swapped: tail 2, the normal approximation (smooth, tiny
2534        // but not the exact value of the table above).
2535        y.swap(20, 21);
2536        let r2 = kendall_tau_b(&x, &y).expect("computes");
2537        assert!(r2.p_value > r.p_value && r2.p_value < 1e-9);
2538    }
2539}