Skip to content

Commit 881df16

Browse files
aryan26royadam2392
andauthored
[DOCS] Add an example for the Inducing Path function (#83)
* Formed inducing path graph example --------- Signed-off-by: Aryan Roy <[email protected]> Co-authored-by: Adam Li <[email protected]>
1 parent b71f7ee commit 881df16

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

examples/intro/inducing_path.py

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
======================================================
3+
An introduction to Inducing Paths and how to find them
4+
======================================================
5+
6+
A path p is called an ``inducing path`` relative to <L,S>
7+
on an mixed-edge graph with directed and bidirected edges, where every non-endpoint vertex on p
8+
is either in L or a collider, and every collider on p is an ancestor
9+
of either X , Y or a member of S.
10+
11+
12+
In other words, it is a path between two nodes that cannot be
13+
d-seperated, making it active regardless of what variables
14+
we condition on.
15+
16+
More details on inducing paths can be found at :footcite:`Zhang2008`.
17+
18+
"""
19+
20+
import pywhy_graphs
21+
from pywhy_graphs import ADMG
22+
from pywhy_graphs.viz import draw
23+
24+
# %%
25+
# Construct an example graph
26+
# ---------------------------
27+
# To illustrate the workings of the inducing path algorithm, we will
28+
# construct the causal graph from figure 2 of :footcite:`Colombo2012`.
29+
30+
31+
G = ADMG()
32+
G.add_edge("X4", "X1", G.directed_edge_name)
33+
G.add_edge("X2", "X5", G.directed_edge_name)
34+
G.add_edge("X2", "X6", G.directed_edge_name)
35+
G.add_edge("X4", "X6", G.directed_edge_name)
36+
G.add_edge("X3", "X6", G.directed_edge_name)
37+
G.add_edge("X3", "X4", G.directed_edge_name)
38+
G.add_edge("X3", "X2", G.directed_edge_name)
39+
G.add_edge("X5", "X6", G.directed_edge_name)
40+
G.add_edge("L2", "X4", G.directed_edge_name)
41+
G.add_edge("L2", "X5", G.directed_edge_name)
42+
G.add_edge("L1", "X1", G.directed_edge_name)
43+
G.add_edge("L1", "X2", G.directed_edge_name)
44+
45+
46+
# this is the Figure 2(a) in the paper as we see.
47+
dot_graph = draw(G)
48+
dot_graph.render(outfile="graph.png", view=True)
49+
50+
51+
# %%
52+
# Adjacent nodes trivially have an inducing path
53+
# -----------------------------------------------
54+
# By definition, all adjacent nodes have a trivial inducing path between them,
55+
# that path only consists of one edge, which is the edge between those two nodes.
56+
57+
L = {}
58+
S = {}
59+
60+
# All such tests will return True.
61+
print(pywhy_graphs.inducing_path(G, "X1", "X4", L, S))
62+
print(pywhy_graphs.inducing_path(G, "X3", "X2", L, S))
63+
64+
# %%
65+
# Inducing paths between non-adjacent nodes
66+
# ------------------------------------------
67+
# Given the definition of an inducing path, we need to satisfy all
68+
# requirements for the function to return True. Adding the latent
69+
# variables to L is not enough for the pair [X1,X5]. As we see in
70+
# Figure 2(c) in :footcite:`Colombo2012`, (X1, X5) are not adjacent
71+
# in the final skeleton of the equivalence class, which makes sense
72+
# because a MAG is an equivalence class of a DAG and contains an
73+
# edge among two nodes if i) the two nodes are adjacent in the DAG,
74+
# or ii) the two nodes have a primitive inducing path between them.
75+
# Since there is no adjacency among (X1, X5) in the final skeleton,
76+
# there is no primitive inducing path between them.
77+
78+
L = {"L1", "L2"}
79+
S = {}
80+
81+
82+
# returns False
83+
print(pywhy_graphs.inducing_path(G, "X1", "X5", L, S))
84+
85+
86+
# However, if we add X3, a non-collider on the path
87+
# from X1 to X5 to L, we make a valid inducing path.
88+
89+
90+
L = {"L1", "L2", "X3"}
91+
S = {}
92+
93+
# now it returns True
94+
print(pywhy_graphs.inducing_path(G, "X1", "X5", L, S))
95+
96+
97+
# %%
98+
# The role of colliders
99+
# ----------------------
100+
# Adding colliders to the set S has a downstream effect.
101+
# Conditioning on a collider, or descendant of a collider opens up that collider path.
102+
# For example, we will add the node 'X6' to the set ``S``. This will make the
103+
# path ``(X1, X2, X3)`` a valid inducing path, since 'X2' is an ancestor of 'X6'.
104+
105+
# Some node pairs still do not have a valid inducing path between them.
106+
# For example, the path between X1 and X3 is not available.
107+
108+
# this returns False
109+
print(pywhy_graphs.inducing_path(G, "X1", "X3", L, S))
110+
111+
# If we add X6 to ``S``, paths containing all the collider ancestors of X6
112+
# will be valid inducing paths.
113+
# Since X2 is a collider and an ancestor of X6, there should be a valid inducing
114+
# path from X1 to X3 now.
115+
116+
L = {"L1", "L2", "X3"}
117+
S = {"X6"}
118+
119+
# now it returns True
120+
print(pywhy_graphs.inducing_path(G, "X1", "X3", L, S))
121+
122+
# %%
123+
# References
124+
# ----------
125+
# .. footbibliography::

pywhy_graphs/algorithms/generic.py

+8
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,14 @@ def inducing_path(G, node_x: Node, node_y: Node, L: Set = None, S: Set = None):
565565
if node_x == node_y:
566566
raise ValueError("The source and destination nodes are the same.")
567567

568+
edges = G.edges()
569+
570+
# XXX: fix this when graphs are refactored to only check for directed/bidirected edge types
571+
for elem in edges.keys():
572+
if elem not in {"directed", "bidirected"}:
573+
if len(edges[elem]) != 0:
574+
raise ValueError("Inducing Path is not defined for this graph.")
575+
568576
path = [] # this will contain the path.
569577

570578
x_ancestors = _directed_sub_graph_ancestors(G, node_x)

0 commit comments

Comments
 (0)