1use crate::error::{StatsError, StatsResult};
27use scirs2_core::ndarray::ArrayView1;
28use scirs2_core::numeric::{Float, NumCast};
29
30#[derive(Debug, Clone)]
32pub struct EffectSizeResult<F: Float> {
33 pub estimate: F,
35 pub measure: String,
37 pub ci_lower: Option<F>,
39 pub ci_upper: Option<F>,
41 pub confidence_level: Option<F>,
43}
44
45fn normal_quantile(p: f64) -> f64 {
52 if p <= 0.0 {
53 return f64::NEG_INFINITY;
54 }
55 if p >= 1.0 {
56 return f64::INFINITY;
57 }
58 if (p - 0.5).abs() < 1e-15 {
59 return 0.0;
60 }
61
62 let a = [
64 -3.969683028665376e+01,
65 2.209460984245205e+02,
66 -2.759285104469687e+02,
67 1.383577518672690e+02,
68 -3.066479806614716e+01,
69 2.506628277459239e+00,
70 ];
71 let b = [
72 -5.447609879822406e+01,
73 1.615858368580409e+02,
74 -1.556989798598866e+02,
75 6.680131188771972e+01,
76 -1.328068155288572e+01,
77 ];
78 let c = [
79 -7.784894002430293e-03,
80 -3.223964580411365e-01,
81 -2.400758277161838e+00,
82 -2.549732539343734e+00,
83 4.374664141464968e+00,
84 2.938163982698783e+00,
85 ];
86 let d = [
87 7.784695709041462e-03,
88 3.224671290700398e-01,
89 2.445134137142996e+00,
90 3.754408661907416e+00,
91 ];
92
93 let p_low = 0.02425;
94 let p_high = 1.0 - p_low;
95
96 if p < p_low {
97 let q = (-2.0 * p.ln()).sqrt();
99 (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
100 / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
101 } else if p <= p_high {
102 let q = p - 0.5;
104 let r = q * q;
105 (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q
106 / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0)
107 } else {
108 let q = (-2.0 * (1.0 - p).ln()).sqrt();
110 -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
111 / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
112 }
113}
114
115fn normal_cdf_f64(x: f64) -> f64 {
117 if x < -8.0 {
118 return 0.0;
119 }
120 if x > 8.0 {
121 return 1.0;
122 }
123 let abs_x = x.abs();
124 let t = 1.0 / (1.0 + 0.2316419 * abs_x);
125 let d = 0.39894228040143268 * (-0.5 * x * x).exp();
126 let p = t
127 * (0.319381530
128 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
129 if x >= 0.0 {
130 1.0 - d * p
131 } else {
132 d * p
133 }
134}
135
136pub fn cohens_d<F>(x: &ArrayView1<F>, y: &ArrayView1<F>) -> StatsResult<EffectSizeResult<F>>
172where
173 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
174{
175 if x.len() < 2 || y.len() < 2 {
176 return Err(StatsError::InvalidArgument(
177 "Both samples must have at least 2 observations for Cohen's d".to_string(),
178 ));
179 }
180
181 let (mean_x, var_x, n1) = sample_stats(x)?;
182 let (mean_y, var_y, n2) = sample_stats(y)?;
183
184 let pooled_var = ((n1 - 1.0) * var_x + (n2 - 1.0) * var_y) / (n1 + n2 - 2.0);
185 let pooled_sd = pooled_var.sqrt();
186
187 if pooled_sd < 1e-30 {
188 return Err(StatsError::ComputationError(
189 "Pooled standard deviation is zero; Cohen's d is undefined".to_string(),
190 ));
191 }
192
193 let d = (mean_x - mean_y) / pooled_sd;
194
195 Ok(EffectSizeResult {
196 estimate: F::from(d).unwrap_or(F::zero()),
197 measure: "Cohen's d".to_string(),
198 ci_lower: None,
199 ci_upper: None,
200 confidence_level: None,
201 })
202}
203
204pub fn hedges_g<F>(x: &ArrayView1<F>, y: &ArrayView1<F>) -> StatsResult<EffectSizeResult<F>>
224where
225 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
226{
227 if x.len() < 2 || y.len() < 2 {
228 return Err(StatsError::InvalidArgument(
229 "Both samples must have at least 2 observations for Hedges' g".to_string(),
230 ));
231 }
232
233 let d_result = cohens_d(x, y)?;
234 let d = <f64 as NumCast>::from(d_result.estimate).unwrap_or(0.0);
235 let df = (x.len() + y.len() - 2) as f64;
236
237 let j = 1.0 - 3.0 / (4.0 * df - 1.0);
239 let g = d * j;
240
241 Ok(EffectSizeResult {
242 estimate: F::from(g).unwrap_or(F::zero()),
243 measure: "Hedges' g".to_string(),
244 ci_lower: None,
245 ci_upper: None,
246 confidence_level: None,
247 })
248}
249
250pub fn glass_delta<F>(
270 treatment: &ArrayView1<F>,
271 control: &ArrayView1<F>,
272) -> StatsResult<EffectSizeResult<F>>
273where
274 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
275{
276 if treatment.len() < 2 || control.len() < 2 {
277 return Err(StatsError::InvalidArgument(
278 "Both samples must have at least 2 observations for Glass's delta".to_string(),
279 ));
280 }
281
282 let (mean_t, _, _) = sample_stats(treatment)?;
283 let (mean_c, var_c, _) = sample_stats(control)?;
284 let sd_c = var_c.sqrt();
285
286 if sd_c < 1e-30 {
287 return Err(StatsError::ComputationError(
288 "Control group standard deviation is zero; Glass's delta is undefined".to_string(),
289 ));
290 }
291
292 let delta = (mean_t - mean_c) / sd_c;
293
294 Ok(EffectSizeResult {
295 estimate: F::from(delta).unwrap_or(F::zero()),
296 measure: "Glass's delta".to_string(),
297 ci_lower: None,
298 ci_upper: None,
299 confidence_level: None,
300 })
301}
302
303pub fn point_biserial_correlation<F>(
323 binary: &ArrayView1<F>,
324 continuous: &ArrayView1<F>,
325) -> StatsResult<EffectSizeResult<F>>
326where
327 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
328{
329 if binary.len() != continuous.len() {
330 return Err(StatsError::DimensionMismatch(
331 "Binary and continuous arrays must have equal length".to_string(),
332 ));
333 }
334 if binary.len() < 3 {
335 return Err(StatsError::InvalidArgument(
336 "At least 3 observations required for point-biserial correlation".to_string(),
337 ));
338 }
339
340 let n = binary.len();
341 let nf = n as f64;
342
343 let mut group0: Vec<f64> = Vec::new();
345 let mut group1: Vec<f64> = Vec::new();
346
347 for i in 0..n {
348 let b = <f64 as NumCast>::from(binary[i]).unwrap_or(0.0);
349 let c = <f64 as NumCast>::from(continuous[i]).unwrap_or(0.0);
350 if (b - 0.0).abs() < 1e-10 {
351 group0.push(c);
352 } else if (b - 1.0).abs() < 1e-10 {
353 group1.push(c);
354 } else {
355 return Err(StatsError::InvalidArgument(format!(
356 "Binary variable must contain only 0 and 1, got {} at index {}",
357 b, i
358 )));
359 }
360 }
361
362 if group0.is_empty() || group1.is_empty() {
363 return Err(StatsError::InvalidArgument(
364 "Both groups must have at least one observation".to_string(),
365 ));
366 }
367
368 let n0 = group0.len() as f64;
369 let n1 = group1.len() as f64;
370 let mean0: f64 = group0.iter().sum::<f64>() / n0;
371 let mean1: f64 = group1.iter().sum::<f64>() / n1;
372
373 let overall_mean: f64 = continuous
375 .iter()
376 .map(|&v| <f64 as NumCast>::from(v).unwrap_or(0.0))
377 .sum::<f64>()
378 / nf;
379 let overall_var: f64 = continuous
380 .iter()
381 .map(|&v| {
382 let vf = <f64 as NumCast>::from(v).unwrap_or(0.0);
383 (vf - overall_mean) * (vf - overall_mean)
384 })
385 .sum::<f64>()
386 / (nf - 1.0);
387 let overall_sd = overall_var.sqrt();
388
389 if overall_sd < 1e-30 {
390 return Err(StatsError::ComputationError(
391 "Standard deviation is zero; point-biserial correlation is undefined".to_string(),
392 ));
393 }
394
395 let rpb = (mean1 - mean0) / overall_sd * (n0 * n1).sqrt() / nf;
396
397 Ok(EffectSizeResult {
398 estimate: F::from(rpb).unwrap_or(F::zero()),
399 measure: "Point-biserial correlation".to_string(),
400 ci_lower: None,
401 ci_upper: None,
402 confidence_level: None,
403 })
404}
405
406pub fn eta_squared<F>(groups: &[ArrayView1<F>]) -> StatsResult<EffectSizeResult<F>>
428where
429 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
430{
431 if groups.len() < 2 {
432 return Err(StatsError::InvalidArgument(
433 "At least 2 groups required for eta-squared".to_string(),
434 ));
435 }
436 for (i, g) in groups.iter().enumerate() {
437 if g.is_empty() {
438 return Err(StatsError::InvalidArgument(format!("Group {} is empty", i)));
439 }
440 }
441
442 let (ss_between, ss_total) = compute_ss(groups)?;
443
444 if ss_total < 1e-30 {
445 return Err(StatsError::ComputationError(
446 "Total sum of squares is zero; eta-squared is undefined".to_string(),
447 ));
448 }
449
450 let eta2 = ss_between / ss_total;
451
452 Ok(EffectSizeResult {
453 estimate: F::from(eta2).unwrap_or(F::zero()),
454 measure: "Eta-squared".to_string(),
455 ci_lower: None,
456 ci_upper: None,
457 confidence_level: None,
458 })
459}
460
461pub fn partial_eta_squared<F>(ss_effect: F, ss_error: F) -> StatsResult<EffectSizeResult<F>>
481where
482 F: Float + NumCast + std::fmt::Display,
483{
484 let ef = <f64 as NumCast>::from(ss_effect).unwrap_or(0.0);
485 let er = <f64 as NumCast>::from(ss_error).unwrap_or(0.0);
486
487 if ef < 0.0 || er < 0.0 {
488 return Err(StatsError::InvalidArgument(
489 "Sums of squares must be non-negative".to_string(),
490 ));
491 }
492 let denom = ef + er;
493 if denom < 1e-30 {
494 return Err(StatsError::ComputationError(
495 "SS_effect + SS_error is zero; partial eta-squared is undefined".to_string(),
496 ));
497 }
498
499 let partial_eta2 = ef / denom;
500
501 Ok(EffectSizeResult {
502 estimate: F::from(partial_eta2).unwrap_or(F::zero()),
503 measure: "Partial eta-squared".to_string(),
504 ci_lower: None,
505 ci_upper: None,
506 confidence_level: None,
507 })
508}
509
510pub fn omega_squared<F>(groups: &[ArrayView1<F>]) -> StatsResult<EffectSizeResult<F>>
528where
529 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
530{
531 if groups.len() < 2 {
532 return Err(StatsError::InvalidArgument(
533 "At least 2 groups required for omega-squared".to_string(),
534 ));
535 }
536 for (i, g) in groups.iter().enumerate() {
537 if g.is_empty() {
538 return Err(StatsError::InvalidArgument(format!("Group {} is empty", i)));
539 }
540 }
541
542 let k = groups.len();
543 let n_total: usize = groups.iter().map(|g| g.len()).sum();
544
545 let (ss_between, ss_total) = compute_ss(groups)?;
546 let ss_within = ss_total - ss_between;
547
548 let df_between = (k - 1) as f64;
549 let df_within = (n_total - k) as f64;
550
551 if df_within < 1.0 {
552 return Err(StatsError::InvalidArgument(
553 "Insufficient degrees of freedom for omega-squared".to_string(),
554 ));
555 }
556
557 let ms_error = ss_within / df_within;
558 let denom = ss_total + ms_error;
559
560 if denom < 1e-30 {
561 return Err(StatsError::ComputationError(
562 "Denominator is zero; omega-squared is undefined".to_string(),
563 ));
564 }
565
566 let omega2 = (ss_between - df_between * ms_error) / denom;
567 let omega2 = omega2.max(0.0);
569
570 Ok(EffectSizeResult {
571 estimate: F::from(omega2).unwrap_or(F::zero()),
572 measure: "Omega-squared".to_string(),
573 ci_lower: None,
574 ci_upper: None,
575 confidence_level: None,
576 })
577}
578
579pub fn cohens_d_ci<F>(
598 x: &ArrayView1<F>,
599 y: &ArrayView1<F>,
600 confidence_level: F,
601) -> StatsResult<EffectSizeResult<F>>
602where
603 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
604{
605 let cl = <f64 as NumCast>::from(confidence_level).unwrap_or(0.95);
606 if cl <= 0.0 || cl >= 1.0 {
607 return Err(StatsError::InvalidArgument(format!(
608 "confidence_level must be in (0, 1), got {}",
609 cl
610 )));
611 }
612
613 let d_result = cohens_d(x, y)?;
614 let d = <f64 as NumCast>::from(d_result.estimate).unwrap_or(0.0);
615 let n1 = x.len() as f64;
616 let n2 = y.len() as f64;
617
618 let se = (1.0 / n1 + 1.0 / n2 + d * d / (2.0 * (n1 + n2))).sqrt();
620
621 let alpha = 1.0 - cl;
622 let z = normal_quantile(1.0 - alpha / 2.0);
623
624 let lower = d - z * se;
625 let upper = d + z * se;
626
627 Ok(EffectSizeResult {
628 estimate: F::from(d).unwrap_or(F::zero()),
629 measure: "Cohen's d".to_string(),
630 ci_lower: Some(F::from(lower).unwrap_or(F::zero())),
631 ci_upper: Some(F::from(upper).unwrap_or(F::zero())),
632 confidence_level: Some(confidence_level),
633 })
634}
635
636pub fn correlation_ci<F>(r: F, n: usize, confidence_level: F) -> StatsResult<EffectSizeResult<F>>
655where
656 F: Float + NumCast + std::fmt::Display,
657{
658 let rf = <f64 as NumCast>::from(r).unwrap_or(0.0);
659 let cl = <f64 as NumCast>::from(confidence_level).unwrap_or(0.95);
660
661 if rf < -1.0 || rf > 1.0 {
662 return Err(StatsError::InvalidArgument(format!(
663 "Correlation must be in [-1, 1], got {}",
664 rf
665 )));
666 }
667 if n < 4 {
668 return Err(StatsError::InvalidArgument(
669 "Sample size must be at least 4 for correlation CI".to_string(),
670 ));
671 }
672 if cl <= 0.0 || cl >= 1.0 {
673 return Err(StatsError::InvalidArgument(format!(
674 "confidence_level must be in (0, 1), got {}",
675 cl
676 )));
677 }
678
679 let z = 0.5 * ((1.0 + rf) / (1.0 - rf)).ln();
681 let se_z = 1.0 / ((n as f64 - 3.0).sqrt());
682
683 let alpha = 1.0 - cl;
684 let z_crit = normal_quantile(1.0 - alpha / 2.0);
685
686 let z_lower = z - z_crit * se_z;
687 let z_upper = z + z_crit * se_z;
688
689 let r_lower = z_lower.tanh();
691 let r_upper = z_upper.tanh();
692
693 Ok(EffectSizeResult {
694 estimate: r,
695 measure: "Pearson correlation".to_string(),
696 ci_lower: Some(F::from(r_lower).unwrap_or(F::zero())),
697 ci_upper: Some(F::from(r_upper).unwrap_or(F::zero())),
698 confidence_level: Some(confidence_level),
699 })
700}
701
702pub fn d_to_r(d: f64, n1: usize, n2: usize) -> f64 {
722 let n1f = n1 as f64;
723 let n2f = n2 as f64;
724 let a = (n1f + n2f) * (n1f + n2f) / (n1f * n2f);
725 d / (d * d + a).sqrt()
726}
727
728pub fn r_to_d(r: f64) -> f64 {
740 if (1.0 - r * r) < 1e-15 {
741 if r > 0.0 {
742 f64::INFINITY
743 } else {
744 f64::NEG_INFINITY
745 }
746 } else {
747 2.0 * r / (1.0 - r * r).sqrt()
748 }
749}
750
751fn sample_stats<F>(x: &ArrayView1<F>) -> StatsResult<(f64, f64, f64)>
757where
758 F: Float + std::iter::Sum<F> + NumCast,
759{
760 let n = x.len();
761 if n == 0 {
762 return Err(StatsError::InvalidArgument(
763 "Input array cannot be empty".to_string(),
764 ));
765 }
766 let nf = n as f64;
767 let mean: f64 = x
768 .iter()
769 .map(|&v| <f64 as NumCast>::from(v).unwrap_or(0.0))
770 .sum::<f64>()
771 / nf;
772 let var: f64 = x
773 .iter()
774 .map(|&v| {
775 let vf = <f64 as NumCast>::from(v).unwrap_or(0.0);
776 (vf - mean) * (vf - mean)
777 })
778 .sum::<f64>()
779 / (nf - 1.0).max(1.0);
780 Ok((mean, var, nf))
781}
782
783fn compute_ss<F>(groups: &[ArrayView1<F>]) -> StatsResult<(f64, f64)>
785where
786 F: Float + std::iter::Sum<F> + NumCast,
787{
788 let mut grand_sum = 0.0_f64;
790 let mut n_total = 0_usize;
791 for g in groups {
792 for &v in g.iter() {
793 grand_sum += <f64 as NumCast>::from(v).unwrap_or(0.0);
794 n_total += 1;
795 }
796 }
797
798 if n_total == 0 {
799 return Err(StatsError::InvalidArgument(
800 "All groups are empty".to_string(),
801 ));
802 }
803
804 let grand_mean = grand_sum / n_total as f64;
805
806 let mut ss_total = 0.0_f64;
808 for g in groups {
809 for &v in g.iter() {
810 let vf = <f64 as NumCast>::from(v).unwrap_or(0.0);
811 ss_total += (vf - grand_mean) * (vf - grand_mean);
812 }
813 }
814
815 let mut ss_between = 0.0_f64;
817 for g in groups {
818 let ni = g.len() as f64;
819 let group_mean: f64 = g
820 .iter()
821 .map(|&v| <f64 as NumCast>::from(v).unwrap_or(0.0))
822 .sum::<f64>()
823 / ni;
824 ss_between += ni * (group_mean - grand_mean) * (group_mean - grand_mean);
825 }
826
827 Ok((ss_between, ss_total))
828}
829
830#[cfg(test)]
835mod tests {
836 use super::*;
837 use scirs2_core::ndarray::array;
838
839 #[test]
840 fn test_cohens_d_basic() {
841 let x = array![5.0, 6.0, 7.0, 8.0, 9.0];
842 let y = array![3.0, 4.0, 5.0, 6.0, 7.0];
843
844 let result = cohens_d(&x.view(), &y.view()).expect("Cohen's d failed");
845 assert!(result.estimate > 1.0);
847 assert!(result.estimate < 2.0);
848 }
849
850 #[test]
851 fn test_cohens_d_equal_means() {
852 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
853 let y = array![1.0, 2.0, 3.0, 4.0, 5.0];
854
855 let result = cohens_d(&x.view(), &y.view()).expect("Cohen's d failed");
856 assert!(result.estimate.abs() < 1e-10);
857 }
858
859 #[test]
860 fn test_cohens_d_too_small() {
861 let x = array![1.0];
862 let y = array![2.0, 3.0];
863 assert!(cohens_d(&x.view(), &y.view()).is_err());
864 }
865
866 #[test]
867 fn test_hedges_g_correction() {
868 let x = array![5.0, 6.0, 7.0, 8.0, 9.0];
869 let y = array![3.0, 4.0, 5.0, 6.0, 7.0];
870
871 let d = cohens_d(&x.view(), &y.view()).expect("d failed");
872 let g = hedges_g(&x.view(), &y.view()).expect("g failed");
873
874 assert!(g.estimate.abs() < d.estimate.abs() + 1e-10);
876 assert!(g.estimate.abs() > 0.0);
877 }
878
879 #[test]
880 fn test_glass_delta_basic() {
881 let treatment = array![6.0, 7.0, 8.0, 9.0, 10.0];
882 let control = array![1.0, 2.0, 3.0, 4.0, 5.0];
883
884 let result = glass_delta(&treatment.view(), &control.view()).expect("Glass's delta failed");
885 assert!(result.estimate > 2.0);
887 }
888
889 #[test]
890 fn test_glass_delta_symmetric() {
891 let a = array![1.0, 2.0, 3.0, 4.0, 5.0];
892 let b = array![3.0, 4.0, 5.0, 6.0, 7.0];
893
894 let delta_ab = glass_delta(&a.view(), &b.view()).expect("delta AB failed");
895 let delta_ba = glass_delta(&b.view(), &a.view()).expect("delta BA failed");
896
897 assert!(delta_ab.estimate < 0.0);
899 assert!(delta_ba.estimate > 0.0);
900 }
901
902 #[test]
903 fn test_point_biserial_basic() {
904 let binary = array![0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0];
905 let continuous = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
906
907 let result =
908 point_biserial_correlation(&binary.view(), &continuous.view()).expect("rpb failed");
909 assert!(result.estimate > 0.0);
910 assert!(result.estimate <= 1.0);
911 }
912
913 #[test]
914 fn test_point_biserial_no_correlation() {
915 let binary = array![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
916 let continuous = array![5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0];
917
918 let result = point_biserial_correlation(&binary.view(), &continuous.view());
919 assert!(result.is_err());
921 }
922
923 #[test]
924 fn test_point_biserial_invalid_binary() {
925 let binary = array![0.0, 1.0, 2.0];
926 let continuous = array![1.0, 2.0, 3.0];
927 let result = point_biserial_correlation(&binary.view(), &continuous.view());
928 assert!(result.is_err());
929 }
930
931 #[test]
932 fn test_eta_squared_basic() {
933 let g1 = array![1.0, 2.0, 3.0];
934 let g2 = array![4.0, 5.0, 6.0];
935 let g3 = array![7.0, 8.0, 9.0];
936
937 let groups = vec![g1.view(), g2.view(), g3.view()];
938 let result = eta_squared(&groups).expect("eta2 failed");
939
940 assert!(result.estimate > 0.0);
941 assert!(result.estimate <= 1.0);
942 assert!(result.estimate > 0.8);
944 }
945
946 #[test]
947 fn test_eta_squared_no_effect() {
948 let g1 = array![5.0, 5.0, 5.0];
949 let g2 = array![5.0, 5.0, 5.0];
950
951 let groups = vec![g1.view(), g2.view()];
952 let result = eta_squared(&groups);
953 assert!(result.is_err());
955 }
956
957 #[test]
958 fn test_partial_eta_squared() {
959 let result = partial_eta_squared(10.0_f64, 40.0_f64).expect("partial eta2 failed");
960 assert!((result.estimate - 0.2).abs() < 1e-10); }
962
963 #[test]
964 fn test_partial_eta_squared_zero() {
965 let result = partial_eta_squared(0.0_f64, 0.0_f64);
966 assert!(result.is_err());
967 }
968
969 #[test]
970 fn test_omega_squared_basic() {
971 let g1 = array![1.0, 2.0, 3.0, 4.0, 5.0];
972 let g2 = array![6.0, 7.0, 8.0, 9.0, 10.0];
973
974 let groups = vec![g1.view(), g2.view()];
975 let result = omega_squared(&groups).expect("omega2 failed");
976
977 assert!(result.estimate > 0.0);
978 assert!(result.estimate <= 1.0);
979 let eta2_result = eta_squared(&groups).expect("eta2 failed");
981 assert!(result.estimate <= eta2_result.estimate + 1e-10);
982 }
983
984 #[test]
985 fn test_cohens_d_ci() {
986 let x = array![5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
987 let y = array![3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
988
989 let result = cohens_d_ci(&x.view(), &y.view(), 0.95).expect("CI failed");
990
991 assert!(result.ci_lower.is_some());
992 assert!(result.ci_upper.is_some());
993
994 let lower = result.ci_lower.expect("no lower bound");
995 let upper = result.ci_upper.expect("no upper bound");
996 assert!(lower < result.estimate);
997 assert!(upper > result.estimate);
998 assert!((result.confidence_level.expect("no CL") - 0.95).abs() < 1e-10);
999 }
1000
1001 #[test]
1002 fn test_correlation_ci() {
1003 let result = correlation_ci(0.5_f64, 100, 0.95).expect("corr CI failed");
1004 let lower = result.ci_lower.expect("no lower");
1005 let upper = result.ci_upper.expect("no upper");
1006
1007 assert!(lower < 0.5);
1008 assert!(upper > 0.5);
1009 assert!(lower > 0.0); }
1011
1012 #[test]
1013 fn test_correlation_ci_invalid() {
1014 assert!(correlation_ci(1.5_f64, 100, 0.95).is_err());
1015 assert!(correlation_ci(0.5_f64, 2, 0.95).is_err());
1016 assert!(correlation_ci(0.5_f64, 100, 0.0).is_err());
1017 }
1018
1019 #[test]
1020 fn test_d_to_r_and_back() {
1021 let d = 0.8;
1022 let r = d_to_r(d, 50, 50);
1023 assert!(r > 0.0 && r < 1.0);
1024 let d_back = r_to_d(r);
1026 assert!((d_back - d).abs() < 0.1); }
1028
1029 #[test]
1030 fn test_r_to_d_extremes() {
1031 assert!(r_to_d(0.0).abs() < 1e-10);
1032 assert!(r_to_d(0.999) > 10.0);
1033 assert!(r_to_d(-0.999) < -10.0);
1034 }
1035
1036 #[test]
1037 fn test_normal_quantile() {
1038 assert!((normal_quantile(0.5) - 0.0).abs() < 1e-10);
1039 assert!((normal_quantile(0.975) - 1.96).abs() < 0.01);
1040 assert!((normal_quantile(0.025) + 1.96).abs() < 0.01);
1041 }
1042
1043 #[test]
1044 fn test_effect_size_result_fields() {
1045 let x = array![5.0, 6.0, 7.0, 8.0, 9.0];
1046 let y = array![3.0, 4.0, 5.0, 6.0, 7.0];
1047 let result = cohens_d(&x.view(), &y.view()).expect("d failed");
1048 assert_eq!(result.measure, "Cohen's d");
1049 assert!(result.ci_lower.is_none());
1050 assert!(result.ci_upper.is_none());
1051 }
1052}