Causal Identification

This guide expands on the Getting Started tutorial with more advanced identification techniques.

using CausalStructures
using CairoMakie
using Sugiyama

Adjustment 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
 :C

Backdoor 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}:
 :A

Optimal 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}:
 :K

Minimal 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
 :D

The restrict keyword limits the search to a specific pool of variables.

minimal_separator(dag, :X, :Y; restrict = [:D, :K])
2-element Vector{Symbol}:
 :D
 :K

The include keyword forces certain variables to always be in the result:

minimal_separator(dag, :X, :Y; include = :K)
2-element Vector{Symbol}:
 :D
 :K

Frontdoor 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)
false

However, 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)
true
frontdoor_set(dag2, :X, :Y)
1-element Vector{Symbol}:
 :M

Instrumental 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)
false

However, 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)
true

ADMG 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)
true

General 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) === nothing
true

idc 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])