SciPy Cheatsheet
Sparse Matrices
Use this SciPy reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Core Import
import scipy.sparse as sp import numpy as np # Modern API (SciPy 1.8+): prefer *array* over *matrix* from scipy.sparse import ( csr_array, csc_array, coo_array, lil_array, dok_array, bsr_array, dia_array, eye, identity, diags, block_diag, block_array, spdiags, triu, tril, hstack, vstack, kron, issparse, isspmatrix ) from scipy.sparse.linalg import ( spsolve, spsolve_triangular, eigs, eigsh, svds, cg, bicg, bicgstab, cgs, gmres, lgmres, minres, qmr, gcrotmk, splu, spilu, factorized, LinearOperator, aslinearoperator, norm )
csr_arrayvscsr_matrix: From SciPy 1.8, array classes are preferred. They follow ndarray semantics (no implicit broadcasting of*as matrix multiply — use@instead). Oldcsr_matrixstill works but is effectively in maintenance mode.
Sparse Formats
| Format | Full Name | Best For | Notes |
|---|---|---|---|
csr_array | Compressed Sparse Row | Row slicing, matrix-vector multiply | Most common; fast A @ v |
csc_array | Compressed Sparse Column | Column slicing, direct solvers | Fast column ops; preferred by UMFPACK |
coo_array | Coordinate | Building / converting | Duplicate entries allowed (summed) |
lil_array | List of Lists | Incremental construction | Slow arithmetic; use only to build |
dok_array | Dictionary of Keys | Random element access/modification | Like a sparse dict |
bsr_array | Block Sparse Row | Block-structured matrices (FEM) | |
dia_array | Diagonal | Diagonal/banded matrices | Very memory efficient |
Construction
From Dense Array
import numpy as np from scipy.sparse import csr_array A = np.array([[1, 0, 2], [0, 3, 0], [4, 0, 5]]) sparse_A = csr_array(A) print(sparse_A.shape) # (3, 3) print(sparse_A.nnz) # 5 (number of stored elements) print(sparse_A.dtype) # dtype of stored values
From COO (Most Flexible)
from scipy.sparse import coo_array row = np.array([0, 0, 1, 2, 2]) col = np.array([0, 2, 1, 0, 2]) data = np.array([1, 2, 3, 4, 5], dtype=float) A = coo_array((data, (row, col)), shape=(3, 3)) A_csr = A.tocsr()
From CSR Directly
from scipy.sparse import csr_array # CSR format: data, (row indices sorted, column indices), pointers into arrays # Easier: build from COO then convert # Or using indptr, indices, data indptr = np.array([0, 2, 3, 5]) # row i has cols in indices[indptr[i]:indptr[i+1]] indices = np.array([0, 2, 1, 0, 2]) # column indices data = np.array([1, 2, 3, 4, 5], dtype=float) A = csr_array((data, indices, indptr), shape=(3, 3))
Incremental Construction
from scipy.sparse import lil_array, dok_array # lil_array: fastest for row-wise construction A = lil_array((1000, 1000)) A[0, 5] = 1.0 A[10, 200] = 3.0 A[500:510, 0] = np.arange(10) # slice assignment A_csr = A.tocsr() # convert before arithmetic # dok_array: fast for random access A = dok_array((1000, 1000)) A[5, 10] = 2.0 A[(5, 15)] = 3.0
Special Matrices
from scipy.sparse import eye_array, diags_array, block_diag, kron # Identity (sparse ARRAY constructors, SciPy 1.12+) I = eye_array(5, dtype=float, format='csr') # Diagonal d = diags_array([1.0, 2.0, 3.0, 4.0, 5.0]) # main diagonal d = diags_array([[4., 5.], [1., 2., 3.], [6., 7.]], offsets=[-1, 0, 1]) # tridiagonal 3x3 # Block diagonal from scipy.sparse import block_diag as sp_block_diag A = sp_block_diag([A1, A2, A3], format='csr') # Kronecker product K = kron(A1, A2, format='csr')
Legacy:
eye,diags, andspdiagsreturn sparse matrix objects. On SciPy 1.12+ useeye_array/diags_array, which return sparse arrays and match the rest of the array API.
Format Conversion
csr = A.tocsr() csc = A.tocsc() coo = A.tocoo() lil = A.tolil() dok = A.todok() bsr = A.tobsr() dia = A.todia() # To/from dense dense = A.toarray() # → numpy ndarray sparse = csr_array(dense_array) # Check format print(A.format) # 'csr', 'csc', 'coo', etc.
Array Properties & Access
A.shape # (m, n) A.nnz # number of stored elements A.ndim # 2 A.dtype A.data # stored values A.indices # column indices (CSR) or row indices (CSC) A.indptr # row pointers (CSR) or column pointers (CSC) # Element access (slow for CSR/CSC on random access) val = A[1, 2] row_slice = A[0, :] # returns sparse array col_slice = A[:, 1] # Nonzero indices rows, cols = A.nonzero() # Density density = A.nnz / (A.shape[0] * A.shape[1])
Arithmetic Operations
# Matrix-vector multiply (key operation) x = np.random.rand(A.shape[1]) y = A @ x # fast sparse matvec y = A.dot(x) # equivalent # Matrix-matrix multiply C = A @ B C = A.dot(B) # Element-wise C = A + B C = A - B C = A.multiply(B) # element-wise multiply (Hadamard) C = A.power(2) # element-wise square # Scalar C = 2.0 * A C = A / 3.0 # Transpose At = A.T At = A.transpose() # Conjugate transpose Ah = A.conj().T # Diagonal d = A.diagonal() d = A.diagonal(k=1) # superdiagonal
Stacking & Combining
from scipy.sparse import hstack, vstack, block_array C = hstack([A, B]) # horizontal concatenation C = hstack([A, B], format='csr') C = vstack([A, B]) # vertical concatenation C = vstack([A, B, C], format='csc') # block_array (SciPy 1.11+, ndarray-style) C = block_array([[A, B], [None, D]], format='csr') # None → zero block of appropriate size
Sparse Solvers
Direct Solver
from scipy.sparse.linalg import spsolve, spsolve_triangular, splu, factorized A = csr_array(...) # must be square b = np.random.rand(n) # One-shot solve x = spsolve(A, b) # uses UMFPACK/SuperLU x = spsolve(A, B) # B is dense matrix → multiple RHS # Triangular solve x = spsolve_triangular(A, b, lower=True) # Factorize once, solve many times (LU) lu = splu(A.tocsc()) # SuperLU factorization (needs CSC) x1 = lu.solve(b1) x2 = lu.solve(b2) x_T = lu.solve(b, trans='T') # solve A.T @ x = b # factorized: returns a callable solve = factorized(A.tocsc()) x = solve(b) # ILU preconditioner ilu = spilu(A.tocsc(), drop_tol=1e-4, fill_factor=10) M = sp.linalg.LinearOperator(A.shape, ilu.solve) # use as preconditioner
Iterative Solvers
from scipy.sparse.linalg import cg, bicgstab, gmres, minres x0 = np.zeros(n) # Conjugate Gradient (symmetric positive definite) x, info = cg(A, b, x0=x0, tol=1e-8, maxiter=1000, M=M_precond) # BiCGSTAB (general, non-symmetric) x, info = bicgstab(A, b, tol=1e-8, maxiter=1000) # GMRES (general, non-symmetric, more memory) x, info = gmres(A, b, tol=1e-8, restart=30, maxiter=100) # MINRES (symmetric, possibly indefinite) x, info = minres(A, b, tol=1e-8) # info: 0 = success, >0 = convergence not achieved, <0 = illegal input if info == 0: print("Converged") else: print(f"Did not converge (info={info})") # Callback to track convergence residuals = [] x, _ = gmres(A, b, callback=lambda r: residuals.append(r))
| Solver | Matrix Type | Notes |
|---|---|---|
cg | SPD | Fastest for well-conditioned SPD |
minres | Symmetric (indefinite ok) | |
bicg | General | Less stable than bicgstab |
bicgstab | General | More stable than bicg |
cgs | General | Squared CG |
gmres | General | Robust; stores Krylov basis (memory) |
lgmres | General | Less memory than gmres |
gcrotmk | General | Flexible restart, memory efficient |
qmr | General | Quasi-minimal residual |
Eigenvalue Solvers (ARPACK)
from scipy.sparse.linalg import eigs, eigsh # eigs: general (non-symmetric) eigenvalues vals, vecs = eigs(A, k=6, which='LM') # 6 largest magnitude eigenvalues vals, vecs = eigs(A, k=6, which='SM') # 6 smallest magnitude vals, vecs = eigs(A, k=6, which='LR') # largest real part # eigsh: symmetric (real symmetric / Hermitian) vals, vecs = eigsh(A, k=6, which='LM') vals, vecs = eigsh(A, k=6, which='SM', sigma=0.0) # near sigma=0 (shift-invert) # Shift-invert (for interior eigenvalues) vals, vecs = eigsh(A, k=6, sigma=1.0, which='LM') # which options: 'LM', 'SM', 'LR', 'SR', 'LI', 'SI' (complex only)
SVD
from scipy.sparse.linalg import svds # k largest singular values/vectors U, s, Vh = svds(A, k=5) # Note: results are in *ascending* order — reverse if needed U, s, Vh = svds(A, k=5) idx = np.argsort(s)[::-1] U, s, Vh = U[:, idx], s[idx], Vh[idx, :]
Sparse Norm
from scipy.sparse.linalg import norm n1 = norm(A, ord=1) # max column sum ni = norm(A, ord=np.inf) # max row sum nf = norm(A, ord='fro') # Frobenius
Linear Operators
from scipy.sparse.linalg import LinearOperator # Define a matrix-free operator (e.g., FFT-based) def matvec(x): return np.fft.ifft(np.fft.fft(x) * freq_domain_multiplier) def rmatvec(x): return np.conj(matvec(np.conj(x))) op = LinearOperator(shape=(n, n), matvec=matvec, rmatvec=rmatvec, dtype=complex) # Use with iterative solvers x, info = gmres(op, b) vals, vecs = eigs(op, k=5)
Common Gotchas
*`csr_array csr_array
is element-wise** in new array API. Use@for matrix multiplication. Oldcsr_matrix * csr_matrix` was matrix multiply — a major semantic difference.
Convert to CSC for direct solvers.
spsolveandspluinternally convert to CSC if needed, which silently copies the data. Pass CSC from the start to avoid the copy.
lil_arrayis slow for arithmetic. Use it only to build the matrix incrementally, then convert with.tocsr()before any computation.
eigs/eigshcannot return all eigenvalues. Usek < n - 1. For all eigenvalues of a sparse matrix, convert to dense and usescipy.linalg.eigh.
ARPACK
which='SM'is numerically unstable without shift-invert. Usesigma=0.0(shift-invert mode) to find eigenvalues near zero reliably.
Iterative solver
tolis relative to the initial residual||b||. If the initial guess is bad andbis large, you may need a lowertolthan expected.