pub fn distance_correlation(x: &[f64], y: &[f64]) -> f64 {
let n = x.len();
assert_eq!(n, y.len(), "x and y must have the same length");
if n < 4 {
return 0.0;
}
let a = doubly_centered_distances(x);
let b = doubly_centered_distances(y);
let dcov2 = inner_product(&a, &b, n);
let dvar_x = inner_product(&a, &a, n);
let dvar_y = inner_product(&b, &b, n);
if dvar_x <= 0.0 || dvar_y <= 0.0 {
return 0.0;
}
let dcor2 = dcov2 / (dvar_x * dvar_y).sqrt();
dcor2.max(0.0).sqrt()
}
fn doubly_centered_distances(x: &[f64]) -> Vec<f64> {
let n = x.len();
let n_f = n as f64;
let mut d = vec![0.0; n * n];
for i in 0..n {
for j in i + 1..n {
let dist = (x[i] - x[j]).abs();
d[i * n + j] = dist;
d[j * n + i] = dist;
}
}
let mut row_means = vec![0.0; n];
let mut grand_mean = 0.0;
for i in 0..n {
let row_sum: f64 = (0..n).map(|j| d[i * n + j]).sum();
row_means[i] = row_sum / n_f;
grand_mean += row_sum;
}
grand_mean /= (n * n) as f64;
for i in 0..n {
for j in 0..n {
d[i * n + j] = d[i * n + j] - row_means[i] - row_means[j] + grand_mean;
}
}
d
}
fn inner_product(a: &[f64], b: &[f64], n: usize) -> f64 {
let mut sum = 0.0;
for i in 0..n * n {
sum += a[i] * b[i];
}
sum / (n * n) as f64
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn identical_variables_dcor_is_one() {
let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
let dc = distance_correlation(&x, &x);
assert_relative_eq!(dc, 1.0, epsilon = 1e-6);
}
#[test]
fn independent_variables_dcor_near_zero() {
let x: Vec<f64> = (0..200).map(|i| (i as f64 * 0.07).sin()).collect();
let y: Vec<f64> = (0..200)
.map(|i| ((i * 13 + 7) % 97) as f64 / 97.0)
.collect();
let dc = distance_correlation(&x, &y);
assert!(dc < 0.15, "independent dCor should be near 0, got {}", dc);
}
#[test]
fn nonlinear_dependence_detected() {
let x: Vec<f64> = (0..200).map(|i| (i as f64 - 100.0) * 0.05).collect();
let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
let dc = distance_correlation(&x, &y);
assert!(
dc > 0.3,
"quadratic dependence dCor should be > 0.3, got {}",
dc
);
}
#[test]
fn dcor_bounded_zero_one() {
let x: Vec<f64> = (0..80).map(|i| (i as f64 * 0.1).sin()).collect();
let y: Vec<f64> = (0..80).map(|i| (i as f64 * 0.13).cos()).collect();
let dc = distance_correlation(&x, &y);
assert!((0.0..=1.001).contains(&dc), "dCor out of range: {}", dc);
}
}