DSCI 512 Lecture 3 : Graphs and Sparse Matrices

Sky (Kehan) Sheng

Housekeeping: labs

  • “Single Attempt” on a lab question means one attempt per variant. You get unlimited attempts per question.

    PrairieLearn question footer showing Save & Grade (Single attempt) and the message: Additional attempts available with new variants

  • New: the Jupyter scratchpad has starter code for every coding question. Play with the data and test your code there before you submit on PL.

  • Week 1 practice problems are on PL. Keep practising as you study.

Housekeeping: live coding quizzes

  • The goal is a safe space to make mistakes and practise before a real technical interview.
  • Go to your own lab session. Bring your UBC ID, or show your photo on Canvas or Workday.
  • Each quiz covers the previous week. This week: Week 1, binary and linear search.
  • Grade by attempts: the first 5 attempts get full marks, the next 5 get 4, the next 5 get 3, then 2, then 1.
  • Retakes: up to two extra tries, closed-book, new questions but on the same concepts. During any TA or instructor office hours, before November 6.

Where we stopped last time

The rest of lecture 2 shrinks Instagram’s user table about 10× by picking the right dtypes. Read it on your own: lecture 2 slides.

How does Instagram know who you might know?

You open Instagram. A card says Suggested for you: nori.

You have never met nori, but the card says Followed by clam, kelp and mochi.

Out of 3 billion users, how did Instagram pick nori? 🤔

Draw it

Users and lines between them: this is called a graph.

Graph, defined

A graph \(G = (V, E)\) has

  • \(V\), a set of vertices (also called nodes): the users.
  • \(E\), a set of edges: the friendships. Each edge \((u, v)\) connects two vertices \(u, v \in V\).

Many kinds of data are graphs:

data vertices edges
social network people who is friends with whom
the internet web pages which page links to which
transportation cities direct flights
MDS courses courses which course you need first

Build it in networkx

One at a time: G.add_node("ada"), G.add_edge("sky", "otter"). Many at once: add_nodes_from, add_edges_from.

Neighbours and degree

  • \(u\) and \(v\) are neighbours if there is an edge between them.
  • The degree of a vertex is the number of edges touching it.

Paths and distance

  • A path is a sequence of vertices joined by edges. Its length is the number of edges.
  • The shortest one is the distance: “degrees of separation”.
  • A cycle is a path that starts and ends at the same vertex: sky → clam → kelp → sky.

Shortest path in code

How does networkx find it? Breadth-first search, next lecture.

Connected or not?

  • Connected: there is a path between every pair of vertices.
  • Our graph is disconnected: no path from sky to waffle. Instagram can’t suggest waffle through friends.
  • Each separate group is a connected component.

Connected components in code

iClicker: how many edges?

A group chat of \(V\) users where everyone follows everyone. This is a complete graph: every pair of vertices has an edge.

How many edges does it have?

  • (A) \(V - 1\)
  • (B) \(V\)
  • (C) \(\frac{V(V-1)}{2}\)
  • (D) \(V^2\)
  • Answer: C
  • Each user has degree \(V - 1\). Adding up all \(V\) degrees counts every edge twice, once from each end.

Check it

About half of \(V^2\): a complete graph has \(O(V^2)\) edges. Keep this in mind for memory.

Graph types

Instagram followers are one-way

  • Undirected: an edge goes both ways. Facebook friends: if Alice is friends with Bob, Bob is friends with Alice.
  • Directed: an edge has a direction, drawn as an arrow. Instagram followers: You can follow natgeo without natgeo following you.

Degree splits in two:

  • in-degree: arrows in, followers
  • out-degree: arrows out, following

Directed graphs in code

Weighted graphs

Weighted: every edge carries a number. Unweighted: an edge either exists or it doesn’t.

Undirected: one number per pair. Weight = number of mutual friends, 12 mutual friends between sky and otter.

Directed: one number per direction. Weight = likes given: sky liked otter 40 times, otter liked sky only 3 times.

Weighted graphs in code

Question: which kind of graph?

A soccer league: two teams are connected if they played each other, labelled with how many games they played.

  • (A) undirected, unweighted
  • (B) undirected, weighted
  • (C) directed, unweighted
  • (D) directed, weighted
  • Answer: B. A game involves both teams, so there is no direction, and the number of games is the weight.

How do we store a graph?

Two standard ways: an adjacency list and an adjacency matrix. The list is a dict, so first a short detour.

A plain dict and a missing key

Who liked your posts today? Count the likes per user.

likes = ["otter", "clam", "otter", "kelp", "otter"]

counts = {}
for user in likes:
    counts[user] += 1     # "otter" is not a key yet

You have to check every key yourself:

defaultdict does the check for you

defaultdict(f) calls f() to make the value for a key it has not seen yet.

int to count, list to group, set to collect unique items.

Two things to watch

⚠️ Just looking at a missing key adds it:

Why defaultdict(list) and not defaultdict(list())?

  • defaultdict needs a function it can call for every missing key. list is that function.
  • list() is one empty list that already exists. It raises a TypeError.

Counter is made for counting

How many r’s are in “strawberry”? One line.

you want to… use
look things up, every key known up front dict
group items under a key defaultdict(list) or defaultdict(set)
count things Counter or defaultdict(int)

Adjacency list

For every user, keep a list of their friends: a dict of lists.

Directed graph? Drop the second append: an edge from u to v only goes in u’s list.

Adjacency list with networkx

Edge lists often come as a table. nx.from_pandas_edgelist turns one into a graph:

iClicker: space of an adjacency list

What is the space complexity of an adjacency list, with \(V\) vertices and \(E\) edges?

  • (A) \(O(V)\)
  • (B) \(O(E)\)
  • (C) \(O(VE)\)
  • (D) \(O(V + E)\)
  • Answer: D
  • One key per vertex: \(O(V)\). Each edge appears once, or twice if undirected: \(O(E)\).
  • Not \(O(E)\): a graph can have vertices and no edges. Not \(O(V^2)\): most graphs are far from complete.

Adjacency matrix

Number the vertices \(1, \dots, V\). The adjacency matrix \(A\) is \(V \times V\):

\[a_{ij} = \begin{cases} 1, & \text{if there is an edge from } i \text{ to } j \\ 0, & \text{otherwise} \end{cases}\]

Same graph, two ways

Reading the matrix

  • Row sum = degree.
  • Undirected, so \(a_{ij} = a_{ji}\): symmetric. A directed graph’s matrix usually isn’t.
  • Is there an edge? One lookup, A[i, j]: \(O(1)\).

Question: space of an adjacency matrix

  • (A) \(O(VE)\)
  • (B) \(O(E^2)\)
  • (C) \(O(V^2)\)
  • (D) \(O(V + E)\)
  • Answer: C. One number per pair of vertices, edge or not.

Directed: read it off the matrix

The follow graph from earlier, as a matrix:

row follows column sky otter clam kelp natgeo
sky 0 1 0 0 1
otter 1 0 0 0 1
clam 0 0 0 0 1
kelp 1 0 0 0 0
natgeo 0 0 0 0 0
  • Who follows whom? Draw it on paper.
  • Is it symmetric? No: sky → natgeo is a 1, natgeo → sky is a 0.
  • Only sky and otter have a 1 both ways: they follow each other.

Which one to use?

adjacency list adjacency matrix
space \(O(V + E)\) \(O(V^2)\)
is there an edge from \(u\) to \(v\)? \(O(\text{degree of } u)\), \(O(V)\) worst case \(O(1)\): A[u, v]
all neighbours of \(u\) \(O(\text{degree of } u)\) \(O(V)\): scan the row
good for big graphs with few edges small or dense graphs

A trick from lecture 2: store neighbours in a set, and the edge check is \(O(1)\) on average. networkx does this: a graph is a dict of dicts.

Can we have both?

  • A sparse matrix gets close to both. networkx already uses one:

Sparse matrices

Store only the friendships, not the 3 billion “no”s.

“Sparse matrix” means two things

  • In maths: a matrix that is mostly zeros.
  • In code: a data structure that stores only the non-zero entries.
  • Use the data structure when your matrix is mostly zeros.

Our friend graph, stored sparse

How much memory does it save?

A random graph with 100,000 users, where each possible friendship exists with probability 0.0001:

G_big = nx.fast_gnp_random_graph(100_000, 1e-4, seed=512)
am_big = nx.adjacency_matrix(G_big)

V = G_big.number_of_nodes()
E = G_big.number_of_edges()
print(f"V = {V:,} users, E = {E:,} friendships, nnz = {am_big.nnz:,}")

dense_bytes = V**2 * 8                                                    # every entry, 8 bytes each
sparse_bytes = am_big.data.nbytes + am_big.indices.nbytes + am_big.indptr.nbytes   # only what is stored
print(f"dense:  {dense_bytes / 1e9:,.1f} GB")
print(f"sparse: {sparse_bytes / 1e6:,.1f} MB")
V = 100,000 users, E = 499,519 friendships, nnz = 999,038
dense:  80.0 GB
sparse: 16.8 MB

The formula

At 8 bytes per number, for an undirected graph:

what is stored bytes space
dense matrix all \(V^2\) entries \(8V^2\) \(O(V^2)\)
sparse matrix (CSR) data + indices + indptr \(8 \times (\text{nnz} + \text{nnz} + V + 1)\) \(O(V + E)\)

with \(\text{nnz} = 2E\).

Same space complexity as an adjacency list, and that is no coincidence.

The three arrays of CSR

CSR = Compressed Sparse Row. Walk the matrix row by row and keep only the non-zeros, in three arrays.

  • data: the non-zero values, row 0 first. nnz of them.
  • indices: the column of each value in data. nnz of them.
  • indptr: how many non-zeros come before each row, which is also where the row starts in data. \(V + 1\) of them.

indptr starts at 0, ends at nnz, and row \(i\) is data[indptr[i]:indptr[i+1]].

How CSR stores a matrix

Click through the rows and watch the three arrays fill in.

Today’s cheat sheet

Storing a graph with \(V\) vertices and \(E\) edges:

representation space edge from \(u\) to \(v\)? neighbours of \(u\) use it when
adjacency list (dict of lists) \(O(V + E)\) \(O(\text{degree of } u)\) \(O(\text{degree of } u)\) the graph is big, has few edges, or changes often
adjacency matrix (NumPy) \(O(V^2)\) \(O(1)\) \(O(V)\) the graph is small, or most pairs are connected
sparse matrix (CSR) \(O(V + E)\) \(O(\text{degree of } u)\) \(O(\text{degree of } u)\) the graph is big and you want matrix maths

Today’s takeaways

  • A graph is vertices plus edges. Directed or not, weighted or not.
  • networkx: G.neighbors(v), G.degree(v), nx.shortest_path_length(G, u, v), nx.connected_components(G).
  • defaultdict fills in missing keys, Counter counts a whole list in one step.
  • Adjacency list: \(O(V + E)\). Adjacency matrix: \(O(V^2)\). Real graphs have far fewer than \(V^2\) edges.
  • A sparse matrix stores only the non-zeros. CSR is fast at rows, CSC at columns. Never .toarray() a big one.

Questions?