This function uses manipulation of powers of the adjacency matrix (via numpy & scipy) to detect all of the potential triadic closures in a graph. The key fact to note is that the n-th power of the adjacency matrix corresponds to a matrix containing the number of n-hop walks between each pair of nodes. Some tests on various graphs are included below the function.
import networkx as nx
import numpy as np
from scipy.sparse import triu
%matplotlib inline
def get_all_open_triangles(G, nodes=None):
""" Detect all potential triadic closures in a graph, using manipulation of powers of the adjacency matrix.
G: Networkx undirected graph (of type networkx.classes.graph.Graph)
nodes: Node list which, if passed, will be used to order the result
returns: List of 2-tuples containing nodes which, if connected, would form a new triangle (triadic closure)
"""
if type(G) is not nx.classes.graph.Graph:
raise ValueError('This method only works for undirected graphs')
# Use specified node order, if given
nodes = G.nodes() if not nodes else nodes
# Retrieve adjacency matrix and its square
adj1 = nx.adjacency_matrix(G, nodes)
adj2 = adj1**2
# According to basic graph theory, the n-th power of the adjacency matrix
# corresponds to a matrix containing the number of n-hop walks between 2 nodes
# Note that we're only concerned with the upper triangle of these matrices,
# since the graph is undirected and we also don't care about the diagonal
one_hop = np.nonzero(triu(adj1, 1) > 0)
two_hop = np.nonzero(triu(adj2, 1) > 0)
one_hop_set = set(zip(one_hop[0], one_hop[1]))
two_hop_set = set(zip(two_hop[0], two_hop[1]))
# Take the difference of the two sets, and any positive entries represent open triangles
# That is, nodes with a 2-hop path, but no 1-hop path, comprise an open triangle
open_triangle_indices = two_hop_set.difference(one_hop_set)
# Map the open triangle indices back to an interpretable list of nodes
triangle_nodes = [(nodes[ix[0]], nodes[ix[1]]) for ix in open_triangle_indices]
return triangle_nodes
# Example 1: Simple Graph
G = nx.Graph([('A', 'B'),
('A', 'D'),
('B', 'D'),
('D', 'C')])
nx.draw(G, with_labels=True)
print(get_all_open_triangles(G, ['A', 'B', 'C', 'D']))
# Example 2: Barabasi Albert Graph
bag = nx.barabasi_albert_graph(15, 2, 999)
nx.draw(bag, with_labels=True)
print(get_all_open_triangles(bag))
# Example 3: Caveman Graph
cmg = nx.caveman_graph(4, 3)
nx.draw(cmg, with_labels=True)
print(get_all_open_triangles(cmg))
# Example 4: Florentine Families Graph
ffg = nx.florentine_families_graph()
nx.draw(ffg, with_labels=True)
print(get_all_open_triangles(ffg))
# Example 5: Newman Watts Strogatz Graph
nwsg = nx.newman_watts_strogatz_graph(10, 2, .5, 999)
nx.draw(nwsg, with_labels=True)
print(get_all_open_triangles(nwsg))