discovery.TBFPC

Target-first Bayes Factor PC (TBF-PC) causal-discovery algorithm.

Usage

discovery.TBFPC(
    target,
    *,
    target_edge_rule="any",
    bf_thresh=1.0,
    max_conditioning_set_size=3,
    forbidden_edges=None,
    required_edges=None
)

A target-oriented variant of the Peter–Clark (PC) algorithm that uses Bayes factors (via a ΔBIC approximation) as the conditional-independence test.

For each conditional-independence test of the form

.. math::

H_0 : Y \perp X \mid S
\quad \text{vs.} \quad
H_1 : Y \not\!\perp X \mid S

two linear models are compared:

.. math::

M_0 : Y \sim S
\\
M_1 : Y \sim S + X

where :math:S is a conditioning set of variables.

The Bayesian Information Criterion (BIC) is defined as

.. math::

\mathrm{BIC}(M) = n \log\!\left(\frac{\mathrm{RSS}}{n}\right)
                  + k \log(n),

with residual sum of squares :math:\mathrm{RSS}, sample size :math:n, and number of parameters :math:k. The Bayes factor is approximated by

.. math::

\log \mathrm{BF}_{10} \approx -\tfrac{1}{2}
\left[ \mathrm{BIC}(M_1) - \mathrm{BIC}(M_0) \right].

Independence is declared when :math:\mathrm{BF}_{10} < \tau, where :math:\tau is set via bf_thresh.

Target Edge Rules

Different rules govern how driver → target edges are retained:

  • "any": keep :math:X \to Y unless any conditioning set renders :math:X \perp Y \mid S.
  • "conservative": keep :math:X \to Y if at least one conditioning set shows dependence.
  • "fullS": test only with the full set of other drivers as :math:S.

Parameters

target: str

Name of the outcome variable used to orient the search. Must be present in the data passed to fit().

target_edge_rule: ("any", "conservative", "fullS") = "any"

Rule controlling which driver → target edges are retained.

bf_thresh: float = 1.0

Positive Bayes-factor threshold for the conditional-independence tests.

max_conditioning_set_size: int = 3

Largest conditioning set |S| searched in the "any" and "conservative" target phases and in the driver-skeleton phase (default 3). This bounds the combinatorial cost of the search; a pair that is only separable by a larger conditioning set will not be separated, so its edge is retained. The "fullS" target rule ignores this and always conditions on the full set of other drivers. It also controls the separating sets used by the v-structure orientation, so an overly small value can leave colliders undetected.

forbidden_edges: Sequence[tuple[str, str]] | None = None

Node pairs that must never be connected in the learned graph (background knowledge / orientation constraints). Symmetric: an entry (u, v) also forbids v—u.

required_edges: Sequence[tuple[str, str]] | None = None
Directed (u, v) pairs that must appear as u -> v in the learned graph.

Examples

Basic usage with the full conditioning set::

import numpy as np, pandas as pd
from pathmc import TBFPC

rng = np.random.default_rng(7)
n = 2000
C = rng.gamma(2, 1, n)
A = 0.7 * C + rng.gamma(2, 1, n)
D = 0.5 * C + rng.gamma(2, 1, n)
B = 0.8 * A + rng.gamma(2, 1, n)
Y = 0.9 * B + 0.6 * D + 0.7 * C + rng.gamma(2, 1, n)

df = pd.DataFrame({"A": A, "B": B, "C": C, "D": D, "Y": Y})
df = (df - df.mean()) / df.std()  # recommended scaling

model = TBFPC(target="Y", target_edge_rule="fullS")
model.fit(df, drivers=["A", "B", "C", "D"])
print(model.to_digraph())

Background knowledge — forbid an edge, force another::

model = TBFPC(
    target="Y",
    forbidden_edges=[("A", "C")],
    required_edges=[("B", "Y")],
)
model.fit(df, drivers=["A", "B", "C", "D"])

References

  • Spirtes, Glymour, Scheines (2000). Causation, Prediction, and Search. MIT Press. [PC algorithm]
  • Spirtes & Glymour (1991). “An Algorithm for Fast Recovery of Sparse Causal Graphs.”
  • Kass, R. & Raftery, A. (1995). “Bayes Factors.”

Methods

Name Description
fit() Fit the TBF-PC procedure to df.
get_all_cdags_from_cpdag() Enumerate the member DAGs of the CPDAG’s Markov equivalence class.
get_directed_edges() Return the directed edges learned by the algorithm.
get_test_results() Return ΔBIC diagnostics for the unordered pair {x, y}.
get_undirected_edges() Return the undirected edges remaining after orientation.
summary() Render a text summary of the learned graph and the CI-test count.
to_digraph() Return the learned CPDAG encoded in DOT format.

fit()

Fit the TBF-PC procedure to df.

Usage

fit(df, drivers)
Parameters
df: pandas.DataFrame

Dataset containing the target column and every candidate driver. Standardizing the columns (zero mean, unit variance) is recommended so the ΔBIC test is well scaled.

drivers: Sequence[str]
Column names to treat as potential drivers of the target.
Returns
TBFPC
The fitted instance (self) with internal adjacency structures populated.
Examples

::

model = TBFPC(target="Y", target_edge_rule="fullS")
model.fit(df, drivers=["A", "B", "C"])

get_all_cdags_from_cpdag()

Enumerate the member DAGs of the CPDAG’s Markov equivalence class.

Usage

get_all_cdags_from_cpdag(dot_cpdag=None)

This is what makes the discovery output a set of equally plausible graphs rather than one arbitrary DAG: every undirected edge is oriented both ways, then an orientation is kept only if it (a) stays acyclic and (b) introduces no new v-structure (unshielded collider) beyond those already compelled by the CPDAG’s directed edges. Those two filters together are exactly the membership test for the Markov equivalence class, so every returned DAG satisfies pathmc.same_markov_equivalence_class() against the input CPDAG. Downstream model averaging fits each returned DAG and pools the effect posteriors.

Because fit() already orients the compelled edges (v-structures + Meek rules), a CPDAG with no reversible edges collapses to a single DAG, while genuinely reversible structure (chains/forks, cliques) yields several members.

Parameters
dot_cpdag: str | None = None
If provided, parse the CPDAG from this DOT string (undirected edges encoded as [style=dashed, dir=none]). If None, use this model’s current CPDAG from get_directed_edges() and get_undirected_edges().
Returns
list[str]
DOT strings, each a fully oriented DAG (no dashed edges) and a member of the same Markov equivalence class as the CPDAG.

get_directed_edges()

Return the directed edges learned by the algorithm.

Usage

get_directed_edges()
Returns
list[tuple[str, str]]
Sorted list of (u, v) oriented edges.

get_test_results()

Return ΔBIC diagnostics for the unordered pair {x, y}.

Usage

get_test_results(x, y)
Parameters
x: str

The two variables in the pair (order does not matter).

y: str
The two variables in the pair (order does not matter).
Returns
list[TestResult]
One entry per conditioning set tested, each holding bic0, bic1, delta_bic, logBF10, BF10, independent, and the conditioning_set used.

get_undirected_edges()

Return the undirected edges remaining after orientation.

Usage

get_undirected_edges()
Returns
list[tuple[str, str]]
Sorted list of (u, v) pairs for unresolved adjacencies.

summary()

Render a text summary of the learned graph and the CI-test count.

Usage

summary()
Returns
str
Multiline string listing directed edges, undirected edges, and the number of conditional-independence tests executed.

to_digraph()

Return the learned CPDAG encoded in DOT format.

Usage

to_digraph()

Directed edges render as u -> v; undirected (unoriented) edges as u -> v [style=dashed, dir=none]; required edges are highlighted, and the target node is filled.

Returns
str
DOT string compatible with Graphviz rendering utilities and with pathmc.same_markov_equivalence_class().