Tutorials

Bipartite Graphs with NetworkX

A bipartite graph divides its nodes into two sets — call them "top" and "bottom" — where every edge connects a node from one set to a node from the other. No edges exist within a set. This structure naturally models many real-world relationships: movies and the actors in them, users and the products they've purchased, researchers and the papers they've co-authored, or students and the courses they've taken. Analyzing bipartite graphs lets you find dense connections, project onto one mode (e.g., which actors have appeared together), and apply bipartite-specific algorithms.

### Creating a Bipartite Graph

NetworkX represents bipartite graphs as ordinary graphs with a `bipartite` node attribute (0 for one set, 1 for the other). The `bipartite` submodule provides specialized tools.

import networkx as nx
from networkx.algorithms import bipartite
import matplotlib.pyplot as plt

B = nx.Graph()
# Top nodes: movies; bottom nodes: actors
movies = ["Inception", "Interstellar", "Dunkirk"]
actors = ["DiCaprio", "Caine", "Murphy", "Hardy"]

B.add_nodes_from(movies, bipartite=0)
B.add_nodes_from(actors, bipartite=1)
B.add_edges_from([
    ("Inception", "DiCaprio"), ("Inception", "Caine"),
    ("Interstellar", "Caine"), ("Interstellar", "Murphy"),
    ("Dunkirk", "Hardy"), ("Dunkirk", "Murphy"),
    ("Dunkirk", "Caine"),
])

print("Is bipartite:", nx.is_bipartite(B))
print("Nodes:", dict(B.nodes(data="bipartite")))
Is bipartite: True
Nodes: {'Inception': 0, 'Interstellar': 0, 'Dunkirk': 0, 'DiCaprio': 1, 'Caine': 1, 'Murphy': 1, 'Hardy': 1}
- `add_nodes_from(movies, bipartite=0)` attaches the `bipartite` attribute to each node, marking which partition it belongs to.
- `nx.is_bipartite(B)` verifies the structure — useful when you receive a graph from an external source.
- Edges only run between movies (0) and actors (1); no actor-actor or movie-movie edges exist.

### Visualizing a Bipartite Graph

Bipartite graphs look clearest when the two node sets are drawn on separate horizontal lines.

import networkx as nx
from networkx.algorithms import bipartite
import matplotlib.pyplot as plt

B = nx.Graph()
movies = ["Inception", "Interstellar", "Dunkirk"]
actors = ["DiCaprio", "Caine", "Murphy", "Hardy"]
B.add_nodes_from(movies, bipartite=0)
B.add_nodes_from(actors, bipartite=1)
B.add_edges_from([
    ("Inception", "DiCaprio"), ("Inception", "Caine"),
    ("Interstellar", "Caine"), ("Interstellar", "Murphy"),
    ("Dunkirk", "Hardy"), ("Dunkirk", "Murphy"), ("Dunkirk", "Caine"),
])

pos = {}
for i, m in enumerate(movies):
    pos[m] = (i * 2, 1)
for i, a in enumerate(actors):
    pos[a] = (i * 1.5, 0)

plt.figure(figsize=(8, 4))
nx.draw(B, pos, with_labels=True, node_color=
        ["steelblue" if B.nodes[n]["bipartite"] == 0 else "crimson"
         for n in B.nodes()],
        node_size=1200, font_size=9, edge_color="gray")
plt.title("Movie–Actor Bipartite Graph")
plt.tight_layout()
plt.show()
/var/folders/8l/xyy39wqj1c71tdcxq7vndwvr0000gn/T/ipykernel_68552/2750782488.py:28: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
  plt.tight_layout()
- Manually assigning y=1 to movies and y=0 to actors creates the two-row layout — this is the standard visual convention for bipartite graphs.
- Blue nodes (movies) and red nodes (actors) make the partition immediately clear.
- Spreading nodes evenly along x avoids overlap while keeping edges readable.

### Bipartite Projection

A bipartite projection collapses the graph onto one set of nodes by connecting two nodes if they share a common neighbor in the other set. Projecting the actor set creates an actor-actor network where edges mean "appeared in the same movie".

import networkx as nx
from networkx.algorithms import bipartite
import matplotlib.pyplot as plt

B = nx.Graph()
movies = ["Inception", "Interstellar", "Dunkirk"]
actors = ["DiCaprio", "Caine", "Murphy", "Hardy"]
B.add_nodes_from(movies, bipartite=0)
B.add_nodes_from(actors, bipartite=1)
B.add_edges_from([
    ("Inception", "DiCaprio"), ("Inception", "Caine"),
    ("Interstellar", "Caine"), ("Interstellar", "Murphy"),
    ("Dunkirk", "Hardy"), ("Dunkirk", "Murphy"), ("Dunkirk", "Caine"),
])

# Weighted projection: edge weight = number of shared movies
actor_graph = bipartite.weighted_projected_graph(B, actors)

print("Actor co-appearance network:")
for u, v, w in actor_graph.edges(data="weight"):
    print(f"  {u} — {v}: {w} shared movie(s)")

pos = nx.spring_layout(actor_graph, seed=1)
widths = [actor_graph[u][v]["weight"] * 2 for u, v in actor_graph.edges()]
plt.figure(figsize=(6, 5))
nx.draw(actor_graph, pos, with_labels=True, node_color="crimson",
        node_size=1200, font_size=9, edge_color="gray", width=widths)
plt.title("Actor Co-appearance Network (edge width = shared movies)")
plt.tight_layout()
plt.show()
Actor co-appearance network:
  DiCaprio — Caine: 1 shared movie(s)
  Caine — Hardy: 1 shared movie(s)
  Caine — Murphy: 2 shared movie(s)
  Murphy — Hardy: 1 shared movie(s)
/var/folders/8l/xyy39wqj1c71tdcxq7vndwvr0000gn/T/ipykernel_68552/604697218.py:29: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
  plt.tight_layout()
- `bipartite.weighted_projected_graph(B, actors)` creates a graph on just the actor nodes; the `weight` attribute counts how many movies each pair shared.
- Edge widths scaled by `weight * 2` make stronger collaborations visually thicker.
- `bipartite.projected_graph(B, actors)` gives an unweighted version — useful when you only care about whether a connection exists.

### Bipartite Statistics

The `bipartite` module provides degree and centrality functions designed for two-mode networks, where degree is measured only within the relevant partition.

import networkx as nx
from networkx.algorithms import bipartite

B = nx.Graph()
movies = ["Inception", "Interstellar", "Dunkirk"]
actors = ["DiCaprio", "Caine", "Murphy", "Hardy"]
B.add_nodes_from(movies, bipartite=0)
B.add_nodes_from(actors, bipartite=1)
B.add_edges_from([
    ("Inception", "DiCaprio"), ("Inception", "Caine"),
    ("Interstellar", "Caine"), ("Interstellar", "Murphy"),
    ("Dunkirk", "Hardy"), ("Dunkirk", "Murphy"), ("Dunkirk", "Caine"),
])

# Degree in bipartite sense: count edges crossing the partition
deg = bipartite.degree_centrality(B, actors)
print("Actor degree centrality:")
for node in sorted(actors, key=lambda a: deg[a], reverse=True):
    print(f"  {node}: {deg[node]:.3f}")

print("\nMovie degree centrality:")
deg_movies = bipartite.degree_centrality(B, movies)
for node in sorted(movies, key=lambda m: deg_movies[m], reverse=True):
    print(f"  {node}: {deg_movies[m]:.3f}")
Actor degree centrality:
  Caine: 1.000
  Murphy: 0.667
  DiCaprio: 0.333
  Hardy: 0.333

Movie degree centrality:
  Dunkirk: 0.750
  Inception: 0.750
  Interstellar: 0.750
- `bipartite.degree_centrality(B, nodes)` normalizes by the size of the *opposite* partition — the maximum possible number of connections — unlike standard degree centrality which uses n−1.
- Michael Caine appears in all three films, so he has the highest actor degree centrality (1.0).
- Dunkirk has the most actors (3 out of 4), so it has the highest movie degree centrality.

For a deeper look at the projected actor-actor or movie-movie networks, apply [centrality measures](/tutorials/centrality-measures-with-networkx) or [community detection](/tutorials/community-detection-with-networkx) to the projected graph.