# 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
# Patient counterfactual graph
# Nodes: 1=U (unobserved confounder), 2=A (treatment), 3=Y (outcome), 4=L (observed confounder)
g = DiGraph(4)
add_edge!(g, 1, 2) # U β A
add_edge!(g, 1, 3) # U β Y
add_edge!(g, 2, 3) # A β Y
add_edge!(g, 4, 2) # L β A
add_edge!(g, 4, 3) # L β Y
# To compute Y^{do(A=0)}(u) given Y^{do(A=1)}(u) = y_obs:
# 1. Check if A β Y is identifiable
is_identifiable = is_backdoor_adjustable(g, 2, 3)
println("Treatment effect identifiable: ", is_identifiable) # true
# 2. Find what variables are needed
adj_set = backdoor_adjustment_set(g, 2, 3)
println("Adjustment set: ", adj_set) # Set([1, 4]) = {U, L}
# 3. Problem: U is unobserved
# Solution: Infer U from observations using state-space methods
# The graph structure shows that U affects both A and Y,
# so we can potentially infer U from (A, Y, L) if we have:
# - Structural assignments f_A(U, L) and f_Y(A, U, L)
# - Sufficient data to estimate these mechanisms
# 4. Once U is inferred, we can compute counterfactual:
# - Given: Y^{do(A=1)}(u) = y_obs
# - Infer: u from y_obs, a_obs=1, l_obs, and known mechanisms
# - Compute: Y^{do(A=0)}(u) using same u but A=0
# The Markov boundary shows what we need:
mb_Y = markov_boundary(g, 3)
println("Variables needed for Y: ", mb_Y) # Set([1, 2, 4]) = {U, A, L}
# Visualise graph
let
# Highlight adjustment set (U and L) in yellow, treatment and outcome in lightblue
node_colors = [:yellow, :lightblue, :lightblue, :yellow]
fig, ax, p = dagplot(g;
figure_size = (600, 400),
layout_mode = :acyclic,
node_color = node_colors,
nlabels = ["U (unobserved)", "A (treatment)", "Y (outcome)", "L (observed)"]
)
fig # Only this gets displayed
end