Dependence & association · system health management

Reading the Couplings

A field guide to the statistics that tell you whether two signals are related, whether that relation is real, and which way it flows.

Diagnostics lives or dies on one skill: deciding when two signals are genuinely related. Every method below answers some version of that question, but they answer different versions. Reading them as one family makes the toolbox far easier to navigate. Three questions organize everything:

Question 1
Are they coupled?

Do the signals move together at all, and in what shape: straight, curved, or something stranger?

Question 2
Is it direct?

Or are both just following a shared driver like load or temperature, a spurious link to be stripped out?

Question 3
Which way does it flow?

Which signal moves first, and how does a disturbance propagate through the system?

Each card gives the idea in plain words, a system-health example, a figure, and runnable Python. A decision map at the end keys every method back to the question it answers.

Interactive explorers

The catalog shows one frozen example per method. These four move. Drag the controls and watch the numbers respond.

A

Why escalate past Pearson

shape

Morph one relationship from a straight line to a U to a ring while four measures recompute live.

B

Real link, or shared driver?

direct vs. confounded

Dial the two paths from a common driver C, and a direct A–B link. Watch raw correlation rise while partial correlation tells the truth.

C A B shared driver (load / temp)
raw corr (A,B)0.00
partial (A,B | C)0.00

C

Recover a propagation delay

lagged

Shift B behind A, and the cross-correlation peak tracks the true delay.

D

Coupling lives in a band

frequency

A shared 30 Hz source and a B-only 80 Hz tone. Coherence sees only the shared band.

Core dependence

The everyday question: do two signals move together, and is that link real or borrowed?

01

Pearson correlation

linear

Linear coupling between two signals; the baseline for sensor-pair relationships.

In plain words

Pearson r asks one thing: when one signal goes up by a fixed amount, does the other go up (or down) by a roughly fixed amount too? It is the slope of the best straight line, scaled to sit between minus one and plus one. It only sees straight-line relationships, so a strong curved link can still give r near zero.

Health-management example

Two temperature sensors on the same coolant loop should rise and fall together. An r of 0.95 confirms they track each other; a sudden drop to 0.3 over a week is an early sign that one sensor is drifting or the loop has a new restriction.

2026-06-19T16:49:24.641241 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from scipy.stats import pearsonr

r, p = pearsonr(sensor_A, sensor_B)
print(f"Pearson r = {r:.3f}  (p = {p:.1e})")
Practical note

Scale-free, so no need to standardize, but detrend and screen outliers first: one leverage point can set the whole coefficient. Reliable from about 30 paired samples. If r is near zero, confirm with a rank or nonlinear measure before declaring independence.

Pearson, K. (1896). Mathematical contributions to the theory of evolution III. Phil. Trans. R. Soc. A 187.

02

Spearman / Kendall's tau

rank

Rank-based, robust to nonlinearity and outliers; good when the monotonic trend matters more than the exact shape.

In plain words

Instead of the raw values, these use the order (1st, 2nd, 3rd ...). Spearman is Pearson computed on ranks; Kendall's tau counts how often two points agree on direction (both up, or both down). Because ranks ignore the exact spacing, a curved-but-always-rising relationship scores near one, and a single wild outlier barely moves the result.

Health-management example

Wear grows with cumulative load, but the curve bends upward sharply near end of life. Pearson underrates it; Spearman rho near 0.98 correctly says the ordering is almost perfectly preserved, which is what a health trend monitor cares about.

2026-06-19T16:49:24.784756 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
from scipy.stats import spearmanr, kendalltau

rho, _ = spearmanr(load, wear)     # Pearson on the ranks
tau, _ = kendalltau(load, wear)    # concordant vs discordant pairs
print(f"Spearman {rho:.3f}   Kendall {tau:.3f}")
Practical note

Reach for it when the link is monotonic but curved, or when heavy-tailed noise throws outliers. Kendall's tau is more robust with a cleaner small-sample distribution but costs more. If Pearson and Spearman diverge sharply, suspect nonlinearity or outliers.

Spearman (1904), Am. J. Psychol. 15; Kendall (1938), Biometrika 30.

03

Partial correlation

direct vs. confounded

Association between two variables after regressing out a controlling set; the key tool for separating direct coupling from shared-driver effects.

In plain words

Two signals can look coupled only because a third thing drives them both. Partial correlation strips the influence of that third set out of BOTH signals first, then measures what correlation is left. If the link survives, it is more likely a direct one; if it collapses, the third variable was the real story.

Health-management example

Battery current and inverter temperature both rise with vehicle load. Their raw correlation is high, but after removing load and ambient temperature from both, the partial correlation falls near zero, telling you they are not directly coupled, load was the shared driver.

2026-06-19T16:49:24.874198 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import pandas as pd, pingouin as pg

df = pd.DataFrame({"A": sensor_A, "B": sensor_B,
                   "load": load, "temp": ambient})
# remove load and temp from BOTH A and B, then correlate
res = pg.partial_corr(data=df, x="A", y="B", covar=["load", "temp"])
print(res[["r", "p-val"]])
Practical note

The control set must be measured and low-noise; error in the controls leaves residual confounding. Linear by construction, so switch to partial distance correlation or conditional HSIC when the confounding bends. Avoid over-controlling: conditioning on a mediator or a collider can create a false link.

Baba, Shibata & Sibuya (2004). Partial correlation and conditional correlation as measures of conditional independence. Aust. N. Z. J. Stat. 46.

04

Semi-partial (part) correlation

direct vs. confounded

Removes the confounders from only one variable; useful for attributing incremental explanatory power.

In plain words

Same idea as partial correlation, but the controlling set is removed from just one of the two variables, not both. That answers a subtly different question: how much UNIQUE information does this one predictor add about the target, beyond what the controls already explain? It is the natural language of incremental contribution.

Health-management example

You already predict a fault indicator from load and temperature. Semi-partial correlation of a new vibration feature with the indicator (controls removed from the vibration feature only) tells you the extra explanatory power that feature buys you on top of the controls, the basis for deciding whether to keep it.

2026-06-19T16:49:24.908620 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import pingouin as pg
# x_covar removes controls from X (the predictor) ONLY
res = pg.partial_corr(data=df, x="vibration", y="fault_idx",
                      x_covar=["load", "temp"])
print("unique contribution:", res["r"].iloc[0])
Practical note

Report it next to the full-model R-squared to express incremental value. Sensitive to multicollinearity: a useful feature can look worthless only because a correlated one already covers its information.

Cohen, Cohen, West & Aiken (2003). Applied Multiple Regression/Correlation Analysis, 3rd ed.

05

Cross-correlation (lagged)

lagged / time

Adds time-shift, so you recover propagation delay and direction of influence between subsystems.

In plain words

Ordinary correlation lines up two signals at the same instant. Cross-correlation slides one signal in time and measures correlation at every shift. The shift where the match is strongest is the delay between them, and its sign tells you which signal moved first.

Health-management example

A pressure spike at the pump shows up later at a downstream valve. The cross-correlation peak at +0.7 seconds gives both the transport delay and the direction of flow, useful for locating where a transient originates in a hydraulic subsystem.

2026-06-19T16:49:24.995031 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from scipy import signal

a = a - a.mean(); b = b - b.mean()
xc   = signal.correlate(b, a, mode="full")
lags = signal.correlation_lags(len(b), len(a), mode="full")
delay = lags[np.argmax(xc)]
print(f"B lags A by {delay} samples  (>0 means A leads)")
Practical note

Detrend and ideally pre-whiten both signals; shared slow drift inflates the peak and smears the lag. Resolution is one sample, so sample fast enough that the true delay spans several. A broad, flat peak means the delay is poorly identified.

Box & Jenkins (1976), Time Series Analysis; Knapp & Carter (1976), IEEE Trans. ASSP 24.

06

Distance correlation / MIC

nonlinear

Capture nonlinear and non-monotonic dependence that Pearson misses.

In plain words

Some relationships are real but not straight or even always-rising, think of a U-shape, or points scattered on a ring. Distance correlation is zero only when two variables are truly independent, so it catches these. MIC (maximal information coefficient) scans many grid resolutions to score how cleanly one variable carves up the other, again catching curves and clusters.

Health-management example

An actuator's error is small at low and high duty but large in the mid range, a U-shape. Pearson reads near zero and would call it healthy; distance correlation of 0.55 flags the genuine dependence that a linear screen would miss.

2026-06-19T16:49:25.056680 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import dcor
from minepy import MINE

dc = dcor.distance_correlation(x, y)   # 0 iff independent
m = MINE(); m.compute_score(x, y)
print(f"dCor {dc:.3f}   MIC {m.mic():.3f}")
Practical note

Distance correlation needs roughly 100-plus samples and is order n-squared in memory; use its permutation test for significance. MIC is powerful but can be unstable and slow on noisy data. Standardize heterogeneous scales first.

Szekely, Rizzo & Bakirov (2007). Measuring and testing dependence by correlation of distances. Ann. Statist. 35; Reshef et al. (2011), Science 334 (MIC).

07

Mutual information & conditional MI

information

Information-theoretic dependence; conditional MI is the nonlinear analogue of partial correlation for fault isolation.

In plain words

Mutual information measures how many bits knowing one signal saves you when guessing the other, of any shape of relationship, not just linear. Conditional mutual information asks the same thing AFTER you already know a third signal: if it drops to near zero, the third signal explained the link. That is the nonlinear cousin of partial correlation.

Health-management example

A vibration band and a fault flag share high mutual information. Conditioning on operating mode, the conditional MI nearly vanishes, meaning the apparent coupling was just both responding to mode changes, exactly the isolation logic you want before declaring a root cause.

2026-06-19T16:49:25.142163 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
from sklearn.feature_selection import mutual_info_regression
from npeet import entropy_estimators as ee   # kNN estimators

mi  = mutual_info_regression(x.reshape(-1, 1), y)[0]
cmi = ee.cmi(x.reshape(-1, 1), y.reshape(-1, 1), z.reshape(-1, 1))
print(f"MI {mi:.3f}   conditional MI I(X;Y|Z) {cmi:.3f}")
Practical note

The k-nearest-neighbor (Kraskov) estimator is the practical default with k around 3 to 5. Needs a few hundred samples, more in higher dimensions. MI has no fixed ceiling, so compare like-for-like rather than reading an absolute value.

Kraskov, Stogbauer & Grassberger (2004), Phys. Rev. E 69; Frenzel & Pompe (2007), Phys. Rev. Lett. 99 (conditional MI).

08

Coherence

frequency

Frequency-domain correlation; tells you at which frequencies two signals are coupled.

In plain words

Coherence is correlation computed frequency by frequency. It runs from zero to one and answers: at this particular vibration frequency, do the two signals rise and fall in lockstep? Two signals can be uncorrelated overall yet strongly coherent in one narrow band.

Health-management example

A gearbox accelerometer and a motor-current sensor share strong coherence only around 30 Hz. That common band points to a single mechanical source feeding both channels, the starting thread for NVH source tracing.

2026-06-19T16:49:25.994642 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
from scipy.signal import coherence

f, Cxy = coherence(sig_a, sig_b, fs=fs, nperseg=1024)
peak = f[Cxy.argmax()]
print(f"strongest coupling at {peak:.1f} Hz  (coherence {Cxy.max():.2f})")
Practical note

Pick the segment length to balance frequency resolution against variance, and average several segments or coherence is artificially near one. Window to limit spectral leakage. It shows coupling, never direction.

Carter (1987), Proc. IEEE 75; Bendat & Piersol, Random Data.

Agreement & redundancy

Sensor voting and calibration drift, where matching VALUES, not just shapes, is the whole point.

09

Intraclass correlation (ICC)

agreement

Agreement among repeated or redundant measurements of the same quantity; the right metric for triple-redundant sensor consistency, not Pearson (which ignores systematic offset).

In plain words

When several sensors are supposed to measure the SAME thing, you do not just want their shapes to match, you want their numbers to match. Pearson happily reports r equal one even if one sensor reads a constant ten degrees high. ICC treats that constant offset as disagreement, which is what redundancy management actually needs.

Health-management example

Three redundant cell-temperature sensors feed a voting scheme. A high ICC means any of the three can be trusted; a falling ICC, even while Pearson stays near one, exposes a sensor reading consistently high, a calibration fault a correlation check would hide.

2026-06-19T16:49:26.074588 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import pingouin as pg
# long format: one row per (sample, sensor, reading)
icc = pg.intraclass_corr(data=long_df, targets="sample",
                         raters="sensor", ratings="value")
print(icc[icc.Type == "ICC2"][["ICC", "CI95%"]])
Practical note

Choose the form deliberately: (2,1) for absolute agreement of single redundant sensors, (3,k) for averaged consistency. Report the confidence interval, it is noisy with few targets. A constant offset correctly lowers ICC where Pearson would not.

Shrout & Fleiss (1979), Psychol. Bull. 86; McGraw & Wong (1996), Psychol. Methods 1.

10

Lin's concordance correlation (CCC)

agreement

Agreement vs. a reference, penalizing both scatter and bias; ideal for sensor-vs-truth calibration-drift monitoring.

In plain words

CCC measures how close paired readings sit to the perfect y equals x line, not just how tightly they cluster around SOME line. It combines two penalties: random scatter, and any systematic gap (bias). A sensor that is precise but biased gets a high Pearson and a low CCC, which is the honest verdict.

Health-management example

During end-of-line calibration you compare a production sensor against a lab reference. CCC near one passes; CCC dropping because the cloud has shifted off the y equals x diagonal flags a developing offset before it breaches a hard limit.

2026-06-19T16:49:26.174227 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
def ccc(x, y):
    cov = np.cov(x, y, bias=True)[0, 1]
    return 2 * cov / (x.var() + y.var() + (x.mean() - y.mean()) ** 2)
print("CCC =", ccc(reference, sensor))
Practical note

Best for sensor-versus-reference checks. Decompose into precision (correlation) times accuracy (bias) to see whether a low score is scatter or drift. A narrow operating range deflates CCC even with good agreement.

Lin, L. (1989). A concordance correlation coefficient to evaluate reproducibility. Biometrics 45.

11

Kendall's W (concordance)

agreement

Agreement among multiple rankers/sensors when only ordering is trusted.

In plain words

Kendall's W rates how much several rankers agree on an ordering, from zero (no agreement) to one (identical rankings). Use it when the absolute numbers are not comparable but the ORDER is, for example several diagnostics each ranking suspects.

Health-management example

Four technicians (or four scoring models) each rank five fault codes by severity. A W of 0.85 says they strongly agree on the priority order, so the consensus ranking is trustworthy; a low W means the triage logic disagrees and needs a tie-breaker.

2026-06-19T16:49:26.265731 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
def kendall_w(ranks):                 # ranks: raters x items
    m, n = ranks.shape
    R = ranks.sum(axis=0)
    S = ((R - R.mean()) ** 2).sum()
    return 12 * S / (m ** 2 * (n ** 3 - n))
print("Kendall W =", kendall_w(rank_matrix))
Practical note

Use when only the ordering is comparable across raters or models, and correct for ties. With many items and few raters even modest W can be significant, so check the chi-square test, not just the magnitude.

Kendall & Babington Smith (1939). The problem of m rankings. Ann. Math. Statist. 10.

Categorical & mixed-type coupling

DTCs, discrete modes and fault flags, where at least one variable is a label, not a number.

12

Cramer's V / chi-square

categorical

Association between categorical variables; fault-code co-occurrence, fault-mode vs. operating-mode tables.

In plain words

The chi-square test asks whether two labels occur together more (or less) often than chance. Cramer's V turns that test into a strength score between zero and one, so you can compare associations across different tables, the categorical answer to 'how strongly related are these?'.

Health-management example

Cross-tabulate diagnostic trouble codes against operating mode. A high Cramer's V between a specific DTC and 'regen braking' says that fault concentrates in one mode, a strong hint about where in the duty cycle to look.

2026-06-19T16:49:26.331811 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from scipy.stats import chi2_contingency

chi2, p, dof, _ = chi2_contingency(table)     # DTC x mode counts
n = table.sum(); r, k = table.shape
V = np.sqrt(chi2 / (n * (min(r, k) - 1)))
print(f"Cramer's V = {V:.3f}  (p = {p:.1e})")
Practical note

Apply a bias correction (Bergsma) for small or sparse tables; raw V is upward-biased. Keep expected cell counts around five or pool rare categories. V is strength, not direction.

Cramer (1946), Mathematical Methods of Statistics; Bergsma (2013), J. Korean Statist. Soc. 42 (bias correction).

13

Phi / point-biserial

categorical

Binary-binary and continuous-binary association (a continuous signal vs. fault-present flag).

In plain words

Phi is correlation between two yes/no variables (a tidy 2x2 case). Point-biserial is correlation between a continuous measurement and a yes/no label, mathematically just Pearson with one side coded as 0 and 1. It tells you how cleanly a numeric feature separates the two classes.

Health-management example

Code a window as fault-present (1) or not (0) and correlate against vibration RMS. A point-biserial of 0.7 means RMS runs clearly higher when the fault is present, a candidate detector threshold lives right there.

2026-06-19T16:49:26.398170 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
from scipy.stats import pointbiserialr

r_pb, p = pointbiserialr(fault_flag, vibration_rms)   # flag in {0,1}
print(f"point-biserial r = {r_pb:.3f}")
# phi for a 2x2 table:  phi = sqrt(chi2 / n)
Practical note

Pearson with a 0/1 variable, so it inherits linearity and outlier sensitivity. Strongly affected by class balance: a rare fault inflates variance. Good to screen a detector threshold, then validate with ROC and AUC.

Tate, R. F. (1954). Correlation between a discrete and a continuous variable. Ann. Math. Statist. 25.

14

Theil's U (uncertainty coefficient)

categorical

Directional categorical dependence; how much knowing the operating mode reduces uncertainty about the fault state.

In plain words

Theil's U measures, in information terms, how much knowing one label shrinks your uncertainty about another, and crucially it is directional. U(fault | mode) can differ from U(mode | fault). That asymmetry is exactly what you want when one variable is a cause-like driver and the other a consequence.

Health-management example

Knowing the operating mode might tell you a lot about the likely fault state, while the fault state tells you little about the mode. A high U(fault | mode) and low U(mode | fault) supports the reading that mode drives the fault distribution, not the reverse.

2026-06-19T16:49:26.447495 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from collections import Counter
def entropy(v):
    p = np.array(list(Counter(v).values()), float); p /= p.sum()
    return -(p * np.log(p)).sum()
def theils_u(x, y):                       # U(x | y): does y predict x?
    Hx = entropy(x)
    Hxy = sum((c / len(y)) * entropy([x[i] for i in range(len(y)) if y[i] == yv])
              for yv, c in Counter(y).items())
    return (Hx - Hxy) / Hx
print("U(fault|mode) =", theils_u(fault, mode))
Practical note

Report both directions, the asymmetry is the point. Estimated from a contingency table, so it needs enough counts per cell, and it is sensitive to the number of categories, keep them comparable across comparisons.

Theil, H. (1972). Statistical Decomposition Analysis; Press et al., Numerical Recipes.

15

Tetrachoric / polychoric

categorical

Latent-continuous correlation behind thresholded or ordinal indicators.

In plain words

Sometimes a 0/1 flag (or an ordinal level) is really a hidden continuous quantity that crossed a threshold. Tetrachoric (for binary) and polychoric (for ordinal) estimate the correlation of those HIDDEN continuous variables, undoing the information you lost by thresholding. The result is usually larger and more honest than treating the flags as plain numbers.

Health-management example

Two pass/fail screens are coarse views of underlying continuous stress. The tetrachoric correlation recovers how strongly the two latent stresses move together, better for reliability modeling than the raw phi between the flags.

2026-06-19T16:49:26.489430 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from scipy.stats import norm, multivariate_normal as mvn
from scipy.optimize import brentq
def tetrachoric(t):                       # t = [[n11,n10],[n01,n00]]
    n = t.sum()
    hx = norm.ppf(t[0].sum() / n); hy = norm.ppf(t[:, 0].sum() / n)
    f = lambda r: mvn([0, 0], [[1, r], [r, 1]]).cdf([hx, hy]) - t[0, 0] / n
    return brentq(f, -0.999, 0.999)
print("tetrachoric r =", tetrachoric(np.array([[40, 12], [10, 38]])))
Practical note

Assumes the latent variables are bivariate normal, so check that. Unstable with empty or near-empty cells (add a continuity correction). Polychoric extends it to ordinal levels; both recover fairer correlations than treating codes as plain numbers.

Pearson (1900), Phil. Trans. R. Soc. A 195; Olsson (1979), Psychometrika 44 (polychoric).

Stronger nonlinear detectors

When the relationship is real but neither straight nor monotonic, reach past distance correlation.

16

HSIC (Hilbert-Schmidt Independence Criterion)

nonlinear

Kernel independence test; a robust general-purpose nonlinear dependence detector and a standard primitive in causal discovery.

In plain words

HSIC maps each variable through a kernel (think: into a rich space of smooth features) and measures the covariance between the two feature sets. It is zero only when the variables are genuinely independent, so it catches arbitrary nonlinear dependence. It is the dependence test that modern causal-discovery algorithms lean on.

Health-management example

A control residual and a load signal should be independent if the controller is healthy. A clearly non-zero HSIC (with a permutation p-value) says some unmodeled load dependence has leaked into the residual, a structural fault a linear check would pass.

2026-06-19T16:49:26.528826 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
def rbf(v):
    v = v.reshape(-1, 1); d2 = (v - v.T) ** 2
    s = np.sqrt(np.median(d2[d2 > 0]) / 2) + 1e-9
    return np.exp(-d2 / (2 * s ** 2))
def hsic(x, y):
    n = len(x); H = np.eye(n) - 1 / n
    K, L = rbf(x), rbf(y)
    return np.trace(K @ H @ L @ H) / (n - 1) ** 2
print("HSIC =", hsic(x, y))
Practical note

Use the median-distance heuristic for the bandwidth as a default but sanity-check a couple of scales, the value depends on it. Significance comes from a permutation or gamma test, not the raw number. Order n-squared, so subsample very large n; standardize inputs.

Gretton, Bousquet, Smola & Scholkopf (2005), ALT; Gretton et al. (2008), NeurIPS (test).

17

Chatterjee's xi

nonlinear

Recent rank correlation that's asymmetric and consistent against any dependence, including noisy functional relationships; cheap, and good at flagging 'Y is a function of X' couplings that symmetric measures blur.

In plain words

Chatterjee's xi (2021) is near zero when two variables are independent and near one when one is a (possibly noisy) function of the other. Unlike Pearson or Spearman it is asymmetric and detects ANY functional shape. It is a few lines of code and scales to large data.

Health-management example

A sensor output is a wiggly but deterministic function of crank angle. Symmetric measures wash out across the wiggles; xi near 0.8 correctly flags that the output is essentially determined by angle, useful for spotting hidden functional dependencies in feature screening.

2026-06-19T16:49:26.575532 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
def xicor(x, y):                          # estimates xi(X -> Y)
    n = len(x)
    order = np.argsort(x)
    r = np.argsort(np.argsort(y[order])) + 1.0       # ranks of y by x-order
    return 1 - 3 * np.abs(np.diff(r)).sum() / (n ** 2 - 1)
print("xi(X->Y) =", xicor(x, y), "  xi(Y->X) =", xicor(y, x))
Practical note

Cheap, order n log n, and excellent for screening which variable is a function of which. Lower power than distance correlation or HSIC for weak dependence at small n. Always compute both directions and compare.

Chatterjee, S. (2021). A new coefficient of correlation. J. Amer. Statist. Assoc. 116.

18

Maximal correlation (HGR / ACE)

nonlinear

Supremum of correlation over all transformations of each variable; the theoretical ceiling on nonlinear association.

In plain words

Ask: what is the best Pearson correlation you could get if you were allowed to reshape each variable through any transformation? That maximum is the Hirschfeld-Gebelein-Renyi (HGR) maximal correlation. The ACE algorithm finds the transformations that achieve it, so you both score the dependence and see the shape that straightens it.

Health-management example

A flux measurement relates to current through a saturating, nonlinear curve. Maximal correlation finds the warping that linearizes the pair, giving a clean dependence score and, as a bonus, the empirical transform you might bake into a feature.

2026-06-19T16:49:26.653548 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from ace.ace import ACESolver            # pip install ace

s = ACESolver(); s.specify_data_set([x], y); s.solve()
rho_max = np.corrcoef(s.x_transforms[0], s.y_transform)[0, 1]
print("maximal correlation =", abs(rho_max))
Practical note

ACE returns the transforms that straighten the relationship, useful as engineered features, not just a score. Can overfit at small n, so regularize or cross-validate. Implementation-fragile, treat as optional.

Renyi (1959), Acta Math. Hungar. 10; Breiman & Friedman (1985), JASA 80 (ACE).

19

Copula-based dependence & tail dependence

nonlinear

Separates the dependence structure from the marginals; tail dependence captures joint extreme behavior invisible to body-of-distribution correlation.

In plain words

A copula is the dependence pattern you get after converting each variable to its rank (uniform) scale, stripping away the individual distributions. That isolates HOW the two move together. Tail dependence is a special slice: the chance both hit extreme values at the same time, which ordinary correlation, dominated by the bulk, barely registers.

Health-management example

Two subsystem stress signals are mildly correlated in normal operation but tend to spike together at the limits. Lower-tail dependence quantifies that 'fail together' tendency, the risk that matters for redundancy, even when the everyday correlation looks comfortable.

2026-06-19T16:49:26.740436 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
def lower_tail_dep(a, b, q=0.05):
    u = (np.argsort(np.argsort(a)) + 1) / (len(a) + 1)   # pseudo-obs
    v = (np.argsort(np.argsort(b)) + 1) / (len(b) + 1)
    return ((u <= q) & (v <= q)).mean() / q              # ~ lambda_L
print("lower-tail dependence =", lower_tail_dep(stress_a, stress_b))
Practical note

Estimate tail dependence on the rank (pseudo-observation) scale, never mix marginals in. The empirical tail estimate is noisy by definition, so you need many joint extremes or fit a parametric copula (Clayton for lower tail, Gumbel for upper). Bulk correlation can be low while tail dependence is high.

Joe (1997), Multivariate Models and Dependence Concepts; Nelsen (2006), An Introduction to Copulas.

20

Randomized dependence coefficient (RDC)

nonlinear

Scalable nonlinear, near-Pearson cost; practical for wide fleet feature matrices.

In plain words

RDC gets most of the nonlinear-detection power of the heavy methods for almost the price of a correlation. It rank-transforms each variable, projects through a handful of random sine features, then takes the canonical correlation between the two feature sets. Cheap enough to run across thousands of feature pairs.

Health-management example

A fleet feature matrix has hundreds of columns and you want a fast nonlinear dependence screen between every signal and the failure label. RDC ranks the candidates in one pass, and the top hits go to the more expensive HSIC or distance-correlation tests.

2026-06-19T16:49:26.804233 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from sklearn.cross_decomposition import CCA
def rdc(x, y, k=20, s=1/6):
    def feat(z):
        z = (np.argsort(np.argsort(z)) + 1) / len(z)       # copula transform
        W = np.random.randn(1, k) * s; b = np.random.rand(k) * 2 * np.pi
        return np.sin(z[:, None] * W + b)
    u, v = CCA(1).fit_transform(feat(x), feat(y))
    return abs(np.corrcoef(u.ravel(), v.ravel())[0, 1])
print("RDC =", rdc(x, y))
Practical note

A fast first-pass screen, not a verdict; confirm top hits with distance correlation or HSIC. Results vary with the random seed and the feature count k, so average over a few seeds.

Lopez-Paz, Hennig & Scholkopf (2013). The randomized dependence coefficient. NeurIPS.

Directional / causal extensions

The propagation story: not just whether two things move together, but which one moves first.

21

Granger causality

directional

Does A's past improve prediction of B; the linear workhorse for fault-propagation chains.

In plain words

A 'Granger-causes' B if adding A's recent history to a model of B's own history makes B more predictable. It is about predictive precedence, not philosophical cause, but for tracing how a disturbance moves through a system it is the practical first tool. It assumes mostly linear dynamics.

Health-management example

Suspect a sticking valve upstream is causing pressure swings downstream. If the valve signal's past significantly improves prediction of the downstream pressure (and not the reverse), Granger causality supports the upstream-to-downstream propagation hypothesis.

2026-06-19T16:49:26.857293 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from statsmodels.tsa.stattools import grangercausalitytests

data = np.column_stack([B, A])            # does A (col 2) cause B (col 1)?
res = grangercausalitytests(data, maxlag=5, verbose=False)
pmin = min(res[l][0]["ssr_ftest"][1] for l in res)
print("min p-value (A -> B):", pmin)
Practical note

Requires weakly stationary series, so difference or detrend first, and choose the lag order by AIC or BIC. It is predictive precedence, not mechanism: an unobserved common driver can fake it. Sensitive to sampling rate relative to the true delay.

Granger, C. W. J. (1969). Investigating causal relations by econometric models and cross-spectral methods. Econometrica 37.

22

Transfer entropy

directional

Model-free, nonlinear directed information flow; the nonlinear generalization of Granger, strong for tracing root cause through coupled subsystems.

In plain words

Transfer entropy measures how many bits of B's future are explained by A's past, beyond what B's own past already explains, with no assumption about linearity. Compute it both ways and the larger direction is the dominant information flow. It is Granger causality freed from the straight-line assumption.

Health-management example

In a tightly coupled thermal-electrical subsystem the dynamics are nonlinear. Transfer entropy from the current signal to the temperature signal, exceeding the reverse, points to current disturbances driving the thermal response, narrowing the root-cause search.

2026-06-19T16:49:26.906924 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
from pyinform.transferentropy import transfer_entropy   # pip install pyinform
# symbolize continuous signals first (e.g. bin into a few levels)
te_ab = transfer_entropy(sym_A, sym_B, k=1)
te_ba = transfer_entropy(sym_B, sym_A, k=1)
print(f"A->B {te_ab:.3f}   B->A {te_ba:.3f}   net {te_ab - te_ba:+.3f}")
Practical note

Model-free but data-hungry, especially as the history length k grows. The binning or kNN choice changes the value, so compare directions on identical settings, and bias-correct with surrogates before trusting small differences.

Schreiber, T. (2000). Measuring information transfer. Phys. Rev. Lett. 85.

23

Partial directed coherence (PDC) / DTF

directional

Frequency-domain Granger causality; which subsystem drives which at which frequency. A natural fit for vibration/NVH source tracing where coherence alone cannot resolve direction.

In plain words

Plain coherence tells you two channels share a frequency but not who leads. PDC and the directed transfer function fit a multivariate AR model to several channels at once, then read off directed influence frequency by frequency. You get a per-frequency arrow: channel i drives channel j at this Hz.

Health-management example

Three accelerometers share energy at 120 Hz, but you need the source. PDC shows the 120 Hz influence flowing from the bearing channel to the housing channels and not back, isolating the bearing as the originating NVH source.

2026-06-19T16:49:26.968880 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
import connectivipy as cp                 # pip install connectivipy

data = np.vstack([ch_A, ch_B, ch_C])      # channels x samples
conn = cp.Connect()                        # API varies by version
pdc = cp.conn.PDC().calculate(data=data, fs=fs, order=8)
print(pdc.shape)   # [freq, to, from]  -> directed coupling per frequency
Practical note

Only as good as the multivariate AR fit beneath it, so check residual whiteness and model order. Include every relevant channel or omitted-variable bias distorts the directions. Be consistent about the normalization (PDC versus generalized PDC).

Baccala & Sameshima (2001), Biol. Cybern. 84; Kaminski & Blinowska (1991), Biol. Cybern. 65 (DTF).

24

Convergent cross mapping (CCM)

directional

Causality detection for deterministic coupled dynamical systems where Granger assumptions break.

In plain words

In deterministic nonlinear systems (think coupled oscillators), cause and effect share a common attractor, and you can detect causation by checking whether the history of one variable can reconstruct the states of the other. The reconstruction quality must IMPROVE as you use more data ('converge'), which is the signature CCM looks for.

Health-management example

Two mechanical modes are coupled through nonlinear dynamics where Granger gives ambiguous results. CCM, by testing whether each mode's reconstructed attractor predicts the other, recovers the true coupling direction the linear test could not.

2026-06-19T16:49:27.015872 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
from causal_ccm.causal_ccm import ccm      # pip install causal-ccm

c = ccm(A, B, tau=1, E=3, L=len(A))         # embedding dim E, lag tau
corr, p = c.causality()
print(f"A xmap B skill = {corr:.3f}  (rises with L if A drives B)")
Practical note

For deterministic, low-noise coupled dynamics; heavy measurement noise breaks it. Tune the embedding dimension E and lag tau with false-nearest-neighbor and mutual-information heuristics. The signature is convergence: skill must rise with library length.

Sugihara et al. (2012). Detecting causality in complex ecosystems. Science 338.

25

Wavelet coherence

directional

Time-localized frequency-domain coupling for non-stationary regimes (run-ups, transients).

In plain words

Ordinary coherence assumes the coupling holds steady over the whole record. Wavelet coherence instead gives a time-frequency map: it shows WHEN and at WHICH frequency two signals are coupled, so a band that appears only during a transient stands out clearly.

Health-management example

During an engine run-up the coupling frequency rises with speed. Wavelet coherence draws a diagonal ridge in the time-frequency plane, capturing the moving resonance that a single fixed-frequency coherence value would average away.

2026-06-19T16:49:27.085945 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
import pycwt                                # pip install pycwt

dt = 1 / fs
WCT, _, coi, freqs, _ = pycwt.wct(A, B, dt, sig=False)
print(WCT.shape)    # [scale, time]  coherence in 0..1, localized in time
Practical note

For non-stationary coupling such as run-ups. Mind the cone of influence, the edges are unreliable, and get significance from surrogate (red-noise) testing. The mother wavelet trades time against frequency resolution.

Torrence & Compo (1998), Bull. Amer. Meteor. Soc. 79; Grinsted, Moore & Jevrejeva (2004), Nonlin. Processes Geophys. 11.

Conditional nonlinear analogues

Partial correlation and conditional MI, but without the linearity assumption.

26

Partial / conditional distance correlation

conditional nonlinear

Controls for a third variable set nonlinearly; the distance-correlation counterpart to partial correlation.

In plain words

Partial correlation removes a confounder assuming straight-line relationships. Partial distance correlation does the same job but for ANY shape: it measures the dependence of X and Y that remains after nonlinearly accounting for Z. It is the tool when both the confounding and the link are curved.

Health-management example

Two signals are coupled through a nonlinear dependence on load. Linear partial correlation might leave a misleading residual link; partial distance correlation, removing load nonlinearly, correctly shrinks the X-Y association to near zero.

2026-06-19T16:49:27.180623 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import dcor
# nonlinear association of x and y after removing z
pdc = dcor.partial_distance_correlation(x, y, z)
print("partial distance correlation =", pdc)
Practical note

Inherits distance correlation's order n-squared cost and large-sample appetite. The control can itself be multivariate. Like all conditioning, it removes only the confounders you supply.

Szekely & Rizzo (2014). Partial distance correlation with methods for dissimilarities. Ann. Statist. 42.

27

Conditional HSIC

conditional nonlinear

Kernel conditional-independence test; the isolation primitive when you need 'A independent of B given C' without linearity assumptions.

In plain words

Conditional HSIC tests whether A and B are independent ONCE you account for C, using kernels so it works for any nonlinear relationship. A common recipe: nonlinearly regress A and B on C, then run HSIC on the residuals. If they come out independent, C fully explained the apparent A-B link.

Health-management example

Two signals look coupled, but you suspect both merely follow operating mode C nonlinearly. Conditional HSIC on the mode-residuals returns near zero, the formal statement that A is independent of B given C, clearing them of a direct fault link.

2026-06-19T16:49:27.317612 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from sklearn.kernel_ridge import KernelRidge
def resid(t, z):
    z = z.reshape(-1, 1)
    return t - KernelRidge(kernel="rbf").fit(z, t).predict(z)
# hsic() defined earlier; run it on the C-residuals of A and B
print("conditional HSIC ~", hsic(resid(A, C), resid(B, C)))   # ~0 => A _|_ B | C
Practical note

The residualization assumes your kernel regression truly captures the dependence on the conditioning set, so choose the regressor well. The KCI test is the more rigorous option when you need calibrated p-values. Order n-squared.

Fukumizu, Gretton, Sun & Scholkopf (2008), NeurIPS; Zhang et al. (2011), UAI (KCI test).

Set-level linear

Coupling between whole groups of variables, not just single pairs.

28

Multiple correlation R / R-squared

set-level

Coupling of one target to a linear combination of predictors.

In plain words

Multiple R is the correlation between a target and the best linear blend of several predictors at once; R-squared is its square, read as the fraction of the target's variance the predictors jointly explain. It tells you how well a whole panel of sensors, together, tracks one quantity.

Health-management example

You want to know how much remaining useful life is explained by temperature, current, vibration and voltage combined. An R-squared of 0.82 says these four together capture most of the variation, a green light to build a model on that panel.

2026-06-19T16:49:27.390674 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from sklearn.linear_model import LinearRegression

X = np.column_stack([temp, current, vibration, voltage])
r2 = LinearRegression().fit(X, rul).score(X, rul)
print(f"R = {np.sqrt(r2):.3f}   R^2 = {r2:.3f}")
Practical note

R-squared always rises as predictors are added, so judge a panel by adjusted R-squared or cross-validation. High R-squared with multicollinear inputs hides which sensor matters, pair it with semi-partial correlations. It measures fit, not generalization.

Cohen, Cohen, West & Aiken (2003); Hotelling (1936), Biometrika 28.

29

Partial canonical correlation

set-level

Association between two variable sets after removing a controlling set.

In plain words

Canonical correlation finds the strongest link between two GROUPS of variables by combining each group into a single best-aligned summary. The partial version first removes a control set from both groups, so you see set-to-set coupling that is not just a shared response to the controls.

Health-management example

Relate the actuator-command set to the response-sensor set, after removing operating-condition variables from both. A strong partial canonical correlation says the commands and responses are coupled beyond what conditions alone explain, evidence the control path itself is the link.

2026-06-19T16:49:27.419133 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.cross_decomposition import CCA
def resid(M, C): return M - LinearRegression().fit(C, M).predict(C)

u, v = CCA(1).fit_transform(resid(set_X, control), resid(set_Y, control))
print("partial canonical corr =", abs(np.corrcoef(u.ravel(), v.ravel())[0, 1]))
Practical note

Regularize when either set is wide relative to the sample, or the canonical correlations overfit, and validate on held-out data. The control set is removed from both sides before the canonical fit.

Hotelling (1936), Biometrika 28; Rao (1973), Linear Statistical Inference (partial CCA).

Decision map

Start from the question you actually have, not the method you know. The badge marks which of the three framing questions it serves.

If you are asking...Reach forKind
Are two numeric signals coupled at all, linearly?Pearson rlinear
Coupled but the curve bends (still always rising)?Spearman rho / Kendall taurank
Coupled in any shape, even U-shapes and rings?Distance correlation, MIC, HSIC, Chatterjee xinonlinear
Is the link DIRECT, or borrowed from a shared driver?Partial correlation; conditional MI / partial dCor / cond. HSIC if nonlineardirect vs. confounded
How much UNIQUE value does one predictor add?Semi-partial correlationdirect vs. confounded
Which signal moves first / where does it propagate?Cross-correlation, Granger, transfer entropy, CCMdirectional
Coupled at a specific frequency, and who leads there?Coherence; PDC / DTF for direction; wavelet coherence if non-stationaryfrequency
Do redundant sensors AGREE in value (not just shape)?ICC, Lin's CCC, Kendall's Wagreement
At least one variable is a label or flag?Cramer's V, point-biserial, Theil's U, tetrachoriccategorical
Coupling between whole GROUPS of variables?Multiple R / R-squared, partial canonical correlationset-level
Risk that two systems fail TOGETHER at the extremes?Copula tail dependencenonlinear
Fast nonlinear screen across a wide feature matrix?RDC, then confirm hits with HSIC / dCornonlinear

A working order of escalation

  1. Start linear: Pearson for a quick read on numeric pairs.
  2. Distrust straight lines: move to Spearman / Kendall, then to distance correlation, MIC, HSIC or Chatterjee's xi if the shape might bend.
  3. Challenge every link: re-run with partial correlation (or conditional MI / partial distance correlation when nonlinear) to kill shared-driver illusions.
  4. Find the direction: cross-correlation for delay, Granger and transfer entropy for propagation, PDC for the per-frequency arrow.
  5. Mind the special cases: ICC and CCC for redundant-sensor agreement, the categorical block for fault codes and flags, copula tail dependence for joint-extreme risk.
# common stack
pip install numpy scipy scikit-learn statsmodels pingouin dcor minepy
pip install pyinform connectivipy causal-ccm pycwt npeet ace

Rules of thumb

Heuristics that hold across most diagnostics work, before the method-specific detail.

  1. Start with the cheapest measure that could answer the question, and escalate only when it fails or its assumptions do not hold.
  2. If Pearson and Spearman agree, the relationship is roughly linear; if they diverge, suspect nonlinearity or outliers and move to a rank or nonlinear measure.
  3. Never use plain correlation for redundant sensors: a biased sensor still scores near one. Use ICC for agreement or CCC against a reference.
  4. A correlation that survives partialing on load and temperature is worth investigating; one that collapses was confounded.
  5. Coupled is necessary, not sufficient, for connected. Confirm a lead-lag or causal measure before drawing a propagation arrow.
  6. Standardize heterogeneous scales (and consider whitening) before any distance or kernel measure.
  7. For any nonlinear or kernel measure, significance comes from a permutation test, not the raw magnitude.
  8. Rough sample-size floors: Pearson and Spearman about 30; distance correlation, HSIC and MI about 100 to 200; transfer entropy, PDC and CCM want thousands.
  9. Check stationarity before every time-series measure; for couplings that move in time use wavelet coherence.
  10. Conditioning removes only the confounders you measured, so state your control set with every direct-link claim.
Pick by scenario
ScenarioPick & whyKind
Fast screen of a wide fleet feature matrixRDC, then confirm hits with distance correlation or HSICnear-linear cost, still catches nonlinear couplingnonlinear
Confirm a suspected root cause among confounded signalsPartial correlation; partial distance correlation or conditional HSIC if nonlinearremoves the shared driver so only a direct path survivesdirect vs. confounded
Validate redundant sensors / detect calibration driftICC for agreement, Lin's CCC versus a referencepenalizes bias and offset, not just matching shapeagreement
Locate where a transient originatesCross-correlation lagrecovers both the delay and which side leadsdirectional
Trace fault propagation through coupled subsystemsGranger (linear), transfer entropy (nonlinear), CCM (deterministic)directed: who drives whomdirectional
NVH / vibration source localizationCoherence for the band, PDC for direction, wavelet coherence if non-stationaryfrequency-resolved coupling and directionfrequency
Relate fault codes to operating modesCramer's V, Theil's Ucategorical strength, and directional with Theil's Ucategorical
A continuous signal versus a fault flagPoint-biserial correlationscores how cleanly the signal separates the classescategorical
Suspected nonlinear link where Pearson is near zeroDistance correlation, MIC, HSIC, Chatterjee's xishape-agnostic dependence detectionnonlinear
Joint-failure risk at the operating limitsCopula lower-tail dependencecaptures co-extremes the bulk correlation missesnonlinear
How well a sensor panel tracks remaining lifeMultiple R / R-squared, plus semi-partial per sensorgroup-to-target fit and each sensor's unique shareset-level
Couple two subsystem variable groups under fixed conditionsPartial canonical correlationset-to-set coupling beyond the shared conditionsset-level
Small sample, possibly non-monotonicKendall's tau; be cautious with kernel methodsrobust and stable at low nrank
Streaming / online monitoringPearson or Spearman on sliding windows, with an EWMA of the statisticcheap and incremental to updatelagged / time

Worked case study: a DC-DC converter

The catalog is a parts bin. This is the assembly. One synthetic but structured fault, a MOSFET whose on-resistance is slowly rising, taken through the whole ladder. Every number below is computed by the engine on the same 700-sample record.

Step 1

Screen wide, cheaply

Run a fast RDC screen of every channel against output ripple. It is nearly free and catches nonlinear coupling, so it is the right first filter. The strongest associations are EMI, inductor temperature, load, and the cooling fan. Note efficiency: its RDC is real (0.52) yet its Pearson is almost zero, a nonlinear hit a linear screen would have thrown away.

2026-06-19T19:17:19.248541 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
Step 2

Confirm the nonlinear hit

Efficiency versus load is the textbook trap. Pearson reads 0.04 and Spearman -0.01, both essentially zero, because efficiency peaks at mid load and falls off at both ends. Distance correlation (0.52) and mutual information (1.09) see the U-shape plainly. Lesson: a near-zero Pearson is not evidence of independence.

Step 3

Kill the confound

Two channels look coupled to ripple. The cooling fan correlates at 0.73 raw, but once load and ambient are partialled out it falls to -0.03: the fan only ever tracked load, a pure confound. EMI correlates at 0.65 raw and holds at 0.55 after the same controls: a direct link that survives. This is the single most important move in the chain.

2026-06-19T19:17:19.384015 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
Step 4

Find the direction

Coupling is not connection until you know which way it flows. EMI is stochastic, so transfer entropy applies: ripple→EMI carries 0.134 nats against 0.077 the other way, and the cross-correlation peak sits at lag 2. The degradation signal is a slow monotone trend, so transfer entropy and Granger are on thin ice there (a trend breaks stationarity); instead its lead is read from lag structure, degradation leads ripple by 7 samples, and it is exogenous. Granger agrees in the supported direction (p = 0.005 forward versus 0.36 reverse), but the honest arrow here rests on lead-lag plus domain knowledge.

2026-06-19T19:17:19.501237 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
Verdict

Root cause and propagation path

Rising MOSFET on-resistance (the degradation) heats the inductor, which drives output ripple, which radiates as switching-node EMI: deg → inductor temp → ripple → EMI. The strong load couplings throughout were real but not causal to the fault; they are removed by partialing, not by the screen. No single measure delivered this. The screen narrowed the field, distance correlation rescued a nonlinear signal, partial correlation demoted the confounds, and lead-lag plus transfer entropy oriented the arrows.

Step 1 RDC scores vs ripple, for reference:

channelRDC|Pearson|
load0.770.76
fan0.740.73
emi0.670.65
t_ind0.650.64
eff0.510.01
deg0.320.26
t_amb0.270.16

Significance at fleet scale

When you screen a wide matrix, the per-test p-value stops meaning what you think it means. Three disciplines keep screening honest: a permutation null, a false-discovery correction, and an effect-size floor.

The problem

In the case-study record there are 28 channel pairs. At p < 0.05, 26 of them are significant, and a Benjamini-Hochberg correction still passes 26. With 700 samples almost nothing fails a significance test, because significance answers a question you did not ask: it asks whether an effect is exactly zero, not whether it is large enough to act on. Add an effect-size floor (here distance correlation ≥ 0.45) and the kept set drops to 10.

2026-06-19T19:17:19.636774 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/

The volcano plot makes the point: vertical axis is significance, horizontal axis is effect size, and only the shaded upper-right quadrant, significant and large, is worth a human's time.

The trap effect size does not fix

One kept pair is ambient temperature versus degradation, with a distance correlation of 0.95 and p = 0.005. They are not coupled. Both are slow trends over the record, and any two trends look strongly dependent. High effect, tiny p, and still spurious. The fix is upstream of the statistic: detrend, or test on stationary residuals, before you screen. A second caution: the screen also keeps ripple-versus-fan, which Step 3 already showed is a load confound. Screening finds marginal dependence; confounds are removed in the next stage, not the screen.

The recipe

For each pair compute the dependence measure and a permutation p-value, apply Benjamini-Hochberg across all pairs to control the false-discovery rate, then keep only pairs that clear both the FDR threshold and a pre-registered effect floor. The engine packages this as screen_pairs.

from measures import screen_pairs, distance_correlation

results = screen_pairs(
    X, names=channel_names,
    measure=distance_correlation,
    n_perm=199,          # permutation null per pair
    alpha=0.05,          # Benjamini-Hochberg FDR target
    effect_floor=0.30,   # minimum dCor worth acting on
)
kept = [r for r in results if r["keep"]]   # passes BOTH FDR and effect floor
Practical note

Pick the effect floor from what is operationally meaningful, not from the data. Use the same permutation seed and settings across pairs so they are comparable. Detrend or difference non-stationary channels first, or trends will dominate both the effect and the significance. Permutation cost scales with n_perm times the per-pair cost, so screen on a subsample, then re-test the survivors at full resolution.

Benjamini & Hochberg (1995), J. R. Statist. Soc. B 57; Westfall & Young (1993), Resampling-Based Multiple Testing.

References

  1. Pearson, K. (1896). Mathematical contributions to the theory of evolution III. Phil. Trans. R. Soc. A 187.
  2. Spearman (1904), Am. J. Psychol. 15; Kendall (1938), Biometrika 30.
  3. Baba, Shibata & Sibuya (2004). Partial correlation and conditional correlation as measures of conditional independence. Aust. N. Z. J. Stat. 46.
  4. Cohen, Cohen, West & Aiken (2003). Applied Multiple Regression/Correlation Analysis, 3rd ed.
  5. Box & Jenkins (1976), Time Series Analysis; Knapp & Carter (1976), IEEE Trans. ASSP 24.
  6. Szekely, Rizzo & Bakirov (2007). Measuring and testing dependence by correlation of distances. Ann. Statist. 35; Reshef et al. (2011), Science 334 (MIC).
  7. Kraskov, Stogbauer & Grassberger (2004), Phys. Rev. E 69; Frenzel & Pompe (2007), Phys. Rev. Lett. 99 (conditional MI).
  8. Carter (1987), Proc. IEEE 75; Bendat & Piersol, Random Data.
  9. Shrout & Fleiss (1979), Psychol. Bull. 86; McGraw & Wong (1996), Psychol. Methods 1.
  10. Lin, L. (1989). A concordance correlation coefficient to evaluate reproducibility. Biometrics 45.
  11. Kendall & Babington Smith (1939). The problem of m rankings. Ann. Math. Statist. 10.
  12. Cramer (1946), Mathematical Methods of Statistics; Bergsma (2013), J. Korean Statist. Soc. 42 (bias correction).
  13. Tate, R. F. (1954). Correlation between a discrete and a continuous variable. Ann. Math. Statist. 25.
  14. Theil, H. (1972). Statistical Decomposition Analysis; Press et al., Numerical Recipes.
  15. Pearson (1900), Phil. Trans. R. Soc. A 195; Olsson (1979), Psychometrika 44 (polychoric).
  16. Gretton, Bousquet, Smola & Scholkopf (2005), ALT; Gretton et al. (2008), NeurIPS (test).
  17. Chatterjee, S. (2021). A new coefficient of correlation. J. Amer. Statist. Assoc. 116.
  18. Renyi (1959), Acta Math. Hungar. 10; Breiman & Friedman (1985), JASA 80 (ACE).
  19. Joe (1997), Multivariate Models and Dependence Concepts; Nelsen (2006), An Introduction to Copulas.
  20. Lopez-Paz, Hennig & Scholkopf (2013). The randomized dependence coefficient. NeurIPS.
  21. Granger, C. W. J. (1969). Investigating causal relations by econometric models and cross-spectral methods. Econometrica 37.
  22. Schreiber, T. (2000). Measuring information transfer. Phys. Rev. Lett. 85.
  23. Baccala & Sameshima (2001), Biol. Cybern. 84; Kaminski & Blinowska (1991), Biol. Cybern. 65 (DTF).
  24. Sugihara et al. (2012). Detecting causality in complex ecosystems. Science 338.
  25. Torrence & Compo (1998), Bull. Amer. Meteor. Soc. 79; Grinsted, Moore & Jevrejeva (2004), Nonlin. Processes Geophys. 11.
  26. Szekely & Rizzo (2014). Partial distance correlation with methods for dissimilarities. Ann. Statist. 42.
  27. Fukumizu, Gretton, Sun & Scholkopf (2008), NeurIPS; Zhang et al. (2011), UAI (KCI test).
  28. Cohen, Cohen, West & Aiken (2003); Hotelling (1936), Biometrika 28.
  29. Hotelling (1936), Biometrika 28; Rao (1973), Linear Statistical Inference (partial CCA).

Take it into your editor

The same content is packaged as a VS Code Copilot skill in the progressive-disclosure layout: a routing SKILL.md, per-group reference docs, a tested measures.py engine that implements every method with numpy, scipy, scikit-learn and statsmodels only, and Copilot integration files (a diagnostics analyst chat mode, a /dependence prompt, and path-scoped instructions). Drop the folder into your kit, or point Copilot at the skill directly.

diagnostic-dependence-kit.zip
SKILL.md · references · measures.py · Copilot agent + prompt

Link resolves when the zip sits beside this file. Run python scripts/measures.py for the self-test on synthetic diagnostic data.