# 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 the recorded adjustment set, L; U remains unavailable.
observed_adjustment_valid = is_valid_adjustment_set(g, 2, 3, Set([4]))
@assert !observed_adjustment_valid
println("Adjustment for recorded L sufficient: ", observed_adjustment_valid)
# 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
# A model-dependent analysis requires an explicit likelihood and prior for U.
# 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)
# - Assumptions and data that identify or constrain these mechanisms
# 4. Integrate counterfactual outcomes over the posterior of U:
# - 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 lists local graph neighbours, not an identification certificate:
mb_Y = markov_boundary(g, 3)
println("Markov boundary of Y: ", mb_Y) # Set([1, 2, 4]) = {U, A, L}
# Visualise: observed good control L via adjustment colouring; U is latent (unobserved)
let
fig, ax, p = dagplot(g;
figure_size = (600, 400),
layout = Point2f[
Point2f(-0.8, 1.0), # U (latent)
Point2f(-1.0, 0.0), # A
Point2f(1.0, 0.0), # Y
Point2f(0.8, 1.0), # L (observed good control)
],
color_by = :adjustment,
exposure = 2,
outcome = 3,
adjustment = Set([4]),
labels = ["U", "A", "Y", "L"],
edge_routing = Dict(
(1, 3) => CurvedEdge(bow = 0.14, side = :right),
(4, 3) => CurvedEdge(bow = 0.12, side = :left),
),
node_size = 40,
edge_width = 2.0,
arrow_size = 15,
nlabels_fontsize = 16,
padding = 0.40,
)
fig # Only this gets displayed
end