30 min read

> "Everything in the universe is connected to everything else. But some things are more connected than others, and the pattern of connection is the story."

Learning Objectives

  • Explain the fundamentals of graph/network data: nodes, edges, directed vs. undirected, weighted vs. unweighted
  • Create network visualizations with NetworkX and matplotlib
  • Apply layout algorithms (spring, circular, shell, kamada_kawai, spectral) and explain when each is appropriate
  • Create interactive network visualizations with Plotly and pyvis
  • Encode node and edge attributes visually: size for centrality, color for community, width for weight
  • Evaluate when network visualization is appropriate and when a matrix (heatmap) is better
  • Handle the hairball problem: too many nodes and edges, node overlap, readability

Chapter 24: Network and Graph Visualization — Nodes, Edges, and Relationships

"Everything in the universe is connected to everything else. But some things are more connected than others, and the pattern of connection is the story." — Albert-László Barabási, paraphrased from Linked (2002)


24.1 When Relationships Are the Story

Most of the charts in this textbook have treated data as a collection of independent observations. A scatter plot assumes that each point is a separate observation with its own x and y. A bar chart assumes each bar is a separate category. Even a correlation heatmap assumes the relationships between columns are the data, not the columns themselves. In all of these, the individual observations are the atoms, and the chart shows their distribution or aggregate behavior.

But some data is fundamentally about connections between things. A social network is a collection of people plus a collection of friendships — the people are nothing special on their own; what matters is who knows whom. A food web is a collection of species plus a record of who eats whom. A citation network is a collection of papers plus a record of which papers cite which. A supply chain is a collection of companies plus a record of which companies buy from which. In each case, the interesting data is the relationships between the entities, not the entities themselves.

The mathematical object that represents relational data is the graph (in the combinatorial sense, not the plotting sense — unfortunately the two meanings of "graph" coexist in practice, and context disambiguates). A graph consists of a set of nodes (or vertices) and a set of edges (or links) connecting pairs of nodes. Each edge connects two specific nodes; the structure of which edges exist is the data.

Graphs come in several flavors:

  • Undirected: edges have no direction. A friendship edge between Alice and Bob is symmetric — if Alice is friends with Bob, Bob is friends with Alice.
  • Directed (digraph): edges have a direction. A "follows" edge on Twitter/X is asymmetric — Alice following Bob does not imply Bob follows Alice. Directed graphs distinguish sources and targets.
  • Weighted: edges have numeric weights representing strength, frequency, distance, capacity, or similar magnitudes. An edge in a road network might have a weight equal to the road's length or the typical travel time.
  • Multigraph: multiple edges can exist between the same pair of nodes. In a communication network, Alice and Bob might have many message exchanges.
  • Bipartite: nodes fall into two distinct sets, and edges only connect nodes from different sets. A recommendation system is bipartite (users and items) with edges representing purchases or ratings.

Visualizing a graph is fundamentally different from visualizing a table. Where a scatter plot has fixed axes (x and y), a graph has no intrinsic spatial layout — nodes do not "live" at specific coordinates. The visualization has to construct a spatial layout by choosing positions for the nodes, and then drawing edges as lines between the positions. The choice of layout algorithm is the most consequential decision in network visualization, and different algorithms can produce completely different-looking pictures of the same graph.

This chapter covers the main Python libraries for network visualization (NetworkX for the data structure and basic plotting, Plotly and pyvis for interactive versions) and the design principles that should guide their use. The threshold concept, unusually, is a negative one: networks are not always the right answer. The natural instinct when faced with relational data is to draw a network diagram, but beyond a few dozen nodes the result becomes a "hairball" — a visual tangle from which the reader can extract no information. Part of becoming fluent with relational data is knowing when to draw a network and when to use an alternative (heatmap, matrix, arc diagram, chord diagram).

24.2 NetworkX: The Data Structure Library

The foundational Python library for network data is NetworkX, developed at Los Alamos National Laboratory and released in 2005. NetworkX is the de facto standard for representing and analyzing graphs in Python. It provides data structures for graphs, implementations of classic graph algorithms (shortest paths, centrality, community detection, random graph generation), and basic plotting integration with matplotlib.

NetworkX is primarily an analysis library, not a visualization library. Its plotting capabilities are bolted onto the data structures and are fine for exploratory work but limited for polished output. For publication-quality or interactive graphs, you typically use NetworkX for the graph data structure and algorithms, then pass the result to a separate visualization library (Plotly, pyvis, or direct matplotlib customization).

Basic usage:

import networkx as nx

G = nx.Graph()
G.add_node("Alice")
G.add_node("Bob")
G.add_node("Carol")
G.add_edge("Alice", "Bob")
G.add_edge("Bob", "Carol")
G.add_edge("Alice", "Carol")

print(G.number_of_nodes())  # 3
print(G.number_of_edges())  # 3
print(list(G.neighbors("Alice")))  # ["Bob", "Carol"]

For directed graphs, use nx.DiGraph:

D = nx.DiGraph()
D.add_edge("Alice", "Bob")    # Alice → Bob
D.add_edge("Bob", "Alice")    # Bob → Alice
print(list(D.predecessors("Alice")))  # ["Bob"]
print(list(D.successors("Alice")))    # ["Bob"]

For weighted graphs, pass weight as an attribute:

G = nx.Graph()
G.add_edge("Alice", "Bob", weight=5)
G.add_edge("Bob", "Carol", weight=2)
G["Alice"]["Bob"]["weight"]  # 5

Nodes and edges can carry arbitrary attributes as keyword arguments to add_node and add_edge. These attributes are stored on the graph and can be used for visualization (node_color, node_size, edge_width) or further analysis.

NetworkX includes many built-in graph generators for common testbeds:

G = nx.karate_club_graph()          # Zachary's karate club (34 nodes, 78 edges)
G = nx.les_miserables_graph()       # Les Misérables character co-appearances
G = nx.florentine_families_graph()  # Medici Renaissance families
G = nx.davis_southern_women_graph() # Southern Women social network

These datasets are small enough for fast experimentation and are used extensively in network science pedagogy. We will use a few of them in examples.

24.3 Creating a Graph from a DataFrame

Real network data often comes as an edge list — a pandas DataFrame with columns for source, target, and optional attributes. NetworkX has a convenience function for this:

import pandas as pd
import networkx as nx

edges = pd.DataFrame({
    "source": ["Alice", "Bob", "Carol", "Alice", "Dave"],
    "target": ["Bob", "Carol", "Dave", "Dave", "Erin"],
    "weight": [1.0, 2.5, 0.8, 1.2, 3.0],
    "type": ["friend", "colleague", "friend", "colleague", "friend"],
})

G = nx.from_pandas_edgelist(
    edges,
    source="source",
    target="target",
    edge_attr=["weight", "type"],
)

The from_pandas_edgelist function builds an undirected graph from the edge list. Use create_using=nx.DiGraph to create a directed graph instead. The edge_attr argument tells NetworkX which columns to store as edge attributes.

For graphs with node-level attributes (say, a DataFrame of node names and properties), create the graph from the edges first, then set node attributes:

node_attrs = pd.DataFrame({
    "name": ["Alice", "Bob", "Carol", "Dave", "Erin"],
    "department": ["Engineering", "Sales", "Engineering", "Marketing", "Sales"],
    "years_at_company": [5, 3, 8, 2, 10],
})

nx.set_node_attributes(
    G,
    node_attrs.set_index("name").to_dict("index"),
)

The to_dict("index") converts each row of the DataFrame to a dict keyed by the node name, which is the format NetworkX expects.

24.4 Layout Algorithms

Once you have a graph, the question becomes: where do you draw the nodes? NetworkX provides several layout algorithms that compute 2D positions for the nodes. Each algorithm has different strengths and produces a different-looking visualization.

nx.spring_layout(G) — the most common and the default. Uses a force-directed algorithm (Fruchterman-Reingold) where nodes repel each other and edges act as springs pulling connected nodes together. The result is an organic layout that tends to put densely-connected nodes close together and sparsely-connected nodes at the periphery. Spring layouts are the default because they work reasonably well for most small-to-medium graphs, but they are also stochastic — rerunning the algorithm produces different layouts — and they can produce cluttered results for large graphs.

nx.kamada_kawai_layout(G) — another force-directed algorithm with a different cost function. Tends to produce cleaner layouts than spring for some graph types, especially graphs with clear structure. Slower than spring for large graphs.

nx.circular_layout(G) — places all nodes evenly around a circle. Useful when the identity of nodes is more important than the graph structure, or when the graph has roughly uniform structure (no meaningful clusters). Circular layouts are poor for large graphs because edges crisscross the interior.

nx.shell_layout(G, shells=[[group1], [group2], ...]) — places nodes in concentric circles (shells), each shell containing a specified subset of nodes. Useful when you have a natural grouping (core vs. periphery) or a hierarchy.

nx.spectral_layout(G) — positions nodes using the eigenvectors of the graph Laplacian. This often reveals community structure when it exists. Mathematically elegant but produces less intuitive layouts than force-directed methods.

nx.bipartite_layout(G, nodes=[set1]) — places bipartite graph nodes in two parallel lines, one for each set. Good for bipartite graphs where the two-sided structure is the point.

nx.planar_layout(G) — works only for planar graphs (graphs that can be drawn without edge crossings). Produces a minimal-crossing layout if the graph is planar.

nx.random_layout(G) — places nodes at random positions. Useful as a baseline to compare against other algorithms, rarely useful as a final visualization.

Choosing the right layout is part of the design work. For an unknown graph, start with spring_layout and see if the structure emerges. If the graph has a known structure (bipartite, hierarchical, community-based), use a layout that matches the structure (bipartite_layout, shell_layout, spectral_layout). If spring_layout produces a hairball, try kamada_kawai or reduce the graph's size.

Layout algorithms return a dict mapping node names to (x, y) tuples:

pos = nx.spring_layout(G, seed=42)
pos["Alice"]  # (0.1234, 0.5678)

Passing the same seed produces reproducible layouts. Without a seed, each run gives a different result. For publication, always set a seed so the figure is reproducible.

24.5 Drawing with NetworkX and matplotlib

The simplest way to draw a graph is with nx.draw:

import matplotlib.pyplot as plt
import networkx as nx

G = nx.karate_club_graph()
pos = nx.spring_layout(G, seed=42)

fig, ax = plt.subplots(figsize=(10, 8))
nx.draw(
    G,
    pos,
    with_labels=True,
    node_color="lightblue",
    node_size=500,
    font_size=10,
    edge_color="gray",
    ax=ax,
)
ax.set_title("Zachary's Karate Club")
plt.show()

nx.draw is the all-in-one function — it draws nodes, edges, and labels in one call. For more control, use the underlying functions separately:

fig, ax = plt.subplots(figsize=(10, 8))
nx.draw_networkx_nodes(G, pos, node_color="lightblue", node_size=500, ax=ax)
nx.draw_networkx_edges(G, pos, edge_color="gray", alpha=0.5, ax=ax)
nx.draw_networkx_labels(G, pos, font_size=10, ax=ax)

The separate functions let you customize each layer independently. This is especially useful when you want nodes colored by one attribute, edges colored by another, and labels styled separately.

Encoding node and edge attributes visually:

# Node size by degree centrality
degrees = dict(G.degree())
node_sizes = [v * 30 for v in degrees.values()]

# Node color by community (using a detection algorithm)
from networkx.algorithms.community import greedy_modularity_communities
communities = list(greedy_modularity_communities(G))
node_color = {}
for i, community in enumerate(communities):
    for node in community:
        node_color[node] = i
node_colors = [node_color[n] for n in G.nodes()]

# Edge width by weight (if applicable)
edge_widths = [G[u][v].get("weight", 1) for u, v in G.edges()]

fig, ax = plt.subplots(figsize=(12, 10))
nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_colors,
                       cmap="Set2", ax=ax)
nx.draw_networkx_edges(G, pos, width=edge_widths, edge_color="gray", alpha=0.5, ax=ax)
nx.draw_networkx_labels(G, pos, font_size=8, ax=ax)
ax.set_axis_off()
ax.set_title("Karate Club — size by degree, color by community")

This produces a network where highly-connected nodes (hubs) are visually larger and nodes belonging to the same community are the same color. These are the two most useful node encodings in network visualization, and they give the reader a way to interpret the structure beyond just "nodes and edges."

24.6 The Hairball Problem

Every network visualization practitioner has encountered the hairball. You have a graph with 500 nodes and 3000 edges. You call nx.draw(G, pos=nx.spring_layout(G)). The result is a dense tangle of gray lines and colored dots that looks like a bird's nest. You cannot identify individual nodes. You cannot trace individual edges. You cannot see any structure. The visualization is beautiful in a way (the density is striking) but conveys nothing.

This is the central challenge of network visualization at scale. Small graphs (up to about 50 nodes) draw cleanly with spring_layout. Medium graphs (50–200 nodes) can work with careful layout and styling. Above about 500 nodes, the hairball is almost unavoidable. Above a few thousand nodes, no layout algorithm produces a useful static picture.

The responses to the hairball problem fall into several categories:

1. Filter the graph. Show only the most important nodes and edges. For example:

  • Degree filtering: keep only nodes with degree above a threshold.
  • Weight filtering: keep only edges with weight above a threshold.
  • Centrality filtering: keep only high-centrality nodes.
  • Sample: randomly drop nodes or edges to reduce density.

The filtered graph is no longer the "real" graph, but the filtered version might convey structural information that the full graph hides. Filtering is lossy but often necessary.

2. Aggregate the graph. Merge similar nodes into super-nodes and draw the aggregated graph. For example:

  • Community aggregation: collapse each detected community into a single node; edges between communities become edges between super-nodes.
  • Type aggregation: if nodes have a type attribute, collapse all nodes of the same type.

The aggregated graph is smaller and often cleaner. The trade-off is that individual nodes disappear.

3. Use an alternative representation. Some visualizations work better than networks for certain relational data:

  • Adjacency matrix (heatmap): a matrix with nodes on both axes and edge presence/weight as the cell value. Matrices scale to thousands of nodes without tangling. Readers can sort the rows and columns to reveal structure.
  • Arc diagram: nodes are arranged on a line, edges are drawn as arcs above. Useful for showing connectivity patterns without node-position clutter.
  • Chord diagram: nodes around a circle, chords across the interior representing edges. Good for showing "who connects to whom" at aggregate levels.
  • Sankey diagram: flow-oriented visualization for directed weighted networks, especially for processes and pipelines.

The decision between "network diagram" and "alternative representation" depends on the question. If you want to see specific connections between specific nodes, a network is right. If you want to see aggregate connectivity patterns, a matrix or chord diagram is often better.

4. Go interactive. An interactive network lets the reader zoom, pan, click, and hover — which can make a hairball tractable even when the static version is illegible. Plotly and pyvis (Section 24.8) are the main tools for this.

24.7 Interactive Networks with Plotly

Plotly does not have a built-in network chart type, but you can build one by converting NetworkX positions into Plotly Scatter traces. The pattern is:

  1. Compute a layout with NetworkX (pos = nx.spring_layout(G)).
  2. Build an edge trace: a Scatter with all edge endpoints concatenated (with None as a separator to produce discontinuous line segments).
  3. Build a node trace: a Scatter with all node positions.
  4. Combine into a Figure.
import plotly.graph_objects as go
import networkx as nx

G = nx.karate_club_graph()
pos = nx.spring_layout(G, seed=42)

# Edge trace
edge_x, edge_y = [], []
for u, v in G.edges():
    x0, y0 = pos[u]
    x1, y1 = pos[v]
    edge_x.extend([x0, x1, None])
    edge_y.extend([y0, y1, None])

edge_trace = go.Scatter(x=edge_x, y=edge_y, mode="lines",
                        line=dict(width=0.5, color="gray"), hoverinfo="none")

# Node trace
node_x = [pos[n][0] for n in G.nodes()]
node_y = [pos[n][1] for n in G.nodes()]
node_trace = go.Scatter(x=node_x, y=node_y, mode="markers+text",
                        text=[str(n) for n in G.nodes()],
                        textposition="top center",
                        marker=dict(size=[G.degree(n) * 3 for n in G.nodes()],
                                    color=[G.degree(n) for n in G.nodes()],
                                    colorscale="viridis", showscale=True))

fig = go.Figure(data=[edge_trace, node_trace])
fig.update_layout(title="Karate Club — Interactive",
                  showlegend=False,
                  xaxis=dict(visible=False),
                  yaxis=dict(visible=False))
fig.show()

This produces an interactive network where hovering over a node shows its label, scrolling zooms, and dragging pans. The node color encodes degree centrality; the node size does too. The result is clickable and explorable in a way that a static matplotlib version is not.

The code is verbose, but the pattern is reusable. Many data science projects keep a helper function plot_network_plotly(G, pos, ...) that wraps this boilerplate, and subsequent network visualizations are one-line calls. Once you have the pattern, producing interactive networks in Plotly is straightforward.

24.8 pyvis: Quick Interactive Networks

pyvis is a Python wrapper around vis.js, a JavaScript network visualization library. Pyvis lets you build interactive networks in a few lines without writing any custom plotting code. It is the fastest path to an interactive network visualization in Python.

from pyvis.network import Network
import networkx as nx

G = nx.karate_club_graph()
net = Network(notebook=True, cdn_resources="in_line")
net.from_nx(G)
net.show("karate.html")

This produces an HTML file with the full interactive network: draggable nodes, zoomable view, physics-based animation, hover tooltips. The physics simulation runs in the browser, so the layout is dynamic — you can drag a node and the rest of the graph rearranges itself in response.

Pyvis is particularly good for exploratory work. When you are trying to understand a graph's structure, the ability to grab a node and pull it around while the rest of the graph settles into a new configuration is genuinely useful. For production output, it is less polished than Plotly or hand-tuned matplotlib, but for the exploration-to-understanding phase it is ideal.

Node and edge attributes transfer from NetworkX to pyvis automatically:

G = nx.karate_club_graph()
for node in G.nodes():
    G.nodes[node]["size"] = G.degree(node) * 3
    G.nodes[node]["title"] = f"Degree: {G.degree(node)}"

net = Network(notebook=True, cdn_resources="in_line")
net.from_nx(G)
net.show("karate_annotated.html")

The title attribute becomes the hover tooltip; the size attribute controls the node size. Other attributes (color, shape, label, group) also pass through.

24.9 Visual Encoding for Networks

A well-designed network visualization uses visual encoding to reveal structure that the raw node-edge data does not immediately show. The main channels:

Node size — typically encodes a centrality measure: degree centrality (number of connections), betweenness centrality (how often the node lies on shortest paths), eigenvector centrality (influence), or PageRank. Large nodes are "important" in the specific sense of the centrality metric used. Be explicit in the caption about which metric you are using — "size = degree centrality" is clearer than just "size by importance."

Node color — typically encodes community membership (from a community detection algorithm like Louvain, Leiden, or modularity maximization). Colors let the reader see which nodes group together without the reader having to trace individual edges. Secondary uses: encoding a node attribute like department, country, or age.

Edge width — typically encodes edge weight or frequency. Thick edges represent strong connections, thin edges represent weak ones. Useful when edges have meaningful weights; useless when they do not.

Edge color — typically encodes edge type or direction. In a heterogeneous network (collaborations, friendships, and messages all in one graph), color distinguishes the types. In directed networks, edge color sometimes encodes direction (though arrows are usually better).

Edge opacity — usually a global parameter that reduces the visual weight of edges so nodes stand out. Setting alpha=0.3 on edges is standard practice for dense graphs.

Arrows — for directed graphs, arrows indicate direction. NetworkX's draw_networkx_edges has an arrows=True option; in Plotly, arrows require custom annotation traces.

The rule of thumb for visual encoding: don't encode everything. A network chart with size + color + edge width + edge color + opacity is a visual mess. Pick one or two encodings based on what you want the reader to notice, and let the rest use uniform styling.

24.10 Sankey Diagrams for Flow Networks

A Sankey diagram is a specialized network visualization for flow data: energy flows, budget allocations, customer journeys, material flows through a supply chain. In a Sankey, nodes are arranged in columns from left to right (representing stages or categories), and edges are drawn as thick ribbons whose width encodes the flow magnitude.

Plotly supports Sankey diagrams directly:

import plotly.graph_objects as go

fig = go.Figure(data=[go.Sankey(
    node=dict(
        label=["A", "B", "C", "D", "E"],
        color=["#4C72B0"] * 5,
    ),
    link=dict(
        source=[0, 0, 1, 2, 3],
        target=[2, 3, 3, 4, 4],
        value=[8, 4, 2, 8, 4],
    ),
)])
fig.update_layout(title="Flow from A/B through C/D to E")
fig.show()

Sankey diagrams are effective when:

  • The data has a clear flow structure (stages, pipelines, hierarchies).
  • The magnitudes of flows are the main story.
  • The number of nodes is small enough that the diagram is readable (typically under 30 nodes, under 100 edges).

They are less effective for abstract social networks, undirected relationships, or anywhere there is no natural "left to right" ordering. For those, a traditional network diagram or a matrix is better.

24.11 Progressive Project: Climate Variable Correlation Network

For the climate project in this chapter, we will build a network where nodes represent climate variables (temperature, CO2, sea level, ice extent, ENSO index, solar irradiance, volcanic aerosol) and edges represent correlations above a threshold. This is a compact way to show which variables move together.

import pandas as pd
import networkx as nx

# Hypothetical climate correlations (in practice, compute from real data)
corr = climate_full.corr()

# Build graph: edges for correlation > 0.5 (absolute value)
G = nx.Graph()
for col in corr.columns:
    G.add_node(col)

threshold = 0.5
for i, col1 in enumerate(corr.columns):
    for col2 in corr.columns[i+1:]:
        r = corr.loc[col1, col2]
        if abs(r) >= threshold:
            G.add_edge(col1, col2, weight=abs(r), sign="+" if r > 0 else "-")

# Node size = degree centrality
# Edge width = correlation strength
# Edge color = sign (positive=red, negative=blue)

pos = nx.kamada_kawai_layout(G)

fig, ax = plt.subplots(figsize=(10, 8))
nx.draw_networkx_nodes(G, pos, node_size=[G.degree(n) * 200 for n in G.nodes()],
                       node_color="#4C72B0", ax=ax)
nx.draw_networkx_labels(G, pos, font_size=10, ax=ax)

pos_edges = [(u, v) for u, v, d in G.edges(data=True) if d["sign"] == "+"]
neg_edges = [(u, v) for u, v, d in G.edges(data=True) if d["sign"] == "-"]
nx.draw_networkx_edges(G, pos, edgelist=pos_edges, edge_color="red",
                       width=[G[u][v]["weight"] * 3 for u, v in pos_edges], ax=ax)
nx.draw_networkx_edges(G, pos, edgelist=neg_edges, edge_color="blue",
                       width=[G[u][v]["weight"] * 3 for u, v in neg_edges], ax=ax)
ax.set_axis_off()
ax.set_title("Climate Variable Correlation Network (|r| ≥ 0.5)")
plt.show()

The result is a compact network that shows which climate variables are strongly correlated. Temperature, CO2, and sea level form a tight triangle of strong positive correlations (they all track the anthropogenic warming signal). Solar irradiance sits more isolated (its correlations with the anthropogenic variables are weak). ENSO has strong correlations with temperature but not with the long-term trends.

This is the same information you would see in a Chapter 19 clustermap, but the network representation emphasizes the structure of the correlations rather than the full matrix. For seven variables, both work. For 50 variables, the network would probably hairball and the clustermap would be cleaner. The choice between representations depends on the number of variables and what you want to emphasize.

24.12 When to Use a Matrix Instead

The adjacency matrix is the most underrated network visualization. Instead of drawing nodes as dots and edges as lines, you draw a matrix where rows and columns both represent nodes, and each cell is colored by whether (and how strongly) the two nodes are connected.

import seaborn as sns
import numpy as np

G = nx.karate_club_graph()
adj = nx.to_numpy_array(G)
labels = list(G.nodes())

fig, ax = plt.subplots(figsize=(10, 10))
sns.heatmap(adj, xticklabels=labels, yticklabels=labels, cmap="Blues",
            cbar_kws={"label": "Connected"}, ax=ax, square=True)
ax.set_title("Karate Club — Adjacency Matrix")
plt.show()

The adjacency matrix has several advantages over a network diagram:

Scalability. A matrix can show 1000 nodes as a 1000×1000 grid — still legible, if you have enough pixels — while a network diagram of 1000 nodes is a useless hairball. For large graphs, matrices are often the only option that conveys any information.

Stability. A matrix does not depend on a layout algorithm. Two analysts looking at the same matrix will see the same image; two analysts with different seeds for a force-directed layout may produce visibly different network diagrams.

Sortability. Rows and columns can be reordered to reveal structure. If you order by community membership, nodes within a community form a block of dense connections on the diagonal. If you order by degree, the high-degree nodes cluster at one end. Different orderings reveal different patterns. (This is the same principle as Chapter 19's clustermap — reordering the correlation matrix reveals cluster structure.)

Precision. Every cell shows a specific relationship. There is no ambiguity about whether two nodes are connected — the cell either has a color or it does not. In a network diagram with many edges, individual edges are hard to trace from one endpoint to another.

Quantitative encoding. Edge weights become cell colors on a continuous scale, which is easier to read than edge thickness in a network diagram.

The downsides of adjacency matrices:

  • Global structure is less visible. In a network diagram, you can see a "star" pattern (one central node with many spokes) at a glance. In a matrix, the same pattern is a single row and column with more colored cells than others — harder to spot visually.
  • Individual paths are hard to trace. Following a chain of connections (A → B → C → D) in a matrix requires jumping between rows and columns; in a network diagram, you trace the line.
  • Matrices are unfamiliar to general audiences. Readers who have seen many network diagrams may not know how to read an adjacency matrix, and you may need to explain it.

The takeaway: adjacency matrices are the default for large or dense graphs. Network diagrams are the default for small, sparse graphs where the reader wants to see specific connections. The decision depends on size and on whether the question is about structure ("are there communities?") or about individual relationships ("is A connected to B?").

24.13 Community Detection and Visualization

One of the most valuable things you can do with a network is find communities — clusters of nodes that are densely connected to each other and sparsely connected to the rest of the graph. A community corresponds to a natural grouping: friends in a social network, collaborating scientists in a citation graph, departments in an organizational chart.

NetworkX provides several community detection algorithms:

from networkx.algorithms.community import greedy_modularity_communities, louvain_communities

# Greedy modularity — deterministic, fast, reasonable
communities1 = list(greedy_modularity_communities(G))

# Louvain — probabilistic, more flexible
communities2 = louvain_communities(G, seed=42)

Both algorithms optimize modularity, a measure of how "community-like" a partition is. High modularity means many within-community edges and few between-community edges. The algorithms differ in details — Louvain is more flexible but stochastic, greedy modularity is deterministic but can produce suboptimal partitions.

Other algorithms available in NetworkX:

  • girvan_newman — iteratively removes high-betweenness edges, producing a hierarchy of communities.
  • label_propagation_communities — fast and deterministic.
  • kernighan_lin_bisection — partitions the graph into two equally-sized groups.
  • k_clique_communities — finds overlapping communities based on k-clique percolation.

Once you have communities, you can visualize them by coloring nodes by community membership. The karate club example in Section 24.5 used this pattern. For graphs with clear community structure, the colors make the partition obvious at a glance.

Warning: community detection is not deterministic in general (especially Louvain), and different runs can produce different partitions. Small changes in the graph can also produce different community structure. Report the algorithm, the seed, and the resolution parameter (for resolution-dependent algorithms) so your results are reproducible. Do not treat the output as absolute truth — it is an algorithmic partition, not an observed ground truth.

24.14 Centrality: Which Nodes Are Important?

Centrality measures quantify how "important" each node is in a network, but the definition of importance depends on the measure. NetworkX implements the main ones:

Degree centrality (nx.degree_centrality(G)) — fraction of the other nodes a node is directly connected to. The simplest measure. A high-degree node is a "hub" with many direct connections. Use for: identifying popular or highly-connected nodes in a social network.

Betweenness centrality (nx.betweenness_centrality(G)) — fraction of shortest paths that pass through a node. A high-betweenness node lies on many paths between other nodes. Use for: identifying bottleneck nodes or brokers — the node that controls flow between otherwise-separated parts of the graph.

Closeness centrality (nx.closeness_centrality(G)) — inverse of the average shortest-path distance from a node to all others. A high-closeness node is close to everyone else on average. Use for: identifying nodes that can reach others quickly.

Eigenvector centrality (nx.eigenvector_centrality(G)) — a recursive measure where a node is important if it is connected to other important nodes. This is the basis for Google's PageRank (which has its own function, nx.pagerank(G)). Use for: identifying nodes that are influential beyond just their direct connections.

Katz centrality, harmonic centrality, load centrality, and several others exist for specialized use cases.

Different centrality measures can produce very different rankings. A node can be high in degree centrality (many direct connections) but low in betweenness centrality (not on any shortest paths); or vice versa. When you encode node size by centrality, always specify which measure you are using in the caption. "Size = degree centrality" is honest; "size = importance" is vague.

24.15 Arc Diagrams and Chord Diagrams

Beyond the standard network diagram, two specialized layouts deserve mention: arc diagrams and chord diagrams. Both trade off the flexibility of 2D layouts for a more constrained but often clearer representation.

Arc diagrams place nodes on a horizontal line and draw edges as semicircular arcs above (or below) the line. The linear ordering of the nodes matters: adjacent nodes in the ordering produce short arcs, distant nodes produce long arcs. A well-chosen ordering reveals community structure — dense arcs within a community cluster become visible.

Arc diagrams work well when:

  • The graph has a natural linear ordering (chronology, hierarchy, alphabet).
  • You want to see patterns of connectivity without the clutter of a 2D layout.
  • The number of nodes is moderate (under ~200) so the line is readable.

Building an arc diagram in Python requires manual work. Matplotlib has no built-in support, but you can draw it by placing node markers on a horizontal axis and using arc patches (matplotlib.patches.Arc) for the edges. The d3-arc-diagram and similar JavaScript tools produce more polished arc diagrams for web use.

Chord diagrams place nodes around the perimeter of a circle and draw edges as chords (straight lines or curves) across the interior. The width of each chord can encode the edge weight or frequency. Chord diagrams are especially effective for showing flows between categories — migrations between countries, transitions between states, calls between departments.

The main library for chord diagrams in Python is holoviews + bokeh, or mpl-chord-diagram for static output. Plotly does not have a built-in chord diagram type, though you can approximate one with polar coordinates and lines.

Chord diagrams work well when:

  • The edges represent flows between categories (not individual connections between entities).
  • The number of categories is small (typically under 30).
  • Symmetry and aesthetic polish matter (chord diagrams are visually striking).

They work poorly when:

  • The graph is sparse (few chords means wasted space).
  • The node ordering around the circle is arbitrary (which it often is, producing visual noise).
  • The reader needs to trace specific connections (chords overlap in the center and become hard to distinguish).

Both arc diagrams and chord diagrams are niche tools. You will not reach for them often, but when the data fits their strengths, they produce more legible output than a general network diagram would.

24.16 Bipartite Networks

A bipartite graph has two distinct sets of nodes, and edges only connect nodes from different sets. Common examples: users and items (recommendation systems), authors and papers (co-authorship), customers and products (sales), genes and diseases (biology). The two-sided structure is informative and often underused.

NetworkX has a bipartite module with dedicated algorithms:

from networkx.algorithms import bipartite

G = nx.Graph()
G.add_nodes_from(["Alice", "Bob", "Carol"], bipartite=0)  # users
G.add_nodes_from(["Book1", "Book2", "Book3"], bipartite=1)  # items
G.add_edges_from([("Alice", "Book1"), ("Alice", "Book2"),
                  ("Bob", "Book2"), ("Carol", "Book3")])

# Check that the graph is actually bipartite
print(bipartite.is_bipartite(G))  # True

# Get the two node sets
top_nodes = {n for n, d in G.nodes(data=True) if d["bipartite"] == 0}
bottom_nodes = set(G) - top_nodes

For visualization, the nx.bipartite_layout places the two sets in two parallel columns:

pos = nx.bipartite_layout(G, top_nodes)
nx.draw(G, pos, with_labels=True)

This produces a layout with users on the left and items on the right, with edges crossing between them. It is the canonical way to visualize a bipartite network, and it scales to moderate sizes (up to a few hundred nodes on each side).

Alternative visualizations for bipartite networks:

  • Heatmap: rows are one node set, columns are the other, cells are edge presence/weight. Scales to thousands of nodes on each side.
  • Sankey diagram: if the edges represent flows, a Sankey shows magnitude clearly.
  • Co-occurrence network: collapse one side of the bipartite graph into a unipartite network (items are connected if they share a user; users are connected if they share items). This reveals structural patterns but loses the bipartite framing.

Recommendation systems, academic citation analysis, and product-review data all fall naturally into the bipartite framework, and choosing between the bipartite layout, heatmap, and collapsed projection is a common design decision.

24.17 Temporal Networks and Dynamic Visualization

A static network diagram shows a single snapshot of relationships. But many real networks change over time: friendships form and dissolve, collaborations start and end, supply chains reconfigure. A temporal network adds a time dimension to the graph — each edge has a timestamp, or the graph itself evolves through a sequence of states.

There are two main strategies for visualizing temporal networks:

Animation. Show the network as a sequence of frames, each frame a snapshot of the graph at a specific time. Plotly's animation_frame (Chapter 20) can drive this if you prepare the data as a sequence of edge lists indexed by time. The challenge is maintaining a stable layout across frames — a force-directed layout recomputed at every frame produces violent jumps as nodes reposition, which is disorienting. Solutions include computing a single layout from the union graph and reusing it for all frames, or using a physics simulation that evolves smoothly.

Time-slices in small multiples. Show several static snapshots side by side, labeled by time. This is the small-multiples pattern from Chapter 8 applied to temporal networks. The advantage is that the reader can compare snapshots directly without waiting for animation. The disadvantage is that fine-grained temporal resolution is hard — you can show 4 or 6 snapshots in a row of panels, not 100.

Timeline diagrams. For networks where the temporal dimension is the story, plot nodes on a horizontal axis (e.g., birth or start date) and draw edges as arcs or lines over time. This mixes temporal and relational information in one view.

Python support for temporal networks is less mature than for static ones. NetworkX handles temporal networks through external libraries like teneto or dynetx. For visualization, custom matplotlib or Plotly code is usually required. If temporal networks are central to your work, budget time for tool selection and possibly custom development — the off-the-shelf tools are limited.

24.18 The Network Visualization Ecosystem Beyond Python

Python's network visualization tools are adequate for most purposes, but they are not the best in the world for this specific domain. If you do serious network visualization, you will likely encounter these other tools:

Gephi — a free desktop application for exploratory network analysis and visualization. Gephi has a polished interactive interface, many layout algorithms, community detection, filtering, and export to publication-ready images. For exploring an unfamiliar network, Gephi is often more productive than any Python library. You can export from NetworkX to GraphML or GEXF and load into Gephi for exploration.

Cytoscape — another free desktop application, originally built for biological networks (protein-protein interactions, gene regulation) but general-purpose. Cytoscape has a large plugin ecosystem and is standard in many biological research labs.

D3.js — the JavaScript library has extensive network visualization examples, including sophisticated force-directed layouts, sankey diagrams, arc diagrams, and chord diagrams. The code is more verbose than Python, but the visual polish is unmatched. The NYT and FiveThirtyEight's network visualizations are built in D3.

sigma.js — a JavaScript library specifically for large networks (thousands to millions of nodes). Uses WebGL rendering for performance. Best when you need to visualize a network in a web context with many nodes.

vis.js (via pyvis) — the JavaScript library behind pyvis. Good for medium-sized networks with physics-based interaction.

igraph — a C library with R and Python bindings. Faster than NetworkX for large graphs, with better community detection and statistical analysis. The Python binding (python-igraph) is worth considering for any project beyond about 10,000 nodes.

Neo4j Bloom — a commercial network visualization tool from the Neo4j graph database company. Designed for interactive exploration of large graph databases. Overkill for small projects, but impressive for enterprise graph data.

For most data-science use cases, NetworkX + Plotly is sufficient. For serious network analysis, Gephi or igraph is a worthwhile upgrade. For the best visual polish, D3 is still the gold standard — expensive in development time, but unmatched in output quality.

24.19 Check Your Understanding

Before continuing to Chapter 25 (Part VI begins), make sure you can answer:

  1. What are the main kinds of graph (undirected, directed, weighted, bipartite)?
  2. What is the role of layout algorithms, and why does the choice of layout matter?
  3. When does spring_layout produce good results, and when does it fail?
  4. What is the hairball problem, and what are the main strategies for responding to it?
  5. Name three alternative representations for relational data besides the network diagram.
  6. When is a Sankey diagram more appropriate than a general network diagram?
  7. What is NetworkX, and what is its relationship to visualization libraries?
  8. What are the two most common visual encodings in network visualization, and what do they typically represent?

If any of these are unclear, re-read the relevant section. Chapter 24 is the last chapter of Part V. Part VI begins with time series visualization in Chapter 25.

24.20 Chapter Summary

This chapter introduced network and graph visualization:

  • Graphs are collections of nodes and edges representing relational data. Flavors include undirected, directed, weighted, bipartite, and multigraph.
  • NetworkX is the de facto Python library for graph data structures and algorithms. It provides basic matplotlib plotting but is primarily an analysis library.
  • Layout algorithms compute 2D positions for nodes. Spring layout (force-directed) is the default; Kamada-Kawai, circular, shell, spectral, and bipartite layouts are alternatives for specific graph types.
  • Visual encoding typically uses node size for centrality and node color for community. Edge width encodes weight; edge color encodes type or direction.
  • The hairball problem is the central challenge: large networks produce dense visual tangles. Responses include filtering, aggregation, alternative representations, and interactivity.
  • Plotly networks require manual conversion from NetworkX positions to Scatter traces. The code is verbose but produces fully interactive results.
  • pyvis wraps vis.js for quick interactive network prototypes with physics simulation.
  • Sankey diagrams are a specialized network visualization for flow data with clear stage structure.

The chapter's threshold concept — networks are not always the right answer — argues that the natural instinct to draw a network for every relational dataset is often wrong. Above a few dozen nodes, networks become hairballs. Matrices, chord diagrams, arc diagrams, and aggregated representations may communicate better. The practitioner's job is to match the representation to the data and the question, not to default to network diagrams because they are the most dramatic-looking option.

Part V is now complete. Chapter 25 begins Part VI (Specialized Domains) with time series visualization — the specific techniques and tools for plotting data indexed by time.

24.21 Spaced Review

Questions that reach back to earlier chapters:

  • From Chapter 19 (Multi-Variable Exploration): A correlation heatmap and a correlation network both show pairwise relationships among variables. When does each representation work better?
  • From Chapter 2 (Perception): Network diagrams rely on position and proximity as Gestalt cues. How does this interact with the hairball problem?
  • From Chapter 20 (Plotly Express): Plotly does not have a built-in network function. What does this say about the maturity of Python's network visualization ecosystem?
  • From Chapter 14 (Specialized Charts): Some specialized matplotlib charts (arc, chord, Sankey) overlap with network visualization. How do you decide which is which?
  • From Chapter 5 (Choosing the Right Chart): Chart selection is question-driven. What question should you ask before drawing a network, and what should the answer lead you to?

Network visualization is a specialized skill that rewards discipline and punishes overconfidence. The hairball problem is real, and most "impressive" network diagrams on the internet communicate less than they appear to. When you have relational data, the first question is not "how do I draw this network?" but "is a network the right representation at all?" Sometimes the answer is no, and you should reach for a heatmap, an arc diagram, or an aggregated summary. When the answer is yes, the tools in this chapter (NetworkX for data, Plotly or pyvis for interactive output, matplotlib for static print) are sufficient for most real-world needs. Part VI begins next with specialized visualization for time series, text, and other specific data types.