use symplex::linprog::{q, qi};
use symplex::prelude::*;
use symplex::stats::anova::{
Adjustment, Observation, Source, SsType, TwoWayData, anova_one_way, anova_repeated_measures,
anova_two_way, anova_two_way_with, pairwise_t_tests, studentized_range_cdf,
studentized_range_quantile, studentized_range_sf, tukey_hsd,
};
use symplex::stats::data::from_i64;
fn close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() < tol,
"got {actual}, expected {expected} (tol {tol})"
);
}
fn ev(e: &Ex) -> f64 {
e.eval_f64().unwrap()
}
fn qf(x: &Q) -> f64 {
symplex::stats::data::to_f64(std::slice::from_ref(x))[0]
}
fn bal() -> TwoWayData {
TwoWayData::from_i64(&[
&[&[4, 5, 6], &[6, 7, 8], &[9, 10, 12]],
&[&[5, 5, 7], &[8, 9, 11], &[13, 14, 16]],
])
.unwrap()
}
fn unb() -> TwoWayData {
TwoWayData::from_i64(&[
&[&[4, 5, 6, 7], &[6, 8], &[9, 10, 12]],
&[&[5, 7], &[8, 9, 11, 10], &[13, 14, 16]],
])
.unwrap()
}
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]),
]
}
fn rm4() -> Vec<Vec<Q>> {
vec![
from_i64(&[3, 5, 6, 8]),
from_i64(&[2, 4, 4, 7]),
from_i64(&[5, 6, 8, 9]),
from_i64(&[4, 4, 7, 10]),
from_i64(&[3, 6, 5, 9]),
from_i64(&[6, 7, 9, 12]),
]
}
fn g3() -> Vec<Vec<Q>> {
vec![
from_i64(&[6, 8, 4, 5, 3, 4]),
from_i64(&[8, 12, 9, 11, 6, 8]),
from_i64(&[13, 9, 11, 8, 7, 12]),
]
}
fn g3u() -> Vec<Vec<Q>> {
vec![
from_i64(&[4, 5, 6, 7]),
from_i64(&[6, 8, 9]),
from_i64(&[9, 10, 12, 11, 13]),
]
}
#[test]
fn two_way_balanced_sums_of_squares_and_df_exact() {
let ctx = Context::new();
let r = anova_two_way(&ctx, &bal()).unwrap();
assert_eq!(r.ss_type, SsType::TypeII);
assert_eq!((r.factor_a.ss.clone(), r.factor_a.df), (q(49, 2), 1));
assert_eq!((r.factor_b.ss.clone(), r.factor_b.df), (q(1339, 9), 2));
assert_eq!((r.interaction.ss.clone(), r.interaction.df), (q(25, 3), 2));
assert_eq!((r.residual.ss.clone(), r.residual.df), (q(62, 3), 12));
assert_eq!((r.total.ss.clone(), r.total.df), (q(3641, 18), 17));
close(qf(&r.factor_b.ss), 148.777_777_777_777_86, 1e-12);
}
#[test]
fn two_way_balanced_f_statistics_and_p_values() {
let ctx = Context::new();
let r = anova_two_way(&ctx, &bal()).unwrap();
assert_eq!(r.factor_a.f, Some(q(441, 31)));
assert_eq!(r.factor_b.f, Some(q(1339, 31)));
assert_eq!(r.interaction.f, Some(q(75, 31)));
close(
r.factor_a.p_value_f64().unwrap(),
0.002_663_477_688_683_533_4,
1e-12,
);
close(
r.factor_b.p_value_f64().unwrap(),
3.291_990_727_040_258e-6,
1e-12,
);
close(
r.interaction.p_value_f64().unwrap(),
0.130_988_938_057_322_53,
1e-12,
);
close(
qf(r.factor_a.f.as_ref().unwrap()),
14.225_806_451_612_904,
1e-12,
);
}
#[test]
fn two_way_balanced_all_ss_types_coincide() {
let ctx = Context::new();
let t2 = anova_two_way_with(&ctx, &bal(), SsType::TypeII).unwrap();
for t in [SsType::TypeI, SsType::TypeIII] {
let r = anova_two_way_with(&ctx, &bal(), t).unwrap();
assert_eq!(r.ss_type, t);
assert_eq!(r.factor_a.ss, t2.factor_a.ss);
assert_eq!(r.factor_b.ss, t2.factor_b.ss);
assert_eq!(r.interaction.ss, t2.interaction.ss);
assert_eq!(r.factor_a.f, t2.factor_a.f);
assert_eq!(r.factor_b.f, t2.factor_b.f);
assert_eq!(r.residual, t2.residual);
}
}
#[test]
fn two_way_balanced_effect_sizes_exact() {
let ctx = Context::new();
let r = anova_two_way(&ctx, &bal()).unwrap();
assert_eq!(r.factor_a.partial_eta_squared, Some(q(147, 271)));
assert_eq!(r.factor_b.partial_eta_squared, Some(q(1339, 1525)));
assert_eq!(r.interaction.partial_eta_squared, Some(q(25, 87)));
assert_eq!(r.factor_a.eta_squared, Some(q(441, 3641)));
assert_eq!(r.factor_b.eta_squared, Some(q(2678, 3641)));
assert_eq!(r.interaction.eta_squared, Some(q(150, 3641)));
close(
qf(r.factor_b.partial_eta_squared.as_ref().unwrap()),
0.878_032_786_885_245_9,
1e-15,
);
assert_eq!(r.residual.partial_eta_squared, None);
assert_eq!(r.total.eta_squared, None);
}
#[test]
fn two_way_balanced_means_exact() {
let ctx = Context::new();
let r = anova_two_way(&ctx, &bal()).unwrap();
assert_eq!(r.grand_mean, q(155, 18));
assert_eq!(
r.cell_means,
vec![
vec![qi(5), qi(7), q(31, 3)],
vec![q(17, 3), q(28, 3), q(43, 3)]
]
);
}
#[test]
fn two_way_rows_in_table_order_and_untested_rows() {
let ctx = Context::new();
let r = anova_two_way(&ctx, &bal()).unwrap();
let sources: Vec<Source> = r.rows().iter().map(|row| row.source).collect();
assert_eq!(
sources,
vec![
Source::FactorA,
Source::FactorB,
Source::Interaction,
Source::Residual,
Source::Total
]
);
assert_eq!(r.residual.ms, Some(q(31, 18)));
assert_eq!(
(r.residual.f.clone(), r.residual.p_value.clone()),
(None, None)
);
assert_eq!((r.total.ms.clone(), r.total.f.clone()), (None, None));
assert!(matches!(
r.residual.p_value_f64(),
Err(SymplexError::InvalidArgument { .. })
));
assert_eq!(r.factor_a.ms, Some(q(49, 2)));
assert_eq!(r.factor_b.ms, Some(q(1339, 18)));
}
#[test]
fn two_way_balanced_sums_of_squares_decompose_total() {
let ctx = Context::new();
let r = anova_two_way(&ctx, &bal()).unwrap();
let sum = &r.factor_a.ss + &r.factor_b.ss + &r.interaction.ss + &r.residual.ss;
assert_eq!(sum, r.total.ss);
assert_eq!(
r.factor_a.df + r.factor_b.df + r.interaction.df + r.residual.df,
r.total.df
);
}
#[test]
fn two_way_unbalanced_type1_sequential() {
let ctx = Context::new();
let r = anova_two_way_with(&ctx, &unb(), SsType::TypeI).unwrap();
assert!(!unb().is_balanced());
assert_eq!(r.factor_a.ss, q(338, 9));
assert_eq!(r.factor_a.f, Some(q(676, 35)));
assert_eq!(r.factor_b.ss, q(1082, 9));
assert_eq!(r.factor_b.f, Some(q(1082, 35)));
assert_eq!(r.interaction.ss, q(26, 3));
assert_eq!(r.interaction.f, Some(q(78, 35)));
assert_eq!((r.residual.ss.clone(), r.residual.df), (q(70, 3), 12));
close(
r.factor_a.p_value_f64().unwrap(),
0.000_873_317_589_171_924_5,
1e-12,
);
close(
r.factor_b.p_value_f64().unwrap(),
1.843_914_104_949_99e-5,
1e-12,
);
close(
r.interaction.p_value_f64().unwrap(),
0.150_300_644_143_945_67,
1e-12,
);
}
#[test]
fn two_way_unbalanced_type2_hierarchical() {
let ctx = Context::new();
let r = anova_two_way(&ctx, &unb()).unwrap();
assert_eq!(r.factor_a.ss, qi(24));
assert_eq!(r.factor_a.f, Some(q(432, 35)));
close(
r.factor_a.p_value_f64().unwrap(),
0.004_276_317_115_677_296,
1e-12,
);
assert_eq!(r.factor_b.ss, q(1082, 9));
assert_eq!(r.interaction.ss, q(26, 3));
assert_eq!(r.factor_a.partial_eta_squared, Some(q(36, 71)));
assert_eq!(r.factor_a.eta_squared, Some(q(54, 427)));
}
#[test]
fn two_way_unbalanced_type3_sum_to_zero_contrasts() {
let ctx = Context::new();
let r = anova_two_way_with(&ctx, &unb(), SsType::TypeIII).unwrap();
assert_eq!(r.factor_a.ss, q(294, 13));
assert_eq!(r.factor_a.f, Some(q(756, 65)));
assert_eq!(r.factor_b.ss, q(9442, 75));
assert_eq!(r.factor_b.f, Some(q(28326, 875)));
assert_eq!(r.interaction.ss, q(26, 3));
assert_eq!(r.interaction.f, Some(q(78, 35)));
close(
r.factor_a.p_value_f64().unwrap(),
0.005_169_506_744_760_451,
1e-12,
);
close(
r.factor_b.p_value_f64().unwrap(),
1.461_443_679_017_884_8e-5,
1e-12,
);
close(qf(&r.factor_b.ss), 125.893_333_333_333_37, 1e-12);
assert_eq!(r.factor_a.partial_eta_squared, Some(q(63, 128)));
}
#[test]
fn two_way_unbalanced_type1_a_is_the_one_way_anova_on_a() {
let ctx = Context::new();
let r = anova_two_way_with(&ctx, &unb(), SsType::TypeI).unwrap();
let data = unb();
let level = |a: usize| -> Vec<Q> {
(0..data.b_levels())
.flat_map(|b| data.cell(a, b).unwrap().to_vec())
.collect()
};
let one_way = anova_one_way(&ctx, &[level(0), level(1)]).unwrap();
assert_eq!(one_way.ss_between, r.factor_a.ss);
assert_eq!(one_way.ss_between, q(338, 9));
}
#[test]
fn two_way_unbalanced_only_type1_decomposes_total() {
let ctx = Context::new();
let t1 = anova_two_way_with(&ctx, &unb(), SsType::TypeI).unwrap();
let t2 = anova_two_way_with(&ctx, &unb(), SsType::TypeII).unwrap();
let sum = |r: &symplex::stats::anova::TwoWayAnova| {
&r.factor_a.ss + &r.factor_b.ss + &r.interaction.ss + &r.residual.ss
};
assert_eq!(t1.total.ss, q(1708, 9));
assert_eq!(sum(&t1), t1.total.ss);
assert_eq!(sum(&t2), q(1586, 9));
assert_ne!(sum(&t2), t2.total.ss);
}
#[test]
fn two_way_unbalanced_residual_and_means() {
let ctx = Context::new();
let data = unb();
assert_eq!(data.n_obs(), 18);
assert_eq!((data.a_levels(), data.b_levels()), (2, 3));
let r = anova_two_way(&ctx, &data).unwrap();
assert_eq!(r.residual.ss, q(70, 3));
assert_eq!(r.residual.ms, Some(q(35, 18)));
assert_eq!(r.grand_mean, q(80, 9));
assert_eq!(
r.cell_means,
vec![
vec![q(11, 2), qi(7), q(31, 3)],
vec![qi(6), q(19, 2), q(43, 3)]
]
);
assert_eq!(r.total.df, 17);
}
#[test]
fn two_way_from_long_matches_from_cells() {
let ctx = Context::new();
let mut rows = Vec::new();
for (a, row) in unb().cells().iter().enumerate() {
for (b, cell) in row.iter().enumerate() {
for y in cell {
rows.push(Observation { a, b, y: y.clone() });
}
}
}
let (first, second) = rows.split_at(9);
let interleaved: Vec<Observation> = first
.iter()
.zip(second)
.flat_map(|(x, y)| [x.clone(), y.clone()])
.collect();
let long = TwoWayData::from_long(&interleaved).unwrap();
assert_eq!(long, unb());
assert_eq!(long.cell(0, 1), Some(&[qi(6), qi(8)][..]));
assert_eq!(long.cell(2, 0), None);
assert_eq!(long.cell(0, 3), None);
assert_eq!(
anova_two_way(&ctx, &long).unwrap(),
anova_two_way(&ctx, &unb()).unwrap()
);
}
#[test]
fn two_way_invalid_inputs() {
let ctx = Context::new();
let invalid = |r: Result<TwoWayData, SymplexError>| {
assert!(matches!(r, Err(SymplexError::InvalidArgument { .. })));
};
invalid(TwoWayData::from_i64(&[&[&[1, 2], &[3, 4]]]));
invalid(TwoWayData::from_i64(&[&[&[1, 2]], &[&[3, 4]]]));
invalid(TwoWayData::from_i64(&[&[&[1, 2], &[3, 4]], &[&[5, 6]]]));
invalid(TwoWayData::from_i64(&[&[&[1, 2], &[]], &[&[5, 6], &[7]]]));
invalid(TwoWayData::from_long(&[
Observation {
a: 0,
b: 0,
y: qi(1),
},
Observation {
a: 1,
b: 1,
y: qi(2),
},
]));
invalid(TwoWayData::from_long(&[]));
let saturated = TwoWayData::from_i64(&[&[&[1], &[2]], &[&[3], &[5]]]).unwrap();
assert!(matches!(
anova_two_way(&ctx, &saturated),
Err(SymplexError::InvalidArgument { .. })
));
let constant = TwoWayData::from_i64(&[&[&[2, 2], &[2, 2]], &[&[2, 2], &[2, 2]]]).unwrap();
assert!(matches!(
anova_two_way(&ctx, &constant),
Err(SymplexError::InvalidArgument { .. })
));
let zero_resid = TwoWayData::from_i64(&[&[&[1, 1], &[2, 2]], &[&[3, 3], &[5, 5]]]).unwrap();
assert!(matches!(
anova_two_way(&ctx, &zero_resid),
Err(SymplexError::InvalidArgument { .. })
));
}
#[test]
fn rm_sums_of_squares_and_df_exact() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
assert_eq!((r.n_subjects, r.n_conditions), (5, 3));
assert_eq!((r.conditions.ss.clone(), r.conditions.df), (q(542, 15), 2));
assert_eq!((r.subjects.ss.clone(), r.subjects.df), (q(764, 15), 4));
assert_eq!((r.error.ss.clone(), r.error.df), (q(178, 15), 8));
assert_eq!((r.total.ss.clone(), r.total.df), (q(1484, 15), 14));
assert_eq!(r.conditions.ms, Some(q(271, 15)));
assert_eq!(r.subjects.ms, Some(q(191, 15)));
assert_eq!(r.error.ms, Some(q(89, 60)));
assert_eq!(&r.conditions.ss + &r.subjects.ss + &r.error.ss, r.total.ss);
let sources: Vec<Source> = r.rows().iter().map(|row| row.source).collect();
assert_eq!(
sources,
vec![
Source::Conditions,
Source::Subjects,
Source::Residual,
Source::Total
]
);
}
#[test]
fn rm_f_statistic_and_p_value() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
assert_eq!(r.f, q(1084, 89));
assert_eq!(r.conditions.f, Some(q(1084, 89)));
close(qf(&r.f), 12.179_775_280_898_882, 1e-12);
close(r.p_value_f64().unwrap(), 0.003_735_511_033_474_317, 1e-12);
close(
r.conditions.p_value_f64().unwrap(),
0.003_735_511_033_474_313_6,
1e-12,
);
assert_eq!((r.subjects.f.clone(), r.error.f.clone()), (None, None));
}
#[test]
fn rm_effect_sizes_and_means_exact() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
assert_eq!(r.conditions.partial_eta_squared, Some(q(271, 360)));
assert_eq!(r.conditions.eta_squared, Some(q(271, 742)));
close(
qf(r.conditions.eta_squared.as_ref().unwrap()),
0.365_229_110_512_129_53,
1e-15,
);
assert_eq!(r.grand_mean, q(104, 15));
assert_eq!(r.condition_means, vec![qi(5), qi(7), q(44, 5)]);
assert_eq!(
r.subject_means,
vec![qi(7), q(17, 3), qi(8), q(13, 3), q(29, 3)]
);
}
#[test]
fn rm_greenhouse_geisser_epsilon_exact() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
assert_eq!(r.epsilon_gg, q(7921, 14597));
close(qf(&r.epsilon_gg), 0.542_645_749_126_532_6, 1e-12);
}
#[test]
fn rm_huynh_feldt_epsilon_exact() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
assert_eq!(r.epsilon_hf, Some(q(4168, 7091)));
close(
qf(r.epsilon_hf.as_ref().unwrap()),
0.587_787_336_059_793_6,
1e-12,
);
}
#[test]
fn rm_sphericity_corrected_p_values() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
close(
r.p_value_gg_f64().unwrap(),
0.021_264_365_858_261_566,
1e-12,
);
close(r.p_value_gg_f64().unwrap(), 0.021_264_365_858_261_44, 1e-12);
close(
r.p_value_hf_f64().unwrap().unwrap(),
0.017_842_253_026_512_39,
1e-12,
);
assert!(r.p_value_gg_f64().unwrap() > r.p_value_f64().unwrap());
}
#[test]
fn rm_mauchly_three_conditions() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm3()).unwrap();
let m = r.mauchly.as_ref().unwrap();
assert_eq!(m.w, q(1245, 7921));
assert_eq!(m.df, 2);
close(m.chi_squared_f64().unwrap(), 5.551_145_791_696_411, 1e-12);
close(m.p_value_f64().unwrap(), 0.062_313_767_163_236_534, 1e-12);
}
#[test]
fn rm_four_conditions_epsilons_mauchly_and_corrected_p() {
let ctx = Context::new();
let r = anova_repeated_measures(&ctx, &rm4()).unwrap();
assert_eq!((r.n_subjects, r.n_conditions), (6, 4));
assert_eq!(r.conditions.ss, q(2195, 24));
assert_eq!(r.subjects.ss, q(1001, 24));
assert_eq!(r.error.ss, q(211, 24));
assert_eq!((r.conditions.df, r.error.df), (3, 15));
assert_eq!(r.f, q(10975, 211));
close(r.p_value_f64().unwrap(), 3.678_653_133_464_614_4e-8, 1e-15);
assert_eq!(r.epsilon_gg, q(211, 345));
assert_eq!(r.epsilon_hf, Some(q(37, 39)));
close(
r.p_value_gg_f64().unwrap(),
1.182_595_561_866_387_6e-5,
1e-13,
);
close(
r.p_value_hf_f64().unwrap().unwrap(),
7.849_523_323_673_606e-8,
1e-15,
);
let m = r.mauchly.as_ref().unwrap();
assert_eq!(m.w, q(979_776, 9_393_931));
assert_eq!(m.df, 5);
close(m.chi_squared_f64().unwrap(), 8.414_065_270_641_222, 1e-12);
close(m.p_value_f64().unwrap(), 0.146_712_510_366_879_3, 1e-12);
}
#[test]
fn rm_two_conditions_is_the_squared_paired_t() {
let ctx = Context::new();
let y = vec![
from_i64(&[5, 7]),
from_i64(&[4, 5]),
from_i64(&[6, 8]),
from_i64(&[3, 6]),
from_i64(&[7, 9]),
];
let r = anova_repeated_measures(&ctx, &y).unwrap();
assert_eq!(r.f, qi(40));
assert_eq!(
(
r.conditions.ss.clone(),
r.subjects.ss.clone(),
r.error.ss.clone()
),
(qi(10), qi(19), qi(1))
);
close(r.p_value_f64().unwrap(), 0.003_198_202_152_335_305_6, 1e-12);
assert_eq!(r.epsilon_gg, qi(1));
assert!(r.mauchly.is_none());
close(r.p_value_gg_f64().unwrap(), r.p_value_f64().unwrap(), 1e-15);
}
#[test]
fn rm_two_subjects_three_conditions_degenerate_corrections() {
let ctx = Context::new();
let y = vec![from_i64(&[1, 2, 4]), from_i64(&[2, 5, 3])];
let r = anova_repeated_measures(&ctx, &y).unwrap();
assert_eq!(r.f, q(4, 3));
assert_eq!((r.conditions.df, r.error.df), (2, 2));
close(r.p_value_f64().unwrap(), 0.428_571_428_571_428_6, 1e-12);
assert_eq!(r.epsilon_gg, q(1, 2));
assert_eq!(r.epsilon_hf, None);
assert_eq!(r.p_value_hf, None);
assert_eq!(r.p_value_hf_f64().unwrap(), None);
assert!(r.mauchly.is_none());
}
#[test]
fn rm_invalid_inputs() {
let ctx = Context::new();
let invalid = |rows: Vec<Vec<Q>>| {
assert!(matches!(
anova_repeated_measures(&ctx, &rows),
Err(SymplexError::InvalidArgument { .. })
));
};
invalid(vec![from_i64(&[1, 2, 3])]);
invalid(vec![from_i64(&[1]), from_i64(&[2])]);
invalid(vec![from_i64(&[1, 2]), from_i64(&[2, 3, 4])]);
invalid(vec![
from_i64(&[1, 3, 4]),
from_i64(&[2, 4, 5]),
from_i64(&[5, 7, 8]),
]);
invalid(vec![]);
}
#[test]
fn studentized_range_sf_matches_scipy() {
for (qv, k, df, want) in [
(3.0, 3, 12.0, 0.127_032_591_355_744_18),
(3.5, 3, 15.0, 0.062_915_354_643_710_23),
(1.0, 4, 10.0, 0.892_014_018_618_922_4),
(5.0, 4, 10.0, 0.023_445_996_365_997_646),
(2.0, 2, 5.0, 0.216_437_229_269_685_34),
(4.2, 5, 30.0, 0.042_726_538_220_137_61),
(3.0, 3, 200.0, 0.088_098_322_023_517_08),
] {
close(studentized_range_sf(qv, k, df).unwrap(), want, 1e-9);
close(studentized_range_cdf(qv, k, df).unwrap(), 1.0 - want, 1e-9);
}
}
#[test]
fn studentized_range_quantile_matches_scipy() {
for (p, k, df, want) in [
(0.95, 3, 15.0, 3.673_377_658_897_097_7),
(0.95, 4, 10.0, 4.326_582_115_731_219),
(0.99, 3, 12.0, 5.045_934_725_166_239),
(0.9, 5, 20.0, 3.736_402_824_922_501),
] {
let got = studentized_range_quantile(p, k, df).unwrap();
close(got, want, 1e-7);
close(studentized_range_cdf(got, k, df).unwrap(), p, 1e-9);
}
}
#[test]
fn studentized_range_edge_cases_and_invalid_arguments() {
assert_eq!(studentized_range_cdf(0.0, 3, 10.0).unwrap(), 0.0);
assert_eq!(studentized_range_sf(-1.0, 3, 10.0).unwrap(), 1.0);
let invalid = |r: Result<f64, SymplexError>| {
assert!(matches!(r, Err(SymplexError::InvalidArgument { .. })));
};
invalid(studentized_range_cdf(3.0, 1, 10.0));
invalid(studentized_range_cdf(3.0, 3, 0.5));
invalid(studentized_range_cdf(f64::NAN, 3, 10.0));
invalid(studentized_range_quantile(1.0, 3, 10.0));
invalid(studentized_range_quantile(0.5, 3, f64::INFINITY));
let a = studentized_range_cdf(2.0, 4, 8.0).unwrap();
let b = studentized_range_cdf(3.0, 4, 8.0).unwrap();
assert!(0.0 < a && a < b && b < 1.0);
}
#[test]
fn tukey_hsd_balanced_matches_scipy() {
let ctx = Context::new();
let pairs = tukey_hsd(&ctx, &g3(), 0.95).unwrap();
assert_eq!(pairs.len(), 3);
let idx: Vec<(usize, usize)> = pairs.iter().map(|p| (p.i, p.j)).collect();
assert_eq!(idx, vec![(0, 1), (0, 2), (1, 2)]);
assert_eq!(pairs[0].diff, qi(-4));
assert_eq!(pairs[1].diff, qi(-5));
assert_eq!(pairs[2].diff, qi(-1));
let se = ctx.from_ratio(q(34, 45)).sqrt();
for p in &pairs {
assert_eq!(p.se, se);
}
close(ev(&pairs[0].se), 0.869_226_987_360_353_2, 1e-12);
close(ev(&pairs[0].statistic), 4.601_789_933_084_222_5, 1e-12);
close(ev(&pairs[1].statistic), 5.752_237_416_355_278, 1e-12);
close(pairs[0].p_adj, 0.013_913_287_267_276_697, 1e-9);
close(pairs[1].p_adj, 0.002_732_121_967_372_935_8, 1e-9);
close(pairs[2].p_adj, 0.700_659_938_532_346, 1e-9);
close(pairs[0].ci.lower, -7.192_998_995_879_951, 1e-8);
close(pairs[0].ci.upper, -0.807_001_004_120_048_8, 1e-8);
close(pairs[2].ci.lower, -4.192_998_995_879_951, 1e-8);
close(pairs[2].ci.upper, 2.192_998_995_879_951, 1e-8);
for p in &pairs {
assert_eq!(p.ci.lower > 0.0 || p.ci.upper < 0.0, p.p_adj < 0.05);
}
}
#[test]
fn tukey_hsd_unbalanced_tukey_kramer_matches_scipy() {
let ctx = Context::new();
let pairs = tukey_hsd(&ctx, &g3u(), 0.95).unwrap();
assert_eq!(pairs[0].diff, q(-13, 6));
assert_eq!(pairs[1].diff, q(-11, 2));
assert_eq!(pairs[2].diff, q(-10, 3));
assert_eq!(pairs[0].se, ctx.from_ratio(q(413, 648)).sqrt());
assert_eq!(pairs[1].se, ctx.from_ratio(q(59, 120)).sqrt());
assert_eq!(pairs[2].se, ctx.from_ratio(q(236, 405)).sqrt());
close(pairs[0].p_adj, 0.188_811_304_715_616_36, 1e-9);
close(pairs[1].p_adj, 0.000_933_779_692_532_055_2, 1e-9);
close(pairs[2].p_adj, 0.031_533_822_027_608_016, 1e-9);
close(pairs[0].ci.lower, -5.318_903_270_038_215_5, 1e-8);
close(pairs[0].ci.upper, 0.985_569_936_704_881_6, 1e-8);
close(pairs[1].ci.lower, -8.268_641_138_063_197, 1e-8);
close(pairs[1].ci.upper, -2.731_358_861_936_802_6, 1e-8);
close(pairs[2].ci.lower, -6.347_448_030_725_932, 1e-8);
close(pairs[2].ci.upper, -0.319_218_635_940_734_1, 1e-8);
let wide = tukey_hsd(&ctx, &g3u(), 0.99).unwrap();
close(wide[0].ci.lower, -6.500_086_708_915_748, 1e-8);
close(wide[0].ci.upper, 2.166_753_375_582_414, 1e-8);
assert_eq!(wide[0].diff, pairs[0].diff);
close(wide[0].p_adj, pairs[0].p_adj, 1e-15);
}
#[test]
fn tukey_hsd_invalid_inputs() {
let ctx = Context::new();
let invalid = |groups: Vec<Vec<Q>>, confidence: f64| {
assert!(matches!(
tukey_hsd(&ctx, &groups, confidence),
Err(SymplexError::InvalidArgument { .. })
));
};
invalid(vec![from_i64(&[1, 2, 3])], 0.95);
invalid(vec![from_i64(&[1, 2, 3]), vec![]], 0.95);
invalid(vec![from_i64(&[1]), from_i64(&[2])], 0.95);
invalid(vec![from_i64(&[2, 2]), from_i64(&[3, 3])], 0.95);
invalid(g3(), 1.0);
invalid(g3(), 0.0);
}
#[test]
fn pairwise_welch_t_tests_holm_matches_scipy_and_statsmodels() {
let ctx = Context::new();
let t = pairwise_t_tests(&ctx, &g3(), Adjustment::Holm, 0.05).unwrap();
assert_eq!(t.len(), 3);
assert_eq!((t[0].i, t[0].j, t[2].i, t[2].j), (0, 1, 1, 2));
assert_eq!(t[0].diff, qi(-4));
assert_eq!(t[1].diff, qi(-5));
assert_eq!(t[2].diff, qi(-1));
assert_eq!(t[0].test.df, Some(ctx.from_ratio(q(125, 13))));
assert_eq!(t[1].test.df, Some(ctx.from_ratio(q(121, 13))));
assert_eq!(t[2].test.df, Some(ctx.from_ratio(q(169, 17))));
close(
t[0].test.statistic_f64().unwrap(),
-3.464_101_615_137_755,
1e-12,
);
close(
t[0].test.p_value_f64().unwrap(),
0.006_443_866_163_955_33,
1e-12,
);
close(
t[1].test.p_value_f64().unwrap(),
0.002_386_749_612_426_776,
1e-12,
);
close(
t[2].test.p_value_f64().unwrap(),
0.465_151_039_753_495_75,
1e-12,
);
close(t[0].p_adj, 0.012_887_732_327_910_66, 1e-12);
close(t[1].p_adj, 0.007_160_248_837_280_328, 1e-12);
close(t[2].p_adj, 0.465_151_039_753_495_75, 1e-12);
assert_eq!(
t.iter().map(|p| p.reject).collect::<Vec<_>>(),
vec![true, true, false]
);
}
#[test]
fn pairwise_welch_t_tests_bonferroni_unbalanced() {
let ctx = Context::new();
let t = pairwise_t_tests(&ctx, &g3u(), Adjustment::Bonferroni, 0.05).unwrap();
assert_eq!(t[0].test.df, Some(ctx.from_ratio(q(1849, 467))));
assert_eq!(t[1].test.df, Some(ctx.from_ratio(q(363, 52))));
assert_eq!(t[2].test.df, Some(ctx.from_ratio(q(2116, 473))));
close(t[0].p_adj, 0.357_572_119_856_329_14, 1e-12);
close(t[1].p_adj, 0.002_127_921_228_112_409, 1e-12);
close(t[2].p_adj, 0.109_765_463_195_487_8, 1e-12);
assert_eq!(
t.iter().map(|p| p.reject).collect::<Vec<_>>(),
vec![false, true, false]
);
let h = pairwise_t_tests(&ctx, &g3u(), Adjustment::Holm, 0.05).unwrap();
close(h[0].p_adj, 0.119_190_706_618_776_38, 1e-12);
close(h[2].p_adj, 0.073_176_975_463_658_53, 1e-12);
for (groups, alpha) in [
(vec![from_i64(&[1, 2])], 0.05),
(vec![from_i64(&[1, 2]), from_i64(&[3])], 0.05),
(g3u(), 1.5),
] {
assert!(matches!(
pairwise_t_tests(&ctx, &groups, Adjustment::Holm, alpha),
Err(SymplexError::InvalidArgument { .. })
));
}
}
#[test]
fn book_two_way_example() {
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();
assert_eq!(format!("{}", r.factor_a.ss), "49/2");
assert_eq!(r.factor_a.df, 1);
assert_eq!(format!("{}", r.factor_a.f.clone().unwrap()), "441/31");
close(
r.factor_a.p_value_f64().unwrap(),
0.002_663_477_688_683_533_4,
1e-12,
);
assert_eq!(format!("{}", r.factor_b.ss), "1339/9");
assert_eq!(r.factor_b.df, 2);
assert_eq!(format!("{}", r.factor_b.f.clone().unwrap()), "1339/31");
close(
r.factor_b.p_value_f64().unwrap(),
3.291_990_727_040_258e-6,
1e-12,
);
assert_eq!(format!("{}", r.interaction.ss), "25/3");
assert_eq!(r.interaction.df, 2);
assert_eq!(format!("{}", r.interaction.f.clone().unwrap()), "75/31");
close(
r.interaction.p_value_f64().unwrap(),
0.130_988_938_057_322_53,
1e-12,
);
assert_eq!(format!("{}", r.residual.ss), "62/3");
assert_eq!(r.residual.df, 12);
assert_eq!(format!("{}", r.residual.ms.clone().unwrap()), "31/18");
assert_eq!(format!("{}", r.total.ss), "3641/18");
assert_eq!(r.total.df, 17);
assert_eq!(
format!("{}", r.factor_b.partial_eta_squared.clone().unwrap()),
"1339/1525"
);
let unb = TwoWayData::from_i64(&[
&[&[4, 5, 6, 7], &[6, 8], &[9, 10, 12]],
&[&[5, 7], &[8, 9, 11, 10], &[13, 14, 16]],
])
.unwrap();
assert_eq!(
format!("{}", anova_two_way(&ctx, &unb).unwrap().factor_a.ss),
"24"
);
assert_eq!(
format!(
"{}",
anova_two_way_with(&ctx, &unb, SsType::TypeI)
.unwrap()
.factor_a
.ss
),
"338/9"
);
}
#[test]
fn book_repeated_measures_example() {
let ctx = Context::new();
let y = [
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]),
];
let r = anova_repeated_measures(&ctx, &y).unwrap();
assert_eq!(
(format!("{}", r.conditions.ss), r.conditions.df),
("542/15".into(), 2)
);
assert_eq!(
(format!("{}", r.subjects.ss), r.subjects.df),
("764/15".into(), 4)
);
assert_eq!(
(format!("{}", r.error.ss), r.error.df),
("178/15".into(), 8)
);
assert_eq!(format!("{}", r.f), "1084/89");
close(qf(&r.f), 12.179_775_280_898_877, 1e-12);
close(r.p_value_f64().unwrap(), 0.003_735_511_033_474_317, 1e-12);
assert_eq!(format!("{}", r.epsilon_gg), "7921/14597");
close(qf(&r.epsilon_gg), 0.542_645_749_126_532_9, 1e-12);
assert_eq!(format!("{}", r.epsilon_hf.clone().unwrap()), "4168/7091");
close(
qf(r.epsilon_hf.as_ref().unwrap()),
0.587_787_336_059_794,
1e-12,
);
close(
r.p_value_gg_f64().unwrap(),
0.021_264_365_858_261_566,
1e-12,
);
let m = r.mauchly.as_ref().unwrap();
assert_eq!(format!("{}", m.w), "1245/7921");
close(m.chi_squared_f64().unwrap(), 5.551_145_791_696_415, 1e-12);
assert_eq!(m.df, 2);
close(m.p_value_f64().unwrap(), 0.062_313_767_163_236_4, 1e-12);
}