In [ ]:
import networkx as nx
import matplotlib.pyplot as plt
import warnings
from custom import load_data as cf
from itertools import combinations
warnings.filterwarnings('ignore')

%matplotlib inline
%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = 'retina'

Load Data

As usual, let's start by loading some network data. This time round, we have a physician trust network, but slightly modified such that it is undirected rather than directed.

This directed network captures innovation spread among 246 physicians in for towns in Illinois, Peoria, Bloomington, Quincy and Galesburg. The data was collected in 1966. A node represents a physician and an edge between two physicians shows that the left physician told that the righ physician is his friend or that he turns to the right physician if he needs advice or is interested in a discussion. There always only exists one edge between two nodes even if more than one of the listed conditions are true.


In [ ]:
# Load the network.
G = cf.load_physicians_network()

In [ ]:
# Make a Circos plot of the graph
from nxviz import CircosPlot

c = CircosPlot(G)
c.draw()

Question

What can you infer about the structure of the graph from the Circos plot?

Structures in a Graph

We can leverage what we have learned in the previous notebook to identify special structures in a graph.

In a network, cliques are one of these special structures.

Cliques

In a social network, cliques are groups of people in which everybody knows everybody.

Questions:

  1. What is the simplest clique?
  2. What is the simplest complex clique?

Let's try implementing a simple algorithm that finds out whether a node is present in a simple complex clique.


In [ ]:
# Example code.
def in_triangle(G, node):
    """
    Returns whether a given node is present in a triangle relationship or not.
    """
    # We first assume that the node is not present in a triangle.
    is_in_triangle = False
    
    # Then, iterate over every pair of the node's neighbors.
    for nbr1, nbr2 in combinations(G.neighbors(node), 2):
        # Check to see if there is an edge between the node's neighbors.
        # If there is an edge, then the given node is present in a triangle.
        if G.has_edge(nbr1, nbr2):
            is_in_triangle = True
            # We break because any triangle that is present automatically 
            # satisfies the problem requirements.
            break
    return is_in_triangle

in_triangle(G, 3)

In reality, NetworkX already has a function that counts the number of triangles that any given node is involved in. This is probably more useful than knowing whether a node is present in a triangle or not, but the above code was simply for practice.


In [ ]:
nx.triangles(G, 3)

Exercise

Can you write a function that takes in one node and its associated graph as an input, and returns a list or set of itself + all other nodes that it is in a triangle relationship with? Do not return the triplets, but the set/list of nodes. (5 min.)

Possible Implementation: If I check every pair of my neighbors, any pair that are also connected in the graph are in a triangle relationship with me.

Hint: Python's itertools module has a combinations function that may be useful.

Hint: NetworkX graphs have a .has_edge(node1, node2) function that checks whether an edge exists between two nodes.

Verify your answer by drawing out the subgraph composed of those nodes.


In [ ]:
# Possible answer
def get_triangles(G, node):
    neighbors = set(G.neighbors(node))
    triangle_nodes = set()
    """
    Fill in the rest of the code below.
    """

    
    
    
    
    return triangle_nodes

# Verify your answer with the following funciton call. Should return something of the form:
# {3, 9, 11, 41, 42, 67}
get_triangles(G, 3)

In [ ]:
# Then, draw out those nodes.
nx.draw(G.subgraph(get_triangles(G, 3)), with_labels=True)

In [ ]:
# Compare for yourself that those are the only triangles that node 3 is involved in.
neighbors3 = G.neighbors(3)
neighbors3.append(3)
nx.draw(G.subgraph(neighbors3), with_labels=True)

Friend Recommendation: Open Triangles

Now that we have some code that identifies closed triangles, we might want to see if we can do some friend recommendations by looking for open triangles.

Open triangles are like those that we described earlier on - A knows B and B knows C, but C's relationship with A isn't captured in the graph.

What are the two general scenarios for finding open triangles that a given node is involved in?

  1. The given node is the centre node.
  2. The given node is one of the termini nodes.

Exercise

Can you write a function that identifies, for a given node, the other two nodes that it is involved with in an open triangle, if there is one? (5 min.)

Note: For this exercise, only consider the case when the node of interest is the centre node.

Possible Implementation: Check every pair of my neighbors, and if they are not connected to one another, then we are in an open triangle relationship.


In [ ]:
# Fill in your code here.
def get_open_triangles(G, node):
    """
    There are many ways to represent this. One may choose to represent only the nodes involved 
    in an open triangle; this is not the approach taken here.
    
    Rather, we have a code that explicitly enumrates every open triangle present.
    """
    open_triangle_nodes = []
    neighbors = set(G.neighbors(node))
    
    for n1, n2 in combinations(G.neighbors(node), 2):
        ...
        
        
        
        
        
        
    return open_triangle_nodes

In [ ]:
# # Uncomment the following code if you want to draw out each of the triplets.
# nodes = get_open_triangles(G, 2)
# for i, triplet in enumerate(nodes):
#     fig = plt.figure(i)
#     nx.draw(G.subgraph(triplet), with_labels=True)
print(get_open_triangles(G, 3))
len(get_open_triangles(G, 3))

Triangle closure is also the core idea behind social networks' friend recommendation systems; of course, it's definitely more complicated than what we've implemented here.

Cliques

We have figured out how to find triangles. Now, let's find out what cliques are present in the network. Recall: what is the definition of a clique?

  • NetworkX has a clique-finding algorithm implemented.
  • This algorithm finds all maximally-sized cliques for a given node.
  • Note that maximal cliques of size n include all cliques of size < n

In [ ]:
list(nx.find_cliques(G))

Exercise

Try writing a function maximal_cliques_of_size(size, G) that implements a search for all maximal cliques of a given size. (3 min.)


In [ ]:
def maximal_cliqes_of_size(size, G):
    return ______________________

maximal_cliqes_of_size(2, G)

Connected Components

From Wikipedia:

In graph theory, a connected component (or just component) of an undirected graph is a subgraph in which any two vertices are connected to each other by paths, and which is connected to no additional vertices in the supergraph.

NetworkX also implements a function that identifies connected component subgraphs.

Remember how based on the Circos plot above, we had this hypothesis that the physician trust network may be divided into subgraphs. Let's check that, and see if we can redraw the Circos visualization.


In [ ]:
ccsubgraph_nodes = list(nx.connected_components(G))
ccsubgraph_nodes

Exercise

Draw a circos plot of the graph, but now colour and order the nodes by their connected component subgraph. (5 min.)

Recall Circos API:

c = CircosPlot(G, node_order='...', node_color='...')
c.draw()
plt.show()  # or plt.savefig(...)

In [ ]:
# Start by labelling each node in the master graph G by some number
# that represents the subgraph that contains the node.
for i, g in enumerate(_____________):
    # Fill in code below.

In [ ]:
c = CircosPlot(G, _________)
c.draw()
plt.savefig('images/physicians.png', dpi=300)

And "admire" the division of the US congress over the years...


In [ ]: