use super::helpers::bootstrap_ci;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffectCategory {
Negligible,
Small,
Medium,
Large,
}
impl EffectCategory {
pub fn description(&self) -> &'static str {
match self {
EffectCategory::Negligible => "negligible practical significance",
EffectCategory::Small => "small effect",
EffectCategory::Medium => "medium effect",
EffectCategory::Large => "large effect",
}
}
pub fn from_cohens_d(d: f64) -> Self {
let abs_d = d.abs();
if abs_d < 0.2 {
EffectCategory::Negligible
} else if abs_d < 0.5 {
EffectCategory::Small
} else if abs_d < 0.8 {
EffectCategory::Medium
} else {
EffectCategory::Large
}
}
}
#[derive(Debug, Clone)]
pub struct StatisticalAnalysis {
pub mean: f64,
pub std_dev: f64,
pub std_error: f64,
pub ci_lower: f64,
pub ci_upper: f64,
pub confidence_level: f64,
pub n: usize,
pub cv_percent: f64,
}
impl StatisticalAnalysis {
pub fn from_samples(samples: &[f64], confidence_level: f64) -> Option<Self> {
if samples.is_empty() {
return None;
}
let valid: Vec<f64> = samples.iter().copied().filter(|x| x.is_finite()).collect();
if valid.is_empty() {
return None;
}
let n = valid.len();
let mean = valid.iter().sum::<f64>() / n as f64;
if n == 1 {
return Some(Self {
mean,
std_dev: 0.0,
std_error: 0.0,
ci_lower: mean,
ci_upper: mean,
confidence_level,
n,
cv_percent: 0.0,
});
}
let variance = valid.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
let std_dev = variance.sqrt();
let std_error = std_dev / (n as f64).sqrt();
let (ci_lower, ci_upper) = bootstrap_ci(&valid, confidence_level, 10000);
let cv_percent = if mean != 0.0 {
(std_dev / mean.abs()) * 100.0
} else {
0.0
};
Some(Self {
mean,
std_dev,
std_error,
ci_lower,
ci_upper,
confidence_level,
n,
cv_percent,
})
}
pub fn from_samples_default(samples: &[f64]) -> Option<Self> {
Self::from_samples(samples, 0.95)
}
pub fn ci_width(&self) -> f64 {
self.ci_upper - self.ci_lower
}
pub fn ci_is_narrow(&self) -> bool {
if self.mean == 0.0 {
return self.ci_width() < 0.1;
}
(self.ci_width() / self.mean.abs()) < 0.1
}
}
#[derive(Debug, Clone)]
pub struct EffectSize {
pub cohens_d: f64,
pub category: EffectCategory,
pub ci_lower: f64,
pub ci_upper: f64,
}
impl EffectSize {
pub fn cohens_d(sample1: &[f64], sample2: &[f64]) -> Option<Self> {
if sample1.is_empty() || sample2.is_empty() {
return None;
}
let n1 = sample1.len() as f64;
let n2 = sample2.len() as f64;
let mean1 = sample1.iter().sum::<f64>() / n1;
let mean2 = sample2.iter().sum::<f64>() / n2;
let var1 = sample1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (n1 - 1.0).max(1.0);
let var2 = sample2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (n2 - 1.0).max(1.0);
let pooled_var = ((n1 - 1.0) * var1 + (n2 - 1.0) * var2) / (n1 + n2 - 2.0).max(1.0);
let pooled_std = pooled_var.sqrt();
if pooled_std == 0.0 {
return Some(Self {
cohens_d: 0.0,
category: EffectCategory::Negligible,
ci_lower: 0.0,
ci_upper: 0.0,
});
}
let d = (mean1 - mean2) / pooled_std;
let category = EffectCategory::from_cohens_d(d);
let se_d = ((n1 + n2) / (n1 * n2) + d.powi(2) / (2.0 * (n1 + n2))).sqrt();
let ci_lower = d - 1.96 * se_d;
let ci_upper = d + 1.96 * se_d;
Some(Self {
cohens_d: d,
category,
ci_lower,
ci_upper,
})
}
pub fn is_significant(&self) -> bool {
self.category != EffectCategory::Negligible
}
}