use super::types::*;
use crate::{EvaluationError, EvaluationResult};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum CorrelationMethod {
Pearson,
Spearman,
Kendall,
Partial,
Multiple,
}
#[derive(Debug, Clone)]
pub struct CorrelationResult {
pub coefficient: f32,
pub p_value: f32,
pub confidence_interval: (f32, f32),
pub n: usize,
pub method: CorrelationMethod,
pub degrees_freedom: usize,
pub test_statistic: f32,
}
#[derive(Debug, Clone)]
pub struct CorrelationMatrix {
pub variables: Vec<String>,
pub coefficients: Vec<Vec<f32>>,
pub p_values: Vec<Vec<f32>>,
pub n: usize,
pub method: CorrelationMethod,
}
#[derive(Debug, Clone)]
pub struct PartialCorrelationResult {
pub coefficient: f32,
pub p_value: f32,
pub controlling_variables: Vec<String>,
pub n: usize,
pub degrees_freedom: usize,
}
pub struct CorrelationAnalyzer {
pub confidence_level: f32,
pub min_sample_size: usize,
}
impl Default for CorrelationAnalyzer {
fn default() -> Self {
Self {
confidence_level: 0.95,
min_sample_size: 3,
}
}
}
impl CorrelationAnalyzer {
pub fn new() -> Self {
Self::default()
}
pub fn with_confidence_level(mut self, level: f32) -> Self {
self.confidence_level = level.clamp(0.01, 0.99);
self
}
pub fn with_min_sample_size(mut self, size: usize) -> Self {
self.min_sample_size = size.max(2);
self
}
pub fn pearson_correlation(&self, x: &[f32], y: &[f32]) -> EvaluationResult<CorrelationResult> {
if x.len() != y.len() {
return Err(EvaluationError::InvalidInput {
message: String::from("Input vectors must have the same length"),
}
.into());
}
if x.len() < self.min_sample_size {
return Err(EvaluationError::InvalidInput {
message: format!(
"Sample size {} is too small, minimum is {}",
x.len(),
self.min_sample_size
),
}
.into());
}
let n = x.len();
let n_f = n as f32;
let filtered_pairs: Vec<(f32, f32)> = x
.iter()
.zip(y.iter())
.filter(|(a, b)| a.is_finite() && b.is_finite())
.map(|(a, b)| (*a, *b))
.collect();
if filtered_pairs.len() < self.min_sample_size {
return Err(EvaluationError::InvalidInput {
message: String::from("Too many invalid values after filtering"),
}
.into());
}
let filtered_n = filtered_pairs.len();
let filtered_n_f = filtered_n as f32;
let mean_x = filtered_pairs.iter().map(|(x, _)| x).sum::<f32>() / filtered_n_f;
let mean_y = filtered_pairs.iter().map(|(_, y)| y).sum::<f32>() / filtered_n_f;
let mut numerator = 0.0;
let mut sum_sq_x = 0.0;
let mut sum_sq_y = 0.0;
for (xi, yi) in &filtered_pairs {
let dx = xi - mean_x;
let dy = yi - mean_y;
numerator += dx * dy;
sum_sq_x += dx * dx;
sum_sq_y += dy * dy;
}
let denominator = (sum_sq_x * sum_sq_y).sqrt();
if denominator == 0.0 {
return Err(EvaluationError::InvalidInput {
message: "Cannot calculate correlation: zero variance in one or both variables"
.to_string(),
}
.into());
}
let r = numerator / denominator;
let df = filtered_n - 2;
let t_stat = if (1.0 - r * r) > 0.0 {
r * ((df as f32) / (1.0 - r * r)).sqrt()
} else {
f32::INFINITY
};
let p_value = self.calculate_t_test_p_value(t_stat, df);
let z_r = 0.5 * ((1.0 + r) / (1.0 - r)).ln();
let se_z = 1.0 / ((filtered_n - 3) as f32).sqrt();
let z_critical = self.get_critical_z_value();
let z_lower = z_r - z_critical * se_z;
let z_upper = z_r + z_critical * se_z;
let r_lower = ((2.0 * z_lower).exp() - 1.0) / ((2.0 * z_lower).exp() + 1.0);
let r_upper = ((2.0 * z_upper).exp() - 1.0) / ((2.0 * z_upper).exp() + 1.0);
Ok(CorrelationResult {
coefficient: r,
p_value,
confidence_interval: (r_lower, r_upper),
n: filtered_n,
method: CorrelationMethod::Pearson,
degrees_freedom: df,
test_statistic: t_stat,
})
}
pub fn spearman_correlation(
&self,
x: &[f32],
y: &[f32],
) -> EvaluationResult<CorrelationResult> {
if x.len() != y.len() {
return Err(EvaluationError::InvalidInput {
message: String::from("Input vectors must have the same length"),
}
.into());
}
if x.len() < self.min_sample_size {
return Err(EvaluationError::InvalidInput {
message: format!(
"Sample size {} is too small, minimum is {}",
x.len(),
self.min_sample_size
),
}
.into());
}
let ranks_x = self.assign_ranks(x)?;
let ranks_y = self.assign_ranks(y)?;
self.pearson_correlation(&ranks_x, &ranks_y)
.map(|mut result| {
result.method = CorrelationMethod::Spearman;
result
})
}
pub fn kendall_correlation(&self, x: &[f32], y: &[f32]) -> EvaluationResult<CorrelationResult> {
if x.len() != y.len() {
return Err(EvaluationError::InvalidInput {
message: String::from("Input vectors must have the same length"),
}
.into());
}
if x.len() < self.min_sample_size {
return Err(EvaluationError::InvalidInput {
message: format!(
"Sample size {} is too small, minimum is {}",
x.len(),
self.min_sample_size
),
}
.into());
}
let n = x.len();
let mut concordant = 0;
let mut discordant = 0;
let mut ties_x = 0;
let mut ties_y = 0;
let mut _ties_xy = 0;
for i in 0..n {
for j in (i + 1)..n {
if !x[i].is_finite() || !x[j].is_finite() || !y[i].is_finite() || !y[j].is_finite()
{
continue;
}
let dx = x[i] - x[j];
let dy = y[i] - y[j];
if dx == 0.0 && dy == 0.0 {
_ties_xy += 1;
} else if dx == 0.0 {
ties_x += 1;
} else if dy == 0.0 {
ties_y += 1;
} else if dx.signum() == dy.signum() {
concordant += 1;
} else {
discordant += 1;
}
}
}
let n_pairs = n * (n - 1) / 2;
if n_pairs == 0 {
return Err(EvaluationError::InvalidInput {
message: String::from("Cannot calculate Kendall's tau: insufficient valid pairs"),
}
.into());
}
let numerator = (concordant - discordant) as f32;
let denominator = ((n_pairs - ties_x) * (n_pairs - ties_y)) as f32;
if denominator <= 0.0 {
return Err(EvaluationError::InvalidInput {
message: String::from("Cannot calculate Kendall's tau: too many ties"),
}
.into());
}
let tau = numerator / denominator.sqrt();
let var_tau = (2.0 * (2.0 * n as f32 + 5.0)) / (9.0 * n as f32 * (n as f32 - 1.0));
let se_tau = var_tau.sqrt();
let z_stat = tau / se_tau;
let p_value = 2.0 * (1.0 - self.standard_normal_cdf(z_stat.abs()));
let z_critical = self.get_critical_z_value();
let margin = z_critical * se_tau;
let ci_lower = (tau - margin).max(-1.0);
let ci_upper = (tau + margin).min(1.0);
Ok(CorrelationResult {
coefficient: tau,
p_value,
confidence_interval: (ci_lower, ci_upper),
n,
method: CorrelationMethod::Kendall,
degrees_freedom: n - 2,
test_statistic: z_stat,
})
}
pub fn correlation_matrix(
&self,
data: &HashMap<String, Vec<f32>>,
method: CorrelationMethod,
) -> EvaluationResult<CorrelationMatrix> {
if data.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("Data cannot be empty"),
}
.into());
}
let variables: Vec<String> = data.keys().cloned().collect();
let n_vars = variables.len();
let first_length = data
.values()
.next()
.expect("iterator should have next element")
.len();
for (name, values) in data {
if values.len() != first_length {
return Err(EvaluationError::InvalidInput {
message: format!("Variable '{}' has different length", name),
}
.into());
}
}
let mut coefficients = vec![vec![0.0; n_vars]; n_vars];
let mut p_values = vec![vec![0.0; n_vars]; n_vars];
for (i, var1) in variables.iter().enumerate() {
for (j, var2) in variables.iter().enumerate() {
if i == j {
coefficients[i][j] = 1.0;
p_values[i][j] = 0.0;
} else if i < j {
let x = &data[var1];
let y = &data[var2];
let result = match method {
CorrelationMethod::Pearson => self.pearson_correlation(x, y)?,
CorrelationMethod::Spearman => self.spearman_correlation(x, y)?,
CorrelationMethod::Kendall => self.kendall_correlation(x, y)?,
_ => {
return Err(EvaluationError::FeatureNotSupported {
feature: format!("{:?} correlation matrix", method),
}
.into())
}
};
coefficients[i][j] = result.coefficient;
coefficients[j][i] = result.coefficient;
p_values[i][j] = result.p_value;
p_values[j][i] = result.p_value;
}
}
}
Ok(CorrelationMatrix {
variables,
coefficients,
p_values,
n: first_length,
method,
})
}
pub fn partial_correlation(
&self,
x: &[f32],
y: &[f32],
control_vars: &[Vec<f32>],
control_names: Vec<String>,
) -> EvaluationResult<PartialCorrelationResult> {
if x.len() != y.len() {
return Err(EvaluationError::InvalidInput {
message: String::from("X and Y must have the same length"),
}
.into());
}
for (i, control) in control_vars.iter().enumerate() {
if control.len() != x.len() {
return Err(EvaluationError::InvalidInput {
message: format!("Control variable {} has different length", i),
}
.into());
}
}
if control_vars.is_empty() {
let result = self.pearson_correlation(x, y)?;
return Ok(PartialCorrelationResult {
coefficient: result.coefficient,
p_value: result.p_value,
controlling_variables: control_names,
n: result.n,
degrees_freedom: result.degrees_freedom,
});
}
if control_vars.len() != 1 {
return Err(EvaluationError::FeatureNotSupported {
feature: String::from("Higher-order partial correlations"),
}
.into());
}
let z = &control_vars[0];
let r_xy = self.pearson_correlation(x, y)?.coefficient;
let r_xz = self.pearson_correlation(x, z)?.coefficient;
let r_yz = self.pearson_correlation(y, z)?.coefficient;
let numerator = r_xy - (r_xz * r_yz);
let denominator = ((1.0 - r_xz * r_xz) * (1.0 - r_yz * r_yz)).sqrt();
if denominator == 0.0 {
return Err(EvaluationError::InvalidInput {
message:
"Cannot calculate partial correlation: control variable explains all variance"
.to_string(),
}
.into());
}
let r_partial = numerator / denominator;
let df = x.len() - control_vars.len() - 2;
let t_stat = if (1.0 - r_partial * r_partial) > 0.0 {
r_partial * ((df as f32) / (1.0 - r_partial * r_partial)).sqrt()
} else {
f32::INFINITY
};
let p_value = self.calculate_t_test_p_value(t_stat, df);
Ok(PartialCorrelationResult {
coefficient: r_partial,
p_value,
controlling_variables: control_names,
n: x.len(),
degrees_freedom: df,
})
}
fn assign_ranks(&self, values: &[f32]) -> EvaluationResult<Vec<f32>> {
let mut indexed_values: Vec<(usize, f32)> = values
.iter()
.enumerate()
.filter(|(_, &v)| v.is_finite())
.map(|(i, &v)| (i, v))
.collect();
if indexed_values.is_empty() {
return Err(EvaluationError::InvalidInput {
message: String::from("No valid values found for ranking"),
}
.into());
}
indexed_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut ranks = vec![0.0; values.len()];
let mut i = 0;
while i < indexed_values.len() {
let current_value = indexed_values[i].1;
let mut j = i + 1;
while j < indexed_values.len()
&& (indexed_values[j].1 - current_value).abs() < f32::EPSILON
{
j += 1;
}
let avg_rank = ((i + 1) + j) as f32 / 2.0;
for k in i..j {
ranks[indexed_values[k].0] = avg_rank;
}
i = j;
}
for (i, &value) in values.iter().enumerate() {
if !value.is_finite() {
ranks[i] = 0.0;
}
}
Ok(ranks)
}
fn calculate_t_test_p_value(&self, t_stat: f32, df: usize) -> f32 {
if !t_stat.is_finite() || df == 0 {
return 1.0;
}
if df > 30 {
return 2.0 * (1.0 - self.standard_normal_cdf(t_stat.abs()));
}
let p_approx = 2.0 * (1.0 - self.standard_normal_cdf(t_stat.abs()));
let adjustment = 1.0 + 1.0 / (4.0 * df as f32);
(p_approx * adjustment).min(1.0)
}
fn standard_normal_cdf(&self, x: f32) -> f32 {
if x < -8.0 {
return 0.0;
}
if x > 8.0 {
return 1.0;
}
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_abs = x.abs();
let t = 1.0 / (1.0 + p * x_abs);
let y = 1.0
- (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x_abs * x_abs / 2.0).exp();
0.5 * (1.0 + sign * y)
}
fn get_critical_z_value(&self) -> f32 {
match (self.confidence_level * 100.0) as u32 {
90 => 1.645,
95 => 1.96,
99 => 2.576,
_ => {
let alpha = 1.0 - self.confidence_level;
if alpha <= 0.01 {
2.576
} else if alpha <= 0.05 {
1.96
} else {
1.645
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pearson_correlation_perfect_positive() {
let analyzer = CorrelationAnalyzer::new();
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
let result = analyzer.pearson_correlation(&x, &y).unwrap();
assert!((result.coefficient - 1.0).abs() < 0.001);
assert!(
result.p_value >= 0.0 && result.p_value <= 1.0,
"P-value should be valid: {}",
result.p_value
);
}
#[test]
fn test_pearson_correlation_perfect_negative() {
let analyzer = CorrelationAnalyzer::new();
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![5.0, 4.0, 3.0, 2.0, 1.0];
let result = analyzer.pearson_correlation(&x, &y).unwrap();
assert!((result.coefficient + 1.0).abs() < 0.001);
assert!(
result.p_value >= 0.0 && result.p_value <= 1.0,
"P-value should be valid: {}",
result.p_value
);
}
#[test]
fn test_spearman_correlation() {
let analyzer = CorrelationAnalyzer::new();
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![1.0, 4.0, 9.0, 16.0, 25.0];
let result = analyzer.spearman_correlation(&x, &y).unwrap();
assert!((result.coefficient - 1.0).abs() < 0.001); }
#[test]
fn test_kendall_correlation() {
let analyzer = CorrelationAnalyzer::new();
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = analyzer.kendall_correlation(&x, &y).unwrap();
assert!((result.coefficient - 1.0).abs() < 0.001);
}
#[test]
fn test_correlation_matrix() {
let analyzer = CorrelationAnalyzer::new();
let mut data = HashMap::new();
data.insert(String::from("x"), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
data.insert(String::from("y"), vec![2.0, 4.0, 6.0, 8.0, 10.0]);
data.insert(String::from("z"), vec![5.0, 4.0, 3.0, 2.0, 1.0]);
let result = analyzer
.correlation_matrix(&data, CorrelationMethod::Pearson)
.unwrap();
assert_eq!(result.variables.len(), 3);
assert_eq!(result.coefficients.len(), 3);
assert_eq!(result.coefficients[0].len(), 3);
for i in 0..3 {
assert!((result.coefficients[i][i] - 1.0).abs() < 0.001);
}
}
#[test]
fn test_partial_correlation() {
let analyzer = CorrelationAnalyzer::new();
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
let z = vec![1.0, 1.0, 2.0, 2.0, 2.0];
let result = analyzer
.partial_correlation(&x, &y, &[z], vec![String::from("z")])
.unwrap();
assert!(result.coefficient.is_finite());
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
}
#[test]
fn test_rank_assignment() {
let analyzer = CorrelationAnalyzer::new();
let values = vec![3.0, 1.0, 4.0, 1.0, 5.0];
let ranks = analyzer.assign_ranks(&values).unwrap();
assert!((ranks[0] - 3.0).abs() < 0.001); assert!((ranks[1] - 1.5).abs() < 0.001); assert!((ranks[2] - 4.0).abs() < 0.001); assert!((ranks[3] - 1.5).abs() < 0.001); assert!((ranks[4] - 5.0).abs() < 0.001); }
#[test]
fn test_invalid_input_handling() {
let analyzer = CorrelationAnalyzer::new();
let x = vec![1.0, 2.0, 3.0];
let y = vec![1.0, 2.0];
assert!(analyzer.pearson_correlation(&x, &y).is_err());
let x_small = vec![1.0];
let y_small = vec![2.0];
assert!(analyzer.pearson_correlation(&x_small, &y_small).is_err());
let x_nan = vec![1.0, f32::NAN, 3.0, 4.0, 5.0];
let y_clean = vec![2.0, 4.0, 6.0, 8.0, 10.0];
let result = analyzer.pearson_correlation(&x_nan, &y_clean);
assert!(result.is_ok()); }
}