ergodicDocs
Products: switch from ergodic to another product
Get started

ReferenceIdentification

Identification

Identification: turn a causal graph into an estimand. Import from ergodic.identification.

Given a graph, a treatment, and an outcome, the question is whether the interventional distribution P(Y | do(X)) can be written in terms of the observational distribution, and if so, how. The work is graph-only and deterministic: no data, no statistics, no estimator choices yet.

The named criteria (adjustment, front-door, instruments) live in _criteria; the Identification record in _record. identify_effect() tries them in order, falling through to the general do-calculus ID algorithm on a single graph.

The general engine is exposed too: identify_distribution() runs ID (or IDC) for any query and returns an IDResult carrying the typed estimand Functional (or the Hedge witness when not identifiable). ida() covers an equivalence class by enumerating members.

Also exported from ergodic: IDA, Functional, Hedge, IDResult, Identification, Marginal, MediationID, Prob, Product, Ratio, adjustment_set, frontdoor_set, ida, identify_distribution, identify_effect, identify_mediation, instruments, is_frontdoor_set, is_instrument, is_valid_adjustment_set, minimal_adjustment_set.

IDA#

python
class IDA(
    treatment: frozenset[str],
    outcome: frozenset[str],
    class_kind: GraphKind,
    members: tuple[Identification, ...],
) -> None

Per-member identification across an equivalence class.

members is the tuple of per-member Identification results, in the enumerator's deterministic order. For a CPDAG that is one entry per DAG member; for a PAG or MAG handled through the class-level adjustment criterion it is a single entry standing for the whole class (the criterion's answer holds in every member). class_kind records the face that was enumerated.

The summary properties read the multiset: how many members identify, the distinct strategies and estimands seen, and whether the class agrees.

Field Type Default
treatment frozenset[str] required
outcome frozenset[str] required
class_kind GraphKind required
members tuple[Identification, ...] required

IDA.n_members#

python
property IDA.n_members: int

The number of members examined.

IDA.n_identifiable#

python
property IDA.n_identifiable: int

How many members have an identifiable effect.

IDA.all_identifiable#

python
property IDA.all_identifiable: bool

True when every member identifies.

IDA.strategies#

python
property IDA.strategies: frozenset[str]

The distinct strategies seen across members (e.g. {"adjustment", "id"}).

IDA.estimands#

python
property IDA.estimands: frozenset[str]

The distinct estimand strings across the identifiable members.

IDA.agree#

python
property IDA.agree: bool

True when every member identifies and shares one estimand.

The classic IDA reading: a single, member-invariant answer. False when some member fails or the members disagree about the estimand.

Functional#

python
class Functional()

The sealed base of the estimand algebra. Use the frozen subclasses below.

Three operations are common to every node: render() (the readable estimand string), free_vars() (the variable scope, used by recognition and the plug-in evaluator), and simplify() (conservative rewrites only). Subclasses are frozen value objects, so two structurally equal functionals are equal and hash alike.

Functional.render#

python
def Functional.render() -> str

The readable estimand string, e.g. sum_z P(Y | X, z) P(z).

Functional.free_vars#

python
def Functional.free_vars() -> frozenset[str]

The variable scope: heads and givens, minus bound marginal indices.

Functional.simplify#

python
def Functional.simplify() -> Functional

A conservatively simplified functional (safe rewrites only).

Flatten nested products, drop a marginal over an empty index, drop a factor that is identically one. No cancellations, no clever algebra: two runs render the same estimand.

Hedge#

python
class Hedge(nodes: frozenset[str], district: frozenset[str]) -> None

The structural witness that an effect is not identifiable (a C-forest pair).

ID fails at a subproblem whose whole graph is a single c-component over nodes and whose treatment-free part is the district district. That pair is a hedge in the sense of Shpitser-Pearl: it is the obstruction that proves P(y | do(x)) is not a function of the observational distribution. The SCM bounds layer reads this object to decide where to return bounds instead of a point.

The witness is the (nodes, district) summary of the failing subproblem, enough to mark non-identifiability and to seed the bounds. The explicit minimal C-forest pair (the rooted forests of Shpitser-Pearl) is not extracted.

Hedge.render#

python
def Hedge.render() -> str

A human-readable reason.

IDResult#

python
class IDResult(
    identifiable: bool,
    functional: Functional | None,
    hedge: Hedge | None,
    query: tuple[frozenset[str], frozenset[str], frozenset[str]],
) -> None

The outcome of running ID or IDC on a single graph.

identifiable is the verdict; functional is the estimand when it holds (None otherwise); hedge is the witness when it fails (None otherwise). query records the (outcome, do, given) that was asked.

Field Type Default
identifiable bool required
functional Functional | None required
hedge Hedge | None required
query tuple[frozenset[str], frozenset[str], frozenset[str]] required

IDResult.render#

python
def IDResult.render() -> str

Return a readable result: the estimand, the hedge, or "not identifiable".

Identification#

python
class Identification(
    treatment: frozenset[str],
    outcome: frozenset[str],
    identifiable: bool,
    strategy: str,
    adjustment_set: frozenset[str] | None = None,
    mediators: frozenset[str] | None = None,
    instruments: tuple[tuple[str, frozenset[str]], ...] = (),
    estimand: str = '',
    functional: Functional | None = None,
    hedge: Hedge | None = None,
) -> None

The result of trying to identify an effect from a graph.

The named strategies ("null", "adjustment", "frontdoor", "iv") carry the set or instruments their estimator reads and a string estimand. When only the general do-calculus ID algorithm identifies an effect (outside the three named criteria, the napkin and Verma graphs and their kin), strategy is "id" and functional carries the typed estimand, with estimand set to functional.render(). A not-identifiable result built by the ID fallthrough carries the hedge witness. Existing strategies set neither, and estimand stays a string, so the field is a pure extension.

Field Type Default
treatment frozenset[str] required
outcome frozenset[str] required
identifiable bool required
strategy str required
adjustment_set frozenset[str] | None None
mediators frozenset[str] | None None
instruments tuple[tuple[str, frozenset[str]], ...] ()
estimand str ''
functional Functional | None None
hedge Hedge | None None

Marginal#

python
class Marginal(index: frozenset[str], body: Functional) -> None

Sum index out of body (a marginalization).

Field Type Default
index frozenset[str] required
body Functional required

Marginal.render#

python
def Marginal.render() -> str

The readable estimand string, e.g. sum_z P(Y | X, z) P(z).

Marginal.free_vars#

python
def Marginal.free_vars() -> frozenset[str]

The variable scope: heads and givens, minus bound marginal indices.

Marginal.simplify#

python
def Marginal.simplify() -> Functional

A conservatively simplified functional (safe rewrites only).

Flatten nested products, drop a marginal over an empty index, drop a factor that is identically one. No cancellations, no clever algebra: two runs render the same estimand.

MediationID#

python
class MediationID(
    treatment: frozenset[str],
    outcome: frozenset[str],
    mediators: frozenset[str],
    identifiable: bool,
    nde_estimand: str,
    nie_estimand: str,
    cde_identifiable: bool,
    cde_estimand: str,
    covariates: frozenset[str] | None,
    assumptions: tuple[str, ...],
) -> None

The result of trying to identify a mediation decomposition from a graph.

identifiable is the verdict for the natural direct and indirect effects: True when a covariate set covariates makes the mediation formula valid, False when none exists (the cross-world / post-treatment-confounder failure). nde_estimand and nie_estimand render the mediation formula with that set; they are empty strings when not identifiable.

The controlled direct effect is tracked separately, since it is a plain interventional contrast (no cross-world assumption): cde_identifiable is its verdict and cde_estimand its formula, decided through the general ID algorithm on P(Y | do(X), do(M)). CDE can be identified when NDE/NIE are not, and vice versa.

assumptions states, in words, the (untestable) claims the decomposition rests on, the same honesty the Design records carry for the quasi-experimental designs.

Field Type Default
treatment frozenset[str] required
outcome frozenset[str] required
mediators frozenset[str] required
identifiable bool required
nde_estimand str required
nie_estimand str required
cde_identifiable bool required
cde_estimand str required
covariates frozenset[str] | None required
assumptions tuple[str, ...] required

Prob#

python
class Prob(head: frozenset[str], given: frozenset[str] = frozenset()) -> None

An atomic term P(head | given) (a marginal when given is empty).

Field Type Default
head frozenset[str] required
given frozenset[str] frozenset()

Prob.render#

python
def Prob.render() -> str

The readable estimand string, e.g. sum_z P(Y | X, z) P(z).

Prob.free_vars#

python
def Prob.free_vars() -> frozenset[str]

The variable scope: heads and givens, minus bound marginal indices.

Prob.simplify#

python
def Prob.simplify() -> Functional

A conservatively simplified functional (safe rewrites only).

Flatten nested products, drop a marginal over an empty index, drop a factor that is identically one. No cancellations, no clever algebra: two runs render the same estimand.

Product#

python
class Product(factors: tuple[Functional, ...]) -> None

A product of factors.

Field Type Default
factors tuple[Functional, ...] required

Product.render#

python
def Product.render() -> str

The readable estimand string, e.g. sum_z P(Y | X, z) P(z).

Product.free_vars#

python
def Product.free_vars() -> frozenset[str]

The variable scope: heads and givens, minus bound marginal indices.

Product.simplify#

python
def Product.simplify() -> Functional

A conservatively simplified functional (safe rewrites only).

Flatten nested products, drop a marginal over an empty index, drop a factor that is identically one. No cancellations, no clever algebra: two runs render the same estimand.

Ratio#

python
class Ratio(num: Functional, den: Functional) -> None

A ratio num / den (the IDC normalization P_x(y, z) / P_x(z)).

Field Type Default
num Functional required
den Functional required

Ratio.render#

python
def Ratio.render() -> str

The readable estimand string, e.g. sum_z P(Y | X, z) P(z).

Ratio.free_vars#

python
def Ratio.free_vars() -> frozenset[str]

The variable scope: heads and givens, minus bound marginal indices.

Ratio.simplify#

python
def Ratio.simplify() -> Functional

A conservatively simplified functional (safe rewrites only).

Flatten nested products, drop a marginal over an empty index, drop a factor that is identically one. No cancellations, no clever algebra: two runs render the same estimand.

adjustment_set#

python
def adjustment_set(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
) -> frozenset[str] | None

Return the canonical valid adjustment set, or None if none exists.

The canonical set is the ancestors of treatment and outcome (possible ancestors, on a class face), minus the forbidden nodes and the latent nodes. If any valid adjustment set over the observed nodes exists, this one is valid; on a class, validity means valid in every member.

frontdoor_set#

python
def frontdoor_set(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
) -> frozenset[str] | None

Return a front-door mediator set, or None if none is found.

Tries the full set of observed nodes between treatment and outcome first, then single mediators. This is a search over likely candidates, not every subset.

ida#

python
def ida(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
    *,
    max_members: int = 1000,
) -> IDA

Identify treatment -> outcome across every member of an equivalence class.

On a CPDAG, enumerate the DAG members (dags_of_cpdag()) and run identify_effect() per member, returning the multiset as an IDA. On a PAG or MAG, return the class-level generalized-adjustment answer where it applies (a single member-invariant result), and raise when the case needs general per-member ID (not available: it needs a DAG realization of each MAG member). A single graph (DAG or ADMG) raises with a pointer to identify_effect().

identify_distribution#

python
def identify_distribution(
    graph: MixedGraph,
    outcome: str | Iterable[str],
    do: str | Iterable[str],
    given: str | Iterable[str] = frozenset(),
) -> IDResult

Run the general do-calculus ID (or IDC) algorithm on a single graph.

Decides whether P(outcome | do(do), given) is identifiable from the observational distribution and, when it is, returns the estimand as a typed functional inside an IDResult. With given empty this is ID; with given nonempty it is the conditional algorithm IDC. The graph may be a DAG or an ADMG, and may carry latent= designations or bows (it is latent-projected to an ADMG first).

A CPDAG, MAG, or PAG raises: ID is defined on a single graph, and the class is covered by ida(), which enumerates members. A mixed-face graph raises, like the rest of identification.

identify_effect#

python
def identify_effect(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
) -> Identification

Try adjustment, then the front-door, then instruments, and report the result.

Before any strategy runs, the trivial case is settled: when no directed path runs from the treatment to the outcome, the intervention cannot move the outcome, so P(y | do(x)) = P(y) and the strategy is "null".

On a class face (CPDAG, MAG, PAG) only the null and adjustment strategies apply, and a success holds in every graph the class represents: the null when no member has a causal path, adjustment through the generalized criterion. The front-door and instruments need a single graph, so a class that adjustment cannot identify reports not-identifiable and says whether amenability failed or no uniform set exists.

identify_mediation#

python
def identify_mediation(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
    mediators: str | Iterable[str],
) -> MediationID

Identify the mediation decomposition of treatment -> outcome through mediators.

Searches for a covariate set W of non-descendants of the treatment under which the natural direct and indirect effects are identified by the mediation formula (Pearl, 2001): W is a valid adjustment set for X -> Y and for X -> M, and W u {X} is a valid adjustment set for M -> Y. When such a W exists, NDE and NIE are identifiable and their estimands render in the mediation-formula style; when none exists, they are not identifiable, the canonical case being a mediator-outcome confounder that is itself affected by the treatment (a cross-world obstruction).

The controlled direct effect is identified separately, through the general ID algorithm on P(Y | do(X), do(M)): a plain interventional contrast that needs no cross-world assumption, and may identify when NDE/NIE do not.

Single graphs only (a DAG or ADMG). A CPDAG, MAG, or PAG raises (extract a member or enumerate the class).

instruments#

python
def instruments(
    graph: MixedGraph,
    treatment: str,
    outcome: str,
) -> list[tuple[str, frozenset[str]]]

Find conditional instruments for treatment -> outcome.

Returns (instrument, conditioning set) pairs. Candidates and conditioning nodes are observed non-descendants of the treatment and the outcome; the search tries the empty set and single nodes as conditioning sets.

is_frontdoor_set#

python
def is_frontdoor_set(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
    mediators: str | Iterable[str],
) -> bool

Return True if mediators satisfies the front-door criterion.

A mediator set holding a latent node is never valid, since the front-door formula conditions on the mediators.

is_instrument#

python
def is_instrument(
    graph: MixedGraph,
    instrument: str,
    treatment: str,
    outcome: str,
    given: str | Iterable[str] = frozenset(),
) -> bool

Return True if instrument is a conditional instrument given given.

The instrument and the conditioning set are observed non-descendants of the treatment and the outcome, the instrument is associated with the treatment, and the instrument reaches the outcome only through the treatment (it separates from the outcome once the treatment's outgoing edges are cut).

Descendants of the treatment are rejected outright: cutting the treatment's outgoing edges would sever the very edge that makes them endogenous, so the exclusion check alone would pass them.

is_valid_adjustment_set#

python
def is_valid_adjustment_set(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
    adjustment: str | Iterable[str],
) -> bool

Return True if adjustment is a valid adjustment set for the effect.

On a DAG or ADMG, the complete adjustment criterion: the set avoids the forbidden nodes (descendants of the causal-path nodes) and blocks every proper non-causal path from treatment to outcome. A set holding a latent node is never valid, since a latent node cannot be conditioned on.

On a CPDAG, MAG, or PAG, the generalized criterion (Perkovic et al., 2018): the class must be amenable, the set avoids the possible descendants of the possibly-causal-path nodes, and it blocks every proper definite-status non-causal path. True then means the set is valid in every graph the class represents.

minimal_adjustment_set#

python
def minimal_adjustment_set(
    graph: MixedGraph,
    treatment: str | Iterable[str],
    outcome: str | Iterable[str],
) -> frozenset[str] | None

Return one minimal valid adjustment set, or None if none exists.

Suggest an edit
Documentation

Search ergodic 0.2.0

Search ergodic by task, function or error.