antecedent.gcm

Discover-then-fit GCM composition helpers.

Attribution never discovers structure internally (ADR 0012/0015). These helpers compose discover_*discovery_to_dagfit_gcm / attribute_*.

  1"""Discover-then-fit GCM composition helpers.
  2
  3Attribution never discovers structure internally (ADR 0012/0015). These helpers
  4compose ``discover_*`` → ``discovery_to_dag`` → ``fit_gcm`` / ``attribute_*``.
  5"""
  6
  7from __future__ import annotations
  8
  9from typing import Any, Sequence
 10
 11from ._data import as_columns
 12from ._native import (
 13    anomaly_attribution,
 14    attribute_distribution_change,
 15    attribute_paths,
 16    fit_gcm,
 17)
 18from .discovery import (
 19    FCI,
 20    GES,
 21    LiNGAM,
 22    NOTEARS,
 23    PC,
 24    RFCI,
 25    discover_ges,
 26    discover_lingam,
 27    discover_notears,
 28    discover_pc,
 29    discovery_to_dag,
 30)
 31
 32
 33def _run_static_discovery(data, discovery, *, seed: int, threads: int):
 34    if isinstance(discovery, PC):
 35        return discover_pc(
 36            data, alpha=discovery.alpha, fdr=discovery.fdr, seed=seed, threads=threads
 37        ), "pc"
 38    if isinstance(discovery, GES):
 39        return discover_ges(
 40            data, alpha=discovery.alpha, fdr=discovery.fdr, seed=seed, threads=threads
 41        ), "ges"
 42    if isinstance(discovery, LiNGAM):
 43        return discover_lingam(data, seed=seed, threads=threads), "lingam"
 44    if isinstance(discovery, NOTEARS):
 45        return discover_notears(data, seed=seed, threads=threads), "notears"
 46    if isinstance(discovery, (FCI, RFCI)):
 47        algo = "fci" if isinstance(discovery, FCI) else "rfci"
 48        raise ValueError(
 49            f"{algo}: fit_gcm_discovered requires a fully oriented DAG; "
 50            "use PC/GES/LiNGAM/NOTEARS, or orient the PAG and call fit_gcm directly"
 51        )
 52    raise TypeError(f"unsupported discovery type for GCM compose: {type(discovery)!r}")
 53
 54
 55def fit_gcm_discovered(
 56    data: Any,
 57    *,
 58    discovery: PC | GES | LiNGAM | NOTEARS,
 59    seed: int = 1,
 60    threads: int = 1,
 61):
 62    """Discover structure, coerce to a DAG, then ``fit_gcm``.
 63
 64    Returns ``(fitted_gcm, graph_edges)``. Incomplete CPDAG/PAG marks raise
 65    ``ValueError`` (orientations are never invented). Structure provenance is
 66    the caller-supplied ``discovery`` algorithm — attribution does not discover.
 67    """
 68    result, _algo = _run_static_discovery(data, discovery, seed=seed, threads=threads)
 69    dag = discovery_to_dag(result)
 70    names, columns = as_columns(data)
 71    edges = list(dag.edges())
 72    fitted = fit_gcm(names, columns, edges, threads=threads)
 73    return fitted, edges
 74
 75
 76def attribute_paths_discovered(
 77    data: Any,
 78    *,
 79    discovery: PC | GES | LiNGAM | NOTEARS,
 80    sources: Sequence[str],
 81    outcome: str,
 82    max_paths: int = 64,
 83    max_len: int = 16,
 84    seed: int = 1,
 85    threads: int = 1,
 86):
 87    """``fit_gcm_discovered`` then ``attribute_paths``. Returns ``(result, graph_edges)``."""
 88    fitted, edges = fit_gcm_discovered(
 89        data, discovery=discovery, seed=seed, threads=threads
 90    )
 91    _ = fitted
 92    names, columns = as_columns(data)
 93    result = attribute_paths(
 94        names,
 95        columns,
 96        edges,
 97        list(sources),
 98        outcome,
 99        max_paths=max_paths,
100        max_len=max_len,
101        seed=seed,
102        threads=threads,
103    )
104    return result, edges
105
106
107def anomaly_attribution_discovered(
108    data: Any,
109    *,
110    discovery: PC | GES | LiNGAM | NOTEARS,
111    outcomes: Sequence[str],
112    max_units: int = 0,
113    seed: int = 1,
114    threads: int = 1,
115):
116    """``fit_gcm_discovered`` then ``anomaly_attribution``. Returns ``(result, graph_edges)``."""
117    fitted, edges = fit_gcm_discovered(
118        data, discovery=discovery, seed=seed, threads=threads
119    )
120    _ = fitted
121    names, columns = as_columns(data)
122    result = anomaly_attribution(
123        names, columns, edges, list(outcomes), max_units=max_units
124    )
125    return result, edges
126
127
128def attribute_distribution_change_discovered(
129    data: Any,
130    *,
131    discovery: PC | GES | LiNGAM | NOTEARS,
132    outcome: str,
133    baseline_start: int,
134    baseline_end: int,
135    comparison_start: int,
136    comparison_end: int,
137    n_samples: int = 500,
138    seed: int = 1,
139    threads: int = 1,
140):
141    """Compose discover → DAG → ``attribute_distribution_change``."""
142    fitted, edges = fit_gcm_discovered(
143        data, discovery=discovery, seed=seed, threads=threads
144    )
145    _ = fitted
146    names, columns = as_columns(data)
147    result = attribute_distribution_change(
148        names,
149        columns,
150        edges,
151        outcome,
152        baseline_start,
153        baseline_end,
154        comparison_start,
155        comparison_end,
156        n_samples=n_samples,
157        seed=seed,
158        threads=threads,
159    )
160    return result, edges
161
162
163__all__ = [
164    "anomaly_attribution_discovered",
165    "attribute_distribution_change_discovered",
166    "attribute_paths_discovered",
167    "fit_gcm_discovered",
168]
def anomaly_attribution_discovered( data: Any, *, discovery: antecedent.PC | antecedent.GES | antecedent.LiNGAM | antecedent.NOTEARS, outcomes: Sequence[str], max_units: int = 0, seed: int = 1, threads: int = 1):
108def anomaly_attribution_discovered(
109    data: Any,
110    *,
111    discovery: PC | GES | LiNGAM | NOTEARS,
112    outcomes: Sequence[str],
113    max_units: int = 0,
114    seed: int = 1,
115    threads: int = 1,
116):
117    """``fit_gcm_discovered`` then ``anomaly_attribution``. Returns ``(result, graph_edges)``."""
118    fitted, edges = fit_gcm_discovered(
119        data, discovery=discovery, seed=seed, threads=threads
120    )
121    _ = fitted
122    names, columns = as_columns(data)
123    result = anomaly_attribution(
124        names, columns, edges, list(outcomes), max_units=max_units
125    )
126    return result, edges

fit_gcm_discovered then anomaly_attribution. Returns (result, graph_edges).

def attribute_distribution_change_discovered( data: Any, *, discovery: antecedent.PC | antecedent.GES | antecedent.LiNGAM | antecedent.NOTEARS, outcome: str, baseline_start: int, baseline_end: int, comparison_start: int, comparison_end: int, n_samples: int = 500, seed: int = 1, threads: int = 1):
129def attribute_distribution_change_discovered(
130    data: Any,
131    *,
132    discovery: PC | GES | LiNGAM | NOTEARS,
133    outcome: str,
134    baseline_start: int,
135    baseline_end: int,
136    comparison_start: int,
137    comparison_end: int,
138    n_samples: int = 500,
139    seed: int = 1,
140    threads: int = 1,
141):
142    """Compose discover → DAG → ``attribute_distribution_change``."""
143    fitted, edges = fit_gcm_discovered(
144        data, discovery=discovery, seed=seed, threads=threads
145    )
146    _ = fitted
147    names, columns = as_columns(data)
148    result = attribute_distribution_change(
149        names,
150        columns,
151        edges,
152        outcome,
153        baseline_start,
154        baseline_end,
155        comparison_start,
156        comparison_end,
157        n_samples=n_samples,
158        seed=seed,
159        threads=threads,
160    )
161    return result, edges

Compose discover → DAG → attribute_distribution_change.

def attribute_paths_discovered( data: Any, *, discovery: antecedent.PC | antecedent.GES | antecedent.LiNGAM | antecedent.NOTEARS, sources: Sequence[str], outcome: str, max_paths: int = 64, max_len: int = 16, seed: int = 1, threads: int = 1):
 77def attribute_paths_discovered(
 78    data: Any,
 79    *,
 80    discovery: PC | GES | LiNGAM | NOTEARS,
 81    sources: Sequence[str],
 82    outcome: str,
 83    max_paths: int = 64,
 84    max_len: int = 16,
 85    seed: int = 1,
 86    threads: int = 1,
 87):
 88    """``fit_gcm_discovered`` then ``attribute_paths``. Returns ``(result, graph_edges)``."""
 89    fitted, edges = fit_gcm_discovered(
 90        data, discovery=discovery, seed=seed, threads=threads
 91    )
 92    _ = fitted
 93    names, columns = as_columns(data)
 94    result = attribute_paths(
 95        names,
 96        columns,
 97        edges,
 98        list(sources),
 99        outcome,
100        max_paths=max_paths,
101        max_len=max_len,
102        seed=seed,
103        threads=threads,
104    )
105    return result, edges

fit_gcm_discovered then attribute_paths. Returns (result, graph_edges).

def fit_gcm_discovered( data: Any, *, discovery: antecedent.PC | antecedent.GES | antecedent.LiNGAM | antecedent.NOTEARS, seed: int = 1, threads: int = 1):
56def fit_gcm_discovered(
57    data: Any,
58    *,
59    discovery: PC | GES | LiNGAM | NOTEARS,
60    seed: int = 1,
61    threads: int = 1,
62):
63    """Discover structure, coerce to a DAG, then ``fit_gcm``.
64
65    Returns ``(fitted_gcm, graph_edges)``. Incomplete CPDAG/PAG marks raise
66    ``ValueError`` (orientations are never invented). Structure provenance is
67    the caller-supplied ``discovery`` algorithm — attribution does not discover.
68    """
69    result, _algo = _run_static_discovery(data, discovery, seed=seed, threads=threads)
70    dag = discovery_to_dag(result)
71    names, columns = as_columns(data)
72    edges = list(dag.edges())
73    fitted = fit_gcm(names, columns, edges, threads=threads)
74    return fitted, edges

Discover structure, coerce to a DAG, then fit_gcm.

Returns (fitted_gcm, graph_edges). Incomplete CPDAG/PAG marks raise ValueError (orientations are never invented). Structure provenance is the caller-supplied discovery algorithm — attribution does not discover.