9  Do-Calculus: Rules for Interventions

Status: Draft

v0.4

9.1 Learning Objectives

After reading this chapter, you will be able to:

  • Understand the three rules of do-calculus
  • Apply do-calculus to transform interventional queries into observational queries
  • Recognise when do-calculus applies vs template-based methods
  • Use do-calculus to determine identifiability

9.2 Introduction

The do-calculus provides rules for computing interventional distributions from observational distributions (Pearl 2009). These rules allow us to answer: β€œCan we express \(P^{do(X=x)}(Y)\) in terms of \(P(Y, X, Z)\)?” While template-based methods (backdoor, frontdoor, IV) are often more intuitive, do-calculus provides the theoretical foundation and handles cases where templates don’t apply.

9.3 The Three Rules of Do-Calculus

The do-calculus consists of three rules that allow us to manipulate interventional distributions algebraically. These rules are based on d-separation in modified graphs (see Graph Theory and Causal Patterns for d-separation).

9.3.1 Rule 1: Insertion/Deletion of Observations

Rule 1 (Insertion/Deletion of Observations): If \(Y\) and \(Z\) are d-separated by \(X\) in \(G_{\overline{X}}\) (graph with edges into \(X\) removed), then: \[ P^{do(X=x)}(Y \mid Z) = P^{do(X=x)}(Y) \]

Interpretation: If \(Y\) and \(Z\) are conditionally independent given the intervention on \(X\) (in the modified graph), then we can insert or delete the observation of \(Z\) without changing the interventional distribution.

When to use: When we want to simplify an interventional query by removing unnecessary conditioning.

9.3.2 Implementation: Rule 1 Example

We can check Rule 1 by creating the modified graph \(G_{\overline{X}}\) (edges into \(X\) removed) and checking d-separation:

# Find project root and include ensure_packages.jl
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

# Example graph: Z β†’ X β†’ Y, Z β†’ Y
# We want to check: P^{do(X=x)}(Y | Z) = P^{do(X=x)}(Y)?
# Rule 1 applies if Y and Z are d-separated by X in G_overline{X}

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

# G_overline{X}: Remove edges INTO X
g_overline_X = copy(g)
rem_edge!(g_overline_X, 3, 1)  # Remove Z β†’ X

println("Original graph: Z β†’ X β†’ Y, Z β†’ Y")
println("G_overline{X}: Remove edges into X")
println("  Y β«« Z | X in G_overline{X: ", CausalDynamics.d_separated(g_overline_X, 2, 3, [1]))
if CausalDynamics.d_separated(g_overline_X, 2, 3, [1])
    println("  βœ“ Rule 1 applies: P^{do(X=x)}(Y | Z) = P^{do(X=x)}(Y)")
else
    println("  βœ— Rule 1 does not apply")
end
Original graph: Z β†’ X β†’ Y, Z β†’ Y
G_overline{X}: Remove edges into X
  Y β«« Z | X in G_overline{X: false
  βœ— Rule 1 does not apply

9.3.3 Rule 2: Action/Observation Exchange

Rule 2 (Action/Observation Exchange): If \(Y\) and \(Z\) are d-separated by \(X\) in \(G_{\underline{X}}\) (graph with edges out of \(X\) removed), then: \[ P^{do(X=x)}(Y \mid Z) = P(Y \mid Z, X=x) \]

Interpretation: If \(Y\) and \(Z\) are conditionally independent given \(X\) in the graph with edges out of \(X\) removed, then we can exchange the intervention \(do(X=x)\) with the observation \(X=x\).

When to use: When we want to convert an interventional query into an observational query (this is the key to identification).

9.3.4 Implementation: Rule 2 Example

Rule 2 is the key to identificationβ€”it converts interventions to observations. We check d-separation in \(G_{\underline{X}}\) (edges out of \(X\) removed):

# Find project root and include ensure_packages.jl
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 CausalDynamics Graphs GraphMakie CairoMakie DAGMakie

# Example graph: Z β†’ X β†’ Y
# We want to check: P^{do(X=x)}(Y | Z) = P(Y | Z, X=x)?
# Rule 2 applies if Y and Z are d-separated by X in G_underline{X}

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

# G_underline{X}: Remove edges OUT OF X
g_underline_X = copy(g)
rem_edge!(g_underline_X, 1, 2)  # Remove X β†’ Y

println("Original graph: Z β†’ X β†’ Y")
println("G_underline{X}: Remove edges out of X")
println("  Y β«« Z | X in G_underline{X: ", CausalDynamics.d_separated(g_underline_X, 2, 3, [1]))
if CausalDynamics.d_separated(g_underline_X, 2, 3, [1])
    println("  βœ“ Rule 2 applies: P^{do(X=x)}(Y | Z) = P(Y | Z, X=x)")
    println("  This converts the interventional query to an observational query!")
else
    println("  βœ— Rule 2 does not apply")
end

# Visualise the modified graph
let
    # Original graph: Z β†’ X β†’ Y
    fig1, ax1, p1 = dagplot(g;
        figure_size = (800, 400),
        layout_mode = :acyclic,
        nlabels = ["X", "Y", "Z"]
    )
    # Modified graph G_underline{X}: Remove X β†’ Y
    fig2, ax2, p2 = dagplot(g_underline_X;
        figure_size = (800, 400),
        layout_mode = :acyclic,
        node_color = :lightgreen,
        nlabels = ["X", "Y", "Z"]
    )
    fig1
    fig2
end
Original graph: Z β†’ X β†’ Y
G_underline{X}: Remove edges out of X
  Y β«« Z | X in G_underline{X: true
  βœ“ Rule 2 applies: P^{do(X=x)}(Y | Z) = P(Y | Z, X=x)
  This converts the interventional query to an observational query!

Do-calculus Rule 2: Action/observation exchange (key to identification)

9.3.5 Rule 3: Insertion/Deletion of Actions

Rule 3 (Insertion/Deletion of Actions): If \(Y\) and \(Z\) are d-separated by \(X\) in \(G_{\overline{X}\underline{Z}}\) (graph with edges into \(X\) and out of \(Z\) removed), then: \[ P^{do(X=x, Z=z)}(Y) = P^{do(X=x)}(Y) \]

Interpretation: If \(Y\) and \(Z\) are conditionally independent given the intervention on \(X\) (in the modified graph), then we can insert or delete the intervention on \(Z\) without changing the interventional distribution.

When to use: When we want to simplify an interventional query by removing unnecessary interventions.

9.3.6 Implementation: Rule 3 Example

Rule 3 allows us to remove unnecessary interventions. We check d-separation in \(G_{\overline{X}\underline{Z}}\) (edges into \(X\) and out of \(Z\) removed):

# Find project root and include ensure_packages.jl
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 CausalDynamics Graphs

# Example graph: X β†’ Y ← Z, X β†’ Z
# We want to check: P^{do(X=x, Z=z)}(Y) = P^{do(X=x)}(Y)?
# Rule 3 applies if Y and Z are d-separated by X in G_overline{X,underline{Z}}

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

# G_overline{X,underline{Z}}: Remove edges INTO X and OUT OF Z
g_overline_X_underline_Z = copy(g)
# No edges into X to remove (X has no parents)
rem_edge!(g_overline_X_underline_Z, 3, 2)  # Remove Z β†’ Y (edge out of Z)

println("Original graph: X β†’ Y ← Z, X β†’ Z")
println("G_overline{X,underline{Z}}: Remove edges into X and out of Z")
println("  Y β«« Z | X in G_overline{X,underline{Z}: ", CausalDynamics.d_separated(g_overline_X_underline_Z, 2, 3, [1]))
if CausalDynamics.d_separated(g_overline_X_underline_Z, 2, 3, [1])
    println("  βœ“ Rule 3 applies: P^{do(X=x, Z=z)}(Y) = P^{do(X=x)}(Y)")
    println("  We can remove the intervention on Z without changing the result")
else
    println("  βœ— Rule 3 does not apply")
end
Original graph: X β†’ Y ← Z, X β†’ Z
G_overline{X,underline{Z}}: Remove edges into X and out of Z
  Y β«« Z | X in G_overline{X,underline{Z}: true
  βœ“ Rule 3 applies: P^{do(X=x, Z=z)}(Y) = P^{do(X=x)}(Y)
  We can remove the intervention on Z without changing the result

9.4 Systematic Application

These rules allow us to systematically transform interventional queries into observational queries when identification is possible. The process:

  1. Start with interventional query: \(P^{do(X=x)}(Y)\)
  2. Apply rules: Use Rules 1-3 to transform the query
  3. Check if observational: If we can express it as \(P(Y \mid \ldots)\), the effect is identified
  4. If not identifiable: If do-calculus cannot find an expression, the effect is not identifiable

9.5 When Do-Calculus Applies vs Template-Based Methods

Template-based methods (backdoor, frontdoor, IV) are: - More intuitive - Computationally efficient - Easy to apply when patterns match

Do-calculus is: - Theoretically complete - Handles cases where templates don’t apply - Provides the foundation for template-based methods

Best practice: Try template-based methods first (they’re easier), then use do-calculus for cases where templates don’t apply or to verify template results.

9.6 Example: Applying Do-Calculus

Consider a graph where we want to identify \(P^{do(X=x)}(Y)\):

  1. Check template methods: Does backdoor/frontdoor/IV apply?
  2. If not, try do-calculus: Apply Rules 1-3 systematically
  3. Verify result: Check if we can express it in terms of observable distributions

9.6.1 Implementation: Systematic Application Example

Here’s a complete example showing how to systematically apply do-calculus rules:

# Find project root and include ensure_packages.jl
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 CausalDynamics Graphs

# Example: Graph Z β†’ X β†’ Y, Z β†’ Y
# Query: Can we identify P^{do(X=x)}(Y)?

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

println("Graph: Z β†’ X β†’ Y, Z β†’ Y")
println("Query: P^{do(X=x)}(Y)")
println("\nStep 1: Check template methods (backdoor)")
adj_set = backdoor_adjustment_set(g, 1, 2)  # X β†’ Y
if !isempty(adj_set)
    println("  Backdoor adjustment set: ", adj_set)
    println("  βœ“ Can use backdoor criterion (easier than do-calculus)")
else
    println("  βœ— Backdoor criterion doesn't apply")
    println("  β†’ Try do-calculus")

    # Try Rule 2: P^{do(X=x)}(Y) = P(Y | X=x) if Y β«« Z | X in G_underline{X}
    g_underline_X = copy(g)
    rem_edge!(g_underline_X, 1, 2)  # Remove X β†’ Y

    if CausalDynamics.d_separated(g_underline_X, 2, 3, [1])
        println("\nStep 2: Apply Rule 2")
        println("  Y β«« Z | X in G_underline{X: true")
        println("  β†’ P^{do(X=x)}(Y) = P(Y | X=x)")
        println("  βœ“ Identified!")
    end
end
Graph: Z β†’ X β†’ Y, Z β†’ Y
Query: P^{do(X=x)}(Y)

Step 1: Check template methods (backdoor)
  Backdoor adjustment set: Set([3])
  βœ“ Can use backdoor criterion (easier than do-calculus)

9.7 World Context

This chapter addresses Doing in the Structural worldβ€”how can we compute interventional distributions? Do-calculus provides the symbolic rules for transforming interventions into observations, complementing the template-based methods in Identification: When Can We Learn from Data?.

9.8 Key Takeaways

  1. Three rules: Insertion/deletion of observations, action/observation exchange, insertion/deletion of actions
  2. Systematic transformation: Rules allow us to transform interventional queries into observational queries
  3. Theoretical completeness: Do-calculus provides the foundation for all identification methods
  4. Complement to templates: Use template methods when possible, do-calculus when needed
  5. Based on d-separation: Rules rely on conditional independence in modified graphs

9.9 Further Reading