Coverage for /opt/conda/lib/python3.14/site-packages/medil/_independence_testing.py: 83%
72 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 02:29 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 02:29 +0000
1"""Independence testing on samples of random variables."""
3from multiprocessing import Pool, cpu_count
4from typing import NamedTuple, Optional
6import numpy as np
7import numpy.typing as npt
8from scipy.spatial.distance import pdist, squareform
9from scipy.stats import chatterjeexi, chi2, chi2_contingency
12def _dcov(samples):
13 r"""Compute sample distance covariance matrix.
15 Parameters
16 ----------
17 samples : 2d numpy array of floats
18 A :math:`N \times M` matrix with :math:`N` samples of
19 :math:`M` random variables.
21 Returns
22 -------
23 2d numpy array
24 A square matrix :math:`C`, where :math:`C_{i,j}` is the sample
25 distance covariance between random variables :math:`R_i` and
26 :math:`R_j`.
28 Notes
29 -----
30 Trades time complexity for space complexity, so it can be too
31 memory-intensive for larger datasets, in which case recommend to
32 use xicor or distance_correlation_t_test from the dcor package.
33 """
34 num_samps, num_feats = samples.shape
35 num_pairs = num_samps * (num_samps - 1) // 2
36 dists = np.zeros((num_feats, num_pairs))
37 d_bars = np.zeros(num_feats)
38 # compute doubly centered distance matrix for every feature:
39 for feat_idx in range(num_feats):
40 n = num_samps
41 t = np.tile
42 # raw distance matrix:
43 d = squareform(pdist(samples[:, feat_idx].reshape(-1, 1), "cityblock"))
44 # doubly centered:
45 d_bar = d.mean()
46 d -= t(d.mean(0), (n, 1)) + t(d.mean(1), (n, 1)).T - t(d_bar, (n, n))
47 d = squareform(d, checks=False) # ignore assymmetry due to numerical error
48 dists[feat_idx] = d
49 d_bars[feat_idx] = d_bar
50 return dists @ dists.T / num_samps**2, d_bars
53def _estimate_UDG(sample, method="dcov_fast", significance_level=0.05):
54 samp_size, num_feats = sample.shape
56 if isinstance(method, np.ndarray):
57 p_vals = method
58 udg = p_vals < significance_level
59 elif method == "dcov_fast":
60 cov, d_bars = _dcov(sample)
61 crit_val = chi2(1).ppf(1 - significance_level)
62 test_val = samp_size * cov / np.outer(d_bars, d_bars)
63 udg = test_val >= crit_val
64 p_vals = None
65 elif method == "g-test":
66 if not np.issubdtype(sample.dtype, np.integer):
67 raise ValueError(
68 f"g-test requires integer-valued data; got dtype {sample.dtype!r}"
69 )
70 p_vals = np.zeros((num_feats, num_feats), float)
71 idxs, jdxs = np.triu_indices(num_feats, 1)
72 sample_iter = (sample[:, i_j].T for i_j in zip(idxs, jdxs))
73 with Pool(max(1, int(0.75 * cpu_count()))) as p:
74 p_vals[idxs, jdxs] = p_vals[jdxs, idxs] = np.fromiter(
75 p.imap(_g_test, sample_iter, 100), float
76 )
77 udg = p_vals < significance_level
78 else:
79 p_vals = np.zeros((num_feats, num_feats), float)
80 idxs, jdxs = np.triu_indices(num_feats, 1)
81 zipped = zip(idxs, jdxs)
82 sample_iter = (sample[:, i_j].T for i_j in zipped)
83 # if method == "dcov_big":
84 # can use distance_correlation_t_test from dcor package
85 if method == "xicor":
86 test = _xicor_test
87 with Pool(max(1, int(0.75 * cpu_count()))) as p:
88 p_vals[idxs, jdxs] = p_vals[jdxs, idxs] = np.fromiter(
89 p.imap(test, sample_iter, 100), float
90 )
91 udg = p_vals < significance_level
92 np.fill_diagonal(udg, False)
93 return udg, p_vals
96def _g_test(x_y):
97 x, y = x_y
98 x_vals, xi = np.unique(x, return_inverse=True)
99 y_vals, yi = np.unique(y, return_inverse=True)
100 table = np.zeros((len(x_vals), len(y_vals)), dtype=int)
101 np.add.at(table, (xi, yi), 1)
102 _, p, _, _ = chi2_contingency(table, lambda_="log-likelihood")
103 return p
106def _xicor_test(x_y):
107 x, y = x_y
108 xi, pvalue = _xicorr(x, y)
109 return pvalue
112class _XiCorrResult(NamedTuple):
113 correlation: float
114 pvalue: Optional[float]
117def _xicorr(x: npt.ArrayLike, y: npt.ArrayLike, ties: bool = True) -> _XiCorrResult:
118 """Compute Chatterjee's xi correlation coefficient."""
119 x = np.asarray(x).ravel()
120 y = np.asarray(y).ravel()
121 if x.size != y.size:
122 raise ValueError(
123 "All inputs to `_xicorr` must be of the same "
124 f"size, found x-size {x.size} and y-size {y.size}"
125 )
126 result = chatterjeexi(x, y, y_continuous=not ties)
127 return _XiCorrResult(result.statistic, result.pvalue)