5.4.13 · D3Scientific Computing (Python)

Worked examples — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

1,894 words9 min readBack to topic

The scenario matrix

Every example is tagged with the cell of this table it covers. By the end, every cell has a green tick.

# Case class What is unusual here Example that covers it
C1 Standard build + verify indptr the "normal" case Ex 1
C2 Empty row (all zeros in a row) indptr repeats a value Ex 2
C3 Duplicate COO entries must be summed on convert Ex 3
C4 Degenerate density (nearly dense) sparse costs more Ex 4
C5 SpMV by hand, row-slice formula verify sum Ex 5
C6 SPD system → cg symmetric positive-definite Ex 6
C7 Non-symmetric system → spsolve/gmres cg would be wrong Ex 7
C8 Singular (degenerate) matrix no unique solution Ex 8
C9 Real-world word problem: graph adjacency → reachability Ex 9
C10 Exam twist: reconstruct from raw arrays reverse the format Ex 10

Prerequisites used along the way: Dense matrices (NumPy ndarray), LU decomposition, Conjugate Gradient method, Graph adjacency matrices, Big-O complexity.


Ex 1 — The standard build (C1)

Forecast: row counts are , so indptr should be their running total starting at 0.

  1. List nonzeros row by row. Why this step? CSR's rule is "scan each row left→right", so the storage order is fixed by the rows. Row0 ; Row1 ; Row2 ; Row3 . → data = [10,20,30,40,50,60], indices = [0,1,0,2,3,1].
  2. Count per row → cumulative-sum. Why? indptr[i] must equal how many nonzeros came before row , so a running total is exactly the boundary list. Counts indptr = [0,1,2,5,6].

Verify: data[indptr[2]:indptr[3]] = data[2:5] = [30,40,50] — the three entries of row 2. ✓ Last value of indptr . ✓


Ex 2 — An empty row (C2)

Forecast: row 1 has zero nonzeros, so its "start" and the next "start" must be the same number.

  1. List nonzeros. Why? Same left→right rule. Row0 ; Row1 (nothing); Row2 . → data=[5,7,8], indices=[0,1,2].
  2. Cumulative counts. Counts . Why the 0? An empty row adds nothing, so the cumulative total does not advance. indptr = [0, 1, 1, 3].
Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers
  1. Read back row 1. data[indptr[1]:indptr[2]] = data[1:1] = []. Why empty? A zero-width slice — exactly what "no nonzeros" should return.

Verify: length of indptr with . ✓ Last value . ✓


Ex 3 — Duplicate COO entries (C3)

Forecast: COO is a scratchpad that allows duplicates; conversion should sum them, so .

  1. Write the triples. Why COO? It is the only format that tolerates the same twice — the parent note flagged this as its build-time superpower. .
  2. Convert to CSR. Why does summing happen now? CSR requires each column index to appear once per row; the constructor collapses duplicates by addition. , data=[7,9], indices=[0,1], indptr=[0,1,2].

Verify: , , . ✓


Ex 4 — Degenerate density: sparse is worse (C4)

Forecast: CSR stores numbers. With so many nonzeros this may exceed the dense .

  1. Dense cost. Why the baseline? Dense matrices (NumPy ndarray) stores every cell: numbers.
  2. CSR cost. Why this formula? One data + one indices per nonzero, plus indptr of length :

Verify: — sparse is nearly double the memory here. ✓ Confirms the parent's rule: use sparse only when .


Ex 5 — Sparse mat–vec by hand (C5)

Forecast: Row 2 is the busy one: dominates.

The parent's formula:

  1. Row 0: slice data[0:1]=[10] at cols [0]. Why the slice? indptr[0:1]=[0,1] bounds it.
  2. Row 1: data[1:2]=[20] at col [1].
  3. Row 2: data[2:5]=[30,40,50] at cols [0,2,3].
  4. Row 3: data[5:6]=[60] at col [1].

So . Why is this ? We touched each of the 6 nonzeros exactly once — never the zeros (see Big-O complexity).

Verify: dense check . ✓


Ex 6 — SPD system with Conjugate Gradient (C6)

Forecast: this matrix is symmetric and (as the tridiagonal Laplacian) positive-definite, so CG applies and should give a clean rational answer.

  1. Check SPD. Why check first? CG assumes symmetric positive-definite; using it blindly is the parent's fourth mistake. Symmetric: ✓. Positive-definite: leading minors are — all ✓.
  2. Solve. Why by hand is fine? Only ; the sparse solver would return the same. Gaussian elimination gives
Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers
  1. Why CG in practice? Why not LU? For the real version, CG only needs the product (which is ) and never forms a dense factor — no fill-in.

Verify: . ✓


Ex 7 — Non-symmetric system: cg is illegal here (C7)

Forecast: , so CG is invalid; use spsolve (sparse LU) or gmres.

  1. Reject CG. Why? but — not symmetric, so CG's core assumption fails and it may diverge.
  2. Back-substitute (it's upper-triangular). Why triangular helps? LU's whole game is to reach a triangular system, then solve cheaply. Row 2: . Row 1: .

Verify: . ✓


Ex 8 — Singular / degenerate system (C8)

Forecast: row 2 is exactly row 1, so — the matrix is singular, no unique solution.

  1. Compute the determinant. Why? A direct solver needs an invertible ; means LU hits a zero pivot. .
  2. Interpret. Why not just an error? The two equations and are the same line — infinitely many solutions (e.g. or ). spsolve warns of a singular matrix / returns nan or inf.

Verify: both and satisfy : and . ✓ (Two distinct solutions ⇒ not unique.)


Ex 9 — Real-world word problem: a friendship graph (C9)

Forecast: counts walks of length 2 from to ; the diagonal counts "there-and-back" (equal to each node's degree).

  1. Build . Why sparse? A real social graph has millions of nodes but each person has a handful of friends — the classic sparse case.
  2. Square it. Why ? Matrix-multiply chains edges: counts a friend-of-a-friend route . Use A @ A (the parent warned: @, never * for old-style ambiguity).
Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers
  1. Read it. Why the diagonal? = person 1's degree (2 friends), i.e. two "out-and-back" walks. : person 0 reaches person 2 in exactly one 2-hop route (via person 1).

Verify: , diagonal degrees. ✓


Ex 10 — Exam twist: reconstruct from raw arrays (C10)

Forecast: indptr=[0,1,1,3] has a repeated 1row 1 is empty (same trap as Ex 2).

  1. Split by row using indptr. Why? indptr[i]:indptr[i+1] is exactly row 's slice.
    • Row 0: [0:1] → value at col indices[0]=2.
    • Row 1: [1:1]empty.
    • Row 2: [1:3] → values at cols indices[1:3]=[0,1].
  2. Assemble. Why place by (row, col)? data[k] sits at row (from the slice it fell in) and column indices[k].

Verify: last indptr. ✓ Nonzero positions with values . ✓


Active recall

Recall Which solver for which matrix?
  • Symmetric positive-definite ::: Conjugate Gradient (cg).
  • Non-symmetric, exact ::: spsolve (LU) or gmres.
  • Singular () ::: no unique solution — no direct solver works.
Recall Trap checks
  • Empty row in CSR shows up as… ::: a repeated value in indptr (never a missing slot).
  • Duplicate COO triples become… ::: summed on .tocsr().
  • Sparse beats dense only when… ::: density ; near-dense it uses more memory.