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#
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#
property IDA.n_members: int
The number of members examined.
IDA.n_identifiable#
property IDA.n_identifiable: int
How many members have an identifiable effect.
IDA.all_identifiable#
property IDA.all_identifiable: bool
True when every member identifies.
IDA.strategies#
property IDA.strategies: frozenset[str]
The distinct strategies seen across members (e.g. {"adjustment", "id"}).
IDA.estimands#
property IDA.estimands: frozenset[str]
The distinct estimand strings across the identifiable members.
IDA.agree#
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#
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#
def Functional.render() -> str
The readable estimand string, e.g. sum_z P(Y | X, z) P(z).
Functional.free_vars#
def Functional.free_vars() -> frozenset[str]
The variable scope: heads and givens, minus bound marginal indices.
Functional.simplify#
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#
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#
def Hedge.render() -> str
A human-readable reason.
IDResult#
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#
def IDResult.render() -> str
Return a readable result: the estimand, the hedge, or "not identifiable".
Identification#
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#
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#
def Marginal.render() -> str
The readable estimand string, e.g. sum_z P(Y | X, z) P(z).
Marginal.free_vars#
def Marginal.free_vars() -> frozenset[str]
The variable scope: heads and givens, minus bound marginal indices.
Marginal.simplify#
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#
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#
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#
def Prob.render() -> str
The readable estimand string, e.g. sum_z P(Y | X, z) P(z).
Prob.free_vars#
def Prob.free_vars() -> frozenset[str]
The variable scope: heads and givens, minus bound marginal indices.
Prob.simplify#
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#
class Product(factors: tuple[Functional, ...]) -> None
A product of factors.
| Field | Type | Default |
|---|---|---|
factors |
tuple[Functional, ...] |
required |
Product.render#
def Product.render() -> str
The readable estimand string, e.g. sum_z P(Y | X, z) P(z).
Product.free_vars#
def Product.free_vars() -> frozenset[str]
The variable scope: heads and givens, minus bound marginal indices.
Product.simplify#
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#
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#
def Ratio.render() -> str
The readable estimand string, e.g. sum_z P(Y | X, z) P(z).
Ratio.free_vars#
def Ratio.free_vars() -> frozenset[str]
The variable scope: heads and givens, minus bound marginal indices.
Ratio.simplify#
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#
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#
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#
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#
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#
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#
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#
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#
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#
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#
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#
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.