use u_numflow::special;
use u_numflow::stats;
#[derive(Debug, Clone, Copy)]
pub struct TestResult {
pub statistic: f64,
pub df: f64,
pub p_value: f64,
}
pub fn one_sample_t_test(data: &[f64], mu0: f64) -> Option<TestResult> {
let n = data.len();
if n < 2 {
return None;
}
if data.iter().any(|v| !v.is_finite()) || !mu0.is_finite() {
return None;
}
let mean = stats::mean(data)?;
let sd = stats::std_dev(data)?;
if sd < 1e-300 {
return None; }
let t = (mean - mu0) / (sd / (n as f64).sqrt());
let df = (n - 1) as f64;
let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
Some(TestResult {
statistic: t,
df,
p_value,
})
}
pub fn two_sample_t_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
let n1 = a.len();
let n2 = b.len();
if n1 < 2 || n2 < 2 {
return None;
}
if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
return None;
}
let mean1 = stats::mean(a)?;
let mean2 = stats::mean(b)?;
let var1 = stats::variance(a)?;
let var2 = stats::variance(b)?;
let n1f = n1 as f64;
let n2f = n2 as f64;
let se_sq = var1 / n1f + var2 / n2f;
if se_sq < 1e-300 {
return None;
}
let t = (mean1 - mean2) / se_sq.sqrt();
let v1 = var1 / n1f;
let v2 = var2 / n2f;
let df = (v1 + v2).powi(2) / (v1 * v1 / (n1f - 1.0) + v2 * v2 / (n2f - 1.0));
let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
Some(TestResult {
statistic: t,
df,
p_value,
})
}
pub fn paired_t_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
if x.len() != y.len() || x.len() < 2 {
return None;
}
let diffs: Vec<f64> = x.iter().zip(y.iter()).map(|(&a, &b)| a - b).collect();
one_sample_t_test(&diffs, 0.0)
}
#[derive(Debug, Clone)]
pub struct AnovaResult {
pub f_statistic: f64,
pub df_between: usize,
pub df_within: usize,
pub p_value: f64,
pub ss_between: f64,
pub ss_within: f64,
pub ms_between: f64,
pub ms_within: f64,
pub group_means: Vec<f64>,
pub grand_mean: f64,
}
pub fn one_way_anova(groups: &[&[f64]]) -> Option<AnovaResult> {
let k = groups.len();
if k < 2 {
return None;
}
for g in groups {
if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
return None;
}
}
let total_n: usize = groups.iter().map(|g| g.len()).sum();
let grand_sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
let grand_mean = grand_sum / total_n as f64;
let group_means: Vec<f64> = groups
.iter()
.map(|g| g.iter().sum::<f64>() / g.len() as f64)
.collect();
let ss_between: f64 = groups
.iter()
.zip(group_means.iter())
.map(|(g, &gm)| g.len() as f64 * (gm - grand_mean).powi(2))
.sum();
let ss_within: f64 = groups
.iter()
.zip(group_means.iter())
.map(|(g, &gm)| g.iter().map(|&x| (x - gm).powi(2)).sum::<f64>())
.sum();
let df_between = k - 1;
let df_within = total_n - k;
if df_within == 0 {
return None;
}
let ms_between = ss_between / df_between as f64;
let ms_within = ss_within / df_within as f64;
let f_statistic = if ms_within > 1e-300 {
ms_between / ms_within
} else {
f64::INFINITY
};
let p_value = if f_statistic.is_infinite() {
0.0
} else {
1.0 - special::f_distribution_cdf(f_statistic, df_between as f64, df_within as f64)
};
Some(AnovaResult {
f_statistic,
df_between,
df_within,
p_value,
ss_between,
ss_within,
ms_between,
ms_within,
group_means,
grand_mean,
})
}
pub fn chi_squared_goodness_of_fit(observed: &[f64], expected: &[f64]) -> Option<TestResult> {
let k = observed.len();
if k < 2 || k != expected.len() {
return None;
}
for &e in expected {
if e <= 0.0 || !e.is_finite() {
return None;
}
}
for &o in observed {
if o < 0.0 || !o.is_finite() {
return None;
}
}
let chi2: f64 = observed
.iter()
.zip(expected.iter())
.map(|(&o, &e)| (o - e).powi(2) / e)
.sum();
let df = (k - 1) as f64;
let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
Some(TestResult {
statistic: chi2,
df,
p_value,
})
}
pub fn chi_squared_independence(table: &[f64], n_rows: usize, n_cols: usize) -> Option<TestResult> {
if n_rows < 2 || n_cols < 2 || table.len() != n_rows * n_cols {
return None;
}
for &v in table {
if v < 0.0 || !v.is_finite() {
return None;
}
}
let mut row_sums = vec![0.0; n_rows];
let mut col_sums = vec![0.0; n_cols];
let mut total = 0.0;
for i in 0..n_rows {
for j in 0..n_cols {
let val = table[i * n_cols + j];
row_sums[i] += val;
col_sums[j] += val;
total += val;
}
}
if total <= 0.0 {
return None;
}
for &r in &row_sums {
if r <= 0.0 {
return None;
}
}
for &c in &col_sums {
if c <= 0.0 {
return None;
}
}
let mut chi2 = 0.0;
for i in 0..n_rows {
for j in 0..n_cols {
let observed = table[i * n_cols + j];
let expected = row_sums[i] * col_sums[j] / total;
chi2 += (observed - expected).powi(2) / expected;
}
}
let df = ((n_rows - 1) * (n_cols - 1)) as f64;
let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
Some(TestResult {
statistic: chi2,
df,
p_value,
})
}
pub fn jarque_bera_test(data: &[f64]) -> Option<TestResult> {
let n = data.len();
if n < 8 {
return None;
}
if data.iter().any(|v| !v.is_finite()) {
return None;
}
let s = stats::skewness(data)?;
let k = stats::kurtosis(data)?;
let nf = n as f64;
let jb = (nf / 6.0) * (s * s + k * k / 4.0);
let p_value = 1.0 - special::chi_squared_cdf(jb, 2.0);
Some(TestResult {
statistic: jb,
df: 2.0,
p_value,
})
}
#[derive(Debug, Clone, Copy)]
pub struct AndersonDarlingResult {
pub statistic: f64,
pub statistic_star: f64,
pub p_value: f64,
}
pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
let n = data.len();
if n < 8 {
return None;
}
if data.iter().any(|v| !v.is_finite()) {
return None;
}
let mean = stats::mean(data)?;
let sd = stats::std_dev(data)?;
if sd < 1e-300 {
return None; }
let mut x: Vec<f64> = data.to_vec();
x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let nf = n as f64;
let mut s = 0.0;
for i in 0..n {
let z = (x[i] - mean) / sd;
let phi = special::standard_normal_cdf(z);
let phi = phi.clamp(1e-15, 1.0 - 1e-15);
let z_rev = (x[n - 1 - i] - mean) / sd;
let phi_rev = special::standard_normal_cdf(z_rev);
let phi_rev = phi_rev.clamp(1e-15, 1.0 - 1e-15);
let coeff = (2 * (i + 1) - 1) as f64;
s += coeff * (phi.ln() + (1.0 - phi_rev).ln());
}
let a2 = -nf - s / nf;
let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
let p = if a2_star >= 0.6 {
(1.2937 - 5.709 * a2_star + 0.0186 * a2_star * a2_star).exp()
} else if a2_star > 0.34 {
(0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
} else if a2_star > 0.2 {
1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
} else {
1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
};
Some(AndersonDarlingResult {
statistic: a2,
statistic_star: a2_star,
p_value: p.clamp(0.0, 1.0),
})
}
#[derive(Debug, Clone, Copy)]
pub struct AdNormalityResult {
pub statistic: f64,
pub statistic_modified: f64,
pub p_value: f64,
}
pub fn anderson_darling_normality(data: &[f64]) -> Option<AdNormalityResult> {
let n = data.len();
if n < 3 {
return None;
}
if data.iter().any(|v| !v.is_finite()) {
return None;
}
let mean = stats::mean(data)?;
let sd = stats::std_dev(data)?;
if sd < 1e-15 {
return None;
}
let mut x: Vec<f64> = data.to_vec();
x.sort_by(|a, b| a.partial_cmp(b).expect("values are finite"));
let nf = n as f64;
let mut s = 0.0;
for i in 0..n {
let z_i = (x[i] - mean) / sd;
let z_rev = (x[n - 1 - i] - mean) / sd;
let phi_i = special::standard_normal_cdf(z_i).clamp(1e-15, 1.0 - 1e-15);
let phi_rev = special::standard_normal_cdf(z_rev).clamp(1e-15, 1.0 - 1e-15);
let coeff = (2 * i + 1) as f64;
s += coeff * (phi_i.ln() + (1.0 - phi_rev).ln());
}
let a2 = -nf - s / nf;
let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
let p = if a2_star < 0.200 {
1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
} else if a2_star < 0.340 {
1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
} else if a2_star < 0.600 {
(0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
} else {
(1.2937 - 5.709 * a2_star + 0.0186 * a2_star * a2_star).exp()
};
Some(AdNormalityResult {
statistic: a2,
statistic_modified: a2_star,
p_value: p.clamp(0.0, 1.0),
})
}
#[derive(Debug, Clone, Copy)]
pub struct ShapiroWilkResult {
pub w: f64,
pub p_value: f64,
}
pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
let n = data.len();
if !(3..=5000).contains(&n) {
return None;
}
if data.iter().any(|v| !v.is_finite()) {
return None;
}
let mut x: Vec<f64> = data.to_vec();
x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
if x[n - 1] - x[0] < 1e-300 {
return None; }
let nn2 = n / 2;
if n == 3 {
return shapiro_wilk_n3(&x);
}
let a = sw_coefficients(n, nn2)?;
let w = sw_statistic(&x, &a, n, nn2);
if !(0.0..=1.0 + 1e-10).contains(&w) {
return None;
}
let w = w.min(1.0);
let p_value = sw_p_value(w, n);
Some(ShapiroWilkResult {
w,
p_value: p_value.clamp(0.0, 1.0),
})
}
fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
if ss < 1e-300 {
return None;
}
let numerator = a1 * (x[2] - x[0]);
let w = (numerator * numerator) / ss;
let w = w.clamp(0.75, 1.0);
let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
let p = p.clamp(0.0, 1.0);
Some(ShapiroWilkResult { w, p_value: p })
}
const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
const SW_G: [f64; 2] = [-2.273, 0.459];
fn sw_poly(c: &[f64], x: f64) -> f64 {
let mut result = c[c.len() - 1];
for i in (0..c.len() - 1).rev() {
result = result * x + c[i];
}
result
}
fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
let mut a = vec![0.0; nn2];
let mut m = vec![0.0; nn2];
let mut summ2 = 0.0;
for (i, mi) in m.iter_mut().enumerate() {
let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
*mi = special::inverse_normal_cdf(p);
summ2 += *mi * *mi;
}
summ2 *= 2.0;
let ssumm2 = summ2.sqrt();
let rsn = 1.0 / (n as f64).sqrt();
let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
if n <= 5 {
let fac_sq = summ2 - 2.0 * m[0] * m[0];
let one_minus = 1.0 - 2.0 * a1 * a1;
if fac_sq <= 0.0 || one_minus <= 0.0 {
return None;
}
let fac = (fac_sq / one_minus).sqrt();
a[0] = a1;
for i in 1..nn2 {
a[i] = -m[i] / fac;
}
} else {
let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
if fac_sq <= 0.0 || one_minus <= 0.0 {
return None;
}
let fac = (fac_sq / one_minus).sqrt();
a[0] = a1;
a[1] = a2;
for i in 2..nn2 {
a[i] = -m[i] / fac;
}
}
Some(a)
}
fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
let mut sa = 0.0;
for i in 0..nn2 {
sa += a[i] * (x[n - 1 - i] - x[i]);
}
let mean = x.iter().sum::<f64>() / n as f64;
let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
if ss < 1e-300 {
return 1.0; }
(sa * sa) / ss
}
fn sw_p_value(w: f64, n: usize) -> f64 {
let nf = n as f64;
if n == 3 {
let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
return p.clamp(0.0, 1.0);
}
let w1 = 1.0 - w;
if w1 <= 0.0 {
return 1.0; }
let y = w1.ln();
if n <= 11 {
let gamma = sw_poly(&SW_G, nf);
if y >= gamma {
return 0.0; }
let y2 = -(gamma - y).ln();
let m = sw_poly(&SW_C3, nf);
let s = sw_poly(&SW_C4, nf).exp();
if s < 1e-300 {
return 0.0;
}
let z = (y2 - m) / s;
1.0 - special::standard_normal_cdf(z)
} else {
let xx = nf.ln();
let m = sw_poly(&SW_C5, xx);
let s = sw_poly(&SW_C6, xx).exp();
if s < 1e-300 {
return 0.0;
}
let z = (y - m) / s;
1.0 - special::standard_normal_cdf(z)
}
}
pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
let n1 = a.len();
let n2 = b.len();
if n1 < 2 || n2 < 2 {
return None;
}
if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
return None;
}
let n = n1 + n2;
let n1f = n1 as f64;
let n2f = n2 as f64;
let nf = n as f64;
let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
for &v in a {
combined.push((v, 0)); }
for &v in b {
combined.push((v, 1)); }
combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
let ranks = average_ranks(&combined);
let r1: f64 = combined
.iter()
.zip(ranks.iter())
.filter(|((_, g), _)| *g == 0)
.map(|(_, &r)| r)
.sum();
let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
let tie_correction = compute_tie_correction(&combined);
let mu = n1f * n2f / 2.0;
let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
if sigma_sq <= 0.0 {
return None;
}
let z = (u1 - mu) / sigma_sq.sqrt();
let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
Some(TestResult {
statistic: u1,
df: 0.0, p_value,
})
}
pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
if x.len() != y.len() || x.len() < 2 {
return None;
}
if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
return None;
}
let diffs: Vec<f64> = x
.iter()
.zip(y.iter())
.map(|(&a, &b)| a - b)
.filter(|&d| d.abs() > 1e-300)
.collect();
let nr = diffs.len();
if nr < 2 {
return None;
}
let nf = nr as f64;
let mut abs_diffs: Vec<(f64, usize)> = diffs
.iter()
.enumerate()
.map(|(i, &d)| (d.abs(), i))
.collect();
abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let ranks = average_ranks(&abs_diffs);
let t_plus: f64 = abs_diffs
.iter()
.zip(ranks.iter())
.filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
.map(|(_, &r)| r)
.sum();
let tie_correction_val = compute_tie_correction(&abs_diffs);
let mu = nf * (nf + 1.0) / 4.0;
let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
if sigma_sq <= 0.0 {
return None;
}
let z = (t_plus - mu) / sigma_sq.sqrt();
let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
Some(TestResult {
statistic: t_plus,
df: 0.0,
p_value,
})
}
fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
let n = sorted.len();
let mut ranks = vec![0.0; n];
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
j += 1;
}
let avg_rank = (i + 1 + j) as f64 / 2.0;
for rank in ranks.iter_mut().take(j).skip(i) {
*rank = avg_rank;
}
i = j;
}
ranks
}
fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
let n = sorted.len();
let mut correction = 0.0;
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
j += 1;
}
let t = (j - i) as f64;
if t > 1.0 {
correction += t * (t * t - 1.0);
}
i = j;
}
correction
}
pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
let k = groups.len();
if k < 2 {
return None;
}
for g in groups {
if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
return None;
}
}
let total_n: usize = groups.iter().map(|g| g.len()).sum();
let nf = total_n as f64;
let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
for (gi, g) in groups.iter().enumerate() {
for &v in *g {
combined.push((v, gi));
}
}
combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let ranks = average_ranks(&combined);
let mut rank_sums = vec![0.0; k];
for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
rank_sums[*gi] += r;
}
let mean_rank = (nf + 1.0) / 2.0;
let mut h = 0.0;
for (gi, g) in groups.iter().enumerate() {
let ni = g.len() as f64;
let mean_rank_i = rank_sums[gi] / ni;
h += ni * (mean_rank_i - mean_rank).powi(2);
}
h *= 12.0 / (nf * (nf + 1.0));
let tie_corr = compute_tie_correction(&combined);
let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
if denom > 1e-15 {
h /= denom;
}
let df = (k - 1) as f64;
let p_value = 1.0 - special::chi_squared_cdf(h, df);
Some(TestResult {
statistic: h,
df,
p_value,
})
}
pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
let k = groups.len();
if k < 2 {
return None;
}
for g in groups {
if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
return None;
}
}
let z_groups: Vec<Vec<f64>> = groups
.iter()
.map(|g| {
let median = stats::median(g).unwrap_or(0.0);
g.iter().map(|&x| (x - median).abs()).collect()
})
.collect();
let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
let anova = one_way_anova(&z_refs)?;
Some(TestResult {
statistic: anova.f_statistic,
df: anova.df_between as f64,
p_value: anova.p_value,
})
}
pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
if p_values.is_empty() || p_values.iter().any(|v| !v.is_finite()) {
return None;
}
let m = p_values.len() as f64;
Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
}
pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
let m = p_values.len();
if m == 0 || p_values.iter().any(|v| !v.is_finite()) {
return None;
}
let mut indices: Vec<usize> = (0..m).collect();
indices.sort_by(|&a, &b| {
p_values[a]
.partial_cmp(&p_values[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mf = m as f64;
let mut adjusted = vec![0.0; m];
let mut cummin = f64::INFINITY;
for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
cummin = cummin.min(adj);
adjusted[orig_idx] = cummin;
}
Some(adjusted)
}
pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
let k = groups.len();
if k < 2 {
return None;
}
let mut sizes = Vec::with_capacity(k);
let mut vars = Vec::with_capacity(k);
let mut n_total: usize = 0;
for g in groups {
if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
return None;
}
let n = g.len();
let v = stats::variance(g)?;
if v <= 0.0 {
return None; }
sizes.push(n);
vars.push(v);
n_total += n;
}
let nk = n_total - k; if nk == 0 {
return None;
}
let nk_f = nk as f64;
let s2_pooled: f64 = sizes
.iter()
.zip(vars.iter())
.map(|(&n, &v)| (n as f64 - 1.0) * v)
.sum::<f64>()
/ nk_f;
if s2_pooled <= 0.0 {
return None;
}
let num = nk_f * s2_pooled.ln()
- sizes
.iter()
.zip(vars.iter())
.map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
.sum::<f64>();
let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
let statistic = num / c;
let df = (k - 1) as f64;
let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
Some(TestResult {
statistic,
df,
p_value,
})
}
pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
let row1 = a + b;
let row2 = c + d;
let col1 = a + c;
let col2 = b + d;
let n = a + b + c + d;
if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
return None;
}
let log_prob = |a_i: u64| -> f64 {
let b_i = row1 - a_i;
let c_i = col1 - a_i;
let d_i = row2 - c_i;
ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
- ln_factorial(a_i)
- ln_factorial(b_i)
- ln_factorial(c_i)
- ln_factorial(d_i)
- ln_factorial(n)
};
let a_min = col1.saturating_sub(row2);
let a_max = row1.min(col1);
let log_p_obs = log_prob(a);
let mut p_value = 0.0;
for a_i in a_min..=a_max {
let lp = log_prob(a_i);
if lp <= log_p_obs + 1e-10 {
p_value += lp.exp();
}
}
let p_value = p_value.min(1.0);
let odds_ratio = if b > 0 && c > 0 {
(a as f64 * d as f64) / (b as f64 * c as f64)
} else {
f64::INFINITY
};
Some(TestResult {
statistic: odds_ratio,
df: 1.0,
p_value,
})
}
#[derive(Debug, Clone, Copy)]
pub struct MannKendallResult {
pub s_statistic: i64,
pub variance: f64,
pub z_statistic: f64,
pub p_value: f64,
pub kendall_tau: f64,
pub sen_slope: f64,
}
pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
let n = data.len();
if n < 4 || data.iter().any(|v| !v.is_finite()) {
return None;
}
let mut s: i64 = 0;
for i in 0..n - 1 {
for j in (i + 1)..n {
let diff = data[j] - data[i];
if diff > 0.0 {
s += 1;
} else if diff < 0.0 {
s -= 1;
}
}
}
let mut sorted: Vec<f64> = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
let mut tie_correction: f64 = 0.0;
let mut current_count: usize = 1;
for i in 1..sorted.len() {
if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
current_count += 1;
} else {
if current_count > 1 {
let t = current_count as f64;
tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
}
current_count = 1;
}
}
if current_count > 1 {
let t = current_count as f64;
tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
}
let nf = n as f64;
let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
if variance < 1e-300 {
return None; }
let sigma = variance.sqrt();
let z_statistic = if s > 0 {
(s as f64 - 1.0) / sigma
} else if s < 0 {
(s as f64 + 1.0) / sigma
} else {
0.0
};
let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z_statistic.abs()));
let p_value = p_value.clamp(0.0, 1.0);
let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
for i in 0..n - 1 {
for j in (i + 1)..n {
let dx = (j - i) as f64;
slopes.push((data[j] - data[i]) / dx);
}
}
slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
let m = slopes.len();
let sen_slope = if m % 2 == 0 {
(slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
} else {
slopes[m / 2]
};
Some(MannKendallResult {
s_statistic: s,
variance,
z_statistic,
p_value,
kendall_tau,
sen_slope,
})
}
fn ln_factorial(n: u64) -> f64 {
if n <= 1 {
return 0.0;
}
special::ln_gamma(n as f64 + 1.0)
}
#[derive(Debug, Clone)]
pub struct AdfResult {
pub statistic: f64,
pub n_lags: usize,
pub n_obs: usize,
pub critical_values: [f64; 3],
pub rejected: [bool; 3],
}
#[derive(Debug, Clone, Copy)]
pub enum AdfModel {
None,
Constant,
ConstantTrend,
}
pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
let n = data.len();
if n < 10 || data.iter().any(|v| !v.is_finite()) {
return None;
}
let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
let best_lag = match max_lags {
Some(p) => p, None => {
let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
let p_max = schwert.min(n / 3);
select_adf_lag(data, &dy, model, p_max)
}
};
adf_ols(data, &dy, model, best_lag)
}
fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
let mut best_aic = f64::INFINITY;
let mut best_p = 0;
for p in 0..=p_max {
if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
if aic < best_aic {
best_aic = aic;
best_p = p;
}
}
}
best_p
}
#[allow(clippy::type_complexity)]
fn adf_build_matrix(
data: &[f64],
dy: &[f64],
model: AdfModel,
p: usize,
) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
let start = p + 1;
if start >= dy.len() || dy.len() - start < 5 {
return None;
}
let m = dy.len() - start;
let y_dep: Vec<f64> = dy[start..].to_vec();
let has_intercept = !matches!(model, AdfModel::None);
let has_trend = matches!(model, AdfModel::ConstantTrend);
let ncols = has_intercept as usize + 1 + has_trend as usize + p;
let mut x_data = Vec::with_capacity(m * ncols);
let mut gamma_col = 0;
for i in 0..m {
let t = start + i;
if has_intercept {
x_data.push(1.0); gamma_col = 1;
}
x_data.push(data[t]); if has_trend {
x_data.push((t + 1) as f64); }
for lag in 1..=p {
x_data.push(dy[t - lag]);
}
}
Some((x_data, y_dep, m, ncols, gamma_col))
}
fn adf_ols_core(
x_data: &[f64],
y: &[f64],
m: usize,
ncols: usize,
gamma_col: usize,
) -> Option<(f64, f64, usize)> {
let mut xtx = vec![0.0_f64; ncols * ncols];
for i in 0..m {
let row = &x_data[i * ncols..(i + 1) * ncols];
for j in 0..ncols {
for k in j..ncols {
xtx[j * ncols + k] += row[j] * row[k];
}
}
}
for j in 0..ncols {
for k in (j + 1)..ncols {
xtx[k * ncols + j] = xtx[j * ncols + k];
}
}
let mut xty = vec![0.0_f64; ncols];
for i in 0..m {
let row = &x_data[i * ncols..(i + 1) * ncols];
for j in 0..ncols {
xty[j] += row[j] * y[i];
}
}
let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
for i in 0..ncols {
for j in 0..ncols {
augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
}
augmented[i * (ncols + 1) + ncols] = xty[i];
}
for col in 0..ncols {
let mut max_row = col;
let mut max_val = augmented[col * (ncols + 1) + col].abs();
for row in (col + 1)..ncols {
let val = augmented[row * (ncols + 1) + col].abs();
if val > max_val {
max_val = val;
max_row = row;
}
}
if max_val < 1e-15 {
return None; }
if max_row != col {
for j in 0..=ncols {
let a = col * (ncols + 1) + j;
let b = max_row * (ncols + 1) + j;
augmented.swap(a, b);
}
}
let pivot = augmented[col * (ncols + 1) + col];
for row in (col + 1)..ncols {
let factor = augmented[row * (ncols + 1) + col] / pivot;
for j in col..=ncols {
let above = augmented[col * (ncols + 1) + j];
augmented[row * (ncols + 1) + j] -= factor * above;
}
}
}
let mut beta = vec![0.0_f64; ncols];
for i in (0..ncols).rev() {
let mut sum = augmented[i * (ncols + 1) + ncols];
for j in (i + 1)..ncols {
sum -= augmented[i * (ncols + 1) + j] * beta[j];
}
beta[i] = sum / augmented[i * (ncols + 1) + i];
}
let mut rss = 0.0;
for i in 0..m {
let row = &x_data[i * ncols..(i + 1) * ncols];
let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
let resid = y[i] - y_hat;
rss += resid * resid;
}
let df = m - ncols;
if df == 0 {
return None;
}
let mse = rss / df as f64;
let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
for j in 0..ncols {
xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
}
xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
}
for col in 0..ncols {
let mut max_row = col;
let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
for row in (col + 1)..ncols {
let val = xtx_aug[row * 2 * ncols + col].abs();
if val > max_val {
max_val = val;
max_row = row;
}
}
if max_val < 1e-15 {
return None;
}
if max_row != col {
for j in 0..(2 * ncols) {
let a = col * 2 * ncols + j;
let b = max_row * 2 * ncols + j;
xtx_aug.swap(a, b);
}
}
let pivot = xtx_aug[col * 2 * ncols + col];
for j in 0..(2 * ncols) {
xtx_aug[col * 2 * ncols + j] /= pivot;
}
for row in 0..ncols {
if row == col {
continue;
}
let factor = xtx_aug[row * 2 * ncols + col];
for j in 0..(2 * ncols) {
let above = xtx_aug[col * 2 * ncols + j];
xtx_aug[row * 2 * ncols + j] -= factor * above;
}
}
}
let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
if var_gamma <= 0.0 {
return None;
}
let se_gamma = var_gamma.sqrt();
let t_gamma = beta[gamma_col] / se_gamma;
Some((t_gamma, rss, ncols))
}
fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
Some((aic, m))
}
fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
let critical_values = adf_critical_values(model, m);
let rejected = [
gamma_t <= critical_values[0],
gamma_t <= critical_values[1],
gamma_t <= critical_values[2],
];
Some(AdfResult {
statistic: gamma_t,
n_lags: p,
n_obs: m,
critical_values,
rejected,
})
}
fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
AdfModel::None => (
[-2.5658, -1.9393, -1.6156],
[-1.960, -0.398, -0.181],
[-10.04, 0.0, 0.0],
),
AdfModel::Constant => (
[-3.4336, -2.8621, -2.5671],
[-5.999, -2.738, -1.438],
[-29.25, -8.36, -4.48],
),
AdfModel::ConstantTrend => (
[-3.9638, -3.4126, -3.1279],
[-8.353, -4.039, -2.418],
[-47.44, -17.83, -7.58],
),
};
let nf = n as f64;
let inv_n = 1.0 / nf;
let inv_n2 = inv_n * inv_n;
[
tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_sample_null_true() {
let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
let r = one_sample_t_test(&data, 5.0).expect("should compute");
assert!(r.p_value > 0.3, "p = {}", r.p_value);
}
#[test]
fn one_sample_null_false() {
let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
let r = one_sample_t_test(&data, 10.0).expect("should compute");
assert!(r.p_value < 0.001, "p = {}", r.p_value);
}
#[test]
fn one_sample_edge_cases() {
assert!(one_sample_t_test(&[1.0], 0.0).is_none()); assert!(one_sample_t_test(&[5.0, 5.0, 5.0], 5.0).is_none()); assert!(one_sample_t_test(&[1.0, f64::NAN, 3.0], 2.0).is_none());
}
#[test]
fn two_sample_same_mean() {
let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
let r = two_sample_t_test(&a, &b).expect("should compute");
assert!(r.p_value > 0.3, "p = {}", r.p_value);
}
#[test]
fn two_sample_different_means() {
let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
let r = two_sample_t_test(&a, &b).expect("should compute");
assert!(r.p_value < 0.001, "p = {}", r.p_value);
}
#[test]
fn two_sample_different_sizes() {
let a = [1.0, 2.0, 3.0];
let b = [4.0, 5.0, 6.0, 7.0, 8.0];
let r = two_sample_t_test(&a, &b).expect("should compute");
assert!(r.p_value < 0.05);
}
#[test]
fn two_sample_edge_cases() {
assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
}
#[test]
fn paired_no_difference() {
let x = [5.0, 6.0, 7.0, 8.0, 9.0];
let y = [5.1, 5.9, 7.1, 7.9, 9.1];
let r = paired_t_test(&x, &y).expect("should compute");
assert!(r.p_value > 0.3, "p = {}", r.p_value);
}
#[test]
fn paired_significant_difference() {
let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
let r = paired_t_test(&before, &after).expect("should compute");
assert!(r.p_value < 0.001, "p = {}", r.p_value);
assert!(r.statistic < 0.0); }
#[test]
fn paired_edge_cases() {
assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
#[test]
fn anova_same_means() {
let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
assert!(r.p_value > 0.3, "p = {}", r.p_value);
}
#[test]
fn anova_different_means() {
let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
assert!(r.p_value < 0.001, "p = {}", r.p_value);
assert!(r.df_between == 2);
assert!(r.df_within == 12);
}
#[test]
fn anova_ss_decomposition() {
let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
let r = one_way_anova(&[&g1, &g2]).expect("should compute");
let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
assert!(
(ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
"SS decomposition: {ss_total} vs {} + {}",
r.ss_between,
r.ss_within
);
}
#[test]
fn anova_edge_cases() {
let g1 = [1.0, 2.0, 3.0];
assert!(one_way_anova(&[&g1]).is_none()); }
#[test]
fn chi2_gof_uniform() {
let observed = [25.0, 25.0, 25.0, 25.0];
let expected = [25.0, 25.0, 25.0, 25.0];
let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
assert!((r.statistic).abs() < 1e-15);
assert!((r.p_value - 1.0).abs() < 0.01);
}
#[test]
fn chi2_gof_significant() {
let observed = [90.0, 10.0];
let expected = [50.0, 50.0];
let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
assert!(r.p_value < 0.001, "p = {}", r.p_value);
}
#[test]
fn chi2_gof_edge_cases() {
assert!(chi_squared_goodness_of_fit(&[10.0], &[10.0]).is_none()); assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0, 0.0]).is_none()); assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0]).is_none()); }
#[test]
fn chi2_independence_significant() {
let table = [30.0, 10.0, 10.0, 50.0];
let r = chi_squared_independence(&table, 2, 2).expect("should compute");
assert!(r.p_value < 0.001, "p = {}", r.p_value);
assert!((r.df - 1.0).abs() < 1e-10);
}
#[test]
fn chi2_independence_not_significant() {
let table = [25.0, 25.0, 25.0, 25.0];
let r = chi_squared_independence(&table, 2, 2).expect("should compute");
assert!(r.p_value > 0.3, "p = {}", r.p_value);
}
#[test]
fn chi2_independence_3x3() {
let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
let r = chi_squared_independence(&table, 3, 3).expect("should compute");
assert!((r.df - 4.0).abs() < 1e-10);
assert!(r.p_value < 0.05);
}
#[test]
fn chi2_independence_edge_cases() {
assert!(chi_squared_independence(&[10.0, 20.0], 1, 2).is_none()); assert!(chi_squared_independence(&[10.0, 20.0], 2, 1).is_none()); assert!(chi_squared_independence(&[10.0], 2, 2).is_none()); }
#[test]
fn jb_normal_data() {
let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
let r = jarque_bera_test(&data).expect("should compute");
assert!(r.p_value > 0.05, "p = {}", r.p_value);
}
#[test]
fn jb_skewed_data() {
let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
let r = jarque_bera_test(&data).expect("should compute");
assert!(r.p_value < 0.05, "p = {}", r.p_value);
}
#[test]
fn jb_edge_cases() {
assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
#[test]
fn ad_normal_data() {
let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
let r = anderson_darling_test(&data).expect("should compute");
assert!(r.p_value > 0.05, "p = {}", r.p_value);
assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
}
#[test]
fn ad_skewed_data() {
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];
let r = anderson_darling_test(&data).expect("should compute");
assert!(
r.p_value < 0.05,
"p = {} (should reject normality)",
r.p_value
);
}
#[test]
fn ad_bimodal_data() {
let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
let r = anderson_darling_test(&data).expect("should compute");
assert!(
r.p_value < 0.01,
"p = {} (bimodal should reject normality)",
r.p_value
);
}
#[test]
fn ad_large_normal_sample() {
let n = 100;
let data: Vec<f64> = (1..=n)
.map(|i| {
let p = (i as f64 - 0.5) / n as f64;
special::inverse_normal_cdf(p)
})
.collect();
let r = anderson_darling_test(&data).expect("should compute");
assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
}
#[test]
fn ad_edge_cases() {
assert!(anderson_darling_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); assert!(anderson_darling_test(&[5.0; 10]).is_none()); assert!(anderson_darling_test(&[1.0, f64::NAN, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).is_none());
}
#[test]
fn ad_p_value_ranges() {
let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
let r = anderson_darling_test(&near_normal).expect("should compute");
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
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];
let r = anderson_darling_test(&heavy_tail).expect("should compute");
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
}
#[test]
fn ad_normal_data_large_p() {
let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
let r = anderson_darling_normality(&data).unwrap();
assert!(r.p_value > 0.05, "p={}", r.p_value);
}
#[test]
fn ad_exponential_data_small_p() {
let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
let r = anderson_darling_normality(&data).unwrap();
assert!(r.p_value < 0.05, "p={}", r.p_value);
}
#[test]
fn ad_statistic_non_negative() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let r = anderson_darling_normality(&data).unwrap();
assert!(r.statistic >= 0.0);
assert!(r.statistic_modified >= r.statistic);
}
#[test]
fn ad_p_value_in_range() {
let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
let r = anderson_darling_normality(&data).unwrap();
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
}
#[test]
fn ad_insufficient_data() {
assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
#[test]
fn ad_degenerate_data() {
assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
}
#[test]
fn ad_modified_statistic_formula() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
let r = anderson_darling_normality(&data).unwrap();
assert!(r.statistic_modified > r.statistic - 1e-10);
}
#[test]
fn ad_correction_factor_formula() {
let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
let n = data.len() as f64;
let r = anderson_darling_normality(&data).unwrap();
let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
assert!(
(r.statistic_modified - expected_star).abs() < 1e-12,
"A²* = {}, expected = {}",
r.statistic_modified,
expected_star
);
assert!(
r.statistic >= 0.0,
"A² = {} must be non-negative",
r.statistic
);
}
#[test]
fn ad_formula_structure_symmetric_vs_skewed() {
let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
let r_sym = anderson_darling_normality(&symmetric).unwrap();
let r_skew = anderson_darling_normality(&skewed).unwrap();
assert!(
r_sym.statistic < r_skew.statistic,
"Symmetric A²={} should be less than skewed A²={}",
r_sym.statistic,
r_skew.statistic
);
}
#[test]
fn ad_pvalue_invariants() {
let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
let r_normal = anderson_darling_normality(&normal_data).unwrap();
assert!(
r_normal.p_value > 0.05,
"Normal data: p = {} (expected > 0.05)",
r_normal.p_value
);
let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
let r_exp = anderson_darling_normality(&exp_data).unwrap();
assert!(
r_exp.p_value < 0.05,
"Exponential data: p = {} (expected < 0.05)",
r_exp.p_value
);
}
#[test]
fn sw_normal_data() {
let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
let r = shapiro_wilk_test(&data).expect("should compute");
assert!(r.w > 0.9, "W = {}", r.w);
assert!(r.p_value > 0.05, "p = {}", r.p_value);
}
#[test]
fn sw_bimodal_data() {
let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
let r = shapiro_wilk_test(&data).expect("should compute");
assert!(
r.p_value < 0.01,
"p = {} (bimodal should reject normality)",
r.p_value
);
}
#[test]
fn sw_n3() {
let data = [1.0, 2.0, 3.0];
let r = shapiro_wilk_test(&data).expect("n=3 should work");
assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
#[test]
fn sw_n4() {
let data = [1.0, 2.0, 3.0, 4.0];
let r = shapiro_wilk_test(&data).expect("n=4 should work");
assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
#[test]
fn sw_n5() {
let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
let r = shapiro_wilk_test(&data).expect("n=5 should work");
assert!(r.w > 0.9, "W = {}", r.w);
assert!(r.p_value > 0.05, "p = {}", r.p_value);
}
#[test]
fn sw_skewed_data() {
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];
let r = shapiro_wilk_test(&data).expect("should compute");
assert!(
r.p_value < 0.05,
"p = {} (skewed data should reject normality)",
r.p_value
);
}
#[test]
fn sw_large_normal_sample() {
let n = 100;
let data: Vec<f64> = (1..=n)
.map(|i| {
let p = (i as f64 - 0.5) / n as f64;
special::inverse_normal_cdf(p)
})
.collect();
let r = shapiro_wilk_test(&data).expect("should compute");
assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
assert!(r.p_value > 0.05, "p = {}", r.p_value);
}
#[test]
fn sw_w_bounded() {
let datasets: Vec<Vec<f64>> = vec![
vec![1.0, 2.0, 3.0],
vec![1.0, 1.0, 2.0, 3.0, 3.0],
vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
(0..20).map(|i| (i as f64).powi(2)).collect(),
];
for (idx, data) in datasets.iter().enumerate() {
let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
assert!(
r.p_value >= 0.0 && r.p_value <= 1.0,
"dataset {idx}: p = {}",
r.p_value
);
}
}
#[test]
fn sw_edge_cases() {
assert!(shapiro_wilk_test(&[1.0, 2.0]).is_none()); assert!(shapiro_wilk_test(&[]).is_none()); assert!(shapiro_wilk_test(&[5.0, 5.0, 5.0]).is_none()); assert!(shapiro_wilk_test(&[1.0, f64::NAN, 3.0]).is_none()); assert!(shapiro_wilk_test(&[1.0, f64::INFINITY, 3.0]).is_none()); }
#[test]
fn sw_n5001_rejected() {
let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
assert!(shapiro_wilk_test(&data).is_none()); }
#[test]
fn mw_clearly_different() {
let a = [1.0, 2.0, 3.0, 4.0, 5.0];
let b = [6.0, 7.0, 8.0, 9.0, 10.0];
let r = mann_whitney_u_test(&a, &b).expect("should compute");
assert!(r.p_value < 0.05, "p = {}", r.p_value);
}
#[test]
fn mw_same_distribution() {
let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
let r = mann_whitney_u_test(&a, &b).expect("should compute");
assert!(
r.p_value > 0.3,
"p = {} (interleaved, same dist)",
r.p_value
);
}
#[test]
fn mw_with_ties() {
let a = [1.0, 2.0, 2.0, 3.0, 3.0];
let b = [3.0, 4.0, 4.0, 5.0, 5.0];
let r = mann_whitney_u_test(&a, &b).expect("should compute");
assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
}
#[test]
fn mw_different_sizes() {
let a = [1.0, 2.0, 3.0];
let b = [4.0, 5.0, 6.0, 7.0, 8.0];
let r = mann_whitney_u_test(&a, &b).expect("should compute");
assert!(r.p_value < 0.05);
}
#[test]
fn mw_edge_cases() {
assert!(mann_whitney_u_test(&[1.0], &[2.0, 3.0]).is_none()); assert!(mann_whitney_u_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(mann_whitney_u_test(&[1.0, f64::NAN], &[2.0, 3.0]).is_none());
}
#[test]
fn wilcoxon_significant_increase() {
let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
}
#[test]
fn wilcoxon_no_difference() {
let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
}
#[test]
fn wilcoxon_with_ties() {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
assert!(r.statistic > 0.0);
}
#[test]
fn wilcoxon_with_zero_diffs() {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
}
#[test]
fn wilcoxon_edge_cases() {
assert!(wilcoxon_signed_rank_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(wilcoxon_signed_rank_test(&[1.0], &[2.0]).is_none()); assert!(wilcoxon_signed_rank_test(&[5.0, 5.0], &[5.0, 5.0]).is_none());
}
#[test]
fn kw_clearly_different() {
let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
assert!(r.p_value < 0.01, "p = {}", r.p_value);
assert!((r.df - 2.0).abs() < 1e-10);
}
#[test]
fn kw_same_distribution() {
let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
}
#[test]
fn kw_with_ties() {
let g1 = [1.0, 2.0, 2.0, 3.0];
let g2 = [3.0, 4.0, 4.0, 5.0];
let g3 = [5.0, 6.0, 6.0, 7.0];
let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
assert!(r.statistic > 0.0);
}
#[test]
fn kw_edge_cases() {
let g1 = [1.0, 2.0, 3.0];
assert!(kruskal_wallis_test(&[&g1]).is_none()); }
#[test]
fn levene_equal_variance() {
let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
let r = levene_test(&[&g1, &g2]).expect("should compute");
assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
}
#[test]
fn levene_unequal_variance() {
let g1 = [4.5, 4.8, 5.0, 5.2, 5.5]; let g2 = [0.0, 2.0, 5.0, 8.0, 10.0]; let r = levene_test(&[&g1, &g2]).expect("should compute");
assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
}
#[test]
fn levene_three_groups() {
let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
assert!(r.df >= 2.0);
}
#[test]
fn levene_edge_cases() {
let g1 = [1.0, 2.0, 3.0];
assert!(levene_test(&[&g1]).is_none()); }
#[test]
fn bonferroni_basic() {
let ps = [0.01, 0.04, 0.03, 0.005];
let adj = bonferroni_correction(&ps).expect("should compute");
assert!((adj[0] - 0.04).abs() < 1e-10);
assert!((adj[1] - 0.16).abs() < 1e-10);
assert!((adj[2] - 0.12).abs() < 1e-10);
assert!((adj[3] - 0.02).abs() < 1e-10);
}
#[test]
fn bonferroni_capped_at_one() {
let ps = [0.5, 0.6];
let adj = bonferroni_correction(&ps).expect("should compute");
assert!((adj[0] - 1.0).abs() < 1e-10);
assert!((adj[1] - 1.0).abs() < 1e-10);
}
#[test]
fn bh_basic() {
let ps = [0.01, 0.04, 0.03, 0.005];
let adj = benjamini_hochberg(&ps).expect("should compute");
for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
assert!(
adjusted >= orig - 1e-15,
"adj[{i}] = {adjusted} < original {orig}"
);
}
}
#[test]
fn bh_all_significant() {
let ps = [0.001, 0.002, 0.003];
let adj = benjamini_hochberg(&ps).expect("should compute");
for &a in &adj {
assert!(a < 0.05);
}
}
#[test]
fn correction_edge_cases() {
assert!(bonferroni_correction(&[]).is_none());
assert!(benjamini_hochberg(&[]).is_none());
assert!(bonferroni_correction(&[f64::NAN]).is_none());
}
#[test]
fn bartlett_equal_variances() {
let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
let r = bartlett_test(&[&g1, &g2]).expect("should compute");
assert!(
r.p_value > 0.9,
"equal variance → p high, got {}",
r.p_value
);
}
#[test]
fn bartlett_unequal_variances() {
let g1 = [2.0, 3.0, 4.0, 5.0, 6.0]; let g2 = [10.0, 20.0, 30.0, 40.0, 50.0]; let r = bartlett_test(&[&g1, &g2]).expect("should compute");
assert!(
r.p_value < 0.01,
"very different variances → p < 0.01, got {}",
r.p_value
);
assert!((r.df - 1.0).abs() < 1e-10); }
#[test]
fn bartlett_three_groups() {
let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
assert!(r.p_value < 0.05, "one group with high variance");
assert!((r.df - 2.0).abs() < 1e-10); }
#[test]
fn bartlett_edge_cases() {
let g1 = [1.0, 2.0, 3.0];
assert!(bartlett_test(&[&g1]).is_none());
let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
}
#[test]
fn fisher_tea_tasting() {
let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
assert!(
(r.p_value - 0.4857).abs() < 0.01,
"p ≈ 0.4857, got {}",
r.p_value
);
}
#[test]
fn fisher_significant() {
let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
assert!(r.p_value < 0.001, "perfect association → p very small");
}
#[test]
fn fisher_no_association() {
let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
assert!(
r.p_value > 0.9,
"no association → p ≈ 1.0, got {}",
r.p_value
);
}
#[test]
fn fisher_small_table() {
let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
assert!(r.p_value > 0.0 && r.p_value <= 1.0);
}
#[test]
fn fisher_asymmetric() {
let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
assert!(r.p_value < 0.05, "significant association");
}
#[test]
fn fisher_edge_cases() {
assert!(fisher_exact_test(0, 0, 1, 2).is_none()); assert!(fisher_exact_test(1, 2, 0, 0).is_none()); assert!(fisher_exact_test(0, 1, 0, 2).is_none()); }
#[test]
fn fisher_odds_ratio() {
let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
assert!(
(r.statistic - 9.0).abs() < 1e-10,
"OR = 9, got {}",
r.statistic
);
}
#[test]
fn mk_increasing_trend() {
let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
let r = mann_kendall_test(&data).expect("should compute");
assert!(r.p_value < 0.01, "p = {}", r.p_value);
assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
assert!(r.s_statistic > 0);
}
#[test]
fn mk_decreasing_trend() {
let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
let r = mann_kendall_test(&data).expect("should compute");
assert!(r.p_value < 0.01, "p = {}", r.p_value);
assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
assert!(r.s_statistic < 0);
}
#[test]
fn mk_no_trend() {
let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
let r = mann_kendall_test(&data).expect("should compute");
assert!(
r.p_value > 0.05,
"p = {} (should be > 0.05 for no trend)",
r.p_value
);
}
#[test]
fn mk_perfect_monotone() {
let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
let r = mann_kendall_test(&data).expect("should compute");
assert_eq!(r.s_statistic, 45);
assert!((r.kendall_tau - 1.0).abs() < 1e-10);
assert!((r.sen_slope - 1.0).abs() < 1e-10);
}
#[test]
fn mk_with_ties() {
let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
let r = mann_kendall_test(&data).expect("should compute");
assert!(r.s_statistic > 0);
let n = data.len() as f64;
let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
assert!(r.variance < base_var, "ties should reduce variance");
}
#[test]
fn mk_edge_cases() {
assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
}
#[test]
fn mk_minimum_n() {
let data = [1.0, 2.0, 3.0, 4.0];
let r = mann_kendall_test(&data).expect("n=4 should work");
assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
}
#[test]
fn mk_sen_slope_robust_to_outlier() {
let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let r = mann_kendall_test(&data).expect("should compute");
assert!(
(r.sen_slope - 1.0).abs() < 0.5,
"sen slope = {}, expected ≈ 1.0",
r.sen_slope
);
}
#[test]
fn adf_stationary_mean_reverting() {
let data = [
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,
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,
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,
];
let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
assert!(
r.rejected[2],
"should reject at 10%: stat={}, cv={}",
r.statistic, r.critical_values[2]
);
}
#[test]
fn adf_nonstationary_random_walk() {
let increments = [
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,
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,
-0.13, 0.15, -0.1,
];
let mut walk = Vec::with_capacity(increments.len());
let mut cum = 0.0;
for &inc in &increments {
cum += inc;
walk.push(cum);
}
let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
assert!(
!r.rejected[0],
"should NOT reject at 1%: stat={}, cv={}",
r.statistic, r.critical_values[0]
);
}
#[test]
fn adf_with_fixed_lags() {
let data: Vec<f64> = (0..50)
.map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
.collect();
let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
assert_eq!(r.n_lags, 2);
assert!(r.statistic.is_finite());
}
#[test]
fn adf_constant_trend_model() {
let data: Vec<f64> = (0..30)
.map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
.collect();
let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
assert!(r.statistic.is_finite());
assert_eq!(r.critical_values.len(), 3);
}
#[test]
fn adf_edge_cases() {
assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
let mut data = vec![0.0; 20];
data[5] = f64::NAN;
assert!(adf_test(&data, AdfModel::Constant, None).is_none());
}
#[test]
fn adf_critical_values_constant() {
let cv = adf_critical_values(AdfModel::Constant, 100);
assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
}
#[test]
fn adf_critical_values_ordering() {
let cv = adf_critical_values(AdfModel::Constant, 50);
assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn one_sample_p_bounded(
data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
mu0 in -1e3_f64..1e3
) {
if let Some(r) = one_sample_t_test(&data, mu0) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
}
#[test]
fn two_sample_p_bounded(
a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
) {
if let Some(r) = two_sample_t_test(&a, &b) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
}
#[test]
fn anova_p_bounded(
g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
) {
if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
}
}
#[test]
fn bonferroni_monotone(
ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
) {
let adj = bonferroni_correction(&ps).expect("should compute");
for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
prop_assert!(adjusted >= orig - 1e-15,
"adj[{i}] = {adjusted} < orig = {orig}");
}
}
#[test]
fn shapiro_wilk_p_bounded(
data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
) {
if let Some(r) = shapiro_wilk_test(&data) {
prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
}
#[test]
fn anderson_darling_p_bounded(
data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
) {
if let Some(r) = anderson_darling_test(&data) {
prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
}
#[test]
fn mann_whitney_p_bounded(
a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
) {
if let Some(r) = mann_whitney_u_test(&a, &b) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
}
}
#[test]
fn wilcoxon_p_bounded(
diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
) {
let zeros: Vec<f64> = vec![0.0; diffs.len()];
if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
}
#[test]
fn bartlett_p_bounded(
g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
) {
if let Some(r) = bartlett_test(&[&g1, &g2]) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
}
}
#[test]
fn fisher_p_bounded(
a in 0_u64..20,
b in 0_u64..20,
c in 0_u64..20,
d in 0_u64..20,
) {
if let Some(r) = fisher_exact_test(a, b, c, d) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
}
}
#[test]
fn mann_kendall_p_bounded(
data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
) {
if let Some(r) = mann_kendall_test(&data) {
prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
"tau = {}", r.kendall_tau);
}
}
#[test]
fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
let r = mann_kendall_test(&data).expect("should compute");
prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
"tau should be 1.0 for monotone, got {}", r.kendall_tau);
prop_assert!(r.sen_slope > 0.0, "slope should be positive");
}
}
}