Causal Identification
See the Causal Identification guide for worked examples.
Adjustment
CausalStructures.adjustment_set — Function
adjustment_set(cg::ADMG, x, y) -> Union{Nothing,Vector{Symbol}}Return a valid inclusion-minimal adjustment set for the causal effect of x on y in cg, or nothing if none exists.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
cg::ADMG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
A Vector{Symbol} adjustment set, or nothing if none exists.
julia> admg = ADMG("L --> X --> Y, L --> Y");
julia> adjustment_set(admg, :X, :Y)
1-element Vector{Symbol}:
:L
julia> admg2 = ADMG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> sort(adjustment_set(admg2, [:X1, :X2], :Y))
2-element Vector{Symbol}:
:L1
:L2
julia> admg3 = ADMG(directed(:X, :Y), bidirected(:X, :Y)); # direct edge plus latent confounder
julia> adjustment_set(admg3, :X, :Y) === nothing
trueadjustment_set(cg::AbstractAG, x, y) -> Union{Nothing,Vector{Symbol}}Return a inclusion-minimal valid adjustment set for the causal effect of x on y in cg, or nothing if none exists.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
cg::AbstractAG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
A Vector{Symbol} adjustment set, or nothing if none exists.
julia> mag = MAG("A <-> X, A --> M --> Y, X --> Y");
julia> adjustment_set(mag, :X, :Y)
1-element Vector{Symbol}:
:A
julia> mag2 = MAG("A <-> X1, B <-> X2, A --> M1 --> Y, B --> M2 --> Y, X1 --> Y, X2 --> Y");
julia> sort(adjustment_set(mag2, [:X1, :X2], :Y))
2-element Vector{Symbol}:
:A
:B
julia> mag3 = MAG(directed(:X, :Y)); # invisible edge: no witness rules out a latent confounder
julia> adjustment_set(mag3, :X, :Y) === nothing
trueadjustment_set(cg::PAG, x, y) -> Union{Nothing,Vector{Symbol}}Return a inclusion-minimalvalid adjustment set for the causal effect of x on y in cg, or nothing if none exists.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
cg::PAG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
A Vector{Symbol} adjustment set, or nothing if none exists.
julia> mag = MAG("B --> X, A <-> X, A --> Y, X --> Y");
julia> pag = mag_to_pag(mag);
julia> adjustment_set(pag, :X, :Y)
1-element Vector{Symbol}:
:A
julia> mag2 = MAG(
"B1 --> X1, A1 <-> X1, A1 --> Y, X1 --> Y,
B2 --> X2, A2 <-> X2, A2 --> Y, X2 --> Y");
julia> pag2 = mag_to_pag(mag2);
julia> sort(adjustment_set(pag2, [:X1, :X2], :Y))
2-element Vector{Symbol}:
:A1
:A2
julia> pag3 = mag_to_pag(MAG(directed(:A, :X), directed(:X, :Y), directed(:A, :Y)));
julia> adjustment_set(pag3, :X, :Y) === nothing # not adjustment-amenable
trueadjustment_set(cg::DAG, x, y; type::Symbol = :optimal) -> Union{Nothing,Vector{Symbol}}Compute an adjustment set for the causal effect of x on y in cg, or nothing if no valid adjustment set exists.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
Three types are supported:
:parents: $\bigcup \mathrm{Pa}(x) \setminus \{x, y\}$.:backdoor: Pearl backdoor formula.:optimal: O-set $\mathrm{Pa}(\mathrm{cn}(x,y)) \setminus \mathrm{Forb}(x,y)$, where $\mathrm{cn}(x,y)$ is the set of nodes other thanxon proper causal paths fromxtoy(paths that meetxonly at their first node) and $\mathrm{Forb}(x,y) = \mathrm{De}(\mathrm{cn}(x,y)) \cup x$.
The O-set (Henckel et al., 2022) is defined when every node in y is a descendant of x. It is then a valid adjustment set whenever any valid adjustment set exists, and it is asymptotically optimal among them. If some node in y is not a descendant of x (so x has no causal effect on it), :optimal emits a warning: its result is then not guaranteed to be optimal, and it may return nothing even though a valid adjustment set exists (e.g. x = :X, y = :Y in A --> X, A --> Y); use :backdoor or all_adjustment_sets instead.
The type keyword is specific to the DAG/AbstractPDAG methods; the ADMG/AbstractAG/PAG methods of adjustment_set take no type keyword and always return a fixed inclusion-minimal valid adjustment set.
cg::DAG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
type::Symbol = :optimal: one of:parents,:backdoor, or:optimal.
A Vector{Symbol} adjustment set, or nothing if none exists.
julia> dag = DAG(
"C --> X, X --> F, X --> D --> Y, A --> X,
A --> K --> Y, D --> G, Y --> H");
julia> sort(adjustment_set(dag, :X, :Y; type = :parents))
2-element Vector{Symbol}:
:A
:C
julia> adjustment_set(dag, :X, :Y; type = :backdoor)
1-element Vector{Symbol}:
:A
julia> adjustment_set(dag, :X, :Y; type = :optimal)
1-element Vector{Symbol}:
:K
julia> dag2 = DAG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> sort(adjustment_set(dag2, [:X1, :X2], :Y; type = :optimal))
2-element Vector{Symbol}:
:L1
:L2adjustment_set(cg::AbstractPDAG, x, y; type::Symbol = :optimal)
-> Union{Nothing,Vector{Symbol}}Compute an adjustment set for the causal effect of x on y in cg, or nothing if no valid adjustment set exists.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
Two types are supported:
:parents: directed parents ofx.:optimal: O-set $\mathrm{Pa}(\mathrm{Cn}(x,y)) \setminus \mathrm{Forb}(x,y)$, where $\mathrm{Cn}(x,y)$ is the set of nodes on proper possibly directed paths fromxtoyand $\mathrm{Forb}(x,y)$ is the forbidden set (seeis_valid_adjustment).
The O-set (Henckel et al., 2022) is defined when every node in y lies on a proper possibly causal path from x. If additionally the effect is amenable (see is_valid_adjustment), it is a valid adjustment set whenever any valid adjustment set exists, and it is asymptotically optimal among them in every DAG that cg represents. If some node in y lies on no such path (so x has no causal effect on it in any represented DAG), :optimal emits a warning: its result is then not guaranteed to be optimal, and it may return nothing even though a valid adjustment set exists; use all_adjustment_sets instead.
The type keyword is specific to the DAG/AbstractPDAG methods; the ADMG/AbstractAG/PAG methods of adjustment_set take no type keyword and always return a fixed inclusion-minimal valid adjustment set.
cg::AbstractPDAG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
type::Symbol = :optimal: one of:parentsor:optimal.
A Vector{Symbol} adjustment set, or nothing if none exists.
julia> pdag = PDAG("A --> X --> Y, A --> Y");
julia> adjustment_set(pdag, :X, :Y)
1-element Vector{Symbol}:
:A
julia> is_valid_adjustment(pdag, :X, :Y, :A)
true
julia> pdag2 = PDAG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> sort(adjustment_set(pdag2, [:X1, :X2], :Y))
2-element Vector{Symbol}:
:L1
:L2CausalStructures.is_valid_adjustment — Function
is_valid_adjustment(cg::DAG, x, y, z = Symbol[]) -> BoolReturn true if z is a valid adjustment set for estimating the total causal effect of x on y in cg using the Generalized Adjustment Criterion (GAC).
x and y may each be a single Symbol or an AbstractVector{Symbol}.
For a DAG this reduces to the adjustment criterion of Shpitser (2012), which is sound and complete for adjustment. z may include descendants of x as long as they are not on a causal path from x to y, so this criterion accepts some valid sets the backdoor criterion rejects.
cg::DAG: the graph to check.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).z::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: the candidate adjustment set.
true if z is a valid adjustment set, false otherwise.
is_valid_adjustment(cg::Union{ADMG,AbstractAG}, x, y, z = Symbol[]) -> BoolReturn true if z is a valid adjustment set for estimating the total causal effect of x on y in cg using the Generalized Adjustment Criterion (GAC).
x, y, and z may each be a single Symbol or an AbstractVector{Symbol}.
A set z is valid if it contains no forbidden node (no node in De(cn(x,y) \ {y}) ∪ {x}, where cn(x,y) are the proper causal nodes from x to y) and x and y are m-separated by z in the proper backdoor graph of cg.
cg::Union{ADMG,AbstractAG}: the graph to check.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).z::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: the candidate adjustment set.
true if z is a valid adjustment set, false otherwise.
julia> admg = ADMG("L --> X --> Y, L --> Y");
julia> is_valid_adjustment(admg, :X, :Y) # empty Z does not block L --> Y
false
julia> is_valid_adjustment(admg, :X, :Y, :L) # conditioning on L blocks the backdoor path
truejulia> mag = MAG("A <-> X, A --> M --> Y, X --> Y");
julia> is_valid_adjustment(mag, :X, :Y)
false
julia> is_valid_adjustment(mag, :X, :Y, :A)
truejulia> admg2 = ADMG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> is_valid_adjustment(admg2, [:X1, :X2], :Y, [:L1, :L2])
trueis_valid_adjustment(cg::PAG, x, y, z = Symbol[]) -> BoolReturn true if z is a valid adjustment set for estimating the total causal effect of x on y in cg using the Generalized Adjustment Criterion (GAC).
x, y, and z may each be a single Symbol or an AbstractVector{Symbol}.
Circle marks collapse to tails for reachability purposes, and edges out of x are only removed from the proper backdoor graph if they are visible: there must be a witness node with an arrowhead into x (or reaching x via a collider path through parents of the edge's target) that is not adjacent to that target, ruling out a latent confounder riding along the edge (Zhang, 2006). If cg is not adjustment-amenable relative to (x, y) – some proper possibly-directed path from x to y starts with an invisible edge – no set satisfies the criterion, including the empty set.
cg::PAG: the graph to check.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).z::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: the candidate adjustment set.
true if z is a valid adjustment set, false otherwise.
julia> mag = MAG("B --> X, A <-> X, A --> Y, X --> Y");
julia> pag = mag_to_pag(mag);
julia> is_valid_adjustment(pag, :X, :Y)
false
julia> is_valid_adjustment(pag, :X, :Y, :A)
true
julia> mag2 = MAG(
"B1 --> X1, A1 <-> X1, A1 --> Y, X1 --> Y,
B2 --> X2, A2 <-> X2, A2 --> Y, X2 --> Y");
julia> pag2 = mag_to_pag(mag2);
julia> is_valid_adjustment(pag2, [:X1, :X2], :Y, [:A1, :A2])
trueis_valid_adjustment(cg::AbstractPDAG, x, y, z = Symbol[]) -> BoolReturn true if z is a valid adjustment set for estimating the total causal effect of x on y in cg using the Generalized Adjustment Criterion (GAC).
x, y, and z may each be a single Symbol or an AbstractVector{Symbol}.
The effect must be amenable (every proper possibly causal path from x to y starts with a directed edge), z must avoid the forbidden set (possible descendants of nodes on proper possibly causal paths), and z must block every proper non-causal path of definite status. A z overlapping y is never valid.
A PDAG is first closed under Meek's rules (see meek_closure), since the criterion is stated for MPDAGs.
cg::AbstractPDAG: the graph to check.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).z::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: the candidate adjustment set.
true if z is a valid adjustment set, false otherwise.
CausalStructures.all_adjustment_sets — Function
all_adjustment_sets(cg::DAG, x, y;
minimal::Bool = true, max_size::Int = 3)
-> Vector{Vector{Symbol}}Return all valid adjustment sets for the total causal effect of x on y in cg, up to size max_size.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
Sets are validated using is_valid_adjustment. When minimal = true (default), only inclusion-minimal sets are returned.
cg::DAG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
minimal::Bool = true: return only inclusion-minimal sets.max_size::Int = 3: the maximum candidate set size to consider.
A Vector{Vector{Symbol}} of valid adjustment sets.
julia> dag = DAG("A --> X, B --> X, X --> Y, A --> Y");
julia> all_adjustment_sets(dag, :X, :Y)
1-element Vector{Vector{Symbol}}:
[:A]
julia> all_adjustment_sets(dag, :X, :Y; minimal=false)
2-element Vector{Vector{Symbol}}:
[:A]
[:A, :B]
julia> dag2 = DAG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> all_adjustment_sets(dag2, [:X1, :X2], :Y)
1-element Vector{Vector{Symbol}}:
[:L1, :L2]all_adjustment_sets(cg::Union{ADMG,AbstractAG,AbstractPDAG}, x, y;
minimal::Bool = true, max_size::Int = 3)
-> Vector{Vector{Symbol}}Return all valid adjustment sets for the total causal effect of x on y in cg, up to size max_size.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
Bruteforces over subsets of the allowed universe of nodes (nodes that are not forbidden and not y), checking each for validity using is_valid_adjustment. When minimal = true (default), only inclusion-minimal sets are returned.
cg::Union{ADMG,AbstractAG,AbstractPDAG}: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
minimal::Bool = true: return only inclusion-minimal sets.max_size::Int = 3: the maximum candidate set size to consider.
A Vector{Vector{Symbol}} of valid adjustment sets.
julia> admg = ADMG("L --> X --> Y, L --> Y");
julia> all_adjustment_sets(admg, :X, :Y)
1-element Vector{Vector{Symbol}}:
[:L]julia> mag = MAG("A <-> X, A --> M --> Y, X --> Y");
julia> all_adjustment_sets(mag, :X, :Y)
2-element Vector{Vector{Symbol}}:
[:A]
[:M]julia> mpdag = MPDAG("A --> X, B --> X, X --> Y, A --> Y, B --- K, K --> Y");
julia> all_adjustment_sets(mpdag, :X, :Y)
2-element Vector{Vector{Symbol}}:
[:A, :B]
[:A, :K]
julia> all_adjustment_sets(mpdag, :X, :Y, minimal = false)
3-element Vector{Vector{Symbol}}:
[:A, :B]
[:A, :K]
[:A, :B, :K]julia> admg2 = ADMG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> all_adjustment_sets(admg2, [:X1, :X2], :Y)
1-element Vector{Vector{Symbol}}:
[:L1, :L2]all_adjustment_sets(cg::PAG, x, y;
minimal::Bool = true, max_size::Int = 3)
-> Vector{Vector{Symbol}}Return all valid adjustment sets for the total causal effect of x on y in cg, up to size max_size.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
Sets are validated using is_valid_adjustment. When minimal = true (default), only inclusion-minimal sets are returned.
cg::PAG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
minimal::Bool = true: return only inclusion-minimal sets.max_size::Int = 3: the maximum candidate set size to consider.
A Vector{Vector{Symbol}} of valid adjustment sets.
julia> mag = MAG("B --> X, A <-> X, A --> Y, X --> Y");
julia> pag = mag_to_pag(mag);
julia> all_adjustment_sets(pag, :X, :Y)
1-element Vector{Vector{Symbol}}:
[:A]
julia> mag2 = MAG(
"B1 --> X1, A1 <-> X1, A1 --> Y, X1 --> Y,
B2 --> X2, A2 <-> X2, A2 --> Y, X2 --> Y");
julia> pag2 = mag_to_pag(mag2);
julia> all_adjustment_sets(pag2, [:X1, :X2], :Y)
1-element Vector{Vector{Symbol}}:
[:A1, :A2]all_adjustment_sets(cg::AbstractPDAG, x, y;
minimal::Bool = true, max_size::Int = 3)
-> Vector{Vector{Symbol}}Return all valid adjustment sets for the total causal effect of x on y in cg, up to size max_size.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
Sets are validated using is_valid_adjustment. When minimal = true (default), only inclusion-minimal sets are returned.
cg::AbstractPDAG: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
minimal::Bool = true: return only inclusion-minimal sets.max_size::Int = 3: the maximum candidate set size to consider.
A Vector{Vector{Symbol}} of valid adjustment sets.
julia> mpdag = MPDAG("A --> X, B --> X, X --> Y, A --> Y, B --- K, K --> Y");
julia> all_adjustment_sets(mpdag, :X, :Y)
2-element Vector{Vector{Symbol}}:
[:A, :B]
[:A, :K]
julia> all_adjustment_sets(mpdag, :X, :Y, minimal = false)
3-element Vector{Vector{Symbol}}:
[:A, :B]
[:A, :K]
[:A, :B, :K]
julia> pdag2 = PDAG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> all_adjustment_sets(pdag2, [:X1, :X2], :Y)
1-element Vector{Vector{Symbol}}:
[:L1, :L2]CausalStructures.possible_optimal_adjustment_sets — Function
possible_optimal_adjustment_sets(cg::AbstractPDAG, x::Symbol, y::Symbol) ->
Vector{Union{Vector{Symbol},Nothing}}Return the optimal adjustment set (Henckel, Perkovic & Maathuis 2022) for each locally valid orientation of x's undirected neighbors in cg. The graph part of optimal-adjustment IDA (Witte, Henckel, Maathuis & Didelez 2020).
For each subset S of x's undirected neighbors that possible_parent_sets would accept (no new v-structure with collider x), this directs S --> x and x's remaining undirected neighbors away from x, closes the result under Meek's rules (apply_background_knowledge), and returns adjustment_set(mpdag, x, y; type = :optimal) for the resulting MPDAG. Entries are in the same order (one per locally valid orientation) as possible_parent_sets(cg, x), so pairing the two element-wise recovers, for each orientation, both its parent set and its optimal adjustment set.
An entry is nothing when y is among the resulting parents of x for that orientation, so no adjustment set applies, or when y is not a possible descendant of x in the resulting MPDAG. In the latter case the O-set is undefined and adjustment_set warns, but a valid adjustment set may still exist.
cg::AbstractPDAG: the graph to search.x::Symbol: the treatment node.y::Symbol: the outcome node.
A Vector{Union{Vector{Symbol},Nothing}}, one entry per locally valid orientation of x's undirected neighbors.
julia> cpdag = CPDAG("X1 --- X2 + X3 + X4, X3 + X4 --> Y");
julia> possible_parent_sets(cpdag, :X1)
4-element Vector{Vector{Symbol}}:
[]
[:X2]
[:X3]
[:X4]
julia> possible_optimal_adjustment_sets(cpdag, :X1, :Y)
4-element Vector{Union{Nothing, Vector{Symbol}}}:
Symbol[]
Symbol[]
[:X3]
[:X4]CausalStructures.pagcauses — Function
pagcauses(cg::PAG, x::Symbol, y::Symbol) -> Vector{Vector{Symbol}}Return adjustment sets for the effect of x on y (Wang et al. 2025, Algorithm 1, "PAGcauses"; Theorem 4). Together, these give every possible causal effect identifiable by adjustment in some DAG consistent with cg.
Returns Vector{Symbol}[] if x is not a possible ancestor of y. If the effect is directly identifiable in cg, returns the corresponding backdoor_set.
Throws ArgumentError if cg contains selection bias (undirected edges), which is not covered by the algorithm.
cg::PAG: the graph to search.x::Symbol: the treatment node.y::Symbol: the outcome node.
A Vector{Vector{Symbol}} of adjustment sets.
Rather than enumerating the $O(3^{(d^2-d)/2})$ MAGs consistent with a PAG and checking D-SEP in each, the algorithm performs a graphical check for each of the $O(2^d)$ candidate sets. For each possible local structure at x (possible_local_structures), it constructs the corresponding maximal local MAG (maximal_local_mag) and searches for adjustment sets satisfying Theorem 2, pruned by DD-SEP (Definition 7). The overall complexity is $O(5^d d^6)$ (Sec. 3.4).
Parallelizes over Threads.nthreads() when there are enough candidates.
julia> pag = PAG("X o-> Y, X o-> C, X o-> A, Y o-o C, C o-o A, B o-> C, B o-> Y, B o-> A");
julia> sort(sort.(pagcauses(pag, :X, :Y)))
5-element Vector{Vector{Symbol}}:
[]
[:A, :B]
[:A, :B, :C]
[:B, :C]
[:C]Backdoor
CausalStructures.is_valid_backdoor — Function
is_valid_backdoor(cg::Union{DAG,ADMG}, x, y, z = Symbol[]) -> BoolReturn true if z satisfies the backdoor criterion for the causal effect of x on y in cg.
x, y, and z may each be a single Symbol or an AbstractVector{Symbol}.
z is a valid backdoor set if (1) no node in z is a descendant of x, and (2) z blocks every backdoor path from x to y.
DAG: equivalently, every parent ofxis d-separated fromygivenz ∪ x.ADMG: equivalently,zm-separatesxfromyin the graph obtained by removing every directed edge out ofx.
cg::Union{DAG,ADMG}: the graph to check.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).z::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: the candidate backdoor set.
true if z is a valid backdoor set, false otherwise.
julia> dag = DAG("A --> X --> Y, A --> Y");
julia> is_valid_backdoor(dag, :X, :Y) # empty Z leaves the backdoor path A --> Y open
false
julia> is_valid_backdoor(dag, :X, :Y, :A) # conditioning on A blocks the backdoor path
true
julia> admg = ADMG("A --> X --> Y, A <-> Y");
julia> is_valid_backdoor(admg, :X, :Y) # A <-> Y is an unobserved confounder of the path
false
julia> is_valid_backdoor(admg, :X, :Y, :A) # conditioning on A still blocks it
true
julia> dag2 = DAG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> is_valid_backdoor(dag2, [:X1, :X2], :Y) # both confounding paths are open
false
julia> is_valid_backdoor(dag2, [:X1, :X2], :Y, [:L1, :L2]) # conditioning on both blocks them
trueCausalStructures.all_backdoor_sets — Function
all_backdoor_sets(cg::Union{DAG,ADMG}, x, y;
minimal::Bool = true, max_size::Int = 3)
-> Vector{Vector{Symbol}}Return all sets satisfying the backdoor criterion for the causal effect of x on y in cg, up to size max_size.
x and y may each be a single Symbol or an AbstractVector{Symbol}.
Bruteforces over subsets of the allowed universe of nodes (nodes that are not descendants of x and not y), checking each for validity using is_valid_backdoor. When minimal = true (default), only inclusion-minimal sets are returned. For ADMG, the universe is drawn from the graph's observed nodes; latent confounders are already summarized by bidirected edges and are never candidates.
cg::Union{DAG,ADMG}: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
minimal::Bool = true: return only inclusion-minimal sets.max_size::Int = 3: the maximum candidate set size to consider.
A Vector{Vector{Symbol}} of valid backdoor sets.
julia> dag = DAG("A --> X --> Y, A --> Y");
julia> all_backdoor_sets(dag, :X, :Y)
1-element Vector{Vector{Symbol}}:
[:A]
julia> admg = ADMG("A --> X --> Y, A <-> Y");
julia> all_backdoor_sets(admg, :X, :Y)
1-element Vector{Vector{Symbol}}:
[:A]
julia> dag2 = DAG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");
julia> all_backdoor_sets(dag2, [:X1, :X2], :Y)
1-element Vector{Vector{Symbol}}:
[:L1, :L2]CausalStructures.backdoor_set — Function
backdoor_set(cg::DAG, x::Symbol, y::Symbol) -> Union{Vector{Symbol},Nothing}Return a generalized back-door set relative to (x, y) and cg using the Generalized Backdoor Criterion (GBC; Maathuis and Colombo (2015), Corollary 4.1), or nothing if none exists.
For a DAG this reduces to Pearl's original result (Pearl (2009)): a generalized back-door set exists if and only if y is not a parent of x, and when it exists, parents(cg, x) is such a set (not necessarily minimal).
cg::DAG: the graph to search.x::Symbol: the treatment node.y::Symbol: the outcome node.
A Vector{Symbol} back-door set, or nothing if none exists.
julia> dag = DAG("A --> X --> Y, A --> Y");
julia> backdoor_set(dag, :X, :Y)
1-element Vector{Symbol}:
:A
julia> backdoor_set(dag, :Y, :A) === nothing # A is a parent of Y
truebackdoor_set(cg::ADMG, x::Symbol, y::Symbol) -> Union{Vector{Symbol},Nothing}Return a generalized back-door set relative to (x, y) and cg, or nothing if none exists.
cg::ADMG: the graph to search.x::Symbol: the treatment node.y::Symbol: the outcome node.
A Vector{Symbol} back-door set, or nothing if none exists.
julia> admg = ADMG("A --> X --> Y, A --> Y");
julia> backdoor_set(admg, :X, :Y)
1-element Vector{Symbol}:
:A
julia> admg2 = ADMG("A --> X --> Y, A <-> Y"); # latent confounder A also causes Y
julia> backdoor_set(admg2, :X, :Y)
1-element Vector{Symbol}:
:A
julia> admg3 = ADMG(directed(:X, :Y), bidirected(:X, :Y)); # direct edge plus latent confounder
julia> backdoor_set(admg3, :X, :Y) === nothing # Y stays adjacent to X in M_X via X <-> Y
truebackdoor_set(cg::CPDAG, x::Symbol, y::Symbol) -> Union{Vector{Symbol},Nothing}Return a generalized back-door set relative to (x, y) and cg using the Generalized Backdoor Criterion (GBC; Maathuis and Colombo (2015), Corollary 4.2), or nothing if none exists.
Let C_X be cg with every directed edge out of x removed. A generalized back-door set exists if and only if y is not a parent of x and y is not a possible descendant of x in C_X; when it exists, parents(cg, x) is such a set (not necessarily minimal).
Deliberately not defined for MPDAG or plain PDAG: Corollary 4.2 relies on a CPDAG-specific fact (every back-door path into x passes through a compelled parent) that fails once background knowledge introduces a partially directed cycle.
cg::CPDAG: the graph to search.x::Symbol: the treatment node.y::Symbol: the outcome node.
A Vector{Symbol} back-door set, or nothing if none exists.
backdoor_set(cg::MAG, x::Symbol, y::Symbol) -> Union{Vector{Symbol},Nothing}Return a generalized back-door set relative to (x, y) and cg using the Generalized Backdoor Criterion (GBC; Maathuis and Colombo (2015), Corollary 4.3), or nothing if none exists.
Let M_X be cg with every visible directed edge out of x removed (Definition 4.2). A generalized back-door set exists if and only if y is not adjacent to x in M_X and D-SEP(x, y, M_X) does not intersect the descendants of x in cg; when it exists, D-SEP(x, y, M_X) is such a set (not necessarily minimal).
cg::MAG: the graph to search.x::Symbol: the treatment node.y::Symbol: the outcome node.
A Vector{Symbol} back-door set, or nothing if none exists.
backdoor_set(cg::PAG, x::Symbol, y::Symbol) -> Union{Vector{Symbol},Nothing}Return a generalized back-door set relative to (x, y) and cg using the Generalized Backdoor Criterion (GBC; Maathuis and Colombo (2015), Theorem 4.1), or nothing if none exists.
Let R be any MAG in the Markov equivalence class of cg with the same number of edges into x as cg (Definition 4.2; such an R always exists, Lemma 7.6), and let R_X be R with every directed edge out of x that is visible in cg removed. A generalized back-door set exists if and only if y is not adjacent to x in R_X and D-SEP(x, y, R_X) does not intersect the possible descendants of x in cg; when it exists, D-SEP(x, y, R_X) is such a set (not necessarily minimal; Theorem 4.1 shows the result does not depend on which admissible R is used).
cg::PAG: the graph to search.x::Symbol: the treatment node.y::Symbol: the outcome node.
A Vector{Symbol} back-door set, or nothing if none exists.
julia> mag = MAG("B --> X, A <-> X, A --> Y, X --> Y");
julia> pag = mag_to_pag(mag); # A o-> X, A --> Y, B o-> X, X --> Y
julia> sort(backdoor_set(pag, :X, :Y))
2-element Vector{Symbol}:
:A
:BFrontdoor
CausalStructures.frontdoor_set — Function
frontdoor_set(cg::Union{DAG,ADMG}, x, y; include=[], restrict=nothing)
-> Vector{Symbol} or nothingReturn a front-door adjustment set Z with include ⊆ Z ⊆ restrict satisfying all three front-door conditions relative to (x, y) in cg, or nothing if no such set exists.
x, y, include, and restrict may each be a single Symbol or an AbstractVector{Symbol}.
include: nodes forced into the set.restrict: candidate pool from which the set is drawn. Defaults to all nodes incgexceptxandy.
Implements Algorithm 1 of (Jeong et al., 2022):
- Step 1 (GETCAND2NDFDC): drop candidates that have a backdoor path from X.
- Step 2 (GETCAND3RDFDC): drop candidates for which condition 3 cannot be met.
- Step 3: check that the remaining set blocks all directed paths X –> Y.
The returned set is the full R'' from Steps 1-2 (not necessarily minimal). For ADMG, backdoor and condition-3 checks are done via m-separation, with bidirected edges treated as latent-confounder edges (a spouse contributes an arrowhead into a node just like a directed parent does).
cg::Union{DAG,ADMG}: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
include::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: nodes forced into the set.restrict::Union{Nothing,Symbol,AbstractVector{Symbol}} = nothing: candidate pool from which the set is drawn. Defaults to all nodes exceptxandy.
A Vector{Symbol} front-door set, or nothing if none exists.
julia> dag = DAG("U --> X + Y, X --> Z --> Y");
julia> frontdoor_set(dag, :X, :Y; restrict = :Z)
1-element Vector{Symbol}:
:ZJeong et al. (2022) Fig. 1b:
julia> dag = DAG("U1 --> X + Y, U2 --> X + D, X --> A, A --> B + C + D, B + C + D --> Y");
julia> frontdoor_set(dag, :X, :Y; restrict = [:A, :B, :C, :D])
3-element Vector{Symbol}:
:A
:B
:C
julia> frontdoor_set(dag, :X, :Y; include = :C, restrict = [:A, :C])
2-element Vector{Symbol}:
:A
:C
julia> frontdoor_set(dag, :X, :Y; include = :D, restrict = [:A, :B, :C, :D]) === nothing
trueFig. 1b latent-projected to an ADMG: U1 -> X <-> Y, U2 -> X <-> D:
julia> admg = ADMG("X <-> Y, X <-> D, X --> A, A --> B + C + D, B + C + D --> Y");
julia> frontdoor_set(admg, :X, :Y; restrict = [:A, :B, :C, :D])
3-element Vector{Symbol}:
:A
:B
:Cjulia> dag2 = DAG("U1 --> X1 + Y1, U2 --> X2 + Y2, X1 --> M, X2 --> M, M --> Y1 + Y2");
julia> frontdoor_set(dag2, [:X1, :X2], [:Y1, :Y2]; restrict = :M)
1-element Vector{Symbol}:
:MCausalStructures.is_valid_frontdoor — Function
is_valid_frontdoor(cg::DAG, x, y, z = Symbol[]) -> BoolReturn true if z satisfies the front-door criterion for the causal effect of x on y in cg.
x, y, and z may each be a single Symbol or an AbstractVector{Symbol}.
z is a valid front-door set if:
zintercepts all directed paths fromxtoy.- There are no unblocked backdoor paths from
xto any node inz(given the empty set). - For every node
ziinz, all backdoor paths fromzitoyare blocked byxtogether with the remaining nodes inz(i.e., conditioned onx ∪ (z \ {zi})).
When these conditions hold, the causal effect is identified by the front-door formula, even in the presence of unmeasured confounders between x and y.
cg::DAG: the graph to check.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).z::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: the candidate front-door set.
true if z is a valid front-door set, false otherwise.
julia> dag = DAG("U --> X --> M --> Y, U --> Y");
julia> is_valid_frontdoor(dag, :X, :Y, :M) # M mediates X -> Y and satisfies all conditions
true
julia> is_valid_frontdoor(dag, :X, :Y) # empty Z leaves directed path X -> M -> Y open
false
julia> is_valid_frontdoor(dag, :X, :Y, :U) # U does not intercept X -> M -> Y
false
julia> dag2 = DAG("U1 --> X1 + Y1, U2 --> X2 + Y2, X1 --> M, X2 --> M, M --> Y1 + Y2");
julia> is_valid_frontdoor(dag2, [:X1, :X2], [:Y1, :Y2], :M) # M mediates every X --> Y path
trueCausalStructures.all_frontdoor_sets — Function
all_frontdoor_sets(cg::Union{DAG,ADMG}, x, y; include=[], restrict=nothing)
-> Vector{Vector{Symbol}}Return all front-door adjustment sets Z with include ⊆ Z ⊆ restrict relative to (x, y) in cg.
x, y, include, and restrict may each be a single Symbol or an AbstractVector{Symbol}.
include: nodes forced into every returned set.restrict: candidate pool from which sets are drawn. Defaults to all nodes incgexceptxandy.
Implements Algorithm 2 (LISTFDSETS) of (Jeong et al., 2022). The algorithm has polynomial-delay guarantees: it outputs the first result in polynomial time and takes polynomial time between consecutive results. For ADMG, see the note on m-separation in frontdoor_set.
cg::Union{DAG,ADMG}: the graph to search.x::Union{Symbol,AbstractVector{Symbol}}: the treatment node(s).y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
include::Union{Symbol,AbstractVector{Symbol}} = Symbol[]: nodes forced into every returned set.restrict::Union{Nothing,Symbol,AbstractVector{Symbol}} = nothing: candidate pool from which sets are drawn. Defaults to all nodes exceptxandy.
A Vector{Vector{Symbol}} of front-door sets.
julia> dag = DAG("U --> X + Y, X --> Z --> Y");
julia> all_frontdoor_sets(dag, :X, :Y; restrict = :Z)
1-element Vector{Vector{Symbol}}:
[:Z]Jeong et al. (2022) Fig. 1b:
julia> dag = DAG("U1 --> X + Y, U2 --> X + D, X --> A, A --> B + C + D, B + C + D --> Y");
julia> sort(all_frontdoor_sets(dag, :X, :Y; restrict = [:A, :B, :C, :D]))
4-element Vector{Vector{Symbol}}:
[:A]
[:A, :B]
[:A, :B, :C]
[:A, :C]Fig. 1b latent-projected to an ADMG: U1 -> X <-> Y, U2 -> X <-> D:
julia> admg = ADMG("X <-> Y, X <-> D, X --> A, A --> B + C + D, B + C + D --> Y");
julia> sort(all_frontdoor_sets(admg, :X, :Y; restrict = [:A, :B, :C, :D]))
4-element Vector{Vector{Symbol}}:
[:A]
[:A, :B]
[:A, :B, :C]
[:A, :C]julia> dag2 = DAG("U1 --> X1 + Y1, U2 --> X2 + Y2, X1 --> M, X2 --> M, M --> Y1 + Y2");
julia> all_frontdoor_sets(dag2, [:X1, :X2], [:Y1, :Y2]; restrict = :M)
1-element Vector{Vector{Symbol}}:
[:M]Instrumental variables
CausalStructures.is_valid_iv — Function
is_valid_iv(cg::Union{DAG,ADMG}, x::Symbol, y, z) -> BoolReturn true if z is a valid instrumental set for the causal effect of x on y in cg.
y and z may each be a single Symbol or an AbstractVector{Symbol}. x must be a single Symbol, since the instrumental-set criterion is defined for one structural coefficient x -> y.
z is a valid instrumental set if:
- Every
zi ∈ zis d-/m-separated fromyin the graph obtained fromGby deleting the first edgex --> cof every causal path fromxtoy. This is the exclusion restriction:zcan only affectythroughx. - At least one
zi ∈ zis d-/m-connected toxinG. This is the relevance condition:zmust be associated with the treatment.
When the only causal path is the edge x --> y, this is Definition 3.1 of van der Zander et al. (2015); deleting the first edge of every causal path extends it to the total effect.
cg::Union{DAG,ADMG}: the graph to check.x::Symbol: the treatment node.y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).z::Union{Symbol,AbstractVector{Symbol}}: the candidate instrumental set.
true if z is a valid instrumental set, false otherwise.
Classic IV graph: Z --> X --> Y with hidden confounder U --> X, U --> Y:
julia> dag = DAG("Z --> X --> Y, U --> X + Y");
julia> is_valid_iv(dag, :X, :Y, :Z) # Z is a valid instrument
true
julia> is_valid_iv(dag, :X, :Y, :U) # U confounds X and Y; fails exclusion restriction
falseADMG: X <-> Y encodes the hidden confounder directly:
julia> admg = ADMG("X <-> Y, Z --> X --> Y");
julia> is_valid_iv(admg, :X, :Y, :Z)
trueZ instruments X, which affects two outcomes Y1 and Y2:
julia> dag2 = DAG("Z --> X, X --> Y1 + Y2, U --> X + Y1 + Y2");
julia> is_valid_iv(dag2, :X, [:Y1, :Y2], :Z)
trueCausalStructures.all_iv_sets — Function
all_iv_sets(cg::Union{DAG,ADMG}, x::Symbol, y;
minimal::Bool = true, max_size::Int = 3)
-> Vector{Vector{Symbol}}Return all valid instrumental sets for the causal effect of x on y in cg, up to size max_size.
y may be a single Symbol or an AbstractVector{Symbol}; x must be a single Symbol (see is_valid_iv).
Bruteforces over subsets of the allowed universe of nodes (nodes that are not x or y), checking each for validity using is_valid_iv. When minimal = true (default), only inclusion-minimal sets are returned.
cg::Union{DAG,ADMG}: the graph to search.x::Symbol: the treatment node.y::Union{Symbol,AbstractVector{Symbol}}: the outcome node(s).
minimal::Bool = true: return only inclusion-minimal sets.max_size::Int = 3: the maximum candidate set size to consider.
A Vector{Vector{Symbol}} of valid instrumental sets.
julia> dag = DAG("Z1 --> X, Z2 --> X, X --> Y, U --> X + Y");
julia> all_iv_sets(dag, :X, :Y)
2-element Vector{Vector{Symbol}}:
[:Z1]
[:Z2]
julia> dag2 = DAG("Z1 --> X, Z2 --> X, X --> Y1 + Y2, U --> X + Y1 + Y2");
julia> all_iv_sets(dag2, :X, [:Y1, :Y2])
2-element Vector{Vector{Symbol}}:
[:Z1]
[:Z2]Identification
CausalStructures.id — Function
id(cg::Union{DAG,ADMG}, x, y) -> Union{Estimand,Nothing}Identify the interventional distribution P(y | do(x)), following the ID algorithm of Shpitser and Pearl (2008).
x and y may each be a Symbol or a vector of them, and must be disjoint.
Bidirected edges represent latent confounders. To identify an effect in a DAG with unobserved variables, project them out first with latent_project.
The returned formula may mention variables outside x and y. Line 3 of the algorithm enlarges the intervention set by a set W whenever intervening on W provably changes nothing, and the resulting expression is then a function of those values too.
cg::Union{DAG,ADMG}: the graph to identify the effect in.x: the treatment node(s).y: the outcome node(s).
Back-door adjustment is recovered as the g-formula:
julia> dag = DAG("Z --> X + Y, X --> Y");
julia> id(dag, :X, :Y)
Σ_{Z} P(Y | X, Z) P(Z)The front-door graph, where X and Y are confounded but the effect is still identifiable through the mediator M:
julia> admg = ADMG("X --> M, M --> Y, X <-> Y");
julia> id(admg, :X, :Y)
Σ_{M} P(M | X) (Σ_{X'} P(X') P(Y | M, X'))The inner sum ranges over a value of X distinct from the one intervened on, so it is printed under a primed name; summation indices are always renamed away from the variables of the query.
The bow arc, the canonical unidentifiable effect:
julia> bow = ADMG("X --> Y, X <-> Y");
julia> id(bow, :X, :Y) === nothing
trueCausalStructures.idc — Function
idc(cg::Union{DAG,ADMG}, x, y; given) -> Union{Estimand,Nothing}Identify the conditional interventional distribution P(y | do(x), given), following the IDC algorithm of Shpitser and Pearl (2008). Each variable in given that satisfies rule 2 of do-calculus is moved from the conditioning set into the intervention set; whatever remains is handled by id and normalized:
\[P(y | do(x), z) = ID(y ∪ z, x) / Σ_y ID(y ∪ z, x)\]
x, y, and given must be pairwise disjoint. With an empty given this reduces to id.
cg::Union{DAG,ADMG}: the graph to identify the effect in.x: the treatment node(s).y: the outcome node(s).
given: the conditioning node(s).
julia> dag = DAG("Z --> X + Y, X --> Y");
julia> idc(dag, :X, :Y; given = :Z)
P(Y | X, Z)Conditioning on the mediator of the front-door graph leaves an effect that no longer depends on the intervened value at all:
julia> admg = ADMG("X --> M, M --> Y, X <-> Y");
julia> idc(admg, :X, :Y; given = :M)
Σ_{X'} P(X') P(Y | M, X')CausalStructures.idp — Function
idp(cg::PAG, x, y) -> Union{Estimand,Nothing}Identify the interventional distribution P(y | do(x)) from the PAG cg. This is the IDP algorithm of Jaber et al. (2022), complete for identifying marginal effects from a partial ancestral graph (a Markov equivalence class of causal diagrams), generalizing id from a single ADMG to the equivalence-class setting.
x and y may each be a Symbol or a vector of them, and must be disjoint.
cg::PAG: the graph to identify the effect in.x: the treatment node(s).y: the outcome node(s).
Effect identifiable through a witnessed backdoor:
julia> mag = MAG("B --> X, A <-> X, A --> Y, X --> Y");
julia> pag = mag_to_pag(mag);
julia> idp(pag, :X, :Y) === nothing
falseEffect not identifiable, since the PAG has no orientable structure at all (every conditional independence in the underlying equivalence class is consistent with several different causal directions):
julia> pag = mag_to_pag(MAG("Z --> X, Z --> Y, X --> Y"));
julia> idp(pag, :X, :Y) === nothing
trueCausalStructures.cidp — Function
cidp(cg::PAG, x, y; given) -> Union{Estimand,Nothing}Identify the conditional interventional distribution P(y | do(x), given) from the PAG cg. This is the CIDP algorithm of Jaber et al. (2022), complete for identifying conditional effects from a partial ancestral graph, generalizing idc from a single ADMG to the equivalence-class setting.
x, y, and given must be pairwise disjoint. With an empty given this reduces to idp.
cg::PAG: the graph to identify the effect in.x: the treatment node(s).y: the outcome node(s).
given: the conditioning node(s).
Effect identifiable through a witnessed backdoor, conditioning on the confounder:
julia> mag = MAG("B --> X, A <-> X, A --> Y, X --> Y");
julia> pag = mag_to_pag(mag);
julia> cidp(pag, :X, :Y; given = :A) === nothing
falseEffect not identifiable, for the same reason as in idp's example:
julia> pag = mag_to_pag(MAG("Z --> X, Z --> Y, X --> Y"));
julia> cidp(pag, :X, :Y; given = :Z) === nothing
trueEstimands
CausalStructures.Estimand — Type
EstimandA symbolic causal estimand: a formula expressed purely in terms of the observational distribution, as an immutable expression tree. Build one with the smart constructors prob, marginal, product, and quotient.
A single concrete type covers every node of the tree, tagged by kind; the fields double up across kinds rather than one field per possible meaning:
Fields
kind::Symbol: one of:prob,:marginal,:product,:quotient.vars::Vector{Symbol}: a:probnode's head, or a:marginalnode's summation index. Empty for:product/:quotient.given::Vector{Symbol}: a:probnode's conditioning set. Empty otherwise.terms::Vector{Estimand}: a:marginalnode's single summand (as a one-element vector), a:productnode's factors, or a:quotientnode's[numerator, denominator]. Empty for:prob.
Output formats
It can be rendered as text or as LaTeX:
julia> e = marginal([:Z], product([prob(:Y; given = [:X, :Z]), prob(:Z)]));
julia> string(e)
"Σ_{Z} P(Y | X, Z) P(Z)"
julia> e.kind
:marginal
julia> e.vars
1-element Vector{Symbol}:
:ZThe LaTeX form comes from the MIME"text/latex" method, so repr(MIME("text/latex"), e) gives $\sum_{Z} P(Y \mid X, Z) P(Z)$, used for frontend documentation.
CausalStructures.prob — Function
prob(vars; given = Symbol[]) -> EstimandBuild the probability term P(vars | given).
vars may be a single Symbol or a vector of them. Both lists are sorted and de-duplicated, and any variable appearing in given is dropped from vars (since P(X, Y | X) = P(Y | X)). A term left with an empty head is the constant 1.
CausalStructures.marginal — Function
marginal(index, term) -> EstimandSum term over the variables in index.
An empty index returns term unchanged, and nested sums are flattened into a single one. Summing a probability term over part of its own head marginalizes it directly: Σ_a P(a, b | c) becomes P(b | c).
Sums over a product are narrowed as far as they go: factors that do not mention the summation index are constants of the sum and are pulled out in front of it, and a factor whose whole head is summed over, and whose head no other factor under the sum mentions, sums to 1 and drops out. Under a ratio, the part of the index the denominator does not mention is summed in the numerator alone.
index: the variable(s) to sum over.term::Estimand: the expression being summed.
Here W is in the conditioning set rather than the head, so the sum stands:
julia> marginal([:W], prob(:Y; given = [:W, :X]))
Σ_{W} P(Y | W, X)Summing over a variable in the head marginalizes it away instead:
julia> marginal([:W], prob([:W, :Y]; given = [:X]))
P(Y | X)julia> marginal(Symbol[], prob(:Y))
P(Y)P(Z | X) does not mention W, so it leaves the sum, and what remains sums to one:
julia> marginal([:W], product([prob(:Z; given = [:X]), prob(:W; given = [:X, :Z])]))
P(Z | X)CausalStructures.product — Function
product(terms) -> EstimandMultiply terms together.
Nested products are flattened and factors equal to 1 are dropped. A single-factor product collapses to that factor, and an empty product is 1. The order of the remaining factors is preserved.
CausalStructures.quotient — Function
quotient(num, den) -> EstimandForm the ratio num / den.
A denominator of 1 returns num unchanged. When the denominator is itself a / b and the numerator is exactly a, the ratio collapses to b; this is what most often turns the divisions performed while identifying a PAG effect back into something readable. Otherwise, a factor appearing on both sides cancels, and a numerator factor and a denominator factor related by the chain rule P(a, b | c) = P(a | b, c) P(b | c) divide out: P(a, b | c) / P(b | c) becomes P(a | b, c), and P(a, b | c) / P(a | b, c) becomes P(b | c). That is what turns the ratios of marginals produced by the ID recursion back into ordinary conditionals.
Cancellation assumes the cancelled factor is non-zero, which is the positivity assumption the identification results are stated under anyway.
num::Estimand: the numerator.den::Estimand: the denominator.
julia> quotient(prob([:Y, :Z]; given = [:X]), prob(:Z; given = [:X]))
P(Y | X, Z)julia> quotient(prob([:W, :Y, :Z]), prob(:W; given = [:Y, :Z]))
P(Y, Z)julia> a = quotient(prob([:Y, :Z]), prob(:X));
julia> b = marginal(:W, quotient(prob([:W, :Y, :Z]), prob(:W; given = [:X])));
julia> quotient(a, quotient(a, b))
Σ_{W} (P(W, Y, Z) / P(W | X))The ratio is kept when it is not a conditional, here because the two terms condition on different things:
julia> quotient(prob([:Y, :Z]; given = [:X]), prob(:Z))
P(Y, Z | X) / P(Z)