Causal Identification
This guide expands on the Getting Started tutorial with more advanced identification techniques.
using CausalStructures
using CairoMakie
using SugiyamaAdjustment strategies
The adjustment_set function supports three different strategies for finding adjustment sets. Let's build a DAG with multiple confounders and mediators – Figure 6.5 of Peters et al. (2017):
dag = DAG(
"C --> X, A --> X + K, X --> F + D, K --> Y, D --> Y + G, Y --> H")
plot(dag)Parents strategy: Adjusts for all parents of X. Simple but often includes unnecessary variables:
adjustment_set(dag, :X, :Y; type = :parents)2-element Vector{Symbol}:
:A
:CBackdoor strategy: Applies Pearl's backdoor criterion to find valid adjustment sets that block all confounding paths:
adjustment_set(dag, :X, :Y; type = :backdoor)1-element Vector{Symbol}:
:AOptimal strategy (default): Computes the O-set, which minimizes the asymptotic variance of the causal effect estimator:
adjustment_set(dag, :X, :Y; type = :optimal)1-element Vector{Symbol}:
:KMinimal separator
Sometimes we need the smallest set of variables that d-separates two nodes. The minimal_separator function finds one:
minimal_separator(dag, :X, :Y)2-element Vector{Symbol}:
:A
:DThe restrict keyword limits the search to a specific pool of variables.
minimal_separator(dag, :X, :Y; restrict = [:D, :K])2-element Vector{Symbol}:
:D
:KThe include keyword forces certain variables to always be in the result:
minimal_separator(dag, :X, :Y; include = :K)2-element Vector{Symbol}:
:D
:KFrontdoor adjustment
When confounders are unobserved, the backdoor criterion doesn't apply. Consider a graph where an unobserved confounder U affects both the treatment X and outcome Y:
dag2 = DAG("U --> X + Y, X --> M --> Y")
plot(dag2)The mediator M is a descendant of X, so adjusting for it violates the backdoor criterion:
is_valid_backdoor(dag2, :X, :Y, :M)falseHowever, M intercepts every causal path from X to Y and is itself unconfounded; this is exactly the frontdoor criterion:
is_valid_frontdoor(dag2, :X, :Y, :M)truefrontdoor_set(dag2, :X, :Y)1-element Vector{Symbol}:
:MInstrumental variables
When hidden confounders block backdoor adjustment and no suitable mediator exists for the frontdoor criterion, an instrumental variable (IV) can still identify the causal effect. An instrument Z must satisfy two conditions: it must be associated with the treatment X (relevance), and it can only affect the outcome Y through X (exclusion restriction).
Consider a graph where U is an unobserved confounder and Z is an available instrument, but there is no mediator on the path from X to Y:
dag3 = DAG("U --> X + Y, Z --> X --> Y")
plot(dag3)Without a mediator, the frontdoor criterion cannot apply here:
is_valid_frontdoor(dag3, :X, :Y, :Z)falseHowever, Z is a valid instrument since it is d-connected to X and d-separated from Y given X in the interventional graph $G_{\overline{X}}$ (the graph with all incoming edges to X removed):
is_valid_iv(dag3, :X, :Y, :Z)trueADMG adjustment
In an ADMG (Acyclic Directed Mixed Graph), bidirected edges <-> represent hidden common causes directly, without explicitly keeping the unobserved nodes in the graph. An ADMG arises naturally by projecting unobserved variables out of a DAG via latent_project. Let's project U out of dag3:
admg = latent_project(dag3, :U)ADMG with 3 nodes and 3 edges:
nodes: X, Y, Z
edges:
X --> Y, X <-> Y, Z --> X
Removing U introduces X <-> Y, which captures its confounding effect. The functions discussed above also work on ADMGs, e.g.:
is_valid_iv(admg, :X, :Y, :Z)trueGeneral identification
The methods discussed above are each sufficient, not necessary. CausalStructures also implements the ID algorithm of Shpitser and Pearl (2008) and returns the estimand as an Estimand.
On a graph with an observed confounder, it recovers the familiar g-formula:
dag4 = DAG("Z --> X + Y, X --> Y")
id(dag4, :X, :Y)\[\sum_{Z} P(Y \mid X, Z) P(Z)\]
Unlike the criteria above, id also works when no adjustment set exists at all. Recall the front-door graph from before, where U is an unobserved confounder of X and Y, and M mediates the effect of X on Y:
plot(dag2)Projecting U out gives an ADMG with no valid adjustment set, yet id still identifies the effect through the mediator:
admg2 = latent_project(dag2, :U)
id(admg2, :X, :Y)\[\sum_{M} P(M \mid X) \left(\sum_{X'} P(X') P(Y \mid M, X')\right)\]
which is the standard front-door adjustment formula. The primed X' is a summation index distinct from the intervened value of X, following the usual convention.
id can also detect non-identifiability. Recall the ADMG obtained earlier by projecting the unobserved instrument-confounder graph's U out of dag3:
plot(admg)Here the causal effect cannot be identified:
id(admg, :X, :Y) === nothingtrueidc extends id to conditional effects $P(y \mid do(x), z)$:
idc(admg2, :X, :Y; given = :M)\[\sum_{X'} P(X') P(Y \mid M, X')\]
Working with the result
The result is an immutable Estimand expression tree, tagged by kind (:prob, :marginal, :product, or :quotient), so it can be inspected or transformed programmatically:
e = id(dag4, :X, :Y)
(e.kind, e.vars, [t.kind for t in e.terms[1].terms])(:marginal, [:Z], [:prob, :prob])