9  Do-Calculus: Rules for Interventions

Status: Draft

v0.5

9.1 Introduction

The do-calculus is Pearl’s symbolic calculus for rewriting interventional distributions in terms of observational ones (Pearl 2009). It answers: can \(P(Y \mid do(X=x))\) be expressed using \(P(\cdot)\) on the observed variables? Template criteria (backdoor, frontdoor, instruments) in Chapter 5 are the usual first tools; do-calculus is the complete foundation those templates sit on, and the fallback when no template matches.

CausalDynamics does not yet implement a full symbolic do-calculus solver (is_identifiable / identify_formula remain stubs). What we can do here is check the graphical side conditions of each rule with d_separated on surgically modified graphs, and fall back to identify / backdoor_adjustment_set for everyday queries.

9.2 Modified graphs

Each rule refers to a mutilated DAG:

Notation Surgery Display helper
\(G_{\overline{X}}\) remove incoming edges to \(X\) DAGMakie do_surgery (plotting only)
\(G_{\underline{X}}\) remove outgoing edges from \(X\) manual rem_edge! on a copy
\(G_{\overline{X}\underline{Z}}\) both of the above for the named nodes combine the two

Executable SCM interventions (apply_intervention) also remove incoming edges to the intervened node; that is the same mutilation as \(G_{\overline{X}}\), but for simulation rather than display (Chapter 4; Tables 4 and 8 in the Concept Reference).

9.3 The three rules

9.3.1 Rule 1: Insertion / deletion of observations

If \((Y \perp Z \mid X)_{G_{\overline{X}}}\), then

\[ P(Y \mid do(X=x), Z) = P(Y \mid do(X=x)). \]

Conditioning on \(Z\) is redundant under the intervention when \(Z\) carries no information about \(Y\) in the graph with parents of \(X\) cut.

project_root = let
    current = pwd()
    while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
        parent = dirname(current)
        parent == current && break
        current = parent
    end
    current
end
include(joinpath(project_root, "scripts", "ensure_packages.jl"))
@auto_using DAGMakie CairoMakie CausalDynamics Graphs

# Z β†’ X β†’ Y, Z β†’ Y  (nodes: 1=X, 2=Y, 3=Z)
g = SimpleDiGraph(3)
add_edge!(g, 3, 1)
add_edge!(g, 1, 2)
add_edge!(g, 3, 2)

g_bar_X = do_surgery(g, 1)  # G_overline{X}: drop edges into X
rule1 = d_separated(g_bar_X, 2, 3, [1])
println("Graph: Z β†’ X β†’ Y, Z β†’ Y")
println("Y β«« Z | X in G_overline{X}? ", rule1)
println(rule1 ? "Rule 1 applies." : "Rule 1 does not apply (here Z β†’ Y stays open).")
Graph: Z β†’ X β†’ Y, Z β†’ Y
Y β«« Z | X in G_overline{X}? false
Rule 1 does not apply (here Z β†’ Y stays open).

Left: original confounding DAG. Right: \(G_{\overline{X}}\) after do_surgery (edge \(Z \rightarrow X\) removed).

9.3.2 Rule 2: Action / observation exchange

If \((Y \perp Z \mid X)_{G_{\underline{X}}}\), then

\[ P(Y \mid do(X=x), Z) = P(Y \mid Z, X=x). \]

This is the usual bridge from intervention to observation (given covariates \(Z\)).

# Chain Z β†’ X β†’ Y (no direct Z β†’ Y)
g2 = SimpleDiGraph(3)
add_edge!(g2, 3, 1)
add_edge!(g2, 1, 2)

g_under_X = copy(g2)
for c in collect(outneighbors(g_under_X, 1))
    rem_edge!(g_under_X, 1, c)
end
rule2 = d_separated(g_under_X, 2, 3, [1])
println("Graph: Z β†’ X β†’ Y")
println("Y β«« Z | X in G_underline{X}? ", rule2)
println(rule2 ? "Rule 2 applies: P(Y | do(X), Z) = P(Y | Z, X)." : "Rule 2 does not apply.")
Graph: Z β†’ X β†’ Y
Y β«« Z | X in G_underline{X}? true
Rule 2 applies: P(Y | do(X), Z) = P(Y | Z, X).

Left: chain \(Z \rightarrow X \rightarrow Y\). Right: \(G_{\underline{X}}\) with outgoing edges from \(X\) removed.

9.3.3 Rule 3: Insertion / deletion of actions

If \((Y \perp Z \mid X)_{G_{\overline{X}\underline{Z}}}\), then

\[ P(Y \mid do(X=x, Z=z)) = P(Y \mid do(X=x)). \]

An extra intervention on \(Z\) can be dropped when it does not open a path to \(Y\) under that double surgery.

# X β†’ Y ← Z, X β†’ Z
g3 = SimpleDiGraph(3)
add_edge!(g3, 1, 2)
add_edge!(g3, 3, 2)
add_edge!(g3, 1, 3)

g_bar_X_under_Z = do_surgery(g3, 1)
for c in collect(outneighbors(g_bar_X_under_Z, 3))
    rem_edge!(g_bar_X_under_Z, 3, c)
end
rule3 = d_separated(g_bar_X_under_Z, 2, 3, [1])
println("Graph: X β†’ Y ← Z, X β†’ Z")
println("Y β«« Z | X in G_overline{X} underline{Z}? ", rule3)
println(rule3 ? "Rule 3 applies: do(Z) can be deleted beside do(X)." : "Rule 3 does not apply.")
Graph: X β†’ Y ← Z, X β†’ Z
Y β«« Z | X in G_overline{X} underline{Z}? true
Rule 3 applies: do(Z) can be deleted beside do(X).

9.4 Templates first, calculus when needed

Practice: try identify / backdoor / frontdoor / IV (Chapter 5). If a template succeeds, you already have an observational formula. Use the rule checks above to understand why a rewrite is valid, or when templates fail and a longer do-calculus derivation would be required (still done by hand until a symbolic solver lands in CausalDynamics).

g_q = SimpleDiGraph(3)
add_edge!(g_q, 3, 1)
add_edge!(g_q, 1, 2)
add_edge!(g_q, 3, 2)

println("Query: total effect of X on Y in Z β†’ X β†’ Y, Z β†’ Y")
id = identify(g_q, TotalEffectQuery(1, 2); node_names = [:X, :Y, :Z])
println("identify β†’ strategy=$(id.strategy), adjustment=$(id.adjustment)")
println("Backdoor set: ", backdoor_adjustment_set(g_q, 1, 2))
Query: total effect of X on Y in Z β†’ X β†’ Y, Z β†’ Y
identify β†’ strategy=backdoor, adjustment=[:Z]
Backdoor set: Set([3])

9.5 Stratum context

This chapter is doing in the Structural stratum: algebraic rules that turn interventions into observations when the graph permits. Estimation of the resulting functionals lives in the Observable part.

9.6 Summary

Do-calculus has three rules, each conditioned on d-separation in a surgically modified DAG. Template identification is the everyday path; d_separated on \(G_{\overline{X}}\) / \(G_{\underline{X}}\) makes the graphical hypotheses of the rules checkable in code. Full symbolic rewriting is not yet in CausalDynamics; use identify for certificates that templates provide.

9.7 Further Reading