How to build directed graph from nested dictionary? (Python 3 | NetworkX) - python

I am trying to build a hierarchical directed network where some nodes can branch into others, while others do not. The values in the inner dictionary (i.e. the integers) are to keep track of leaves in the tree-like structure. I've created a naive way to turn this particular nested dictionary graph_data into a directed graph but it is only specific to 3 layers. Below shows the hierarchy:
How can I create a nested function that adds edges to the directed graph for any number of levels? For example, if there was a level-3 or a level-4 this would not work and I would have to expand it out each time. Do I need to use a while loop?
import numpy as np
from collections import *
import networkx as nx
%matplotlib inline
# Hierarchical data
graph_data = {"root": {"level-0.A":0,
"level-0.B":{"level-1.B.1":2,
"level-1.B.2": {"level-2.B.2.1":3, "level-2.B.2.2":1}}}}
# Empty directed graph
G = nx.DiGraph()
# Helper functions
is_dict = lambda x: type(x) in {dict, OrderedDict, defaultdict}
# Iterate through the layers
for root, level_0 in graph_data.items():
if len(level_0) > 0:
for level_0_node, level_1 in level_0.items():
G.add_edge(root, level_0_node)
if is_dict(level_1):
for level_1_node, level_2 in level_1.items():
G.add_edge(level_0_node, level_1_node)
if is_dict(level_2):
for level_2_node, level_3 in level_2.items():
G.add_edge(level_1_node, level_2_node)
np.random.seed(8)
nx.draw(G, with_labels=True)

Use a queue to hold the details, e.g.:
from collections import Mapping
graph_data = {"root": {"level-0.A":0,
"level-0.B":{"level-1.B.1":2,
"level-1.B.2": {"level-2.B.2.1":3, "level-2.B.2.2":1}}}}
# Empty directed graph
G = nx.DiGraph()
# Iterate through the layers
q = list(graph_data.items())
while q:
v, d = q.pop()
for nv, nd in d.items():
G.add_edge(v, nv)
if isinstance(nd, Mapping):
q.append((nv, nd))
np.random.seed(8)
nx.draw(G, with_labels=True)

Related

Remove weights from networkx graph

I have a weighted Networkx graph G. I first want to make some operation on G with weights (which is why I just don't read the input and set weights=None) and then remove them from G afterwards. What is the most straightforward way to make it unweighted?
I could just do:
G = nx.from_scipy_sparse_array(nx.to_scipy_sparse_array(G,weight=None))
Or loop through the G.adj dictionary and set weights=0, but both of these options feels too complicated. Something like:
G = G.drop_weights()
It is possible to access the data structure of the networkx graphs directly and remove any unwanted attributes.
At the end, what you can do is define a function that loops over the dictionaries and remove the "weight" attribute.
def drop_weights(G):
'''Drop the weights from a networkx weighted graph.'''
for node, edges in nx.to_dict_of_dicts(G).items():
for edge, attrs in edges.items():
attrs.pop('weight', None)
and an example of usage:
import networkx as nx
def drop_weights(G):
'''Drop the weights from a networkx weighted graph.'''
for node, edges in nx.to_dict_of_dicts(G).items():
for edge, attrs in edges.items():
attrs.pop('weight', None)
G = nx.Graph()
G.add_weighted_edges_from([(1,2,0.125), (1,3,0.75), (2,4,1.2), (3,4,0.375)])
print(nx.is_weighted(G)) # True
F = nx.Graph(G)
print(nx.is_weighted(F)) # True
# OP's suggestion
F = nx.from_scipy_sparse_array(nx.to_scipy_sparse_array(G,weight=None))
print(nx.is_weighted(F)) # True
# Correct solution
drop_weights(F)
print(nx.is_weighted(F)) # False
Note that even reconstructing the graph without the weights through nx.to_scipy_sparse_array is not enough because the graph is constructed with weights, only these are set to 1.

How to combine two egdes and nodes in to one that has common starting nodes in Networkx?

I am quite new for networkx and I am asking help from the Stackeroverflow community.
I am trying to combine nodes and edges that have a common starting node as shown below in the figure. The arrow shows the expected result.
nodes_to_combine = [n for n in graph.nodes if len(list(graph.neighbors(n))) == 2]
for node in nodes_to_combine:
graph.add_edge(*graph.neighbors(node))
nx.draw(graph, with_labels=True)
Can anyone help me to figure out this?
NetworkX has no functions to merge nodes in the graph so it should be implemented manually. Here is the example without attributes merging (it can has its own logic):
def merge(G, n1, n2):
# Get all predecessors and successors of two nodes
pre = set(G.predecessors(n1)) | set(G.predecessors(n2))
suc = set(G.successors(n1)) | set(G.successors(n2))
# Create the new node with combined name
name = str(n1) + '/' + str(n2)
# Add predecessors and successors edges
# We have DiGraph so there should be one edge per nodes pair
G.add_edges_from([(p, name) for p in pre])
G.add_edges_from([(name, s) for s in suc])
# Remove old nodes
G.remove_nodes_from([n1, n2])
Here is how it works:
import networkx as nx
G = nx.DiGraph()
G.add_edges_from([
('0','20'),
('10','20'),
('10','30'),
('20','40'),
('30','50'),
])
nx.draw(
G,
pos=nx.nx_agraph.graphviz_layout(G, prog='dot'),
node_color='#FF0000',
with_labels=True
)
merge(G, '20', '30')
nx.draw(
G,
pos=nx.nx_agraph.graphviz_layout(G, prog='dot'),
node_color='#FF0000',
with_labels=True
)

Draw a scale-free network in a lattice

I want to generate graph form an adjacency list but I am not happy with how the nodes are positioned. I want them to be positioned according to a pre-defined scheme which resembles a regular grid with arbitrary coordinates x and y, while still maintaining the scale-free features. Let me give an example: A barabasi-albert network with node 1 located at x_1 = 0.6 and y_1 = 0.5, node 2 located at x_2 = -0.5 and y_2 = 1 ... and so on. I have a list of coordinates of each node.
Have a look at the pos parameter of draw_networkx_XXX functions here.
It can be used like this:
import networkx as nx
import matplotlib.pyplot as plt
from random import randint,seed;
seed(1)
nodes = list(range(5))
edges = [ (nodes[i-1],nodes[i]) for i in range(1,len(nodes)) ]
# here we can set the coordinates to our liking
positions = { node:(randint(0,9),randint(0,9)) for node in nodes }
G=nx.Graph()
G.add_nodes_from(nodes)
G.add_edges_from(edges)
nx.draw_networkx(G,pos=positions, with_labels=False, node_size=100)
plt.show()
[Edit]
Here's how we can build the graph from the adjancency list and assign real values to node positions.
import networkx as nx
import matplotlib.pyplot as plt
from random import randint,seed
from pprint import pprint
seed(0)
edges = [ (randint(0,5),randint(0,5)) for i in range(5) ]
G=nx.Graph()
# nodes added automatically with add_edges_from
G.add_edges_from(edges)
# here we can set positions to our liking
positions = { node: (round((5-randint(0,9))/7.0,2)
, round((5-randint(0,9))/7.0,2)) for node in G.nodes }
pprint({ "edges:": edges, "nodes:":list(G.nodes), "positions:":positions }, width=100)
nx.draw_networkx(G, pos = positions, with_labels=False, node_size=100)
plt.show()
Using positions from a csv file is straightforward.
The pos parameter is really supposed to be a dict with node names as keys (I edited the first snippet to reflect that).
So, if we have a csv file with node names and positions, we just build a dict from it and supply the dict for pos.

Is there a way to run pagerank algorithm on NetworkX's MultiGraph?

I'm working on a graph with multiple edges between the same nodes (edges are having different values). In order to model this graph I need to use MultiGraph instead of normal Graph. Unfortunately, it's not possible to run PageRank algo on it.
Any workarounds known ?
NetworkXNotImplemented: not implemented for multigraph type
You could create make a graph without parallel edges and then run pagerank.
Here is an example of summing edge weights of parallel edges to make a simple graph:
import networkx as nx
G = nx.MultiGraph()
G.add_edge(1,2,weight=7)
G.add_edge(1,2,weight=10)
G.add_edge(2,3,weight=9)
# make new graph with sum of weights on each edge
H = nx.Graph()
for u,v,d in G.edges(data=True):
w = d['weight']
if H.has_edge(u,v):
H[u][v]['weight'] += w
else:
H.add_edge(u,v,weight=w)
print H.edges(data=True)
#[(1, 2, {'weight': 17}), (2, 3, {'weight': 9})]
print nx.pagerank(H)
#{1: 0.32037465332634, 2: 0.4864858243244209, 3: 0.1931395223492388}
You can still compose a Digraph by combining the edges
while adding their weights.
# combining edges using defaultdict
# input-- combined list of all edges
# ouput-- list of edges with summed weights for duplicate edges
from collections import defaultdict
def combine_edges(combined_edge_list):
ddict = defaultdict(list)
for edge in combined_edge_list:
n1,n2,w = edge
ddict[(n1,n2)].append(w)
for k in ddict.keys():
ddict[k] = sum(ddict[k])
edges = list(zip( ddict.keys(), ddict.values() ) )
return [(n1,n2,w) for (n1,n2),w in edges]

Graph traversal with Networkx (Python)

I'm playing a bit with Networkx to manage a graph of dependencies.
Let's say I have this Graph which each letter represent a server
>>> G = nx.Graph()
>>> G.add_edge("A","B")
>>> G.add_edge("A","H")
>>> G.add_edge("H","C")
>>> G.add_edge("B","C")
>>> G.add_edge("B","D")
A
/ \
H B
/ / \
C C D
So here we can see that before starting A we need to start H and B and to start H we need to start C and then to start B wee need to start C and D
By fiddling a bit with Networkx I found that I can get that by doing a dfs traversal
print nx.dfs_successors(G,"A")
{A:[H,B], H:[C], B:[D] }
But I have a problem with that method. As you can see when there is two same letter in the tree, Networkx only chose to put one of them in the final structure (which is correct) But I need to have the complete structure
How can I force Networkx to add in the structure B:[D,C] ??
I want to precise that by doing
>>> nx.dfs_successors(G,"B")
{'B': ['C', 'D']}
So everything is "Internally" correct, it's just the dfs_successors that displays it not in the way I wish.
Thank you
Taking your code, your graph doesn't come out as you'd expect. If you do:
import pylab as p
import networkx as nx
G = nx.Graph()
G.add_edge("A","B")
G.add_edge("A","H")
G.add_edge("H","C")
G.add_edge("B","C")
G.add_edge("B","D")
nx.draw(G)
p.show()
you will see your graph as:
This is due to the logic of G.add_edge("A", "B"):
If G has no node of id "A", add it.
If G has no node of id "B", add it.
Connect "A" to "B" with a new edge.
Thus, you only create five nodes, not six as in your picture.
Edit
Networkx can take any hashable as value for a node, and in the graph it uses str(node) to label each circle. So we can simply define our own Node class (which you maybe want to call Server?) and give it the desired behavior.
import pylab as p
import networkx as nx
class Node(object):
nodes = []
def __init__(self, label):
self._label = label
def __str__(self):
return self._label
nodes = [Node(l) for l in ["A","B","C","C","D","H"]]
edges = [(0,1),(0,5),(5,2),(1,3),(1,4)]
G = nx.Graph()
for i,j in edges:
G.add_edge(nodes[i], nodes[j])
nx.draw(G)
p.show()
gives us
and so what you wanted.
I think what you are looking for is a topological sort https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.dag.topological_sort.html
This only works if you have a DAG (directed acyclic graph).
If so you can draw the tree you want too - like this:
import uuid
import networkx as nx
import matplotlib.pyplot as plt
G = nx.DiGraph()
G.add_edge("A","B")
G.add_edge("A","H")
G.add_edge("H","C")
G.add_edge("B","C")
G.add_edge("B","D")
order = nx.topological_sort(G)
print "topological sort"
print order
# build tree
start = order[0]
nodes = [order[0]] # start with first node in topological order
labels = {}
print "edges"
tree = nx.Graph()
while nodes:
source = nodes.pop()
labels[source] = source
for target in G.neighbors(source):
if target in tree:
t = uuid.uuid1() # new unique id
else:
t = target
labels[t] = target
tree.add_edge(source,t)
print source,target,source,t
nodes.append(target)
nx.draw(tree,labels=labels)
plt.show()
The drawing uses a label mapping to map the ids of the node to the original labels.

Categories