use std::f64::consts::LN_10;
use symplex::linprog::{q, qi};
use symplex::prelude::*;
use symplex::stats::PValue;
use symplex::stats::agreement::{RatingTable, cochrans_q};
use symplex::stats::anova::{
AnovaResult, Mauchly, RepeatedMeasuresAnova, TwoWayData, anova_one_way,
anova_repeated_measures, anova_two_way,
};
use symplex::stats::data::from_i64;
use symplex::stats::hypothesis::{
Alternative, ChiSquareResult, TestResult, binomial_test, chi_square_independence, pearson_test,
t_test_one_sample, z_test_proportion,
};
use symplex::stats::regression::ols;
use symplex::stats::survival::{Observation, log_rank_test};
fn close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() < tol,
"got {actual}, expected {expected} (tol {tol})"
);
}
fn mantissa_exponent(s: &str) -> (f64, i32) {
match s.split_once('e') {
Some((m, e)) => (m.parse().unwrap(), e.parse().unwrap()),
None => (s.parse().unwrap(), 0),
}
}
fn log10_from_decimal(s: &str) -> f64 {
let (m, e) = mantissa_exponent(s);
m.log10() + f64::from(e)
}
fn assert_decimal(s: &str, prefix: &str, exponent: i32) {
let (mantissa, e) = match s.split_once('e') {
Some((m, e)) => (m, e.parse::<i32>().unwrap()),
None => (s, 0),
};
assert!(
mantissa.starts_with(prefix),
"decimal {s}: mantissa does not start with {prefix}"
);
assert_eq!(e, exponent, "decimal {s}: exponent");
}
fn is_invalid_argument(e: &SymplexError) -> bool {
matches!(e, SymplexError::InvalidArgument { .. })
}
fn extreme_table() -> Vec<Vec<Q>> {
vec![from_i64(&[9000, 1000]), from_i64(&[1000, 9000])]
}
fn extreme_groups() -> Vec<Vec<Q>> {
(0..3)
.map(|i| (0..50).map(|j| qi(1000 * i + (j % 2))).collect())
.collect()
}
fn extreme_repeated() -> Vec<Vec<Q>> {
(0..30i64)
.map(|i| {
let noise = [i % 3, (i * i) % 5, (7 * i) % 4];
(0..3i64)
.map(|j| qi(1_000_000 * j + noise[j as usize] + i))
.collect()
})
.collect()
}
fn extreme_regression() -> (Vec<Q>, Vec<Vec<Q>>) {
let x: Vec<Vec<Q>> = (0..80i64).map(|i| vec![qi(i)]).collect();
let y: Vec<Q> = (0..80i64).map(|i| qi(1000 * i + (i % 2))).collect();
(y, x)
}
fn rm3() -> Vec<Vec<Q>> {
vec![
from_i64(&[5, 7, 9]),
from_i64(&[4, 5, 8]),
from_i64(&[6, 8, 10]),
from_i64(&[3, 6, 4]),
from_i64(&[7, 9, 13]),
]
}
#[test]
fn chi_square_book_example() {
let ctx = Context::new();
let r = chi_square_independence(&ctx, &extreme_table(), false).unwrap();
assert_eq!(r.statistic, qi(12800));
assert_eq!(r.df, 1);
assert_eq!(r.p_value.to_string(), "uppergamma(1/2, 6400)/Gamma(1/2)");
assert_eq!(r.p_value_f64().unwrap(), 0.0);
close(r.p_value_log10().unwrap(), -2_781.636_383_026_783, 1e-9);
close(r.p_value_ln().unwrap(), -6_404.954_469_687_346, 1e-9);
assert_eq!(
r.p_value_decimal(20).unwrap(),
"2.3100265595063985852e-2782"
);
assert_eq!(r.p_value_decimal(5).unwrap(), "2.31e-2782");
}
#[test]
fn chi_square_inherent_equals_trait() {
let ctx = Context::new();
let r = chi_square_independence(&ctx, &extreme_table(), false).unwrap();
assert_eq!(<ChiSquareResult as PValue>::p_value_ex(&r), &r.p_value);
assert_eq!(
r.p_value_log10().unwrap(),
PValue::p_value_log10(&r).unwrap()
);
assert_eq!(r.p_value_ln().unwrap(), PValue::p_value_ln(&r).unwrap());
assert_eq!(
r.p_value_decimal(12).unwrap(),
PValue::p_value_decimal(&r, 12).unwrap()
);
assert_eq!(r.p_value_f64().unwrap(), PValue::p_value_f64(&r).unwrap());
}
#[test]
fn log10_agrees_with_decimal_exponent_path() {
let ctx = Context::new();
let chi = chi_square_independence(&ctx, &extreme_table(), false).unwrap();
let via_ln = chi.p_value_log10().unwrap();
let via_decimal = log10_from_decimal(&chi.p_value_decimal(20).unwrap());
close(via_ln, via_decimal, 1e-9);
close(chi.p_value_ln().unwrap(), via_ln * LN_10, 1e-9);
let z = z_test_proportion(&ctx, 9000, 10000, &q(1, 2), Alternative::TwoSided).unwrap();
close(
z.p_value_log10().unwrap(),
log10_from_decimal(&z.p_value_decimal(20).unwrap()),
1e-9,
);
let x = from_i64(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let t = t_test_one_sample(&ctx, &x, &qi(5), Alternative::TwoSided).unwrap();
let s = t.p_value_decimal(20).unwrap();
assert!(!s.contains('e'), "{s}");
close(t.p_value_log10().unwrap(), log10_from_decimal(&s), 1e-12);
}
#[test]
fn binomial_exact_rational_tiny_p() {
let ctx = Context::new();
let r = binomial_test(&ctx, 0, 2000, &q(1, 2), Alternative::Less).unwrap();
assert!(r.p_value_exact().is_some(), "p should be an exact rational");
assert_eq!(r.p_value_f64().unwrap(), 0.0);
close(r.p_value_log10().unwrap(), -602.059_991_327_962_4, 1e-9);
close(r.p_value_ln().unwrap(), -1_386.294_361_119_890_6, 1e-9);
close(
r.p_value_ln().unwrap(),
-2000.0 * std::f64::consts::LN_2,
1e-9,
);
assert_decimal(&r.p_value_decimal(20).unwrap(), "8.70980981621721", -603);
}
#[test]
fn z_test_erfc_tiny_p() {
let ctx = Context::new();
let r = z_test_proportion(&ctx, 9000, 10000, &q(1, 2), Alternative::TwoSided).unwrap();
close(r.statistic_f64().unwrap(), 80.0, 1e-12);
assert_eq!(r.p_value_f64().unwrap(), 0.0);
close(r.p_value_log10().unwrap(), -1_391.743_559_847_938_8, 1e-9);
close(r.p_value_ln().unwrap(), -3_204.607_974_176_330_4, 1e-9);
assert_decimal(&r.p_value_decimal(20).unwrap(), "1.80484600324094", -1392);
}
#[test]
fn ordinary_p_matches_f64_logarithms() {
let ctx = Context::new();
let x = from_i64(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let r = t_test_one_sample(&ctx, &x, &qi(5), Alternative::TwoSided).unwrap();
let p = r.p_value_f64().unwrap();
close(p, 0.614_117_254_808_393_9, 1e-12);
close(r.p_value_log10().unwrap(), p.log10(), 1e-12);
close(r.p_value_log10().unwrap(), -0.211_748_700_105_543_6, 1e-12);
close(r.p_value_ln().unwrap(), p.ln(), 1e-12);
assert_eq!(r.p_value_decimal(5).unwrap(), "0.61412");
assert_eq!(r.p_value_decimal(20).unwrap(), "0.61411725480839390773");
let table = vec![from_i64(&[10, 20]), from_i64(&[30, 25])];
let c = chi_square_independence(&ctx, &table, false).unwrap();
let pc = c.p_value_f64().unwrap();
close(pc, 0.061_150_897_576_067_77, 1e-12);
close(c.p_value_log10().unwrap(), pc.log10(), 1e-12);
close(c.p_value_log10().unwrap(), -1.213_597_163_983_828_8, 1e-12);
close(c.p_value_ln().unwrap(), pc.ln(), 1e-12);
}
#[test]
fn exact_zero_p_value() {
let ctx = Context::new();
let x = from_i64(&[1, 2, 3, 4]);
let y = from_i64(&[2, 4, 6, 8]);
let r = pearson_test(&ctx, &x, &y, Alternative::TwoSided).unwrap();
assert_eq!(r.p_value_ex(), &ctx.zero());
assert_eq!(r.p_value_exact(), Some(qi(0)));
assert_eq!(r.p_value_f64().unwrap(), 0.0);
assert_eq!(r.p_value_log10().unwrap(), f64::NEG_INFINITY);
assert_eq!(r.p_value_ln().unwrap(), f64::NEG_INFINITY);
assert_eq!(r.p_value_decimal(10).unwrap(), "0");
let built = TestResult {
statistic: ctx.int(1),
p_value: ctx.zero(),
df: None,
alternative: Alternative::Greater,
};
assert_eq!(PValue::p_value_log10(&built).unwrap(), f64::NEG_INFINITY);
}
#[test]
fn exact_one_p_value() {
let ctx = Context::new();
let x = from_i64(&[1, 2, 3, 4]);
let y = from_i64(&[2, 4, 6, 8]);
let r = pearson_test(&ctx, &x, &y, Alternative::Less).unwrap();
assert_eq!(r.p_value_ex(), &ctx.one());
assert_eq!(r.p_value_log10().unwrap(), 0.0);
assert_eq!(r.p_value_ln().unwrap(), 0.0);
assert_eq!(r.p_value_decimal(10).unwrap(), "1");
let b = binomial_test(&ctx, 5, 10, &q(1, 2), Alternative::TwoSided).unwrap();
assert_eq!(b.p_value_exact(), Some(qi(1)));
assert_eq!(b.p_value_f64().unwrap(), 1.0);
assert_eq!(b.p_value_log10().unwrap(), 0.0);
assert_eq!(b.p_value_ln().unwrap(), 0.0);
}
#[test]
fn test_results_from_other_modules() {
let ctx = Context::new();
let obs = Observation::from_i64(
&[3, 5, 6, 7, 8, 10, 12, 12, 4, 9, 11, 13, 15, 16, 18, 20],
&[
true, false, true, true, false, true, true, false, true, true, false, true, true, true,
false, true,
],
);
let groups = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];
let lr = log_rank_test(&ctx, &obs, &groups).unwrap();
close(lr.p_value_f64().unwrap(), 0.079_572_501_549_774_13, 1e-12);
close(lr.p_value_log10().unwrap(), -1.099_236_988_650_168_3, 1e-12);
close(
lr.p_value_ln().unwrap(),
lr.p_value_f64().unwrap().ln(),
1e-12,
);
let t = RatingTable::from_i64(&[
&[1, 1, 0],
&[1, 1, 0],
&[1, 0, 0],
&[1, 1, 1],
&[0, 1, 0],
&[1, 0, 0],
&[1, 1, 0],
&[1, 1, 0],
&[0, 0, 0],
&[1, 1, 1],
&[1, 0, 0],
&[1, 1, 0],
])
.unwrap();
let cq = cochrans_q(&ctx, &t).unwrap();
close(cq.p_value_log10().unwrap(), -2.509_257_006_552_155, 1e-11);
assert_eq!(cq.p_value_decimal(6).unwrap(), "0.00309559");
}
#[test]
fn anova_one_way_tiny_p() {
let ctx = Context::new();
let r = anova_one_way(&ctx, &extreme_groups()).unwrap();
assert_eq!(r.f, qi(196_000_000));
assert_eq!((r.df_between, r.df_within), (2, 147));
assert_eq!(r.ss_between, qi(100_000_000));
assert_eq!(r.ss_within, q(75, 2));
assert_eq!(r.p_value_f64().unwrap(), 0.0);
close(r.p_value_log10().unwrap(), -472.308_713_792_252_1, 1e-9);
close(r.p_value_ln().unwrap(), -1_087.531_003_669_230_9, 1e-9);
assert_decimal(&r.p_value_decimal(20).unwrap(), "4.91231499754172", -473);
assert_eq!(
<AnovaResult as PValue>::p_value_log10(&r).unwrap(),
r.p_value_log10().unwrap()
);
}
#[test]
fn repeated_measures_tiny_p() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &extreme_repeated()).unwrap();
assert_eq!(r.f, q(1_740_000_870_000_435, 97));
assert_eq!(r.conditions.ss, qi(60_000_030_000_015));
assert_eq!(r.error.ss, qi(97));
assert_eq!(r.epsilon_gg, q(9409, 11810));
assert_eq!(r.epsilon_hf, Some(q(67615, 80918)));
assert_eq!(r.p_value_f64().unwrap(), 0.0);
close(r.p_value_log10().unwrap(), -341.950_012_264_696_5, 1e-9);
close(r.p_value_ln().unwrap(), -787.369_000_789_821_2, 1e-9);
assert_decimal(&r.p_value_decimal(20).unwrap(), "1.12198676837902", -342);
assert_eq!(
r.conditions.p_value_log10().unwrap(),
r.p_value_log10().unwrap()
);
assert_eq!(r.conditions.p_value_ln().unwrap(), r.p_value_ln().unwrap());
assert_eq!(
r.conditions.p_value_decimal(15).unwrap(),
r.p_value_decimal(15).unwrap()
);
let gg = r.p_value_gg_f64().unwrap();
assert!(gg > 0.0, "the GG-corrected p is representable: {gg}");
close(r.p_value_gg_log10().unwrap(), -272.776_978_439_357_66, 1e-9);
close(r.p_value_gg_log10().unwrap(), gg.log10(), 1e-9);
let hf = r.p_value_hf_log10().unwrap().unwrap();
close(hf, -286.013_648_111_672_08, 1e-9);
close(hf, r.p_value_hf_f64().unwrap().unwrap().log10(), 1e-9);
assert_eq!(
<RepeatedMeasuresAnova as PValue>::p_value_ex(&r),
&r.p_value
);
}
#[test]
fn mauchly_and_repeated_measures_ordinary_p() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
close(r.p_value_log10().unwrap(), -2.427_649_976_489_498, 1e-11);
close(
r.p_value_log10().unwrap(),
r.p_value_f64().unwrap().log10(),
1e-12,
);
close(
r.p_value_gg_log10().unwrap(),
-1.672_347_564_201_508_9,
1e-11,
);
let hf = r.p_value_hf_log10().unwrap().unwrap();
close(hf, r.p_value_hf_f64().unwrap().unwrap().log10(), 1e-12);
let m = r.mauchly.as_ref().unwrap();
assert_eq!(m.w, q(1245, 7921));
close(m.p_value_f64().unwrap(), 0.062_313_767_163_236_4, 1e-12);
close(m.p_value_log10().unwrap(), -1.205_415_992_787_105_5, 1e-11);
close(
m.p_value_ln().unwrap(),
m.p_value_f64().unwrap().ln(),
1e-12,
);
assert_eq!(m.p_value_decimal(4).unwrap(), "0.06231");
assert_eq!(<Mauchly as PValue>::p_value_ex(m), &m.p_value);
assert_eq!(
<Mauchly as PValue>::p_value_log10(m).unwrap(),
m.p_value_log10().unwrap()
);
}
#[test]
fn anova_row_accessors() {
let ctx = Context::new();
let data = TwoWayData::from_i64(&[
&[&[4, 5, 6], &[6, 7, 8], &[9, 10, 12]],
&[&[5, 5, 7], &[8, 9, 11], &[13, 14, 16]],
])
.unwrap();
let r = anova_two_way(&ctx, &data).unwrap();
close(
r.factor_b.p_value_log10().unwrap(),
-5.482_541_396_789_33,
1e-10,
);
close(
r.factor_b.p_value_ln().unwrap(),
r.factor_b.p_value_f64().unwrap().ln(),
1e-12,
);
assert_decimal(&r.factor_b.p_value_decimal(6).unwrap(), "3.29199", -6);
for row in [&r.residual, &r.total] {
assert!(row.p_value.is_none());
assert!(is_invalid_argument(&row.p_value_f64().unwrap_err()));
assert!(is_invalid_argument(&row.p_value_log10().unwrap_err()));
assert!(is_invalid_argument(&row.p_value_ln().unwrap_err()));
assert!(is_invalid_argument(&row.p_value_decimal(10).unwrap_err()));
}
let msg = r.residual.p_value_log10().unwrap_err().to_string();
assert!(msg.contains("Residual row has no F test"), "{msg}");
}
#[test]
fn ols_p_values_log10() {
let ctx = Context::new();
let (y, x) = extreme_regression();
let fit = ols(&y, &x, true).unwrap();
assert_eq!(fit.coefficients, vec![q(13, 27), q(2_133_001, 2133)]);
assert_eq!(fit.ssr, q(42640, 2133));
let ps = fit.p_values(&ctx).unwrap();
assert_eq!(ps[1].eval_f64().unwrap(), 0.0);
let logs = fit.p_values_log10(&ctx).unwrap();
assert_eq!(logs.len(), 2);
close(logs[0], -4.299_706_263_138_431_5, 1e-11);
close(logs[0], ps[0].eval_f64().unwrap().log10(), 1e-11);
close(logs[1], -364.884_100_971_665_2, 1e-9);
let tests = fit.coefficient_tests(&ctx).unwrap();
close(tests[1].p_value_log10().unwrap(), logs[1], 1e-12);
assert_decimal(
&tests[1].p_value_decimal(20).unwrap(),
"1.30586724411883",
-365,
);
let x: Vec<Vec<Q>> = from_i64(&[1, 2, 3, 4, 5, 6, 7])
.into_iter()
.map(|v| vec![v])
.collect();
let y = from_i64(&[2, 3, 5, 4, 6, 8, 9]);
let fit = ols(&y, &x, true).unwrap();
let logs = fit.p_values_log10(&ctx).unwrap();
close(logs[1], -3.307_235_313_250_605_6, 1e-11);
let f = fit.f_test(&ctx).unwrap();
close(f.p_value_log10().unwrap(), logs[1], 1e-11); }
fn neg_log10<T: PValue + ?Sized>(r: &T) -> f64 {
-r.p_value_log10().unwrap()
}
#[test]
fn trait_is_generic_and_object_safe() {
let ctx = Context::new();
let chi = chi_square_independence(&ctx, &extreme_table(), false).unwrap();
let t = binomial_test(&ctx, 0, 2000, &q(1, 2), Alternative::Less).unwrap();
let a = anova_one_way(&ctx, &extreme_groups()).unwrap();
let rm = anova_repeated_measures(&ctx, &rm3()).unwrap();
let m = rm.mauchly.clone().unwrap();
close(neg_log10(&chi), 2_781.636_383_026_783, 1e-9);
close(neg_log10(&t), 602.059_991_327_962_4, 1e-9);
close(neg_log10(&a), 472.308_713_792_252_1, 1e-9);
close(neg_log10(&rm), 2.427_649_976_489_498, 1e-11);
close(neg_log10(&m), 1.205_415_992_787_105_5, 1e-11);
let all: Vec<&dyn PValue> = vec![&chi, &t, &a, &rm, &m];
let order: Vec<f64> = all.iter().map(|r| neg_log10(*r)).collect();
assert!(order.iter().all(|v| v.is_finite() && *v > 0.0));
let mut sorted = order.clone();
sorted.sort_by(f64::total_cmp);
assert_eq!(
sorted,
vec![order[4], order[3], order[2], order[1], order[0]]
);
}
#[test]
fn underflow_contract() {
let ctx = Context::new();
let floor = f64::MIN_POSITIVE.log10(); let tiny: Vec<Box<dyn PValue>> = vec![
Box::new(chi_square_independence(&ctx, &extreme_table(), false).unwrap()),
Box::new(binomial_test(&ctx, 0, 2000, &q(1, 2), Alternative::Less).unwrap()),
Box::new(z_test_proportion(&ctx, 9000, 10000, &q(1, 2), Alternative::TwoSided).unwrap()),
Box::new(anova_one_way(&ctx, &extreme_groups()).unwrap()),
Box::new(anova_repeated_measures(&ctx, &extreme_repeated()).unwrap()),
];
for r in &tiny {
assert_eq!(r.p_value_f64().unwrap(), 0.0);
let l = r.p_value_log10().unwrap();
assert!(l.is_finite() && l < floor, "log10 p = {l}");
let s = r.p_value_decimal(3).unwrap();
assert!(s.contains("e-"), "{s}");
}
}