ergodicDocs
Products: switch from ergodic to another product
Get started

ReferenceDiscovery

Discovery

Causal discovery: learn causal structure from data. Import from ergodic.discovery.

Two ingredients drive the searches. A conditional-independence test (CITest) is a class constructed with data, which makes it the stateful oracle the constraint algorithms query: Linear (partial correlation), Discrete (G squared, natively categorical), Mixed (degenerate Gaussian for mixed columns), GCM (any regression Learner becomes a test), and DSeparationOracle as ground truth. A decomposable Score answers local(node, parents) for the score-based searches: BIC (Gaussian), DiscreteBIC, BDeu, and MixedBIC (degenerate Gaussian). Algorithms are thin consumers: PC wraps a test and returns a CPDAG, FCI wraps a test and returns a PAG (latent confounders allowed), MMPC wraps a test and returns an undirected skeleton screen, GES wraps a score and returns a CPDAG, AStar wraps a score and returns the exact optimum, and HillClimb is the fast greedy baseline. discover is the one-line facade, and bootstrap() reruns any method on resamples and returns edge frequencies (BootstrapResult), the first soft output. EdgeProbabilities holds a model's directed-edge probabilities (or bootstrap frequencies) over named variables, and decode() turns one into a DAG with an explicit, versioned decoder (DecodeResult). Background knowledge (including a skeleton restriction through restrict_to) enters through DomainKnowledge.

Time series and panels get their own methods: PCMCI reads a TimeSeriesData or PanelData through a lagged design matrix (lagged_tabular()) and returns a window graph over nodes like "X[t-1]" (TimeSeriesDiscoveryResult), and granger() is the predictive baseline, a pair-level table on purpose rather than a graph.

Also exported from ergodic: DecodeResult, EdgeProbabilities, decode, discover, granger.

BIC#

python
class BIC(data: TabularData | None = None, *, penalty: float = 1.0) -> None

The Gaussian BIC, decomposable and score-equivalent.

Nodes must be single numeric columns (continuous, datetime, or binary); scoring a wider categorical block raises TypeError lazily, at the first query against that node. Categorical parents are fine: their one-hot columns enter the regression and each column counts toward k.

Parameter Description
data Bind at construction, or later through bind.
penalty A multiplier on the complexity term ((k + 1)/2) log n. 1.0 is the textbook BIC; smaller values prune less.

BIC.penalty#

python
property BIC.penalty: float

The multiplier on the complexity term.

BIC.local_from_arrays#

python
def BIC.local_from_arrays(
    y: npt.NDArray[np.float64],
    X: npt.NDArray[np.float64],
) -> float

The same local by direct least squares on (n, 1) y and (n, k) X.

Agrees with the covariance path to numerical precision; the tests hold the two to 1e-8.

BIC.is_bound#

python
property BIC.is_bound: bool

Whether the score is bound to data and can be evaluated.

BIC.data#

python
property BIC.data: TabularData | None

The dataset this score is bound to, after dropping incomplete rows.

BIC.n#

python
property BIC.n: int

Rows the score is computed from (after dropping incomplete rows); 0 before binding.

BIC.variables#

python
property BIC.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

BIC.bind#

python
def BIC.bind(data: TabularData) -> Self

A new instance of this score bound to data; never mutates self.

BIC.local#

python
def BIC.local(node: str, parents: Iterable[str] = ()) -> float

The cached local score of node given parents.

BIC.local_diff#

python
def BIC.local_diff(node: str, parents: Iterable[str], add: str) -> float

The gain from adding add to the parents of node.

The default is two locals; an implementation may override it with a cheaper incremental form.

BIC.enumerate_locals#

python
def BIC.enumerate_locals(
    node: str,
    candidates: Iterable[str],
    max_parents: int | None = None,
) -> dict[frozenset[str], float]

Every candidate parent set up to max_parents, scored.

The access pattern of the exact solvers (A*, dynamic programming, ILP): all the local scores up front under an in-degree cap.

BIC.graph_score#

python
def BIC.graph_score(parents_of: dict[str, frozenset[str]]) -> float

The decomposable total: the sum of locals over a parent map.

BIC.diagnostics#

python
def BIC.diagnostics() -> dict[str, float]

Run counters: evaluations and cache hits.

BIC.local_from_codes#

python
def BIC.local_from_codes(
    y: npt.NDArray[np.int64],
    y_card: int,
    zs: tuple[npt.NDArray[np.int64], ...],
    z_cards: tuple[int, ...],
) -> float

The local score from codes: y is (n,) integer codes, zs the parents'.

BOSS#

python
class BOSS(
    score: Score | str = 'auto',
    *,
    use_bes: bool = True,
    restarts: int = 0,
    seed: int | None = None,
) -> None

Best order score search over a decomposable score.

Parameter Description
score A Score instance (bound or unbound) or a name: "auto" (pick by the data schema), "bic", "bic_discrete", "bdeu", "bic_mixed".
use_bes Finish with the backward equivalence step (GES's delete phase) on the CPDAG of the projected DAG. The paper's correctness proposition needs it, so it defaults on; the paper's experiments ran without it.
restarts Extra runs from shuffled starting permutations, on top of the sorted start. The best final score across starts wins, with the earlier start keeping ties.
seed The seed for the restart shuffles. None means a fixed default of 0, so a run is reproducible without choosing one; unused when restarts is 0.

fit(data, knowledge) returns a DiscoveryResult whose graph carries the CPDAG face when the final PDAG is a valid CPDAG and the mixed face otherwise (forced knowledge orientations can leave the class; diagnostics["cpdag_valid"] says which happened).

BOSS.fit#

python
def BOSS.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Sweep permutations to convergence and return the class found.

DECODERS#

python
DECODERS: mappingproxy = mappingproxy({'greedy_v1': 'greedy pair-MAP: best-scoring edges first, skip any that closes a cycle', 'exact_map_v1': 'exact MAP of the factorized pair score, A* over topological orders', 'expected_count_v1': "the MAP order's best K edges, K the rounded expected edge count", 'soft_f1_v1': "the MAP order's best K edges, K from a label-free soft-F1 proxy", 'pair_sampler_v1': "sample each pair's state, add edges in random order, drop cycle-closing ones", 'perturb_and_map_v1': 'Gumbel-perturb the pair states, then solve exact MAP per sample'})

The registered decoders: versioned id to a one-line summary (read-only).

FCI#

python
class FCI(
    test: CITest | str = 'auto',
    *,
    alpha: float = 0.05,
    max_cond_size: int | None = None,
) -> None

FCI over a conditional-independence test, returning a PAG.

Parameter Description
test A CITest instance (bound or unbound, including DSeparationOracle), or a name: "auto" (pick by the data schema), "linear"/"fisher_z", "discrete"/ "g_sq", "mixed"/"dg", "gcm".
alpha Significance level for the independence decisions.
max_cond_size Cap on the conditioning-set size, applied to the skeleton phase and to the possible-d-sep subsets alike; None for no cap. Possible-d-sep sets can span most of the graph, so the cap is the honest scaling knob.

fit(data, knowledge) returns a DiscoveryResult whose graph carries the PAG face when the result is a valid complete PAG and the mixed face otherwise (noisy tests or knowledge-seeded marks). With a perfect oracle and no knowledge the output is exactly the canonical PAG: pag_of_mag(mag_of_dag(truth)).

FCI.fit#

python
def FCI.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Run FCI and return the discovered graph with its provenance.

GCM#

python
class GCM(data: TabularData | None = None, *, learner: Any = None) -> None

The generalized covariance measure over a regression learner.

Regresses x and y on z with a fresh clone of the learner per endpoint, forms the elementwise residual product, and reports sqrt(n) * mean(R) / sd(R) against the standard normal, two-sided. Residuals are in-sample, as in the paper's basic form. With an empty conditioning set the endpoints are centered instead, so the statistic is a plain covariance test.

Parameter Description
data The dataset to bind, or None for a reusable config.
learner The regression slot, accepting exactly what an estimator's learner slot accepts: any sklearn-style regressor or an alias string like "ols" or "knn". None means the native KNNRegressor, constructed lazily at first use.

GCM.statistic#

python
def GCM.statistic(
    x: npt.NDArray[np.float64],
    y: npt.NDArray[np.float64],
    z: npt.NDArray[np.float64],
) -> tuple[float, float, float | None]

Return (statistic, p_value, dof); signature depends on needs.

numeric: statistic(x, y, z) with x (n, dx), y (n, dy), z (n, dz) float blocks. codes: statistic(x, y, z, cards) with x, y (n,) integer codes, z a tuple of (n,) code columns, and cards the matching (card_x, card_y, cards_z).

GCM.is_bound#

python
property GCM.is_bound: bool

Whether the test is bound to data and can answer queries.

GCM.data#

python
property GCM.data: TabularData | None

The dataset this test is bound to, after dropping incomplete rows.

GCM.n#

python
property GCM.n: int

Rows the oracle answers from (after dropping incomplete rows).

GCM.variables#

python
property GCM.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

GCM.bind#

python
def GCM.bind(data: TabularData) -> Self

A new instance of this test bound to data; never mutates self.

GCM.test#

python
def GCM.test(x: str, y: str, given: Iterable[str] = ()) -> CIResult

The cached conditional-independence query, symmetric in x and y.

GCM.independent#

python
def GCM.independent(
    x: str,
    y: str,
    given: Iterable[str] = (),
    *,
    alpha: float,
) -> bool

The decision the algorithms read: fail to reject at alpha.

GCM.diagnostics#

python
def GCM.diagnostics() -> dict[str, float]

Run counters: total queries, largest conditioning set, cache hits.

GES#

python
class GES(
    score: Score | str = 'auto',
    *,
    max_degree: int | None = None,
) -> None

Greedy equivalence search over a decomposable score.

Parameter Description
score A Score instance (bound or unbound) or a name: "auto" (pick by the data schema), "bic", "bic_discrete", "bdeu", "bic_mixed".
max_degree A cap on the number of edges at any node; an Insert that touches a node already at the cap is never considered. None leaves degrees free.

fit(data, knowledge) returns a DiscoveryResult whose graph carries the CPDAG face when the final PDAG is a valid CPDAG and the mixed face otherwise (forced knowledge orientations can leave the class; diagnostics["cpdag_valid"] says which happened).

GES.fit#

python
def GES.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Run the forward and backward phases and return the class found.

MMPC#

python
class MMPC(
    test: CITest | str = 'auto',
    *,
    alpha: float = 0.05,
    max_cond_size: int | None = 3,
) -> None

The max-min parents-and-children screen over a conditional-independence test.

MMPC is a cheap skeleton screen to run before an expensive search: it returns the undirected adjacencies the data supports, and DomainKnowledge().restrict_to(result.graph) turns them into a support that PC, GES, or AStar then searches inside, with far fewer tests or score calls. Adjacencies are kept under the AND rule (each endpoint must sit in the other's parents-children set), the stricter of the two symmetry choices, so the screen errs toward sparsity.

Parameter Description
test A CITest instance (bound or unbound, including DSeparationOracle), or a name: "auto" (pick by the data schema), "linear"/"fisher_z", "discrete"/ "g_sq", "mixed"/"dg", "gcm".
alpha Significance level for the independence decisions.
max_cond_size Cap on the conditioning-set size, None for no cap. Subsets of a growing CPC multiply combinatorially, so the default keeps the standard practical cap of 3.

Knowledge enters in three modes. Prune: a pair forbidden in both directions is never a candidate. Protect: required and confounded pairs enter every CPC untested, survive the backward sweep, and pass the AND rule unconditionally. Validate: the finished skeleton is checked at the adjacency level only, because a skeleton has no directions, so is_consistent (which demands required edges be directed) cannot apply; orientation constraints (tiers, single-direction forbids, roots, sinks) do not constrain an undirected adjacency and are left to the search that runs inside the support.

fit(data, knowledge) returns a DiscoveryResult whose graph holds one tail-tail edge per surviving pair, with the mixed face.

MMPC.fit#

python
def MMPC.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Run MMPC and return the undirected skeleton with its provenance.

PC#

python
class PC(
    test: CITest | str = 'auto',
    *,
    alpha: float = 0.05,
    max_cond_size: int | None = None,
    colliders: str = 'standard',
) -> None

PC-stable over a conditional-independence test.

Parameter Description
test A CITest instance (bound or unbound, including DSeparationOracle), or a name: "auto" (pick by the data schema), "linear"/"fisher_z", "discrete"/ "g_sq", "mixed"/"dg", "gcm".
alpha Significance level for the independence decisions.
max_cond_size Cap on the conditioning-set size, None for no cap.
colliders The v-structure rule: "standard" reads the recorded sepsets, "conservative" and "majority" re-test every unshielded triple and leave the ambiguous ones unoriented (see the module docstring).

fit(data, knowledge) returns a DiscoveryResult whose graph carries the CPDAG face when the oriented PDAG is a valid CPDAG and the mixed face otherwise.

PC.fit#

python
def PC.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Run PC-stable and return the discovered graph with its provenance.

PCMCI#

python
class PCMCI(
    test: CITest | str = 'auto',
    *,
    alpha: float = 0.05,
    max_lag: int = 1,
    max_cond_size: int | None = None,
) -> None

PCMCI+ over a conditional-independence test, for time-series and panel data.

Parameter Description
test A CITest instance or a name; "auto" reads the lagged frame's schema (all-continuous data gets the partial-correlation test).
alpha Significance level for every independence decision.
max_lag Largest lag a reported link can have, at least one.
max_cond_size Cap on the screen round and on the contemporaneous subset size, None for no cap.

fit(data) takes TimeSeriesData or PanelData (panel windows stack per entity) and returns a TimeSeriesDiscoveryResult whose graph lives on the window nodes v[t-k].

fit(data, knowledge) reads a DomainKnowledge over the process names, not the window nodes, so a constraint on (x, y) applies at every lag, the contemporaneous slice included:

diagnostics["n_knowledge_pruned"] counts what the knowledge took off the table: lagged links never screened, contemporaneous pairs removed outright, and lag-zero direction bans, one count each.

PCMCI.fit#

python
def PCMCI.fit(
    data: TimeSeriesData | PanelData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> TimeSeriesDiscoveryResult

Run PCMCI+ and return the window graph with its provenance.

knowledge constrains processes, so every constraint applies at every lag; the class docstring lists the mappings. confound raises here (a window graph has no bidirected edges), and a required edge that survived at no lag raises after the run.

RFCI#

python
class RFCI(
    test: CITest | str = 'auto',
    *,
    alpha: float = 0.05,
    max_cond_size: int | None = None,
) -> None

RFCI over a conditional-independence test: FCI's marks at PC's price.

Parameter Description
test A CITest instance (bound or unbound, including DSeparationOracle), or a name: "auto" (pick by the data schema), "linear"/"fisher_z", "discrete"/ "g_sq", "mixed"/"dg", "gcm".
alpha Significance level for the independence decisions.
max_cond_size Cap on the conditioning-set size in the skeleton phase; None for no cap. The later stages only ever condition on subsets of recorded separating sets, which the cap already bounds, so unlike FCI there is no second phase the cap must reach.

fit(data, knowledge) returns a DiscoveryResult whose graph carries the PAG face when the output happens to be a valid complete PAG and the mixed face with diagnostics["pag_valid"] = 0.0 otherwise. Unlike FCI, the mixed face is an expected honest outcome here even under a perfect oracle: the RFCI skeleton can keep edges only the possible-d-sep phase would remove, and the result then stands for more than one Markov equivalence class. Domain knowledge enters exactly as in FCI: prune (a pair forbidden in both directions is never tested), protect (required and confounded pairs are never tested or removed), orient through ancestry (the same seeding helpers), validate at the end.

RFCI.fit#

python
def RFCI.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Run RFCI and return the discovered graph with its provenance.

AStar#

python
class AStar(
    score: Score | str = 'auto',
    *,
    max_parents: int | None = None,
    node_limit: int = 20,
) -> None

Exact structure search: the highest-BIC DAG, found with A*.

Parameter Description
score A Score instance (bound or unbound) or a name: "auto" (pick by the data schema), "bic", "bic_discrete", "bdeu", "bic_mixed".
max_parents An in-degree cap; None enumerates every parent set.
node_limit Refuse problems with more variables than this; exact search is exponential in the node count.

AStar.fit#

python
def AStar.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Search for a highest-scoring DAG and return it with its provenance.

BDeu#

python
class BDeu(data: TabularData | None = None, *, ess: float = 10.0) -> None

The Bayesian Dirichlet equivalent uniform score, decomposable.

Every column must be discrete (categorical, ordinal, or binary): the score runs on integer codes, so binding to data with a continuous column raises TypeError. Score-equivalent for any fixed ess, but the value of ess shapes the result; see the module docstring.

Parameter Description
data Bind at construction, or later through bind.
ess The equivalent sample size, the total prior mass spread over each node's table. Must be positive. Larger values smooth the cell estimates and favor denser graphs; 10.0 is a common default, and a sweep is worth it when the result matters.

BDeu.ess#

python
property BDeu.ess: float

The equivalent sample size of the Dirichlet prior.

BDeu.local_from_codes#

python
def BDeu.local_from_codes(
    y: npt.NDArray[np.int64],
    y_card: int,
    zs: tuple[npt.NDArray[np.int64], ...],
    z_cards: tuple[int, ...],
) -> float

The Dirichlet log marginal likelihood from integer codes.

BDeu.is_bound#

python
property BDeu.is_bound: bool

Whether the score is bound to data and can be evaluated.

BDeu.data#

python
property BDeu.data: TabularData | None

The dataset this score is bound to, after dropping incomplete rows.

BDeu.n#

python
property BDeu.n: int

Rows the score is computed from (after dropping incomplete rows); 0 before binding.

BDeu.variables#

python
property BDeu.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

BDeu.bind#

python
def BDeu.bind(data: TabularData) -> Self

A new instance of this score bound to data; never mutates self.

BDeu.local#

python
def BDeu.local(node: str, parents: Iterable[str] = ()) -> float

The cached local score of node given parents.

BDeu.local_diff#

python
def BDeu.local_diff(node: str, parents: Iterable[str], add: str) -> float

The gain from adding add to the parents of node.

The default is two locals; an implementation may override it with a cheaper incremental form.

BDeu.enumerate_locals#

python
def BDeu.enumerate_locals(
    node: str,
    candidates: Iterable[str],
    max_parents: int | None = None,
) -> dict[frozenset[str], float]

Every candidate parent set up to max_parents, scored.

The access pattern of the exact solvers (A*, dynamic programming, ILP): all the local scores up front under an in-degree cap.

BDeu.graph_score#

python
def BDeu.graph_score(parents_of: dict[str, frozenset[str]]) -> float

The decomposable total: the sum of locals over a parent map.

BDeu.diagnostics#

python
def BDeu.diagnostics() -> dict[str, float]

Run counters: evaluations and cache hits.

BDeu.local_from_arrays#

python
def BDeu.local_from_arrays(
    y: npt.NDArray[np.float64],
    X: npt.NDArray[np.float64],
) -> float

The local score from arrays: y is (n, dy), X is (n, k).

BootstrapResult#

python
class BootstrapResult(
    method: str,
    n_runs: int,
    n_failed: int,
    edges: Mapping[frozenset[str], float],
    marks: Mapping[str, float],
    variables: tuple[str, ...],
) -> None

Edge frequencies from rerunning one discovery method on bootstrap resamples.

edges maps an unordered node pair to the fraction of successful runs in which the pair was adjacent, whatever the marks. marks maps a canonical edge glyph (glyph(), for example "X --> Y" or "X <-> Y") to its frequency, so one pair can spread its mass over several glyphs; the glyph frequencies for a pair sum to its adjacency frequency. Both mappings hold only pairs and glyphs that appeared at least once, with keys in sorted order. Frequencies are over the n_runs successful replicates, not over what was attempted. A temporal method keys them on the window nodes, "X[t-1] --> Y[t]".

Field Type Default
method str required
n_runs int required
n_failed int required
edges Mapping[frozenset[str], float] required
marks Mapping[str, float] required
variables tuple[str, ...] required

BootstrapResult.frequency#

python
def BootstrapResult.frequency(a: str, b: str) -> float

The adjacency frequency of the pair, 0.0 when it never appeared.

BootstrapResult.summary#

python
def BootstrapResult.summary(threshold: float = 0.5) -> MixedGraph

The stability graph: pairs at or above threshold, one edge each.

A qualifying pair gets its modal glyph, the orientation it wore most often (ties broken by the sorted glyph string, so the result is deterministic). The graph keeps the mixed kind: a vote over graphs carries no equivalence-class guarantee, even when every replicate was a CPDAG. Nodes are all the data's variables, so a variable that never earned a stable edge shows up isolated. threshold must be in (0, 1].

CIResult#

python
class CIResult(
    statistic: float,
    p_value: float,
    dof: float | None = None,
) -> None

One conditional-independence query: the statistic, its p-value, and dof.

Field Type Default
statistic float required
p_value float required
dof float | None None

CITest#

python
class CITest(data: TabularData | None = None) -> None

Base for conditional-independence tests; see the module docstring.

Subclasses set name and needs, may take options in __init__ (set them before calling super().__init__(data), since binding may read them), and implement statistic. The optional _prepare hook runs once after binding, for sufficient statistics.

CITest.is_bound#

python
property CITest.is_bound: bool

Whether the test is bound to data and can answer queries.

CITest.data#

python
property CITest.data: TabularData | None

The dataset this test is bound to, after dropping incomplete rows.

CITest.n#

python
property CITest.n: int

Rows the oracle answers from (after dropping incomplete rows).

CITest.variables#

python
property CITest.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

CITest.bind#

python
def CITest.bind(data: TabularData) -> Self

A new instance of this test bound to data; never mutates self.

CITest.test#

python
def CITest.test(x: str, y: str, given: Iterable[str] = ()) -> CIResult

The cached conditional-independence query, symmetric in x and y.

CITest.independent#

python
def CITest.independent(
    x: str,
    y: str,
    given: Iterable[str] = (),
    *,
    alpha: float,
) -> bool

The decision the algorithms read: fail to reject at alpha.

CITest.diagnostics#

python
def CITest.diagnostics() -> dict[str, float]

Run counters: total queries, largest conditioning set, cache hits.

CITest.statistic#

python
def CITest.statistic(*args: Any) -> tuple[float, float, float | None]

Return (statistic, p_value, dof); signature depends on needs.

numeric: statistic(x, y, z) with x (n, dx), y (n, dy), z (n, dz) float blocks. codes: statistic(x, y, z, cards) with x, y (n,) integer codes, z a tuple of (n,) code columns, and cards the matching (card_x, card_y, cards_z).

DSeparationOracle#

python
class DSeparationOracle(graph: MixedGraph) -> None

The ground-truth oracle: answers queries by m-separation in a known graph.

Quacks like any bound test (p-value 1.0 when separated, 0.0 when connected), so every constraint algorithm can be checked for exactness and run in teaching mode. Needs no data; bind returns self.

Latent and selection nodes are not query-able variables: the oracle exposes the observed margin, which is what a constraint algorithm searching over data would see. Separation is still computed on the full graph, latents included, and every query conditions on the selection nodes, because data sampled under selection only ever shows the distribution given them.

DSeparationOracle.is_bound#

python
property DSeparationOracle.is_bound: bool

Always True: the oracle answers from its graph and needs no data.

DSeparationOracle.graph#

python
property DSeparationOracle.graph: MixedGraph

The graph whose d-separations (m-separations) the oracle reports.

DSeparationOracle.bind#

python
def DSeparationOracle.bind(data: TabularData) -> Self

A new instance of this test bound to data; never mutates self.

DSeparationOracle.data#

python
property DSeparationOracle.data: TabularData | None

The dataset this test is bound to, after dropping incomplete rows.

DSeparationOracle.n#

python
property DSeparationOracle.n: int

Rows the oracle answers from (after dropping incomplete rows).

DSeparationOracle.variables#

python
property DSeparationOracle.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

DSeparationOracle.test#

python
def DSeparationOracle.test(
    x: str,
    y: str,
    given: Iterable[str] = (),
) -> CIResult

The cached conditional-independence query, symmetric in x and y.

DSeparationOracle.independent#

python
def DSeparationOracle.independent(
    x: str,
    y: str,
    given: Iterable[str] = (),
    *,
    alpha: float,
) -> bool

The decision the algorithms read: fail to reject at alpha.

DSeparationOracle.diagnostics#

python
def DSeparationOracle.diagnostics() -> dict[str, float]

Run counters: total queries, largest conditioning set, cache hits.

DSeparationOracle.statistic#

python
def DSeparationOracle.statistic(
    *args: Any,
) -> tuple[float, float, float | None]

Return (statistic, p_value, dof); signature depends on needs.

numeric: statistic(x, y, z) with x (n, dx), y (n, dy), z (n, dz) float blocks. codes: statistic(x, y, z, cards) with x, y (n,) integer codes, z a tuple of (n,) code columns, and cards the matching (card_x, card_y, cards_z).

DecodeResult#

python
class DecodeResult(
    graph: MixedGraph,
    method: str,
    test: str | None = None,
    score: str | None = None,
    alpha: float | None = None,
    n: int = 0,
    diagnostics: Mapping[str, Any] = <factory>,
    sepsets: Mapping[frozenset[str], frozenset[str]] | None = None,
    decoder: str = '',
    candidates: tuple[MixedGraph, ...] = (),
    probabilities: EdgeProbabilities | None = None,
) -> None

A DAG decoded from edge probabilities, with how it was decoded.

A DiscoveryResult whose graph is the decoded DAG (kind dag): the single decoded graph for a deterministic decoder, the consensus (medoid) of the candidates for a sampler. method is "decode:<decoder id>" (a Welt run decoded locally keeps "welt:<model>").

diagnostics always labels the graph: decoded is True, native is False, and it records decoder, parameters, seed, samples, rejected_cycles (nominated edges skipped because they would close a directed cycle), order_skipped (skipped only because they would break the order knowledge) and search_expansions (each one per candidate), threshold_source, edge_budget, objective (the factorized pair log score of graph), n_edges, source_kind, source_provenance, knowledge_applied and note ("not a graph posterior; orientation not certified").

Attribute Description
decoder The versioned decoder id, for example "greedy_v1".
candidates Every decoded DAG; one for a deterministic decoder, one per sample (in sample order) for a sampler. Candidates, not posterior draws.
probabilities The edge probabilities that were decoded, as given.
Field Type Default
graph MixedGraph required
method str required
test str | None None
score str | None None
alpha float | None None
n int 0
diagnostics Mapping[str, Any] factory
sepsets Mapping[frozenset[str], frozenset[str]] | None None
decoder str ''
candidates tuple[MixedGraph, ...] ()
probabilities EdgeProbabilities | None None

Decoder#

python
class Decoder(
    method: str = 'greedy',
    threshold: float | None = None,
    samples: int = 1,
    seed: int | None = None,
    max_nodes: int = 20,
) -> None

A decoder and its settings, checked up front and reusable.

Decoder("greedy", threshold=0.3).decode(probabilities) is decode(probabilities, "greedy", threshold=0.3). Pass one to discover() as decoder= to fix every setting before a paid run.

Attribute Description
method A decoder id (see decode()).
threshold Greedy only: nominate an edge only when its input value is above this (None keeps edges with positive weight).
samples Candidate count for a sampler; must be 1 otherwise.
seed Sampler seed (required for a sampler, refused otherwise).
max_nodes The variable cap for decoders that search orders exactly.
Field Type Default
method str 'greedy'
threshold float | None None
samples int 1
seed int | None None
max_nodes int 20

Decoder.stochastic#

python
property Decoder.stochastic: bool

Whether this decoder samples candidates (and so needs a seed).

Decoder.decode#

python
def Decoder.decode(
    probabilities: EdgeProbabilities,
    *,
    knowledge: DomainKnowledge | None = None,
) -> DecodeResult

Decode probabilities with these settings; see decode().

DirectLiNGAM#

python
class DirectLiNGAM(prune: str = 'bic') -> None

DirectLiNGAM over continuous tabular data.

Parameter Description
prune "bic" (the default) backward-eliminates each variable's predecessors by Gaussian BIC on the original data; "none" keeps every predecessor, the full lower-triangular DAG of the found order.

fit(data, knowledge) returns a DiscoveryResult whose graph is a DAG: with independent non-Gaussian noises the whole orientation is identified, so there is no equivalence class to report. The class consumes neither a CI test nor a decomposable score (ingredient is None); the data is its only ingredient.

DirectLiNGAM.fit#

python
def DirectLiNGAM.fit(
    data: TabularData,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Find the causal order, prune the parents, and return the DAG.

DiscoveryResult#

python
class DiscoveryResult(
    graph: MixedGraph,
    method: str,
    test: str | None = None,
    score: str | None = None,
    alpha: float | None = None,
    n: int = 0,
    diagnostics: Mapping[str, Any] = <factory>,
    sepsets: Mapping[frozenset[str], frozenset[str]] | None = None,
) -> None

What a discovery run found, and how.

graph is the headline. test or score names the ingredient the algorithm consumed (one of them is None). diagnostics carries the run counters (tests by conditioning size, cache hits, score evaluations); a welt:<model> run carries its Welt provenance there instead (ids, versions and the native edge-score dataframe, see discover()). sepsets maps an unordered removed pair to the set that separated it, when a constraint method produced one.

Field Type Default
graph MixedGraph required
method str required
test str | None None
score str | None None
alpha float | None None
n int 0
diagnostics Mapping[str, Any] factory
sepsets Mapping[frozenset[str], frozenset[str]] | None None

Discrete#

python
class Discrete(data: TabularData | None = None) -> None

The G squared conditional-independence test on contingency tables.

Degrees of freedom are adjusted for sparsity, bnlearn-style: each nonempty stratum of the conditioning configuration contributes (card_x - 1) * (card_y - 1), and configurations with no observed rows contribute nothing. The unadjusted product over the full configuration space would overstate the dof badly on sparse tables and make the test blind. When the dof comes out zero (a single-level endpoint, or no rows), the query is vacuous and the result is (0.0, 1.0, 0.0).

Discrete.statistic#

python
def Discrete.statistic(
    x: npt.NDArray[np.int64],
    y: npt.NDArray[np.int64],
    z: tuple[npt.NDArray[np.int64], ...],
    cards: tuple[int, int, tuple[int, ...]],
) -> tuple[float, float, float | None]

Return (statistic, p_value, dof); signature depends on needs.

numeric: statistic(x, y, z) with x (n, dx), y (n, dy), z (n, dz) float blocks. codes: statistic(x, y, z, cards) with x, y (n,) integer codes, z a tuple of (n,) code columns, and cards the matching (card_x, card_y, cards_z).

Discrete.is_bound#

python
property Discrete.is_bound: bool

Whether the test is bound to data and can answer queries.

Discrete.data#

python
property Discrete.data: TabularData | None

The dataset this test is bound to, after dropping incomplete rows.

Discrete.n#

python
property Discrete.n: int

Rows the oracle answers from (after dropping incomplete rows).

Discrete.variables#

python
property Discrete.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

Discrete.bind#

python
def Discrete.bind(data: TabularData) -> Self

A new instance of this test bound to data; never mutates self.

Discrete.test#

python
def Discrete.test(x: str, y: str, given: Iterable[str] = ()) -> CIResult

The cached conditional-independence query, symmetric in x and y.

Discrete.independent#

python
def Discrete.independent(
    x: str,
    y: str,
    given: Iterable[str] = (),
    *,
    alpha: float,
) -> bool

The decision the algorithms read: fail to reject at alpha.

Discrete.diagnostics#

python
def Discrete.diagnostics() -> dict[str, float]

Run counters: total queries, largest conditioning set, cache hits.

DiscreteBIC#

python
class DiscreteBIC(
    data: TabularData | None = None,
    *,
    penalty: float = 1.0,
) -> None

The multinomial BIC, decomposable and score-equivalent.

Every column must be discrete (categorical, ordinal, or binary): the score runs on integer codes, so binding to data with a continuous column raises TypeError. Parameter counts agree across a Markov equivalence class, so Markov-equivalent DAGs get equal totals, exactly as with the Gaussian BIC.

Parameter Description
data Bind at construction, or later through bind.
penalty A multiplier on the complexity term ((r - 1) q / 2) log n. 1.0 is the textbook BIC; smaller values prune less.

DiscreteBIC.penalty#

python
property DiscreteBIC.penalty: float

The multiplier on the complexity term.

DiscreteBIC.local_from_codes#

python
def DiscreteBIC.local_from_codes(
    y: npt.NDArray[np.int64],
    y_card: int,
    zs: tuple[npt.NDArray[np.int64], ...],
    z_cards: tuple[int, ...],
) -> float

The multinomial local from integer codes; see the module docstring.

DiscreteBIC.is_bound#

python
property DiscreteBIC.is_bound: bool

Whether the score is bound to data and can be evaluated.

DiscreteBIC.data#

python
property DiscreteBIC.data: TabularData | None

The dataset this score is bound to, after dropping incomplete rows.

DiscreteBIC.n#

python
property DiscreteBIC.n: int

Rows the score is computed from (after dropping incomplete rows); 0 before binding.

DiscreteBIC.variables#

python
property DiscreteBIC.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

DiscreteBIC.bind#

python
def DiscreteBIC.bind(data: TabularData) -> Self

A new instance of this score bound to data; never mutates self.

DiscreteBIC.local#

python
def DiscreteBIC.local(node: str, parents: Iterable[str] = ()) -> float

The cached local score of node given parents.

DiscreteBIC.local_diff#

python
def DiscreteBIC.local_diff(
    node: str,
    parents: Iterable[str],
    add: str,
) -> float

The gain from adding add to the parents of node.

The default is two locals; an implementation may override it with a cheaper incremental form.

DiscreteBIC.enumerate_locals#

python
def DiscreteBIC.enumerate_locals(
    node: str,
    candidates: Iterable[str],
    max_parents: int | None = None,
) -> dict[frozenset[str], float]

Every candidate parent set up to max_parents, scored.

The access pattern of the exact solvers (A*, dynamic programming, ILP): all the local scores up front under an in-degree cap.

DiscreteBIC.graph_score#

python
def DiscreteBIC.graph_score(parents_of: dict[str, frozenset[str]]) -> float

The decomposable total: the sum of locals over a parent map.

DiscreteBIC.diagnostics#

python
def DiscreteBIC.diagnostics() -> dict[str, float]

Run counters: evaluations and cache hits.

DiscreteBIC.local_from_arrays#

python
def DiscreteBIC.local_from_arrays(
    y: npt.NDArray[np.float64],
    X: npt.NDArray[np.float64],
) -> float

The local score from arrays: y is (n, dy), X is (n, k).

EdgeProbabilities#

python
class EdgeProbabilities(
    variables: tuple[str, ...],
    values: npt.NDArray[np.float64],
    kind: ProbabilityKind = 'independent',
    calibrated: bool = False,
    provenance: Mapping[str, Any] = <factory>,
) -> None

A matrix of directed-edge probabilities over named variables.

values[i, j] is the number for an edge from variables[i] (the source row) to variables[j] (the target column). The diagonal is zero, every entry is finite and in [0, 1], and the stored matrix is a read-only copy, so the caller's array is never shared or changed.

kind says how to read the numbers:

calibrated is False unless the source states the numbers match observed edge frequencies; ergodic never sets it on its own. provenance is free-form, read-only metadata (model, version, job id, threshold, ...).

None of this is a posterior over graphs. The marginals do not account for acyclicity or for dependence between edges; a decoded graph built from them is a modelling choice, not the model's claim.

Attribute Description
variables Variable names, the row and column order of values.
values The (d, d) float matrix, read-only.
kind "independent", "pair_state" or "frequency".
calibrated Whether the source states its numbers are calibrated.
provenance Read-only metadata about where the numbers came from.
Field Type Default
variables tuple[str, ...] required
values npt.NDArray[np.float64] required
kind ProbabilityKind 'independent'
calibrated bool False
provenance Mapping[str, Any] factory

EdgeProbabilities.to_pair_states#

python
def EdgeProbabilities.to_pair_states() -> EdgeProbabilities

The pair-state reading: three exclusive states per unordered pair.

For "independent" marginals p = P[i, j] and q = P[j, i], the two edges are treated as independent coin flips and conditioned on "not both" (a DAG cannot hold a two-cycle): no edge (1 - p)(1 - q), i -> j p(1 - q), j -> i q(1 - p), renormalized. When both are exactly 1 the limit is undecided, so the pair becomes (0, 0.5, 0.5). This keeps each model's direction ranking and never looks at a true graph. It is the conversion Ergodic's research decoders were evaluated with.

"pair_state" values are returned unchanged (the same object). "frequency" values already satisfy the pair simplex (one edge per pair per run), so they are read as pair states as they are: a new object of kind "pair_state" with the same numbers.

Returns: Edge probabilities of kind "pair_state", with the conversion recorded in provenance["pair_state_conversion"].

EdgeProbabilities.existence#

python
def EdgeProbabilities.existence() -> npt.NDArray[np.float64]

Skeleton probabilities: the chance each pair is adjacent, either way.

Read from the pair-state view (to_pair_states()), so it is P[i, j] + P[j, i] there: for "independent" marginals that is (p(1 - q) + q(1 - p)) / (1 - pq). The matrix is symmetric with a zero diagonal and read-only.

EdgeProbabilities.threshold#

python
def EdgeProbabilities.threshold(t: float = 0.5) -> MixedGraph

The directed edges whose value is above t, as a diagnostic graph.

Keeps every edge with values[i, j] > t (strictly, as AVICI and CDFM threshold natively). The result is a mixed-kind graph over every variable and may hold directed cycles. A pair whose two directions both pass cannot be drawn as two edges (one edge per pair), so it shows as an undirected X --- Y edge: adjacent, direction unresolved. This is not a DAG and not a decoded graph; use decode() for one.

Parameter Description
t The threshold, in [0, 1).

Returns: A mixed MixedGraph.

EdgeProbabilities.to_frame#

python
def EdgeProbabilities.to_frame() -> pd.DataFrame

The matrix as a dataframe: index source, columns target.

EdgeProbabilities.from_frame#

python
classmethod EdgeProbabilities.from_frame(
    frame: pd.DataFrame,
    *,
    kind: ProbabilityKind = 'independent',
    calibrated: bool = False,
    provenance: Mapping[str, Any] | None = None,
) -> EdgeProbabilities

Build from a square dataframe: rows the source, columns the target.

The index gives the variable order; the columns must hold the same names (in any order) and are reordered to match. Labels are read as strings.

Parameter Description
frame The square matrix.
kind How to read the numbers, as on the class.
calibrated Whether the source states its numbers are calibrated.
provenance Metadata to keep.

EdgeProbabilities.from_bootstrap#

python
classmethod EdgeProbabilities.from_bootstrap(
    result: BootstrapResult,
    *,
    undirected: Literal['split', 'drop'] = 'split',
) -> EdgeProbabilities

Directed-edge frequencies from a bootstrap() run.

Each canonical glyph frequency lands on its directed cell: "X --> Y" on [X, Y] and "X <-- Y" on [Y, X]. An undirected X --- Y (a CPDAG edge, "one of the two directions") is split half and half under undirected="split" and dropped under "drop". Bidirected and circle-marked glyphs say something other than "this direction" and are always dropped; the dropped mass is recorded in provenance["dropped_frequency"]. Frequencies measure resampling stability, not probability.

Parameter Description
result An iid bootstrap result (window-graph results are refused).
undirected "split" or "drop", as above.

Returns: Edge probabilities of kind "frequency".

GRaSP#

python
class GRaSP(score: Score | str = 'auto', *, depth: int = 3) -> None

Greedy relaxations of the sparsest permutation over a decomposable score.

Parameter Description
score A Score instance (bound or unbound) or a name: "auto" (pick by the data schema), "bic", "bic_discrete", "bdeu", "bic_mixed".
depth How many levels of tied covered tucks the walk may chain before backtracking; the paper's experiments use 3.

fit(data, knowledge) returns a DiscoveryResult whose graph carries the CPDAG face when the final PDAG is a valid CPDAG and the mixed face otherwise (forced knowledge orientations can leave the class; diagnostics["cpdag_valid"] says which happened).

GRaSP.fit#

python
def GRaSP.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Walk tucks to convergence and return the class found.

Glasso#

python
class Glasso(
    lam: float | None = None,
    *,
    gamma: float = 0.5,
    n_lambdas: int = 100,
    tol: float = 0.001,
    max_iter: int = 100,
) -> None

The graphical-lasso screen: an undirected skeleton from a sparse precision.

Like MMPC, this is a cheap screen to run before an expensive search: DomainKnowledge().restrict_to(result.graph) turns the recovered support into a constraint that PC, GES, or AStar then searches inside, with far fewer tests or score calls. The honesty note: a precision zero is a conditional independence under Gaussianity, so the graph is a skeleton source, not causal structure; it says which adjacencies the data supports, never which way they point.

The solve is FHT 2008 block coordinate descent on the correlation matrix (the graph, not the scale, is the deliverable), with the diagonal unpenalized (the scikit-learn convention; the paper's differs, see the module docstring) and the shrunk initialization that keeps every column subproblem positive definite.

Parameter Description
lam The penalty. None (the default) runs the EBIC path: n_lambdas log-spaced values from max |off-diagonal correlation| (the empty graph) down to one percent of it, warm-started descending. Each support met on the path is scored by EBIC at its restricted maximum likelihood estimate (the Foygel-Drton definition; the shrunk path estimate would bias the pick dense as n grows), and the minimizer wins, ties to the sparser, larger lambda. An explicit value runs single-shot; 0.0 asks for the unpenalized MLE and raises when p >= n.
gamma The EBIC sparsity weight (Foygel and Drton 2010), 0.5 by default; 0.0 recovers the plain BIC.
n_lambdas Path length when lam is None.
tol Outer convergence, FHT's criterion: the mean absolute change of the working covariance over a full cycle must drop under tol times the mean absolute off-diagonal correlation.
max_iter Cap on outer cycles per solve; past it the fit raises RuntimeError rather than return unconverged in silence.

Knowledge enters through the penalty, and only its adjacency content binds, because the screen is undirected. A pair forbidden in both directions (an adjacency ban, including pairs outside a restrict_to support) is a hard zero, excluded from the updates entirely. A required pair (either direction) is unpenalized, so it survives every lambda the data gives it any conditional association at. Directional knowledge (tiers, one-way forbids, roots, sinks) does not constrain an undirected adjacency and is left to the search that runs inside the support.

fit(data, knowledge) needs a TabularData whose variables are all continuous (a Gaussian model) and returns a DiscoveryResult whose graph holds one tail-tail edge per nonzero precision pair, on the mixed face. The chosen penalty lives in the diagnostics, with the EBIC value when the path ran.

Glasso.fit#

python
def Glasso.fit(
    data: TabularData,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Run the screen and return the undirected skeleton with its provenance.

GrangerResult#

python
class GrangerResult(
    pvalues: Mapping[tuple[str, str], float],
    significant: frozenset[tuple[str, str]],
    max_lag: int,
    alpha: float,
    n: int,
) -> None

Granger tests for every ordered pair: a predictive table, not a graph.

pvalues maps (cause, effect) to the likelihood-ratio p-value; significant holds the pairs with p <= alpha. n counts the stacked window rows the regressions used.

Field Type Default
pvalues Mapping[tuple[str, str], float] required
significant frozenset[tuple[str, str]] required
max_lag int required
alpha float required
n int required

HillClimb#

python
class HillClimb(
    score: Score | str = 'auto',
    *,
    tabu: int = 10,
    max_iter: int = 10000,
    max_parents: int | None = None,
) -> None

Greedy DAG search: add, remove, and reverse moves with a tabu ring.

Steepest ascent from the empty (or knowledge-seeded) graph, returning the best state seen. n_iterations in the diagnostics counts applied moves, so it always equals n_adds + n_removes + n_reverses.

Parameter Description
score A Score instance (bound or unbound) or a name: "auto" (pick by the data schema), "bic", "bic_discrete", "bdeu", "bic_mixed".
tabu The length of the tabu ring (inverses of recent moves that may not be replayed) and the bound on consecutive non-improving escape steps; 0 disables both, so the search stops at the first local optimum.
max_iter A hard cap on the number of applied moves.
max_parents An in-degree cap enforced on adds and reversals; None leaves the in-degree free. Required edges are seeded whatever the cap.

HillClimb.fit#

python
def HillClimb.fit(
    data: TabularData | None = None,
    knowledge: DomainKnowledge | None = None,
) -> DiscoveryResult

Climb to a high-scoring DAG and return it with its provenance.

Linear#

python
class Linear(data: TabularData | None = None) -> None

Partial correlation through the Fisher z transform.

Residualizes x and y on [1, z], computes the correlation r of the residuals, and reports sqrt(n - dz - 3) * atanh(r) against the standard normal, two-sided. The sign of r survives in the statistic; the p-value does not depend on it.

Linear.statistic#

python
def Linear.statistic(
    x: npt.NDArray[np.float64],
    y: npt.NDArray[np.float64],
    z: npt.NDArray[np.float64],
) -> tuple[float, float, float | None]

Return (statistic, p_value, dof); signature depends on needs.

numeric: statistic(x, y, z) with x (n, dx), y (n, dy), z (n, dz) float blocks. codes: statistic(x, y, z, cards) with x, y (n,) integer codes, z a tuple of (n,) code columns, and cards the matching (card_x, card_y, cards_z).

Linear.is_bound#

python
property Linear.is_bound: bool

Whether the test is bound to data and can answer queries.

Linear.data#

python
property Linear.data: TabularData | None

The dataset this test is bound to, after dropping incomplete rows.

Linear.n#

python
property Linear.n: int

Rows the oracle answers from (after dropping incomplete rows).

Linear.variables#

python
property Linear.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

Linear.bind#

python
def Linear.bind(data: TabularData) -> Self

A new instance of this test bound to data; never mutates self.

Linear.test#

python
def Linear.test(x: str, y: str, given: Iterable[str] = ()) -> CIResult

The cached conditional-independence query, symmetric in x and y.

Linear.independent#

python
def Linear.independent(
    x: str,
    y: str,
    given: Iterable[str] = (),
    *,
    alpha: float,
) -> bool

The decision the algorithms read: fail to reject at alpha.

Linear.diagnostics#

python
def Linear.diagnostics() -> dict[str, float]

Run counters: total queries, largest conditioning set, cache hits.

Mixed#

python
class Mixed(data: TabularData | None = None) -> None

Partial canonical correlations with Bartlett's chi-square.

Residualizes the x and y blocks on [1, z], takes the reduced QR of each residual block, and reads the canonical correlations rho_i off the singular values of Qx.T @ Qy (clipped below 1). Bartlett's statistic -(n - dz - (dx + dy + 3) / 2) * sum(log(1 - rho_i^2)) is referred to a chi-square with dx * dy degrees of freedom.

With scalar endpoints (dx == dy == 1) the single canonical correlation is the absolute partial correlation, so Mixed agrees with Linear asymptotically. A zero-width block (a single-level categorical) carries no information; that query is vacuous and returns (0.0, 1.0, 0.0).

Mixed.statistic#

python
def Mixed.statistic(
    x: npt.NDArray[np.float64],
    y: npt.NDArray[np.float64],
    z: npt.NDArray[np.float64],
) -> tuple[float, float, float | None]

Return (statistic, p_value, dof); signature depends on needs.

numeric: statistic(x, y, z) with x (n, dx), y (n, dy), z (n, dz) float blocks. codes: statistic(x, y, z, cards) with x, y (n,) integer codes, z a tuple of (n,) code columns, and cards the matching (card_x, card_y, cards_z).

Mixed.is_bound#

python
property Mixed.is_bound: bool

Whether the test is bound to data and can answer queries.

Mixed.data#

python
property Mixed.data: TabularData | None

The dataset this test is bound to, after dropping incomplete rows.

Mixed.n#

python
property Mixed.n: int

Rows the oracle answers from (after dropping incomplete rows).

Mixed.variables#

python
property Mixed.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

Mixed.bind#

python
def Mixed.bind(data: TabularData) -> Self

A new instance of this test bound to data; never mutates self.

Mixed.test#

python
def Mixed.test(x: str, y: str, given: Iterable[str] = ()) -> CIResult

The cached conditional-independence query, symmetric in x and y.

Mixed.independent#

python
def Mixed.independent(
    x: str,
    y: str,
    given: Iterable[str] = (),
    *,
    alpha: float,
) -> bool

The decision the algorithms read: fail to reject at alpha.

Mixed.diagnostics#

python
def Mixed.diagnostics() -> dict[str, float]

Run counters: total queries, largest conditioning set, cache hits.

MixedBIC#

python
class MixedBIC(
    data: TabularData | None = None,
    *,
    penalty: float = 1.0,
) -> None

The degenerate-Gaussian BIC, decomposable and score-equivalent.

Any column kind is a valid node: a continuous node is its single column and a categorical node is its one-hot block (first level dropped), scored jointly. local = -(n/2) log det Sigma_{Y|X} - penalty * p/2 * log n with p = dy dx + dy (dy + 1) / 2 for a dy-column node and dx total parent columns.

Parameter Description
data Bind at construction, or later through bind.
penalty A multiplier on the complexity term, inherited from BIC. 1.0 is the textbook BIC; smaller values prune less.

MixedBIC.local_from_arrays#

python
def MixedBIC.local_from_arrays(
    y: npt.NDArray[np.float64],
    X: npt.NDArray[np.float64],
) -> float

The same local by direct least squares on (n, dy) y and (n, k) X.

Agrees with the covariance path to numerical precision; the tests hold the two to 1e-8.

MixedBIC.penalty#

python
property MixedBIC.penalty: float

The multiplier on the complexity term.

MixedBIC.is_bound#

python
property MixedBIC.is_bound: bool

Whether the score is bound to data and can be evaluated.

MixedBIC.data#

python
property MixedBIC.data: TabularData | None

The dataset this score is bound to, after dropping incomplete rows.

MixedBIC.n#

python
property MixedBIC.n: int

Rows the score is computed from (after dropping incomplete rows); 0 before binding.

MixedBIC.variables#

python
property MixedBIC.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

MixedBIC.bind#

python
def MixedBIC.bind(data: TabularData) -> Self

A new instance of this score bound to data; never mutates self.

MixedBIC.local#

python
def MixedBIC.local(node: str, parents: Iterable[str] = ()) -> float

The cached local score of node given parents.

MixedBIC.local_diff#

python
def MixedBIC.local_diff(
    node: str,
    parents: Iterable[str],
    add: str,
) -> float

The gain from adding add to the parents of node.

The default is two locals; an implementation may override it with a cheaper incremental form.

MixedBIC.enumerate_locals#

python
def MixedBIC.enumerate_locals(
    node: str,
    candidates: Iterable[str],
    max_parents: int | None = None,
) -> dict[frozenset[str], float]

Every candidate parent set up to max_parents, scored.

The access pattern of the exact solvers (A*, dynamic programming, ILP): all the local scores up front under an in-degree cap.

MixedBIC.graph_score#

python
def MixedBIC.graph_score(parents_of: dict[str, frozenset[str]]) -> float

The decomposable total: the sum of locals over a parent map.

MixedBIC.diagnostics#

python
def MixedBIC.diagnostics() -> dict[str, float]

Run counters: evaluations and cache hits.

MixedBIC.local_from_codes#

python
def MixedBIC.local_from_codes(
    y: npt.NDArray[np.int64],
    y_card: int,
    zs: tuple[npt.NDArray[np.int64], ...],
    z_cards: tuple[int, ...],
) -> float

The local score from codes: y is (n,) integer codes, zs the parents'.

Score#

python
class Score(data: TabularData | None = None) -> None

Base for decomposable scores; see the module docstring.

Subclasses set name, needs, and the property flags, may take options in __init__ (set them before calling super().__init__(data)), and implement the method matching needs: local_from_arrays for "numeric", local_from_codes for "codes". _prepare runs once after binding for sufficient statistics; a native score may also override _local_value to skip array assembly entirely (the covariance path in BIC).

Score.is_bound#

python
property Score.is_bound: bool

Whether the score is bound to data and can be evaluated.

Score.data#

python
property Score.data: TabularData | None

The dataset this score is bound to, after dropping incomplete rows.

Score.n#

python
property Score.n: int

Rows the score is computed from (after dropping incomplete rows); 0 before binding.

Score.variables#

python
property Score.variables: tuple[str, ...]

The variables this oracle answers about, sorted.

Score.bind#

python
def Score.bind(data: TabularData) -> Self

A new instance of this score bound to data; never mutates self.

Score.local#

python
def Score.local(node: str, parents: Iterable[str] = ()) -> float

The cached local score of node given parents.

Score.local_diff#

python
def Score.local_diff(node: str, parents: Iterable[str], add: str) -> float

The gain from adding add to the parents of node.

The default is two locals; an implementation may override it with a cheaper incremental form.

Score.enumerate_locals#

python
def Score.enumerate_locals(
    node: str,
    candidates: Iterable[str],
    max_parents: int | None = None,
) -> dict[frozenset[str], float]

Every candidate parent set up to max_parents, scored.

The access pattern of the exact solvers (A*, dynamic programming, ILP): all the local scores up front under an in-degree cap.

Score.graph_score#

python
def Score.graph_score(parents_of: dict[str, frozenset[str]]) -> float

The decomposable total: the sum of locals over a parent map.

Score.diagnostics#

python
def Score.diagnostics() -> dict[str, float]

Run counters: evaluations and cache hits.

Score.local_from_arrays#

python
def Score.local_from_arrays(
    y: npt.NDArray[np.float64],
    X: npt.NDArray[np.float64],
) -> float

The local score from arrays: y is (n, dy), X is (n, k).

Score.local_from_codes#

python
def Score.local_from_codes(
    y: npt.NDArray[np.int64],
    y_card: int,
    zs: tuple[npt.NDArray[np.int64], ...],
    z_cards: tuple[int, ...],
) -> float

The local score from codes: y is (n,) integer codes, zs the parents'.

TimeSeriesDiscoveryResult#

python
class TimeSeriesDiscoveryResult(
    graph: MixedGraph,
    method: str,
    test: str | None = None,
    score: str | None = None,
    alpha: float | None = None,
    n: int = 0,
    diagnostics: Mapping[str, Any] = <factory>,
    sepsets: Mapping[frozenset[str], frozenset[str]] | None = None,
    max_lag: int = 0,
    links: Mapping[tuple[str, str, int], float] = <factory>,
) -> None

A DiscoveryResult over window nodes, with the per-link p-values.

links maps (source, target, lag) to the p-value of the test that kept the link. A lag of one or more is the lagged edge source[t-lag] -> target[t]. A lag of zero is a contemporaneous edge: an oriented edge is keyed (source, target), an undirected edge is keyed once with the pair in sorted order.

Field Type Default
graph MixedGraph required
method str required
test str | None None
score str | None None
alpha float | None None
n int 0
diagnostics Mapping[str, Any] factory
sepsets Mapping[frozenset[str], frozenset[str]] | None None
max_lag int 0
links Mapping[tuple[str, str, int], float] factory

TimeSeriesDiscoveryResult.summary_edges#

python
def TimeSeriesDiscoveryResult.summary_edges() -> frozenset[tuple[str, str]]

Variable-level pairs implied by the links.

(x, y) is present when any lagged link x -> y exists or a contemporaneous edge x -> y is oriented in the window graph. An undirected contemporaneous pair contributes both orders, and a lagged self-link (autocorrelation) contributes (x, x).

bootstrap#

python
def bootstrap(
    data: TabularData | TimeSeriesData | PanelData,
    *,
    method: str = 'pc',
    n_bootstrap: int = 100,
    seed: int = 0,
    knowledge: DomainKnowledge | None = None,
    block_length: int | None = None,
    **kwargs: Any,
) -> BootstrapResult

Rerun a discovery method on bootstrap resamples and count what survives.

Answers the question a single run cannot: how stable is this edge under resampling? Each replicate draws a resample, runs discover(sample, method=method, knowledge=knowledge, **kwargs), and the result aggregates adjacency and glyph frequencies over the runs that finished. Extra keyword arguments flow to the algorithm unchanged (alpha, max_cond_size, score=..., max_lag=...), so any registered method, iid or temporal, sits behind the same call.

An iid method on a TabularData draws data.n_rows rows with replacement, the classic bootstrap; block_length does not apply there and raises. A temporal method ("pcmci") never resamples rows, which would shred the serial dependence it reads. On a TimeSeriesData it runs a moving-block bootstrap: ceil(T / L) blocks of L consecutive rows (block_length, default max(ceil(sqrt(T)), 2 * (max_lag + 1))) are drawn with replacement and enter the replicate as separate entities of a panel, "block_0", "block_1", ... with fresh time stamps. Blocks-as-entities is the point: gluing blocks back into one series would build lagged windows that straddle two unrelated fragments and let fake adjacencies in, while per-entity pooling never reads across a block seam. On a PanelData it runs the cluster bootstrap instead, drawing n_entities entities whole with replacement (relabeled in draw order so repeated draws stay distinct); block_length does not apply to a panel and raises. One honesty note: block resampling treats the series as stationary at the block scale, so a trend or a regime change inside the sampling window leaks into the frequencies. An iid method on temporal data still refuses, inside the replicate, exactly as discover() does.

The frequencies measure resampling variability, not Bayesian uncertainty; they are not posterior probabilities over graphs (ergodic has no graph posteriors). The run is deterministic: the same data, seed, and settings give the same result. A temporal method keys the frequencies on window-graph glyphs like "X[t-1] --> Y[t]".

A replicate that raises ValueError or RuntimeError (degenerate resamples happen: a constant column, a knowledge conflict on noisy data) counts in n_failed and is skipped; frequencies are over the successful runs only. When every replicate fails, the wrapper raises RuntimeError chaining the last error.

Parameter Description
data The dataset to resample: tabular for the iid methods, a time series or panel for the temporal ones.
method A registered discovery method name, as in discover(). welt:<model> methods are refused: each resample would be a paid job.
n_bootstrap How many resamples to draw, at least 1.
seed Seed for the resampling generator.
knowledge Hard constraints, handed to every replicate.
block_length Moving-block length for a time series under a temporal method; None picks max(ceil(sqrt(T)), 2 * (max_lag + 1)).
**kwargs Passed through to discover and on to the algorithm.

decode#

python
def decode(
    probabilities: EdgeProbabilities,
    method: str = 'greedy',
    *,
    threshold: float | None = None,
    knowledge: DomainKnowledge | None = None,
    samples: int = 1,
    seed: int | None = None,
    max_nodes: int = 20,
) -> DecodeResult

Decode edge probabilities into a DAG with a named, versioned decoder.

The input is read as pair states (to_pair_states()) and every decoder works on the edge weight w(i -> j) = log P(i -> j) - log P(no edge). The decoders:

Ties are broken by variable order, so a decoder is deterministic given its inputs (and seed). The input is never changed and the global numpy random state is never touched.

Knowledge constrains the decode. Forbidden edges, tiers, roots, sinks and a skeleton restriction remove directed edges (their weight becomes minus infinity). Order (order, tiers) is enforced as one time order: the decoded graph has a topological order consistent with every order statement and every edge at once. That is exactly the tier reading, and stricter than violations() for separate order pairs (it only asks that no later variable be an ancestor of an earlier one), so a DAG that satisfies each pair alone but no single order is ruled out. Required edges go in first, whatever their weight, and must not form a cycle. confound is refused: a DAG cannot hold a latent confounder. The decoded graph is checked against the knowledge afterwards.

Parameter Description
probabilities The edge probabilities to decode.
method A decoder id, with or without its _v1 version.
threshold Greedy only; see above. None keeps the w > 0 rule.
knowledge Hard constraints, all naming variables of probabilities.
samples How many candidates a sampler draws; 1 for the others.
seed The sampler's seed (required); deterministic decoders refuse one.
max_nodes The variable cap for the decoders that search orders exactly (all but greedy and pair_sampler). The search can grow exponentially with the variable count, so above the cap they raise.

Returns: A DecodeResult: graph a DAG over every variable, with the decoder, its parameters and the diagnostics recorded.

Raises Description
TypeError probabilities is not an EdgeProbabilities.
ValueError An unknown decoder, a setting that does not apply, too many variables for an exact search, knowledge naming unknown variables, confounded pairs, inconsistent knowledge or a cyclic required set.

discover#

python
def discover(
    data: TabularData | TimeSeriesData | PanelData,
    method: str = 'pc',
    *,
    test: Any = None,
    score: Any = None,
    knowledge: DomainKnowledge | None = None,
    alpha: float = 0.05,
    **kwargs: Any,
) -> DiscoveryResult

Learn causal structure from data with the named method.

test feeds a constraint method (a CITest instance built without data, since discover binds it to data, or an alias like "linear"; a bound test runs through the algorithm class, as in PC(test=GCM(data)).fit()); score feeds a score-based method (a Score instance or an alias like "bic", "bdeu"); both default to "auto", picked from the column schema. Each algorithm declares which ingredient it consumes. Extra keyword arguments pass to the algorithm (max_cond_size, max_parents, node_limit, max_degree, tabu, max_lag).

The iid methods read a TabularData and reject temporal shapes (rows are not exchangeable); method="pcmci" reads a TimeSeriesData or PanelData and returns a window graph over lagged nodes like "X[t-1]".

A few methods consume neither ingredient because the machinery is the algorithm: "lingam" (regression and residual independence) and "glasso" (a sparse precision estimate). They reject test= and score=, and alpha does not apply (their own knobs, like lam or prune, pass through the keyword arguments).

method="welt:<model>" (for example "welt:arrow") runs a hosted Welt model through discover(), one paid Welt job. It needs pip install "ergodic[welt]", takes no test, score or alpha, and passes only client, seed, timeout and decoder through the keyword arguments; anything else raises before any request. knowledge is refused unless decoder= asks for local decoding of the model's edge scores (decode()), which is also what lets a non-DAG model such as "welt:avici" return a (decoded) DAG.

granger#

python
def granger(
    data: TimeSeriesData,
    *,
    max_lag: int = 1,
    alpha: float = 0.05,
) -> GrangerResult

Granger-test every ordered pair of variables in data.

For each effect Y, the full regression puts Y[t] on an intercept plus every variable's lags 1..max_lag; the restricted one drops the candidate cause's lags. All variables must be continuous. See the module docstring for what this does and does not mean.

lagged_node#

python
def lagged_node(name: str, lag: int) -> str

The window-column name for name at lag steps back.

Lag zero reads "X[t]", lag k reads "X[t-k]". The name must carry no whitespace, since the graph layer parses edges on whitespace.

lagged_tabular#

python
def lagged_tabular(
    data: TimeSeriesData | PanelData,
    max_lag: int,
) -> TabularData

Stack the windows of data into a tabular frame of lagged columns.

Row t holds every variable at lags 0..max_lag; the column for variable v at lag k is named lagged_node(v, k) and holds v at time t - k. Column types carry over: continuous stays continuous, categorical keeps its categories, ordinal keeps its level order.

A time series needs at least max_lag + 8 rows. Panel windows are built within each entity (sorted entity order) and stacked; entities with fewer than max_lag + 2 rows are skipped with a warning, and it is an error when no rows survive. Unequal time steps warn too: lags count rows, not calendar time.

parse_lagged#

python
def parse_lagged(node: str) -> tuple[str, int]

Invert lagged_node(): "X[t-2]" becomes ("X", 2).

Suggest an edit
Documentation

Search ergodic 0.2.0

Search ergodic by task, function or error.