Appendix D — numpy / scipy.linalg Cheatsheet
This is the dense lookup card: every numpy or scipy operation the book uses, the
mathematics it computes, and a one-line example you can paste and run. The book's
philosophy is that code demonstrates the math (it never replaces the proof or the
hand computation), and this card is the dictionary that lets you translate a formula
into a call. Each example assumes the imports
import numpy as np and from scipy import linalg as sla, and that A, B are
square matrices and x, b are vectors, defined as needed.
A reminder that governs the whole card: math indexes from 1, numpy from 0 (so
$a_{ij}$ is A[i-1, j-1]), @ is matrix multiplication while * is elementwise,
and never test equality with == — use np.allclose. With that, the dictionary.
D.1 Creating and inspecting arrays
| You want | Code | Math / note |
|---|---|---|
| A matrix from rows | A = np.array([[1.,2.],[3.,4.]]) |
the $2\times2$ matrix; use floats |
| A vector | x = np.array([1., 2., 3.]) |
a column $\mathbf{x}$ by convention |
| Identity $I_n$ | np.eye(3) |
$I_3$ |
| Zeros / ones | np.zeros((2,3)), np.ones(4) |
a $2\times3$ zero matrix; the all-ones vector |
| Diagonal matrix | np.diag([2.,3.,5.]) |
$\operatorname{diag}(2,3,5)$ |
| Pull out the diagonal | np.diag(A) |
the vector of diagonal entries (same function, dual use) |
| Shape / size | A.shape, A.size |
$(m,n)$ and $mn$ |
| Reshape | np.arange(6).reshape(2,3) |
re-lay 6 numbers as $2\times3$ |
| Stack columns | np.column_stack([u, v]) |
build $A=[\,\mathbf{u}\ \mathbf{v}\,]$ |
Common Pitfall — A 1-D array of shape
(n,)is neither a row nor a column; it is a flat vector that numpy reshapes contextually.A @ xtreatsxas a column and returns a 1-D result. If you genuinely need a column matrix of shape(n,1), writex.reshape(-1, 1)orx[:, None]. Most of the book is happier with flat 1-D vectors.
D.2 The core operations
| You want | Code | Math |
|---|---|---|
| Matrix × matrix (composition) | A @ B |
$AB$ (Ch. 8) |
| Matrix × vector | A @ x |
$A\mathbf{x}$ (Ch. 7) |
| Elementwise product | A * B |
Hadamard, not matrix product |
| Transpose | A.T |
$A^{\mathsf{T}}$ |
| Conjugate transpose | A.conj().T |
$A^{*}$ (Hermitian; Ch. 27) |
| Dot product | u @ v or np.dot(u, v) |
$\mathbf{u}\cdot\mathbf{v}=\sum_i u_iv_i$ |
| Outer product | np.outer(u, v) |
$\mathbf{u}\mathbf{v}^{\mathsf{T}}$, a rank-1 matrix |
| Scalar multiple / sum | 2*A, A + B |
$2A$, $A+B$ |
| Trace | np.trace(A) |
$\operatorname{tr}(A)=\sum_i a_{ii}$ |
A = np.array([[2.,1.],[1.,3.]]); x = np.array([1.,2.])
A @ x # -> array([4., 7.]) matrix-vector product A x
A.T @ A # -> the symmetric matrix A^T A (appears in least squares)
D.3 Solving systems and inverting (Ch. 4, 9)
| You want | Code | Math |
|---|---|---|
| Solve $A\mathbf{x}=\mathbf{b}$ | np.linalg.solve(A, b) |
the unique solution (square, invertible $A$) |
| Inverse | np.linalg.inv(A) |
$A^{-1}$ |
| Determinant | np.linalg.det(A) |
$\det(A)$ |
| Rank | np.linalg.matrix_rank(A) |
$\operatorname{rank}(A)$ via SVD |
| Matrix power | np.linalg.matrix_power(A, k) |
$A^k$ (integer $k$; negative inverts) |
A = np.array([[2.,1.],[1.,3.]]); b = np.array([1.,2.])
np.linalg.solve(A, b) # -> array([0.2, 0.6]) solves A x = b
np.linalg.det(A) # -> 5.0
np.linalg.matrix_power(np.array([[1,1],[0,1]]), 3) # -> [[1,3],[0,1]] the shear cubed
Computational Note — Prefer
np.linalg.solve(A, b)tonp.linalg.inv(A) @ b. They are mathematically equal, butsolveis faster and numerically more accurate — forming the inverse explicitly wastes work and amplifies rounding error. Compute the inverse only when you genuinely need the matrix $A^{-1}$ itself, not merely a solution.
D.4 Eigenvalues: eig versus eigh (Ch. 23–27)
| You want | Code | Math / when |
|---|---|---|
| Eigenvalues + eigenvectors (general) | w, V = np.linalg.eig(A) |
$A\mathbf{v}=\lambda\mathbf{v}$; any square $A$ |
| Eigenvalues only | np.linalg.eigvals(A) |
the $\lambda$'s, no vectors |
| Symmetric/Hermitian version | w, V = np.linalg.eigh(A) |
$A=A^{\mathsf{T}}$ (or $A^{*}=A$) |
S = np.array([[2.,1.],[1.,2.]]) # symmetric
w, V = np.linalg.eigh(S) # w -> [1., 3.] (REAL, ASCENDING); V columns orthonormal
R = np.array([[0.,-1.],[1.,0.]]) # a 90-degree rotation
np.linalg.eigvals(R) # -> [0.+1.j, 0.-1.j] COMPLEX pair: no real fixed direction
Warning
— eig and eigh are not interchangeable. Use eigh for symmetric or
Hermitian matrices: it returns real eigenvalues in ascending order with
orthonormal eigenvectors (the columns of V), exactly as the Spectral Theorem of
Chapter 27 promises. Use eig for everything else: it returns eigenvalues in no
guaranteed order, possibly complex (a rotation gives a conjugate pair, as above),
with eigenvectors normalized to unit length but not generally orthogonal. Running
eig on a symmetric matrix discards those guarantees and can leak tiny imaginary
parts from rounding. The eigenvectors in each column of V also carry a sign
ambiguity ($\mathbf{v}$ and $-\mathbf{v}$ are both valid), and eig does not even
fix the order — so compare eigenvalues or spanned subspaces, never raw signed vectors.
D.5 The factorizations (Ch. 10, 20, 28, 30)
| You want | Code | Math |
|---|---|---|
| SVD | U, s, Vt = np.linalg.svd(A) |
$A=U\Sigma V^{\mathsf{T}}$ (Ch. 30) |
| Reduced/economy SVD | np.linalg.svd(A, full_matrices=False) |
thin $U$, $V$ — what you want for tall $A$ |
| QR | Q, R = np.linalg.qr(A) |
$A=QR$, $Q$ orthonormal columns (Ch. 20) |
| LU (with pivoting) | P, L, U = sla.lu(A) |
$A=PLU$ (Ch. 10) |
| Cholesky | L = np.linalg.cholesky(A) |
$A=LL^{\mathsf{T}}$, $A$ symmetric positive definite (Ch. 28) |
| Matrix exponential | sla.expm(A) |
$e^{A}$ (Ch. 37) |
| Null space basis | sla.null_space(A) |
orthonormal basis of $N(A)$ (Ch. 13) |
A = np.array([[3.,0.],[0.,-2.],[0.,0.]])
U, s, Vt = np.linalg.svd(A) # s -> [3., 2.] singular values, DESCENDING
# reconstruct: build a full m-by-n Sigma, then U @ Sigma @ Vt == A
Sigma = np.zeros_like(A); Sigma[:len(s),:len(s)] = np.diag(s)
np.allclose(U @ Sigma @ Vt, A) # -> True (note Vt is V^T already, not V)
Q, R = np.linalg.qr(np.array([[1.,1.],[1.,0.],[0.,1.]]))
np.allclose(Q.T @ Q, np.eye(2)) # -> True Q has orthonormal columns (3x2 reduced by default)
P, L, U = sla.lu(np.array([[0.,2.],[1.,1.]]))
np.allclose(P @ L @ U, [[0.,2.],[1.,1.]]) # -> True convention is A = P @ L @ U
Warning
— SVD and QR have convention traps; read these before trusting the
output.
(1) svd returns the singular values in s in descending order (matches
Eckart–Young, Ch. 31) and returns Vt = $V^{\mathsf{T}}$, not $V$ — reconstruct
with U @ Sigma @ Vt, and read right singular vectors off the rows of Vt.
(2) The sign of each singular vector (and of each eigenvector) is arbitrary:
flipping a matched pair $(\mathbf{u}_i,\mathbf{v}_i)\to(-\mathbf{u}_i,-\mathbf{v}_i)$
leaves $A$ unchanged, so different machines or versions may differ by a sign. Compare
singular values or the reconstructed $A$, not raw vectors.
(3) np.linalg.qr returns the reduced ("economy") factorization by default for a
tall $A$ — for a $3\times2$ input, $Q$ is $3\times2$ and $R$ is $2\times2$. Pass
mode='complete' if you want the full square $Q$. The signs of $Q$'s columns (and
$R$'s diagonal) are likewise convention-dependent.
Computational Note — numpy and scipy disagree on Cholesky's default
triangle. np.linalg.cholesky(A) returns the lower factor $L$ with
$A=LL^{\mathsf{T}}$. But sla.cholesky(A) returns the upper factor $R$ by default,
with $A=R^{\mathsf{T}}R$; pass sla.cholesky(A, lower=True) to get the lower $L$.
Mixing them up reconstructs the transpose of what you expect. Both require $A$ to be
symmetric positive definite (Ch. 28) and raise an error otherwise — which is itself a
cheap positive-definiteness test.
D.6 Norms, conditioning, and least squares (Ch. 18, 19, 38)
| You want | Code | Math |
|---|---|---|
| Vector norm | np.linalg.norm(x) |
$\lVert\mathbf{x}\rVert_2=\sqrt{\sum x_i^2}$ |
| 1-norm / ∞-norm | np.linalg.norm(x, 1), np.linalg.norm(x, np.inf) |
$\sum|x_i|$ ; $\max|x_i|$ |
| Frobenius norm of a matrix | np.linalg.norm(A) |
$\sqrt{\sum a_{ij}^2}$ (matrix default) |
| Spectral (operator 2-) norm | np.linalg.norm(A, 2) |
largest singular value $\sigma_1$ (Ch. 30) |
| Condition number | np.linalg.cond(A) |
$\kappa(A)=\sigma_1/\sigma_n$ (Ch. 38) |
| Least squares | np.linalg.lstsq(A, b, rcond=None) |
minimizes $\lVert A\mathbf{x}-\mathbf{b}\rVert$ (Ch. 19) |
np.linalg.norm([3., 4.]) # -> 5.0
np.linalg.cond(np.array([[1.,0.],[0.,1e-3]])) # -> 1000.0 (ill-conditioned)
# Least squares: fit a line y = c0 + c1 t through 3 points
M = np.array([[1.,0.],[1.,1.],[1.,2.]]); y = np.array([1.,2.,2.])
coef, *_ = np.linalg.lstsq(M, y, rcond=None) # coef -> [1.1667, 0.5]
Computational Note —
np.linalg.norm(A)on a matrix defaults to the Frobenius norm (treat all entries as one long vector), whereas on a vector it defaults to the Euclidean 2-norm. To get the matrix's operator 2-norm — its largest singular value, the gain of the worst-case input direction (Ch. 30) — you must pass the explicit order,np.linalg.norm(A, 2). The two matrix norms are different numbers; name the one you mean. Andlstsqreturns a tuple (solution, residuals, rank, singular values), so unpack it —coef, *_ = np.linalg.lstsq(...)— and passrcond=Noneto silence a version-dependent warning and get the documented behavior.
D.7 A one-screen summary
import numpy as np
from scipy import linalg as sla
A @ B # matrix product (composition) | A * B is elementwise!
A.T, A.conj().T # transpose A^T ; conjugate transpose A^*
np.linalg.solve(A, b) # solve A x = b (preferred over inv(A) @ b)
np.linalg.inv(A) # inverse A^{-1}
np.linalg.det(A) # determinant
np.linalg.matrix_rank(A) # rank
np.linalg.matrix_power(A,k)# A^k
np.linalg.eig(A) # general eigen-decomposition (unsorted, maybe complex)
np.linalg.eigh(S) # symmetric/Hermitian: REAL, ascending, orthonormal
np.linalg.svd(A) # U, s, Vt (s descending; Vt is V^T)
np.linalg.qr(A) # Q, R (reduced by default; signs vary)
np.linalg.lstsq(A,b,rcond=None) # least squares min ||A x - b||
np.linalg.norm(x) # ||x||_2 ; norm(A) is Frobenius ; norm(A,2) is sigma_1
np.linalg.cond(A) # condition number sigma_1/sigma_n
sla.lu(A) # P, L, U with A = P @ L @ U
sla.expm(A) # matrix exponential e^A
sla.null_space(A) # orthonormal basis of N(A)
sla.cholesky(A, lower=True)# lower L with A = L L^T (scipy default is UPPER)
Pin this to the wall. The math comes first in this book; numpy is how you watch it come
true. When a result surprises you, the cause is almost always a convention on this card
— a default order, a transpose already taken, a sign you cannot pin down, or * where
you meant @.