diff --git a/.github/linters/.codespellrc b/.github/linters/.codespellrc index 88c818e..9b4f59e 100644 --- a/.github/linters/.codespellrc +++ b/.github/linters/.codespellrc @@ -6,10 +6,12 @@ # ignore-words-list: legitimate domain acronyms codespell mistakes for typos # (GES = Greedy Equivalence Search; FPR = false-positive rate) and identifiers # that intentionally misspell a Python reserved word to use it as a name -# (lamda = lambda, a regularization-coefficient parameter name in sigkci_gpu.py). +# (lamda = lambda, a regularization-coefficient parameter name in sigkci_gpu.py), +# plus "retuned" -- a real word ("re-tuned") that codespell mistakes for +# "returned" (routed_deconf.py's research-history docstring). [codespell] # docs/_build is generated Sphinx output (HTML + a minified search index) and # is gitignored, so CI never scans it -- but a local run over the repo root # does, and reports spurious hits from the minified JS. skip = *.ipynb,./docs/_build -ignore-words-list = ges,fpr,lamda +ignore-words-list = ges,fpr,lamda,retuned diff --git a/CHANGELOG.md b/CHANGELOG.md index b39f01e..3ac4614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +* **LUCID** (`causalts.confounders`) — regime-adaptive deconfounding for causal discovery + under latent confounders. `run_lucid(df, max_lag)` diagnoses whether latent confounding + is sparse or pervasive from the residual spectrum (against a Marchenko–Pastur no-factor + null) and applies the matching correction, returning a `LucidResult`. Every + `CausalResult` now also exposes `.deconfound()`, `.tetrad_filter()`, and `.pds_filter()` + so LUCID (or a fixed-strategy comparator) can be applied to a graph already discovered + with any algorithm, without re-running the skeleton search. See the new + [Unobserved Confounders (LUCID)](examples/latent_confounder_detection) tutorial and the + `causalts.confounders` API page. * `corrplot(..., diag="glyph")` — renders the diagonal as an ordinary cell, using the same `method` and colormap as the rest of the matrix. Intended for *directed* matrices (a cause→effect adjacency or an edge-stability matrix), diff --git a/causalts/__init__.py b/causalts/__init__.py index f187726..f2dd0f0 100644 --- a/causalts/__init__.py +++ b/causalts/__init__.py @@ -37,6 +37,12 @@ if TYPE_CHECKING: # pragma: no cover - import for type checkers only from .cedar.legacy import SYPI # noqa: F401 + from .confounders import ( # noqa: F401 + LucidResult, + deconfound, + routed_deconfound, + run_lucid, + ) from .grace.gated_discovery import ( # noqa: F401 run_cdnots_gated, run_stability_selection, @@ -44,10 +50,15 @@ from .grace.result import GraceResult # noqa: F401 # Served on demand so that ``import causalts`` stays cheap: cedar.legacy pulls -# in dcor + statsmodels, grace pulls in pytorch-lightning, and neither is on the -# common discovery path. Maps attribute name -> module that defines it. +# in dcor + statsmodels, grace pulls in pytorch-lightning, confounders pulls in +# statsmodels + scikit-learn's cluster/linear_model, and none is on the common +# discovery path. Maps attribute name -> module that defines it. _LAZY_ATTRS = { "SYPI": "causalts.cedar.legacy", + "LucidResult": "causalts.confounders", + "deconfound": "causalts.confounders", + "routed_deconfound": "causalts.confounders", + "run_lucid": "causalts.confounders", "run_cdnots_gated": "causalts.grace.gated_discovery", "run_stability_selection": "causalts.grace.gated_discovery", "GraceResult": "causalts.grace.result", diff --git a/causalts/confounders/__init__.py b/causalts/confounders/__init__.py new file mode 100644 index 0000000..0b72f28 --- /dev/null +++ b/causalts/confounders/__init__.py @@ -0,0 +1,39 @@ +# Copyright 2025 Bloomberg Finance L.P. +# SPDX-License-Identifier: GPL-3.0-or-later +"""LUCID: regime-adaptive deconfounding for time-series causal discovery. + +Latent confounders leave different statistical fingerprints depending on their +structure, and no single correction handles all of them. LUCID infers the confounding +regime from the data with a Marchenko-Pastur spectral router, then applies the strategy +matched to that regime. + +The main entry point is :func:`run_lucid`, which returns a +:class:`~causalts.confounders.result.LucidResult` carrying the graph alongside the +router's diagnostics. :func:`routed_deconfound` is the lower-level array-returning form. +Every result object also exposes ``.deconfound()``, which applies LUCID to an +already-discovered graph without re-running the skeleton search. + +See :mod:`causalts.confounders.routed_deconf` for details. +""" + +from .result import LucidResult +from .routed_deconf import ( + deconfound, + mp_factor_count, + pds_filter, + routed_deconfound, + run_lucid, + spectral_gap, + tetrad_filter, +) + +__all__ = [ + "run_lucid", + "LucidResult", + "routed_deconfound", + "deconfound", + "spectral_gap", + "mp_factor_count", + "pds_filter", + "tetrad_filter", +] diff --git a/causalts/confounders/result.py b/causalts/confounders/result.py new file mode 100644 index 0000000..80de83a --- /dev/null +++ b/causalts/confounders/result.py @@ -0,0 +1,109 @@ +# Copyright 2025 Bloomberg Finance L.P. +# SPDX-License-Identifier: GPL-3.0-or-later +"""Result object for LUCID.""" + +from __future__ import annotations + +import numpy as np + +from ..result import CausalResult + + +class LucidResult(CausalResult): + """Result from :func:`~causalts.confounders.run_lucid`. + + Inherits plotting and DoWhy bridge methods from :class:`CausalResult`. + + Attributes + ---------- + cg_tig : np.ndarray, shape (d, d, max_lag+1) + Binary adjacency. ``cg_tig[cause, effect, lag] == 1`` means + cause(t-lag) -> effect(t). + var_names : list[str] + Variable names. + regime : str + Confounding regime chosen by the router: ``"sparse"``, ``"pervasive"``, or + ``"sf"`` (an intermediate hub/scale-free regime). + spectral_ratio : float or None + The router statistic ``R`` -- the share of residual correlation mass carried by + the top ``router_k`` eigenvalues. ``None`` for routers that do not compute it. + tau : float or None + Routing threshold ``R`` was compared against. ``R > tau`` selects the pervasive + branch. + router : str + Which router ran (``"auto"``, ``"spectral"`` or ``"mp"``). + n_factors : int or None + Number of pervasive latent factors implied by the Marchenko-Pastur edge + (:func:`~causalts.confounders.mp_factor_count`). ``0`` means no pervasive + factor was detected. + factor_loadings : np.ndarray or None, shape (n_factors, d) + Leading right singular vectors of the VAR residuals, one row per detected + factor, columns aligned with ``var_names``. + + .. warning:: + These are **descriptive, not identified**. Factor directions are recovered + only up to rotation, so a large entry means "this variable carries dominant + shared variation", *not* "this variable has an identified latent parent". + LUCID does not identify latent variables; it corrects for their footprint. + info : dict + Full router/pipeline diagnostics as returned by + :func:`~causalts.confounders.routed_deconfound` with ``return_info=True``. + runtime : float or None + Wall-clock seconds for the LUCID call. + """ + + def __init__( + self, + graph: np.ndarray, + df, + var_names: list[str], + *, + info: dict | None = None, + n_factors: int | None = None, + factor_loadings: np.ndarray | None = None, + runtime: float | None = None, + ): + self.cg_tig = graph + self.var_names = list(var_names) + self._df = df + self._scm_cache = {} + self.info = dict(info or {}) + self.regime = self.info.get("regime") + self.spectral_ratio = self.info.get("R") + self.tau = self.info.get("tau") + self.router = self.info.get("router") + self.n_factors = n_factors + self.factor_loadings = factor_loadings + self.runtime = runtime + + def plot(self, **kwargs): + """Plot the deconfounded causal graph.""" + kwargs.setdefault("show_colorbar", False) + return super().plot(**kwargs) + + def top_factor_variables(self, factor: int = 0, n: int = 5): + """Variables loading most strongly on one factor direction, by ``|loading|``. + + Descriptive only -- see the ``factor_loadings`` warning above. + + Returns a list of ``(var_name, loading)`` pairs, largest ``|loading|`` first. + """ + if self.factor_loadings is None: + return [] + if not 0 <= factor < self.factor_loadings.shape[0]: + raise IndexError( + f"factor {factor} out of range (n_factors={self.factor_loadings.shape[0]})" + ) + row = self.factor_loadings[factor] + order = np.argsort(np.abs(row))[::-1][:n] + return [(self.var_names[i], float(row[i])) for i in order] + + def __repr__(self): + d = len(self.var_names) + edges = int((self.cg_tig == 1).sum()) + bits = [f"regime={self.regime!r}"] + if self.spectral_ratio is not None and self.tau is not None: + bits.append(f"R={self.spectral_ratio:.3f} vs tau={self.tau:.3f}") + if self.n_factors is not None: + bits.append(f"n_factors={self.n_factors}") + return f"LucidResult(d={d}, edges={edges}, " + ", ".join(bits) + ")" diff --git a/causalts/confounders/routed_deconf.py b/causalts/confounders/routed_deconf.py new file mode 100644 index 0000000..760eab8 --- /dev/null +++ b/causalts/confounders/routed_deconf.py @@ -0,0 +1,1646 @@ +# Copyright 2025 Bloomberg Finance L.P. +# SPDX-License-Identifier: GPL-3.0-or-later +"""LUCID: regime-adaptive deconfounding for time-series causal discovery. + +LUCID (Learning Under Confounding for Inference and Discovery). Latent confounders +leave different statistical fingerprints depending on their structure, and no single +correction handles all of them: rank/tetrad methods exploit the low-rank covariance of +*pervasive* factor confounding but destroy true edges under *sparse* (local fork) +confounding, and vice-versa. Applying the wrong correction can be as damaging as +applying none. This module infers the regime from the data and applies the matching +correction. + +Pipeline (per dataset): + + 1. Route by a data-only statistic into a regime: + - "sparse" : full-rank residual covariance (local latent forks) + - "pervasive" : one or more dominant latent factors (e.g. GARCH factors) + ("sf" is an intermediate hub/scale-free regime; see routing below.) + 2. Run skeleton discovery. Both branches use the *same* base engine (plain + ``run_cdnots``), so the regime affects only the correction that follows -- which + isolates the contribution of routing from any change in the search itself, and + means an already-discovered graph can be reused (``run_lucid(discovery=...)``). + 3. Apply the regime-matched correction: + - sparse : a post-double-selection (PDS) edge filter over observed controls. + Granger-style at lags >= 1; at lag 0 the corresponding + contemporaneous partial regression with lagged controls. + - pervasive : lagged edges are kept as discovered and the lag-0 slice is + *reconstructed* by S-L -- attenuate the factor-dominated spectral + directions, then recover sparse conditional-dependence candidates + from the residual innovations against an edge-free null, with a + persistence gate to limit moralization. + +Routers: + - router="auto" (default): parameter-free; the sparse<->pervasive boundary is derived + from the Marchenko-Pastur no-factor null (adapts to d/T), with a robust sub-regime + split. + - router="spectral": spectral-gap statistic with calibrated thresholds (ablation; + expert users may override the thresholds). + - router="mp": Marchenko-Pastur factor count (ablation). + +Public API: + run_lucid(df_obs, max_lag, ...) -> LucidResult [graph + router diagnostics] + routed_deconfound(df_obs, max_lag, ...) -> (d, d, max_lag+1) graph [low-level] + deconfound(graph, df_obs, max_lag, regime, ...) -> graph [post-hoc layer] + tetrad_filter(df_obs, graph, max_lag, threshold) -> graph [fixed comparator] + pds_filter(df_obs, graph, max_lag, alpha) -> graph + spectral_gap(X, k) / mp_factor_count(X) -> router statistics + +Every result object also exposes ``.deconfound()``, ``.tetrad_filter()`` and +``.pds_filter()``; see :mod:`causalts.result`. + +Non-default research knobs are retained for the paper's ablations but are not the +shipped configuration: ``pervasive_base="tetrad"`` swaps in the CD-NOTS+ engine plus +the tetrad lag-0 filter, and ``pervasive_filters=True`` re-enables the LLCCA, +volatility-invariance and factor-PDS filters, which the filter ablation found +net-harmful on the unified base. +""" + +from __future__ import annotations + +import numpy as np +import statsmodels.api as sm +from sklearn.cluster import KMeans +from sklearn.linear_model import LassoLarsIC + +from ..cdnots.phase3_utils import run_cdnots, run_cdnots_plus +from ..ci_tests.parcorr_gpu import ParCorrGPU +from ..utils.tetrad import apply_tetrad_lag0_filter +from .result import LucidResult + +# ── Defaults ───────────────────────────────────────────────────────────────── +# Spectral-router thresholds — calibrated on the synthetic benchmark (ER ~0.16-0.23, +# SF ~0.24-0.34, GARCH ~0.49-0.51). Only used by router="spectral". +_SPECTRAL_TAU = 0.215 +_SPECTRAL_TAU2 = 0.40 +# router="auto": the sparse<->pervasive boundary is derived from the Marchenko-Pastur +# no-factor null (adapts to d/T, no user knob). _TAU_MARGIN is a mild safety factor +# above the null edge; at d=15/T=1000 it recovers the calibrated 0.215. +_TAU_MARGIN = 1.3 +# PDS significance (edge kept iff p < alpha). Plateau values from the alpha sweep. +_ALPHA_PDS_SPARSE = 1e-10 +_ALPHA_PDS_PERVASIVE = 1e-8 +_ALPHA_VOL = 0.05 +_LLCCA_THRESHOLD = 0.05 +# Factor-augmented PDS level (homoskedastic-pervasive branch) + ARCH heteroskedasticity +# gate level. Both conventional defaults; no per-problem tuning required. +_ALPHA_FACTOR_PDS = 0.01 +_ARCH_ALPHA = 0.05 +_HAC_LAGS = 2 +_MAX_SEL = 20 +_MIN_ENV_SIZE = 30 +_TETRAD_THRESHOLD = 0.25 +# Number of leading eigenvalues in the router statistic R and its threshold. +_ROUTER_K = 2 +# Multiplicative slack on the MP edge in mp_factor_count. +_FACTOR_COUNT_MARGIN = 1.02 +# Double-persistence lag-0 gate (research-loop ideas 001c/002/003/007). Soft gate on +# the full-conditioning |pcorr| score by the S=empty marginal (Spearman rank) +# correlation of the deconfounded residuals: gate(m) = 1 - exp(-(m/tau)^2). tau=0.10 +# is the VAL-tuned optimum for the rank-correlation gate. +_GATE_TAU = ( + 0.15 # validated (30-seed Wilcoxon): 0.15 > 0.10 (tau=0.10 sig. hurts garch/t3_d40) +) +# Winsorization constant (idea 007) for the full-conditioning covariance arm only. +_PC_COV_ESTIMATOR = ( + "sample" # gate-only is the default method. winsor_4.5 is an OPTIONAL +) +# heavy-tail refinement (30-seed Wilcoxon: significant only on t3_d24 +0.033; neutral on the +# other 6 cells; the apparent t3_d40 dip is not significant). Off by default for parsimony. + + +# ── Router statistics (data-only) ──────────────────────────────────────────── +def _var1_residuals(X): + """Residuals of a VAR(1) least-squares fit (with intercept).""" + T = X.shape[0] + Y = X[1:] + Z = np.column_stack([np.ones(T - 1), X[:-1]]) + beta, *_ = np.linalg.lstsq(Z, Y, rcond=None) + return Y - Z @ beta + + +def _varp_residuals(X, p): + """Residuals of a VAR(p) least-squares fit (with intercept). p>=1.""" + X = np.asarray(X) + T = X.shape[0] + if p <= 1: + return _var1_residuals(X) + Y = X[p:] + Z = np.column_stack([np.ones(T - p)] + [X[p - 1 - k : T - 1 - k] for k in range(p)]) + beta, *_ = np.linalg.lstsq(Z, Y, rcond=None) + return Y - Z @ beta + + +def _select_var_order_whiteness(X, max_p=6, alpha=0.05): + """Lag order by RESIDUAL WHITENESS: raise p until the VAR(p) residuals have no significant + lagged cross-correlation at lags > p (max |cross-corr| test, Bonferroni over the d^2 pairs). + + Why not AIC/BIC: at d~20 a full VAR(p) adds a dense d x d coefficient block (~d^2 params), + so an information criterion over the full likelihood over-penalizes it and stays at p=1 even + when a FEW sparse higher-lag edges are present (validated: BIC no-op; AIC recovers only when + higher-lag structure is dense/strong). A whiteness test targets exactly "is there temporal + structure LEFT in the residuals", so it fires on sparse higher-lag too. Bonferroni over d^2 + (not d^2 x horizon) is the calibrated sweet spot: PERFECT do-no-harm on genuinely lag-1 data + (returns 1, residualization unchanged) with 2-4x more higher-lag recovery than AIC. A + portmanteau (chi^2 aggregate) variant over-selects and harms lag-1 -- rejected. Recovery is + partial on the weakest sparse lag>=2 (picks p~1.5-2, not the full order); that residual gap + is a documented limitation.""" + from scipy.stats import norm + + X = np.asarray(X) + T, d = X.shape + max_p = max(1, min(max_p, (T - 1) // (2 * (d + 1)))) + for p in range(1, max_p + 1): + R = _varp_residuals(X, p) + n = R.shape[0] + if n < 8: + return p + Rs = (R - R.mean(0)) / (R.std(0) + 1e-12) + thr = norm.ppf(1 - alpha / (2 * d * d)) / np.sqrt(n) + white = True + for lag in range(p + 1, max_p + 1): + if n - lag < 5: + continue + C = (Rs[:-lag].T @ Rs[lag:]) / (n - lag) + if np.max(np.abs(C)) > thr: + white = False + break + if white: + return p + return max_p + + +def _var_residuals(X, order=1): + """VAR residuals; order=int for a fixed lag, or "auto" for whiteness-selected VAR(p) + (`_select_var_order_whiteness`; do-no-harm on lag-1, partially recovers higher-lag). + An AIC/BIC order selector was evaluated and dropped: it under-detects sparse + higher-lag structure, because a full VAR(p) block costs ~d^2 parameters.""" + p = _select_var_order_whiteness(X) if order == "auto" else int(order) + return _varp_residuals(X, p) + + +def spectral_gap(X, k=2): + """Top-k eigenvalue mass of the VAR(1) innovation *correlation* matrix.""" + d = X.shape[1] + C = np.corrcoef(_var1_residuals(X), rowvar=False) + C = np.nan_to_num(C, nan=0.0) + eigvals = np.sort(np.linalg.eigvalsh(C))[::-1] + return float(eigvals[:k].sum() / d) + + +def _mp_null_top_mass(d, T_eff, k=2): + """Expected top-k eigenvalue mass (/d) of a correlation matrix under the + Marchenko-Pastur no-factor null. Used to set the sparse<->pervasive boundary + parameter-free, adapting to the d/T aspect ratio.""" + lam_plus = (1.0 + np.sqrt(d / max(T_eff, 1))) ** 2 + return k * lam_plus / d + + +def mp_factor_count(X, var_order=1, factor_count_margin=_FACTOR_COUNT_MARGIN): + """Parameter-free count of pervasive latent factors via the Marchenko-Pastur law. + + Eigenvalues of the VAR(1) innovation *correlation* matrix that exceed the MP upper + edge lambda_+ = (1 + sqrt(d/T_eff))^2 cannot come from a no-factor (bulk) null, so + they signal pervasive latent factors. Uses the correlation matrix (unit-variance, + so the MP null scale sigma^2 = 1), which also makes it robust to per-series + heteroskedasticity (e.g. GARCH) that would distort a covariance-based edge. + + ``factor_count_margin`` is a small multiplicative slack on the edge so borderline + bulk eigenvalues are not counted as factors (paper Table 4). + + Returns the number of eigenvalues above the edge (0 = sparse/no pervasive factor). + """ + resid = _var_residuals(X, var_order) + T_eff, d = resid.shape + C = np.corrcoef(resid, rowvar=False) + C = np.nan_to_num(C, nan=0.0) + eigvals = np.linalg.eigvalsh(C) + q = d / max(T_eff, 1) + lambda_plus = (1.0 + np.sqrt(q)) ** 2 + return int(np.sum(eigvals > lambda_plus * factor_count_margin)) + + +def _vol_clustering_pvalue(X, lags=10): + """Ljung-Box p-value for autocorrelation in the shared-volatility series (mean + squared VAR(1) innovation across variables). Small p => ARCH-type volatility + clustering (GARCH-like); large p => no clustering (e.g. a scale-free hub factor). + Returns None on failure.""" + try: + from statsmodels.stats.diagnostic import acorr_ljungbox + + resid = _var1_residuals(X) + totvol = np.mean(resid**2, axis=1) + totvol = totvol - totvol.mean() + lb = acorr_ljungbox(totvol, lags=[min(lags, len(totvol) // 5)], return_df=True) + return float(lb["lb_pvalue"].iloc[-1]) + except Exception: + return None + + +def _route( + X, + router="mp", + thresholds=None, + router_k=_ROUTER_K, + gamma=_TAU_MARGIN, + factor_count_margin=_FACTOR_COUNT_MARGIN, +): + """Return regime label in {"sparse", "sf", "pervasive"} plus a diagnostics dict. + + ``router_k`` is the number of leading eigenvalues entering the statistic and its + threshold; because both scale with k they largely cancel under the no-factor null, + so the routing decision is insensitive to it (paper Appendix C). ``gamma`` is the + fixed margin over the Marchenko-Pastur edge. + """ + if router in ("auto", "spectral"): + R = spectral_gap(X, k=router_k) + if router == "auto": + # sparse<->pervasive boundary from the MP null (adapts to d/T); SF<->GARCH + # boundary is a robust default in a wide gap (0.31->0.49), expert-overridable. + T_eff, d = X.shape[0] - 1, X.shape[1] + tau = gamma * _mp_null_top_mass(d, T_eff, k=router_k) + tau2 = thresholds[1] if thresholds else _SPECTRAL_TAU2 + else: # "spectral": fixed calibrated thresholds + tau, tau2 = thresholds or (_SPECTRAL_TAU, _SPECTRAL_TAU2) + if R <= tau: + regime = "sparse" + elif R <= tau2: + regime = "sf" + else: + regime = "pervasive" + return regime, { + "router": router, + "regime": regime, + "R": R, + "tau": tau, + "tau2": tau2, + "router_k": router_k, + "gamma": gamma, + } + elif router == "mp": + # Stage 1 (Marchenko-Pastur edge): any pervasive latent factor present? + # This cleanly separates sparse (0 factors) from the pervasive family, but + # NOT scale-free from GARCH -- a scale-free hub is also low-rank, so both + # show factors. Stage 2 splits the pervasive family by a volatility- + # clustering test: GARCH has ARCH-type clustering, a hub confounder does not. + n = mp_factor_count(X, factor_count_margin=factor_count_margin) + if n == 0: + regime = "sparse" + return regime, { + "router": "mp", + "regime": regime, + "n_factors": n, + "vol_clustering_p": None, + } + p_arch = _vol_clustering_pvalue(X) + regime = "pervasive" if (p_arch is not None and p_arch < 0.05) else "sf" + return regime, { + "router": "mp", + "regime": regime, + "n_factors": n, + "vol_clustering_p": p_arch, + } + else: + raise ValueError(f"unknown router {router!r} (use 'mp' or 'spectral')") + + +# ── Edge filters ───────────────────────────────────────────────────────────── +def _lagged_design(X, max_lag): + """Stack lag blocks 1..max_lag into one design matrix aligned to Y = X[max_lag:].""" + T, d = X.shape + rows = T - max_lag + cols, names = [], [] + for lag in range(1, max_lag + 1): + start = max_lag - lag + cols.append(X[start : start + rows]) + names += [(j, lag) for j in range(d)] + Z = np.column_stack(cols) if cols else np.zeros((rows, 0)) + return Z, names, rows + + +def _select_controls(Zc, target): + if Zc.shape[1] == 0: + return [] + mu, sd = Zc.mean(0), Zc.std(0) + 1e-8 + try: + m = LassoLarsIC(criterion="bic", max_iter=200).fit((Zc - mu) / sd, target) + return np.where(np.abs(m.coef_) > 1e-8)[0].tolist() + except Exception: + return [] + + +def _pds_test(y, x, Zc, alpha): + sel = list(set(_select_controls(Zc, y)) | set(_select_controls(Zc, x)))[:_MAX_SEL] + Xd = np.column_stack([x, Zc[:, sel]]) if sel else x.reshape(-1, 1) + try: + model = sm.OLS(y, sm.add_constant(Xd)).fit( + cov_type="HAC", cov_kwds={"maxlags": _HAC_LAGS} + ) + pval = model.pvalues[1] + except Exception: + return True # abstain (keep) on numerical failure + return True if np.isnan(pval) else pval < alpha + + +def tetrad_filter(df_obs, g_est, max_lag, threshold=_TETRAD_THRESHOLD): + """Drop lag-$0$ edges between variable pairs that share a latent factor. + + A fixed (non-adaptive) deconfounding strategy, and the natural comparator for + LUCID's regime-adaptive routing: it always assumes pervasive factor structure. Uses + the tetrad vanishing condition (Spearman 1928) via + :func:`causalts.utils.tetrad.detect_shared_factors`. + + Post-hoc like the other filters in this module -- it consumes an already-discovered + graph and never re-runs discovery, so it composes with any base engine. + + Parameters + ---------- + df_obs : DataFrame (T, d) + Observed data. + g_est : ndarray (d, d, max_lag+1) + Discovered graph to filter. + max_lag : int + Maximum lag (present for signature parity with the other filters; the tetrad + test only touches the lag-0 slice). + threshold : float, default 0.25 + Factor-consistency threshold; lower is more conservative. + + Returns + ------- + ndarray + Copy of ``g_est`` with confounded lag-0 edges removed. + """ + return apply_tetrad_lag0_filter( + g_est, df_obs, list(df_obs.columns), threshold=threshold + ) + + +def pds_filter(df_obs, g_est, max_lag, alpha): + """Post-double-selection Granger edge filter: drop edges whose direct effect is + insignificant after Lasso-selecting controls from all lagged variables.""" + X = df_obs.values + d = X.shape[1] + Z_full, names_full, rows = _lagged_design(X, max_lag) + Y = X[max_lag:] + idx_map = {key: c for c, key in enumerate(names_full)} + g_out = g_est.copy() + for i in range(d): + for j in range(d): + if i == j: + continue + for lag in range(max_lag + 1): + if g_est[i, j, lag] == 0: + continue + y = Y[:, j] + if lag == 0: + x_edge, ctrl_keys = X[max_lag:, i], names_full + else: + key = (i, lag) + if key not in idx_map: + continue + x_edge = Z_full[:, idx_map[key]] + ctrl_keys = [k for k in names_full if k != key] + ctrl_idx = [idx_map[k] for k in ctrl_keys] + Zc = Z_full[:, ctrl_idx] if ctrl_idx else np.zeros((rows, 0)) + if not _pds_test(y, x_edge, Zc, alpha): + g_out[i, j, lag] = 0 + return g_out + + +def _llcca_asymmetry(X, i, j, max_lag): + fwd = bwd = 0.0 + T = X.shape[0] + for lag in range(1, max_lag + 1): + if T - lag < 2: + continue + c_fwd = np.corrcoef(X[: T - lag, i], X[lag:, j])[0, 1] + c_bwd = np.corrcoef(X[: T - lag, j], X[lag:, i])[0, 1] + if not np.isnan(c_fwd): + fwd += abs(c_fwd) + if not np.isnan(c_bwd): + bwd += abs(c_bwd) + return (fwd - bwd) / (fwd + bwd + 1e-8) + + +def llcca_filter(df_obs, g_est, max_lag, threshold=_LLCCA_THRESHOLD): + """Lead-lag cross-correlation asymmetry: drop edges whose forward/backward + cross-correlation is near-symmetric (the footprint of a shared latent cause + rather than a directed effect).""" + X = df_obs.values + d = X.shape[1] + g_out = g_est.copy() + for i in range(d): + for j in range(d): + if i == j or not np.any(g_out[i, j, :]): + continue + if _llcca_asymmetry(X, i, j, max_lag) < threshold: + g_out[i, j, :] = 0 + return g_out + + +def _volatility_environments(X, max_lag): + """0/1 low/high shared-volatility label per row of Y=X[max_lag:], from the mean + squared VAR(1) innovation across all variables (a common latent-vol proxy).""" + T, d = X.shape + resid = _var1_residuals(X) + logvol = np.log(np.mean(resid**2, axis=1) + 1e-12) + try: + km = KMeans(n_clusters=2, n_init=10, random_state=0).fit(logvol.reshape(-1, 1)) + env_full = km.labels_ + if logvol[env_full == 0].mean() > logvol[env_full == 1].mean(): + env_full = 1 - env_full + except Exception: + return None + rows = T - max_lag + start = max_lag - 1 + if start < 0 or start + rows > env_full.shape[0]: + return None + return env_full[start : start + rows] + + +def _vol_invariance_test(y, x_edge, env, alpha): + if int(np.sum(env == 0)) < _MIN_ENV_SIZE or int(np.sum(env == 1)) < _MIN_ENV_SIZE: + return True # abstain + design = sm.add_constant(np.column_stack([x_edge, env, x_edge * env])) + try: + model = sm.OLS(y, design).fit(cov_type="HAC", cov_kwds={"maxlags": _HAC_LAGS}) + pval = model.pvalues[-1] + except Exception: + return True + return True if np.isnan(pval) else pval >= alpha + + +def vol_invariance_filter(df_obs, g_est, max_lag, alpha=_ALPHA_VOL): + """Drop edges whose regression coefficient depends on a shared volatility regime + (a by-product of a latent common volatility factor).""" + X = df_obs.values + d = X.shape[1] + env = _volatility_environments(X, max_lag) + if env is None: + return g_est + Z_full, names_full, rows = _lagged_design(X, max_lag) + Y = X[max_lag:] + idx_map = {key: c for c, key in enumerate(names_full)} + g_out = g_est.copy() + for i in range(d): + for j in range(d): + if i == j: + continue + for lag in range(max_lag + 1): + if g_out[i, j, lag] == 0: + continue + y = Y[:, j] + if lag == 0: + x_edge = X[max_lag:, i] + else: + key = (i, lag) + if key not in idx_map: + continue + x_edge = Z_full[:, idx_map[key]] + if not _vol_invariance_test(y, x_edge, env, alpha): + g_out[i, j, lag] = 0 + return g_out + + +# ── Discovery + regime-specific pipelines ──────────────────────────────────── +def _pca_factor_scores(X, k): + """Top-k PCA scores of the VAR(1) innovation covariance (aligned to X[1:]). + A cheap, parameter-free proxy for the MP-detected latent pervasive factor(s): + unlike the lasso-selected controls in `pds_filter`, these scores are an explicit + estimate of the shared confounder itself rather than other (equally confounded) + observed variables.""" + resid = _var1_residuals(X) # (T-1, d), aligned to X[1:] + if k <= 0 or resid.shape[0] < 5: + return None + Xc = resid - resid.mean(axis=0) + try: + _, _, Vt = np.linalg.svd(Xc, full_matrices=False) + except np.linalg.LinAlgError: + return None + k = min(k, Vt.shape[0]) + return Xc @ Vt[:k].T # (T-1, k), row r aligns to X[r + 1] + + +def _drop_instantaneous(g_est): + """Zero out lag-0 (contemporaneous) edges. Under a diagnosed pervasive/hub + latent-factor regime an instantaneous cross-sectional correlation cannot be + assigned a direction without extra assumptions (no time ordering) -- it is the + footprint of a shared same-period confounder, not a lagged direct effect. + `llcca_filter` cannot catch these (its asymmetry statistic uses lag>=1 cross- + correlations only).""" + g_out = g_est.copy() + g_out[:, :, 0] = 0 + return g_out + + +def _kendall_sine_corr(U): + """Tail-robust correlation of the columns of ``U`` via Kendall's tau -> sin(pi/2 * tau) + (the transelliptical/nonparanormal SKEPTIC relation), PSD-repaired by clipping negative + eigenvalues. Rank-based, so robust to heavy-tailed (Student-t / GARCH) innovations. + """ + from scipy.stats import kendalltau + + d = U.shape[1] + corr = np.eye(d) + for i in range(d): + for j in range(i + 1, d): + tau, _ = kendalltau(U[:, i], U[:, j]) + corr[i, j] = corr[j, i] = np.sin( + np.pi / 2.0 * (0.0 if np.isnan(tau) else tau) + ) + w, V = np.linalg.eigh((corr + corr.T) / 2) + corr = (V * np.clip(w, 1e-4, None)) @ V.T + dd = np.sqrt(np.clip(np.diag(corr), 1e-12, None)) + return corr / np.outer(dd, dd) + + +def _rank_corr(U): + """Pairwise Spearman (rank) correlation matrix of the columns of ``U``.""" + from scipy.stats import rankdata + + R = np.apply_along_axis(rankdata, 0, U) + C = np.corrcoef(R, rowvar=False) + return np.nan_to_num(C, nan=0.0) + + +def _double_persistence_score(pc, marg, tau=_GATE_TAU): + """Soft moralization gate: suppress pairs whose full-conditioning score ``pc`` + lacks any marginal (S=empty) corroboration ``marg``. + + ``tau <= 0`` disables the gate (returns ``pc`` unchanged) -- the ablation arm. Without + this guard tau=0 would give 0/0 -> nan for any exactly-zero marginal correlation. + """ + if tau <= 0: + return pc + gate = 1.0 - np.exp(-np.square(marg / tau)) + return pc * gate + + +def _winsorize_cols(U, c): + """Per-column MAD-based winsorization -- clip each column at median +/- c*MAD.""" + med = np.median(U, axis=0, keepdims=True) + mad = np.median(np.abs(U - med), axis=0, keepdims=True) * 1.4826 + mad = np.maximum(mad, 1e-8) + return med + np.clip(U - med, -c * mad, c * mad) + + +def _sl_cov(U, cov_estimator="sample"): + """Covariance of the deconfounded residuals ``U`` for the S-L contemporaneous step. + + ``cov_estimator``: + "sample" : np.cov -- the shipped default; best under Gaussian noise (do-no-harm + to the existing benchmark). + "kendall_shrink" : transelliptical Kendall sine correlation (`_kendall_sine_corr`) plus + identity shrinkage delta=clip(d/n, 0.05, 0.9), in CORRELATION units + (partial correlation is scale-invariant, so no marginal std/MAD + scaling). Validated (2026-08-12 covariance ablation, 20 seeds, + d in {10,24,40}) to STRICTLY DOMINATE "sample" for lag-0 recovery in + the heavy-tailed, moderate-to-high-dimensional regime (t3 noise, + d>=24) -- the financial regime; it pays a modest efficiency cost + under Gaussian noise, so it is OPTIONAL, not the default. The + identity shrinkage is what rescues the unregularized Kendall + estimator's high-d/low-T collapse. + """ + if cov_estimator == "sample": + return np.cov(U, rowvar=False) + if cov_estimator == "kendall_shrink": + n, d = U.shape + R = _kendall_sine_corr(U) + delta = float(np.clip(d / max(n, 1), 0.05, 0.9)) + return (1 - delta) * R + delta * np.eye(d) + if cov_estimator.startswith("winsor_"): + c = float(cov_estimator.split("_", 1)[1]) + Uw = _winsorize_cols(U, c) + return np.cov(Uw, rowvar=False) + raise ValueError( + f"unknown cov_estimator {cov_estimator!r} (use 'sample', 'kendall_shrink', or 'winsor_')" + ) + + +def _sl_deconf_pcorr( + X, + k, + ktrim_margin=0, + cov_estimator=_PC_COV_ESTIMATOR, + gate_tau=_GATE_TAU, + presvd_c=None, + var_order=1, +): + """|partial correlation| of the low-rank-deconfounded contemporaneous precision (S-L). + + Spectral-trim the top-k singular values of the VAR(1) innovations (removes the pervasive-factor + footprint), then take partial correlations of the deconfounded precision. Returns + (pcorr[d,d], U[n,d] innovations, (Uu,s,Vt) SVD, Udec[n,d] deconfounded residuals). + + ``ktrim_margin`` (research, ported from the profile="sl" full-replacement research loop's + idea-020/`_sl_multilag_skeleton`, see SL_VALIDATION.md): an extra integer safety margin + added on top of the nominal k trim count. mp_factor_count's k can under-trim under + heteroskedasticity (a volatility-clustering factor's smaller singular values blend into + the bulk edge more than a homoskedastic factor's would), leaving a stable (non-noise) + residual footprint that no downstream threshold can separate from a genuine edge. + Default 0 preserves the original (pre-research) behavior exactly. + """ + U = _var_residuals( + X, var_order + ) # var_order="auto" -> BIC-selected VAR(p) (higher-lag safe) + U = U - U.mean(0, keepdims=True) + d = U.shape[1] + Usvd_in = _winsorize_cols(U, presvd_c) if presvd_c is not None else U + Uu, s, Vt = np.linalg.svd(Usvd_in, full_matrices=False) + ktrim = min(k + max(int(ktrim_margin), 0), len(s) - 1) + s2 = s.copy() + if 0 < ktrim < len(s): + s2[:ktrim] = s[ktrim] + Udec = (Uu * s2) @ Vt + S = _sl_cov(Udec, cov_estimator=cov_estimator) + P = np.linalg.pinv(S + 1e-3 * np.trace(S) / d * np.eye(d)) + dP = np.sqrt(np.clip(np.diag(P), 1e-12, None)) + pc = np.abs(-P / np.outer(dP, dP)) + np.fill_diagonal(pc, 0.0) + marg = np.abs(_rank_corr(Udec)) + pc = _double_persistence_score(pc, marg, tau=gate_tau) + np.fill_diagonal(pc, 0.0) + return pc, U, (Uu, s, Vt), Udec + + +def _sl_maxt_threshold( + U, + svd, + k, + alpha=0.05, + n_boot=200, + seed=0, + ktrim_margin=0, + cov_estimator=_PC_COV_ESTIMATOR, + gate_tau=_GATE_TAU, + presvd_c=None, +): + """Edge-free-null max-T threshold: keep the factor part of the innovations, circular-shift each + idiosyncratic coordinate independently (destroys contemporaneous idiosyncratic EDGES, preserves + the factor FOOTPRINT + serial structure), rerun the S-L score, and return the (1-alpha) quantile + of the null max. Do-no-harm by construction: P(any edge | edge-free) <= alpha. + + The persistence gate is applied to EVERY null draw, so the quantile is taken over the same + gated statistic `_sl_deconf_pcorr` returns for the observed data -- not over a raw |pcorr|. + Calibrating a gated observed score against an ungated null would compare two different + statistics and misplace the threshold. + + ``ktrim_margin``: MUST match the value passed to `_sl_deconf_pcorr` for the observed + statistic -- both the null and the observed score need the same effective trim amount, + or the threshold is calibrated against a different-bias statistic than the one it's + applied to.""" + Uu, s, Vt = svd + n, d = U.shape + Uc = _winsorize_cols(U, presvd_c) if presvd_c is not None else U + ktrim = min(k + max(int(ktrim_margin), 0), len(s) - 1) + # factor part -- ELEVATED ktrim (matches the multilag original's convention: the + # extra margin components are treated as factor for BOTH the null split and the + # trim-squash step) + Uk = (Uu[:, :ktrim] * s[:ktrim]) @ Vt[:ktrim, :] + Ures = Uc - Uk # idiosyncratic (holds genuine edges) + iu = np.triu_indices(d, 1) + rng = np.random.default_rng(seed) + null_max = np.empty(n_boot) + for b in range(n_boot): + sh = rng.integers(1, n, size=d) + Uperm = np.column_stack([np.roll(Ures[:, j], sh[j]) for j in range(d)]) + Ustar = Uk + Uperm + uu, ss, vt = np.linalg.svd(Ustar, full_matrices=False) + ss2 = ss.copy() + if 0 < ktrim < len(ss): + ss2[:ktrim] = ss[ktrim] + Udec_null = (uu * ss2) @ vt + Sd = _sl_cov(Udec_null, cov_estimator=cov_estimator) + Pn = np.linalg.pinv(Sd + 1e-3 * np.trace(Sd) / d * np.eye(d)) + dPn = np.sqrt(np.clip(np.diag(Pn), 1e-12, None)) + pcn = np.abs(-Pn / np.outer(dPn, dPn)) + np.fill_diagonal(pcn, 0.0) + marg_null = np.abs(_rank_corr(Udec_null)) + pcn = _double_persistence_score(pcn, marg_null, tau=gate_tau) + null_max[b] = pcn[iu].max() + return float(np.quantile(null_max, 1 - alpha)) + + +def _sl_block_bootstrap_column(col, block_len, rng): + """Stationary block bootstrap of a single 1-D array (ported from the profile="sl" + full-replacement research loop's idea-004): concatenate randomly-placed (wrap-around) + contiguous blocks of length ``block_len`` until reaching the original length, then + truncate. See `_sl_multilag_blockboot_threshold`'s docstring for the rationale (fixes + a circular-shift null's burst-alignment inflation under GARCH without stripping real + variance-driven signal).""" + n = len(col) + if block_len >= n: + start = rng.integers(0, n) + return np.roll(col, -start)[:n] + n_blocks = int(np.ceil(n / block_len)) + starts = rng.integers(0, n, size=n_blocks) + out = np.empty(n_blocks * block_len) + for bi, s in enumerate(starts): + idx = (s + np.arange(block_len)) % n + out[bi * block_len : (bi + 1) * block_len] = col[idx] + return out[:n] + + +def _sl_blockboot_threshold( + U, + svd, + k, + alpha=0.05, + n_boot=1000, + seed=0, + ktrim_margin=0, + block_len=20, + cov_estimator=_PC_COV_ESTIMATOR, + gate_tau=_GATE_TAU, + presvd_c=None, +): + """Single-lag-0 analogue of `_sl_maxt_threshold`, using the stationary block bootstrap + (`_sl_block_bootstrap_column`) instead of a single circular shift per idiosyncratic + column. Research (2026-08-10): tested as an alternative to `_sl_maxt_threshold` inside + `sl_lag0_recover`/Workstream A; see SL_VALIDATION.md for the result.""" + Uu, s, Vt = svd + n, d = U.shape + Uc = _winsorize_cols(U, presvd_c) if presvd_c is not None else U + ktrim = min(k + max(int(ktrim_margin), 0), len(s) - 1) + Uk = (Uu[:, :ktrim] * s[:ktrim]) @ Vt[:ktrim, :] + Ures = Uc - Uk + iu = np.triu_indices(d, 1) + rng = np.random.default_rng(seed) + null_max = np.empty(n_boot) + for b in range(n_boot): + Uperm = np.column_stack( + [_sl_block_bootstrap_column(Ures[:, j], block_len, rng) for j in range(d)] + ) + Ustar = Uk + Uperm + uu, ss, vt = np.linalg.svd(Ustar, full_matrices=False) + ss2 = ss.copy() + if 0 < ktrim < len(ss): + ss2[:ktrim] = ss[ktrim] + Udec_null = (uu * ss2) @ vt + Sd = _sl_cov(Udec_null, cov_estimator=cov_estimator) + Pn = np.linalg.pinv(Sd + 1e-3 * np.trace(Sd) / d * np.eye(d)) + dPn = np.sqrt(np.clip(np.diag(Pn), 1e-12, None)) + pcn = np.abs(-Pn / np.outer(dPn, dPn)) + np.fill_diagonal(pcn, 0.0) + marg_null = np.abs(_rank_corr(Udec_null)) + pcn = _double_persistence_score(pcn, marg_null, tau=gate_tau) + null_max[b] = pcn[iu].max() + return float(np.quantile(null_max, 1 - alpha)) + + +def _pairwise_lingam_lr(x, y): + """Hyvarinen-Smith pairwise LiNGAM likelihood ratio. >0 => x->y, <0 => y->x, ~0 => Gaussian + (unorientable). Reliable only under non-Gaussianity.""" + x = (x - x.mean()) / (x.std() + 1e-12) + y = (y - y.mean()) / (y.std() + 1e-12) + rho = np.mean(x * y) + return float(rho * (np.mean(x**3 * y) - np.mean(x * y**3))) + + +# Sample excess kurtosis of a true Gaussian at T=300 has sampling std ~= sqrt(24/300) ~= 0.28; +# t3 innovations run excess kurtosis into the several-to-many range. 1.0 (~3.5 sigma) cleanly +# separates the two regimes without false-triggering on Gaussian sampling noise. +_KURT_GATE = 0.6 + + +def _cross_lag_score(xa, xb): + """Cedar-style lag-1 lead-lag asymmetry (Gaussian-friendly -- uses temporal asymmetry, not + higher moments): a_ab = |corr(xa[:-1], xb[1:])|, a_ba = |corr(xb[:-1], xa[1:])|; >0 favors + a->b, <0 favors b->a. (A confound-robust "partial" variant -- residualizing each lag-1 + predictor on the other series' own lag-1 -- was tried and made this WORSE: on gaussAR_d20 it + collapsed a plain-cross_lag 0.75 pairwise accuracy to 0.50/chance, because when a->b and a is + autocorrelated, b's own lag-1 already carries a's lagged influence via the edge, so + partialling it out strips the very signal the asymmetry needs. Kept plain.)""" + a_ab = abs(float(np.corrcoef(xa[:-1], xb[1:])[0, 1])) + a_ba = abs(float(np.corrcoef(xb[:-1], xa[1:])[0, 1])) + return a_ab - a_ba + + +def _bivar_var1_asymmetry(xa, xb): + """Joint bivariate VAR(1) Granger-direction asymmetry (research-loop idea 008). + + Fits [xa_t, xb_t] ~ const + xa_{t-1} + xb_{t-1} jointly (one 2-variable OLS, not two + separate univariate correlations) and compares the cross-lag coefficient magnitudes: + |xa_{t-1} -> xb_t| vs |xb_{t-1} -> xa_t|. >0 favors a->b. + + Why this beats plain pairwise cross-correlation (`_llcca_asymmetry`) on the COLLIDER + regime specifically: when a target has multiple contemporaneous parents (or any other + lag-1 predictor), a plain corr(xa_{t-1}, xb_t) mixes in xb's own persistence (xb_{t-1} + is correlated with xa_{t-1} through the stationary cross-covariance), diluting the + edge-specific signal. The joint regression's coefficient on xa_{t-1} already controls + for xb_{t-1} (and vice versa) in the SAME fit, so each cross-lag coefficient is the + edge's marginal contribution net of the target's own AR and of the correlated regressor + -- a sharper, jointly-estimated Granger-direction statistic instead of two independent + correlations.""" + n = min(len(xa), len(xb)) + xa = np.asarray(xa)[:n] + xb = np.asarray(xb)[:n] + if n < 8: + return 0.0 + Y = np.column_stack([xa[1:], xb[1:]]) + Z = np.column_stack([np.ones(n - 1), xa[:-1], xb[:-1]]) + try: + beta, *_ = np.linalg.lstsq(Z, Y, rcond=None) + except np.linalg.LinAlgError: + return 0.0 + b_b_from_a = abs(beta[1, 1]) # xa_{t-1} -> xb_t (a leads b) + b_a_from_b = abs(beta[2, 0]) # xb_{t-1} -> xa_t (b leads a) + return (b_b_from_a - b_a_from_b) / (b_b_from_a + b_a_from_b + 1e-8) + + +def _bivar_var1_blockboot_median(xa, xb, n_boot=25, block_len=25, seed=0): + """Block-bootstrap MEDIAN of `_bivar_var1_asymmetry` (research-loop idea 014). + + Motivation: ideas 011a/011b/013 each tried reformulating the SAME single joint-OLS fit + (t-stat/SE normalization, extra lags, rank transform) and all made collider WORSE -- + the single-fit point estimate itself is not mis-specified, it is just noisy on collider + (an unobserved co-parent inflates the target equation's residual variance without biasing + the coefficient). Rather than another single-fit reformulation, resample overlapping + contiguous blocks (preserves the VAR(1) lag structure, unlike i.i.d. resampling) and take + the MEDIAN of the resulting asymmetry statistic across resamples -- a variance-reduction + (bagging) move, not a different estimator of the same one-shot quantity.""" + n = min(len(xa), len(xb)) + xa = np.asarray(xa)[:n] + xb = np.asarray(xb)[:n] + if n < 2 * block_len: + return _bivar_var1_asymmetry(xa, xb) + rng = np.random.default_rng(seed) + n_blocks = int(np.ceil(n / block_len)) + vals = [] + for _ in range(n_boot): + starts = rng.integers(0, n - block_len, size=n_blocks) + idx = np.concatenate([np.arange(s, s + block_len) for s in starts])[:n] + vals.append(_bivar_var1_asymmetry(xa[idx], xb[idx])) + return float(np.median(vals)) + + +# Shipped default. Switched twice before landing here (2026-08-17 to "llcca", 2026-08-18 +# back to "leadlag", now to "llcca" again on 2026-08-19) -- see "WHICH RULE SHIPS" in +# orient_lag0_pair for why each prior measurement was wrong and what the corrected, +# code_version-stamped comparison actually shows. Read that docstring before touching this. +_ORIENT_RULE = "llcca" + + +def orient_lag0_pair(xa, xb, ua, ub, rule=None): + """Orientation score for a contemporaneous (lag-0) edge between two variables. + + ``rule`` selects the statistic (default ``_ORIENT_RULE`` = "llcca"): + "llcca" : pairwise lead-lag correlation asymmetry on the raw series. THE DEFAULT -- + see "WHICH RULE SHIPS" below. + "leadlag" : block-bootstrap-median joint bivariate VAR(1) asymmetry + 0.5 * pairwise + cross-lag score (research-loop ideas 008/014/015). A FORMER default; + kept as an ablation arm, and described at length below because the + per-edge validation history is still the clearest statement of what each + term was meant to buy. + "bivar" : idea-008 alone (one joint VAR(1) fit, no bootstrap, no fusion). ABLATION. + "boot" : idea-014 (blockboot median of "bivar", no fusion). ABLATION. + "lingam" : pairwise LiNGAM likelihood ratio on the deconfounded residuals; needs + non-Gaussianity, at chance without it. ABLATION ARM (Appendix app:orient). + + WHICH RULE SHIPS: "llcca", as of 2026-08-19, confirmed on the shipped pipeline with + every unit's producing commit stamped in the saved record (``s3_utils.code_version``). + Restricted to pervasive_dense -- the ONLY family where S-L recovers any lag-0 + candidates; the other nine tie exactly, verified unit-for-unit identical graphs, because + the orientation function is never called there: + + method leadlag llcca delta (llcca-leadlag) + lucid_nofilt 0.3198 0.3460 +0.0262 + lucid_pcmci 0.3645 0.3910 +0.0265 + lucid_nts 0.2894 0.3180 +0.0286 + + llcca wins on all three base engines, consistently rather than noisily. Overall + (family-weighted across all ten families) moves by only ~+0.003, because the effect is + confined to one family out of ten -- do not read a small Overall delta as "the choice + doesn't matter"; read the pervasive_dense row. + + Why llcca over lingam, given lingam was competitive in per-edge validation: lingam's + identifiability requires non-Gaussian residuals and it is at chance without them, and + 6 of the 8 pervasive_dense generators draw Gaussian innovations (`rng.normal()` in + dense_dgps.py) -- lingam's advantage concentrates on the 2 explicitly non-Gaussian + generators (heavy-tailed contamination, GARCH) and is a liability elsewhere. llcca's + lead-lag mechanism has no such dependency. + + TWO PRIOR DEFAULTS, BOTH WRONG FOR DIFFERENT REASONS -- do not repeat either mistake: + + (1) A 20-seed ablation (`run_orient_lag0only.py`) reported llcca beating "leadlag" by + +0.032 on the lag-0 slice, and the default was switched on it. That ablation called + `routed_deconfound_lucid(df, ml, ci=ParCorrGPU(...))` with NO `base_discovery`, building + its own skeleton instead of the shipped `base_discovery=_reuse_base("naive_cdnots", + cdnots@alpha=0.05)`. A different skeleton yields a different lag-0 candidate set, and the + rules ranked differently on it. Fix: any orientation comparison MUST run the shipped + base engine, or it measures a different method. + + (2) The default was then reverted to "leadlag" using an end-to-end comparison that + DID use the correct base engine, but read a stale `results/` store: the sweep ran in + phases, and pervasive_dense's units for lucid_nofilt/pcmci/nts had not been recomputed + since 2026-08-13 -- before `orient_lag0_pair` existed at all (only plain pairwise-LiNGAM + was available then), before the persistence gate, and before VAR-order selection. The + "leadlag" numbers being compared were therefore a mislabeled historical LiNGAM run, not + the shipped rule. Confirmed by checking out that exact commit and reproducing its + number bit-for-bit. Fix: `code_version` is now stamped into every saved result + specifically so a future version of this mistake fails loudly (a mismatch across a + method's files) instead of silently. + + NOTE an all-lag F1 compresses any orientation difference to ~0.002: 158 of the suite's + 167 lag-0 ground-truth cross edges live in pervasive_dense. Read the dense row. + + >0 => a->b, <0 => b->a, ~0 (|.|<1e-6) => unorientable (kept undirected). + + ``xa, xb``: the RAW observed series (retain AR / temporal structure -- needed for any + lead-lag / cross-lag statistic). ``ua, ub``: the deconfounded VAR(1) innovation residuals + (temporally white by construction -- used for the non-Gaussian pairwise-LiNGAM statistic). + + DEFAULT = joint bivariate VAR(1) Granger-direction asymmetry on the RAW series + (`_bivar_var1_asymmetry`, research-loop idea 008). In a FAITHFUL structural SVAR a + contemporaneous edge a->b makes the target b accumulate a's influence and persist it + through b's own AR, so the lead-lag becomes asymmetric even for GAUSSIAN edges -- which + pairwise LiNGAM cannot orient. The joint-regression form additionally controls each + cross-lag coefficient for the OTHER lag-1 regressor in the same fit, which sharpens the + asymmetry when the target has extra structure (e.g. a second parent) diluting a plain + pairwise correlation -- see `_bivar_var1_asymmetry` docstring. + + VALIDATION (faithful (I-B)^{-1} DGPs, orient_loop_bench.py, VAL seeds 0-19 / FINAL seeds + 40-59): bivariate-VAR1 vs the prior LLCCA-pairwise-correlation default -- + block .89/.85->.84/.85, dense .85/.87->.81/.87, weak .94/.88->.86/.88, hetero .88/.87->. + 88/.87, t3 .89/.81->.84/.81, garch .90/.87->.82/.87, hub .883/.883->.892/.883, COLLIDER + (the prior worst cell) .75/.?? -> .7875 (VAL) / .7958 (FINAL) -- consistent +0.04-0.05 lift + on collider on BOTH disjoint splits, all other cells stay within noise of the prior + default (no regression on any cell in either split). Worst-case 0.7500 -> 0.7875 (VAL). + Mechanism: collider targets have TWO contemporaneous parents, so a plain pairwise + cross-correlation for either parent is diluted by the correlated presence of the other + parent's lag-1 value; the joint bivariate-VAR1 fit controls for the co-regressor within + the same regression, recovering more of the edge-specific signal. `_llcca_asymmetry` + (multi-lag pairwise correlation) and `_pairwise_lingam_lr` (residuals) are kept available + as fallbacks/ablations. ``ua, ub`` retained in the signature for that ablation. + + Idea 014 wraps this in a block-bootstrap MEDIAN (`_bivar_var1_blockboot_median`, n_boot=25, + block_len=25) for variance reduction. VALIDATION (orient_loop_bench.py, VAL seeds 0-19 / + FINAL seeds 40-59): block .84/.85->.90/.89, dense .81/.87->.86/.87, weak .86/.88->.88/.88, + hetero .88/.87->.90/.87, t3 .84/.81->.87/.85, garch .82/.87->.84/.87, hub .8917/.8833->. + 9333/.9167, COLLIDER (worst cell) .7875/.7958->.8417/.8208. Every cell improved or held on + BOTH disjoint splits (no regressions) -- worst-case 0.7875->0.8400 (VAL) / 0.7958->0.8208 + (FINAL). Mechanism: a single-shot joint-VAR1 OLS point estimate on T=250 is noisy + (amplified on collider by the unobserved co-parent's residual variance); the block + bootstrap resamples overlapping length-25 blocks (preserving lag-1 structure, unlike i.i.d. + resampling) and takes the MEDIAN of the resulting asymmetry statistic, denoising the + estimate without changing which fit is used -- a variance-reduction (bagging) move, not a + reformulation of the statistic itself (unlike ideas 011a/011b/013, which all reformulated + the single-fit estimator and failed). + + Idea 015 adds a small ADDITIVE fusion term: `bivar_var1_blockboot_median(xa,xb) + + 0.5*cross_lag_score(xa,xb)`. `_cross_lag_score` (Cedar-style raw pairwise lag-1 lead-lag + asymmetry) is an independent estimate of the same lead-lag direction, computed differently + (two separate raw correlations, no joint co-regressor control) -- summing (not gating) the + two denoises further on cells where either alone is marginal. VALIDATION (orient_loop_bench.py, + VAL seeds 0-19 / FINAL seeds 40-59): block .90/.89, dense .86/.87, weak .88/.88, hetero + .90/.87, t3 .87/.85, garch .84/.87, hub .933/.917, COLLIDER (worst cell) .8417/.8208-> + .8625/.8375. Every cell improved or held on both splits. Worst-case 0.8400->0.8625 (VAL) / + 0.8208->0.8375 (FINAL). Note (research-loop idea 017): this fusion was already confirmed by + idea 015 but got accidentally wiped by the idea-016 revert (both were bundled in one commit, + and reverting idea-016's failed 3rd-term extensions reverted idea-015's win along with it) -- + restored here as a standalone commit. Idea 016 (a 3rd additive term: retuned bootstrap + hyperparameters, +gamma*lingam, bootstrapped cross_lag) tried and failed to beat this on + BOTH splits simultaneously -- do not repeat.""" + rule = _ORIENT_RULE if rule is None else rule + if rule == "lingam": + return _pairwise_lingam_lr(ua, ub) + if rule == "llcca": + # multi-lag pairwise lead-lag asymmetry on the raw series (max_lag=1 window) + X = np.column_stack([np.asarray(xa), np.asarray(xb)]) + return _llcca_asymmetry(X, 0, 1, 1) + if rule == "bivar": + # idea-008 ALONE: one joint VAR(1) fit, no bootstrap, no fusion -- the rung the + # orientation audit recommended headlining (no n_boot/block_len/fuse constants). + return _bivar_var1_asymmetry(xa, xb) + if rule == "boot": + # idea-014: blockboot median of idea-008, still WITHOUT the 0.5*cross_lag fusion. + # Isolates what the fusion term (idea-015) is worth end-to-end. + return _bivar_var1_blockboot_median(xa, xb) + if rule != "leadlag": + raise ValueError(f"unknown orientation rule {rule!r}") + return _bivar_var1_blockboot_median(xa, xb) + 0.5 * _cross_lag_score(xa, xb) + + +def sl_lag0_recover( + df_obs, + g_est, + max_lag, + k=None, + alpha=0.05, + n_boot=200, + seed=0, + orient=True, + ktrim_margin=0, + threshold_rule="maxt", + block_len=20, + cov_estimator=_PC_COV_ESTIMATOR, + gate_tau=_GATE_TAU, + presvd_c=None, + var_order=None, + orient_rule=None, +): + """S-L lag-0 recovery: replace the lag-0 slice of ``g_est`` with the low-rank-deconfounded + precision skeleton, thresholded by the edge-free-null max-T rule (do-no-harm by construction, + self-calibrating via ``alpha`` -- no magic amplitude constant). lag>=1 is returned untouched. + + Orientation: where the deconfounded residuals are non-Gaussian, orient each edge by pairwise + LiNGAM; where ~Gaussian (unorientable), keep it undirected (both directions). Skeleton recovery + is the principled part; lag-0 orientation is identifiability-limited to the non-Gaussian case. + + ``ktrim_margin`` (research, default 0 = original behavior): see `_sl_deconf_pcorr`'s + docstring. Ported from the profile="sl" full-replacement research loop's idea-020. TESTED + (2026-08-10) on this lag-0-only engine and found to HURT monotonically (margin=1/2 + degrade lag-0 F1, margin=2 collapses it) -- the fix is specific to the full multilag + joint-stack construction, does NOT transfer here. Left at default 0; do not change. + ``threshold_rule`` : "maxt" (default, original behavior) or "blockboot" (research, + ported from idea-004: stationary block bootstrap instead of a single circular shift per + idiosyncratic column -- see `_sl_blockboot_threshold`). See SL_VALIDATION.md for results. + """ + X = df_obs.values + # VAR order for residualization = the EFFECTIVE lag the base discovery actually found + # (highest occupied lag slice of g_est), capped by max_lag, floored at 1 -- NOT the nominal + # max_lag. Trusting the discovery avoids over-residualization when the user sets max_lag much + # higher than the true order: a too-large VAR(p) regresses on many useless d x d lag blocks, + # overfits at finite T, and DEGRADES S-L (measured: true-lag-1 data, p=6 -> AUPR 0.93->0.82). + # Residualizing to the effective order removes the real lag 1..p structure and fully recovers + # (true-lag-3 -> 0.91) while staying do-no-harm when the effective order is 1. If discovery + # slightly over-finds a lag, we over-residualize by ~1 (small, gradual cost). var_order="auto" + # (whiteness) / an integer override remain available. + if var_order is None: + occ = [lag for lag in range(1, g_est.shape[2]) if np.any(g_est[:, :, lag])] + vo = min(max(occ) if occ else 1, max(int(max_lag), 1)) + else: + vo = var_order + if k is None: + k = max(mp_factor_count(X, var_order=vo), 1) + pc, U, svd, Udec = _sl_deconf_pcorr( + X, + k, + ktrim_margin=ktrim_margin, + cov_estimator=cov_estimator, + gate_tau=gate_tau, + presvd_c=presvd_c, + var_order=vo, + ) + if threshold_rule == "blockboot": + thr = _sl_blockboot_threshold( + U, + svd, + k, + alpha=alpha, + n_boot=n_boot, + seed=seed, + ktrim_margin=ktrim_margin, + block_len=block_len, + cov_estimator=cov_estimator, + gate_tau=gate_tau, + presvd_c=presvd_c, + ) + elif threshold_rule == "maxt": + thr = _sl_maxt_threshold( + U, + svd, + k, + alpha=alpha, + n_boot=n_boot, + seed=seed, + ktrim_margin=ktrim_margin, + cov_estimator=cov_estimator, + gate_tau=gate_tau, + presvd_c=presvd_c, + ) + else: + raise ValueError( + f"unknown threshold_rule {threshold_rule!r} (use 'maxt' or 'blockboot')" + ) + g_out = g_est.copy() + g_out[:, :, 0] = 0 + for a, b in zip(*np.where(pc > thr)): + if a >= b: + continue # each undirected pair once + if orient: + lr = orient_lag0_pair( + X[:, a], X[:, b], Udec[:, a], Udec[:, b], rule=orient_rule + ) + if abs(lr) < 1e-6: # Gaussian / unorientable -> keep undirected + g_out[a, b, 0] = 1 + g_out[b, a, 0] = 1 + elif lr > 0: + g_out[a, b, 0] = 1 + else: + g_out[b, a, 0] = 1 + else: + g_out[a, b, 0] = 1 + g_out[b, a, 0] = 1 + return g_out + + +def _sl_rel50_threshold(pc0, lag_pc, max_lag, floor=0.12): + """Relative-to-max threshold, generalized per lag block: keep |pcorr| > 0.5*max(|pcorr|) + within that block, with an absolute floor (do-no-harm when the block's max is below the + floor -> no edges). Same rule validated in the lag-0-only research (F16/F18) to beat + max-T on raw F1 at the cost of being empirically (not by-construction) do-no-harm; + applied here per lag block for the same reason `_sl_multilag_maxt_threshold` is + per-block, not global.""" + + def _thr(block_vals): + m = block_vals.max() if block_vals.size else 0.0 + return 0.5 * m if m > floor else float("inf") + + d = pc0.shape[0] + iu = np.triu_indices(d, 1) + thr = {"lag0": _thr(pc0[iu])} + off_diag = ~np.eye(d, dtype=bool) + for ell in range(1, max_lag + 1): + thr[ell] = _thr(lag_pc[ell][off_diag]) + return thr + + +def factor_pds_filter(df_obs, g_est, max_lag, alpha=0.01, k=None): + """Post-double-selection Granger filter that forces the MP-estimated pervasive + factor score(s) into the control set, instead of lasso-selecting other lagged + observed variables (as `pds_filter` does). The lasso-selected controls are + themselves correlated with the target only through the SAME shared latent factor, + so including them is redundant with -- and numerically unstable relative to -- + conditioning on the factor directly; that is the documented cause of PDS over- + pruning true edges on homoskedastic pervasive data. Conditioning on an explicit + factor proxy isolates genuine lagged direct effects without that collinearity.""" + X = df_obs.values + d = X.shape[1] + if k is None: + k = max(mp_factor_count(X), 1) + scores = _pca_factor_scores(X, k) + if scores is None: + return g_est + Z_full, names_full, rows = _lagged_design(X, max_lag) + Y = X[max_lag:] + idx_map = {key: c for c, key in enumerate(names_full)} + start = max_lag - 1 # scores row r aligns to X[r+1]; Y row m aligns to X[max_lag+m] + if start < 0 or start + rows > scores.shape[0]: + return g_est + S = scores[start : start + rows] + g_out = g_est.copy() + for i in range(d): + for j in range(d): + if i == j: + continue + for lag in range(max_lag + 1): + if g_out[i, j, lag] == 0: + continue + y = Y[:, j] + if lag == 0: + x_edge = X[max_lag:, i] + else: + key = (i, lag) + if key not in idx_map: + continue + x_edge = Z_full[:, idx_map[key]] + Xd = np.column_stack([x_edge, S]) + try: + model = sm.OLS(y, sm.add_constant(Xd)).fit( + cov_type="HAC", cov_kwds={"maxlags": _HAC_LAGS} + ) + pval = model.pvalues[1] + except Exception: + continue # abstain (keep) on numerical failure + if not np.isnan(pval) and pval >= alpha: + g_out[i, j, lag] = 0 + return g_out + + +def _discover( + df_obs, + ci, + max_lag, + tetrad, + keep_undirected=False, + alpha=0.05, + tetrad_threshold=_TETRAD_THRESHOLD, +): + """Run the base skeleton engine, optionally followed by the tetrad lag-0 filter. + + The tetrad step is applied *after* discovery rather than inside the engine. That is + bit-identical to the old ``run_cdnots_plus(deconf_threshold=...)`` path -- which + itself filtered the finished graph -- and it keeps discovery independent of the + routed regime, so an already-discovered graph can be reused (see ``run_lucid``'s + ``discovery=`` argument). + """ + if tetrad and keep_undirected: + # `keep_undirected` is only meaningful for the CD-NOTS+ orientation tail, and + # this build's `run_cdnots_plus` does not expose it. Nothing combines the two + # (the lag-0 ablation's keep_all arm runs on pervasive_base="naive", where the + # flag is a no-op), so fail loudly rather than silently ignore the request. + raise NotImplementedError( + "keep_undirected=True is not supported with pervasive_base='tetrad'; " + "use the default pervasive_base='naive'." + ) + fn = run_cdnots_plus if tetrad else run_cdnots + kw = dict( + num_lags=max_lag, include_C=True, c_preset="linear", alpha=alpha, verbose=False + ) + result = fn(df_obs, ci, **kw) + cg = result.cg_tig + if tetrad: + cg = apply_tetrad_lag0_filter( + cg, df_obs, list(df_obs.columns), threshold=tetrad_threshold + ) + d = df_obs.shape[1] + return np.asarray(cg[:d, :d, : max_lag + 1]) + + +def deconfound( + graph, + df_obs, + max_lag, + regime, + alpha_pds_sparse=_ALPHA_PDS_SPARSE, + alpha_pds_pervasive=_ALPHA_PDS_PERVASIVE, + alpha_vol=_ALPHA_VOL, + llcca_threshold=_LLCCA_THRESHOLD, +): + """Post-hoc layer: apply the regime-appropriate edge filters to an already + discovered `graph` (d,d,max_lag+1). Works with any base discovery method.""" + if regime == "sparse": + return pds_filter(df_obs, graph, max_lag, alpha=alpha_pds_sparse) + g = pds_filter(df_obs, graph, max_lag, alpha=alpha_pds_pervasive) + if regime == "sf": + return llcca_filter(df_obs, g, max_lag, threshold=llcca_threshold) + if regime == "pervasive": + return vol_invariance_filter(df_obs, g, max_lag, alpha=alpha_vol) + raise ValueError(f"unknown regime {regime!r}") + + +def routed_deconfound( + df_obs, + max_lag, + ci=None, + router="auto", + thresholds=None, + profile="adaptive", + alpha_factor_pds=_ALPHA_FACTOR_PDS, + arch_alpha=_ARCH_ALPHA, + drop_lag0=True, + keep_undirected=False, + recover_lag0=True, + lag0_engine="sl", + alpha_sl=0.05, + n_boot_sl=200, + orient_lag0=True, + seed_sl=0, + pervasive_base="naive", + pervasive_filters=False, + alpha_ci=0.05, + base_discovery=None, + router_k=_ROUTER_K, + gamma=_TAU_MARGIN, + factor_count_margin=_FACTOR_COUNT_MARGIN, + tetrad_threshold=_TETRAD_THRESHOLD, + return_info=False, + **filter_kw, +): + """Regime-adaptive causal discovery under latent confounders. + + Routes the dataset to a regime (data-only via the MP-null spectral router). By + default (the shipped LUCID configuration) both regimes run the SAME base skeleton + engine (plain CD-NOTS); the regime affects only the correction applied afterward + -- the sparse branch gets a PDS edge filter, the pervasive branch gets S-L lag-0 + recovery. Passing ``pervasive_base="tetrad"`` switches the pervasive branch to a + different base engine (CD-NOTS+ with a tetrad deconfounding filter) instead; this + is a research configuration, not the shipped default. + + profile="adaptive" (DEFAULT): + sparse -> CD-NOTS + PDS-Granger. + pervasive, volatility clustering present (heteroskedastic latent factor): + scale-free sub-regime -> PDS + lead-lag-asymmetry (LLCCA); + pervasive sub-regime -> volatility-invariance. + pervasive, NO volatility clustering (homoskedastic latent factor): + lead-lag-asymmetry + factor-augmented PDS (conditions on the PCA-estimated + factor itself, not on other equally-confounded observed vars); then drop + undirectable lag-0 edges (a shared same-period confounder's footprint). + Gating the volatility-based filters on actual heteroskedasticity keeps the + strong heteroskedastic-factor performance without over-pruning true edges on + homoskedastic factors. Fully parameter-free (conventional 0.05 levels). + + profile="unconditional" applies the full deconfounding stack on every pervasive + route without the heteroskedasticity gate — stronger on heteroskedastic + (volatility-clustering) confounding, weaker on homoskedastic; provided for + users who know their confounding is volatility-clustering, and for ablations. + + Parameters + ---------- + df_obs : DataFrame (T, d) of observed variables only. + max_lag : int. + ci : optional CI test; defaults to ParCorrGPU(df_obs.values). + router : "auto" (default; MP-null sparse boundary + robust sub-regime split), + "spectral" (fixed calibrated thresholds), or "mp" (factor count; ablation). + thresholds : optional (tau, tau2); with router="auto" only tau2 is used (expert). + profile : "adaptive" (default) or "unconditional". + alpha_factor_pds, arch_alpha : factor-PDS significance level and ARCH-gate level. + drop_lag0 : if True (default, shipped behavior), zero out lag-0 edges on the + pervasive route (Section on undirectable contemporaneous confounder + footprint); set False for the T0.2 lag-0-contamination ablation. + keep_undirected : if True, the pervasive-route CDNOTS+ base engine keeps + unresolved (o-o) edges as bidirected instead of dropping them (default + False, matching CDNOTS+'s and PCMCI+'s shipped convention). Only + relevant with `drop_lag0=False`, since `drop_lag0=True` zeroes lag-0 + either way. + recover_lag0 : if True (default, shipped behavior), reconstruct the pervasive + branch's lag-0 slice via S-L (`sl_lag0_recover`) instead of blanket-dropping + it. If False, `drop_lag0` decides whether the lag-0 slice is dropped or kept + as discovered. + pervasive_base : "naive" (default, shipped) uses plain CD-NOTS -- the SAME base + engine as the sparse branch -- so both branches share one discovery method, + differing only in which correction runs downstream. "tetrad" (research + configuration) switches the pervasive branch to CD-NOTS+ with a tetrad + deconfounding filter as its base skeleton engine instead. + return_info : if True, also return the routing/gate diagnostics dict. + **filter_kw : optional alpha/threshold overrides passed to `deconfound`. + + Returns + ------- + graph : ndarray (d, d, max_lag+1) in [cause, effect, lag]. (graph, info) if return_info. + """ + X = df_obs.values + if ci is None: + ci = ParCorrGPU(X) + regime, info = _route( + X, + router=router, + thresholds=thresholds, + router_k=router_k, + gamma=gamma, + factor_count_margin=factor_count_margin, + ) + is_sparse = regime == "sparse" + + use_tetrad = (not is_sparse) and pervasive_base == "tetrad" + graph = ( + base_discovery(df_obs, max_lag) + if base_discovery is not None + else _discover( + df_obs, + ci, + max_lag, + tetrad=use_tetrad, + keep_undirected=keep_undirected, + alpha=alpha_ci, + tetrad_threshold=tetrad_threshold, + ) + ) + + if profile == "unconditional": + out = deconfound(graph, df_obs, max_lag, regime, **filter_kw) + return (out, {**info, "profile": "unconditional"}) if return_info else out + if profile != "adaptive": + raise ValueError( + f"unknown profile {profile!r} (use 'adaptive' or 'unconditional')" + ) + + if is_sparse: + out = pds_filter( + df_obs, + graph, + max_lag, + alpha=filter_kw.get("alpha_pds_sparse", _ALPHA_PDS_SPARSE), + ) + info = {**info, "profile": "adaptive", "pervasive_filters": None} + return (out, info) if return_info else out + + if not pervasive_filters: + # No pervasive lag>=1 filtering (ARCH gate is moot with nothing to gate). The + # benchmark ablation (`run_filter_ablation.py`) showed the vol-invariance / LLCCA / + # factor-PDS filters are net-harmful on the unified naive base -- they over-prune, + # increasingly so at higher lags -- so the shipped pipeline omits them and relies on + # the router + S-L lag-0 recovery. The base skeleton passes through unfiltered. + out = graph + p_arch, applied = None, "none" + else: + p_arch = _vol_clustering_pvalue(X) + clustered = p_arch is not None and p_arch < arch_alpha + if clustered and regime == "sf": + g = pds_filter(df_obs, graph, max_lag, alpha=_ALPHA_PDS_PERVASIVE) + out = llcca_filter(df_obs, g, max_lag, threshold=_LLCCA_THRESHOLD) + applied = "pds_llcca" + elif clustered: # pervasive sub-regime + out = vol_invariance_filter(df_obs, graph, max_lag, alpha=_ALPHA_VOL) + applied = "vol_invariance" + else: # homoskedastic pervasive factor + g = llcca_filter(df_obs, graph, max_lag, threshold=_LLCCA_THRESHOLD) + out = factor_pds_filter(df_obs, g, max_lag, alpha=alpha_factor_pds) + applied = "llcca_factorpds" + if recover_lag0: + # Lag-0 recovery: low-rank-deconfounded precision (S-L) skeleton plus an + # edge-free-null max-T threshold (do-no-harm by construction; generalises to + # dense pervasive confounding). This is the only supported engine. + if lag0_engine != "sl": + raise ValueError( + f"unknown lag0_engine {lag0_engine!r} (only 'sl' is supported)" + ) + out = sl_lag0_recover( + df_obs, + out, + max_lag, + alpha=alpha_sl, + n_boot=n_boot_sl, + seed=seed_sl, + orient=orient_lag0, + ktrim_margin=filter_kw.get("sl_ktrim_margin", 0), + threshold_rule=filter_kw.get("sl_lag0_threshold_rule", "maxt"), + block_len=filter_kw.get("sl_lag0_block_len", 20), + cov_estimator=filter_kw.get("sl_cov_estimator", "sample"), + # persistence (moralization) gate width; default _GATE_TAU + # preserves shipped behaviour exactly. Exposed so the + # constant can be swept (run_constant_sensitivity.py). + gate_tau=filter_kw.get("sl_gate_tau", _GATE_TAU), + # lag-0 orientation statistic; None = shipped _ORIENT_RULE ("llcca") + orient_rule=filter_kw.get("sl_orient_rule"), + ) + elif drop_lag0: + out = _drop_instantaneous(out) + info = { + **info, + "profile": "adaptive", + "vol_clustering_p": p_arch, + "pervasive_filters": applied, + } + return (out, info) if return_info else out + + +def routed_deconfound_naive_sl(df_obs, max_lag, **kw): + """Unified-base-engine variant: pervasive branch uses PLAIN CD-NOTS (no tetrad) -- + the SAME discovery method as the sparse branch -- keeps the shipped ARCH-gate/ + LLCCA/vol-invariance/factor-PDS filter stack for lag>=1, and replaces the blanket + lag-0 drop with the already-validated lag-0-only S-L recovery (`sl_lag0_recover`, + F13/F14: max-T, alpha=0.05, n_boot=200 -- stable at this scope since lag-0-only has + far fewer tested pairs than the failed full-multilag-replacement attempt). + + Research question (2026-08-09, `experiments/confounders/SL_VALIDATION.md`): with a + single shared base engine across both branches, does S-L lag-0 recovery alone (not a + full pervasive-branch replacement) match/beat the shipped CDNOTS+/tetrad pervasive + base engine on the paper's OOD suite? + + Reproduce:: + + python run_ood_benchmark.py \\ + --method causalts.confounders.routed_deconf:routed_deconfound_naive_sl \\ + --worktree /Users/mfesanghary1/workspace/causal-ts-lag0 --seeds 0 1 2 + """ + return routed_deconfound( + df_obs, + max_lag, + profile="adaptive", + pervasive_base="naive", + recover_lag0=True, + lag0_engine="sl", + **kw, + ) + + +def routed_deconfound_lucid(df_obs, max_lag, **kw): + """The shipped LUCID pipeline (2026-08-12): unified plain-CD-NOTS base on both branches, + S-L lag-0 recovery, and NO pervasive lag>=1 filters. The benchmark filter ablation + (`experiments/confounders/run_filter_ablation.py`, 20 seeds + higher-lag check) showed the + vol-invariance/LLCCA/factor-PDS filters are net-harmful on the unified base -- they + over-prune, increasingly at higher lags -- so LUCID omits them (and the ARCH gate they + were gated on). This is `routed_deconfound_naive_sl` with `pervasive_filters=False`. + """ + return routed_deconfound( + df_obs, + max_lag, + profile="adaptive", + pervasive_base="naive", + recover_lag0=True, + lag0_engine="sl", + pervasive_filters=False, + **kw, + ) + + +def _factor_diagnostics(X, factor_count_margin=_FACTOR_COUNT_MARGIN): + """(n_factors, loadings) from the VAR residual spectrum. + + ``loadings`` are the leading right singular vectors of the centred residuals, one + row per detected factor. Descriptive only -- factors are identified up to rotation + (see :class:`~causalts.confounders.result.LucidResult`). + """ + try: + k = mp_factor_count(X, factor_count_margin=factor_count_margin) + if k <= 0: + return 0, None + U = _var_residuals(X, 1) + U = U - U.mean(axis=0, keepdims=True) + _, _, Vt = np.linalg.svd(U, full_matrices=False) + return int(k), np.asarray(Vt[:k]) + except Exception: # diagnostics must never break a discovery run + return None, None + + +def _resolve_discovery(discovery, max_lag, d): + """Turn ``discovery=`` into a ``base_discovery`` callable, validating what we can. + + Accepts a :class:`~causalts.result.CausalResult` (any subclass), a raw + ``(d, d, L+1)`` array, a callable ``(df, max_lag) -> graph``, or ``None``. + + A result's ``cg_tig`` carries extra C-node rows/columns when discovery ran with + ``include_C=True``, so it is sliced to the observed block -- exactly what + ``_discover`` returns. + """ + import warnings as _warnings + + if discovery is None or callable(discovery): + return discovery + + graph = discovery + if hasattr(discovery, "cg_tig"): # a CausalResult subclass + graph = discovery.cg_tig + rec_lags = getattr(discovery, "num_lags", None) + if rec_lags is not None and int(rec_lags) != int(max_lag): + raise ValueError( + f"discovery result was built with num_lags={rec_lags}, but max_lag=" + f"{max_lag} was requested; re-run discovery at the same lag horizon." + ) + rec_alpha = getattr(discovery, "alpha", None) + if rec_alpha is not None and not np.isclose(rec_alpha, 0.05): + _warnings.warn( + f"reusing a discovery result built with alpha={rec_alpha}; LUCID's own " + "skeleton uses alpha=0.05, so results may differ from run_lucid(df, ...)", + stacklevel=3, + ) + if getattr(discovery, "include_C", True) is False: + _warnings.warn( + "reusing a discovery result built with include_C=False; LUCID's own " + "skeleton sets include_C=True, so results may differ from " + "run_lucid(df, ...)", + stacklevel=3, + ) + graph = np.asarray(graph)[:d, :d, : max_lag + 1] + return lambda _df, _ml, _g=graph: _g + + +def run_lucid(df_obs, max_lag, ci=None, discovery=None, **kwargs): + """Run LUCID and return a :class:`~causalts.confounders.result.LucidResult`. + + LUCID infers the latent-confounding regime from the data with a Marchenko-Pastur + spectral router, then applies the deconfounding strategy matched to that regime: + a post-double-selection edge filter on the sparse branch, and spectral (S-L) + recovery of the contemporaneous slice on the pervasive branch. + + Parameters + ---------- + df_obs : DataFrame (T, d) + Observed time series. + max_lag : int + Maximum lag to consider. + ci : CIT_Base, optional + Conditional-independence test for the base skeleton search. Defaults to + :class:`ParCorrGPU` on ``df_obs``. + discovery : CausalResult | ndarray | callable, optional + Skip LUCID's own skeleton search and reuse an existing discovery. Because both + branches run the *same* base engine, reuse is exact -- the routing decision does + not change what is discovered. Pass a result object (``CdnotsResult``, + ``CedarResult``, ``GraceResult``, ...), a raw ``(d, d, max_lag+1)`` array, or a + ``(df, max_lag) -> graph`` callable. + + Reuse assumes the graph was discovered with LUCID-compatible settings. A + ``num_lags`` mismatch raises; differing ``alpha``/``include_C`` warn. ``c_preset`` + is not recorded on result objects and therefore cannot be checked. + **kwargs + Forwarded to :func:`routed_deconfound` (``router``, ``router_k``, ``gamma``, + ``alpha_ci``, ``tetrad_threshold``, ...). + + Returns + ------- + LucidResult + + Examples + -------- + >>> res = run_lucid(df, max_lag=2) # doctest: +SKIP + >>> res.regime, res.n_factors # doctest: +SKIP + ('pervasive', 3) + >>> res.plot() # doctest: +SKIP + """ + import time as _time + + t0 = _time.time() + base_discovery = _resolve_discovery(discovery, max_lag, df_obs.shape[1]) + graph, info = routed_deconfound( + df_obs, + max_lag, + ci=ci, + base_discovery=base_discovery, + return_info=True, + **kwargs, + ) + X = df_obs.values if hasattr(df_obs, "values") else np.asarray(df_obs) + n_factors, loadings = _factor_diagnostics( + X, factor_count_margin=kwargs.get("factor_count_margin", _FACTOR_COUNT_MARGIN) + ) + return LucidResult( + np.asarray(graph), + df_obs, + list(df_obs.columns), + info=info, + n_factors=n_factors, + factor_loadings=loadings, + runtime=_time.time() - t0, + ) diff --git a/causalts/result.py b/causalts/result.py index 799c9f7..6746df9 100644 --- a/causalts/result.py +++ b/causalts/result.py @@ -106,3 +106,72 @@ def distribution_change(self, data_new, target, **kwargs): def summary(self, mechanism_type="linear", top_k=5): return self._bridge().summary(mechanism_type=mechanism_type, top_k=top_k) + + # ------------------------------------------------------------------ + # Latent-confounding corrections + # ------------------------------------------------------------------ + + def _max_lag(self): + return int(self.cg_tig.shape[2] - 1) + + def _obs_slice(self): + """``(graph, d, max_lag)`` restricted to observed variables. + + ``cg_tig`` carries extra C-node rows/columns when discovery ran with + ``include_C=True``; the deconfounding layer works on observed variables only. + """ + d = self._df.shape[1] + max_lag = self._max_lag() + return self.cg_tig[:d, :d, : max_lag + 1], d, max_lag + + def deconfound(self, **kwargs): + """Apply LUCID to this already-discovered graph, returning a ``LucidResult``. + + Reuses this result's skeleton instead of re-running discovery: LUCID runs the + same base engine on both branches, so the routing decision never changes what + would be discovered, making reuse exact. Equivalent to + ``run_lucid(df, max_lag, discovery=self)``. + + Compatibility is checked where the information is recorded -- a ``num_lags`` + mismatch raises, differing ``alpha``/``include_C`` warn. See + :func:`~causalts.confounders.run_lucid`. + """ + from .confounders.routed_deconf import run_lucid + + graph, _d, max_lag = self._obs_slice() + return run_lucid(self._df, max_lag, discovery=self, **kwargs) + + def tetrad_filter(self, threshold=0.25): + """Drop lag-0 edges between variable pairs that share a latent factor. + + A fixed (non-adaptive) deconfounding strategy, in contrast to + :meth:`deconfound`'s regime-adaptive routing. Returns a **new result of the + same type** with the pruned graph, so it chains: + ``res.tetrad_filter().deconfound()``. + """ + from .utils.tetrad import apply_tetrad_lag0_filter + + pruned = apply_tetrad_lag0_filter( + self.cg_tig, self._df, list(self._df.columns), threshold=threshold + ) + return self._with_graph(pruned) + + def pds_filter(self, alpha=1e-10): + """Prune edges that fail a post-double-selection test against observed controls. + + Returns a **new result of the same type**. Note this conditions on *observed* + variables only -- it does not make an unobserved common cause observable. + """ + from .confounders.routed_deconf import pds_filter as _pds + + graph, _d, max_lag = self._obs_slice() + return self._with_graph(_pds(self._df, graph, max_lag, alpha=alpha)) + + def _with_graph(self, graph): + """Shallow copy of this result carrying a different graph.""" + import copy + + new = copy.copy(self) + new.cg_tig = np.asarray(graph) + new._scm_cache = {} + return new diff --git a/causalts/synthetic_data/__init__.py b/causalts/synthetic_data/__init__.py index 3c0b97e..8aaf26e 100644 --- a/causalts/synthetic_data/__init__.py +++ b/causalts/synthetic_data/__init__.py @@ -1,6 +1,7 @@ # Copyright 2025 Bloomberg Finance L.P. # SPDX-License-Identifier: GPL-3.0-or-later +from causalts.synthetic_data.confounding import apply_confounding # noqa: F401 from causalts.synthetic_data.weather_datasets import ( # noqa: F401 STATION_META, STATIONS, diff --git a/causalts/synthetic_data/confounding.py b/causalts/synthetic_data/confounding.py new file mode 100644 index 0000000..e32a005 --- /dev/null +++ b/causalts/synthetic_data/confounding.py @@ -0,0 +1,205 @@ +# Copyright 2025 Bloomberg Finance L.P. +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Post-processing utility to introduce latent confounders into SCP samples. + +Usage:: + + from causalts.synthetic_data.synthetic_datasets import erdos_renyi + from causalts.synthetic_data.confounding import apply_confounding + + sample = erdos_renyi(seed=42, n_vars=15, T=500) + confounded = apply_confounding(sample, confound_fraction=0.3, seed=42) + + confounded["df"] # observed variables only + confounded["ground_truth"] # direct GT (confounder edges removed) + confounded["ground_truth_ancestral"] # ancestral GT (transitive closure) + confounded["confounder_nodes"] # list of removed node names + confounded["confounded_pairs"] # [(Xi, Xj), ...] sharing a latent cause +""" + +from __future__ import annotations + +import math +from copy import deepcopy + +import numpy as np + + +def _find_eligible_confounders(gt: np.ndarray) -> list[int]: + """Return node indices that cause >=2 distinct other variables (fork).""" + d = gt.shape[0] + eligible = [] + for i in range(d): + children = set() + for j in range(d): + if i == j: + continue + if gt[i, j, :].any(): + children.add(j) + if len(children) >= 2: + eligible.append(i) + return eligible + + +def _transitive_closure_for_removed( + gt_full: np.ndarray, + removed: set[int], + observed: list[int], +) -> np.ndarray: + """Build ancestral ground truth: connect L's parents to L's children. + + For each removed node L, for each parent P of L and each child C of L + (both in observed set), add edge P -> C at the sum of lags (capped at + max_lag). + """ + d_obs = len(observed) + max_lag = gt_full.shape[2] - 1 + obs_set = set(observed) + obs_idx = {node: i for i, node in enumerate(observed)} + + gt_anc = np.zeros((d_obs, d_obs, max_lag + 1), dtype=np.int8) + + # Copy direct edges between observed nodes + for i, ni in enumerate(observed): + for j, nj in enumerate(observed): + gt_anc[i, j, :] = gt_full[ni, nj, :] + + # Add transitive edges through removed nodes + for L in removed: + parents_lags = [] + children_lags = [] + for p in range(gt_full.shape[0]): + if p == L: + continue + for lag in range(max_lag + 1): + if gt_full[p, L, lag]: + parents_lags.append((p, lag)) + for c in range(gt_full.shape[0]): + if c == L: + continue + for lag in range(max_lag + 1): + if gt_full[L, c, lag]: + children_lags.append((c, lag)) + + for p, lag_p in parents_lags: + if p not in obs_set: + continue + for c, lag_c in children_lags: + if c not in obs_set: + continue + combined_lag = min(lag_p + lag_c, max_lag) + gt_anc[obs_idx[p], obs_idx[c], combined_lag] = 1 + + return gt_anc + + +def apply_confounding( + sample: dict, + confound_fraction: float = 0.3, + seed: int | None = None, +) -> dict: + """Remove a fraction of eligible nodes as latent confounders. + + Parameters + ---------- + sample : dict + Output of ``SCPGraphGenerator.sample()`` or ``topology_scp()`` etc. + Must contain keys: ``df``, ``ground_truth``, ``var_names``, ``max_lag``. + confound_fraction : float + Fraction of eligible nodes (those with >=2 distinct children) to + remove as latent confounders. 0.0 = no confounders, 1.0 = remove + all eligible nodes. + seed : int or None + Random seed for confounder selection. + + Returns + ------- + dict + Modified sample with keys: + + - ``df`` — observed variables only (confounder columns dropped) + - ``ground_truth`` — 3D array, observed nodes only, confounder edges removed + - ``ground_truth_ancestral`` — same but with transitive closure through + removed nodes (L's parents connected to L's children) + - ``ground_truth_full`` — original full graph (for reference) + - ``confounder_nodes`` — list of removed node names + - ``confounder_indices`` — list of removed node indices + - ``confounded_pairs`` — list of (name_i, name_j) pairs that share + a latent common cause + - ``observed_nodes`` — list of observed node names + - ``n_confounders`` — number of removed nodes + - ``n_eligible`` — number of nodes eligible to be confounders + - All other keys from the original sample are preserved. + """ + gt_full = sample["ground_truth"].copy() + var_names = list(sample["var_names"]) + d = len(var_names) + max_lag = sample["max_lag"] + + eligible = _find_eligible_confounders(gt_full) + n_eligible = len(eligible) + + if n_eligible == 0: + out = deepcopy(sample) + out["ground_truth_full"] = gt_full + out["ground_truth_ancestral"] = gt_full.copy() + out["confounder_nodes"] = [] + out["confounder_indices"] = [] + out["confounded_pairs"] = [] + out["observed_nodes"] = var_names + out["n_confounders"] = 0 + out["n_eligible"] = 0 + return out + + n_confounders = max(1, math.ceil(confound_fraction * n_eligible)) + n_confounders = min(n_confounders, n_eligible) + + rng = np.random.default_rng(seed) + confounder_indices = sorted(rng.choice(eligible, size=n_confounders, replace=False)) + removed = set(confounder_indices) + observed = [i for i in range(d) if i not in removed] + + # Confounded pairs: observed children that share a removed parent + confounded_pairs = [] + for L in confounder_indices: + children_obs = [] + for c in range(d): + if c == L or c in removed: + continue + if gt_full[L, c, :].any(): + children_obs.append(c) + for ci in range(len(children_obs)): + for cj in range(ci + 1, len(children_obs)): + pair = (var_names[children_obs[ci]], var_names[children_obs[cj]]) + confounded_pairs.append(pair) + + # Build observed ground truth (direct) + d_obs = len(observed) + gt_obs = np.zeros((d_obs, d_obs, max_lag + 1), dtype=np.int8) + for i, ni in enumerate(observed): + for j, nj in enumerate(observed): + gt_obs[i, j, :] = gt_full[ni, nj, :] + + # Build ancestral ground truth (transitive closure through removed) + gt_anc = _transitive_closure_for_removed(gt_full, removed, observed) + + # Build observed DataFrame + obs_names = [var_names[i] for i in observed] + df_obs = sample["df"][obs_names].copy() + + out = deepcopy(sample) + out["df"] = df_obs + out["ground_truth"] = gt_obs + out["ground_truth_ancestral"] = gt_anc + out["ground_truth_full"] = gt_full + out["var_names"] = obs_names + out["confounder_nodes"] = [var_names[i] for i in confounder_indices] + out["confounder_indices"] = list(confounder_indices) + out["confounded_pairs"] = confounded_pairs + out["observed_nodes"] = obs_names + out["n_confounders"] = n_confounders + out["n_eligible"] = n_eligible + out["name"] = sample.get("name", "") + f" (confounded, {n_confounders} latent)" + + return out diff --git a/causalts/utils/tetrad.py b/causalts/utils/tetrad.py new file mode 100644 index 0000000..91f5620 --- /dev/null +++ b/causalts/utils/tetrad.py @@ -0,0 +1,365 @@ +# Copyright 2025 Bloomberg Finance L.P. +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Tetrad-based latent confounder detection. + +Tetrad constraints (Spearman 1928) exploit the fact that if four observed +variables share a single latent common cause in a linear model, certain +products of covariances satisfy algebraic constraints:: + + sigma_ij * sigma_kl - sigma_ik * sigma_jl = 0 + +This module provides: + +- :func:`wishart_tetrad_test` — test whether a single tetrad vanishes +- :func:`detect_confounded_clusters` — scan all quartets for vanishing + tetrads and return flagged clusters +- :func:`check_tetrads` — post-processing diagnostic after causal + discovery: test tetrads on VAR residuals to flag confounded edges + +Limitations +----------- +- Assumes **linear** relationships. For nonlinear confounding, tetrad + constraints have low power or produce incorrect results. +- Assumes approximate Gaussianity for the Wishart test. +- For time series, pre-whiten with a VAR model first (the + :func:`check_tetrads` function does this automatically). +""" + +from __future__ import annotations + +from itertools import combinations + +import numpy as np +from scipy import stats + + +def wishart_tetrad_test( + S: np.ndarray, + n: int, + i: int, + j: int, + k: int, + l: int, +) -> tuple[float, float, float]: + r"""Test whether tetrad :math:`\sigma_{ij}\sigma_{kl} - \sigma_{ik}\sigma_{jl} = 0`. + + Uses the delta method for the asymptotic standard error under + Gaussianity (Wishart 1928, Bollen & Ting 1993). + + Parameters + ---------- + S : ndarray (d, d) + Sample covariance matrix. + n : int + Sample size. + i, j, k, l : int + Variable indices. + + Returns + ------- + tuple + ``(tetrad_value, z_statistic, p_value)`` + """ + t = S[i, j] * S[k, l] - S[i, k] * S[j, l] + + grad = np.array([S[k, l], S[i, j], -S[j, l], -S[i, k]]) + indices = [(i, j), (k, l), (i, k), (j, l)] + + V = np.zeros((4, 4)) + for a in range(4): + for b in range(4): + p, q = indices[a] + r, s = indices[b] + V[a, b] = (1.0 / n) * (S[p, r] * S[q, s] + S[p, s] * S[q, r]) + + var_t = grad @ V @ grad + se_t = np.sqrt(max(var_t, 1e-15)) + z = t / se_t + pvalue = 2 * stats.norm.sf(abs(z)) + return float(t), float(z), float(pvalue) + + +def detect_confounded_clusters( + data: np.ndarray, + alpha: float = 0.01, + var_names: list[str] | None = None, +) -> dict: + """Scan all variable quartets for vanishing tetrad constraints. + + A quartet with all 3 tetrads vanishing (p > alpha) suggests the + four variables may share a single latent common cause. + + Parameters + ---------- + data : ndarray (T, d) + Observed data matrix (ideally pre-whitened residuals). + alpha : float + Significance level. A tetrad "vanishes" when p > alpha + (i.e., we fail to reject the null that it equals zero). + var_names : list of str, optional + Variable names. Defaults to ``['X0', 'X1', ...]``. + + Returns + ------- + dict + Keys: + + - ``vanishing_quartets`` — list of (i, j, k, l) index tuples + - ``vanishing_names`` — same but with variable names + - ``flagged_pairs`` — set of (name_i, name_j) pairs that appear + in at least one vanishing quartet (potential confounded pairs) + - ``n_tested`` — total quartets tested + - ``n_vanishing`` — number of vanishing quartets + """ + n, d = data.shape + if var_names is None: + var_names = [f"X{i}" for i in range(d)] + + S = np.cov(data.T) + + vanishing_quartets = [] + vanishing_names = [] + + for quartet in combinations(range(d), 4): + q0, q1, q2, q3 = quartet + _, _, p1 = wishart_tetrad_test(S, n, q0, q1, q2, q3) + _, _, p2 = wishart_tetrad_test(S, n, q0, q2, q1, q3) + _, _, p3 = wishart_tetrad_test(S, n, q0, q3, q1, q2) + + if all(p > alpha for p in [p1, p2, p3]): + vanishing_quartets.append(quartet) + vanishing_names.append(tuple(var_names[x] for x in quartet)) + + flagged_pairs = set() + for q in vanishing_names: + for a in range(len(q)): + for b in range(a + 1, len(q)): + flagged_pairs.add((q[a], q[b])) + + n_tested = len(list(combinations(range(d), 4))) + + return { + "vanishing_quartets": vanishing_quartets, + "vanishing_names": vanishing_names, + "flagged_pairs": flagged_pairs, + "n_tested": n_tested, + "n_vanishing": len(vanishing_quartets), + } + + +def factor_consistency_score( + C: np.ndarray, + i: int, + j: int, + min_corr: float = 0.05, +) -> float: + r"""Measure whether variables i and j share the same latent factor. + + For a single-factor model :math:`X_i = \lambda_i F + \epsilon_i`, + the ratio :math:`\text{corr}(X_i, X_k) / \text{corr}(X_j, X_k)` + equals :math:`\lambda_i / \lambda_j` — constant for all reference + variables k. A low coefficient of variation (CV) of these ratios + indicates i and j share the same factor structure. + + Parameters + ---------- + C : ndarray (d, d) + Correlation matrix. + i, j : int + Variable indices to test. + min_corr : float + Skip reference variables with ``|corr(j, k)| < min_corr`` + to avoid division instability. + + Returns + ------- + float + Coefficient of variation of the correlation ratios. + Lower = more likely shared factor. Returns ``inf`` if + fewer than 3 valid reference variables. + """ + d = C.shape[0] + ratios = [] + for k in range(d): + if k == i or k == j: + continue + if abs(C[j, k]) < min_corr: + continue + ratios.append(C[i, k] / C[j, k]) + if len(ratios) < 3: + return float("inf") + return float(np.std(ratios) / (abs(np.mean(ratios)) + 1e-8)) + + +def detect_shared_factors( + data: np.ndarray, + threshold: float = 0.25, + var_names: list[str] | None = None, +) -> dict: + """Detect pairs of variables that likely share a latent common cause. + + Uses the factor consistency score: for each pair (i, j), checks + whether ``corr(i, k) / corr(j, k)`` is approximately constant + across all reference variables k. Constant ratios indicate a + shared latent factor (the "ratio test"). + + More discriminative than vanishing tetrads, which suffer from + near-zero covariance artifacts. Calibrated on S&P 500 daily + returns: F1 = 0.94 for sector detection at default threshold. + + Parameters + ---------- + data : ndarray (T, d) + Observed data matrix. + threshold : float + Pairs with factor consistency score below this threshold are + flagged as sharing a latent factor. Default 0.25 (calibrated + on financial data). + var_names : list of str, optional + Variable names. + + Returns + ------- + dict + Keys: + + - ``flagged_pairs`` — list of (name_i, name_j, score) tuples + - ``score_matrix`` — (d, d) pairwise consistency scores + - ``n_flagged`` — number of flagged pairs + """ + n, d = data.shape + if var_names is None: + var_names = [f"X{i}" for i in range(d)] + + C = np.corrcoef(data.T) + scores = np.full((d, d), np.inf) + + for i in range(d): + for j in range(i + 1, d): + s = factor_consistency_score(C, i, j) + scores[i, j] = scores[j, i] = s + + flagged = [] + for i in range(d): + for j in range(i + 1, d): + if scores[i, j] < threshold: + flagged.append((var_names[i], var_names[j], float(scores[i, j]))) + + flagged.sort(key=lambda x: x[2]) + + return { + "flagged_pairs": flagged, + "score_matrix": scores, + "n_flagged": len(flagged), + } + + +def check_tetrads( + data: np.ndarray, + graph: np.ndarray | None = None, + alpha: float = 0.01, + var_names: list[str] | None = None, + prewhiten: bool = True, +) -> dict: + """Post-processing confounder diagnostic for causal discovery results. + + Fits a VAR model to remove temporal dependencies, then tests tetrad + constraints on the residuals. Vanishing tetrads suggest latent + common causes not captured by the discovered graph. + + Parameters + ---------- + data : ndarray (T, d) + Observed time series data. + graph : ndarray (d, d, max_lag+1) or None + Discovered causal graph (used to inform VAR order if provided). + If None, uses a default VAR(1) for pre-whitening. + alpha : float + Significance level for tetrad tests. + var_names : list of str, optional + Variable names. + prewhiten : bool + If True, fit a VAR model and test tetrads on residuals. + If False, test on raw data (not recommended for time series). + + Returns + ------- + dict + Same as :func:`detect_confounded_clusters`, plus: + + - ``prewhitened`` — whether pre-whitening was applied + - ``var_order`` — VAR order used for pre-whitening + """ + n, d = data.shape + if var_names is None: + var_names = [f"X{i}" for i in range(d)] + + var_order = 1 + if graph is not None: + var_order = max(1, graph.shape[2] - 1) + + if prewhiten: + from statsmodels.tsa.api import VAR + + model = VAR(data) + try: + fitted = model.fit(maxlags=var_order, ic=None, trend="c") + residuals = fitted.resid + except Exception: + residuals = data + else: + residuals = data + + result = detect_confounded_clusters(residuals, alpha=alpha, var_names=var_names) + result["prewhitened"] = prewhiten + result["var_order"] = var_order + + return result + + +def apply_tetrad_lag0_filter(graph, df, var_names, threshold=0.25): + """Remove lag-0 edges between variable pairs that share a latent factor. + + Uses :func:`detect_shared_factors` (tetrad ratio test) to identify pairs whose + contemporaneous correlation is likely driven by a common latent cause, then zeros + the lag-0 entries for those pairs. Lag-1+ edges are untouched -- they carry the + true causal signal. + + This is a *post-hoc* filter: it consumes an already-discovered graph and never + re-runs discovery, so it composes with any engine. It is the shared implementation + behind :func:`causalts.confounders.tetrad_filter`. + + Parameters + ---------- + graph : ndarray (d_full, d_full, num_lags+1) + Discovered graph in tigramite layout. May include extra C-node columns; only + the leading ``len(var_names)`` rows/columns are touched. + df : DataFrame (T, d) + Observed data (no C columns), aligned with ``var_names``. + var_names : list of str + Variable names matching ``df`` columns. + threshold : float, default 0.25 + Factor-consistency score threshold for :func:`detect_shared_factors`. + Lower is more conservative (fewer pairs flagged); useful range 0.15--0.35. + + Returns + ------- + ndarray + Copy of ``graph`` with confounded lag-0 edges removed. + """ + sf = detect_shared_factors( + df.values.astype(float), threshold=threshold, var_names=list(var_names) + ) + flagged = {(a, b) for a, b, _ in sf["flagged_pairs"]} + flagged |= {(b, a) for a, b, _ in sf["flagged_pairs"]} + + g = graph.copy() + for i, vi in enumerate(var_names): + for j, vj in enumerate(var_names): + if i == j: + continue + if (vi, vj) in flagged: + g[i, j, 0] = 0 + g[j, i, 0] = 0 + return g diff --git a/docs/_static/img/thumbnails/latent_confounder_detection_thumb.png b/docs/_static/img/thumbnails/latent_confounder_detection_thumb.png new file mode 100644 index 0000000..1432987 Binary files /dev/null and b/docs/_static/img/thumbnails/latent_confounder_detection_thumb.png differ diff --git a/docs/_static/img/thumbnails/overrides/latent_confounder_detection_thumb.png b/docs/_static/img/thumbnails/overrides/latent_confounder_detection_thumb.png new file mode 100644 index 0000000..e05bbaf Binary files /dev/null and b/docs/_static/img/thumbnails/overrides/latent_confounder_detection_thumb.png differ diff --git a/docs/api/confounders.md b/docs/api/confounders.md new file mode 100644 index 0000000..09207db --- /dev/null +++ b/docs/api/confounders.md @@ -0,0 +1,112 @@ +# Confounders API + +**LUCID** — regime-adaptive deconfounding for causal discovery under latent +confounders. Latent confounding does not have a single statistical signature, and +applying the wrong correction can be as damaging as applying none: a low-rank +correction on sparsely confounded data deletes real edges, while conditioning on +observed controls cannot block a factor that loads on everything. + +LUCID infers the regime from the residual spectrum — against the Marchenko–Pastur +no-factor null — and applies the matching correction. Both branches run the *same* +base discovery engine, so the regime affects only the correction that follows. + +```python +from causalts.confounders import ( + run_lucid, # main entry point -> LucidResult + LucidResult, + routed_deconfound, # low-level, returns a bare array + deconfound, # post-hoc filter layer + pds_filter, + tetrad_filter, + spectral_gap, + mp_factor_count, +) +``` + +See the [Unobserved Confounders (LUCID)](../examples/latent_confounder_detection) +tutorial for a worked example. + +--- + +## Running LUCID + +```{eval-rst} +.. autofunction:: causalts.confounders.routed_deconf.run_lucid +``` + +### `LucidResult` + +```{eval-rst} +.. autoclass:: causalts.confounders.result.LucidResult + :members: top_factor_variables +``` + +--- + +## From an existing discovery + +Every result object exposes the deconfounding layer, so LUCID can be applied to a +graph you already discovered — with any engine — without re-running the skeleton +search: + +```python +res = run_cdnots(df, ci, num_lags=2) +res.deconfound() # -> LucidResult +res.tetrad_filter() # -> same result type, pruned +res.tetrad_filter().deconfound() # filters chain +``` + +See {meth}`causalts.result.CausalResult.deconfound`. + +--- + +## Low-level entry point + +```{eval-rst} +.. autofunction:: causalts.confounders.routed_deconf.routed_deconfound +``` + +## Post-hoc deconfounding layer + +Applies the regime-appropriate edge filter to an already-discovered graph. + +```{eval-rst} +.. autofunction:: causalts.confounders.routed_deconf.deconfound +``` + +--- + +## Filters + +### `pds_filter` + +Post-double-selection filter over observed controls — the sparse-regime default. +Granger-style at lags ≥ 1; at lag 0 the corresponding contemporaneous partial +regression with lagged controls. Conditions on *observed* variables only, so it does +not make an unobserved common cause observable. + +```{eval-rst} +.. autofunction:: causalts.confounders.routed_deconf.pds_filter +``` + +### `tetrad_filter` + +Fixed (non-adaptive) deconfounder: always assumes pervasive factor structure and drops +lag-0 edges consistent with a shared latent cause, via the tetrad vanishing condition. +The natural comparator for LUCID's routing. + +```{eval-rst} +.. autofunction:: causalts.confounders.routed_deconf.tetrad_filter + +.. autofunction:: causalts.utils.tetrad.apply_tetrad_lag0_filter +``` + +--- + +## Router diagnostics + +```{eval-rst} +.. autofunction:: causalts.confounders.routed_deconf.spectral_gap + +.. autofunction:: causalts.confounders.routed_deconf.mp_factor_count +``` diff --git a/docs/api/index.md b/docs/api/index.md index 8f25fb0..9b551df 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -33,6 +33,14 @@ Full API documentation auto-generated from source docstrings (NumPy format). {bdg-warning-line}`requires dowhy` ::: +:::{grid-item-card} {fas}`shield-halved;1.2em;sd-text-primary`   Confounders (LUCID) +:link: confounders +:link-type: doc +:shadow: md + +`run_lucid` · `LucidResult` · `deconfound` · `tetrad_filter` · `pds_filter` · `routed_deconfound` +::: + :::{grid-item-card} {fas}`database;1.2em;sd-text-primary`   Data Generation :link: data :link-type: doc @@ -69,6 +77,7 @@ Full API documentation auto-generated from source docstrings (NumPy format). discovery ../ci_tests effects +confounders data plotting autoapi/causalts/index diff --git a/docs/examples/index.md b/docs/examples/index.md index 111d80a..f74238c 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -129,6 +129,11 @@ Notebooks reproducing experiments from the GRACE paper (preprint forthcoming) wi Causal Feature Selection + + Unobserved Confounders and LUCID + Unobserved Confounders (LUCID) + + --- @@ -270,6 +275,7 @@ multi_c_nonstationarity regime_discovery background_knowledge feature_selection +latent_confounder_detection ``` ```{toctree} diff --git a/docs/examples/latent_confounder_detection.ipynb b/docs/examples/latent_confounder_detection.ipynb new file mode 120000 index 0000000..f90a4a0 --- /dev/null +++ b/docs/examples/latent_confounder_detection.ipynb @@ -0,0 +1 @@ +../../examples/latent_confounder_detection.ipynb \ No newline at end of file diff --git a/examples/latent_confounder_detection.ipynb b/examples/latent_confounder_detection.ipynb new file mode 100644 index 0000000..b08bc8f --- /dev/null +++ b/examples/latent_confounder_detection.ipynb @@ -0,0 +1,1149 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "lucid-00", + "metadata": {}, + "source": [ + "# Unobserved Confounders: Diagnosis and Repair\n", + "\n", + "Ice cream sales and shark attacks rise and fall together. Ban ice cream and nobody is\n", + "safer — **summer heat drives both**. It is the textbook confounder precisely because the\n", + "absurdity is obvious. It stops being obvious when the hidden driver is a market factor behind two credit\n", + "spreads, or a policy shift behind two economic series. And it stops being harmless the\n", + "moment you hand the data to an algorithm, because the algorithm cannot see the\n", + "absurdity — it sees only that two series move together, and it draws the arrow.\n", + "\n", + "The [Beginner's Guide](beginers_guide.ipynb) lists three assumptions that observational\n", + "discovery rests on. The third is **causal sufficiency**: no hidden common causes among\n", + "your observed variables. It is the assumption most likely to be false in practice,\n", + "because it is a claim about the columns you **did not** collect. This notebook is about what happens when it breaks:\n", + "\n", + "1. Watch a discovery algorithm invent the ice-cream-causes-sharks edge, and measure what\n", + " believing it would cost.\n", + "2. See why there is no one-line patch.\n", + "3. Diagnose *which kind* of confounding a dataset has.\n", + "4. Repair it with **LUCID**, the deconfounding layer in this library." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "lucid-01", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:15.595120Z", + "iopub.status.busy": "2026-08-26T21:48:15.595055Z", + "iopub.status.idle": "2026-08-26T21:48:23.126918Z", + "shell.execute_reply": "2026-08-26T21:48:23.126504Z" + } + }, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "from causalts.cdnots.phase3_utils import run_cdnots\n", + "from causalts.ci_tests import ParCorrGPU\n", + "from causalts.confounders import run_lucid, tetrad_filter\n", + "from causalts.plotting import plot_graph\n", + "from causalts.utils.helpers import evaluate_graph\n", + "\n", + "# Colourblind-safe categorical order (Okabe-Ito), fixed - never cycled.\n", + "C_BASE, C_FIXED, C_LUCID = \"#0072B2\", \"#E69F00\", \"#009E73\"\n", + "\n", + "# Lay graphs out with graphviz's sfdp, scaling nodes apart rather than invoking its\n", + "# overlap-removal pass (which needs a triangulation library many builds omit).\n", + "GV = {\"args\": \"-Goverlap=scale -Gsplines=true\"}" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-02", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part 1 — Watching the Assumption Break\n", + "\n", + "Let us build the summer exactly as the story tells it. Heat drives ice cream sales\n", + "quickly — people buy on a hot day. It drives shark encounters more slowly: the heat has\n", + "to fill the beaches first, and only then do swimmers meet sharks.\n", + "\n", + "So `Heat` reaches `IceCream` after one step and `Attacks` after two. **There is no arrow\n", + "from ice cream to sharks.** Because every link is lagged, time fixes all the directions." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "lucid-03", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:23.129000Z", + "iopub.status.busy": "2026-08-26T21:48:23.128808Z", + "iopub.status.idle": "2026-08-26T21:48:23.136516Z", + "shell.execute_reply": "2026-08-26T21:48:23.136066Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "corr(IceCream(t-1), Attacks(t)) = +0.90 <- looks exactly like ice cream causing shark attacks\n" + ] + } + ], + "source": [ + "rng = np.random.default_rng(7)\n", + "T = 1500\n", + "\n", + "heat = rng.standard_normal(T) # the hidden driver\n", + "icecream = np.zeros(T)\n", + "attacks = np.zeros(T)\n", + "for t in range(2, T):\n", + " icecream[t] = 1.3 * heat[t - 1] + 0.4 * rng.normal() # Heat(t-1) -> IceCream(t)\n", + " attacks[t] = 1.2 * heat[t - 2] + 0.4 * rng.normal() # Heat(t-2) -> Attacks(t)\n", + "\n", + "summer = pd.DataFrame({\"Heat\": heat, \"IceCream\": icecream, \"Attacks\": attacks})\n", + "print(f\"corr(IceCream(t-1), Attacks(t)) = \"\n", + " f\"{np.corrcoef(icecream[1:-1], attacks[2:])[0, 1]:+.2f}\"\n", + " \" <- looks exactly like ice cream causing shark attacks\")" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-04", + "metadata": {}, + "source": [ + "Now run discovery twice — once with `Heat` in the data, once without. The *summer* is\n", + "identical; only the **column we collected** differs." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "lucid-05", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:23.137760Z", + "iopub.status.busy": "2026-08-26T21:48:23.137686Z", + "iopub.status.idle": "2026-08-26T21:48:23.275605Z", + "shell.execute_reply": "2026-08-26T21:48:23.275054Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Heat collected : ['Heat(t-1) -> IceCream(t)', 'Heat(t-2) -> Attacks(t)']\n", + "Heat hidden : ['IceCream(t-1) -> Attacks(t)']\n" + ] + }, + { + "data": { + "image/png": "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07Fjx1Dx4sVDf/zxh+ec6N/p3zv0nuHvffnll4fq1q2b9Xj69On2Oa+//rrn/T755BPP8c2bN9vXP+mkk0K7du3KMW3t2rXzvL7j1VdfDRUuXNi+n9vzzz9v32fmzJn28c8//2wfX3fddZ7ndevWzR6/7777QrGi7z2n19R50t/uvPPObH/T59Pfw51++un2x+9nzonS5b4+Ze3atfa7P+ecc0IHDhzIOv7000/b57700kue9OiY3i/SZ9B3JcOHDw8VKlQo9OCDD+aaHve/1evqGnHTvy9Tpkxo8eLFnuM6h0WKFAktX77cPv7iiy/sv7/xxhuzvbZzLTnXQd++fT1/v/XWW+1xvYZs2bIlVKJEidAtt9zied5jjz1mP5NzHy1dutSm4aGHHvI8b86cOaGiRYt6jud03oJ4DSMz5FR2OmVcqVKlQitXrsw6/s0339jjAwYMyPZayjsaNWqU53s690FuP0OGDPF9//sp7/V6kfLaSJzyTp9tz549WceVv+m47vWcysEPPvjAPkf5hmP//v2h0047zR5/+eWXs443b948VKNGDVsmOj799FP7vPyUre48ddq0aZ68PlLeFmvh9QR9btV3lKZNmzblWNafeeaZoSZNmoR2797t+XurVq1sPSo30ZQBoucoz503b57nOVdeeaX9DtavX+853rVr11CFChWyri99Fve1IPpMhx56aKhPnz7Z7qOqVat6vte77rrLHm/WrFlo3759Wccvu+wyWw67P3t+63PR1iWd+kA4XZs6rs8g48aNs4+/++67HNMV7bUZzWvlJNLnuvrqq0OlS5f2nLd2OdQZ9Z7h915ur/3II494yvxoz7+7nrVgwYJQzZo1QyeccEJo48aNWc/56aef7HNUn481X8P51GKj4UJO74SbutncrVyKThVB/vjjj9meq+5liXZVPUWgiijDf3r27Ol5niawqctOrcdKq15fPxpSoCEB06ZNyxqW4E6vll7X89X9p0g9UpqjoZZxvYd6e9SK5+Z046plUC1hau1xD2NSi5HSrZY0RfrRUtSuKFpDJJ3Pqx+1ful7crq9db7UiqPWiPAx+ZG6mCO9j1oXtDyv+30U8YvzPh999JH9rZYRN6dlKdHU0pFf0X5mPz7//HPbKqLz4R7DrlY29Uyqa9xNXd1qacqJWtrUsqghN3fffXfU6VCXvlpmwj+v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DXalOhizdu1a07BhQxtsOg499FCzYMECU6lSJU4RAABAktEThaQP5TvrrLMIoFyqVatmHnvsMc85WrNmjbnjjjsS8wUBAAAgVwRRSKh9+/aZTz75xHOMoXzZaZ+oU045xXNs5MiR5quvvorzNwQAAIC8EEQhoaZPn262bNmSbVEJeGmRjRdeeMEULVrUc/zqq682u3fv5nQBAAAkEUEUkjqUT8ua16xZk28hh014b731Vs8x7a31yCOPcL4AAACSiCAKCaNlusP3h2IoX+7uueeebHtHKYiaN29eHL4hAAAARIMgCgmj1eWWLFniOdahQwe+gVxoxcJnnnkm27yyq666yhw8eJBzBwAAkAQEUUjaUL46deqYZs2a8Q3k4bzzzjOXXnqp59isWbPsnCkAAAAkHkEUEiZ8KJ8WlChUqBDfQBT++9//mvLly3uO3Xnnneavv/7i/AEAACQYQRQSYt26dWb27NmeYwzli54W33jooYc8x7Zu3WpuuOGGGH1DAAAAiBZBFBJi0qRJdmEJR5kyZcwZZ5zB2ffh2muvtasZuo0bN87+AAAAIHEIopCUoXznnHOOKVmyJGffhyJFith5UNpDyu3666+3vVIAAABIDIIoxJ02h508ebLnGEP58ue4444zN954o+fYqlWr2DsKAAAggQqF3GOsgDj45JNP7ApzWRddoULm77//NtWqVeN858O2bdtM48aNzcqVK7OOlShRwixcuNDUq1ePcwoAABBn9EQh4UP5WrZsSQBVAOXKlTNPPvmk59iePXvMHXfcUZCXBQAAQJQIohBX6ugMD6IYyldwHTt2NO3bt/cce/vtt82MGTNi8OoAAADIDcP5EFdff/21Ofnkkz3H5s6da4455hjOfAEtW7bMDuvbuXNn1jGt3vfNN99kW3wCAAAAsUNNC3E1atQoz+MjjjjCVvxRcHXr1jWDBg3yHPv+++/N66+/zukFAACII3qiENcFEGrUqGF27NiRdeyBBx4w99xzD2c9Rvbt22d7n3799desY7Vq1TKLFi2ye3EBAAAg9uiJQtxojo47gNKqfL169eKMx1CxYsWyLTLx119/mSFDhnCeAQAA4oSeKMRNq1atzOzZs7Men3nmmebzzz/njMeBNi/+7LPPsh6XKlXKLF682NSuXZvzDQAAEGP0RCEu5s2b5wmgpHfv3pztOHnwwQc9j3ft2mUX9NAKfjfffDOr9gEAAMQQQRQSsqBE+fLlzUUXXcTZjpOTTjrJs+R51apVTZMmTez/a4n50047zfTs2dPOoQIAAEDBEEQh5rTx65gxYzzHunbtakqXLs3ZjqNbb73V/h42bJhZu3ateeGFF+z///bbb/b7GDt2rHnmmWf4DgAAAAqIIAoxN378eLNhwwbPMYbyxZ/T8/Tzzz+bRo0amcMOO8w0aNDAnHXWWeayyy4z3bp1M88++2wCUgIAAJDeWFgCMde2bVvz6aefZj1u2LChmT9/vl2dD/GlxTu++OKLbMd//PFHuwlv//79zYEDB/gaAAAACoCeKMTUsmXLPKvEyRVXXEEAlSBa/bB79+6eY+XKlTOHH3643TtKe0gBAACgYAiiEFMvv/yyCYVC/7vAChe2CxogMdTbN2jQIFO0aNGsY08//bTZtGmTefHFF+2wPgAAABQMw/kQMxomph6PFStWZB07//zzzaRJkzjLSVit79tvvzUPPfSQueaaa8zpp59u1q9fb3ujtFIiAAAA8o+eKMSMhvG5AyhhQYnEGzp0qA2g7r33XjsHShvxzp0711SoUMGULVs2CSkCAABILwRRiBkNF3OrXLmyZ+8ixJ+G7mmp84EDB9rf5557rvnhhx/s39QLNXjwYL4GAACAAmI4H2JC+xLVrl3bs5nrDTfcYJ588knOcIJs3brVVK9e3Vx99dXmiSeeMDfeeKOZPn161t83b95sewq3b99uSpYsyfcCAACQT/+bfQ4UwKuvvuoJoIShfIm1Zs0as2vXLjsPbefOndl6nT744AO7ct/o0aPtPCkAAADkDz1RKDCtxte4cWOzcOHCrGNNmza1m76yN1Ti7N2719SrV8+sXr061+fpOYsXLzbFihVLWNoAAADSCT1RKLBZs2Z5AiinF4oAKrGKFy9u5syZY6ZMmeLZUPfdd98177//ftbjpUuXmjfffNP06tUrwSkEAABID/REocD69Olj94dylChRwqxcudJUqVKFsxsAmgtVv359u1eU45hjjjG//vqr3ccLAAAA/lCDQoEXMxg7dqznWI8ePQigAqRixYrmtttu8xybN28e+3cBAADkE0EUCuStt96yixi43XTTTZzVgNFKfdWqVfMce/TRR5OWHgAAgFRGEIWY7g3Vpk0b06RJE85qwJQpU8bcddddnmMzZ840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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def discover(frame, k, max_lag=2):\n", + " r = run_cdnots(frame, ParCorrGPU(frame.values.copy()), num_lags=max_lag,\n", + " include_C=True, c_preset=\"linear\", alpha=0.05, verbose=False)\n", + " return np.asarray(r.cg_tig)[:k, :k, : max_lag + 1], r\n", + "\n", + "\n", + "g_full, _ = discover(summer, 3) # sufficiency holds\n", + "g_obs, base_xy = discover(summer[[\"IceCream\", \"Attacks\"]], 2) # sufficiency violated\n", + "\n", + "\n", + "def edge_list(g, names):\n", + " return [f\"{names[i]}(t-{lag}) -> {names[j]}(t)\"\n", + " for lag in range(g.shape[2]) for i in range(len(names))\n", + " for j in range(len(names)) if g[i, j, lag]]\n", + "\n", + "\n", + "print(\"Heat collected :\", edge_list(g_full, [\"Heat\", \"IceCream\", \"Attacks\"]))\n", + "print(\"Heat hidden :\", edge_list(g_obs, [\"IceCream\", \"Attacks\"]))\n", + "\n", + "fork = {\"x\": np.array([0.5, 0.0, 1.0]), \"y\": np.array([1.0, 0.0, 0.0])}\n", + "pair = {\"x\": np.array([0.0, 1.0]), \"y\": np.array([0.0, 0.0])} # same y-span as (a)\n", + "fig, axes = plt.subplots(1, 2, figsize=(11, 4.4))\n", + "plot_graph(g_full, val_matrix=None, var_names=[\"Heat\", \"IceCream\", \"Attacks\"],\n", + " fig_ax=(fig, axes[0]), node_pos=fork)\n", + "plot_graph(g_obs, val_matrix=None, var_names=[\"IceCream\", \"Attacks\"],\n", + " fig_ax=(fig, axes[1]), node_pos=pair)\n", + "for ax, t in zip(axes, [\"(a) Heat collected - true fork recovered\",\n", + " \"(b) Heat hidden - 'ice cream causes attacks'\"]):\n", + " ax.set_ylim(-0.35, 1.35) # common vertical span so the titles line up\n", + " ax.set_title(t, fontsize=12, pad=10)\n", + "plt.tight_layout(); plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-06", + "metadata": {}, + "source": [ + "> **💡 The algorithm is not wrong — the data is incomplete**\n", + ">\n", + "> With `Heat` collected, discovery recovers the fork exactly: `Heat(t-1) → IceCream(t)`\n", + "> and `Heat(t-2) → Attacks(t)`, and no link between ice cream and sharks. Drop that one\n", + "> column and it returns a single edge: **`IceCream(t-1) → Attacks(t)`**.\n", + ">\n", + "> Given only those two series, that is the result the algorithm is entitled to\n", + "> return: under causal sufficiency it is the right answer. They really\n", + "> are dependent, ice cream sales really do come first, and with `Heat` absent there is\n", + "> nothing to condition on that would explain the dependence away.\n", + ">\n", + "> Notice what the lag bought the algorithm: **confidence**. There is no orientation\n", + "> ambiguity — time settles the direction — so it does not hedge. It commits to a\n", + "> direction, and the direction is nonsense.\n", + ">\n", + "> Causal sufficiency cannot in general be verified from the observed data alone — it is a\n", + "> claim about what is **missing** from it. Particular violations can still leave\n", + "> detectable signatures, and finding those is what the rest of this notebook is about." + ] + }, + { + "cell_type": "markdown", + "id": "lucid-07", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part 2 — What It Costs: The Graph Stops Answering Interventional Questions\n", + "\n", + "A causal graph is not a summary of correlations. It is a claim about **what happens if\n", + "you intervene** — an arrow `IceCream → Attacks` promises that changing ice cream\n", + "sales moves shark attacks.\n", + "\n", + "That promise is testable, and here it has a policy reading: **if we banned ice cream,\n", + "would shark attacks fall?** Compare how attacks vary across the ice cream sales we\n", + "*observed* against how they respond when we *set* sales ourselves — written `do(·)`.\n", + "When the effect is genuine and nothing is confounding it, these coincide; when a hidden\n", + "cause is doing the work, they part company." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "lucid-08", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:23.277113Z", + "iopub.status.busy": "2026-08-26T21:48:23.277019Z", + "iopub.status.idle": "2026-08-26T21:48:23.280573Z", + "shell.execute_reply": "2026-08-26T21:48:23.280081Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "observational dAttacks/dIceCream = +0.85 <- what the data shows\n", + "interventional dAttacks/do(IceCream) = +0.00 <- what a ban achieves\n" + ] + } + ], + "source": [ + "# Observational: how do attacks track last period's ice cream sales?\n", + "slope_obs = np.polyfit(icecream[1:-1], attacks[2:], 1)[0]\n", + "\n", + "# do(IceCream): re-run the same data-generating MECHANISM (a fresh summer), but SET\n", + "# sales ourselves - a price cap, a ban, whatever. The sharks' mechanism is unchanged,\n", + "# and notice that it never mentions ice cream.\n", + "rng2 = np.random.default_rng(11)\n", + "heat2 = rng2.standard_normal(T)\n", + "icecream_do = rng2.standard_normal(T) * icecream.std() # <- we set it\n", + "attacks2 = np.zeros(T)\n", + "for t in range(2, T):\n", + " attacks2[t] = 1.2 * heat2[t - 2] + 0.4 * rng2.normal() # <- unchanged mechanism\n", + "slope_do = np.polyfit(icecream_do[1:-1], attacks2[2:], 1)[0]\n", + "\n", + "print(f\"observational dAttacks/dIceCream = {slope_obs:+.2f} <- what the data shows\")\n", + "print(f\"interventional dAttacks/do(IceCream) = {slope_do:+.2f} <- what a ban achieves\")" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-09", + "metadata": {}, + "source": [ + "> **⚠️ Read this graph as causal and you will act on nothing**\n", + ">\n", + "> The observational slope is large; the interventional slope is essentially zero.\n", + "> Closing every ice cream stand on the coast changes nothing, because the sharks'\n", + "> mechanism never mentioned ice cream.\n", + ">\n", + "> The failure is not a slightly-wrong coefficient. It is a graph that recommends pulling\n", + "> a lever which is not attached to anything. Under violated sufficiency a discovered\n", + "> graph is a **statistical** object; treating it as causal is the error, not the\n", + "> algorithm.\n", + ">\n", + "> Practically: discovered edges are hypotheses to corroborate — with domain knowledge, a\n", + "> sensitivity analysis, or a real experiment — before anyone acts on them. Effect\n", + "> estimation on top of a confounded graph (see\n", + "> [`effect_estimation.ipynb`](effect_estimation.ipynb)) inherits the same bias." + ] + }, + { + "cell_type": "markdown", + "id": "7175491a", + "metadata": {}, + "source": [ + "---\n", + "## Part 3 — The Same Problem at Two Different Scales\n", + "\n", + "The natural response to Part 1 is: *measure the confounder*. But if the hidden cause were\n", + "known and available to measure, it would not be hidden in the first place.\n", + "\n", + "What we can observe instead is the **footprint it leaves behind**. And that footprint\n", + "depends on how much of the observed system the confounder affects.\n", + "\n", + "In Part 1, we deliberately kept the example small: one hidden cause acting on two\n", + "observed series. With a larger system, an important distinction appears. A hidden cause\n", + "might affect only a small subset of the variables, or it might influence nearly all of\n", + "them.\n", + "\n", + "These are two different regimes of confounding. When the effect is confined to a small\n", + "part of the system, we call it **local**. When the same hidden driver reaches broadly\n", + "across the system, we call it **pervasive**.\n", + "\n", + "To make the distinction concrete, we now expand the toy example to eight observed\n", + "series. Below are two otherwise identical systems with the same genuine causal\n", + "structure: **traffic today drives rentals tomorrow**. The only thing we change is how\n", + "many of the observed variables are also affected by the hidden heat.\n", + "\n", + "In one system, heat touches only a few variables. In the other, it reaches almost all of\n", + "them. As we will see, that difference changes both the pattern of false edges and the\n", + "kind of correction we should apply." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c0480335", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:23.282008Z", + "iopub.status.busy": "2026-08-26T21:48:23.281925Z", + "iopub.status.idle": "2026-08-26T21:48:23.379037Z", + "shell.execute_reply": "2026-08-26T21:48:23.378401Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "heat reaches 2 of 8 edges reported: 10 (exactly 1 is real)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "heat reaches all 8 edges reported: 38 (exactly 1 is real)\n" + ] + } + ], + "source": [ + "cols = [\"IceCream\", \"Attacks\", \"Parking\", \"Sunscreen\",\n", + " \"Crowds\", \"Traffic\", \"Rentals\", \"Drownings\"]\n", + "\n", + "\n", + "def beach_town(n_touched, seed=3, T=1500):\n", + " \"\"\"Eight series. Heat reaches `n_touched` of them; one real edge throughout.\"\"\"\n", + " rng = np.random.default_rng(seed)\n", + " heat = rng.standard_normal(T)\n", + " X = rng.standard_normal((T, 8))\n", + " for j in range(n_touched): # heat arrives after 1 or 2 steps\n", + " d = (j % 2) + 1\n", + " X[d:, j] += 1.3 * heat[:-d]\n", + " for t in range(1, T): # the one true edge\n", + " X[t, 6] += 0.6 * X[t - 1, 5] # Traffic(t-1) -> Rentals(t)\n", + " return pd.DataFrame(X, columns=cols)\n", + "\n", + "\n", + "town_local = beach_town(2) # heat reaches 2 of 8 series (Part 1, scaled up)\n", + "town_wide = beach_town(8) # heat reaches all 8\n", + "\n", + "for name, town in [(\"heat reaches 2 of 8\", town_local), (\"heat reaches all 8\", town_wide)]:\n", + " r = run_cdnots(town, ParCorrGPU(town.values.copy()), num_lags=2, include_C=True,\n", + " c_preset=\"linear\", alpha=0.05, verbose=False)\n", + " g = np.asarray(r.cg_tig)[:8, :8, :3]\n", + " found = int(sum(g[i, j, lag] for lag in range(3)\n", + " for i in range(8) for j in range(8) if i != j))\n", + " print(f\"{name:22s} edges reported: {found:3d} (exactly 1 is real)\")" + ] + }, + { + "cell_type": "markdown", + "id": "75508936", + "metadata": {}, + "source": [ + "Nine false edges in the first town; thirty-seven in the second. The hidden cause is the\n", + "same sun, but the damage it creates is very different. In the first town, confounding is\n", + "**local**: most of the system is unaffected, and the spurious structure is concentrated\n", + "in one corner. In the second, it is **pervasive**: the hidden driver reaches nearly every\n", + "series, and false relationships spread throughout the graph.\n", + "\n", + "That distinction matters because the two cases call for different repairs. A correction\n", + "aggressive enough to remove pervasive confounding can destroy genuine structure when the\n", + "confounding is only local. So before correcting anything, we first need to determine\n", + "which regime the data are in — without observing the hidden cause itself.\n", + "\n", + "Fortunately, the two regimes leave different signatures. Fit a simple autoregressive\n", + "model and examine the variation it fails to explain. When a hidden driver moves many\n", + "series together, that shared variation remains in the residuals. In their correlation\n", + "matrix, it appears as a small number of unusually large eigenvalues." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3560f7b2", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:23.380448Z", + "iopub.status.busy": "2026-08-26T21:48:23.380334Z", + "iopub.status.idle": "2026-08-26T21:48:23.447834Z", + "shell.execute_reply": "2026-08-26T21:48:23.447398Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABA8AAAFJCAYAAAD0eQQNAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAVctJREFUeJzt3Qm8VPP/x/HPLW1olRaVFtEibbbKkiVCKP2QRKnkZykloohkKSTZ26iI7BJZU0LK0iLKT4RfWVrQXtrP//H+/h/n/mbmzp07997p3pkzr+fjMTTnnjPz/Z5z5ny/53O+S4bneZ4BAAAAAABko0h2fwAAAAAAACB4AAAAAAAAckTLAwAAAAAAEBPBAwAAAAAAEBPBAwAAAAAAEBPBAwAAAAAAEBPBAwAAAAAAEBPBAwAAAAAAEBPBAwAAAAAAEBPBgzRx5513WkZGhv3111+FnZSUU6tWLTv33HMtnWzZssWuvPJKq1Klijtv+vXrZ8ninHPOsV69elkquuKKK9z5FFSnnHKKe8Hsu+++s/3228+WLFnC7kBCUZ6nV3muNKvs8M2ePduVy/p/Mvnyyy+tePHitmLFCks1//3vf90+nTRpkgVRsp4zhaVFixZ28803F3YyUhbBA+xz77zzjqvsIH/+/vtvGzFihJ188sl28MEHW7ly5dwF8KWXXkr4rh02bJgrRK+55hqbPHmyXX755dmuu3fvXhszZow1bdrUDjzwQKtcubKdffbZNnfu3ISn67PPPrMPPvjAbrnllrCbNJ1fKvyRvkaPHm0XXXSRHXrooa6SFFrZjse9995r559/vjt/tX2sa9bvv/9uF198sfsNlilTxtq3b28///xz2DoNGza0du3a2R133JHnPAHJhvI8tfz44492ySWXWPXq1W3//fe3+vXr21133WXbtm1L+Hfddttt1rlzZ6tZs2bmsieffDKwN+SIz+uvv26dOnWyOnXquHOwXr16duONN9qGDRvi3oUvv/yyq++qzD3ooIOsdevW9vbbb0cNqmb3Uv3RpzrkE088YatXr+Yw5sF+edkIyG1lQz9SAgj5M2/ePFc468n74MGD3VPN1157zVUMdAM9dOjQhJ2Ys2bNchfqIUOG5LjugAED7KGHHrLLLrvMrr32WlcgjB071l3cdbE+7rjjEpYuBU9OP/10q1u3buYyP+964p3sT/XHjx/vgi1IvPvvv982b97szrdVq1blenv9ptTSplmzZvb+++/HbJVz6qmn2saNG+3WW2+1YsWK2ahRo9z5/vXXX7uKje/qq692v9effvrJDjvssDznDUgWlOep49dff3XXw7Jly1rv3r2tQoUKrh6hcn3BggU2bdq0hH2Xrn0ffvhhlocGCh5UrFgx18HcgqaAxz///OOu50isq666yg455BBXR1Rw/9tvv7XHH3/cXUsWLlxopUqVirn9Y489Ztdff70Lxt933322fft2F5BSCyLVgTt27OjW0/9D64Y+ldMqt4899tjMZQr4K/Cv81PBNOQOwQMkPc/z3MUipwtM0B155JHuKUJoVF83623atHE3TmqCdcABByTku9auXeuenOZk9+7d7onvhRde6Foo+PQEWFHm559/PmHBA6VJkWa1ckhVVEz2nY8//jiz1YFawOTWL7/84oJP6tqllj3ZUWVDv0M10fUrI2pp06hRIxs5cqRrtePTb7N8+fL2zDPPUEEBKM8LlMpkBfPnzJnj6g/+jZwC2M8++6ytX7/eXZ8SYeLEie76q4cOqUjlRsmSJQs7GYH06quvZunOePTRR1u3bt1cHVFdZHMKHqisfeutt9xxkh49eli1atVc2eoHDxo3buxekQG03377zX2HutT4ihQp4uqt+h3o4ZP/uYgP3RbSjAoSRYDV9EfR6O7du0dtvvbcc8+5H7du2BWt1tNt/QhDffrpp5nNhEuUKGE1atSwG264wUVvffoutTqQ0OZD8fRJ1NO/Y445xqVBT7L99Kv/vb5L36koo26cI5/mPvjgg9aqVSv3FFDbKy+6gEWjvOoGV82pVJCqW4CaxkdSAaz1VMDoxlgXnWj7N570vfjiiy5NpUuXdtHPo446yh555JGY+6V27dphgQPRvuzQoYPt2LEjS7Pp7G7Ae/bs6ZpmKx9NmjRxF9/IfnG6kdKNun+8susSsGvXLne89XmhKlWq5C7OiQz4KD0KVuiGzKfos85B0dNgP72h/fp0s6eKk46Hot/XXXddluZyKth086enMTpvlG7t79wEKuL5zUQb80DdUdQtROeBfpcqUBcvXhy1/+X333/vCjx9vo6ffh9vvvlm2Draxm+i179/f3cjrKDSBRdcYH/++WfmevqN6TyOpmXLlu6zQyuGp512mjuu2o8KLClolBM/LZHnT3b9L7/44gs766yz3LVJv0e/9Uo89NvITwUg3lYruo6oIhP6FENNgdUiRk0rI4NFOrcS+YQP8FGep255ntt6Sl5s2rTJ/T+yfK5ataorn0NvpvLrjTfecGVE6DVY19SlS5e6wK5fNofeRKrOovJb5ZmOlwIPkU3R/bJC3TP1BFmtw1SeqYtZZPkaq5uZbja1H3QcVR+YMGFCXGMevPLKK66803miOsLUqVOjluM6Jx5++GH32VpX3/Xvf//bBWii1W9jnX/z5893aQmtm/lUL9bfpk+f7t5rfAk9RFJXAJ1DOpe0T+Ppxhk5lkassYtUx1SLFf0G/Pq+HlhpeU6ijYOk+oj85z//ies8Vt0j9NzS70wPCXKqY77wwgvuAWSXLl2y/O2MM85w+0+tZpBLHtLCkCFDPB3uZs2aeR07dvSefPJJ78orr3TLbr755rB177nnHi8jI8Pr1KmTW2/o0KFexYoVvVq1annr16/PXK9Pnz7eOeec4w0bNswbO3as17NnT69o0aLehRdemLnO3LlzvTPOOMN9z+TJkzNfsdSsWdOrW7euV758eW/gwIHemDFjvI8++sjbunWr17hxY++ggw7ybr31Vre8a9euLq19+/YN+4zq1at71157rff44497Dz30kHfccce5NEyfPj1svTvvvNMtb9WqlTdixAjvkUce8S699FLvlltuCUtPvXr1vMqVK7vv1Wc2b97cfe+SJUsy14s3fR988IH7ztNPP9174okn3Kt3797eRRdd5OWFvkuf98cff8Rcb9u2bV6DBg28YsWKeTfccIP36KOPeieddJLb9uGHH3brrF692h0fHe+mTZtmHq8tW7Zk+7nHH3+8d8ABB3jPPfect2LFCm/x4sXuHNB++Omnn7xE0fmqzwylz7/++utdHrQf/PQqH6HnfZs2bbzHHnvM7Wedo8cee6y3c+fOzM9p3bq1d8ghh3iVKlVy62jfnHjiiW7bp59+Ose0xfub6datmzuffHv27PFatmzp0qTv1bml30uTJk3cd0+cODFzXZ1rZcuW9Ro2bOjdf//9bt2TTz7Zfe/rr7+euZ628X/rp512msv3jTfe6L7j4osvzlzv2Wefdet9+eWXYXn573//65br9+DT/rriiiu8UaNGuc8788wz3TpKQyjtR70i0/LLL7+Eraffs5br/76ZM2d6xYsXd/tj5MiR7rv0e9KyL774wssNnY/a13nx559/urTp3Imk41WiRAnvmmuuyfK3wYMHu+02bdqU5dwoUqSIt3HjxjylB4hEeR6M8jzeeorSHHo9i3b9jObdd991651//vneokWLvJUrV3ovvviiV6ZMGa9fv34J+2H99ttv7ntUboaaOnWqy2P9+vUzy2btL1EZrWNQunRp77bbbnP5V7mna2Voeebn9aijjnLHQ+upXliyZEnviCOOcPWaWPQ9SkONGjW8u+66yxs9erTbH/pMlTE+lVGRZa6Og463/7233367q5c2atQorBz36yf77bef16tXL3eu6JxTORRZ14j3/KtTp46rX0fq3r27S4P/ma+88orbb3fccYc3btw495n6u75H53CscybyvMquHFe5pzJ///33d+eN6vs6x5Xf9u3be3nxww8/uPTo/iEnqlep/qLzS8fpP//5j/vdlCpVyt1jxKJjp2O/d+/ebM9b1WmQOwQP0qyy0aNHj7DlF1xwQdgNmW4c9CO99957w9b79ttv3YUidHm0i/bw4cPdRVA3kb7rrrvOfXe8dEHT+u+9917Y8rvvvttdjHXRCaWCRGlWwZhd2nSh1QVfN1O+H3/80RVU2ge6OIYKvdD46fnkk08yl61du9bdROimLLfpU8VDhffu3bu9/Pr777/dDa+CADlRgED50E1+6H7RzdqBBx4YdtOjPLdr1y6uNGg/qvDTZ/svFXzff/+9l0i6mT/66KOzLFfhGa0ipWOkG08VeqHHV4W11p8wYULmMhWUWqabVt+OHTtcAEX7N7Twj5Sb30xk8OC1114LC96I0qrzNLIio8qpKlDbt28PO09VUT788MOz3LArYBJ6HitgpHRu2LDBvdfNbOQ5LA888ECW33C033rbtm3dcU5E8EDpVB70maFp1vfWrl3bBVSSIXjg/02V0Ei6adDfIs/7KVOmuOW5DYAA2aE8D0Z5Hk89JT/BAz8fuskKLZ91s55IH374ofvct956K8vfjjzyyLAywaebUG3z6aefZi7bvHmzu94r6O4fQz+v1apVC6ujvPzyy265AkSx6KFW1apVvb/++its+SWXXOKC8f4xiBY8UHmrwIPS5Zs9e7ZbL7QcVx607Pnnnw/7DtVhI5fHe/4NGjTIPehZt25dWJ2kXLlyYfX4aGXzvHnz3HfoAUEiggcK+ui3FXqsREES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U/BAgYMNGzbYl19+aWvXrs3Sh61r165WUDRl5Jw5cwrs+wAAAAAASDd5Ch689dZbrsvCli1bXPPB0FYI+ndBBg++/vprq1q1aoF9HwAAAAAA6SZPwYMbb7zRevToYcOGDbP9998/z1+u4MPy5csz3//yyy8uGFChQgU79NBDbdCgQfb777/bs88+6/7+8MMPu+kgjzzySNu+fbsb82DWrFn2wQcf5DkNAAAAAABgHwQPdEN//fXX5ytwIPPnz7dTTz01833//v3d/7t162aTJk2yVatWufEVfDt37nSBC32/vrtx48b24Ycfhn0GAAAAAABIguBB27Zt3Y1/nTp18vXlp5xyisWa7EEBhFA333yzewEAAAAAgCQPHrRr184GDBhg3333nR111FFWrFixsL+ff/75iUofAAAAAABIxeBBr1693P/vuuuuLH/TgIl79uzJf8oAAAAAAEDqBg8ip2YEAAAAAADBVSS/H6BZDwAAAACkl1GjRlnHjh3t4osvtmSkgdZff/11SxYdOnSwxYsXF3YygIJteaBuCZqmccyYMbZmzRr74Ycf3OCJt99+u9WqVct69uyZ9xQBAAAASGpLliyxTz/91CZPnmwHHHBAvj7r/fffdzf5Y8eOTVj6kHdff/21Pffcc/bjjz+6LulvvPFGzPU//vhjd/x++uknq1atWpbj+MADD9hHH31k++33v1vP+++/3xo2bMhhSoeWB/fee6+bCUEnQvHixTOXN2rUyJ566qlEpg8AAABAktGU6gcffHC+AweJoNnbgj7mWkHmr2TJkm52vauvvjqu9UuXLu1aoHTu3Dnbdc477zx76623Ml8EDtKo5cGzzz5r48aNs9NPPz3spGrSpIl9//33iUwfAAAAgHy67LLL3A3cnDlzbMWKFVa3bl0bOHCgVapUyf39999/t8cee8y1KD7wwANdE3vdEEYzdepUGz9+vLuh1WeedNJJbjr1++67zxYtWuS6NesJ9FVXXWVNmzbN3G7BggXuAeSvv/5qJUqUcN9x7LHH2iOPPJL5WfL000+7wMSrr77qbjS3bNli9erVs+uvv96qVq2amR/NADd37lz7+eef7fHHH7fatWtnSetff/3lui/oKbpaSOvfNWvWdH9bv369205dCfRAtE2bNtatWzcrWrRo1NYQ//73v90+0Y21/3flfdq0ae7vunn2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81WBn+7du1vZsmXt+uuvL+zkpRXK7dSXTmV3UMvtoJfdlNup7/6glduaqhHJRYdl6tSpXpCtXbvW5fPjjz/2gqp8+fLeU0895QXN5s2bvcMPP9ybMWOG17p1a69v375eEAwZMsRr0qSJF2S33HKLd+KJJ3rpROfnYYcd5u3du9cLgnbt2nk9evQIW9axY0evS5cuhZYmUG4HSRDL7qCW2+lQdlNup752ASu36baAQrFx40b3/woVKgTuCOip54svvuieSqkJZNCoBUm7du2sTZs2FjR6Cq+uQ3Xq1LEuXbrYypUrLUjefPNNO+aYY9xTeHUfatasmY0fP96CaufOnfbcc8+5Fl1qyhoErVq1spkzZ9oPP/zg3i9evNjmzJljZ599dmEnDQEX5HI76GV3kMvtoJfdlNupr1XAyu3gtOtByti7d6/rb6fm040aNbKg+Pbbb12FY/v27XbggQfa1KlTrWHDhhYkqlgtXLjQNX8MmuOPP94mTZpk9erVs1WrVtnQoUPtpJNOcs3eNVZHEPz888+u6Vz//v1d1yEdRzWZK168uHXr1s2CRmPHbNiwwa644goLioEDB9qmTZtcn96iRYu6G557773XVZiBfSWo5XY6lN1BLrfToeym3E59A4NWbhd20wekX7eFq6++2qtZs6b366+/ekGyY8cO78cff/Tmz5/vDRw40KtYsaK3dOlSLyhWrlzpVapUyVu8eHHmsqA1fwy1fv16r0yZMoFqvlqsWDGvZcuWYcv69OnjtWjRwguiM8880zv33HO9IHnhhRe86tWru/9/88033rPPPutVqFDBmzRpUmEnLa1RbqeuIJfd6VZuB7HsptxOfS8ErNym5QEKVO/evW369OlulGoNVBQkenpbt25d9++jjz7aRfkfeeQRN0BRECxYsMDWrl1rzZs3z1ym6KmO5eOPP247duxwEdWgKFeunB1xxBG2fPlyC4qqVatmeaLWoEEDe+211yxoVqxY4QbQev311y1IBgwY4J5iXHLJJe69ZstQXjWjTRBbj6DwBbncDnrZnW7ldhDLbsrt1DcgYOU2wQMUCD2Y6dOnj2sOOHv2bDddSTo081TBHBSnn366a94ZSqPFqhnWLbfcErgKyJYtW+ynn36yyy+/3IJCTY4jp0hVH7yaNWta0EycONGN66B+vkGybds2K1IkfLgi/fZ0vQESKR3L7aCV3elWbgex7KbcTn3bAlZuEzxIootdaJT0l19+sa+//toNTHTooYdaEAbrmTJlik2bNs31QVu9erVbrmlKNOdpqhs0aJAb+ETHavPmzS6vqmy9//77FhQ6bpF9XTXN5kEHHRSIPrA33XSTnXfeee5G+o8//rAhQ4a4i3vnzp0tKG644QY3cM+wYcPs4osvti+//NLGjRvnXkGiAlnBA0X0gzRll+gcVV9JXWs05ZOmlHvooYfcoJAoWJTbqS/oZXfQy+10KLspt1PfeUErtwu73wT+30cffeT6TEa+unXrFohdFC1vek2cONELAk3BonEcihcv7h188MHe6aef7n3wwQde0AWp72SnTp28qlWrumNYrVo193758uVe0Lz11lteo0aNvBIlSnj169f3xo0b5wXN+++/764vy5Yt84Jm06ZN7jd36KGHeiVLlvTq1Knj3Xbbba7fNgoW5XbqS8eyO0jldrqU3ZTbqW1TwMrtDP2nsAMYAAAAAAAgeYV3wAAAAAAAAIhA8AAAAAAAAMRE8AAAAAAAAMRE8AAAAAAAAMRE8AAAAAAAAMRE8AAAAAAAAMRE8AAAAAAAAMRE8AAAAAAAAMRE8ABIE7Nnz7aMjAzbsGGDpZpJkyZZuXLlLJWdcsop1q9fv8JOBgAghVB2Fy7KbiAcwQMgTbRq1cpWrVplZcuWLeykAACAOFB2A0gmBA+ANFG8eHGrUqWKa32A+OzcuZNdBQAoNJTduUfZDew7BA+AgNi7d68NHz7cateubaVKlbImTZrYq6++GrPp4/jx461GjRq2//772wUXXGAPPfRQlu4B06ZNs+bNm1vJkiWtTp06NnToUNu9e3fm3/WZTz31lNten3P44Yfbm2++mZmm6tWr2+jRo8M+c9GiRVakSBFbsWKFe6/vPeqoo+yAAw5w6bn22mtty5Yt2eb1iiuusA4dOoQtU5cANS+Md39EU6tWLbv77ruta9euVqZMGbvqqqvc8ltuucWOOOIIlz/tg9tvv9127dqVud2dd95pTZs2tcmTJ7vPUOuOSy65xDZv3pztd7399ttuveeffz5mmgAAwUXZTdkNpBKCB0BA6Eb52WeftTFjxtjSpUvthhtusMsuu8w+/vjjqOt/9tlndvXVV1vfvn3t66+/tjPOOMPuvffesHU+/fRTdyOtdb777jsbO3asG38gcj0FFC6++GL75ptv7JxzzrEuXbrYunXrXICgc+fONmXKlLD1dcN8wgknWM2aNd17rffoo4+6dD/zzDM2a9Ysu/nmmwt0f/gefPBBF2hQgENBAildurTLt/bBI4884oIuo0aNCtvup59+sjfeeMOmT5/uXvqe++67L+p3aH9ov2g/aF8BANITZXf+9oePshsoIB6AlLd9+3Zv//339+bOnRu2vGfPnl7nzp3dvz/66CNPP/n169e79506dfLatWsXtn6XLl28smXLZr4//fTTvWHDhoWtM3nyZK9q1aqZ7/WZgwcPzny/ZcsWt+zdd9917xctWuRlZGR4K1ascO/37NnjVatWzRs9enS2+XnllVe8gw46KPP9xIkTw9LVrVs3r3379mHb9O3b12vdunXc+yOamjVreh06dPByMmLECO/oo4/OfD9kyBD3fZs2bcpcNmDAAO/444/PfK+0KY2PP/64y8vs2bNz/B4AQHBRdlN2A6lmv4IKUgDYd5YvX27btm1zrQci+/01a9Ys6jbLli1zXQ1CHXfcce6puW/x4sWuhUJoS4M9e/bY9u3b3fepGb80btw48+/qeqAm/2vXrnXv1Zy/QYMG7mn7wIED3dMD/e2iiy7K3ObDDz90Txu+//5727Rpk+sWEfkd+3p/+I455pgsy1566SXXMkKtC9SdQulTHkOpu4JaKPiqVq2auQ986jahZdqnxx57bK7zBQAIDsru/O8PH2U3UDAIHgAB4I8PoH701apVC/tbiRIl8vW56pLQsWPHLH/TGAi+YsWKhf1N4yCoH6dPTfP94IH+f9ZZZ9lBBx3k/vbf//7Xzj33XLvmmmtckKJChQo2Z84c69mzp6swRAseqJvD/zd6+J/QMQjysz8U/Ag1b948l37th7Zt27pxCl588UUbOXJk2Ho57QNR5WfhwoU2YcIEV9Fh8EoASF+U3ZTdQKoheAAEQMOGDd1N8cqVK61169ZxbVOvXj376quvwpZFvtdAiWqhULdu3Xyl79JLL7XBgwfbggUL3NN39WX0aZlusnUzrqCAvPzyyzE/7+CDD7YlS5aELdO4Df4NfF72R3bmzp3rxma47bbbMpf5Az3m1mGHHebyqYEdixYtao8//ni+0gYASF2U3ZTdQKoheAAEgJrL33TTTW5gId2In3jiibZx40bXPF7N67t165Zlmz59+tjJJ5/sZjo477zz3CCF7777btjT8DvuuMO1Cjj00EPtwgsvdDf36sqgG/d77rkn7vSpSb/mqlZrAnV7OP/88zP/psCEWg089thjLh1Kc2hwIZrTTjvNRowY4QZVatmypT333HMuTX6zxrzsj+xo9ggFIdTaQF0N1Jph6tSplleateGjjz5yAYT99tvPHn744Tx/FgAgdVF2U3YDqYbZFoCA0BSDmh1AYwdojAF1DdCNrqYqjEazHegmXcEDzS7w3nvvuZvt0O4IaqavMRA++OADd+PcokULN8uAP0tCbqjpvwIPGmdBUyf69N1Kw/3332+NGjVyMxAoD7EoXcqrZmRQujQlomaFyM/+yI4CHdovvXv3duM3qCWCPwtDXqnVh4I1L7zwgt144435+iwAQOqi7KbsBlJJhkZNLOxEAEgOvXr1coMWaopGAACQ/Ci7ARQUui0AaUzzImtUYw0SqC4LzzzzjD355JOFnSwAAJANym4AhYWWB0Aau/jii2327Nmu2X+dOnXcOAhXX311YScLAABkg7IbQGEheAAAAAAAAGJiwEQAAAAAABATwQMAAAAAABATwQMAAAAAABATwQMAAAAAABATwQMAAAAAABATwQMAAAAAABATwQMAAAAAABATwQMAAAAAAGCx/B+w+gml6UrwpQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def var_residuals(frame, p=1):\n", + " \"\"\"What a VAR(p) fit leaves unexplained.\"\"\"\n", + " X = frame.values\n", + " Y = X[p:]\n", + " Z = np.column_stack([np.ones(len(Y))] + [X[p - 1 - k: len(X) - 1 - k] for k in range(p)])\n", + " beta, *_ = np.linalg.lstsq(Z, Y, rcond=None)\n", + " return Y - Z @ beta\n", + "\n", + "\n", + "def spectrum(frame):\n", + " U = var_residuals(frame)\n", + " eig = np.sort(np.linalg.eigvalsh(np.corrcoef(U, rowvar=False)))[::-1]\n", + " return eig, (1 + np.sqrt(frame.shape[1] / len(U))) ** 2\n", + "\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(10.5, 3.4), sharey=True)\n", + "for ax, (frame, name) in zip(axes, [(town_local, \"heat reaches 2 of 8\"),\n", + " (town_wide, \"heat reaches all 8\")]):\n", + " eig, edge = spectrum(frame)\n", + " ax.bar(range(1, len(eig) + 1), eig, width=0.62,\n", + " color=[C_LUCID if e > edge else \"#B0B0B0\" for e in eig])\n", + " ax.axhline(edge, color=\"#444444\", lw=2, ls=\"--\")\n", + " ax.set_title(f\"{name} (top eigenvalue {eig[0]:.2f})\")\n", + " ax.set_xlabel(\"eigenvalue rank\")\n", + " ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + " ax.grid(axis=\"y\", alpha=0.25); ax.set_axisbelow(True)\n", + "axes[0].set_ylabel(\"eigenvalue\")\n", + "axes[1].annotate(f\"no-factor boundary = {edge:.2f}\", xy=(4.0, edge), xytext=(0, 6),\n", + " textcoords=\"offset points\", fontsize=9, color=\"#444444\")\n", + "plt.tight_layout(); plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "57c20798", + "metadata": {}, + "source": [ + "The dashed line is the same in both panels. It is the Marchenko–Pastur upper edge: the\n", + "boundary that residual eigenvalues stay below when there is no latent factor at all.\n", + "\n", + "In the first town nothing reaches it — the heat is there, but it disturbs too small a\n", + "corner of the system to stand out. In the second, two eigenvalues clear it comfortably.\n", + "The distinction the previous cell showed in *consequences* is visible here in the data\n", + "itself, before any graph is drawn.\n", + "\n", + "That gives us the two regimes, and what can be done about each:\n", + "\n", + "| Regime | Fingerprint | What can be done |\n", + "|---|---|---|\n", + "| **Sparse** | residuals stay essentially full-rank | prune edges that do not survive adjustment for a small set of observed predictors |\n", + "| **Pervasive** | a few directions dominate the spectrum | attenuate those directions, then rebuild the affected structure from what remains |\n", + "\n", + " \n", + "\n", + "> **💡 Why one correction cannot serve both**\n", + ">\n", + "> Each repair is harmful in the other's regime. Attenuating the dominant directions of a\n", + "> sparse system discards genuine structure, because nothing up there is confounding to\n", + "> begin with. Pruning against observed predictors is powerless once the sun touches\n", + "> everything — every candidate predictor is contaminated by the same driver.\n", + ">\n", + "> Note the modest wording in the table. Neither repair recovers the hidden variable, and\n", + "> neither is a general solution to a latent fork: conditioning on series you *did* measure\n", + "> cannot block a path running through one you did not. What they remove is the\n", + "> *footprint*. That is a weaker claim than identification, and it is the honest one.\n", + ">\n", + "> So a method must work out which regime it is in before it corrects anything. Making that\n", + "> call from the data is what **LUCID** adds." + ] + }, + { + "cell_type": "markdown", + "id": "lucid-13", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part 4 — A Realistic Case: Pervasive Factor Confounding\n", + "\n", + "Three latent GARCH factors, each driving a group of series, plus genuine lag-1 edges\n", + "*within* groups. This is the shape of a market panel: everything responds to a few shared\n", + "drivers, and the drivers are not in your table." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "lucid-14", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:23.449387Z", + "iopub.status.busy": "2026-08-26T21:48:23.449304Z", + "iopub.status.idle": "2026-08-26T21:48:23.854377Z", + "shell.execute_reply": "2026-08-26T21:48:23.853885Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "observed: T=1500, d=15 latent factors: 3 (not in the data)\n", + "true edges (6): [(0, 1, 1), (1, 2, 1), (5, 6, 1), (6, 7, 1), (10, 11, 1), (11, 12, 1)]\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def make_garch_factor_data(d=15, n_factors=3, T=1500, seed=42):\n", + " \"\"\"Latent GARCH factors + sparse true lag-1 edges. Returns observed data only.\"\"\"\n", + " rng = np.random.default_rng(seed)\n", + " group_size = d // n_factors\n", + "\n", + " factors = np.zeros((T, n_factors))\n", + " sigma2 = np.ones(n_factors)\n", + " for t in range(1, T):\n", + " sigma2 = 0.01 + 0.1 * factors[t - 1] ** 2 + 0.85 * sigma2\n", + " factors[t] = rng.normal(0, np.sqrt(sigma2))\n", + "\n", + " data = np.zeros((T, d))\n", + " loadings = rng.uniform(0.5, 1.5, size=d)\n", + " for t in range(1, T):\n", + " for i in range(d):\n", + " g = min(i // group_size, n_factors - 1)\n", + " data[t, i] = (loadings[i] * factors[t, g] # contemporaneous factor\n", + " + 0.3 * data[t - 1, i] # AR(1)\n", + " + 0.3 * rng.normal())\n", + "\n", + " true_edges = []\n", + " for g in range(n_factors):\n", + " start = g * group_size\n", + " for e in range(2):\n", + " src, tgt = start + e, start + e + 1\n", + " if tgt < (g + 1) * group_size and tgt < d:\n", + " true_edges.append((src, tgt, 1))\n", + " for t in range(1, T):\n", + " data[t, tgt] += 0.4 * data[t - 1, src]\n", + "\n", + " gt = np.zeros((d, d, 2), dtype=int)\n", + " for src, tgt, lag in true_edges:\n", + " gt[src, tgt, lag] = 1\n", + "\n", + " gt_full = np.zeros((d + n_factors, d + n_factors, 2), dtype=int)\n", + " gt_full[:d, :d, :] = gt\n", + " for g in range(n_factors):\n", + " for i in range(g * group_size, min((g + 1) * group_size, d)):\n", + " gt_full[d + g, i, 0] = 1\n", + " names_full = [f\"X{i}\" for i in range(d)] + [f\"F{g}\" for g in range(n_factors)]\n", + "\n", + " df = pd.DataFrame(data, columns=[f\"X{i}\" for i in range(d)])\n", + " return df, gt, true_edges, gt_full, names_full, n_factors\n", + "\n", + "\n", + "df, gt, true_edges, gt_full, names_full, n_factors = make_garch_factor_data()\n", + "d, max_lag = df.shape[1], 1\n", + "print(f\"observed: T={len(df)}, d={d} latent factors: {n_factors} (not in the data)\")\n", + "print(f\"true edges ({len(true_edges)}): {true_edges}\")\n", + "\n", + "plot_graph(gt_full, val_matrix=None, var_names=names_full,\n", + " target_node=[f\"F{g}\" for g in range(n_factors)],\n", + " target_node_color=\"#E91E63\", target_out_color=\"#E91E63\",\n", + " title=\"Ground truth - pink nodes are LATENT (absent from the data)\",\n", + " node_layout=\"sfdp\", node_layout_kwargs=GV);" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-15", + "metadata": {}, + "source": [ + "Run discovery and score it. Part 1's failure, multiplied by every pair inside every\n", + "group." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "lucid-16", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:23.856112Z", + "iopub.status.busy": "2026-08-26T21:48:23.855946Z", + "iopub.status.idle": "2026-08-26T21:48:24.777793Z", + "shell.execute_reply": "2026-08-26T21:48:24.777315Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CDNOTS alone F1=0.172 precision=0.096 recall=0.833 SHD=48\n", + " lag-0 edges predicted: 40 (ground truth has 0)\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXYAAAGbCAYAAADQhLs5AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAgHNJREFUeJztnQf8jeX//6+EjFAqEZGShpJkZFVCUqEhRYOGSiqpNIiKUkZGRTYJSYgkoyFtK5sGSWVmZAu5/4/n9f9e53ef+9z32fvzfj4ex/E5417nnNf1vt7XexxnWZalBEEQhKwhV6oPQBAEQYgvIuyCIAhZhgi7IAhCliHCLgiCkGWIsAuCIGQZIuyCIAhZhgi7IAhCliHCLgiCkGWIsAuCIGQZIuxpyFlnnaVat24d9mtvuOEGlckcd9xx6sUXX1SZxMKFC1XNmjVVwYIF9fEvXbpUpSNcV44v2u+XkIOEfd26derBBx9UZ599tsqXL58qXLiwqlWrlhowYIA6ePCg3xeILxW3XLlyqZNOOkldfPHF6oEHHlDz58933bZ5/euvvx7w3OjRo/VzixYtCnju22+/VTfddJM6/fTT1QknnKD3zTH+8ccfvtf8/vvvvu2HuvFa85577rlHnXPOOfpcixcvrq644gr1wgsvqGSxevVq/QM1x5QujB8/XvXv31/lNI4cOaJuvfVWtXPnTtWvXz/17rvvqjJlyqT6sLIOu344b4cOHdKv2bdvn/4tXnvttapo0aL6udGjR8f9WL777jtVu3ZtVaBAAa0Bjz32mN53OLz99tv6+1K6dGl9fF6D6lVXXeV5vnny5InoeHNH9Gql1IwZM/RBIp533323uuiii9Thw4fVN998ozp27KhWrVqlhg4d6nt9pUqV1JNPPqn/v3fvXrVmzRr1wQcfqGHDhqkOHTqovn37uu6nd+/eqm3btvpChuLNN99U7du31wPNo48+qkqUKKH3M3z4cPX++++rTz75RFtXp512mv4R2mEA+euvv/QP1A6vXbt2rapatarKnz+/uvfee/UXbfPmzerHH39UPXv2VC+99JJKBD///LMeCO3Czr744DmGdBL2lStXqscff1zlJDBsNmzYoL/D999/v8o0nN+vdMauH3by5s2r77dv3666deumRfOSSy5RX375ZdyPgdlYvXr11AUXXKD1Cr3o06eP+vXXX9XMmTNDvh+tQPuqVaum9cOLzp07B3yf9u/frx566CF1zTXXRHbQVgT89ttv1oknnmidf/751qZNmwKe//XXX63+/fv7/i5Tpox1/fXXB7zuwIED1o033kjxMWvQoEF+z/FYpUqV9P3rr7/u99yoUaP04wsXLvQ99s0331i5cuWy6tSpY+3fv9/v9WvXrrVOP/10q0SJEtbOnTtdz4nj4zjdePjhh63cuXNbv//+e8BzW7dutZLFBx98oM977ty5Ac95XeNkEOzaRQLn9sILL1iZwrx58/Qx87mkO1zXCH/maUM43+1Dhw5Zmzdv1v9HFzjXUaNGxfU4GjVqpDVk9+7dvseGDRum9zV79uyQ70c/jh07pv9fsGBBq1WrVmHv+91339X7GTduXETHHNGw3atXLz39GDFihLaKnZQrV05bzqHAAsZyZur0yiuv8K3zex63ztVXX633Z3ftuNG9e3c9VXnnnXcCrHvcJ2yDUXLIkCEqGsusVKlSrtPsYsWKBX3vRx99pI9r+fLlvscmT56sH7v55pv9XoslcNttt7n6QJlWMkOCunXr+qZmTsuEGRMWAe4iZi5jxowJOKbffvtNb4vrzrW6/PLL9QzMzd3ldPuwP/t+mT3wXixXc0yhZhP//vuvnqUxGypUqJBq0qSJtn7c2Lhxo54lGddahQoV1MiRIwNex5QcN1X58uX1ufO95Pry2dmtHqy+M888U2/rvPPO0xaX83vHOTzyyCNq6tSpeiZq9jtr1izfa/hcrrzySv1/riXv4VoYvvjiC1WnTh3te8f12LRpUz17tMM23K6Vmz88nGOyfweYYXId+O57feedPnbzmePOfOKJJ/Tnw/Hj2vz777/93nvs2DF9nGeccYb+DvGdZEaZKr891wPXSKLYs2eP+vTTT9Wdd96pXc4GvBUnnniimjhxYshtoB/OzzWSWTGfBd+jSIhI2KdPn65FA7dGrHBR+OLwA+aL4YQvz9atW7V/yosDBw6ozz//XP+QypYt6/oaBJMP/+OPP474GPlA/vzzT/1jjRT8cXyYX331le+xr7/+Wk+B+QEa+OH89NNP2m/vBo/jz4NOnTrpAZEbg4EBl1GzZs1UgwYNtGvp5JNP1j8y3GIGriWf2+zZs9XDDz+sB1REEXH98MMPIz4/po1Mk0899VTfMYXytzPN5DVMK1977TXtN7z++usDXsexMuh89tlnWtRYu8FouO+++/z28d9//+mFY9xUl112mT53DIvdu3drFxEg3pwjrjb8sEylEXbchoiYEz4brs/tt9+ujQKu0S233KJ27Nihn2fdhs8B+Fw4b64FcLwNGzZU27Zt099fto9vFkMllvWRUMcEK1as0NfV7Jt1IXzPkXy2uDGXLVum34cblN8719/Oc889p693lSpVtLv03HPP1efM4Jmo9QzcLfYbv/to2LdvX8C23G58f+zX9ejRo/p8na4gvv9LlixRiQJtYFC58cYbtbhHRLimPdMQXt60adO4TaX69euntzlt2jTfY/zdrl07/f+6detaxYsX164bN1fM0qVL9d/t27cPehwVK1a0ihYtGrE7YeXKlVb+/Pl97iH2M3Xq1ACXjxcVKlSwmjdv7vu7cuXK1q233qq3t2bNGv3YlClT9N/Lli3zvY7jsU/XQrlieO6rr77yPbZt2zbrhBNOsJ588knfY48//rh+3ddff+17bO/evVbZsmWts846y/rvv//8rvH69ev99sO+nccQiSvGfFa4t+y0bNkywBVz33336anv9u3b/V57++23W0WKFPF9H0aOHKnf27dv34D9makvnxevefnll/2eb9asmXXcccdpd52B1+XNm9fvMT4XHn/zzTcDroXTFcN3pFixYtaOHTv83o+r8O677/Y9xmfrdt3c3CbhHhOuzXz58lkbNmzwPbZ69Wrr+OOPD9im8/tlPvP69ev7rht06NBBv/+ff/7Rf2/ZskW7JtmXnRdffFG/PxIXQziY77bz5uW2C+WKadWqlev2nLcrr7wy4Ldn/30Z+C2jT5EQiSuGz5d9f/LJJ1ak5IpkSgJMoeMFVjuwsOAGlseWLVvU4MGDXZ837wt1TDxvjj8SmPKycMI0DIsLy5HRE/cAC2ehYCaBlW6OFWuIiCCsXPM490zZmWZHy4UXXqj3ZWAqjVWK68XAAjKuGmYS9uvP8XBubrOmeML+wcw+DM6FV7QMl1Xjxo31/+2WFJYh1hSL18DruJZYmk7M1Jf9Hn/88QH7xTXD9p2LX/Xr19duDEPFihX1FNx+Ld3A3cd3hZkSri77+5lJmfOPhlDHxMyFmRjfTRYRDczquGbhwnfB7jLgO8W2cbcBs2OsV2YPdtyuf7yoXr26tlrtN9wg0fD0008HbMvtZo/IM65gZv1O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4Iqf2qv+yrp163RhL8rdsqCK28MOAwBVE1lQjTfMHljAvPbaa9WiRYv8nnNzO5ElmyjYPusYEsMuCMkhxwr7lClTdDleOy+88ELIkrL45O3CjCVONIlXslPPnj31gioumXfeeSdgMZZCWvjy7QNGLPzwww+qfv362jX0zTffuL6mcOHC2v3CQib3+PwT6YZhXYJsX0EQkkOunGqt4yqxQySHc6HTCVmYCKcdrF7ivr2EnSxN44dn0Bg1apRu1OEU92bNmmnXULQwc6BaZI0aNXQsuhuUBiAkkegW3E2UBaB6Y//+/VWiYHE5WBE0QRDiT44UdmqOu1nrodLjTQ0YJ1i9pnGFcxsIKQuXBsSdJhbUL3c2AaHRB92LIoFoFgYFYui9/PUs9DKgrF69Ws8wOAbcTQxI1atX1zXSIynkFS60FuQmCEJyyXFRMcRkV6xYUYucATcEi4ehhJ0ep8R/O2F71FXBj0yPVBYrTYo9ECbJ/uwLrMwaHnzwQT0o2EF0iSUPNXugOiWzDgYOr48QFwix5dR7J1InGGwDS55ZSazgdiJEVOquC0JqyHEW+wcffOAn6uFa68Sf792717UuOX53LG0WSXF3OF0b1E557LHH/B5jfxQHc3YyMslAzvBIw4YNG3SoIgu9hCi6iTruD9wslA6g+1EoUQcWNqmdw/HHUjMdtwvbEFEXhNSRoyx2RJMQRHvfU6xt/NPhVinkciGA9pR84tmx5u2vIRpm1qxZfu8l7NEZGYPljpXv7H/K8djDJEmi6tGjh24/h0/eS1TJlmV70bhWOG62P3HiRO3W4RyZeQSrW89rTC9T9i8JSIKQBlg5iHHjxjGI+d2mTJkS8XZq167tt41evXoFvOaPP/6wChcu7Pe6QoUKWevWrQt47bFjx6x27doFHFuuXLmsgQMHWk8++aSVL1++gOft233hhResf/75J+prs3v3buu2227T26tZs6bfsf37779621u3brU2b95s9e3b17r88sutokWL6tdz7IIgpA85RtiPHj1qnXfeeX6CWKlSJS1ckfLAAw/4bad169aurxs/fnyACFerVs06fPhwwGs5jkcffdRTvN1uBQoUsJ555hlr+/btViwsXrzYKleunG+7tWrVCvr6l19+2e846tatG9P+BUGILznGx86CpLN7EAlK0STN4N+24/TZG3Cj0GLOWQWR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Pn26/m3gPqTSKWWpheQgFnsMsCCUyKbR/CCoHyOiLkQKPm5CGKn5bxd1U9cFP3giLXVE/f7779d9BrjHt9+lSxdJUkoSIuwxUqhQIV38KN6LmljqNCcoUKBAXLcrZL8fnXK5+LSJbnErGkf10Msvv9yzPHSs0LIRS53yveedd56+52/KArMw69akRogvIuxxEncKesUrThcxZ3si6kIkfP3117rNIhby1q1bA54nMmXw4MFaeNevX69zKZYvXx73i0w5YAwd8jKIHuPeGD7vvfeefn7+/Plx36/wf4iPPY5glRAfzC0avztWOrWwiVeXsEYhXOix+/TTT6sPPvjA9Xl86RSXwxVCZBVCi9/dNEqnwqg92xlXCuKPAAfrDeAFBk63bt104p3xsTt/DxwTFnzHjh0lvj0BiLAnaDpM8wK61YRTP50fAlE2RL5IEocQLqTvk0GKULs1V4fGjRvrdou4ZhBXEuUQVDt87z755BMt8s5FT95PmYFwxZ4qkdz4HpNhTSYrx/baa6+5ttaj8B2RM3RaEuKIJSSUw4cPW3v27LG2bt1qbdy40frrr7/0PX/z+JEjR+QTECLiv//+s0aPHm2VKFECM9j1duGFF1qzZ8/2e88jjzzi+touXbpYx44ds/r162cdd9xxVps2bayffvpJ3/N3nTp19D2v5b5JkybWihUr/G7r1q2zDhw44HnMGzZssGrVquW6/9NOO8365JNP5FsQR0TYBSGD+Pbbb62qVat6CnrRokWtt956y89g2Lt3r9WsWTPX1/fp08f3uvLly+vHEHXg3oi5U+zHjx9vrVmzRhspBw8eDOvYOaauXbtauXLlcj2WDh06WIcOHUrAVct5iLALQgaAxduiRQtPQT/++OOtxx57zNqxY4ff+3755RerQoUKAa9HnIcNG+a3fTcRN693ir19QIiUL7/80ipZsqTreVSuXNn6+eefY7hSAoiwC0Ias2/fPm3l5s+f31PUr732Wmv16tUB750+fbpVpEiRgNfnyZPHev/99/1e++KLL/qJvrm/7rrrXC32BQsWxHRe27dvt5o2bep6PgULFrTeeeedmLaf0xFhF4Q0BJ/32LFjPS1bbuedd541Y8aMgPfiT7cLtf128sknW3PmzPF7/dGjR63SpUv7va5mzZo+8c6XL5+f2Ddo0CBu54jb6IQTTnA91jvvvNPavXt3XPaV0xBhF4Q0Y/78+dbll1/uKegnnXSS1b9/f70w7+Sff/6xGjdu7Pq+Sy65RC9yOpk1a1bAaxcvXux7nsVN+3NffPFFXM932bJl1gUXXOB6zOecc07Ms4OciAi7IKQJREzdddddnoLOouPDDz9s/f33367vX7VqlW8B1Hlr2bKltX//ftf3ORdWL730Ur/nWZC1P4+PPBEuJ7tP337LnTu31bt3bz0TEcJDhF0QUgxhgt26dbMKFCjgKer169fXYYVefPDBB9o37baoShgjbg83CLvF525/z8CBA/1e4/TTf/3111aiwPfvti7ArWHDhtaWLVsStu9sQoRdEFIEYjthwoQA/7b9Vq5cOWvatGmewox//JlnnnF9Ly6UuXPnBj0Golvs78GfvmvXLr/XnHjiiX6v+e6776xEsn79eqtGjRqu51SsWDHtOhKCI8IuCClg0aJFVu3atT0FvXDhwlp0//3336C+aS9fPLHuf/zxR9BjYLA4//zz/d6HK8iJMyKHNYBEw/pB586dfYu2zttTTz0V9NrkdETYBSGJbN682brnnns8BYvHH3jgAe0iCeaP7tixo3azuG3j3nvvDStp6Jtvvgl477x58wJelzdvXr/XMCglCxZqvTJsq1SpYv36669JO5ZMQoRdEJIAQtujR48At4b9dtVVV1lLly4NeC/ZmD/++KMW4jFjxlhlypRxfT++8rffftvTbeOkdevWfu8/99xzXd/rHECWLFliJRMWi2+44QbXc+Z6EhYq+CPCLggJBKGcNGmSVbZsWU9B57nJkycHiCr+dzIxnYubbjeiYSLxfRMW6Vys7dmzp+trnftavny5lWy4NgMGDAiYPZjb3XffrWsvCf8fEXZBSADUZyH55qyzzvIUY6zN1157zdVtgo+ZKBeSgYhS+f7773V26Zlnnum3DYSOZKRw67UYBg8eHBBSiJvICSGGzuMmrDJVMHPxCulkxpFMN1E6I8Iu5Biw+hBMXBvcWHwL120RCQiwWXAk05PqiPbKhvjR8YO7Candj85re/XqpUMAEWI4++yzfdupW7eur4ZLpBCbbs8mZcDwKtzlFNBo9xnPQZPr5+WO6tu3b46PeRdhF7IaxJuFyN9++01bms5ysytXrrTWrl1rbdq0KWjZWS8ojWuyORksSKQxom7P3mT/PHbRRReFtCqpo9K+fXs/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p59+6vc6whDpZ0o5AK8KkPbQRS/XDduhg86JJ54Y0Hbvvvvu04lOTgoWLKiLjjnb7AmCkGIsIan89ddf1p133olvw/WWK1cuq23bttbff//te8/OnTutmjVrBrw2X7581scff6xfM3r0aOv666+3hgwZYi1dutRasWKFvjVp0sQ67rjj9Ou552/zHLc1a9ZYW7dutY4ePep5zMeOHbPeeust64QTTnA9Zs5n9+7dSbl+giCERoQ9SRw4cMDq1q2bVaBAAU9Rr1evnrV8+XK/9yG+5cqVC3htoUKFrC+//NL3ulq1avmeK1WqlPXmm29akyZN0mLepk0b66efftL3/M3jGzdu1GKMaIfLsmXLrAsuuMD12M855xxrwYIFcb1mgiBEhwh7gkE4J0yYYJUuXdpT0BHuadOmBYjs+++/bxUsWDDg9aeccoq1cOFC3+tWr14d8JrJkydbr7/+uv4/og7c83e/fv2iPp99+/bpAcLtPHLnzm317t3b+u+//2K4YoIgxIoIewJBfO2WtPNWuHBhLYSHDh3ye9+RI0esp59+2vU9Z5xxhrVq1Sq/1z/11FN+rylWrJj177//agvazWKPh2XNoFOkSBHXY2zYsKG1ZcuWmPchCEJ0iLAngE2bNlmtW7f2+badNyO2+Lad4FuvX7++6/uqV69u/fnnn36vR8BPO+00v9d17NjR9zzia/exX3nllXE7z/Xr11s1atRwPVYGl1mzZsVtX4IghI8Iexw5ePCg1aNHD+vEE0/0tNIR1iVLlri+f/HixVaZMmVc38dA4LTsAX+587UsiBrKly/v9xyvjyeHDx+2Onfu7DmIMZtg8BEEIXmIsMcBfOMIZtmyZT0Fnefwe3stVr7zzjs6ysX5vrx581pDhw71ey0RLPi64dprr/V7fe3atf1e61x4/fDDD61E8Pnnn1slSpRwPfeqVataa9euTch+BUEIRIQ9RrC+scK9BB3r/dVXX9XWvJfF++ijj7q+t2TJktb3338f8B588CeddJJ1zz33BFjKhD3acQ42H330kZUocCPdcMMNrudCFM/YsWMTtm9BEP4PEfYoYXHQLEZ6+dERXvztXhByWKdOHdf383iwBUh82G6LscaSNzijcWbMmGElEmYkAwYM0DMNt/O6++67rT179iT0GAQhpyPCHiH4uXv16qUtUC8rnUiYRYsWeW4DVwpx5l7bwILHkg/GxRdf7Oq2ueOOO/xCIbH67a9J1oImM5nzzjvP9fzOPffcoNdHEITYEGGPwBKdOnWqTsTxEnSsY8IAgyX9/Pjjj9rn7PZ+fOxjxowJ63gaNGjguo3zzz/f2rBhg+91xYsX93v+008/tZIFswdmLW7HmSdPHqtv374S8y4ICUCEPQzIBiUr1EvQySbt3r27zi61s3nzZmv69On6uccff9y6+uqrPV03RMMQFRMud911V8A2Lr/8cmv79u1+r3OGQn7xxRdWshk/frx2E7mdd6NGjVzDPgVBiB4R9hCLgdRtoX6Ll6gjsNR/cWJPMCJTlOgUr1orWLVOQQ6FMymJOjH79+8PeF3RokX9Xjdv3jwrFaxbt07H4budP7OKZM4kBCHbEWF3Af82afdEnngJOtbxDz/84HlhiYZ5+OGHrd9//933mNMFg9skWqHt06eP38BApIwbzuzQb775xkrldX322WddZy089swzz4RcWxAEIUuEndojWKM7duzQVvS2bdu0hUt0hZegRQO+caolei36mQJb48aNC+pH55iwzps2baqjQBo3buwn7DyHe8Yt4ch+zoRIct74qnHz2GuwvPvuu3pbzz33XNBjcSZLuYVPJhusc6fv39yqVaumrXtBEKInbeux04Tin3/+UTt37lSHDh0K+lpqiVO/nCYRefPmjWp/q1ev1p2BZs+e7fp8/vz51dNPP606duyo65C7waWcMWOG6tq1q65/bihXrpxuLVetWjW1adMm9eWXX+rHnBw8eFDt2rVLHThwwPOcOb8CBQqoZcuWqXXr1qnHHnss6HnxWrZroP47x5Fqtm3bpnu6urXaK1y4sBoyZIi6/fbbI9rm0aNH9bXjfLl+/M1nkitXLn3d+AzNLV6NSQQhHUk7YUfQ6dhDgwf+HymFChXSHYDCFXgGjhdffFE3jPBqVEEf0p49e6ozzzzT9XkE5IMPPlCvvfaaWr58ecDzdmGn+fTQoUN9z3H59+zZo8851ABmh/edcMIJ6pRTTtEDmpdQ8Ro6LxnojHTZZZepdIDPd8CAAbqD1JEjRwKev+eee9Sbb77pOZCa60ATEj5HrmM4YAhw3WhOwv8FIdtIq56nWFtr167VIheNqMPevXu1iDIwBBuzEOO33npLnXvuuVo83ES9SpUqusfo+PHjXUUdIcaypH0dLevcRN2JXYARsw0bNqg///wzIlE320GwN2/erH777TfPHqTO8zr++ONVuoAl3aFDB/XDDz/oz8HJqFGj9CC0dOlS1/dzzpz777//Hraom89+69at6qefftLftTSzbQQhe4QdFwQ/Urt1GS38UBG8P/74w3WAmDNnjrrkkkvUo48+qi09J1j89PvEbVGzZk3XwaNPnz7q7LPP1v1GOW43LrroInXHHXe4Prdv3z49AHEfK7ge2BbXMJOE3d4/dfHixapVq1YBz/3888+qevXq6o033vAJMPfbt2/XRoDdzRQNCDwurUgHVkFIZ9JC2BFXr2bOsYAAYxEbcf/ll19U48aNVcOGDbVP3c1t0blzZ/06+nhiUdphFkAf0jJlymhfO4OHGxdeeKEaM2aM+vHHH1XJkiUDnse6xMqMdlbiBdeQYzS4bT8dhd240EaPHq3Gjh0b0HeVwZ7+sLVr11YvvfSSXsfYsmVL3CxtRJ3BmRmjIGQDKfexG/FNJAg2gjxhwgRPMbj11ltVr1691FlnnRXwHCKMxYjbJdiPHx96p06d9ODBFH/YsGFq6tSpavfu3T4fO/7gK664Qt1yyy2qePHiKhHQlJrFZFwONMK2g/sB11E6gwXNwinrAU73E58f91zjV155Ja77ZbvMwlhcFYRMJqUWO26Cv/76K+H7efLJJ32i7lxkvPTSS9W8efPUXXfdpb777jvf4wgwVvfVV1+typYtq/r16+cp6vXr11eff/659hU3bdpUW/pY9K+//rq2BsuXL69fd8455+j9s9Aab1FyWu5YuW7rBulqsdvhOrG2wTU0cN3uv/9+PTBxP336dLVixYq47pfvB4O41yK6IGQKKRV2Qv8S/SPix//xxx/7iQIiQUTEiBEj1MKFC7VrBAsavzoiTzQG1jQ+37lz53pu+6abblILFixQn376qR4A7IMG/t8bb7xRhz1itbNQykDB3wwi+P8TBQKFuGOxZ6KwA1FNzKAIPyUqhnNigGa2wT1/ey2qxgLfRy8XmyBkCikTdixZXBROnn32WR0JgeXkZPjw4eriiy/W4mu3rPv27auuvfZavQhXr149HY9uFtUQUjdRwBq899571axZs7SoI7wsql511VXa1+u1qIkw4n9ftWqVmjJliqpatarr61jwYzsIlPPWv39/fR6JhOtCHoDb8WcS11xzjZo0aZIeNJkB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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ci = ParCorrGPU(df.values.copy())\n", + "base = run_cdnots(df, ci, num_lags=max_lag, include_C=True, c_preset=\"linear\",\n", + " alpha=0.05, verbose=False)\n", + "g_base = np.asarray(base.cg_tig)[:d, :d, : max_lag + 1]\n", + "\n", + "m_base = evaluate_graph(g_base, gt)\n", + "print(f\"CDNOTS alone F1={m_base['F1']:.3f} precision={m_base['Precision']:.3f} \"\n", + " f\"recall={m_base['TPR']:.3f} SHD={m_base['SHD']}\")\n", + "print(f\" lag-0 edges predicted: {int(g_base[:, :, 0].sum())} \"\n", + " f\"(ground truth has {int(gt[:, :, 0].sum())})\")\n", + "\n", + "plot_graph(g_base, val_matrix=None, var_names=list(df.columns),\n", + " title=f\"CDNOTS without deconfounding - F1={m_base['F1']:.2f}\",\n", + " node_layout=\"sfdp\", node_layout_kwargs=GV);" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-17", + "metadata": {}, + "source": [ + "Recall is fine at `0.83` — the real edges are found. Precision collapses to `0.10`,\n", + "because 40 contemporaneous edges were invented where the truth has none." + ] + }, + { + "cell_type": "markdown", + "id": "lucid-18", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part 5 — Diagnosis Before Repair\n", + "\n", + "`run_lucid` performs the Part 3 test automatically: fit a VAR, take the residual\n", + "correlation spectrum, and compare its leading mass against the no-factor null. The\n", + "threshold is derived from the Marchenko–Pastur law, so it adapts to `d` and `T` rather\n", + "than being a number you tune." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "lucid-19", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:24.779024Z", + "iopub.status.busy": "2026-08-26T21:48:24.778949Z", + "iopub.status.idle": "2026-08-26T21:48:25.393357Z", + "shell.execute_reply": "2026-08-26T21:48:25.392691Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "regime : pervasive\n", + "router statistic: R = 0.499 vs tau = 0.210\n", + "factors detected: 3\n", + "runtime : 0.61s\n", + "\n", + "factor 0 loads on: X0 (-0.54), X4 (-0.49), X2 (-0.45), X1 (-0.41), X3 (-0.29)\n", + "factor 1 loads on: X7 (-0.62), X8 (-0.57), X9 (-0.35), X5 (-0.29), X6 (-0.25)\n", + "factor 2 loads on: X10 (-0.58), X11 (-0.50), X12 (-0.39), X13 (-0.39), X14 (-0.31)\n" + ] + } + ], + "source": [ + "res = run_lucid(df, max_lag)\n", + "\n", + "print(f\"regime : {res.regime}\")\n", + "print(f\"router statistic: R = {res.spectral_ratio:.3f} vs tau = {res.tau:.3f}\")\n", + "print(f\"factors detected: {res.n_factors}\")\n", + "print(f\"runtime : {res.runtime:.2f}s\")\n", + "print()\n", + "for f in range(res.n_factors):\n", + " top = res.top_factor_variables(f, 5)\n", + " print(f\"factor {f} loads on: \" + \", \".join(f\"{v} ({l:+.2f})\" for v, l in top))" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-20", + "metadata": {}, + "source": [ + "The router says *pervasive*, finds three factors, and the loadings recover the three\n", + "generated groups — `X0–X4`, `X5–X9`, `X10–X14`.\n", + "\n", + "> **⚠️ Loadings describe, they do not identify**\n", + ">\n", + "> It is tempting to read those groupings as \"we found the latent variables.\" We did not.\n", + "> Factor directions are recovered only up to rotation, so a large loading means *this\n", + "> variable carries dominant shared variation* — not *this variable has an identified\n", + "> hidden parent*.\n", + ">\n", + "> This is the honest boundary of the method: LUCID removes the **footprint** of latent\n", + "> structure. It does not recover the latent structure itself. If you need the ambiguity\n", + "> reported explicitly instead, methods like FCI and LPCMCI return a partial ancestral\n", + "> graph, where `X <-> Y` states outright that a hidden common cause is present." + ] + }, + { + "cell_type": "markdown", + "id": "lucid-21", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part 6 — The Repair, and Whether Adapting Actually Helps\n", + "\n", + "Having diagnosed *pervasive*, LUCID keeps the lagged edges it discovered and\n", + "**reconstructs** the contemporaneous slice: attenuate the factor-dominated directions,\n", + "then re-admit only those lag-0 candidates that survive against an edge-free null.\n", + "\n", + "The comparison worth making is not against doing nothing — it is against a **fixed**\n", + "deconfounder. `tetrad_filter` always assumes pervasive structure and always strips lag-0\n", + "edges consistent with a shared cause. On this dataset that assumption is correct, so it\n", + "is a fair fight." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "lucid-22", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:25.394713Z", + "iopub.status.busy": "2026-08-26T21:48:25.394617Z", + "iopub.status.idle": "2026-08-26T21:48:25.566256Z", + "shell.execute_reply": "2026-08-26T21:48:25.565822Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "method F1 prec recall SHD\n", + "CDNOTS (none) 0.172 0.096 0.833 48\n", + "tetrad (fixed) 0.385 0.250 0.833 16\n", + "LUCID (adaptive) 0.556 0.417 0.833 8\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAsYAAAFZCAYAAACbsOm+AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAV0tJREFUeJzt3Qm8jPX7//Hr2PctQlK0IJU9Rd8S2VpERdJi1yplKaFIi6IoFUlZ2rO0kIpKaaMFUSoklZI1+x7u/+P9Of97fjNz5pwzR8c5M3Nez8djODNzzz33+rmv+7qv+zNJnud5BgAAAORwubJ7AgAAAIBYQGAMAAAAEBgDAAAAycgYAwAAAATGAAAAQDIyxgAAAACBMQAAAJCMjDEAAABAYAwAAAAkI2OMhDJ79myrVauWFShQwJKSkmzbtm0Wizp37myVKlUKeU3Te99992XbNOUUkZY9Et/vv//u9rHJkycfte/QuPUd+q54dvDgQbvrrrusYsWKlitXLmvTpo3FqsxuN+fNm+fGqf8TAceVjCMwzqH8Btx/KJA87rjjrEWLFvbkk0/azp07Ld78888/dtVVV1nBggVtzJgx9tJLL1nhwoWze7ISwvz5893B52idaAwbNszefvvtozJuABkzceJEe/TRR61t27b2wgsvWO/evVmEyDHyZPcEIHvdf//9VrlyZfv3339t/fr17iz5jjvusFGjRtnMmTOtRo0acbOKvv32WxfQP/DAA9a0aVOLN3v37rU8efLEbGA8dOhQl20tUaLEUQmMdRDOiszUc889Z4cPHz7q34PYcuKJJ7p9LG/evNk9KTHv448/tgoVKtjjjz+e3ZMCZLnYPAojy1x00UVWr169wPMBAwa4RvHSSy+1yy67zH7++WeXgY0HGzdudP8fjcAtKyhrnwgUdB44cCBm54fAKPt4nmf79u3LljbFvzKG6NrSeG1Hgf+KUgqk0KRJE7v33nvtjz/+sJdffjnkveXLl7vMXqlSpdxBRkG1MsvhdMldl99Uy5k/f347/vjjrWPHjrZ58+aQxrdbt25WtmxZN66aNWu6y3aR6gIfe+wxGz9+vJ188slufGeddZbLEPsuuOAC69Spk/tb7+kzym76pk2bZnXr1nUH5NKlS9t1111na9euDfkujUOP9GpSo50mn0oEzjjjDDeP+v+tt96KqhZMf+u1VatWBTK1xYsXty5dutiePXtCPqtMWK9evdy8FS1a1J3UaP6irS976qmn7PTTT7dChQpZyZIl3Xp99dVXA9Nx5513ur91dcEvv/HrKPV3z5497ZVXXnHj0LJQrbdoGTVs2NCOOeYYt+y1DqZPn55ivnfv3u3WvT/u4HWn+ejatavbTjRufYcu9YbT9qr5VvnMscce67a/OXPmpKgXjFRjrGD+iSeecOPWetJ33XjjjbZ169aQ4RYuXOjKjbScNT9aHpq2aIwdOzawfFS2dOutt6YoTdH2p23kp59+ssaNG7v1oczdiBEjovqOSZMmuf1X86/vqV69uj3zzDNRfVbLpUiRIrZ69Wo3j1qOmk5dVVJAeyTLS8tZJ9laD9qmtMyeffZZN4+av3Aar+ZXbYwvmm1IPvzwQ/vf//7n9hPNR9WqVW3gwIGp1hhrvHqu7SacEgT58uULmZ+vv/7aWrZs6fZBrZdGjRrZl19+meYyVZukbUVX5MI1b97cTWN69L0XX3yx2y+1TnQVb/To0SHDKJlx3nnnufc1/61bt3ZJjWDRtCf+Mvrkk0/sxx9/DOyP/v6j/bRv376u9ljbl6ZfyzF4+0irlvu/tHH79+93+3SZMmUCbdxff/0VcZlF22bo87pKFdxm6Hui4U+7jokq4StWrJjbRm+//XZ38nck+6W/v3zxxRdWv359t2+ddNJJ9uKLL6YYVm2Hru766+KUU06x4cOHczUsM3jIkSZNmqSWzPv2228jvv/nn3+699u2bRt4bdmyZV7x4sW96tWre8OHD/eefvpp7/zzz/eSkpK8N998MzDczp07vTPOOMPLnTu316NHD++ZZ57xHnjgAe+ss87yvvvuOzfMnj17vNNOO83Lmzev17t3b+/JJ5/0zjvvPPedTzzxRGBcv/32m3utdu3a3imnnOK+d8SIEV7p0qW9448/3jtw4IAb7oMPPvBuuOEGN+z999/vvfTSS978+fND5lXf//jjj3t33323V7BgQa9SpUre1q1bA9/VqFEj9wjXqVMn78QTT8zwNMmcOXO8XLlyueUxatQob9CgQW4Znn766SHjFI1zyJAhgef62/+eK664whs7dqzXvXt399pdd90V8tmrrrrKvX799dd7Y8aMcc9r1qyZYpyRjB8/PrCun332WW/06NFet27dvF69ern3ly5d6nXo0MENo+WnZavH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AACAWMsY9+nTxzp37my//PKLFShQIPD6xRdfbJ999llmTh8AAAAQu4Hxt99+azfeeGOK1ytUqGDr16/P0LjGjBljlSpVcgH22Wefbd988026JRy33nqrlS9f3vLnz29VqlSx9957L8PzAAAAAPznUgoFpDt27Ejx+sqVK61MmTJRj2fKlCku+zxu3DgXFD/xxBPWokULW7FihR177LEphj9w4IA1a9bMvTd9+nQXiP/xxx9WokSJI5kNINMdPHjQRo4cabNnz7akpCRr2bKl28bz5Em5q913331uuLx584acKNaoUSPw/NNPP7Vnn33WlSgVKVLEunfvbm3btmXNAQAQK4HxZZddZvfff79NnTrVPVcAoAN3//797corr4x6PKNGjXL1yV26dHHPFSC/++67NnHiRLv77rtTDK/XdaPf/PnzA8GEss1ArJgwYYItXbrUpk2b5p736tXLJk2a5LbzSNq1a2d9+/aN+J628+HDh7t9rXbt2rZ79+7Aja4AACBGAmNlxJS1UuZ279691qhRI1dC0aBBA3vooYeiGoeyv4sWLXK9Wvhy5crlbupbsGBBxM/MnDnTfYdKKWbMmOGy09dcc40LyHPnzh3xM/v373cPn5/pPnz4sHsAmUnbZe/evd3NqKJa/CeffNK6deuWYljP89wjte3wmWeecZ+rU6eOe66MsR5stwCQLJHbQ+YtcynGPGqBsXqj+PDDD+3LL7902bFdu3a5g7eC2mht3rzZDh06ZGXLlg15Xc+XL18e8TOrV6+2jz/+2K699lpXV7xq1Sq75ZZb7N9//7UhQ4ZE/MzDDz9sQ4cOTfH61q1b3WVvILPs3LnTNm7c6LZhP7Orv3XS+Oeff1rhwoVDhtcJm0olZs2a5QJplRFdfvnlbufdt2+f2w90JUav7dmzx04//XS7+eab7ZhjjmGlAYBZQl9FY94yV+nSpY9OYKwgtGDBgrZkyRI799xz3SMrz56UpR4/frzLENetW9f1m/zoo4+mGhgrI60az+CMccWKFa1kyZJWrFixLJt2JD7tG3L88ccH6t5VZiT58uULZJF9HTt2tDvvvNNthz/99JMNHDjQBc+6CrJhwwaXTf76669t7Nix7mT0kUcesccff9w9BwBYinY12IYEnrd4VyqG5y3DgbFqe0844QSX7f2vkbuCWwUAwfS8XLlyET+jnij0/cFlE6eddprLyKk0Q8FHpBsF9QinrFy0aXUgGn5GWNldf6fX31K0aNEU21v16tUDf9esWdOVXajG/rrrrnMlE3L11Ve7m0zlpptuctljZZp1cgoAOV0iH8eZt+xxRFvUoEGDXHbrv6T5FcQq4zt37tyQjLCeq444EmWnVT4RXHejnjAUMEcKioGspMyvSifUq4pPf+s1P9BNi59d9gPp1E4QlUkGAAAxEhg//fTT7oc8jjvuOKtataqrLw5+REslDs8995y98MIL9vPPP7v6Sd157/dSoUvNwTfn6X0F47fffrsLiJVdGzZsmLsZD4gFrVq1cr2nqIZeD/VI0aZNm4jDqk5f9fkKdFVKof2gSZMmgfeVHVbPL6pbVs2x9pWzzjrLChUqlIVzBABAznFEN9+ldqDPqPbt29umTZts8ODBrhyiVq1arl9X/4Y83XgUfClBtcFz5sxxd/2rr1ddYlaQrF4pgFigfoa3b9/uumGTiy66KHCip5M40dUWUdCrXlxUlqQeVtTTi8oofCqtUE18hw4d3PN69eq5rtsAAMDRkeTlsOuyCjR0I5OCF26+AwDkZEl937F45o1slep76yb/X3laPCrfOfXwLGlSP4tnXpfHLKEyxj71Q6wSCFFXUvoRAgAAACAeHVFgrJpH3S0/b968QLdU27Zts8aNG9vrr7+eoZ+FBgAAAOL25rvbbrvN/ZjBjz/+6G6G02PZsmWuTEE/gQsAAADkiIyxbpD76KOPXB/CwX2yjhkzxpo3b56Z0wdkK+q4AADIOY4oY6x+hPVDG+H0WiL/tjcAAAAS1xEFxuprVd2k/f3334HX9NPM6kbtwgsvzMzpAwAAAGL7Bz5UT1ypUiU7+eST3aNy5crutaeeeirzpxIAAACIxRpj/dDG4sWLXZ3x8uXL3WuqN27atGlmTx8AAAAQ2/0YJyUlWbNmzdwDAAAAyJGlFOqS7cknn4xYYnHHHXdkxnQBAAAAsR8Yv/HGG3buueemeL1hw4Y2ffr0zJguAAAAIPYD43/++ceKFy+e4vVixYrZ5s2bM2O6AAAAgNgPjE855RT3Ix/h3n//fTvppJMyY7oAAACA2L/5rk+fPtazZ0/btGmT69NY5s6da4899piNHj06s6cRAAAAiM3AuGvXrrZ//3576KGH7IEHHnCvqR/jcePGWceOHTN7GgEAAIDYLKXYu3evderUyf766y/bsGGDff/99y6DXLZs2cyfQgAAACBWA+PWrVvbiy++6P7Omzev+2GPUaNGWZs2beyZZ57J7GkEAAAAYjMw1q/enXfeee5vdc+mTPEff/zhguVI/RsDAAAACRkY79mzx4oWLer+/uCDD+yKK66wXLly2TnnnOMCZAAAACDHdNf29ttv259//mlz5syx5s2bu9c3btzo+jIGAAAAckRgPHjwYOvXr59VqlTJzj77bGvQoEEge1y7du3MnkYAQA508OBBGz58uDVu3Nh1DTpixAj3Wlr27dvn7ne54IILAq9t2bLF7rnnHrv44outUaNGds0119inn36aBXMAIEcExm3btrU1a9bYwoULQ37o48ILL7THH388M6cPAJBDTZgwwZYuXWrTpk2zqVOn2pIlS2zSpElpfubZZ5+18uXLpyj/q1q1qvvsJ598YjfddJMNGjTIVq9efZTnAECOCIylXLlyLjus2mJf/fr1rVq1apk1bQCAHGzmzJmu3/zSpUu7h/6eMWNGqsP//PPPNn/+fNedaLDjjz/err/+enejuI5Z559/vp144on2ww8/ZMFcAMgRgTEAAEfLjh07XD/5yvT6qlSpYuvXr7ddu3alGF4lFg8++KD179/f8uRJ+7erVFrx+++/26mnnnpUph1A/CIwBgDEHJU/iN8DUvDfu3fvTjH8Sy+95ILoOnXqpDnef//91wYOHOj6369evXqmTzeAHPiT0AAAHE2FChVy/ys7XKJEicDfUrhw4ZBh1UPSG2+8Ya+88kq6QfFdd91lBQoUcDfjAUA4AmMAQMxR15+qCV6xYoWrERb9rdeKFCkSMqxuylN5hPrU98sqlHHWDeGjR4+2M844wwXFKrPQeyNHjnS/2goA4QiMkeX8A5N6NElKSrKWLVtanz59ItYFqnsmdaukTJEySLr82atXr8BBTXeVP/roo7Z8+XLLly+fu6mmb9++LiMEIL61atXKJk6caDVr1nTP1auEumIL16xZM3fzt0831T3wwAP26quvWqlSpVybc/fdd7uu3NRzktoKAIiEGmPEdBdM7dq1cz87ruD4tddes5UrV9oLL7wQeF+XQ3V3ufrQfv311937zz//fBbODYCjpXv37lajRg3XDuihALlLly7uvWHDhrmH6ERYmWT/odILnXTrb51Eq71RG6L/dXJ93nnn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VLb5X375xfr27Wv33XefPfrooymGO3DgQKZ8n34hNNquDzMyLBCP+93q1avt8ccft2effdZ1CwoIgTFihg7COhgHP/ST4cpaPfjgg3b55Zdn9yQCR2WbP/HEE+3mm2+2pk2b2syZM11Wq02bNvbQQw+5KyVVq1Z1w//555921VVXuZ+1L1WqlLVu3dpdig02ceJEO/300924y5cvbz179ox4CVfBtt7TMAUKFHDT4P+QUviw8sMPP1iTJk2sYMGCdswxx9gNN9xgu3btCrzvT/Njjz3mxqlhbr31Vvv333/ZahCT+13FihXdNqv9Tr+g6//6rvYDJWW0rSspM3369JDP//jjj3bppZe6n4MuWrSonXfeefbrr7+697799ltr1qyZlS5d2ooXL+5+kGzx4sXZMp84MgTGABAjdCD2s8Nz5861FStWuAP2rFmzXIDZokULdyD+/PPP7csvv7QiRYq47Jf/mWeeecYFowpaFcgqyFaJRiRPPvmke3/q1Knue1555RWrVKlSxGF11UbfXbJkSXfgnzZtmn300UchQbd88sknLkDQ/y+88IJNnjzZPYBYtWzZMps/f74rZxIFxS+++KKNGzfOBcC9e/e26667zj799FP3/tq1a+388893wfXHH39sixYtsq5duwZKoHbu3Ol+zfeLL76wr776yv0I2cUXX+xeR5zQD3wA2a1Tp05e7ty5vcKFCwcebdu2TTGcNtm33norW6YRyOxtvnXr1u7vw4cPex9++KGXP39+r1+/fu69smXLevv37w8M/9JLL3lVq1Z1w/r0fsGCBb05c+a458cdd5w3aNCgVL8zeP+57bbbvCZNmoSML7Vhx48f75UsWdLbtWtX4P13333Xy5Url7d+/frA/Jx44onewYMHA8O0a9fOa9++/REvI+BoHmu0v2k713Y8ffp0b9++fV6hQoW8+fPnh3ymW7duXocOHdzfAwYM8CpXruwdOHAgqu87dOiQV7RoUe+dd96JuG/99ttv7vl3332XqfOJI5ftPwkN+Bo3buwyXr7ChQuzcJDQlAlW1lfZYF3Cveaaa1ydsbK+Z555ZiCLJUuXLnU3oypjHEw3DSlLu3HjRvv777/twgsvjOq7VfqgS74q01DWWZeGmzdvHnHYn3/+2V1SDt4nzz33XDfNyjaXLVvWvaYSDpU/+VRSocw1EIvHGl0JUY1xnjx57Morr3QZ4j179rj9IpiuyNSuXdv9rd4jVDqhG/ci2bBhg91zzz02b948t08eOnTIjXPNmjVZMm/47wiMETN00E3tsi+QyAdoBcCqJdYBOrUTQ9Xz1q1b15U8hCtTpozlypWxyrg6derYb7/9Zu+//74ri1Dtsmotw+spMyI8WFCdsoJnIFaPNarJ10nfhAkT7IwzznCvvfvuu1ahQoWQz/g3oqrcKS0qo/jnn39s9OjRrm5fn2vQoEGm3UCLo4/AGADi4GRQgeyUKVPs2GOPdTf9RKIaYdUmK+COhsbTvn1792jbtq3LHG/ZssXd2BfstNNOc7XCyrD5AbtqnBWM+zcGAvFI2/DAgQOtT58+tnLlShfIKrurm+YiqVGjhquf11WeSFlj7Rdjx451dcX+DbObN28+6vOBzMPNd4h5ypTp8pXfAbqyXPqbS1PISa699lp3p7t6otDNd9oPdLm2V69e9tdff7lhVIYxcuRId2OduoDT3fBPPfVUxPGNGjXKXnvtNVu+fLkLCHRDne7UV48Xkb5bPVcoG6ablXRz3W233WbXX399oIwCiFft2rVzJUDqtq1fv37uhjsFvypR8vchPRfdcLpjxw67+uqrbeHChW4/e+mll1xJkehmOz1X+dHXX3/t9p30ssyILWSMEfPU+ARnwHRmLzpIc8c7copChQq5H7vp37+/XXHFFe4ud13uVU2xn0HWPqGaY9VN6gCvQFqZ4EhUqzxixAh3YFdQcNZZZ9l7770XsSRD3z1nzhy7/fbb3XB6rppMBddAvFMJkwJe7Q864VRpknqnUD/HOlHU1RpllUXdEKo3ijvvvNNllbXv1KpVy9Xci0oy1CuMPqPu4IYNG+b2RcSPJN2Bl90TAQAAAGQ3SikAAAAAAmMAAAAgGRljAAAAgMAYAAAASEbGGAAAACAwBgAAAJKRMQYAAAAIjAEAAIBkZIwBAAAAAmMAAAAgGRljAAAAgMAYAAAASEbGGAAAACAwBgAAAJKRMQYAAAAIjAEAAIBkZIwBAAAAAmMAAAAgGRljAAAAgMAYAAAASEbGGAAAxLx//vnHjj32WPv999//03g6d+5sbdq0saxwwQUX2B133JFp49u8ebNbBn/99VemjROh8oQ9BwKS+r6TpUvDG9mKpZ9B119/vZ122mk2cODAmFp2s2fPtrvvvtsWL15suXJlz/n3uslJWfZd5Tt7ligqVarkDuRpHcwPHDhg1atXtxdffNEaNmzoXlu+fLkLOJYsWWLVqlWzt99+2ypXrmzfffed1apVK0umV9NVpUoVmz59utWrV8+yQ9Kkfln6fV6XxzI0vNbRtm3b3PqJFMRpXT3xxBMhr0+ePNktX33Ot2PHDhs+fLi98cYbLlAtUaKEnXHGGXbLLbfY5ZdfbklJSSnGp+effvqp+ztfvnxWunRpq1OnjnXp0sWuuOKKdKf9oYcestatW7t1HmvmzZtnjRs3tq1bt7pl4XvzzTctb968mfY9WmYdO3a0IUOG2IQJEzJtvPg/ZIwR19avX2+33XabnXTSSZY/f36rWLGitWrVyubOnRsYRo2oGmk9ChYs6J5fddVV9vHHH4eMS427htHZ+M6dO0PeU+N+3333hbz2448/uvGUKVPGfbcOyIMHD7Y9e/YEGkr/e1N7aJhDhw7ZI4884oIJTV+pUqXs7LPPtueffz7NeV+6dKm999571qtXL4s1LVu2dAeDV155JbsnJSYdaRYps7NPR2rcuHEu6PWDYtGBunDhwrZixQq3/2lfXLdunQuWsoqCrX79+ln//v2z7DtzIgXIWvc6MRowYIA7Af7ss8+sffv2dtddd9n27dtT/WyPHj3cdvHrr7+6oFonWFdffbXdcMMNaX6n2lUFgt26dbN4ova8aNGimTpOnUiobd2yZUumjhfJCIwRtxTI1q1b1wW4jz76qP3www8uU6mz9ltvvTVk2Pvvv981xjpoqzHXGX3Tpk1dBiKcguLHHks7C/PVV1+54FUZqnfffddWrlzpxqXMSrNmzdzrOnDoO/2HgmgFjMGvaZihQ4fa448/bg888ID99NNP9sknn7iDRHB2JpKnnnrK2rVrZ0WKFLFYpMzUk08+md2TkeN4nmcHDx48quN/+umnUwQoCnT+97//2YknnmjHHHOM5c6d28qVK2d58mTthclrr73WvvjiC3fiiqNDV6jU/n799dfWqVMnF9wqMaCgV1cM0mqTChUq5LaL448/3s455xyXdX722Wftueees48++ijVzykJoASEPuNTUkHboU7SlFSoWrWqjR49OuRzGqZPnz6uzdd2qcBd23AwHTe07frDXHrppW57Dk+avP76667NLlCggDvh87Pfel/HHSlZsqQbVu1f+MmslpuOG+Fq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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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nGlcY86cOdZlIIQQQmQZJ8QAZYZdxeVzE0IIITIhBoI9B8rl7LPPNr169bIpgPnceuutZt111zWjR4+2/8fnz3N9+vQxG2+8sdlhhx3Mqaeear755ptQQY0LFy4s+/VCCCFE2ki86BDuAVL5yoUo/z333NN0797d3HbbbbXPk5K49957m6233tpceeWV9rlTTjnFjBo1yuy7775mrbXWMlOmTDH33XefFQmPPPKI6dixY1mfyf4Fyy23XAXfTgghhHCfxC0DYX3y7DbIJP/222+bxx9/vPb5Sy65xFYQPOuss2qzAV588UXTv39/c95551lBcOyxx5orrrjCbnzE7+I6RyGEECJNpE4MABP7BhtsYIYOHWrN+M8884x57bXXzIABA8yKK65oX8OEH4iHXNq2bWsfl1xyyVCBhEIIIURWSdRNwEd/+eWXFfnk8fvvv//+Zvvttzfvv/++FQH33HOPady4cW3dgl133dX+7QsuuMC6FX7++WfrQiCN8YEHHjDLLLNM2Z+39tprm0aNGoU+TyGEEMJ1EhUD7AWAGKiUYcOG2QDBJk2aWCHQo0ePOr//5JNPrNvgp59+qn2O11x//fW1FoJyWX311bWBkRBCiEzSNMkPb2iRodatW9vHFVZYwU7W+bDyxyLQu3dvs95661lRgHg47bTTzPDhw0NN7i4VRBJCCCEyEzPQEKPEpEmTzA033GBWW201+/OIESPq/H7WrFnm0EMPNT179jQnn3yydSfwf9wEuBUee+yxqp2rEEII4TKJioGG+OAHDhxoH2+88Ua78r/lllvquAPIFiANcdttt63zPuoNtGzZ0nzwwQdVO1chhBDCZRIVA0GwX1heeuklWz+A7IH27dvbuIBmzZrVCgQIdh3MD05khc9zYYMWKz1XIYQQwnUSneGoC0DwXxhIGRw0aJAtInTQQQfZ59q1a2eFAemFzz33nH2uS5cu9vHZZ5+t8/6RI0fadEZiCcJYBZo3bx7qPIUQQoi0kHgFQsoKz549u+zXDx482Nx7773m7rvvNuuss07t86z0DzzwQGsReOKJJ+zk3bdvX/Ptt9/aioVBACHvJbDw4YcfLruqIDUJiE0QQgghskjitu+lllqq7Nd+9tlntpxwv3796ggBwMJw/vnnm6lTp5prr73Wug3uuOMOW4Hwo48+MpdddpmtWEgg4Z133hmqvPDSSy8d6jsJIYQQaSJxywBR/z/88INxGe1N8P9wucybN88WdSLdkv8TT4EYQ9iFdfuI4tC27KPBFt+5bc2BtQrBK4QQqa8zAET2M4G4ujMg8QJhKhVmDSYg3DiINsoyIwRKwQSFKGjRooWtAyFxEK6tiWf59ddfa9u6lFanbbFacdDWEgdCiNRaBoJNhSgV7CJt2rQpe3fDLEF1SPZ9IAYDK0ClQmrZZZe1+0OEcQf5Bqv+oK2xBFQKopXrFSGmVFghROrEAJPNV199ZVyEwMEwmxqlHS4HJiUEWpSXBhYg3C1avdZta6wAEyZMiLTCJddrp06dvLpuhRAZEANApD8Do0uwwuratavxBVal48aNi23LZnzdHTp0sCZt31euWF7Gjx9v3S9xweZd7MHhe1sLIVIkBhgcv/76a2f2AGAAZb8DX+oLTJ8+3a5Qq3E5tGrVyqy88sreFnIiBuPHH3+syrWOe4aaG9T0EEKIYjgzGjNYYUZ2BSob+iIESMdklVotXchqeOzYsc4GjcbJzJkzbW2NaolerDzU2iAjQQghnBcDQLCZC5H7RGcTiOUDxAew0VO1YZKq5qToAoggLAJJxOQgvioNBBVCZB+nxABgHUgy8AlrQOfOnb3ws7JKnThxYmKfjyAgVsQRT1Xs3zUJIRCAEPBNfAkhUiwGyJ1eZZVVzBJLLFH1zybSnc/2wb8aBLC5sFomrS7LMAETmJm06CFA1NUUXlf6idoO1HhgD5Sg1oMElPABZwII88GfzEomrsj2fBAfCAFfUt9YpWIZcAECCQnWzGrbu1ZHo1u3bqr78F9BzD3ApM84U6rGA+MD7kMCMnFnqpiWyBrOigFAkTOIxj2QUhSHNCxfotsZAJM0WRerQ4AYyxpBAJ9LMLEhCHy53vNh8v/ll18qtkipmJbIIk7bwxmsmKQJKsTM2pDqbIVgJUpxFuoJ+ALaj5Wqi+l2TJxZq1TokkUggPsIQUi9B5/A5I9rrKHWRu4hhAQH1gLinJJwawrhjWUgjpKtQZAg1gB2LvRtdYQvlMhyF2FyQpxlBZcrayK6sA74AEMc6bNxiWAsBSxaGFN8CDwW2SQ1MyGTNul+lAdeddVVQ/vteD8WBkzR+Ke5cX0TAoB5tFzOPvts06tXLxu7kc+tt95q1l13XTN69OjFfkeGAO/j92w7XS5UoMxS7QEKOcXR1mzH3bdvX7PllluajTfe2Oy5557mhhtusObvcmF1XN+mU1kRZLhp4rSGITZIz/3uu++UvilSS2osA4Xg1LnZGdQY3Pg5+DoodLICWAFx4BLwXbUTMPXll1+W/XqsMEw03bt3N7fddlvt87hs9t57b7P11lubK6+8crH3nXDCCeatt96yfXLfffeZtddeu+zPpFwxQi3tcB3S1uWKmzBt/Ze//MX06NHDVnHEPM3nPProo7ad77jjjrJFbtY34cKCiBWM675aMM5QwtyXgmUiOzgdM1AfTO7cdBwuFCtynbC+UiblU045xVx44YXm8ccfN3369LHPX3LJJVZonXXWWYu957XXXrPH4YcfboYPH16RGyMLYoCKf2GsHGHa+s4771zs/QiDIUOGmE8++cT07Nmz7DiNrEL7V1sIAAsSLAS4YLKaHSOyiX92co+pJHBq3333NRtssIEZOnSojdl45pln7GQ/YMAA6yfNHwgHDx5s+vfvbyenSghj6va5rfMJVvhhNj5iwsxiDj0iLAkhEMDn+lpuW6QXiQGPqGSCwvpy/vnn20nm4osvNpdffrk1Rx944IGLvfauu+6yUepHHXVUgwbSpAbxKKnEHx+mrWkjYhKmTJliXn/9dXPttdfarBhiC+I+T9ehqmbSpZcRWkmU+RbCSzeBCEelKVUEbR522GE2kI2gzeuvv34xvzTR2jfffLM57bTTbM2AhsAE1dC/keW2BgIzscAEEBiLICCwNux5kh6XFVyqaIlYoz/Sfi0LP5AY8IiGmC2DnPQVVljBZmPkc9VVV9m0QEzdDSUL5tWGWDfqa2vAJ01MBpP5hx9+aN58882KXCxZaOvc7+JCie1cCAClD1WxULiO3AQeUWniCOZOUtdYtfLziBEj6vz+o48+Mk8++aQ588wzI0nXzIIfu9LvUF9bB7Da3Hzzzc32229vTj31VHPooYeaE088MXRdgxQnExVMm3XNxRS4c4RwHYkBUS8DBw60jzfeeKPp3bu3ueWWW2wtgQBS3jbccENbiY2VGUdgqqUCX9idEbOQAlrpd6ivrYux44472keCDn0EUUN6potwXlkSXSKbSAx4RCUT1EsvvWRGjRplI9rbt29vU9xImQomLWAF+95775lddtml9iAiPqg5ENZ14KsYKKet68sMCJsumIW2DmIFyrUKhCnwhOuFIk877LCD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nTOu3WL89ttvtn/iGF+ohcP9GWehNq/EACabfM4++2zTq1evgubsW2+91ay77rpm9OjR9v/PPvusff3uu+9unz/88MMLfs4333xjTj31VLPLLruYjTfe2Gy99dbm0EMPNaNGjSp6bpj7XBgs42zruNo7n+HDh9vX77333gV/z2DKoCP+s9Lkeo276BYmUwbKJAczYkaOPfZYu8q87rrrzJtvvmk++eQT+7sXXnjBuEqQDUPQc5wTIm4h+gj3XlI88sgj5tJLLzXrrbeeufrqq83LL79srrzySjsmuBCIXKpezdiYU5gR6ggChIGrwqhxWga9Qhf5GWecYaM4L7744jrPc+Gxithpp53MNttsY5+7//77zciRI21EOn62YhCTwITTp08fc9ZZZ5mjjz7aPn/CCSeYBx98sOB76FyCtbIApjIG/0LE0d65cFMiKupzuRQ7P59gcPnuu+8itwbUN6lFvbItF+4v7rO99trLtG7d2n73ddZZx5qfXR1cmVyq2WYsSBDqDYkXaQjcvwTQnXfeedYs3rZtWyvsg3NzDa4brF7VTFOfPHly5Hu6eCUGit1MBGiccsop5u233zaPP/547fOXXHKJDdpgMg8YNGiQeeONN8xtt91m2rVrV/Sz/vSnP5mbbrrJrkL2228/079/fzNixAjrm7zzzjuLvs9Vs3pYmFyK5fTH0d65DB061K4q1l577ZKvS3L144o4ZpKptlmY64LPTcJCQBlwXAKHHXaYOfDAA80DDzxgXCYQT9USa7ngMsCtWm24d/lcXAN9+/Y17777rnGVQAgksYibOnWqFQSukQoxUMosvO+++5oNNtjATiR07DPPPGNee+01M2DAALPiiivWvo4VaqUBHATp8P5SN5jr/qByqe97xNXeDByYe88888wGCZaswworCSGQO4jy+QiSameSfPjhh+aVV16ptT65Cm30ww8/VL2N8gVBtd1pO+64o3Xf/Otf/zKug2stSWvu1KlTnYt/SkXOQykzG2ZCglb2339/a75+//337cqS1UNDP5NJh1Uo5u5XX33V7LzzziVNggzUad/EqD4xEEd7025YEvbZZx+zxhprlPUe+iZrGRzlmmKTLs+MEGNVRUpt1FHiTKQMlPhXicVhQss/aIPcDAJcUlyHwe8BEzUH1qzg59yD6Puw5/7pp59a10R94BdOenFAOxJY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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "g_lucid = np.asarray(res.cg_tig)\n", + "m_lucid = evaluate_graph(g_lucid, gt)\n", + "g_tet = np.asarray(tetrad_filter(df, g_base, max_lag, threshold=0.25))\n", + "m_tet = evaluate_graph(g_tet, gt)\n", + "\n", + "rows = [(\"CDNOTS (none)\", m_base, C_BASE),\n", + " (\"tetrad (fixed)\", m_tet, C_FIXED),\n", + " (\"LUCID (adaptive)\", m_lucid, C_LUCID)]\n", + "print(f\"{'method':<20}{'F1':>7}{'prec':>8}{'recall':>8}{'SHD':>6}\")\n", + "for name, m, _ in rows:\n", + " print(f\"{name:<20}{m['F1']:>7.3f}{m['Precision']:>8.3f}{m['TPR']:>8.3f}{m['SHD']:>6}\")\n", + "\n", + "metrics, labels = [\"F1\", \"Precision\", \"TPR\"], [\"F1\", \"Precision\", \"Recall\"]\n", + "x = np.arange(len(metrics)); w = 0.26\n", + "fig, ax = plt.subplots(figsize=(7.2, 3.6))\n", + "for i, (name, m, colour) in enumerate(rows):\n", + " vals = [m[k] for k in metrics]\n", + " bars = ax.bar(x + (i - 1) * w, vals, w * 0.88, label=name, color=colour)\n", + " for b, v in zip(bars, vals): # direct labels (contrast relief)\n", + " ax.annotate(f\"{v:.2f}\", xy=(b.get_x() + b.get_width() / 2, v),\n", + " xytext=(0, 3), textcoords=\"offset points\",\n", + " ha=\"center\", fontsize=9, color=\"#333333\")\n", + "ax.set_xticks(x); ax.set_xticklabels(labels)\n", + "ax.set_ylim(0, 1.12); ax.set_ylabel(\"score\")\n", + "ax.set_title(\"Deconfounding strategies on a pervasively confounded panel\")\n", + "ax.legend(frameon=False, ncols=3, loc=\"upper center\", bbox_to_anchor=(0.5, -0.12))\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "ax.grid(axis=\"y\", alpha=0.25); ax.set_axisbelow(True)\n", + "plt.tight_layout(); plt.show()\n", + "\n", + "res.plot(title=f\"LUCID - F1={m_lucid['F1']:.2f}\", node_layout=\"sfdp\", node_layout_kwargs=GV);" + ] + }, + { + "cell_type": "markdown", + "id": "7c4463a4", + "metadata": {}, + "source": [ + "Both corrections beat doing nothing here. But that margin is not the argument for\n", + "routing, and it is worth being precise about what the argument actually is.\n", + "\n", + "The claim is not \"LUCID wins a benchmark.\" It is that **applying the wrong correction\n", + "does damage**, and that a method which decides first avoids it. That is testable: take the\n", + "two towns from Part 3, where we know the right answer, and force each branch by hand." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "8968954a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:25.567609Z", + "iopub.status.busy": "2026-08-26T21:48:25.567515Z", + "iopub.status.idle": "2026-08-26T21:48:26.817762Z", + "shell.execute_reply": "2026-08-26T21:48:26.817260Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " no action routed (LUCID) forced pervasive forced sparse\n", + "quiet town (truly sparse) 0.18 0.67 0.29 0.67\n", + "busy town (truly pervasive) 0.05 0.11 0.11 0.06\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxIAAAFsCAYAAABYc49TAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAZtRJREFUeJzt3QncTOX///HPbd/3NfnSZqnsIlqUlHxL6FdJhRStSmhBRSiiLC0iQtq+SRttWtAivpStzZKIIlt2sp//4339fmf+M3PPfd8z3Mvc97yej8dwz5kzZ845c811rs+1nSTP8zwDAAAAgBjkimVlAAAAACCQAAAAAHBcaJEAAAAAEDMCCQAAAAAxI5AAAAAAEDMCCQAAAAAxI5AAAAAAEDMCCQAAAAAxI5AAAAAAEDMCCWSYl19+2ZKSkuz333/P0Wf5sccec8e5bdu2NNetWrWq3XzzzZYVtI/aV0Rn5syZVrduXStQoIA7dzt37sy0U5eV6SQl2p8iRYpk9W5ke8oPlZ6efvppy4607927dz+hY9e1AbH78ssv3fl7++23OX2kq7hBIJHg/MK+/1Ch6aSTTrKWLVvas88+a3v27MnqXYwbQ4YMsffffz+rdwOp2L9/vwuWdME9EX///bddd911VrBgQRszZoy9+uqrVrhw4Rx/7tPr/AE4fm+88YaNHj2aU8h5yRbyZPUOID4MGjTITjnlFDt8+LBt2rTJFSTuu+8+GzlypM2YMcNq164d8zY7duxo119/veXPn99ySiBxzTXXWNu2bY97GytXrrRcubImfv/nn38sT548Ob4gPHDgQPf3RRdddNzb+e6771wQPXjwYGvRooVltqxKJ+l1/oBIqlSp4vKhvHnzcoLSCCR++ukndw1G2ueFdJW1cnapAlFr1aqVNWzYMPC8b9++Nnv2bLvyyivtqquusuXLl7va2Vjkzp3bPfD/ZWVQpdam7EoF3EKFCmXa523ZssX9X6JEiXTb5r59+6Ju1cgpwXdWOnbsmB06dChbp/sTTUfxxm/1zo48z7MDBw7EfB1MNFlxnrJzusoJ6NqEFDVv3tweffRRW7dunb322mshrynIuOCCC9wFTYWtNm3auGAjrTES6vut4GTu3LnWqFEj9+M/9dRT7ZVXXkn2+T/88IM1a9bMZUgnn3yyPf744zZ58uSoxl34/bk3bNjgWhD0d9myZe3++++3o0ePJrsw9+7d2ypXruwKcNWrV3f9l5Uh+vSZWm/KlCmBbmDhfdjVh17LdD6KFy9uXbp0cQXg1Pq+++fo22+/tV69erl91Dlt166dbd26NVnBSN1O1PVMheqLL77Yfvnll6j704ePkVCNu2p29H4dd7ly5ezSSy+1xYsXp7gNfSfajlqpfIsWLXLL6tevnyw4bdy4cciyF154wc466yz3eTqOu+++O9nYA9WEn3322W67F154oTvWfv36ude+//571+2uTJkyLl2oFe2WW25xrylN6PyJatX978k/ZrW2rVixwv76669Uz5M+v3Pnzu7vc845J9l3PW3aNGvQoIH7fO3HTTfd5NJZpPT322+/2b///W8rWrSo3XjjjYHv8ZlnnrFatWq59K99vvzyy92xZWU6Sev8+aL5TWkf1DVD37WOsXz58nb77bfbjh07Uj33Slf6TKUz3zvvvOOWXX311SHr1qxZ09q3b5+s7/7rr78eSGMa5yJLlixx6bFYsWJuvy+55BL773//G7K9zPwt+kaNGuVqU5WWlNeptjXadPTNN9/Ytddea//617/csSr/6tmzp6vxj7SNaL+3tNKmT9089TvVZ+t8++c61jESagFXXqk8XtuqWLGiu56klsefSDo5cuSIa2k87bTT3OfpO1P+cvDgwZD3+deqTz/91FWy6Tt68cUX3Wuff/65nX/++S6v1/nUNcPPo3za3oABA+z0008PfD8PPvhgss+JlP989NFH7rrr/wa1L+Hf0xNPPOHOmb4npefVq1eHrJMR6SOS1M7TmjVr3D6UKlXK/U7OPfdcd2zRjKX0x4P43SxTOy+R0lUsx6WurOpBofxB36ny/2XLljGeJ0q0SCBV+nEpg/zss8+sW7dubtkXX3zhLsoKAHQxVcb03HPP2XnnnecKoeGZXjhleOoidOutt7of7KRJk9yPXoUzXZBEP35dnJU5qHVEF/SXXnoppppaZRYqdKowq8BA+z1ixAh3AbnzzjvdOgoW1OIyZ84ctz8aXKsM8YEHHnD7oAu9qI98165dXfBz2223uWXaTjD1qVfBdujQoe48aH9VOB82bFia+3rPPfdYyZIl3YVHmaIKYSoUTZ06NbCOzsPw4cOtdevW7riU0el/1f4cjzvuuMMN2tPnnHnmmS4zVYCngDA8KPCp4KCM9uuvv3bn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Aw884J155pnu72PHjnmlS5f2pk2b5p7XrVvXGzp0qFehQgX3fO7cuV7evHm9ffv2uecDBgzwChUq5O3evTtke40bN05z/wAgMynvrFmzpsvnfA899JBbFm2efc8993jNmzcP2UYw5aXDhg0LPG/durV38803p7hP2q62H2zVqlVuP7799tvAsm3btrnri3/NefbZZ72zzjrL/f3++++7PLdNmzbe2LFj3bIWLVp4/fr1CzmuRx55JPB87969btknn3ySyhkDYkeLBBAF1cKribtx48aBZaVKlbLq1auf0PlbtmyZzZkzx3Vr8h81atQIfOaJUq3VeeedF7JMz9VkfvTo0ZCaLrWOqHXirrvusoMHD7qarki1XKqNK1q0aOC5mte3bNlywvsKAOnt3HPPDenqqRZeP/+LhloK1BVVef29997rWqODqVVi8uTJ7m+1Jn/yySeuy1Os+bRaTYKvL6VLl3afqdfCW4+VL6uVWA/l3Wq1mDdvXkjLsdSuXTvwd+HChV1LCHk10huBBJCOdMEK7n8ryuRTsnfvXmvdurW7UAU/dKFTAT8z+Bejb775xnWx0sXGDy50wdIFLFjevHmTHfOxY8cyZV8BIDPz7Pr169vatWtt8ODBriuRugpdc801gdc1rmHNmjWuO9Jrr71mp5xyil1wwQXp/iXVqlXLVV4pTw4OJPT3d9995/ZZXaeCkVcjMxBIAFE47bTTXKa8YMGCwDL1U121alXIehoQF9xfVgGBP/gtEl2kfv75Z1fLf/rpp4c8VIMk+fLli6r2LNJ6NWvWtG+//TZkmZ5Xq1bNzSAVXNM1bdq0QI2W/v/iiy/cuuG1XACQXQTn2fLf//7XDTz2879o8mxVrmjMw4QJE2zq1KluAgt/rIFaDtq2betaJTT2TIOpjyef1qDw4H39+++/3ZiMM888MxDwKEDRGD1dM84//3zX4qDW4xdffNEaNmwYuGYAmYlAAoiCuhzdeuutbsD17Nmz3UxLavLOlSv0J6SB05oJacmSJW7g3B133JGsVijY3Xff7S5IHTp0cLVK6s706aefuouRf7FRkKELjGZr0mxOKdX+a72vv/7aNmzYEJj1qXfv3jZr1ixXm6agRwMPtX/+AD/RxUgzhbzxxhshgYQGVOsiFd41CgCyi/Xr11uvXr1cofw///mPPffcc9ajR4+o82wNztb7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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from causalts.confounders.routed_deconf import routed_deconfound\n", + "\n", + "gt_town = np.zeros((8, 8, 3), dtype=int)\n", + "gt_town[5, 6, 1] = 1 # the one real edge: Traffic(t-1) -> Rentals(t)\n", + "\n", + "towns = [(\"quiet town\\n(truly sparse)\", town_local), (\"busy town\\n(truly pervasive)\", town_wide)]\n", + "\n", + "\n", + "def no_action(town):\n", + " \"\"\"Plain discovery: the graph you get if you ignore confounding entirely.\"\"\"\n", + " r = run_cdnots(town, ParCorrGPU(town.values.copy()), num_lags=2, include_C=True,\n", + " c_preset=\"linear\", alpha=0.05, verbose=False)\n", + " return np.asarray(r.cg_tig)[:8, :8, :3]\n", + "\n", + "\n", + "arms = {\"no action\": None, # ignore confounding\n", + " \"routed (LUCID)\": {}, # let LUCID decide\n", + " \"forced pervasive\": dict(router=\"spectral\", thresholds=(0.0, 0.0)),\n", + " \"forced sparse\": dict(router=\"spectral\", thresholds=(9.9, 9.9))}\n", + "\n", + "scores = {}\n", + "for tname, town in towns:\n", + " for aname, kw in arms.items():\n", + " g = no_action(town) if kw is None else np.asarray(routed_deconfound(town, 2, **kw))\n", + " scores[(tname, aname)] = evaluate_graph(g, gt_town)[\"F1\"]\n", + "\n", + "print(f\"{'':30s}\" + \"\".join(f\"{a:>18s}\" for a in arms))\n", + "for tname, _ in towns:\n", + " print(f\"{tname.replace(chr(10), ' '):30s}\"\n", + " + \"\".join(f\"{scores[(tname, a)]:>18.2f}\" for a in arms))\n", + "\n", + "x = np.arange(len(towns)); w = 0.20\n", + "colours = [\"#8C8C8C\", C_LUCID, C_FIXED, C_BASE] # grey = do nothing\n", + "fig, ax = plt.subplots(figsize=(8.0, 3.8))\n", + "for i, (aname, colour) in enumerate(zip(arms, colours)):\n", + " vals = [scores[(t, aname)] for t, _ in towns]\n", + " bars = ax.bar(x + (i - 1.5) * w, vals, w * 0.88, label=aname, color=colour)\n", + " for b, v in zip(bars, vals):\n", + " ax.annotate(f\"{v:.2f}\", xy=(b.get_x() + b.get_width() / 2, v), xytext=(0, 3),\n", + " textcoords=\"offset points\", ha=\"center\", fontsize=9, color=\"#333333\")\n", + "ax.set_xticks(x); ax.set_xticklabels([t for t, _ in towns])\n", + "ax.set_ylabel(\"graph $F_1$\"); ax.set_ylim(0, 0.85)\n", + "ax.set_title(\"Doing nothing is worst; forcing the wrong branch is worse than routing\")\n", + "ax.legend(frameon=False, ncols=4, loc=\"upper center\", bbox_to_anchor=(0.5, -0.14))\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "ax.grid(axis=\"y\", alpha=0.25); ax.set_axisbelow(True)\n", + "plt.tight_layout(); plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "277406bc", + "metadata": {}, + "source": [ + "There is the argument, in one figure. Force the pervasive correction onto the quiet town\n", + "and its score collapses — the machinery attenuates directions that were carrying real\n", + "structure, because in that town nothing up there was confounding. Force the sparse\n", + "correction onto the busy town and it under-corrects, since every observed series it might\n", + "adjust against is contaminated by the same sun.\n", + "\n", + "The routed column is at or above the best arm in **both** towns, and it got there without\n", + "being told which town it was in.\n", + "\n", + "> **💡 The value of routing is what it prevents, not what it wins**\n", + ">\n", + "> On a benchmark you already know the regime, so any correction matched to it will look\n", + "> good. On your own data you do not know, and the cost of guessing wrong is the drop you\n", + "> see above. Deciding from the data is the whole point.\n", + ">\n", + "> One caveat on this figure: these towns are small and their absolute scores are low —\n", + "> eight series with a single true edge is a hard target. Read the *differences between\n", + "> the bars*, not the heights." + ] + }, + { + "cell_type": "markdown", + "id": "lucid-24", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part 7 — Using It On Your Own Data\n", + "\n", + "Two things make this practical. First, the router genuinely switches. Hand it the quiet\n", + "town from Part 3 and it takes the other branch:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "lucid-25", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:26.819214Z", + "iopub.status.busy": "2026-08-26T21:48:26.819118Z", + "iopub.status.idle": "2026-08-26T21:48:26.866436Z", + "shell.execute_reply": "2026-08-26T21:48:26.866098Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "quiet town (heat on 2 of 8) -> regime=sparse R=0.269 vs tau=0.374 pervasive factors detected=0\n", + "GARCH panel -> regime=pervasive R=0.499 vs tau=0.210 pervasive factors detected=3\n" + ] + } + ], + "source": [ + "res_local = run_lucid(town_local, 2)\n", + "print(f\"quiet town (heat on 2 of 8) -> regime={res_local.regime:<10} \"\n", + " f\"R={res_local.spectral_ratio:.3f} vs tau={res_local.tau:.3f} \"\n", + " f\"pervasive factors detected={res_local.n_factors}\")\n", + "print(f\"GARCH panel -> regime={res.regime:<10} \"\n", + " f\"R={res.spectral_ratio:.3f} vs tau={res.tau:.3f} \"\n", + " f\"pervasive factors detected={res.n_factors}\")" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-26", + "metadata": {}, + "source": [ + "Exactly the split Part 3 showed, now made by the method rather than by eye: the quiet\n", + "town falls short of its threshold and is routed `sparse`, while the GARCH panel clears\n", + "its own by more than twice. Same call, same defaults, different branch — decided from\n", + "the data.\n", + "\n", + "> **⚠️ `pervasive factors detected = 0` does not mean \"no confounder\"**\n", + ">\n", + "> We *built* the quiet town with hidden heat, and the field still reads zero. It is not\n", + "> claiming the town is unconfounded — it is reporting that no **pervasive factor\n", + "> direction** cleared the diagnostic. The heat is there; it simply touches too little of\n", + "> the system to register as a factor. Read the field as \"nothing large and system-wide\n", + "> here\", never as \"nothing hidden here\".\n", + "\n", + "Second, LUCID is a **layer**, not a replacement. If you already ran discovery, reuse it —\n", + "both branches use the same base engine, so the result is identical and you skip the\n", + "expensive step:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "lucid-27", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-26T21:48:26.867871Z", + "iopub.status.busy": "2026-08-26T21:48:26.867782Z", + "iopub.status.idle": "2026-08-26T21:48:27.269662Z", + "shell.execute_reply": "2026-08-26T21:48:27.269367Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "type : LucidResult\n", + "regime : pervasive\n", + "identical to run_lucid(df): True\n" + ] + } + ], + "source": [ + "reused = base.deconfound() # `base` is the CdnotsResult from Part 4\n", + "\n", + "print(f\"type : {type(reused).__name__}\")\n", + "print(f\"regime : {reused.regime}\")\n", + "print(f\"identical to run_lucid(df): \"\n", + " f\"{np.array_equal(np.asarray(reused.cg_tig), np.asarray(res.cg_tig))}\")" + ] + }, + { + "cell_type": "markdown", + "id": "lucid-28", + "metadata": {}, + "source": [ + "`.deconfound()` lives on the shared result base class, so it works on a `CdnotsResult`,\n", + "`CedarResult` or `GraceResult` alike. The filters are available the same way and return\n", + "the *same* result type, so they chain: `result.tetrad_filter().deconfound()`." + ] + }, + { + "cell_type": "markdown", + "id": "lucid-29", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part 8 — Summary and Next Steps\n", + "\n", + "**The failure.** Causal sufficiency is the assumption most likely to be false, because it\n", + "concerns data you never collected. When it breaks, discovery fabricates edges between the\n", + "variables a hidden cause drives — and the resulting graph stops answering interventional\n", + "questions. Part 2 measured the gap: a large observational slope against an interventional\n", + "slope of essentially zero.\n", + "\n", + "**Why it needs a diagnosis.** Sparse and pervasive confounding leave different spectral\n", + "fingerprints and demand opposite corrections, each harmful in the other's regime. No\n", + "fixed rule — including \"drop all lag-0 edges\" — covers both.\n", + "\n", + "**What LUCID does.** `run_lucid(df, max_lag)` reads the residual spectrum against the\n", + "Marchenko–Pastur null, then applies the matching correction. Inspect the decision through\n", + "`.regime`, `.spectral_ratio`, `.tau` and `.n_factors`, and reuse existing work with\n", + "`result.deconfound()`.\n", + "\n", + "**What it does not do.** It does not identify the latent variables; `factor_loadings` is\n", + "descriptive. It reads second-order structure, so nonlinear factor mixing or factors too\n", + "weak to clear the null can be misread. And where genuine lag-0 structure overlaps the\n", + "factor directions, trimming attenuates signal along with confounding.\n", + "\n", + "Deconfounding improves a graph. It does not turn a discovered graph into an experiment —\n", + "treat edges as hypotheses to corroborate.\n", + "\n", + "**Next steps**\n", + "\n", + "- [`beginers_guide.ipynb`](beginers_guide.ipynb) — the assumptions this notebook stresses\n", + "- [`effect_estimation.ipynb`](effect_estimation.ipynb) — estimating effects once you\n", + " trust a graph\n", + "- [`api_reference.ipynb`](api_reference.ipynb) — the full `causalts` surface" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_confounders.py b/tests/test_confounders.py new file mode 100644 index 0000000..9f0013e --- /dev/null +++ b/tests/test_confounders.py @@ -0,0 +1,378 @@ +# Copyright 2025 Bloomberg Finance L.P. +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Contract / smoke tests for LUCID regime-adaptive deconfounding. + +Covers the public API of :mod:`causalts.confounders` (``run_lucid``, ``LucidResult``, +``routed_deconfound``, the post-hoc ``deconfound`` layer and the standalone filters), +the result-object methods on :class:`causalts.result.CausalResult`, plus the +``apply_confounding`` synthetic-data helper. +Kept CPU-fast and seed-fixed; numeric performance claims live in +``experiments/confounders`` (the paper's reproducibility harness), not here. +""" + +import numpy as np +import pandas as pd +import pytest + +from causalts.cdnots.phase3_utils import run_cdnots +from causalts.cedar.discovery import run_cedar +from causalts.cedar.result import CedarResult +from causalts.ci_tests.parcorr_gpu import ParCorrGPU +from causalts.confounders import ( + LucidResult, + deconfound, + routed_deconfound, + run_lucid, + tetrad_filter, +) +from causalts.confounders.routed_deconf import routed_deconfound_lucid +from causalts.grace.gated_discovery import run_cdnots_gated +from causalts.grace.result import GraceResult +from causalts.synthetic_data.confounding import apply_confounding +from causalts.synthetic_data.synthetic_datasets import SCPGraphGenerator + +D, T, MAX_LAG = 6, 500, 1 +VALID_REGIMES = {"sparse", "sf", "pervasive"} + + +def _sparse_data(d=D, T=T, seed=0): + """Full-rank innovations: a sparse lag-1 chain, no pervasive factor.""" + rng = np.random.default_rng(seed) + X = rng.standard_normal((T, d)) + for t in range(1, T): + X[t, 1] += 0.6 * X[t - 1, 0] + X[t, 3] += 0.6 * X[t - 1, 2] + return pd.DataFrame(X, columns=[f"X{i}" for i in range(d)]) + + +def _pervasive_data(d=D, T=T, n_factors=2, seed=0): + """Low-rank innovations: a few dominant latent factors drive every variable, + on top of two genuine lag-1 edges.""" + rng = np.random.default_rng(seed) + factors = rng.standard_normal((T, n_factors)) + loadings = rng.standard_normal((n_factors, d)) + X = 2.0 * (factors @ loadings) + X[1:, 1] += 0.6 * X[:-1, 0] + X[1:, 3] += 0.6 * X[:-1, 2] + X += 0.3 * rng.standard_normal((T, d)) + return pd.DataFrame(X, columns=[f"X{i}" for i in range(d)]) + + +def _assert_valid_graph(g, d=D, max_lag=MAX_LAG): + assert isinstance(g, np.ndarray) + assert g.shape == (d, d, max_lag + 1) + assert np.issubdtype(g.dtype, np.integer) + assert set(np.unique(g)).issubset({0, 1}) + + +def test_routed_deconfound_sparse_branch(): + """Full-rank data routes to the sparse regime and returns a valid graph.""" + df = _sparse_data() + g, info = routed_deconfound(df, MAX_LAG, return_info=True) + _assert_valid_graph(g) + assert info["regime"] in VALID_REGIMES + assert info["regime"] == "sparse" + + +def test_routed_deconfound_pervasive_branch(): + """Dominant-factor data routes off the sparse branch (pervasive family).""" + df = _pervasive_data() + g, info = routed_deconfound(df, MAX_LAG, return_info=True) + _assert_valid_graph(g) + assert info["regime"] in VALID_REGIMES + assert info["regime"] != "sparse" + # pervasive branch records the gate outcome + assert "pervasive_filters" in info + + +def test_routed_deconfound_default_return_is_bare_graph(): + """Without return_info the call returns just the graph (no tuple).""" + g = routed_deconfound(_sparse_data(), MAX_LAG) + _assert_valid_graph(g) + + +@pytest.mark.parametrize("profile", ["adaptive", "unconditional"]) +def test_profiles_run(profile): + g = routed_deconfound(_pervasive_data(), MAX_LAG, profile=profile) + _assert_valid_graph(g) + + +@pytest.mark.parametrize("router", ["auto", "spectral", "mp"]) +def test_routers_run(router): + g, info = routed_deconfound( + _pervasive_data(), MAX_LAG, router=router, return_info=True + ) + _assert_valid_graph(g) + assert info["router"] == router + + +@pytest.mark.parametrize("regime", sorted(VALID_REGIMES)) +def test_deconfound_posthoc_layer_all_regimes(regime): + """The post-hoc filter layer runs for every regime on a supplied graph and + never adds edges (a filter can only remove).""" + df = _pervasive_data() + g_in = np.ones((D, D, MAX_LAG + 1), dtype=np.int8) + for i in range(D): # self-loops are not meaningful inputs to the filters + g_in[i, i, :] = 0 + g_out = deconfound(g_in, df, MAX_LAG, regime) + _assert_valid_graph(g_out) + assert g_out.sum() <= g_in.sum() + + +def test_invalid_profile_raises(): + with pytest.raises(ValueError): + routed_deconfound(_sparse_data(), MAX_LAG, profile="nope") + + +def test_invalid_router_raises(): + with pytest.raises(ValueError): + routed_deconfound(_sparse_data(), MAX_LAG, router="nope") + + +def test_invalid_regime_raises(): + g = np.zeros((D, D, MAX_LAG + 1), dtype=np.int8) + with pytest.raises(ValueError): + deconfound(g, _sparse_data(), MAX_LAG, regime="nope") + + +def test_apply_confounding_removes_nodes(): + """apply_confounding drops eligible nodes (>=2 distinct children) as latent + confounders, shrinking the observed panel and its ground-truth graph + consistently.""" + rng = np.random.default_rng(0) + d = 6 + gt = np.zeros((d, d, 2), dtype=np.int8) + gt[0, 1, 1] = gt[0, 2, 1] = 1 # node 0 is a fork -> eligible confounder + gt[3, 4, 1] = 1 + var_names = [f"X{i}" for i in range(d)] + sample = { + "df": pd.DataFrame(rng.standard_normal((300, d)), columns=var_names), + "ground_truth": gt, + "var_names": var_names, + "max_lag": 1, + } + out = apply_confounding(sample, confound_fraction=1.0, seed=0) + assert out["n_eligible"] >= 1 + assert out["n_confounders"] >= 1 + assert out["df"].shape[1] == d - out["n_confounders"] + assert out["ground_truth"].shape[0] == out["df"].shape[1] + assert len(out["confounder_nodes"]) == out["n_confounders"] + + +if __name__ == "__main__": + import sys + + sys.exit(pytest.main([__file__, "-v"])) + + +# ── run_lucid / LucidResult ────────────────────────────────────────────────── +def _cdnots_result(df, max_lag=MAX_LAG, alpha=0.05): + ci = ParCorrGPU(df.values.copy()) + return run_cdnots( + df, + ci, + num_lags=max_lag, + include_C=True, + c_preset="linear", + alpha=alpha, + verbose=False, + ) + + +def test_run_lucid_returns_populated_result(): + res = run_lucid(_pervasive_data(), MAX_LAG) + assert isinstance(res, LucidResult) + _assert_valid_graph(np.asarray(res.cg_tig)) + assert res.regime in VALID_REGIMES + assert res.var_names == [f"X{i}" for i in range(D)] + assert 0.0 <= res.spectral_ratio and res.tau > 0 + assert res.n_factors is not None and res.n_factors >= 0 + assert res.runtime >= 0 + assert "regime" in res.info + + +def test_lucid_result_factor_loadings_shape_matches_n_factors(): + res = run_lucid(_pervasive_data(), MAX_LAG) + if res.n_factors: + assert res.factor_loadings.shape == (res.n_factors, D) + top = res.top_factor_variables(0, 3) + assert len(top) == 3 and all(v in res.var_names for v, _ in top) + else: + assert res.factor_loadings is None + + +def test_routed_deconfound_defaults_are_lucid(): + """The low-level entry point's defaults must BE the shipped method.""" + df = _pervasive_data() + assert np.array_equal( + np.asarray(routed_deconfound(df, MAX_LAG)), + np.asarray(routed_deconfound_lucid(df, MAX_LAG)), + ) + + +# ── graph reuse: .deconfound() ─────────────────────────────────────────────── +def test_deconfound_matches_run_lucid(): + """Reusing a discovered skeleton must be EXACT, not merely similar. + + Regression test: an earlier cut passed ``cg_tig`` unsliced, so the C-node + rows/columns leaked into the reused graph and the results diverged. + """ + df = _pervasive_data() + res = run_lucid(df, MAX_LAG) + reused = _cdnots_result(df).deconfound() + assert isinstance(reused, LucidResult) + assert np.array_equal(np.asarray(reused.cg_tig), np.asarray(res.cg_tig)) + + +def test_run_lucid_accepts_raw_array_discovery(): + df = _sparse_data() + graph = np.asarray(_cdnots_result(df).cg_tig)[:D, :D, : MAX_LAG + 1] + res = run_lucid(df, MAX_LAG, discovery=graph) + _assert_valid_graph(np.asarray(res.cg_tig)) + + +def test_deconfound_num_lags_mismatch_raises(): + df = _sparse_data() + res2 = _cdnots_result(df, max_lag=2) + with pytest.raises(ValueError, match="num_lags"): + run_lucid(df, MAX_LAG, discovery=res2) + + +def test_deconfound_alpha_mismatch_warns(): + df = _sparse_data() + odd = _cdnots_result(df, alpha=0.2) + with pytest.warns(UserWarning, match="alpha"): + run_lucid(df, MAX_LAG, discovery=odd) + + +# ── result-object filter methods ───────────────────────────────────────────── +def test_tetrad_filter_method_returns_same_type_and_only_removes(): + cd = _cdnots_result(_pervasive_data()) + out = cd.tetrad_filter() + assert type(out) is type(cd) + assert out.cg_tig.sum() <= cd.cg_tig.sum() + assert cd.cg_tig.sum() == cd.cg_tig.sum() # original untouched + + +def test_tetrad_filter_method_matches_function(): + df = _pervasive_data() + cd = _cdnots_result(df) + method = np.asarray(cd.tetrad_filter(threshold=0.25).cg_tig) + func = np.asarray(tetrad_filter(df, cd.cg_tig, MAX_LAG, threshold=0.25)) + assert np.array_equal(method, func) + + +def test_filters_chain_into_deconfound(): + cd = _cdnots_result(_pervasive_data()) + out = cd.tetrad_filter().deconfound() + assert isinstance(out, LucidResult) + + +def test_pds_filter_method_only_removes(): + cd = _cdnots_result(_sparse_data()) + before = np.asarray(cd.cg_tig)[:D, :D, : MAX_LAG + 1].sum() + out = cd.pds_filter(alpha=1e-10) + assert type(out) is type(cd) + assert out.cg_tig.sum() <= before + + +# ── exposed constants ──────────────────────────────────────────────────────── +def test_exposed_constants_at_defaults_reproduce_shipped_output(): + """Passing each newly exposed knob at its default must change nothing.""" + df = _pervasive_data() + base = np.asarray(routed_deconfound(df, MAX_LAG)) + for kw in ( + {"router_k": 2}, + {"gamma": 1.3}, + {"factor_count_margin": 1.02}, + {"tetrad_threshold": 0.25}, + ): + assert np.array_equal( + np.asarray(routed_deconfound(df, MAX_LAG, **kw)), base + ), kw + + +def test_router_k_is_recorded_in_info(): + _, info = routed_deconfound( + _pervasive_data(), MAX_LAG, router_k=3, return_info=True + ) + assert info["router_k"] == 3 and info["gamma"] == 1.3 + + +# ── unsupported combinations fail loudly ───────────────────────────────────── +def test_invalid_lag0_engine_raises(): + with pytest.raises(ValueError, match="lag0_engine"): + routed_deconfound(_pervasive_data(), MAX_LAG, lag0_engine="adjudicate") + + +def test_keep_undirected_with_tetrad_base_raises(): + with pytest.raises(NotImplementedError, match="keep_undirected"): + routed_deconfound( + _pervasive_data(), MAX_LAG, pervasive_base="tetrad", keep_undirected=True + ) + + +# ── cross-type: .deconfound()/.tetrad_filter()/.pds_filter() on non-CDNOTS results ── +# _with_graph() shallow-copies `self` rather than reconstructing via __init__, so it +# should generalize to any CausalResult subclass without knowing that subclass's own +# fields (GraceResult.gate_values, CedarResult's internals, ...). Assert that directly +# instead of only ever exercising it against CdnotsResult. +# +# module-scoped: deconfound()/tetrad_filter()/pds_filter() copy before mutating (see +# _with_graph, apply_tetrad_lag0_filter, pds_filter), so the same discovery result can +# be reused across every test below instead of re-running GRACE/CEDAR discovery per test. +@pytest.fixture(scope="module") +def grace_result(): + gen = SCPGraphGenerator(n_vars=6, max_lag=1) + data = gen.sample(seed=1, T=400) + return run_cdnots_gated( + df=data["df"], + max_lag=data["max_lag"], + verbose=False, + device="cpu", + model_seed=1, + ) + + +@pytest.fixture(scope="module") +def cedar_result(): + gen = SCPGraphGenerator(n_vars=6, max_lag=1) + data = gen.sample(seed=2, T=400) + ci = ParCorrGPU(data["df"].values.copy()) + return run_cedar(data["df"], ci, data["max_lag"]) + + +@pytest.fixture(params=["grace_result", "cedar_result"]) +def non_cdnots_result(request): + return request.getfixturevalue(request.param) + + +def test_deconfound_generalizes_to_non_cdnots_results(non_cdnots_result): + res = non_cdnots_result + assert isinstance(res, (GraceResult, CedarResult)) + out = res.deconfound() + assert isinstance(out, LucidResult) + assert out.regime in VALID_REGIMES + + +def test_filters_generalize_to_non_cdnots_results(non_cdnots_result): + res = non_cdnots_result + same_type = type(res) + assert same_type in (GraceResult, CedarResult) + + tet = res.tetrad_filter() + assert type(tet) is same_type + assert tet.cg_tig.sum() <= res.cg_tig.sum() + + pds = res.pds_filter() + assert type(pds) is same_type + assert pds.cg_tig.sum() <= res.cg_tig.sum() + + assert isinstance(res.tetrad_filter().deconfound(), LucidResult) + + +def test_grace_specific_fields_survive_the_shallow_copy(grace_result): + """A subclass's own fields (not on the CausalResult base) must not be dropped.""" + filtered = grace_result.tetrad_filter() + assert filtered.gate_values is grace_result.gate_values