Worked examples — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers
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.
- 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]. - 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.
- List nonzeros. Why? Same left→right rule.
Row0 ; Row1 (nothing); Row2 .
→
data=[5,7,8],indices=[0,1,2]. - Cumulative counts. Counts . Why the 0? An empty row adds nothing, so the cumulative total does not advance.
indptr = [0, 1, 1, 3].

- 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 .
- 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. .
- 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 .
- Dense cost. Why the baseline? Dense matrices (NumPy ndarray) stores every cell: numbers.
- CSR cost. Why this formula? One
data+ oneindicesper nonzero, plusindptrof 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:
- Row 0: slice
data[0:1]=[10]at cols[0]→ . Why the slice?indptr[0:1]=[0,1]bounds it. - Row 1:
data[1:2]=[20]at col[1]→ . - Row 2:
data[2:5]=[30,40,50]at cols[0,2,3]→ . - 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.
- 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 ✓.
- Solve. Why by hand is fine? Only ; the sparse solver would return the same. Gaussian elimination gives

- 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.
- Reject CG. Why? but — not symmetric, so CG's core assumption fails and it may diverge.
- 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.
- Compute the determinant. Why? A direct solver needs an invertible ; means LU hits a zero pivot. .
- Interpret. Why not just an error? The two equations and are the same line — infinitely many solutions (e.g. or ).
spsolvewarns of a singular matrix / returnsnanorinf.
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).
- Build . Why sparse? A real social graph has millions of nodes but each person has a handful of friends — the classic sparse case.
- 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).

- 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 1 → row 1 is empty (same trap as Ex 2).
- Split by row using
indptr. Why?indptr[i]:indptr[i+1]is exactly row 's slice.- Row 0:
[0:1]→ value at colindices[0]=2. - Row 1:
[1:1]→ empty. - Row 2:
[1:3]→ values at colsindices[1:3]=[0,1].
- Row 0:
- Assemble. Why place by (row, col)?
data[k]sits at row (from the slice it fell in) and columnindices[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) orgmres. - 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.