Skip to content

Commit fc92611

Browse files
authored
[ENH] Adding acyclification procedure (#17)
* Remove license in doc/index.rst (#14) * Adding acyclification algorithm Signed-off-by: Adam Li <[email protected]>
1 parent a0d46b9 commit fc92611

File tree

7 files changed

+183
-0
lines changed

7 files changed

+183
-0
lines changed

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ causal graph operations.
4646
pds
4747
pds_path
4848
uncovered_pd_path
49+
acyclification
4950

5051
Conversions between other package's causal graphs
5152
=================================================

docs/references.bib

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ @article{Meek1995
4646
journal = {Proceedings of Eleventh Conference on Uncertainty in Artificial Intelligence, Montreal, QU}
4747
}
4848

49+
50+
@InProceedings{Mooij2020cyclic,
51+
title = {Constraint-Based Causal Discovery using Partial Ancestral Graphs in the presence of Cycles},
52+
author = {M. Mooij, Joris and Claassen, Tom},
53+
booktitle = {Proceedings of the 36th Conference on Uncertainty in Artificial Intelligence (UAI)},
54+
pages = {1159--1168},
55+
year = {2020},
56+
editor = {Peters, Jonas and Sontag, David},
57+
volume = {124},
58+
series = {Proceedings of Machine Learning Research},
59+
month = {03--06 Aug},
60+
publisher = {PMLR},
61+
pdf = {http://proceedings.mlr.press/v124/m-mooij20a/m-mooij20a.pdf},
62+
url = {https://proceedings.mlr.press/v124/m-mooij20a.html},
63+
abstract = {While feedback loops are known to play important roles in many complex systems, their existence is ignored in a large part of the causal discovery literature, as systems are typically assumed to be acyclic from the outset. When applying causal discovery algorithms designed for the acyclic setting on data generated by a system that involves feedback, one would not expect to obtain correct results. In this work, we show that—surprisingly—the output of the Fast Causal Inference (FCI) algorithm is correct if it is applied to observational data generated by a system that involves feedback. More specifically, we prove that for observational data generated by a simple and sigma-faithful Structural Causal Model (SCM), FCI is sound and complete, and can be used to consistently estimate (i) the presence and absence of causal relations, (ii) the presence and absence of direct causal relations, (iii) the absence of confounders, and (iv) the absence of specific cycles in the causal graph of the SCM. We extend these results to constraint-based causal discovery algorithms that exploit certain forms of background knowledge, including the causally sufficient setting (e.g., the PC algorithm) and the Joint Causal Inference setting (e.g., the FCI-JCI algorithm).}
64+
}
65+
66+
4967
@book{Neapolitan2003,
5068
author = {Neapolitan, Richard},
5169
year = {2003},

docs/whats_new/v0.1.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Changelog
3030
- |Feature| Implement and test the :class:`pywhy_graphs.PAG` for PAGs, by `Adam Li`_ (:pr:`9`)
3131
- |Feature| Implement and test various PAG algorithms :func:`pywhy_graphs.algorithms.possible_ancestors`, :func:`pywhy_graphs.algorithms.possible_descendants`, :func:`pywhy_graphs.algorithms.discriminating_path`, :func:`pywhy_graphs.algorithms.pds`, :func:`pywhy_graphs.algorithms.pds_path`, and :func:`pywhy_graphs.algorithms.uncovered_pd_path`, by `Adam Li`_ (:pr:`10`)
3232
- |Feature| Implement an array API wrapper to convert between causal graphs in pywhy-graphs and causal graphs in ``causal-learn``, by `Adam Li`_ (:pr:`16`)
33+
- |Feature| Implement an acyclification algorithm for converting cyclic graphs to acyclic with :func:`pywhy_graphs.algorithms.acyclification`, by `Adam Li`_ (:pr:`17`)
3334

3435
Code and Documentation Contributors
3536
-----------------------------------

pywhy_graphs/algorithms/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
from .cyclic import * # noqa: F403
12
from .generic import * # noqa: F403
23
from .pag import * # noqa: F403

pywhy_graphs/algorithms/cyclic.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import networkx as nx
2+
3+
4+
def acyclification(
5+
G: nx.MixedEdgeGraph,
6+
directed_edge_type: str = "directed",
7+
bidirected_edge_type: str = "bidirected",
8+
copy: bool = True,
9+
) -> nx.MixedEdgeGraph:
10+
"""Acyclify a cyclic graph.
11+
12+
Applies the acyclification procedure presented in :footcite:`Mooij2020cyclic`.
13+
This converts to G to what is called :math:`G^{acy}` in the reference.
14+
15+
Parameters
16+
----------
17+
G : nx.MixedEdgeGraph
18+
A graph with cycles.
19+
directed_edge_type : str
20+
The name of the sub-graph of directed edges.
21+
bidirected_edge_type : str
22+
The name of the sub-graph of bidirected edges.
23+
copy : bool
24+
Whether to operate on the graph in place, or make a copy.
25+
26+
Returns
27+
-------
28+
G : nx.MixedEdgeGraph
29+
The acyclified graph.
30+
31+
Notes
32+
-----
33+
This takes
34+
This replaces all strongly connected components of G by fully connected
35+
bidirected components without any directed edges. Then any node with an
36+
edge pointing into the SC (i.e. a directed edge, or bidirected edge) is
37+
made fully connected with the nodes of the SC either with a directed, or
38+
bidirected edge.
39+
40+
References
41+
----------
42+
.. footbibliography::
43+
"""
44+
if copy:
45+
G = G.copy()
46+
47+
# extract the subgraph of directed edges
48+
directed_G: nx.DiGraph = G.get_graphs(directed_edge_type).copy()
49+
bidirected_G: nx.Graph = G.get_graphs(bidirected_edge_type).copy()
50+
51+
# first detect all strongly connected components
52+
scomps = nx.strongly_connected_components(directed_G)
53+
54+
# loop over all strongly connected components and their nodes
55+
for comp in scomps:
56+
if len(comp) == 1:
57+
continue
58+
59+
# extract the parents, or c-components of any node
60+
# in the strongly-connected component
61+
scomp_parents = set()
62+
scomp_c_components = set()
63+
scomp_children = []
64+
65+
for node in comp:
66+
# get any predecessors of SC
67+
for parent in directed_G.predecessors(node):
68+
if parent in comp:
69+
continue
70+
scomp_parents.add(parent)
71+
72+
# get any bidirected edges pointing to elements of SC
73+
for nbr in bidirected_G.neighbors(node):
74+
if nbr in comp:
75+
continue
76+
scomp_c_components.add(nbr)
77+
78+
# keep track of any edges pointing out of the SC
79+
for child in directed_G.successors(node):
80+
if child in comp:
81+
continue
82+
scomp_children.append((node, child))
83+
84+
# first remove all nodes in the cycle
85+
G.remove_nodes_from(comp)
86+
87+
# add them back in as a fully connected bidirected graph
88+
bidirected_fc_G = nx.complete_graph(comp)
89+
G.add_edges_from(bidirected_fc_G.edges, bidirected_edge_type)
90+
91+
# add back the children
92+
G.add_edges_from(scomp_children, directed_edge_type)
93+
94+
# make all variables connect to the strongly connected component
95+
for node in comp:
96+
for parent in scomp_parents:
97+
G.add_edge(parent, node, directed_edge_type)
98+
for c_component in scomp_c_components:
99+
G.add_edge(c_component, node, bidirected_edge_type)
100+
return G
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import networkx as nx
2+
3+
import pywhy_graphs
4+
5+
6+
def test_acyclification():
7+
"""Test acyclification procedure as specified in :footcite:`Mooij2020cyclic`.
8+
9+
Tests the graphs as presented in Figure 2.
10+
"""
11+
directed_edges = nx.DiGraph(
12+
[
13+
("x8", "x2"),
14+
("x9", "x2"),
15+
("x10", "x1"),
16+
("x2", "x4"),
17+
("x4", "x6"), # start of cycle
18+
("x6", "x5"),
19+
("x5", "x3"),
20+
("x3", "x4"), # end of cycle
21+
("x6", "x7"),
22+
]
23+
)
24+
bidirected_edges = nx.Graph([("x1", "x3")])
25+
G = nx.MixedEdgeGraph([directed_edges, bidirected_edges], ["directed", "bidirected"])
26+
acyclic_G = pywhy_graphs.acyclification(G)
27+
28+
directed_edges = nx.DiGraph(
29+
[
30+
("x8", "x2"),
31+
("x9", "x2"),
32+
("x10", "x1"),
33+
("x2", "x4"),
34+
("x6", "x7"),
35+
("x2", "x3"),
36+
("x2", "x5"),
37+
("x2", "x4"),
38+
("x2", "x6"),
39+
]
40+
)
41+
bidirected_edges = nx.Graph(
42+
[
43+
("x1", "x3"),
44+
("x4", "x6"),
45+
("x6", "x5"),
46+
("x5", "x3"),
47+
("x3", "x4"),
48+
("x4", "x5"),
49+
("x3", "x6"),
50+
("x1", "x3"),
51+
("x1", "x5"),
52+
("x1", "x4"),
53+
("x1", "x6"),
54+
]
55+
)
56+
expected_G = nx.MixedEdgeGraph([directed_edges, bidirected_edges], ["directed", "bidirected"])
57+
58+
for edge_type, graph in acyclic_G.get_graphs().items():
59+
expected_graph = expected_G.get_graphs(edge_type)
60+
assert nx.is_isomorphic(graph, expected_graph)

pywhy_graphs/algorithms/tests/test_pag.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def test_discriminating_path():
177177

178178

179179
def test_uncovered_pd_path():
180+
"""Test basic uncovered partially directed path."""
180181
# If A o-> C and there is an undirected pd path
181182
# from A to C through u, where u and C are not adjacent
182183
# then orient A o-> C as A -> C
@@ -221,6 +222,7 @@ def test_uncovered_pd_path():
221222

222223

223224
def test_uncovered_pd_path_intersecting():
225+
"""Test basic uncovered partially directed path with intersecting paths."""
224226
G = pywhy_graphs.PAG()
225227

226228
# make A o-> C

0 commit comments

Comments
 (0)