5.4.13 · D5Scientific Computing (Python)
Question bank — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers
Before we start, a quick vocabulary anchor so nothing is used undefined:
True or false — justify
Sparse storage is always smaller than dense.
False. Each nonzero costs one value plus one integer index (the
indices entry), i.e. extra int per nonzero, so once density climbs past the crossover ( for equal-width value/index types, per the cost model above) the index overhead can make the sparse copy larger than the plain dense array.CSR and CSC store the same matrix using the same three-array scheme, just transposed in meaning.
True. Both use
data, indices, indptr; CSR reads row-by-row with indices = column numbers, CSC reads column-by-column with indices = row numbers. It is one idea applied along the other axis.indptr always has length for an -row CSR matrix, regardless of nnz.
True.
indptr holds one start-offset per row plus a final sentinel equal to nnz; its length depends only on the number of rows (or columns for CSC), never on how many nonzeros exist.Converting COO to CSR can change nnz.
True (usefully). COO allows duplicate
(i,j) triples; conversion sums duplicates into a single entry, so several COO triples can collapse to one CSR nonzero, lowering nnz.For an SPD matrix, Conjugate Gradient and spsolve return mathematically identical answers.
Mostly true in exact arithmetic, but how differs:
spsolve factors (direct, one exact solve) while CG iterates toward the answer and stops at a tolerance, so its result is only approximate to that tolerance.A tridiagonal matrix stored dense wastes memory.
True. A tridiagonal matrix has nonzeros but dense slots; for large that is a huge waste, which is exactly why 1D PDE discretizations are stored sparse.
A @ x and A * x do the same thing for a scipy.sparse matrix in recent SciPy.
For the legacy
spmatrix classes * historically meant matrix-multiply, which broke NumPy's elementwise convention; for the newer sparray arrays (SciPy ) * is elementwise and @ is the only guaranteed matrix-multiply. Because the meaning of * flips between the two class families, always write @ for matmul and .multiply() for elementwise.Iterative solvers like CG need the matrix factorized first.
False. Krylov methods such as CG only need the action (an SpMV) — never an explicit factorization — which is precisely why they dodge fill-in and stay low-memory.
Spot the error
"I'll insert new entries with A[i, j] = v after converting to CSR — it's just indexing."
Wrong: CSR keeps each row's nonzeros in one contiguous slice of
data/indices, so inserting a new nonzero means opening a gap — every entry after position must shift up by one and every later indptr offset must increase by one, an rewrite that also triggers SparseEfficiencyWarning. Build in COO/LIL/DOK (which allow cheap insertion), then .tocsr() once."CG diverged on my matrix, so scipy is buggy."
Not a bug: CG's update directions are only guaranteed to reduce the error when is SPD, because the algorithm minimizes the energy , which needs positive-definite to have a minimum. On a non-symmetric or indefinite matrix that quadratic has no minimum, so it can stagnate or diverge; use
gmres or bicgstab for the general case."I passed CSR to spsolve and it still worked, so CSC is pointless."
It works but silently transposes/converts to CSC first because sparse LU eliminates variables column-by-column and therefore needs column access; that hidden conversion copies the whole matrix, so passing CSC directly saves both the copy time and the extra memory.
"My matrix has 40% nonzeros, so sparse will speed things up."
At you are far from sparse territory: with numbers stored plus index-driven indirect memory access (each column lookup jumps around ), sparse is usually slower than a contiguous dense ndarray. Sparse pays off only when is genuinely small (the practical rule , well below the storage break-even).
"SpMV is because it's a matrix times a vector."
Wrong cost model: a dense product touches all entries, but CSR loops only over the stored nonzeros, and each nonzero contributes exactly one multiply-add to — so the total work is one operation per nonzero, giving .
"I built my graph's adjacency matrix in CSR by appending edges one at a time."
Each append into CSR is the expensive data-shift described above, so building edges costs ; instead accumulate edges as COO triples ( per append, duplicates auto-summed for multi-edges) and pay a single sort-and-compress at
.tocsr()."Cholesky is fine here — my matrix is symmetric."
Symmetry alone is insufficient: Cholesky computes , and the diagonal of involves square roots of pivots that are only guaranteed positive when is positive-definite. A symmetric-but-indefinite matrix hits a non-positive pivot, so no real Cholesky factor exists and the routine fails.
Why questions
Why does CSR store indptr instead of a per-nonzero row array like COO?
Because within a row the row-number is constant, so COO's
row array is redundant; indptr stores only where each row starts (length ), compressing that repetition — the "Compressed" in CSR.Why do direct solvers prefer CSC over CSR?
LU and Cholesky factorizations eliminate variables column-by-column, so column access must be fast; CSC gives column slices, whereas CSR would need a costly transpose.
Why do iterative solvers often need preconditioning?
Krylov convergence depends on the matrix's conditioning; a preconditioner (cheap to invert) transforms the system so the solver converges in far fewer SpMVs.
Why is CSR the natural format for computing ?
SpMV sums each row's nonzeros against ; CSR already groups nonzeros by row and stores them contiguously, so the loop is cache-friendly and touches each nonzero exactly once.
Why does fill-in matter for choosing between direct and iterative solvers?
Direct factorization can turn many stored zeros into nonzeros, blowing up memory to well beyond the original nnz; iterative solvers never factorize, so they sidestep fill-in entirely — a key trade-off.
Why does building in COO and converting beat building directly in CSR?
COO insertion is per triple with duplicates summed later, while CSR insertion is per new entry; you pay the sort/compress cost once at conversion instead of on every insert.
Edge cases
What does CSR look like for a matrix that is all zeros?
data and indices are empty, and indptr is all zeros of length — every row's start equals its end, so every slice is empty. Valid and consistent.What does an identity matrix cost in CSR versus dense?
CSR stores nonzeros plus an -length
indptr ( numbers) versus dense — a large win, and its density as grows.If two COO triples give the same (i, j) but opposite values, what happens on conversion?
They are summed, so
(i,j, +5) and (i,j, -5) collapse to a stored 0; unless explicitly pruned this can leave an explicit zero — a stored entry whose value is zero — inflating nnz.Can spsolve handle a singular sparse matrix?
No exact solution exists when is singular; the factorization hits a zero pivot and either errors or returns garbage/
nan. Detect near-singularity via conditioning, not by trusting the output.For a or empty () sparse matrix, is CSR still well-defined?
Yes — the format is dimension-agnostic: a matrix has
indptr = [0] (length ) and empty data/indices, so no special-casing is needed at the boundary.What density makes sparse and dense storage roughly break even?
Near when a value and an index occupy equal bytes (each nonzero then costs a dense slot), shifting toward when indices are half-width (
int32 vs float64); below that sparse saves bytes, above it dense wins. The stricter practical rule also targets speed, not just storage.Is a non-square matrix allowed in CSR/CSC, and which dimension sets indptr's length?
Yes — for an matrix, CSR's
indptr has length (rows) and CSC's has length (columns); such rectangular systems arise in least-squares solvers like lsqr/lsmr.Recall One-line summary to lock in
Sparse pays off only when is small; build in COO, use CSR for $O(\text{nnz})$ SpMV, use CSC for direct solves, match the iterative solver to the matrix's structure (SPD → CG, else GMRES/BiCGStab), and remember fill-in is the price of a direct factorization.