solow_stats/
dist_dependence.rs1use ndarray::{Array1, Array2};
24use solow_core::{Error, Result};
25use solow_distributions::norm_cdf;
26
27#[derive(Debug, Clone)]
29pub struct DistDependStat {
30 pub test_statistic: f64,
32 pub distance_correlation: f64,
34 pub distance_covariance: f64,
36 pub dvar_x: f64,
38 pub dvar_y: f64,
40 pub s: f64,
43}
44
45#[derive(Debug, Clone)]
47pub struct DcovTest {
48 pub statistic: f64,
50 pub pvalue: f64,
52}
53
54fn distance_matrix(x: &Array2<f64>) -> Array2<f64> {
56 let (n, k) = x.dim();
57 let mut d = Array2::<f64>::zeros((n, n));
58 for i in 0..n {
59 for j in (i + 1)..n {
60 let mut s = 0.0;
61 for c in 0..k {
62 let diff = x[[i, c]] - x[[j, c]];
63 s += diff * diff;
64 }
65 let dist = s.sqrt();
66 d[[i, j]] = dist;
67 d[[j, i]] = dist;
68 }
69 }
70 d
71}
72
73fn double_center(a: &Array2<f64>) -> (Array2<f64>, f64) {
76 let n = a.nrows();
77 let nf = n as f64;
78 let mut row_means = Array1::<f64>::zeros(n);
81 let mut col_means = Array1::<f64>::zeros(n);
82 for i in 0..n {
83 let mut rs = 0.0;
84 let mut cs = 0.0;
85 for j in 0..n {
86 rs += a[[i, j]]; cs += a[[j, i]]; }
89 row_means[i] = rs / nf;
90 col_means[i] = cs / nf;
91 }
92 let grand: f64 = a.iter().sum::<f64>() / (nf * nf);
93 let mut out = Array2::<f64>::zeros((n, n));
94 for i in 0..n {
95 for j in 0..n {
96 out[[i, j]] = a[[i, j]] - col_means[j] - row_means[i] + grand;
101 }
102 }
103 (out, grand)
104}
105
106fn hadamard_mean(a: &Array2<f64>, b: &Array2<f64>) -> f64 {
107 let n = a.nrows();
108 let mut s = 0.0;
109 for i in 0..n {
110 for j in 0..n {
111 s += a[[i, j]] * b[[i, j]];
112 }
113 }
114 s / (n * n) as f64
115}
116
117pub fn distance_statistics(x: &Array2<f64>, y: &Array2<f64>) -> Result<DistDependStat> {
123 let n = x.nrows();
124 if y.nrows() != n {
125 return Err(Error::Shape(
126 "x and y must have the same number of observations (rows)".into(),
127 ));
128 }
129 if n == 0 {
130 return Err(Error::Value("empty sample".into()));
131 }
132 let a = distance_matrix(x);
133 let b = distance_matrix(y);
134 let (ac, a_mean) = double_center(&a);
135 let (bc, b_mean) = double_center(&b);
136
137 let s = a_mean * b_mean;
138 let dcov = hadamard_mean(&ac, &bc).sqrt();
139 let dvar_x = hadamard_mean(&ac, &ac).sqrt();
140 let dvar_y = hadamard_mean(&bc, &bc).sqrt();
141 let dcor = dcov / (dvar_x * dvar_y).sqrt();
142 let test_statistic = n as f64 * dcov * dcov;
143
144 Ok(DistDependStat {
145 test_statistic,
146 distance_correlation: dcor,
147 distance_covariance: dcov,
148 dvar_x,
149 dvar_y,
150 s,
151 })
152}
153
154pub fn distance_covariance(x: &Array2<f64>, y: &Array2<f64>) -> Result<f64> {
156 Ok(distance_statistics(x, y)?.distance_covariance)
157}
158
159pub fn distance_correlation(x: &Array2<f64>, y: &Array2<f64>) -> Result<f64> {
161 Ok(distance_statistics(x, y)?.distance_correlation)
162}
163
164pub fn distance_variance(x: &Array2<f64>) -> Result<f64> {
166 Ok(distance_statistics(x, x)?.distance_covariance)
167}
168
169pub fn distance_covariance_test(x: &Array2<f64>, y: &Array2<f64>) -> Result<DcovTest> {
174 let stats = distance_statistics(x, y)?;
175 let statistic = (stats.test_statistic / stats.s).sqrt();
176 let pvalue = (1.0 - norm_cdf(statistic)) * 2.0;
177 Ok(DcovTest { statistic, pvalue })
178}
179
180pub fn as_column(v: &Array1<f64>) -> Array2<f64> {
183 let n = v.len();
184 let mut out = Array2::<f64>::zeros((n, 1));
185 for i in 0..n {
186 out[[i, 0]] = v[i];
187 }
188 out
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use ndarray::array;
195
196 #[test]
197 fn dcor_of_identical_is_one() {
198 let x = array![1.0, 2.0, 3.0, 5.0, 8.0];
199 let xc = as_column(&x);
200 let st = distance_statistics(&xc, &xc).unwrap();
201 assert!((st.distance_correlation - 1.0).abs() < 1e-12);
202 assert!((st.dvar_x - st.distance_covariance).abs() < 1e-12);
204 assert!((st.dvar_y - st.distance_covariance).abs() < 1e-12);
205 }
206
207 #[test]
208 fn dcor_in_unit_interval() {
209 let x = array![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
210 let y = array![1.0, 0.5, 2.2, -1.0, 3.3, 0.1];
211 let st = distance_statistics(&as_column(&x), &as_column(&y)).unwrap();
212 assert!((0.0..=1.0).contains(&st.distance_correlation));
213 assert!(st.distance_covariance >= 0.0);
214 }
215
216 #[test]
217 fn mismatched_lengths_error() {
218 let x = as_column(&array![1.0, 2.0, 3.0]);
219 let y = as_column(&array![1.0, 2.0]);
220 assert!(distance_statistics(&x, &y).is_err());
221 }
222}