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
true
source
adjustment_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
true
source
adjustment_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
true
source
adjustment_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 than x on proper causal paths from x to y (paths that meet x only 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).

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
 :L2
source
adjustment_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 of x.
  • :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 from x to y and $\mathrm{Forb}(x,y)$ is the forbidden set (see is_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).

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
 :L2
source
CausalStructures.is_valid_adjustment — Function
is_valid_adjustment(cg::DAG, x, y, z = Symbol[]) -> Bool

Return 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.

julia> dag = DAG("A --> X --> Y, A --> Y");

julia> is_valid_adjustment(dag, :X, :Y)
false

julia> is_valid_adjustment(dag, :X, :Y, :A)
true

julia> dag2 = DAG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");

julia> is_valid_adjustment(dag2, [:X1, :X2], :Y, [:L1, :L2])
true
source
is_valid_adjustment(cg::Union{ADMG,AbstractAG}, x, y, z = Symbol[]) -> Bool

Return 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
true
julia> 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)
true
julia> admg2 = ADMG("L1 --> X1, L1 --> Y, L2 --> X2, L2 --> Y, X1 --> Y, X2 --> Y");

julia> is_valid_adjustment(admg2, [:X1, :X2], :Y, [:L1, :L2])
true
source
is_valid_adjustment(cg::PAG, x, y, z = Symbol[]) -> Bool

Return 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])
true
source
is_valid_adjustment(cg::AbstractPDAG, x, y, z = Symbol[]) -> Bool

Return 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.

julia> pdag = PDAG("A --> X --> Y, A --> Y");

julia> is_valid_adjustment(pdag, :X, :Y)
false

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> is_valid_adjustment(pdag2, [:X1, :X2], :Y, [:L1, :L2])
true
source
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).

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]
source
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).

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]
source
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).

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]
source
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).

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]
source
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]
source
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]
source

Backdoor

CausalStructures.is_valid_backdoor — Function
is_valid_backdoor(cg::Union{DAG,ADMG}, x, y, z = Symbol[]) -> Bool

Return 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 of x is d-separated from y given z ∪ x.
  • ADMG: equivalently, z m-separates x from y in the graph obtained by removing every directed edge out of x.
  • 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
true
source
CausalStructures.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).

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]
source
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
true
source
backdoor_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
true
source
backdoor_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.

julia> cpdag = CPDAG("A --> X <-- C, X --> Y");  # A --> X <-- C protects both edges into X

julia> sort(backdoor_set(cpdag, :X, :Y))
2-element Vector{Symbol}:
 :A
 :C

julia> cpdag2 = CPDAG("X --- Y");

julia> backdoor_set(cpdag2, :X, :Y) === nothing  # Y is a possible descendant of X in C_X
true
source
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.

julia> mag = MAG("A <-> X, A --> M --> Y, X --> Y");

julia> backdoor_set(mag, :X, :Y)
1-element Vector{Symbol}:
 :A

julia> mag2 = MAG("X --> Y");

julia> backdoor_set(mag2, :X, :Y) === nothing  # X --> Y is invisible here, so Y ∈ adj(X, M_X)
true
source
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
 :B
source

Frontdoor

CausalStructures.frontdoor_set — Function
frontdoor_set(cg::Union{DAG,ADMG}, x, y; include=[], restrict=nothing)
    -> Vector{Symbol} or nothing

Return 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 in cg except x and y.

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).

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}:
 :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> 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
true

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> frontdoor_set(admg, :X, :Y; restrict = [:A, :B, :C, :D])
3-element Vector{Symbol}:
 :A
 :B
 :C
julia> 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}:
 :M
source
CausalStructures.is_valid_frontdoor — Function
is_valid_frontdoor(cg::DAG, x, y, z = Symbol[]) -> Bool

Return 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:

  1. z intercepts all directed paths from x to y.
  2. There are no unblocked backdoor paths from x to any node in z (given the empty set).
  3. For every node zi in z, all backdoor paths from zi to y are blocked by x together with the remaining nodes in z (i.e., conditioned on x ∪ (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
true
source
CausalStructures.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 in cg except x and y.

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).

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]
source

Instrumental variables

CausalStructures.is_valid_iv — Function
is_valid_iv(cg::Union{DAG,ADMG}, x::Symbol, y, z) -> Bool

Return 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:

  1. Every zi ∈ z is d-/m-separated from y in the graph obtained from G by deleting the first edge x --> c of every causal path from x to y. This is the exclusion restriction: z can only affect y through x.
  2. At least one zi ∈ z is d-/m-connected to x in G. This is the relevance condition: z must 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
false

ADMG: X <-> Y encodes the hidden confounder directly:

julia> admg = ADMG("X <-> Y, Z --> X --> Y");

julia> is_valid_iv(admg, :X, :Y, :Z)
true

Z 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)
true
source
CausalStructures.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).

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]
source

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.

Extra free variables

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).

An Estimand, or nothing if the effect is not identifiable.

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
true
source
CausalStructures.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).

An Estimand, or nothing if the effect is not identifiable.

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')
source
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).

An Estimand, or nothing if the effect is not identifiable.

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
false

Effect 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
true
source
CausalStructures.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).

An Estimand, or nothing if the effect is not identifiable.

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
false

Effect 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
true
source

Estimands

CausalStructures.Estimand — Type
Estimand

A 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 :prob node's head, or a :marginal node's summation index. Empty for :product/:quotient.
  • given::Vector{Symbol}: a :prob node's conditioning set. Empty otherwise.
  • terms::Vector{Estimand}: a :marginal node's single summand (as a one-element vector), a :product node's factors, or a :quotient node'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}:
 :Z

The 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.

source
CausalStructures.prob — Function
prob(vars; given = Symbol[]) -> Estimand

Build 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.

  • vars: a single Symbol or a vector of them, the head of the probability term.
julia> prob([:Y, :W]; given = [:X])
P(W, Y | X)
julia> prob(:Y)
P(Y)
source
CausalStructures.marginal — Function
marginal(index, term) -> Estimand

Sum 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)
source
CausalStructures.product — Function
product(terms) -> Estimand

Multiply 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.

  • terms: the factors to multiply.
julia> product([prob(:W; given = [:X]), prob(:Y; given = [:W, :X])])
P(W | X) P(Y | W, X)
julia> product([prob(:Y)])
P(Y)
source
CausalStructures.quotient — Function
quotient(num, den) -> Estimand

Form 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)
source