<jupyter_start><jupyter_text>This tutorial is Part 1 of an introduction to social network analysis in Python. It covers how to structure network data, as well as how to use NetworkX to: construct graphs, explore their features, and implement simple algorithms. The primary example used for replication is Zachary's (1977) paper on divisions within a collegiate karate club. The paper is available [here](https://www.jstor.org/stable/pdf/3629752.pdf) and is the source of all tables/figures included below. We use the built-in dataset available from NetworkX, supplemented with a parsed and cleaned version of the weights provided [here](http://vlado.fmf.uni-lj.si/pub/networks/data/ucinet/ucidata.htmzachary).  Table of Contents1&nbsp;&nbsp;Working with network data1.1&nbsp;&nbsp;Structuring network data1.1.1&nbsp;&nbsp;(Optional) Subway example1.2&nbsp;&nbsp;Types of network datasets1.3&nbsp;&nbsp;Storing network data2&nbsp;&nbsp;Preparing network data with NetworkX2.1&nbsp;&nbsp;Building a graph from scratch2.1.1&nbsp;&nbsp;Initializing a graph2.1.2&nbsp;&nbsp;Adding nodes2.1.3&nbsp;&nbsp;Adding edges2.2&nbsp;&nbsp;Building a graph from an edgelist2.3&nbsp;&nbsp;Building special graph types2.3.1&nbsp;&nbsp;Weighted graphs2.3.2&nbsp;&nbsp;Directed graphs2.4&nbsp;&nbsp;Importing built-in graphs3&nbsp;&nbsp;Analyzing network data3.1&nbsp;&nbsp;Inspecting graphs3.1.1&nbsp;&nbsp;Graph size3.1.2&nbsp;&nbsp;Node data3.1.3&nbsp;&nbsp;Edge data3.2&nbsp;&nbsp;Local structure3.2.1&nbsp;&nbsp;Exploring neighbors3.2.2&nbsp;&nbsp;Exploring degree3.2.3&nbsp;&nbsp;Local bridges3.2.4&nbsp;&nbsp;Connected components3.3&nbsp;&nbsp;Algorithms3.3.1&nbsp;&nbsp;Shortest path3.3.2&nbsp;&nbsp;Max flow / min cut3.3.2.1&nbsp;&nbsp;Find the minimum cut4&nbsp;&nbsp;Exporting NetworkX graphs to other data structures4.1&nbsp;&nbsp;As edgelist4.2&nbsp;&nbsp;As dictionary4.3&nbsp;&nbsp;As adjacency matrix5&nbsp;&nbsp;Wrap-up5.1&nbsp;&nbsp;Plots5.2&nbsp;&nbsp;Conclusion Let's get started! First, we need to import all of the packages we'll use to do our analysis.<jupyter_code>import pandas as pd               # For analyzing tabular data
import numpy as np                # For working with arrays and numerical operations
import networkx as nx             # For network data specifically
import matplotlib.pyplot as plt   # For making plots
from pyvis import network as pv   # For making interactive plots
import pprint                     # For making output easier to read

%matplotlib inline                          
pd.set_option('display.max_columns', None)  # For preventing horizontal shortening of tables when displayed
print(nx.__version__)                       # Confirm that we have the latest networkx version<jupyter_output><empty_output><jupyter_text>Working with network data  Structuring network data The first challenge you'll face is figuring out how to structure your network data:- What entities will form the **nodes** or **vertices**?- What will define the **edges** or **links**?  In some cases the answer may seem obvious. For example, we are used to thinking about friendship networks, in which the nodes are people, and the edges are personal connections. In other cases, the natural representation is less clear. Example:Consider the Spotify dataset. - Are nodes: songs, albums, artists, playlists, genres, or listeners? - What should the links between these entities be? There are many different possible ways to structure this data. The right answer will depend on the question you're asking.  (Optional) Subway example  For example, consider the NYC subway. An intuitive representation might have subway stops as nodes, and subway lines as edges: However, we could also build a network of lines connected by transfers: Or a bi-partite network in which nodes are lines and stations, and edges represent the fact that a line stops at a station:  Types of network datasets Once you have your nodes and edges, recall that there are two helpful dimensions along which networks can be categorized:1. Whether their edges are **directed** or **undirected** 2. Whether their edges are **weighted** or **unweighted**In this tutorial, we will discuss how to store and work with data from these different types of networks.<jupyter_code>H = nx.DiGraph([('A', 'B'),
                ('A', 'E'),
                ('B', 'C'),
                ('B', 'D'),
                ('C', 'D'),
                ('D', 'A'),
                ('E', 'D')])


fig, ax = plt.subplots(2,2, figsize=[7,7])

pos = nx.kamada_kawai_layout(H)
nx.draw(H, pos=pos, node_color='cornflowerblue', node_size=1000, arrows=False, width=3, ax=ax[0][0])
nx.draw(H, pos=pos, node_color='cornflowerblue', node_size=1000, arrowsize=30, width=3, ax=ax[0][1])
nx.draw(H, pos=pos, node_color='cornflowerblue', node_size=1000, arrows=False, width=3, ax=ax[1][0])
nx.draw(H, pos=pos, node_color='cornflowerblue', node_size=1000, arrows=False, width=3, ax=ax[1][1])

heavy = [('E', 'D'), ('B', 'D'), ('C', 'D')]
medium = [('A', 'E'), ('D', 'A')]

nx.draw_networkx_edges(H, pos=pos, edgelist=heavy, width=10, ax=ax[1][1])
nx.draw_networkx_edges(H, pos=pos, edgelist=medium, width=5, ax=ax[1][1])


ax[0][0].set_title("Undirected")
ax[0][1].set_title("Directed")
ax[1][0].set_title("Unweighted")
ax[1][1].set_title("Weighted")

plt.show()<jupyter_output><empty_output><jupyter_text>Storing network data One of the first challenges of working with network data is to figure out how to store information about the network structure. There are three common ways to store network structure data:1. **An edge list** has one row for each edge, containing the origin node and the destination node.2. **A dictionary** has one key per node, which corresponds to a list of the node's neighbors.3. **An adjacency matrix** has one entry _per_ possible edge, set to 1 if the edge exists and 0 otherwise. Consider the following example:<jupyter_code>fig, ax = plt.subplots(1,1, figsize=[4,4])
nx.draw_kamada_kawai(H,node_color='cornflowerblue', node_size=1000, arrows=True, 
                     width=3, with_labels=True, font_size=20, ax=ax)<jupyter_output><empty_output><jupyter_text>Edge list:                   Dictionary:               Adjacency matrix:           __________                   ___________               _________________                      A,B                          A: B,E                       A  B  C  D  E             A,E                          B: C,D                    A  0  1  0  0  1            B,C                          C: D                      B  0  0  1  1  0           B,D                          D: A                      C  0  0  0  1  0           C,D                          E: D                      D  1  0  0  0  0           D,A                                                    E  0  0  0  1  0           E,DObserve that the storage requirements for each of these data structures is very different. For example, an adjacency matrix has to store lots of zero entries, so in sparse graphs we might prefer to use edge lists or dictionaries.Question: How would the adjacency matrix look different if the graph was weighted? If the graph was undirected? Altogether, we can think of four main pieces of information that we'll need to store: **nodes, edges, node attributes, and edge attributes.**   Attribute information could be attached to one of the data structures above, or stored in a separate dataset.- For example, the cells of an adjacency matrix might contain information on weights.- For example, information about nodes (e.g. demographic characteristics) might be kept in a separate table.  Preparing network data with NetworkX Let's start with a simple example using the graph above. This will help us build some intuition on the networkX package.We will rely on the following commands:```pythonnx.Graph()                     Initialize a graphnx.DiGraph()                   Initialize a directed graphG.clear()                      Clear the graphG.is_empty()                   Confirm that the graph is emptyG.add_edge()                   Add/remove single nodes/edges from graphG.add_node()G.remove_edge()G.remove_nodes()G.add_edges_from([...])        Add/remove a list of multiple nodes/edges from graphG.add_nodes_from([...])G.remove_edges_from([...])G.remove_nodes_from([...])```            Building a graph from scratch Let's build a simple graph from scratch. For this exercise, let's pretend that we're trying to map which NYU schools allow their students to cross-enroll with another.  Initializing a graph<jupyter_code># Let's create an empty graph
G = nx.Graph()

# Sadly, there are no nodes or edges, which we can see if we plot:
nx.draw(G, with_labels=True)
# As we can confirm, it's empty 
nx.is_empty(G)<jupyter_output><empty_output><jupyter_text>Adding nodes<jupyter_code># Add a single node ... 
G.add_node('Steinhardt')

nx.draw(G, with_labels=True)
# ... or  a list of nodes
G.add_nodes_from(['Stern', 'Courant', 'Silver', 
                  'Wagner', 'Tandon'])

nx.draw(G, with_labels=True)<jupyter_output><empty_output><jupyter_text>Adding edges<jupyter_code># Add a single edge...
G.add_edge('Silver', 'Steinhardt')

nx.draw(G, with_labels=True)
# ... or a list of edges
G.add_edges_from([ ('Stern' , 'Courant'),
                   ('Tandon', 'Courant'),
                   ('Wagner', 'Silver'),
                   ('Wagner', 'Steinhardt'),
                   ('Wagner', 'Stern'),
                   ('Wagner', 'Tandon') ])

# Let's see if it worked:
nx.draw(G, with_labels=True)<jupyter_output><empty_output><jupyter_text>Building a graph from an edgelist Alternatively, you can just supply a list of edges to the graph<jupyter_code>H = nx.Graph([     ('Stern' , 'Courant'),
                   ('Tandon', 'Courant'),
                   ('Wagner', 'Silver'),
                   ('Wagner', 'Steinhardt'),
                   ('Wagner', 'Stern'),
                   ('Wagner', 'Tandon'),
                   ('Silver', 'Steinhardt')
               ])

nx.draw(H, with_labels=True)<jupyter_output><empty_output><jupyter_text>Building special graph types  Weighted graphs We can specify a weighted graph by including a dictionary with the weight as an attribute.<jupyter_code>H.clear()
H = nx.Graph([     ('Stern' , 'Courant',    {'weight': 5}),
                   ('Tandon', 'Courant',    {'weight': 7}),
                   ('Wagner', 'Silver',     {'weight': 1}),
                   ('Wagner', 'Steinhardt', {'weight': 2}),
                   ('Wagner', 'Stern',      {'weight': 9}),
                   ('Wagner', 'Tandon',     {'weight': 3}),
                   ('Silver', 'Steinhardt', {'weight': 6})
               ])

nx.draw(H, with_labels=True)
H.edges.data()<jupyter_output><empty_output><jupyter_text>Directed graphs We specify a directed graph using `nx.DiGraph` instead of `nx.Graph`<jupyter_code>H.clear()
H = nx.DiGraph([   ('Stern' , 'Courant'),
                   ('Tandon', 'Courant'),
                   ('Wagner', 'Silver'),
                   ('Wagner', 'Steinhardt'),
                   ('Wagner', 'Stern'),
                   ('Wagner', 'Tandon'),
                   ('Silver', 'Steinhardt')
               ])

nx.draw(H, with_labels=True, arrowsize=25)<jupyter_output><empty_output><jupyter_text>Can you modify H to reflect the following changes? - Courant now allows its students to enroll in Tandon. - GSAS allows enrollment in Steinhardt, Wagner, and Stern. - Silver no longer wants to participate in any cross-enrolment. **Answer**:        H.add_edge('Courant', 'Tandon')    H.add_edges_from([('GSAS', 'Steinhardt'),('GSAS', 'Wagner'),('GSAS', 'Stern')])    H.remove_node('Silver')    nx.draw(H, with_labels=True, arrowsize=25)<jupyter_code>H.add_edge('Courant', 'Tandon')
H.add_edges_from([('GSAS', 'Steinhardt'),('GSAS', 'Wagner'),('GSAS', 'Stern')])
H.remove_node('Silver')
nx.draw(H, with_labels=True, arrowsize=25)<jupyter_output><empty_output><jupyter_text>Importing built-in graphs In the next lesson, we'll learn how to read in graphs from external data sources. For now, perhaps the most interesting way to get a graph is to use built-in graphs! You can find a list of existing graphs, or options for generating certain types of graphs, here: https://networkx.github.io/documentation/stable/reference/generators.html<jupyter_code># For example, let's generate a random tree
T = nx.random_tree(20)
nx.draw(T)
# Or, we can import Zachary (1977)'s Karate Club:
Z = nx.karate_club_graph()
nx.draw(Z)<jupyter_output><empty_output><jupyter_text>Analyzing network data Let's stick with Zachary's Karate club and try to learn some more about it.  Recall the key facts:- This is a network consisting of 34 members of a Karate club, observed for two years (1970-72).- The teacher, <NAME>, and the president, <NAME>. had a dispute about fees.- The club split into two factions, which formed new clubs: <NAME>'s faction, and John's faction (the officers' club).  Just a quick side note: For replicating the original paper and general ease of use, I have included a quick bit of helper code in the cell below. You don't need to understand it fully right now, but make sure you've run the cell.<jupyter_code># Let's shift the keys by 1 to match the original paper.
new_keys = {key: key+1 for key in range(0,34)}
nx.relabel_nodes(Z, new_keys, copy=False)

# Let's set a standard plot layout
pos = nx.spring_layout(Z, seed=2,  k=.25)<jupyter_output><empty_output><jupyter_text>Inspecting graphsReference: https://networkx.github.io/documentation/stable/reference/functions.html We'll rely on the following commands:```pythonnx.info(G)                          Print a quick summary of the graphG.is_directed()                     Check if the graph is directedG.number_of_nodes()                 Get count of nodesG.number_of_edges()                 Get count of edgesG.nodes                             Get a list of nodesG.edges                             Get a list of edges .data()                            Get data attached to nodes/edges .items()                           Get list of nodes/edges in iterable formatnx.get_node_attributes(G, 'attr')   Get a dictionary of nodes and their values of 'attr'```  Graph size<jupyter_code>nx.draw(Z, with_labels=True, pos=pos)
print(nx.info(Z))
# Let's get the graph size another way
n_V = Z.number_of_nodes()
n_E = Z.number_of_edges()

print(f"Our graph has {n_V} nodes and {n_E} edges.")<jupyter_output><empty_output><jupyter_text>Node data<jupyter_code># Let's get the nodes
Z.nodes()
# Let's get any attributes of nodes ...
Z.nodes.data()
# Great! There is an attribute called club. Let's extract it:
nx.get_node_attributes(Z, 'club')<jupyter_output><empty_output><jupyter_text>Edge data<jupyter_code># Let's get the edges
Z.edges()
# Let's get any attributes of edges ...
Z.edges.data()
# OK. We can see that there are no attributes. 

# Instead, let's ask: Are these edges directed?
Z.is_directed()<jupyter_output><empty_output><jupyter_text>Interesting. We see that we have some labels on which club everyone joined (Mr. Hi's or Officer's), but we are using the unweighted version of the graph. As we expect from the paper, it is undirected.  Question:  What do the edges represent? When Zachary does provide weights, how does he determine them?  Local structureReference: https://networkx.github.io/documentation/stable/reference/functions.html We'll use the following commands:```pythonnx.neighbors(G, n)                     Get a list of neighbors for node nnx.common_neighbors(G, n1, n2)         Get common neighbors for nodes n1 and n2nx.non_neighbors(G, n)                 Get non-neighbors of node nG.degree(n)                            Get n's degreeG.degree                               Get all degrees for all nodesnx.local_bridges(G, with_span=False)   Get a list of all edges that form local bridgesnx.number_connected_components(G)      Get count of connected componentslist(nx.connected_components(G))       Get a list of connected components``` Let's take a second look at the graph and start exploring.<jupyter_code>nx.draw(Z, with_labels=True, pos=pos)<jupyter_output><empty_output><jupyter_text>Exploring neighbors<jupyter_code># Node 17 looks lonely. Let's confirm that (s)he's only friends with 6 and 7
list(nx.neighbors(Z, 17))
# Node 1 is important - it's <NAME>. Let's see who his neighbors are:
list(nx.neighbors(Z, 1))
# Do they share any common neighbors? 
list(nx.common_neighbors(Z,1,17))
nx.draw(Z, pos=pos, with_labels=True)
nx.draw_networkx_nodes(Z, nodelist = [17],  node_color='yellow', pos=pos)
nx.draw_networkx_nodes(Z, nodelist = [1],   node_color='green', pos=pos)
nx.draw_networkx_nodes(Z, nodelist = list(nx.common_neighbors(Z,1,17)), node_color='blue', pos=pos)
plt.show()
# Is there anyone that <NAME> is not friends with?
sorted(list(nx.non_neighbors(Z, 1)))
# Let's visualize. Don't worry too much about the code for now
nx.draw(Z, pos=pos, with_labels=True, node_color='violet')
nx.draw_networkx_nodes(Z, nodelist = [1],   node_color='green', pos=pos)
nx.draw_networkx_nodes(Z, nodelist = list(nx.neighbors(Z, 1)), node_color='lightgreen', pos=pos)

plt.show()<jupyter_output><empty_output><jupyter_text>Exploring degree<jupyter_code># How many neighbors does Mr. Hi have after all? Let's get the degree.
Z.degree(1)
# Actually, we can get the degree of all nodes:
Z.degree<jupyter_output><empty_output><jupyter_text>Local bridges<jupyter_code># We can even get a list of local bridges. 
list(nx.local_bridges(Z, with_span=False))
# Recall that a local bridge consists of two nodes who don't share any neighbors
# Let's check if they fit the definition:
list(nx.common_neighbors(Z,34,20))
# Let's visualize. Don't worry too much about the code for now
nx.draw(Z, pos=pos, with_labels=True)
nx.draw_networkx_edges(Z, edgelist = list(nx.local_bridges(Z, with_span=False)),   
                        edge_color='blue', width=5, pos=pos)
plt.show()<jupyter_output><empty_output><jupyter_text>Connected components Reference: https://networkx.github.io/documentation/stable/reference/algorithms/component.html<jupyter_code># This graph is kind of un-interesting: there is only one component
nx.number_connected_components(Z)
# But, we saw that <NAME> was a very central node. What happens if he quits Karate altogether?

# Let's first copy the graph so we don't overwrite Z
Z_without_hi = Z.copy()

# Now, remove him
Z_without_hi.remove_node(1)

# See how many components are left
nx.number_connected_components(Z_without_hi)
# And, we can see these components -- e.g. list(nx.connected_components(Z_without_hi))
for i in nx.connected_components(Z_without_hi):
    print(i)
# We can plot to confirm
nx.draw(Z_without_hi, pos=pos, with_labels=True)
nx.draw_networkx_nodes(Z_without_hi, pos=pos, nodelist = list(nx.connected_components(Z_without_hi))[1], node_color='yellow')
nx.draw_networkx_nodes(Z_without_hi, pos=pos, nodelist = list(nx.connected_components(Z_without_hi))[2], node_color='blue')
plt.show()<jupyter_output><empty_output><jupyter_text>Algorithms We'll use the following commands:```pythonnx.average_shortest_path_length(G)           Get average length of the shortest path between any two nodesnx.single_source_shortest_path(G, source)    Get the shortest paths from a given source nodenx.shortest_path(G, source, sink)            Get the shortest path between a source and a sink node nx.minimum_cut(G, source, sink,                capacity='weight')            Partition the graph using the minimum cut ```  Shortest path<jupyter_code># Let's get the average shortest path length for this club. We can see that there are only 2.4 degrees of separation!
nx.average_shortest_path_length(Z)
# We could also get the shortest path to all nodes from a given source, e.g. Mr. Hi
nx.single_source_shortest_path(Z, 1)
# Finally, we could also get the shortest path for a specific origin and destination. 
# For example how far is Mr. Hi from John?
nx.shortest_path(Z, 1, 34)
# We can plot to confirm
nx.draw(Z, with_labels=True, pos=pos)
nx.draw_networkx_nodes(Z, nodelist=[1,32,34], with_labels=True, pos=pos, node_color='yellow')
nx.draw_networkx_edges(Z, edgelist=[(1,32), (32,34)], with_labels=True, pos=pos, edge_color='blue', width=10)
plt.show()<jupyter_output><empty_output><jupyter_text>Exercise: - What is the shortest path between John and node 17? - If Mr. Hi leaves, what is the shortest path between John and node 17? (don't forget that we already made the graph: Z_without_hi) **Answer:**nx.shortest_path(Z, 17,34)nx.shortest_path(Z_without_hi, 17,34)<jupyter_code># We can plot to confirm
nx.draw(Z, with_labels=True, pos=pos)
nx.draw_networkx_nodes(Z, nodelist=[1,17,7,32,34], with_labels=True, pos=pos, node_color='yellow')
nx.draw_networkx_edges(Z, edgelist=[(17,7), (7,1), (1,32), (32,34)], with_labels=True, pos=pos, edge_color='blue', width=10)
plt.show()<jupyter_output><empty_output><jupyter_text>Exercise: - If we add a node between nodes 17 and 27, how will the average shortest path in the graph change? (Don't forget to copy the graph first). **Answer**:Z_copy = Z.copy()Z_copy.add_edge(17,27)old_average = nx.average_shortest_path_length(Z) new_average = nx.average_shortest_path_length(Z_copy)print(f"The average shortest path decreases from {old_average} to {new_average}.")<jupyter_code># We can plot to confirm
Z_copy = Z.copy()
Z_copy.add_edge(17,27)
nx.draw(Z_copy, with_labels=True, pos=pos)
nx.draw_networkx_nodes(Z, nodelist=[17,27], with_labels=True, pos=pos, node_color='yellow')
nx.draw_networkx_edges(Z_copy, edgelist=[(17,27)], with_labels=True, pos=pos, edge_color='blue', width=10)
plt.show()<jupyter_output><empty_output><jupyter_text>Max flow / min cut Zachary uses a simple model of information flow and conflict: - Information flows between two poles in the network: <NAME>, and John's.- Information flows over edges, i.e. interaction between members.- Information flow increases with the strength of these edges, i.e. the number of interactions.- Bottlenecks in information flow represent weak parts of the network. - These bottlenecks can be used to predict how the network will split in the face of conflict.His two main hypotheses are:    H1: "[There exists] some structural feature in the network inhibiting information flow between factions." H2: "A bottleneck in the network, representing a structural limitation on information flow from the source to the sink, will predict the break that occurred in the club at the time of the fission." Computationally, he identifies bottlenecks using the maximum-flow minimum-cut labeling procedure of Ford and Fulkerson. In short, - In a network, we can find the **maximum flow** that can be sustained between a source and a sink node.      - It is determined by the graph structure and the capacity of the edges in the graph.- This is equivalent in cost to the **minimum cut** needed to partition the graph into two components (one with the source, the other with the sink).       - It is defined as the minimum total weight of edges that can be removed from the graph to partition it.- Ford and Fulkerson provide an algorithm for identifying these edges.     - If we switch the source and the sink, repeat the min-cut procedure, and still get the same answer, then our cut is unique.    > Per Zachary, "intuitively stated, they proved that the maximum flow is equal to the capacity of the smallest possible break in the network separating the source from the sink."  Brief detour: In order to apply the max flow / min cut algorithm to replicate Zachary, we need edge weights. The code below imports and assigns them to our graph. We'll go over importing data from outside sources in the next section. For now,  let's take it on faith that we can read in a weighted edgelist from CSV...<jupyter_code># Import edge weights...
C = pd.read_csv("data/zachary_edge_weights.csv")

# ... and add the edges to our graph
for edge in np.array(C):
    u,v,w= edge
    Z[u][v]['weight'] = w

# Make sure it worked
Z.edges.data()<jupyter_output><empty_output><jupyter_text>Find the minimum cut<jupyter_code>source = 1   # <NAME>
sink   = 34  # John A 

# Let's apply the algorithm to make the minimum cut on the graph
cut_value, partition = nx.minimum_cut(Z, source, sink, capacity='weight')
# We can inspect the remaining partition of the graph:
partition
fig, ax = plt.subplots(1,2,figsize=[15,5])

nx.draw(Z, pos=pos, with_labels=True, ax=ax[1])
nx.draw_networkx_nodes(Z,pos=pos, nodelist =list(partition[0]), node_color='blue', ax=ax[1])
nx.draw_networkx_nodes(Z,pos=pos, nodelist =list(partition[1]), node_color='red', ax=ax[1])
nx.draw_networkx_nodes(Z,pos=pos, nodelist =[1,34], node_color='yellow', ax=ax[1])
ax[1].set_title("Factions predicted by algorithm")

# Now, let's reconstruct and plot the original
mr_hi   = [node for node,data in Z.nodes(data=True) if data['club']=='Mr. Hi']
officer = [node for node,data in Z.nodes(data=True) if data['club']=='Officer']

nx.draw(Z, pos=pos, with_labels=True, ax=ax[0])
nx.draw_networkx_nodes(Z,pos=pos, nodelist =mr_hi, node_color='blue', ax=ax[0])
nx.draw_networkx_nodes(Z,pos=pos, nodelist =officer, node_color='red', ax=ax[0])
nx.draw_networkx_nodes(Z,pos=pos, nodelist =[1,34], node_color='yellow', ax=ax[0])
ax[0].set_title("Clubs found in real life")

plt.show()<jupyter_output><empty_output><jupyter_text>We can see that we have replicated Zachary's findings: we have one misprediction, node 9.   Exporting NetworkX graphs to other data structures Let's revisit the graph representations from above. Now that we have this nice Karate club graph, we can convert it back into the canonical representations and replicate the adjacency matrix from the paper. We'll use the following commands:```pythonnx.to_edgelist(Z)             Convert to edge listnx.to_dict_of_lists(Z)        Convert to a dictionary of listsnx.to_pandas_adjacency(Z)     Convert to an adjacency matrix in pandasnx.to_numpy_matrix(Z)         Convert to an adjacency matrix in numpy```  As edgelist<jupyter_code># The simplest representation is a basic edgelist
list(nx.to_edgelist(Z))<jupyter_output><empty_output><jupyter_text>As dictionary<jupyter_code># We can also convert to a dictionary of lists
nx.to_dict_of_lists(Z)<jupyter_output><empty_output><jupyter_text>As adjacency matrix<jupyter_code># To adjacency matrix (the add-ons are to make the print output match the paper)
nx.to_pandas_adjacency(Z)  .astype(int).sort_index().sort_index(axis=1)<jupyter_output><empty_output><jupyter_text>Wrap-up  Plots Let's end with a simple plot to replicate the classic diagram from Zachary:We will cover graphing in the next notebook, but just for fun...<jupyter_code># Let's tell networkX we want a circular layout
pos = nx.circular_layout(Z)

# We need to jump through some hoops to rotate the graph
new_pos = {}
for k,v in pos.items():
    new_pos[ (k+8)%34 +1 ] = v
    
# And there we go!
nx.draw(Z, pos=new_pos, with_labels=True)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.107600341588386, 'fraction_numerical': 0.013589277095013553, 'mean_word_length': 4.012841987716359, 'pattern_counts': {'":': 0, '<': 125, '<?xml version=': 0, '>': 126, 'https://': 5, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 1, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '29', 'dir': 'jupyter-structured-clean-dedup', 'id': '25518', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 8591, 'n_tokens_neox': 8092, 'n_words': 2856, 'path': 'tutorial_part_1_analyze.ipynb', 'repo_name': 'khof312/networks_tutorial', 'size': '26943'}