In [1]:

```
import networkx as nx
import matplotlib.pyplot as plt
```

In this notebook we will be showing how we can use NetworkX to study weighted and directed graphs. We will be building on the concepts that we followed in Notebook 2.1, and will therefore be reusing some of the code that we discussed there.

Given the amount of repetition on the sequence of commands that we will be using, we will take this opportunity to introduce the use of functions, optional function parameters, and conditional execution of function logic.

We will be jumping straight back into our usual graph - let us populate all its elements.

In [2]:

```
G = nx.Graph()
G.add_node(1)
G.add_node(2)
G.add_node(3)
G.add_node(4)
G.add_edge(1, 2)
G.add_edge(2, 3)
G.add_edge(2, 4)
```

`nx.draw()`

command.

In [3]:

```
nx.draw(G, with_labels = True, font_color = 'white', node_shape='s')
```

As we are going to be using it quite often in this notebook, we can create our custom function, which we can use instead of the full command.

We shall name this `show_graph()`

.

In [4]:

```
def show_graph():
nx.draw(G, with_labels = True, font_color = 'white', node_shape = 's')
```

Note that this function does not take any variables. When it comes to the graph that it is going to plot, it just uses whatever graph happens to be available in the variable G.

This is actually a very poor practice - if we happen to use another graph object, saved in a variable called `G1`

, we wouldn't be able to draw it with the `show_graph()`

command. We can overcome this limitation with the addition of a function parameter:

In [5]:

```
def show_graph(graph):
nx.draw(graph, with_labels = True, font_color = 'white', node_shape = 's')
show_graph(G)
```

`graph`

. If we now use the `show_graph()`

function, we **must** also supply a graph object as a parameter. What would happen if we used it as before?

In [6]:

```
show_graph()
```

Python, very rightly, complaints! The error message very bluntly infroms us that an argument called `'graph'`

is missing.

We can have the best of the two worlds (convenience and customisation) using an optional parameter. In fact, this is what networkx does all of the time - you might have noticed that we can use nx.draw() with only one parameter or many more, and python never complaints!

In [7]:

```
def show_graph(graph=G):
nx.draw(graph, with_labels = True, font_color = 'white', node_shape = 's')
show_graph()
```

In the revised function definition, we tell Python that this function can accept a parameter called `graph`

, however if that is not supplied, it should just take whatever is stored in the variable called `G`

. This will still fail, however, if there is no such variable, so make sure that the optional parameter assumptions you make are sensible!

Let's double check this with the Petersen graph generator, provided by networkx:

In [8]:

```
show_graph(nx.petersen_graph())
```

Success! It all works as it should now. Let's continue by extending our original graph.

In [9]:

```
G.add_edge(4,5)
G.add_edge(3,6)
G.add_edge(5,7)
G.add_edge(6,7)
G.add_edge(7,8)
G.add_edge(2,7)
show_graph()
```

In [10]:

```
path = nx.shortest_path(G, source = 1, target =8)
edges_path = list(zip(path,path[1:]))
edges_path_reversed = [(y,x) for (x,y) in edges_path]
edges_path = edges_path + edges_path_reversed
edge_colors = ['black' if not edge in edges_path else 'red' for edge in G.edges()]
nx.draw(G, with_labels = True, font_color = 'white', edge_color= edge_colors, node_shape = 's')
```

Note that we have reused our edge reversion "hack" to overcome the issues that we encountered with bidirectionality, when it comes to highlighting the edges that are used by our path.

The highlighted edges definitely look useful, but wouldn't it be nice if we also highlighted the nodes? We can do this using the `node_color`

parameter.

In [11]:

```
path = nx.shortest_path(G, source = 1, target =8)
nodecol = ['steelblue' if not node in path else 'red' for node in G.nodes()]
nx.draw(G, with_labels = True, font_color = 'white', edge_color= edge_colors, node_shape = 's', node_color = nodecol)
```

Now, let's convert the above code into a function.

In [12]:

```
def show_path(from_node, to_node):
path = nx.shortest_path(G, source = 1, target =8)
edges_path = list(zip(path,path[1:]))
edges_path_reversed = [(y,x) for (x,y) in edges_path]
edges_path = edges_path + edges_path_reversed
edge_colors = ['black' if not edge in edges_path else 'red' for edge in G.edges()]
nodecol = ['steelblue' if not node in path else 'red' for node in G.nodes()]
nx.draw(G, with_labels = True, font_color = 'white', edge_color= edge_colors, node_shape = 's', node_color = nodecol)
show_path(6, 4)
```

We have so far assumed that our graphs are weightless - when it comes to calculating shortest paths, networkx uses a default weight for each edge.

We can inspect the parameter values of our edges using the following command:

In [13]:

```
nx.get_edge_attributes(G,'weight')
```

Out[13]:

{}

... nothing to see here!

It is to be expected, since we haven't actually assigned any weights so far. Let's initialise all weights to 1.

In [14]:

```
for i,j in G.edges():
G[i][j]['weight'] = 1
nx.get_edge_attributes(G,'weight')
```

Out[14]:

{(1, 2): 1, (2, 3): 1, (2, 4): 1, (2, 7): 1, (3, 6): 1, (4, 5): 1, (5, 7): 1, (6, 7): 1, (7, 8): 1}

Wonderful. We received a dictionary, with the edge values as a key and the weight as the value.

Now, how about plotting the weight of the edges alongside our graph?

In [15]:

```
plt.figure()
pos = nx.spring_layout(G)
weight_labels = nx.get_edge_attributes(G,'weight')
nx.draw(G,pos,font_color = 'white', node_shape = 's', with_labels = True,)
nx.draw_networkx_edge_labels(G,pos,edge_labels=weight_labels)
```

Out[15]:

{(1, 2): Text(-0.015423141932697455, 0.6562154014988251, '1'), (2, 3): Text(-0.13323523892060615, -0.06251299875547647, '1'), (2, 4): Text(-0.3950733096613288, 0.38684649203035615, '1'), (2, 7): Text(0.07839897133863552, 0.05500757131749211, '1'), (3, 6): Text(0.001753648461201851, -0.6268861527478838, '1'), (4, 5): Text(-0.5860361269197304, 0.22848582663567374, '1'), (5, 7): Text(-0.11256384591976601, -0.10335309407719027, '1'), (6, 7): Text(0.21338785872044352, -0.5093655826749153, '1'), (7, 8): Text(0.5997056203912259, -0.2578150753866149, '1')}

Things begin to get a little bit messier. It turns out that our beloved `nx.draw()`

command is not capable of plotting edge labels. To do this we need to use the `nx.draw_networkx_edge_labels()`

function, which renders the edge values on top of a pre-existing drawing.

However, to make sure that the edge labels are rendered correctly, we need to fix position of the nodes. To do this, we use the `nx.spring_layout()`

command, which automatically determines some positions for our graph - these are stored to the variable `pos`

, and are then suplied to `nx.draw()`

and `nx.draw_networkx_edge_labels()`

.

Let's now convert all of this into a new function, as we will be using it a lot more.

In [16]:

```
def show_wgraph():
plt.figure()
pos = nx.spring_layout(G)
weight_labels = nx.get_edge_attributes(G,'weight')
nx.draw(G,pos,font_color = 'white', node_shape = 's', with_labels = True,)
output = nx.draw_networkx_edge_labels(G,pos,edge_labels=weight_labels)
```

We can now experiment with some weights.

In [17]:

```
G[7][2]['weight'] = 10
show_wgraph()
```

In [18]:

```
def show_wpath(from_node, to_node):
plt.figure()
pos = nx.spring_layout(G)
weight_labels = nx.get_edge_attributes(G,'weight')
path = nx.shortest_path(G, source = from_node, target = to_node)
edges_path = list(zip(path,path[1:]))
edges_path_reversed = [(y,x) for (x,y) in edges_path]
edges_path = edges_path + edges_path_reversed
edge_colors = ['black' if not edge in edges_path else 'red' for edge in G.edges()]
nodecol = ['steelblue' if not node in path else 'red' for node in G.nodes()]
nx.draw(G, pos, with_labels = True, font_color = 'white', edge_color= edge_colors, node_shape = 's', node_color = nodecol)
nx.draw_networkx_edge_labels(G,pos,edge_labels=weight_labels)
show_wpath(1,8)
```

There is something wrong here - the shortest path should definitely not pass from edge (2,7), given the weight of 10.

The issue actually lies with the `nx.shortest_path()`

function - unless we explicitely tell it what weight values to consider, it assumes that no weights should be used.

To keep things simple, we will be using the `nx.dijkstra_path()`

instead, which will use the `weight`

parameters if they are present.

In [19]:

```
def show_wpath_d(from_node, to_node):
plt.figure()
pos = nx.spring_layout(G)
weight_labels = nx.get_edge_attributes(G,'weight')
path = nx.dijkstra_path(G, source = from_node, target = to_node)
edges_path = list(zip(path,path[1:]))
edges_path_reversed = [(y,x) for (x,y) in edges_path]
edges_path = edges_path + edges_path_reversed
edge_colors = ['black' if not edge in edges_path else 'red' for edge in G.edges()]
nodecol = ['steelblue' if not node in path else 'red' for node in G.nodes()]
nx.draw(G, pos, with_labels = True, font_color = 'white', edge_color= edge_colors, node_shape = 's', node_color = nodecol)
nx.draw_networkx_edge_labels(G,pos,edge_labels=weight_labels)
show_wpath_d(1,8)
```

In the final part of this notebook, we will focusing on the definition and manipulation of directed graphs.

We can initiate a directed graph object using the `nx.DiGraph()`

class.

To get things started, we will seek to implement the directed graph that we used during when introducing Dijkstra's in our lecture .