use super::types::*;
use crate::{EvaluationError, EvaluationResult};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct StatisticalAnalyzer {
config: StatisticalConfig,
}
#[derive(Debug, Clone)]
pub struct StatisticalConfig {
pub alpha: f32,
pub bootstrap_samples: usize,
pub seed: Option<u64>,
}
impl Default for StatisticalConfig {
fn default() -> Self {
Self {
alpha: 0.05,
bootstrap_samples: 10000,
seed: Some(42),
}
}
}
impl StatisticalAnalyzer {
pub fn new() -> Self {
Self {
config: StatisticalConfig::default(),
}
}
pub fn with_config(config: StatisticalConfig) -> Self {
Self { config }
}
pub fn paired_t_test(
&self,
before: &[f32],
after: &[f32],
alpha: Option<f32>,
) -> EvaluationResult<StatisticalTestResult> {
if before.len() != after.len() {
return Err(EvaluationError::InvalidInput {
message: String::from("Paired samples must have equal length"),
}
.into());
}
if before.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Cannot perform t-test on empty samples"),
}
.into());
}
let alpha = alpha.unwrap_or(self.config.alpha);
let differences: Vec<f32> = before
.iter()
.zip(after.iter())
.map(|(b, a)| a - b)
.collect();
let n = differences.len() as f32;
let mean_diff = differences.iter().sum::<f32>() / n;
let variance = differences
.iter()
.map(|d| (d - mean_diff).powi(2))
.sum::<f32>()
/ (n - 1.0);
let std_error = (variance / n).sqrt();
let t_statistic = if std_error > 0.0 && std_error.is_finite() {
mean_diff / std_error
} else if mean_diff.abs() > 1e-6 {
if mean_diff > 0.0 {
100.0
} else {
-100.0
}
} else {
0.0 };
let degrees_of_freedom = (n - 1.0) as usize;
let abs_t = t_statistic.abs();
let p_value = if abs_t < 1.0 {
1.0 - abs_t * 0.3 } else if abs_t < 2.0 {
0.7 - (abs_t - 1.0) * 0.6 } else if abs_t < 3.0 {
0.1 - (abs_t - 2.0) * 0.08 } else {
0.02 - (abs_t - 3.0) * 0.005 }
.max(0.001)
.min(1.0);
let effect_size = mean_diff / variance.sqrt(); let is_significant = p_value < alpha;
let interpretation = if is_significant {
format!(
"significant difference detected (p = {:.4}, α = {:.3})",
p_value, alpha
)
} else {
format!(
"No significant difference (p = {:.4}, α = {:.3})",
p_value, alpha
)
};
Ok(StatisticalTestResult {
test_statistic: t_statistic as f64,
p_value: p_value as f64,
degrees_of_freedom: Some(degrees_of_freedom as usize),
effect_size: Some(effect_size as f64),
confidence_interval: None, test_type: String::from("PairedTTest"),
alpha: alpha as f64,
is_significant,
interpretation,
confidence_level: 1.0 - alpha as f64,
})
}
pub fn mann_whitney_u_test(
&self,
group1: &[f32],
group2: &[f32],
alpha: Option<f32>,
) -> EvaluationResult<StatisticalTestResult> {
if group1.is_empty() || group2.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Cannot perform Mann-Whitney U test on empty groups"),
}
.into());
}
let alpha = alpha.unwrap_or(self.config.alpha);
let mut combined: Vec<(f32, usize)> = group1
.iter()
.map(|&x| (x, 0))
.chain(group2.iter().map(|&x| (x, 1)))
.collect();
combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut ranks = vec![0.0; combined.len()];
for (i, _) in combined.iter().enumerate() {
ranks[i] = (i + 1) as f32;
}
let r1: f32 = combined
.iter()
.zip(ranks.iter())
.filter(|((_, group), _)| *group == 0)
.map(|(_, rank)| rank)
.sum();
let n1 = group1.len() as f32;
let n2 = group2.len() as f32;
let u1 = r1 - (n1 * (n1 + 1.0)) / 2.0;
let u2 = n1 * n2 - u1;
let u_statistic = u1.min(u2);
let mean_u = n1 * n2 / 2.0;
let std_u = ((n1 * n2 * (n1 + n2 + 1.0)) / 12.0).sqrt();
let z_score = (u_statistic - mean_u) / std_u;
let p_value = 2.0 * (1.0 - self.normal_cdf(z_score.abs()));
let effect_size = 1.0 - (2.0 * u_statistic) / (n1 * n2); let is_significant = p_value < alpha;
let interpretation = if is_significant {
format!(
"Significant difference between groups (p = {:.4}, α = {:.3})",
p_value, alpha
)
} else {
format!(
"No significant difference between groups (p = {:.4}, α = {:.3})",
p_value, alpha
)
};
Ok(StatisticalTestResult {
test_statistic: u_statistic as f64,
p_value: p_value as f64,
degrees_of_freedom: Some(0), effect_size: Some(effect_size as f64),
confidence_interval: None,
test_type: String::from("MannWhitneyU"),
alpha: alpha as f64,
is_significant,
interpretation,
confidence_level: 1.0 - alpha as f64,
})
}
fn t_cdf(&self, t: f32, df: f32) -> f32 {
if !t.is_finite() || !df.is_finite() || df <= 0.0 {
return 0.5;
}
let abs_t = t.abs();
let prob = if abs_t < 0.5 {
0.5 + abs_t * 0.2 } else if abs_t < 1.0 {
0.7 + (abs_t - 0.5) * 0.2
} else if abs_t < 2.0 {
0.9 + (abs_t - 1.0) * 0.08
} else if abs_t < 3.0 {
0.98 + (abs_t - 2.0) * 0.015
} else {
0.995 + (abs_t - 3.0) * 0.001 };
let df_adjustment = if df < 10.0 {
0.95 + df * 0.005 } else {
1.0
};
let result = prob * df_adjustment;
if t >= 0.0 {
result.max(0.5).min(1.0)
} else {
(1.0 - result).max(0.0).min(0.5)
}
}
fn normal_cdf(&self, z: f32) -> f32 {
if !z.is_finite() {
return if z > 0.0 { 1.0 } else { 0.0 };
}
let result = 0.5 * (1.0 + self.erf(z / std::f32::consts::SQRT_2));
result.max(0.0).min(1.0)
}
fn erf(&self, x: f32) -> f32 {
let a1 = 0.254_829_592;
let a2 = -0.284_496_736;
let a3 = 1.421_413_741;
let a4 = -1.453_152_027;
let a5 = 1.061_405_429;
let p = 0.327_591_1;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + p * x);
let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
sign * y
}
pub fn correlation_test(
&self,
x: &[f32],
y: &[f32],
) -> EvaluationResult<StatisticalTestResult> {
if x.len() != y.len() || x.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Variables must have equal non-zero length"),
}
.into());
}
let n = x.len() as f32;
let x_mean = x.iter().sum::<f32>() / n;
let y_mean = y.iter().sum::<f32>() / n;
let numerator: f32 = x
.iter()
.zip(y.iter())
.map(|(xi, yi)| (xi - x_mean) * (yi - y_mean))
.sum();
let x_var: f32 = x.iter().map(|xi| (xi - x_mean).powi(2)).sum();
let y_var: f32 = y.iter().map(|yi| (yi - y_mean).powi(2)).sum();
let denominator = (x_var * y_var).sqrt();
let correlation = if denominator > 0.0 && denominator.is_finite() {
(numerator / denominator).clamp(-1.0, 1.0)
} else {
0.0 };
let t_statistic = if correlation.abs() < 0.9999 && correlation.is_finite() {
correlation * ((n - 2.0) / (1.0 - correlation.powi(2))).sqrt()
} else if correlation.abs() > 0.9999 {
if correlation > 0.0 {
100.0
} else {
-100.0
}
} else {
0.0
};
let degrees_of_freedom = (n - 2.0) as usize;
let abs_t = t_statistic.abs();
let p_value = if abs_t < 1.0 {
1.0 - abs_t * 0.3
} else if abs_t < 2.0 {
0.7 - (abs_t - 1.0) * 0.6
} else if abs_t < 3.0 {
0.1 - (abs_t - 2.0) * 0.08
} else {
0.02 - (abs_t - 3.0) * 0.005
}
.max(0.001)
.min(1.0);
let is_significant = p_value < self.config.alpha;
let interpretation = if is_significant {
format!(
"significant correlation (r = {:.4}, p = {:.4})",
correlation, p_value
)
} else {
format!(
"No significant correlation (r = {:.4}, p = {:.4})",
correlation, p_value
)
};
Ok(StatisticalTestResult {
test_statistic: t_statistic as f64,
p_value: p_value as f64,
degrees_of_freedom: Some(degrees_of_freedom),
effect_size: Some(correlation as f64),
confidence_interval: None,
test_type: String::from("CorrelationTest"),
alpha: self.config.alpha as f64,
is_significant,
interpretation,
confidence_level: 1.0 - self.config.alpha as f64,
})
}
pub fn independent_t_test(
&self,
group1: &[f32],
group2: &[f32],
) -> EvaluationResult<StatisticalTestResult> {
if group1.is_empty() || group2.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Cannot perform t-test on empty groups"),
}
.into());
}
let n1 = group1.len() as f32;
let n2 = group2.len() as f32;
let mean1 = group1.iter().sum::<f32>() / n1;
let mean2 = group2.iter().sum::<f32>() / n2;
let var1 = group1.iter().map(|x| (x - mean1).powi(2)).sum::<f32>() / (n1 - 1.0);
let var2 = group2.iter().map(|x| (x - mean2).powi(2)).sum::<f32>() / (n2 - 1.0);
let pooled_std = ((var1 * (n1 - 1.0) + var2 * (n2 - 1.0)) / (n1 + n2 - 2.0)).sqrt();
let standard_error = pooled_std * (1.0 / n1 + 1.0 / n2).sqrt();
let mean_diff = mean1 - mean2;
let t_statistic = if standard_error > 0.0 && standard_error.is_finite() {
mean_diff / standard_error
} else if mean_diff.abs() > 1e-6 {
if mean_diff > 0.0 {
100.0
} else {
-100.0
}
} else {
0.0 };
let degrees_of_freedom = (n1 + n2 - 2.0) as usize;
let abs_t = t_statistic.abs();
let p_value = if abs_t < 1.0 {
1.0 - abs_t * 0.3
} else if abs_t < 2.0 {
0.7 - (abs_t - 1.0) * 0.6
} else if abs_t < 3.0 {
0.1 - (abs_t - 2.0) * 0.08
} else {
0.02 - (abs_t - 3.0) * 0.005
}
.max(0.001)
.min(1.0);
let is_significant = p_value < self.config.alpha;
let interpretation = if is_significant {
format!("Significant difference between groups (p = {:.4})", p_value)
} else {
format!(
"No significant difference between groups (p = {:.4})",
p_value
)
};
Ok(StatisticalTestResult {
test_statistic: t_statistic as f64,
p_value: p_value as f64,
degrees_of_freedom: Some(degrees_of_freedom),
effect_size: Some((mean1 - mean2) as f64 / pooled_std as f64),
confidence_interval: None,
test_type: String::from("IndependentTTest"),
alpha: self.config.alpha as f64,
is_significant,
interpretation,
confidence_level: 1.0 - self.config.alpha as f64,
})
}
pub fn power_analysis_t_test(&self, config: &ABTestConfig) -> PowerAnalysisResult {
let alpha = config.alpha as f32;
let effect_size = config.effect_size as f32;
let n = (config.sample_size_a + config.sample_size_b) as f32 / 2.0;
let ncp = effect_size * (n / 2.0).sqrt(); let critical_t = self.t_inv(1.0 - alpha / 2.0, (2.0 * n - 2.0) as f32);
let power = 1.0 - self.t_cdf(critical_t, (2.0 * n - 2.0) as f32) as f64;
PowerAnalysisResult {
achieved_power: power,
effect_size: config.effect_size,
sample_size: (config.sample_size_a + config.sample_size_b) / 2,
alpha: config.alpha,
}
}
pub fn bootstrap_confidence_interval(
&self,
data: &[f32],
statistic: fn(&[f32]) -> f32,
) -> EvaluationResult<(f64, f64)> {
if data.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Cannot bootstrap empty data"),
}
.into());
}
let mut bootstrap_stats = Vec::with_capacity(self.config.bootstrap_samples);
for i in 0..self.config.bootstrap_samples {
let mut sample = Vec::with_capacity(data.len());
let seed = (i * 31 + 17) as u64; for j in 0..data.len() {
let a = 1_664_525_u64;
let c = 1_013_904_223_u64;
let m = 1u64 << 32;
let random_val = (a.wrapping_mul(seed + j as u64).wrapping_add(c)) % m;
let idx = (random_val as usize) % data.len();
sample.push(data[idx]);
}
bootstrap_stats.push(statistic(&sample));
}
bootstrap_stats.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let lower_idx = (0.025 * self.config.bootstrap_samples as f32) as usize;
let upper_idx = (0.975 * self.config.bootstrap_samples as f32) as usize;
Ok((
bootstrap_stats[lower_idx] as f64,
bootstrap_stats[upper_idx] as f64,
))
}
pub fn multiple_comparison_correction(
&self,
p_values: &[f32],
method: MultipleComparisonCorrection,
) -> EvaluationResult<MultipleComparisonResult> {
if p_values.is_empty() {
return Ok(MultipleComparisonResult {
adjusted_p_values: vec![],
original_p_values: vec![],
method,
});
}
let adjusted_p_values = match method {
MultipleComparisonCorrection::None => p_values.to_vec(),
MultipleComparisonCorrection::Bonferroni => {
let n = p_values.len() as f32;
p_values.iter().map(|&p| (p * n).min(1.0)).collect()
}
MultipleComparisonCorrection::BenjaminiHochberg => {
let mut indexed_p: Vec<(usize, f32)> =
p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
indexed_p
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let n = p_values.len() as f32;
let mut corrected = vec![0.0; p_values.len()];
for (rank, (original_idx, p)) in indexed_p.iter().enumerate() {
let adjusted = p * n / (rank + 1) as f32;
corrected[*original_idx] = adjusted.min(1.0);
}
corrected
}
_ => {
p_values.to_vec()
}
};
Ok(MultipleComparisonResult {
adjusted_p_values,
original_p_values: p_values.to_vec(),
method,
})
}
fn t_inv(&self, p: f32, df: f32) -> f32 {
if p >= 1.0 {
return f32::INFINITY;
}
if p <= 0.0 {
return f32::NEG_INFINITY;
}
if df > 30.0 {
return self.normal_inv(p);
}
let z = self.normal_inv(p);
z * (1.0 + (z * z + 1.0) / (4.0 * df))
}
fn normal_inv(&self, p: f32) -> f32 {
if p >= 1.0 {
return f32::INFINITY;
}
if p <= 0.0 {
return f32::NEG_INFINITY;
}
if p == 0.5 {
return 0.0;
}
let t = (-2.0 * p.ln()).sqrt();
t - (2.515_517 + 0.802_853 * t + 0.010_328 * t * t)
/ (1.0 + 1.432_788 * t + 0.189_269 * t * t + 0.001_308 * t * t * t)
}
pub fn correlation(&self, x: &[f32], y: &[f32]) -> EvaluationResult<f64> {
if x.len() != y.len() || x.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Variables must have equal non-zero length"),
}
.into());
}
let n = x.len() as f32;
let x_mean = x.iter().sum::<f32>() / n;
let y_mean = y.iter().sum::<f32>() / n;
let numerator: f32 = x
.iter()
.zip(y.iter())
.map(|(xi, yi)| (xi - x_mean) * (yi - y_mean))
.sum();
let x_var: f32 = x.iter().map(|xi| (xi - x_mean).powi(2)).sum();
let y_var: f32 = y.iter().map(|yi| (yi - y_mean).powi(2)).sum();
let correlation = numerator / (x_var * y_var).sqrt();
Ok(correlation as f64)
}
pub fn linear_regression(&self, x: &[f32], y: &[f32]) -> EvaluationResult<RegressionResult> {
if x.len() != y.len() || x.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Variables must have equal non-zero length"),
}
.into());
}
let n = x.len() as f32;
let x_mean = x.iter().sum::<f32>() / n;
let y_mean = y.iter().sum::<f32>() / n;
let numerator: f32 = x
.iter()
.zip(y.iter())
.map(|(xi, yi)| (xi - x_mean) * (yi - y_mean))
.sum();
let denominator: f32 = x.iter().map(|xi| (xi - x_mean).powi(2)).sum();
if denominator == 0.0 {
return Err(EvaluationError::InvalidInput {
message: String::from("No variation in x variable"),
}
.into());
}
let slope = numerator / denominator;
let intercept = y_mean - slope * x_mean;
let y_pred: Vec<f32> = x.iter().map(|xi| slope * xi + intercept).collect();
let ss_res: f32 = y
.iter()
.zip(y_pred.iter())
.map(|(yi, y_pred_i)| (yi - y_pred_i).powi(2))
.sum();
let ss_tot: f32 = y.iter().map(|yi| (yi - y_mean).powi(2)).sum();
let r_squared = if ss_tot > 0.0 {
1.0 - ss_res / ss_tot
} else {
1.0 };
let residual_se = (ss_res / (n - 2.0)).sqrt();
let se_slope = if denominator > 0.0 {
residual_se / denominator.sqrt()
} else {
0.0
};
let se_intercept = residual_se * (1.0 / n + x_mean.powi(2) / denominator).sqrt();
let t_slope = if se_slope > 0.0 {
slope / se_slope
} else {
0.0
};
let t_intercept = if se_intercept > 0.0 {
intercept / se_intercept
} else {
0.0
};
let df = n - 2.0;
let p_slope = 2.0 * (1.0 - self.t_cdf(t_slope.abs(), df));
let p_intercept = 2.0 * (1.0 - self.t_cdf(t_intercept.abs(), df));
let f_statistic = if ss_res > 0.0 {
(ss_tot - ss_res) / (ss_res / (n - 2.0))
} else {
f32::INFINITY
};
let f_p_value = if f_statistic.is_finite() {
1.0 - self.f_cdf(f_statistic, 1.0, df)
} else {
0.0
};
let adjusted_r_squared = 1.0 - (1.0 - r_squared) * (n - 1.0) / (n - 2.0);
Ok(RegressionResult {
coefficients: vec![intercept as f64, slope as f64],
r_squared: r_squared as f64,
adjusted_r_squared: adjusted_r_squared as f64,
standard_errors: vec![se_intercept as f64, se_slope as f64],
t_statistics: vec![t_intercept as f64, t_slope as f64],
p_values: vec![p_intercept as f64, p_slope as f64],
residual_standard_error: residual_se as f64,
f_statistic: f_statistic as f64,
f_p_value: f_p_value as f64,
degrees_of_freedom: (1, df as usize),
slope: slope as f64,
intercept: intercept as f64,
p_value: f_p_value as f64,
standard_error: residual_se as f64,
})
}
fn f_cdf(&self, f: f32, df1: f32, df2: f32) -> f32 {
if !f.is_finite() || f <= 0.0 || df1 <= 0.0 || df2 <= 0.0 {
return 0.0;
}
if df1 == 1.0 {
let t_equiv = f.sqrt();
let t_p = self.t_cdf(t_equiv, df2);
return 2.0 * t_p - 1.0;
}
let mean_f = if df2 > 2.0 { df2 / (df2 - 2.0) } else { 1.0 };
if f < mean_f {
f / (2.0 * mean_f)
} else {
0.5 + (f - mean_f) / (4.0 * mean_f)
}
.min(0.99)
}
}
impl Default for StatisticalAnalyzer {
fn default() -> Self {
Self::new()
}
}