It is suprising right? What is the relationship between a fatansy TV show/novel and network science or python(it's not related to a dragon).
If you haven't heard of Game of Thrones, then you must be really good at hiding. Game of Thrones is the hugely popular television series by HBO based on the (also) hugely popular book series A Song of Ice and Fire by George R.R. Martin. In this notebook, we will analyze the co-occurrence network of the characters in the Game of Thrones books. Here, two characters are considered to co-occur if their names appear in the vicinity of 15 words from one another in the books.
Andrew J. Beveridge, an associate professor of mathematics at Macalester College, and Jie Shan, an undergraduate created a network from the book A Storm of Swords by extracting relationships between characters to find out the most important characters in the book(or GoT).
The dataset is publicly avaiable for the 5 books at https://github.com/mathbeveridge/asoiaf. This is an interaction network and were created by connecting two characters whenever their names (or nicknames) appeared within 15 words of one another in one of the books. The edge weight corresponds to the number of interactions.
Credits:
Blog: https://networkofthrones.wordpress.com
Math Horizons Article: https://www.maa.org/sites/default/files/pdf/Mathhorizons/NetworkofThrones%20%281%29.pdf
In [ ]:
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
import community
import numpy as np
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
In [ ]:
book1 = pd.read_csv('datasets/game_of_thrones_network/asoiaf-book1-edges.csv')
book2 = pd.read_csv('datasets/game_of_thrones_network/asoiaf-book2-edges.csv')
book3 = pd.read_csv('datasets/game_of_thrones_network/asoiaf-book3-edges.csv')
book4 = pd.read_csv('datasets/game_of_thrones_network/asoiaf-book4-edges.csv')
book5 = pd.read_csv('datasets/game_of_thrones_network/asoiaf-book5-edges.csv')
The resulting DataFrame book1 has 5 columns: Source, Target, Type, weight, and book. Source and target are the two nodes that are linked by an edge. A network can have directed or undirected edges and in this network all the edges are undirected. The weight attribute of every edge tells us the number of interactions that the characters have had over the book, and the book column tells us the book number.
In [ ]:
book1.head()
Once we have the data loaded as a pandas DataFrame, it's time to create a network. We create a graph for each book. It's possible to create one MultiGraph instead of 5 graphs, but it is easier to play with different graphs.
In [ ]:
G_book1 = nx.Graph()
G_book2 = nx.Graph()
G_book3 = nx.Graph()
G_book4 = nx.Graph()
G_book5 = nx.Graph()
Let's populate the graph with edges from the pandas DataFrame.
In [ ]:
for row in book1.iterrows():
G_book1.add_edge(row[1]['Source'], row[1]['Target'], weight=row[1]['weight'], book=row[1]['book'])
In [ ]:
for row in book2.iterrows():
G_book2.add_edge(row[1]['Source'], row[1]['Target'], weight=row[1]['weight'], book=row[1]['book'])
for row in book3.iterrows():
G_book3.add_edge(row[1]['Source'], row[1]['Target'], weight=row[1]['weight'], book=row[1]['book'])
for row in book4.iterrows():
G_book4.add_edge(row[1]['Source'], row[1]['Target'], weight=row[1]['weight'], book=row[1]['book'])
for row in book5.iterrows():
G_book5.add_edge(row[1]['Source'], row[1]['Target'], weight=row[1]['weight'], book=row[1]['book'])
In [ ]:
books = [G_book1, G_book2, G_book3, G_book4, G_book5]
Let's have a look at these edges.
In [ ]:
list(G_book1.edges(data=True))[16]
In [ ]:
list(G_book1.edges(data=True))[400]
Is it Jon Snow, Tyrion, Daenerys, or someone else? Let's see! Network Science offers us many different metrics to measure the importance of a node in a network as we saw in the first part of the tutorial. Note that there is no "correct" way of calculating the most important node in a network, every metric has a different meaning.
First, let's measure the importance of a node in a network by looking at the number of neighbors it has, that is, the number of nodes it is connected to. For example, an influential account on Twitter, where the follower-followee relationship forms the network, is an account which has a high number of followers. This measure of importance is called degree centrality.
Using this measure, let's extract the top ten important characters from the first book (book[0]) and the fifth book (book[4]).
In [ ]:
deg_cen_book1 = nx.degree_centrality(books[0])
In [ ]:
deg_cen_book5 = nx.degree_centrality(books[4])
In [ ]:
sorted(deg_cen_book1.items(), key=lambda x:x[1], reverse=True)[0:10]
In [ ]:
sorted(deg_cen_book5.items(), key=lambda x:x[1], reverse=True)[0:10]
In [ ]:
# Plot a histogram of degree centrality
plt.hist(list(nx.degree_centrality(G_book4).values()))
plt.show()
In [ ]:
d = {}
for i, j in dict(nx.degree(G_book4)).items():
if j in d:
d[j] += 1
else:
d[j] = 1
x = np.log2(list((d.keys())))
y = np.log2(list(d.values()))
plt.scatter(x, y, alpha=0.9)
plt.show()
In [ ]:
def weighted_degree(G, weight):
result = dict()
for node in G.nodes():
weight_degree = 0
for n in G.edges([node], data=True):
weight_degree += n[2]['weight']
result[node] = weight_degree
return result
In [ ]:
plt.hist(list(weighted_degree(G_book1, 'weight').values()))
plt.show()
In [ ]:
sorted(weighted_degree(G_book1, 'weight').items(), key=lambda x:x[1], reverse=True)[0:10]
In [ ]:
# First check unweighted, just the structure
sorted(nx.betweenness_centrality(G_book1).items(), key=lambda x:x[1], reverse=True)[0:10]
In [ ]:
# Let's care about interactions now
sorted(nx.betweenness_centrality(G_book1, weight='weight').items(), key=lambda x:x[1], reverse=True)[0:10]
In [ ]:
# by default weight attribute in pagerank is weight, so we use weight=None to find the unweighted results
sorted(nx.pagerank_numpy(G_book1, weight=None).items(), key=lambda x:x[1], reverse=True)[0:10]
In [ ]:
sorted(nx.pagerank_numpy(G_book1, weight='weight').items(), key=lambda x:x[1], reverse=True)[0:10]
In [ ]:
cor = pd.DataFrame.from_records([nx.pagerank_numpy(G_book1, weight='weight'), nx.betweenness_centrality(G_book1, weight='weight'), weighted_degree(G_book1, 'weight'), nx.degree_centrality(G_book1)])
In [ ]:
# cor.T
In [ ]:
cor.T.corr()
According to degree centrality the most important character in the first book is Eddard Stark but he is not even in the top 10 of the fifth book. The importance changes over the course of five books, because you know stuff happens ;)
Let's look at the evolution of degree centrality of a couple of characters like Eddard Stark, Jon Snow, Tyrion which showed up in the top 10 of degree centrality in first book.
We create a dataframe with character columns and index as books where every entry is the degree centrality of the character in that particular book and plot the evolution of degree centrality Eddard Stark, Jon Snow and Tyrion. We can see that the importance of Eddard Stark in the network dies off and with Jon Snow there is a drop in the fourth book but a sudden rise in the fifth book
In [ ]:
evol = [nx.degree_centrality(book) for book in books]
evol_df = pd.DataFrame.from_records(evol).fillna(0)
evol_df[['Eddard-Stark', 'Tyrion-Lannister', 'Jon-Snow']].plot()
In [ ]:
set_of_char = set()
for i in range(5):
set_of_char |= set(list(evol_df.T[i].sort_values(ascending=False)[0:5].index))
set_of_char
In [ ]:
evol_df[list(set_of_char)].plot(figsize=(29,15))
In [ ]:
evol = [nx.betweenness_centrality(graph, weight='weight') for graph in [G_book1, G_book2, G_book3, G_book4, G_book5]]
evol_df = pd.DataFrame.from_records(evol).fillna(0)
set_of_char = set()
for i in range(5):
set_of_char |= set(list(evol_df.T[i].sort_values(ascending=False)[0:5].index))
evol_df[list(set_of_char)].plot(figsize=(19,10))
In [ ]:
nx.draw(nx.barbell_graph(5, 1), with_labels=True)
In [ ]:
sorted(nx.degree_centrality(G_book5).items(), key=lambda x:x[1], reverse=True)[:5]
In [ ]:
sorted(nx.betweenness_centrality(G_book5).items(), key=lambda x:x[1], reverse=True)[:5]
A network is said to have community structure if the nodes of the network can be easily grouped into (potentially overlapping) sets of nodes such that each set of nodes is densely connected internally.
We will use louvain community detection algorithm to find the modules in our graph.
In [ ]:
plt.figure(figsize=(15, 15))
partition = community.best_partition(G_book1)
size = float(len(set(partition.values())))
pos = nx.kamada_kawai_layout(G_book1)
count = 0
colors = ['red', 'blue', 'yellow', 'black', 'brown', 'purple', 'green', 'pink']
for com in set(partition.values()):
list_nodes = [nodes for nodes in partition.keys()
if partition[nodes] == com]
nx.draw_networkx_nodes(G_book1, pos, list_nodes, node_size = 20,
node_color = colors[count])
count = count + 1
nx.draw_networkx_edges(G_book1, pos, alpha=0.2)
plt.show()
In [ ]:
d = {}
for character, par in partition.items():
if par in d:
d[par].append(character)
else:
d[par] = [character]
d
In [ ]:
nx.draw(nx.subgraph(G_book1, d[3]))
In [ ]:
nx.draw(nx.subgraph(G_book1, d[1]))
In [ ]:
nx.density(G_book1)
In [ ]:
nx.density(nx.subgraph(G_book1, d[4]))
In [ ]:
nx.density(nx.subgraph(G_book1, d[4]))/nx.density(G_book1)
In [ ]:
max_d = {}
deg_book1 = nx.degree_centrality(G_book1)
for group in d:
temp = 0
for character in d[group]:
if deg_book1[character] > temp:
max_d[group] = character
temp = deg_book1[character]
In [ ]:
max_d
In [ ]:
G_random = nx.erdos_renyi_graph(100, 0.1)
In [ ]:
nx.draw(G_random)
In [ ]:
G_ba = nx.barabasi_albert_graph(100, 2)
In [ ]:
nx.draw(G_ba)
In [ ]:
# Plot a histogram of degree centrality
plt.hist(list(nx.degree_centrality(G_random).values()))
plt.show()
In [ ]:
plt.hist(list(nx.degree_centrality(G_ba).values()))
plt.show()
In [ ]:
G_random = nx.erdos_renyi_graph(2000, 0.2)
G_ba = nx.barabasi_albert_graph(2000, 20)
In [ ]:
d = {}
for i, j in dict(nx.degree(G_random)).items():
if j in d:
d[j] += 1
else:
d[j] = 1
x = np.log2(list((d.keys())))
y = np.log2(list(d.values()))
plt.scatter(x, y, alpha=0.9)
plt.show()
In [ ]:
d = {}
for i, j in dict(nx.degree(G_ba)).items():
if j in d:
d[j] += 1
else:
d[j] = 1
x = np.log2(list((d.keys())))
y = np.log2(list(d.values()))
plt.scatter(x, y, alpha=0.9)
plt.show()
In [ ]: