use crate::error::{Result, StatError};
#[derive(Debug, Clone)]
pub struct DistanceCorResult {
pub dcor: f64,
pub dcov: f64,
pub dvar_x: f64,
pub dvar_y: f64,
pub statistic: f64,
pub p_value: Option<f64>,
pub n: usize,
pub method: String,
}
pub fn distance_cor(x: &[f64], y: &[f64]) -> Result<DistanceCorResult> {
validate_input(x, y)?;
let n = x.len();
let a = distance_matrix(x);
let b = distance_matrix(y);
let a_centered = double_center(&a);
let b_centered = double_center(&b);
let dcov_sq = distance_covariance_sq(&a_centered, &b_centered);
let dvar_x_sq = distance_covariance_sq(&a_centered, &a_centered);
let dvar_y_sq = distance_covariance_sq(&b_centered, &b_centered);
let dcov = dcov_sq.max(0.0).sqrt();
let dvar_x = dvar_x_sq.max(0.0).sqrt();
let dvar_y = dvar_y_sq.max(0.0).sqrt();
let dcor = if dvar_x > 0.0 && dvar_y > 0.0 {
(dcov_sq / (dvar_x_sq * dvar_y_sq).sqrt()).max(0.0).sqrt()
} else {
0.0
};
let dcor = dcor.clamp(0.0, 1.0);
let statistic = n as f64 * dcov_sq;
Ok(DistanceCorResult {
dcor,
dcov,
dvar_x,
dvar_y,
statistic,
p_value: None,
n,
method: "Distance correlation".to_string(),
})
}
pub fn distance_cor_test(
x: &[f64],
y: &[f64],
n_permutations: usize,
seed: Option<u64>,
) -> Result<DistanceCorResult> {
validate_input(x, y)?;
let n = x.len();
let observed = distance_cor(x, y)?;
let observed_stat = observed.statistic;
let mut rng = SimpleRng::new(seed.unwrap_or(12345));
let mut count_greater = 0usize;
let mut y_perm: Vec<f64> = y.to_vec();
for _ in 0..n_permutations {
fisher_yates_shuffle(&mut y_perm, &mut rng);
let perm_result = distance_cor(x, &y_perm)?;
if perm_result.statistic >= observed_stat {
count_greater += 1;
}
}
let p_value = (count_greater as f64 + 1.0) / (n_permutations as f64 + 1.0);
Ok(DistanceCorResult {
dcor: observed.dcor,
dcov: observed.dcov,
dvar_x: observed.dvar_x,
dvar_y: observed.dvar_y,
statistic: observed_stat,
p_value: Some(p_value),
n,
method: format!(
"Distance correlation test ({} permutations)",
n_permutations
),
})
}
fn validate_input(x: &[f64], y: &[f64]) -> Result<()> {
if x.is_empty() || y.is_empty() {
return Err(StatError::EmptyData);
}
if x.len() != y.len() {
return Err(StatError::InvalidParameter(format!(
"x and y must have same length: {} vs {}",
x.len(),
y.len()
)));
}
if x.len() < 3 {
return Err(StatError::InsufficientData {
needed: 3,
got: x.len(),
});
}
for (i, (&xi, &yi)) in x.iter().zip(y.iter()).enumerate() {
if !xi.is_finite() || !yi.is_finite() {
return Err(StatError::InvalidParameter(format!(
"Non-finite value at index {}: x={}, y={}",
i, xi, yi
)));
}
}
Ok(())
}
fn distance_matrix(x: &[f64]) -> Vec<Vec<f64>> {
let n = x.len();
let mut d = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
d[i][j] = (x[i] - x[j]).abs();
}
}
d
}
fn double_center(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = a.len();
let row_means: Vec<f64> = a
.iter()
.map(|row| row.iter().sum::<f64>() / n as f64)
.collect();
let col_means: Vec<f64> = (0..n)
.map(|j| a.iter().map(|row| row[j]).sum::<f64>() / n as f64)
.collect();
let grand_mean: f64 = row_means.iter().sum::<f64>() / n as f64;
let mut centered = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
centered[i][j] = a[i][j] - row_means[i] - col_means[j] + grand_mean;
}
}
centered
}
fn distance_covariance_sq(a: &[Vec<f64>], b: &[Vec<f64>]) -> f64 {
let n = a.len();
let n_sq = (n * n) as f64;
let mut sum = 0.0;
for i in 0..n {
for j in 0..n {
sum += a[i][j] * b[i][j];
}
}
sum / n_sq
}
struct SimpleRng {
state: u64,
}
impl SimpleRng {
fn new(seed: u64) -> Self {
Self {
state: if seed == 0 { 1 } else { seed },
}
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
fn next_usize(&mut self, max: usize) -> usize {
(self.next_u64() as usize) % max
}
}
fn fisher_yates_shuffle(arr: &mut [f64], rng: &mut SimpleRng) {
let n = arr.len();
for i in (1..n).rev() {
let j = rng.next_usize(i + 1);
arr.swap(i, j);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance_cor_linear() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
let result = distance_cor(&x, &y).unwrap();
assert!(result.dcor > 0.99);
}
#[test]
fn test_distance_cor_nonlinear() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
let result = distance_cor(&x, &y).unwrap();
assert!(result.dcor > 0.9);
}
#[test]
fn test_distance_cor_weak() {
let x = vec![1.0, 5.0, 2.0, 8.0, 3.0, 9.0, 4.0, 7.0, 6.0, 10.0];
let y = vec![3.0, 7.0, 1.0, 6.0, 9.0, 2.0, 8.0, 4.0, 10.0, 5.0];
let result = distance_cor(&x, &y).unwrap();
assert!(result.dcor >= 0.0 && result.dcor <= 1.0);
assert!(result.dcor < 0.99);
}
#[test]
fn test_distance_cor_symmetric() {
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_xy = distance_cor(&x, &y).unwrap();
let result_yx = distance_cor(&y, &x).unwrap();
assert!((result_xy.dcor - result_yx.dcor).abs() < 1e-10);
}
#[test]
fn test_distance_cor_test() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let y: Vec<f64> = x.iter().map(|&xi| xi * 2.0).collect();
let result = distance_cor_test(&x, &y, 99, Some(42)).unwrap();
assert!(result.p_value.unwrap() < 0.05);
assert!(result.dcor > 0.9);
}
#[test]
fn test_distance_cor_bounds() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![2.0, 1.0, 4.0, 3.0, 5.0];
let result = distance_cor(&x, &y).unwrap();
assert!(result.dcor >= 0.0);
assert!(result.dcor <= 1.0);
}
}