use ndarray::{Array1, Array2};
use solow_core::{Error, Result};
use solow_distributions::norm_cdf;
#[derive(Debug, Clone)]
pub struct DistDependStat {
pub test_statistic: f64,
pub distance_correlation: f64,
pub distance_covariance: f64,
pub dvar_x: f64,
pub dvar_y: f64,
pub s: f64,
}
#[derive(Debug, Clone)]
pub struct DcovTest {
pub statistic: f64,
pub pvalue: f64,
}
fn distance_matrix(x: &Array2<f64>) -> Array2<f64> {
let (n, k) = x.dim();
let mut d = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in (i + 1)..n {
let mut s = 0.0;
for c in 0..k {
let diff = x[[i, c]] - x[[j, c]];
s += diff * diff;
}
let dist = s.sqrt();
d[[i, j]] = dist;
d[[j, i]] = dist;
}
}
d
}
fn double_center(a: &Array2<f64>) -> (Array2<f64>, f64) {
let n = a.nrows();
let nf = n as f64;
let mut row_means = Array1::<f64>::zeros(n);
let mut col_means = Array1::<f64>::zeros(n);
for i in 0..n {
let mut rs = 0.0;
let mut cs = 0.0;
for j in 0..n {
rs += a[[i, j]]; cs += a[[j, i]]; }
row_means[i] = rs / nf;
col_means[i] = cs / nf;
}
let grand: f64 = a.iter().sum::<f64>() / (nf * nf);
let mut out = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
out[[i, j]] = a[[i, j]] - col_means[j] - row_means[i] + grand;
}
}
(out, grand)
}
fn hadamard_mean(a: &Array2<f64>, b: &Array2<f64>) -> f64 {
let n = a.nrows();
let mut s = 0.0;
for i in 0..n {
for j in 0..n {
s += a[[i, j]] * b[[i, j]];
}
}
s / (n * n) as f64
}
pub fn distance_statistics(x: &Array2<f64>, y: &Array2<f64>) -> Result<DistDependStat> {
let n = x.nrows();
if y.nrows() != n {
return Err(Error::Shape(
"x and y must have the same number of observations (rows)".into(),
));
}
if n == 0 {
return Err(Error::Value("empty sample".into()));
}
let a = distance_matrix(x);
let b = distance_matrix(y);
let (ac, a_mean) = double_center(&a);
let (bc, b_mean) = double_center(&b);
let s = a_mean * b_mean;
let dcov = hadamard_mean(&ac, &bc).sqrt();
let dvar_x = hadamard_mean(&ac, &ac).sqrt();
let dvar_y = hadamard_mean(&bc, &bc).sqrt();
let dcor = dcov / (dvar_x * dvar_y).sqrt();
let test_statistic = n as f64 * dcov * dcov;
Ok(DistDependStat {
test_statistic,
distance_correlation: dcor,
distance_covariance: dcov,
dvar_x,
dvar_y,
s,
})
}
pub fn distance_covariance(x: &Array2<f64>, y: &Array2<f64>) -> Result<f64> {
Ok(distance_statistics(x, y)?.distance_covariance)
}
pub fn distance_correlation(x: &Array2<f64>, y: &Array2<f64>) -> Result<f64> {
Ok(distance_statistics(x, y)?.distance_correlation)
}
pub fn distance_variance(x: &Array2<f64>) -> Result<f64> {
Ok(distance_statistics(x, x)?.distance_covariance)
}
pub fn distance_covariance_test(x: &Array2<f64>, y: &Array2<f64>) -> Result<DcovTest> {
let stats = distance_statistics(x, y)?;
let statistic = (stats.test_statistic / stats.s).sqrt();
let pvalue = (1.0 - norm_cdf(statistic)) * 2.0;
Ok(DcovTest { statistic, pvalue })
}
pub fn as_column(v: &Array1<f64>) -> Array2<f64> {
let n = v.len();
let mut out = Array2::<f64>::zeros((n, 1));
for i in 0..n {
out[[i, 0]] = v[i];
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn dcor_of_identical_is_one() {
let x = array![1.0, 2.0, 3.0, 5.0, 8.0];
let xc = as_column(&x);
let st = distance_statistics(&xc, &xc).unwrap();
assert!((st.distance_correlation - 1.0).abs() < 1e-12);
assert!((st.dvar_x - st.distance_covariance).abs() < 1e-12);
assert!((st.dvar_y - st.distance_covariance).abs() < 1e-12);
}
#[test]
fn dcor_in_unit_interval() {
let x = array![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
let y = array![1.0, 0.5, 2.2, -1.0, 3.3, 0.1];
let st = distance_statistics(&as_column(&x), &as_column(&y)).unwrap();
assert!((0.0..=1.0).contains(&st.distance_correlation));
assert!(st.distance_covariance >= 0.0);
}
#[test]
fn mismatched_lengths_error() {
let x = as_column(&array![1.0, 2.0, 3.0]);
let y = as_column(&array![1.0, 2.0]);
assert!(distance_statistics(&x, &y).is_err());
}
}