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 n_f = n as f64;
let n2 = (n * n) as f64;
let mut a = vec![0.0; n * n]; let mut x_row_sums = vec![0.0; n];
let mut y_row_sums = vec![0.0; n];
let mut x_grand_sum = 0.0;
let mut y_grand_sum = 0.0;
for i in 0..n {
for j in i + 1..n {
let dx = (x[i] - x[j]).abs();
let dy = (y[i] - y[j]).abs();
a[i * n + j] = dx;
a[j * n + i] = dx;
x_row_sums[i] += dx;
x_row_sums[j] += dx;
y_row_sums[i] += dy;
y_row_sums[j] += dy;
}
}
for i in 0..n {
x_grand_sum += x_row_sums[i];
y_grand_sum += y_row_sums[i];
}
let x_grand_mean = x_grand_sum / n2;
let y_grand_mean = y_grand_sum / n2;
for i in 0..n {
x_row_sums[i] /= n_f;
y_row_sums[i] /= n_f;
}
let mut dcov2 = 0.0;
let mut dvar_x = 0.0;
let mut dvar_y = 0.0;
for i in 0..n {
for j in 0..n {
let aij = a[i * n + j] - x_row_sums[i] - x_row_sums[j] + x_grand_mean;
let bij = (y[i] - y[j]).abs() - y_row_sums[i] - y_row_sums[j] + y_grand_mean;
dcov2 += aij * bij;
dvar_x += aij * aij;
dvar_y += bij * bij;
}
}
dcov2 /= n2;
dvar_x /= n2;
dvar_y /= n2;
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()
}
#[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);
}
}