SciPy Cheatsheet

Linear Algebra

Use this SciPy reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Core Import

from scipy import linalg
import numpy as np

# Or direct imports
from scipy.linalg import (
    solve, lstsq, inv, det, norm,
    eig, eigh, eigvals, eigvalsh,
    svd, svdvals,
    lu, qr, cholesky, ldl,
    expm, logm, sqrtm, funm,
    solve_triangular, solve_banded,
    block_diag, circulant, companion,
    kron, tril, triu
)

Prefer scipy.linalg over numpy.linalg — scipy uses LAPACK directly and supports more options, faster routines, and additional decompositions.

Solving Linear Systems

solve — Square Systems

from scipy.linalg import solve

A = np.array([[3, 1], [1, 2]], dtype=float)
b = np.array([9, 8], dtype=float)

x = solve(A, b)           # solves A @ x = b
# x ≈ [2, 3]

# Multiple right-hand sides (b as 2-D)
B = np.random.rand(2, 5)
X = solve(A, B)           # shape (2, 5)

# Key options
x = solve(A, b,
          lower=False,        # A is upper triangular (skip factorization)
          overwrite_a=False,  # allow overwriting A for memory efficiency
          overwrite_b=False,
          check_finite=True,  # set False to skip input checking (faster)
          assume_a='gen')     # 'gen', 'sym', 'her', 'pos'

# Symmetric positive definite → faster
x = solve(A, b, assume_a='pos')

solve_triangular

from scipy.linalg import solve_triangular

R = np.triu(np.random.rand(4, 4)) + np.eye(4)  # upper triangular
b = np.random.rand(4)

x = solve_triangular(R, b)                   # default: upper
x = solve_triangular(R, b, lower=True)       # treat as lower
x = solve_triangular(R, b, trans='T')        # solve R.T @ x = b
x = solve_triangular(R, b, unit_diagonal=True)

solve_banded

from scipy.linalg import solve_banded

# ab[0] = upper diagonal, ab[1] = main, ab[2] = lower (for l=1, u=1)
ab = np.array([[0, 1, 1, 1],    # upper diag (first element unused)
               [4, 4, 4, 4],    # main diagonal
               [1, 1, 1, 0]])   # lower diag (last element unused)
b = np.array([1, 2, 3, 4], dtype=float)

x = solve_banded((1, 1), ab, b)  # (l, u) = number of sub/superdiagonals

lstsq — Least-Squares (Overdetermined)

from scipy.linalg import lstsq

A = np.random.rand(10, 3)
b = np.random.rand(10)

x, residuals, rank, sv = lstsq(A, b)
# x         — solution minimizing ||A@x - b||
# residuals — sum of squared residuals (empty if rank < N)
# rank      — effective rank of A
# sv        — singular values

Matrix Factorizations

LU Decomposition

from scipy.linalg import lu, lu_factor, lu_solve

P, L, U = lu(A)             # A = P @ L @ U

# Efficient repeated solves with same A
lu_and_piv = lu_factor(A)   # factorize once
x = lu_solve(lu_and_piv, b) # solve many times
x2 = lu_solve(lu_and_piv, b2)

QR Decomposition

from scipy.linalg import qr, qr_update, qr_delete, qr_insert

Q, R = qr(A)                     # full QR: Q (m×m), R (m×n)
Q, R = qr(A, mode='economic')    # thin QR: Q (m×n), R (n×n)
R_only = qr(A, mode='r')         # R only (faster)
Q, R, P = qr(A, pivoting=True)   # column-pivoted: A[:,P] = Q @ R

Cholesky Decomposition

from scipy.linalg import cholesky, cho_factor, cho_solve

# A must be symmetric positive definite
L = cholesky(A, lower=True)    # A = L @ L.T (lower triangular)
U = cholesky(A, lower=False)   # A = U.T @ U (upper triangular)

# Efficient solves
c, low = cho_factor(A, lower=True)
x = cho_solve((c, low), b)

LDL Decomposition

from scipy.linalg import ldl

# For symmetric (not necessarily positive definite)
L, D, perm = ldl(A)   # A[np.ix_(perm,perm)] = L @ D @ L.T

SVD

from scipy.linalg import svd, svdvals

U, s, Vh = svd(A)              # full SVD: A = U @ diag(s) @ Vh
U, s, Vh = svd(A, full_matrices=False)  # thin/economy SVD

s_only = svdvals(A)            # just singular values (faster)

# Reconstruct
A_reconstructed = U @ np.diag(s) @ Vh

# Truncated SVD (use scipy.sparse.linalg for large matrices)
from scipy.sparse.linalg import svds
U_k, s_k, Vh_k = svds(A, k=5)  # top-5 singular values/vectors

Schur Decomposition

from scipy.linalg import schur, rsf2csf

T, Z = schur(A)               # A = Z @ T @ Z.H (T quasi-upper triangular)
T, Z = schur(A, output='complex')  # force complex Schur form

Eigenvalues & Eigenvectors

from scipy.linalg import eig, eigh, eigvals, eigvalsh

# General (non-symmetric)
eigenvalues, eigenvectors = eig(A)
eigenvalues = eigvals(A)          # eigenvalues only (faster)

# Symmetric/Hermitian (real eigenvalues, orthonormal eigenvectors)
eigenvalues, eigenvectors = eigh(A)    # sorted ascending
eigenvalues = eigvalsh(A)

# Generalized eigenvalue problem: A @ v = lambda * B @ v
eigenvalues, eigenvectors = eig(A, b=B)
eigenvalues, eigenvectors = eigh(A, b=B)  # symmetric A, B

# Subset (faster for large matrices)
eigenvalues, eigenvectors = eigh(A, subset_by_index=[0, 4])  # first 5
eigenvalues, eigenvectors = eigh(A, subset_by_value=[-1, 1]) # in [-1,1]

# Left and right eigenvectors
eigenvalues, vl, vr = eig(A, left=True, right=True)

Matrix Operations

from scipy.linalg import inv, det, norm, matrix_rank, pinv

inv_A = inv(A)                         # inverse (avoid if possible — use solve)
d = det(A)                             # determinant
r = matrix_rank(A)                     # numerical rank
A_pinv = pinv(A)                       # Moore-Penrose pseudoinverse

# Norms
n1 = norm(A, ord=1)     # max column sum
n2 = norm(A, ord=2)     # spectral norm (= largest singular value)
nf = norm(A, 'fro')     # Frobenius
ni = norm(A, np.inf)    # max row sum

# Condition number
cond = norm(A) * norm(inv(A))
# or faster:
cond = np.linalg.cond(A)

Matrix Functions

from scipy.linalg import expm, logm, sqrtm, signm, funm, cosm, sinm

eA = expm(A)          # matrix exponential e^A
lA = logm(A)          # matrix logarithm
sA = sqrtm(A)         # matrix square root
sgA = signm(A)        # matrix sign

# General matrix function
fA = funm(A, np.cos)  # cos(A) via Schur decomposition

# Trigonometric
cA = cosm(A)
sA = sinm(A)

Special Matrix Construction

from scipy.linalg import block_diag, circulant, companion, toeplitz, hankel, hadamard, hilbert, leslie

# Block diagonal
BD = block_diag(A1, A2, A3)

# Circulant (from first column)
C = circulant([1, 2, 3, 4])

# Companion matrix of polynomial
# For x^3 - 6x^2 + 11x - 6 = 0:
M = companion([1, -6, 11, -6])

# Toeplitz
T = toeplitz([1, 2, 3], [1, 4, 5])   # (col, row) vectors

# Hilbert (ill-conditioned test matrix)
H = hilbert(5)

# Hadamard
W = hadamard(8)   # n must be power of 2

Solving Structured Systems

# Kronecker product
from scipy.linalg import kron
K = kron(A, B)    # Kronecker product

# Solve with preconditioning (large systems — see also scipy.sparse.linalg)
from scipy.sparse.linalg import spsolve, cg, gmres
from scipy.sparse import csr_array

# For large, sparse A:
A_sparse = csr_array(A)
x = spsolve(A_sparse, b)

# Iterative solvers
x, info = cg(A_sparse, b, tol=1e-8, maxiter=1000)    # Conjugate Gradient (SPD)
x, info = gmres(A_sparse, b, tol=1e-8)               # GMRES (general)

Common Gotchas

Never compute inv(A) @ b to solve A @ x = b. Use solve(A, b) — it is 3x faster, numerically stabler, and avoids explicitly forming the inverse.

eig vs eigh: For symmetric/Hermitian matrices, eigh is faster, returns real eigenvalues, and guarantees orthonormal eigenvectors. eig on a symmetric matrix may still return tiny imaginary parts.

svd vs svds: linalg.svd computes all singular values (dense). For large matrices and only a few singular values, use scipy.sparse.linalg.svds.

Overflow in expm: If A has large entries, expm can overflow. Consider scaling and squaring manually, or check the norm of A first.

norm(A) without ord returns the Frobenius norm for matrices but the 2-norm for vectors — be explicit with ord=.