|
| 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:: |
0 commit comments