import numpy as np
import scipy
from numpy.linalg import cond, matrix_rank, norm
from scipy.linalg import inv, solve, det, eig, lu
from scipy.linalg import solve_triangularGraduate Quantitative Economics and Datascience
reg y x, robust”This section uses the following packages:
Big-O Notation
For a function \(f(N)\), we say \(f(N)\) is \(O(g(N))\) if there exist positive constants \(C\) and \(N_0\) such that
\[ 0 \leq f(N) \leq C \cdot g(N) \quad \text{for all } N \geq N_0 \]
Let \(A\in{\mathbb{R}}^{M\times N}\) have full column rank, with \(b\in{\mathbb{R}}^M\) and \(M\gg N\).
\[ \min_{x\in{\mathbb{R}}^N} \lVert Ax-b\rVert_2^2, \qquad G=A^{\top}A,\quad c=A^{\top}b. \]
Suppose a simple optimizer takes \(K\) iterations, each evaluating \(Gx-c\).
| Algorithm | Total computational cost | Extra storage |
|---|---|---|
| Recompute from rows of \(A,b\) each iteration | \(O(KMN)\) | \(O(N)\) |
| Precompute and store \(G,c\) | \(O(\max(M,K)N^2)\) | \(O(N^2)\) |
| Direct QR solve (factor a copy of \(A\)) | \(O(MN^2)\) | \(O(MN)\) |
| Normal equations + Cholesky | \(O(MN^2)\) | \(O(N^2)\) |
Extra storage excludes the retained inputs \(A,b\).
Machine Epsilon
For a given datatype, \({\epsilon_{\mathrm{mach}}}= \min_{\delta > 0} \left\{ \delta : 1 + \delta > 1 \right\}\)
machine epsilon for float64 = 2.220446049250313e-16
1 + eps/2 == 1? True
machine epsilon for float32 = 1.1920928955078125e-07
Euclidean Norm
For \(x \in {\mathbb{R}}^N\), the Euclidean (or \(L^2\)) norm is
\[ ||x||_2 = \sqrt{\sum_{n=1}^N x_n^2} \]
x = np.array([1, 2, 3]) # Calculating different ways (in order of preference)
print(np.sqrt(sum(xval**2 for xval in x))) # manual with comprehensions
print(np.sqrt(np.sum(np.square(x)))) # broadcasts
print(norm(x)) # built-in to numpy norm(x, ord=2) alternatively
print(f"||x||_2^2 = {norm(x)**2} = {x.T @ x} = {np.dot(x, x)}")3.7416573867739413
3.7416573867739413
3.7416573867739413
||x||_2^2 = 14.0 = 14 = 14
solve with a calculation of an inverseWe can think of solving a system as finding the linear combination of columns of \(A\) that equal \(b\)
Hence, can solve \(A x = b\) for any \(b \in {\mathbb{R}}^2\) since the column space is the entire space \({\mathbb{R}}^2\)
On the other hand, note
So we can only solve \(A x = b\) for \(b \propto \begin{bmatrix} 1 \\ 2 \end{bmatrix} \propto \begin{bmatrix} 2 \\ 4 \end{bmatrix}\)
A = np.array([[1, 2],
[2, 4]])
# An (expensive) way to check if A is singular is if det(A) = 0
print(det(A) == 0.0)
print(matrix_rank(A) != A.shape[0]) # or check rank
# Check before inverting or use exceptions
try:
inv(A)
print("Matrix is not singular (invertible).")
except np.linalg.LinAlgError:
print("Matrix is singular (non-invertible).")True
True
Matrix is singular (non-invertible).
det(A)=-1e-08, det(K*A)=-100
cond(A)=1e+09, cond(K*A)=1e+09,
det(inv(A))=-1e+08, cond(inv(A))=1e+09
solve as well[[-1. -1.33333333]
[ 1. 1.5 ]]
Checking: A*[-1. 1.] = [2. 1.] = [2 1], column of B
eps = 2 * np.finfo(float).eps
A = np.array([[eps, 1.0], [1.0, 0.1]])
b = np.ones(2)
m = A[1, 0] / A[0, 0]
L = np.array([[1.0, 0.0], [m, 1.0]])
U = np.array([[eps, 1.0], [0.0, 0.1 - m]])
y = solve_triangular(L, b, lower=True)
x = solve_triangular(U, y)
with np.printoptions(precision=17):
print(f"Without pivoting:\nL @ U =\n{L @ U}\nx = {x}")
P, L, U = lu(A)
y = solve_triangular(L, P.T @ b, lower=True)
x = solve_triangular(U, y)
with np.printoptions(precision=17):
print(f"With pivoting:\nP @ L @ U =\n{P @ L @ U}\nx = {x}")Without pivoting:
L @ U =
[[4.440892098500626e-16 1.000000000000000e+00]
[1.000000000000000e+00 0.000000000000000e+00]]
x = [1. 0.9999999999999996]
With pivoting:
P @ L @ U =
[[4.440892098500626e-16 1.000000000000000e+00]
[1.000000000000000e+00 1.000000000000000e-01]]
x = [0.9 0.9999999999999996]
solve uses LU with partial pivoting (Gaussian elimination).\[ \begin{aligned} U x &= b\\ U &\equiv \begin{bmatrix} 3 & 1 \\ 0 & 2 \\ \end{bmatrix}, \quad b = \begin{bmatrix} 7 \\ 2 \\ \end{bmatrix} \end{aligned} \]
Solving bottom row for \(x_2\)
\[ 2 x_2 = 2,\quad x_2 = 1 \]
Move up a row, solving for \(x_1\), substituting for \(x_2\)
\[ 3 x_1 + 1 x_2 = 7,\quad 3 x_1 + 1 \times 1 = 7,\quad x_1 = 2 \]
Generalizes to many rows. For \(L\) it is “forward substitution”
Another common matrix type is symmetric, \(A = A^{\top}\)
\[ P = \begin{bmatrix}0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1\end{bmatrix}, \qquad P^{-1} = P^{\top} = \begin{bmatrix}0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1\end{bmatrix}. \]
Orthogonal Matrix
A square matrix \(Q\) is orthogonal if \(Q^{\top} Q = Q Q^{\top} = I\), equivalently \(Q^{-1} = Q^{\top}\).
\[ A x = \lambda x \]
For real square matrix \(A\) we may be able to take the eigenvectors as columns of \(Q\) (eigenvectors) and a diagonal matrix \(\Lambda\) of the eigenvalues such that
\[ A = Q \, \Lambda \, Q^{-1}. \]
These do not always exist. If so, you will hear the matrix is diagonalizable.
Alternatively, every real matrix admits a Singular Value Decomposition (SVD)
A special case of this comes from the spectral theorem, which will sometimes be referred to as a spectral decomposition
For any real symmetric matrix \(A\) (singular or not) there exist an orthogonal \(Q\) and a real diagonal \(\Lambda\) such that
\[ A = Q \, \Lambda \, Q^{\top}. \]
Geometric interpretation: apply \(Q^{\top}\) (rotation/reflection) to rotate into the basis, scale by \(\Lambda\), then apply \(Q\) to rotate/reflect back.
Q =
[[-0.85065081 -0.52573111]
[ 0.52573111 -0.85065081]]
Q^T Q =
[[1.0000000e+00 1.2127222e-17]
[1.2127222e-17 1.0000000e+00]]
Q^-1 == Q^T? True
Lambda = [1.38196601+0.j 3.61803399+0.j]
Q Lambda Q^T =
[[2. 1.]
[1. 3.]]
A = np.array([[5, 1],
[2, 3]])
Lambda, Q = eig(A)
print(f"eigenvectors are column-by-column in Q =\n{Q}")
print(f"Q^-1 == Q^T? {np.allclose(np.linalg.inv(Q), Q.T)}") # should be False
print(f"eigenvalues are in Lambda = {Lambda}")
print(f"Q Lambda inv(Q) =\n{Q @ np.diag(np.real(Lambda)) @ np.linalg.inv(Q)}")eigenvectors are column-by-column in Q =
[[ 0.80689822 -0.34372377]
[ 0.59069049 0.9390708 ]]
Q^-1 == Q^T? False
eigenvalues are in Lambda = [5.73205081+0.j 2.26794919+0.j]
Q Lambda inv(Q) =
[[5. 1.]
[2. 3.]]
Spectral Radius
For \(A \in {\mathbb{R}}^{N \times N}\) with eigenvalues \(\lambda_1, \ldots, \lambda_N\), the spectral radius is
\[ {\varrho\!\left( {A} \right)} = \max_{i}|\lambda_i| \]
Condition Number
For non-singular \(A\), the condition number is \(\kappa(A) = ||A|| \cdot ||A^{-1}||\), the ratio of the largest to the smallest singular value of \(A\). For symmetric \(A\) this is \(\kappa(A) = \left|\frac{\lambda_{\text{max}}}{\lambda_{\text{min}}}\right|\) for \(\lambda\) the eigenvalues of \(A\).
For other matrices the eigenvalues can be far from the singular values
Sense of relative dispersion and consistent scaling
For example, using the fact that a diagonal matrix has its eigenvalues on the diagonal
\[ A = \begin{bmatrix} 1 & 0 \\ 0 & 0.001 \end{bmatrix} \]
Or, the best we can do is \(\kappa(A) = 1\) for the identity (or any orthogonal matrix)
inv is a bad idea due to poor conditioning\[ \begin{aligned} c_0 + c_1 x_1 + c_2 x_1^2 + \ldots + c_N x_1^N &= y_1\\ \vdots & \\ c_0 + c_1 x_N + c_2 x_N^2 + \ldots + c_N x_N^N &= y_N\\ \end{aligned} \]
\[ A \equiv \begin{bmatrix} 1 & x_0 & x_0^2 & \ldots & x_0^N\\ \vdots & \vdots &\vdots & \vdots & \vdots\\ 1 & x_N & x_N^2 & \ldots & x_N^N \end{bmatrix} \]
\[ A c = y \]
solve and invN = 5
x = np.linspace(0.0, 10.0, N + 1)
y = np.exp(x) # example function to interpolate
A = np.array([[x_i**n for n in range(N + 1)] for x_i in x]) # or np.vander
c = solve(A, y)
c_inv = inv(A) @ y
print(f"error = {norm(A @ c - y, np.inf)}, \
error using inv(A) = {norm(A @ c_inv - y, np.inf)}")
print(f"cond(A) = {cond(A)}")error = 1.574562702444382e-11, error using inv(A) = 1.1932570487260818e-09
cond(A) = 564652.3214022254
N = 10
x = np.linspace(0.0, 10.0, N + 1)
y = np.exp(x) # example function to interpolate
A = np.array([[x_i**n for n in range(N + 1)] for x_i in x]) # or np.vander
c = solve(A, y)
c_inv = inv(A) @ y # Solving with inv(A) instead of solve(A, y)
print(f"error = {norm(A @ c - y, np.inf)}, \
error using inv(A) = {norm(A @ c_inv - y, np.inf)}")
print(f"cond(A) = {cond(A)}")error = 5.334186425898224e-10, error using inv(A) = 5.904003046452999e-06
cond(A) = 4462768125092.548
N = 20
x = np.linspace(0.0, 10.0, N + 1)
y = np.exp(x) # example function to interpolate
A = np.array([[x_i**n for n in range(N + 1)] for x_i in x]) # or np.vander
c = solve(A, y)
c_inv = inv(A) @ y # Solving with inv(A) instead of solve(A, y)
print(f"error = {norm(A @ c - y, np.inf)}, \
error using inv(A) = {norm(A @ c_inv - y, np.inf)}")
print(f"cond(A) = {cond(A):.4g}")error = 8.36735125631094e-10, error using inv(A) = 1057.184160412351
cond(A) = 4.126e+24
solve, which is faster and can often solve ill-conditioned problems. Rarely use inv, and only when you know the problem is well-conditionedN = 40
x = np.linspace(-1, 1, N+1) # Or any other range of x values
A = np.array([[np.polynomial.Chebyshev.basis(n)(x_i) for n in range(N+1)] for x_i in x])
A_monomial = np.array([[x_i**n for n in range(N + 1)] for x_i in x]) # or np.vander
print(f"cond(A) = {cond(A):.4g}, cond(A_monomial) = {cond(A_monomial):.4g}")cond(A) = 3.64e+09, cond(A_monomial) = 1.804e+18
Float8 (E4M3): \({\epsilon_{\mathrm{mach}}}= 0.125\).
\[ x = 1.0, \qquad e = 0.0625. \]
What is \(x + e\), computed in float8? In float16?
| float8 | float16 | |
|---|---|---|
| A. | \(1.0\) | \(1.0625\) |
| B. | \(1.0\) | \(1.0\) |
| C. | \(1.0625\) | \(1.0625\) |
| D. | \(1.0625\) | \(1.0\) |
What do these two vectors span?
A. \(\left\{{(x,y) \in {\mathbb{R}}^2 \mid y = x}\right\}\)
B. \({\mathbb{R}}^2\)
C. \(\left\{{(x,y) \in {\mathbb{R}}^2 \mid y = x,\ x \geq 0}\right\}\)
What are the ranks of these matrices?
\[ A = \begin{bmatrix}1 & 0 & 1 \\ 0 & 1 & 1 \\ 1 & 1 & 2\end{bmatrix}, \qquad B = \begin{bmatrix}1 & 0 \\ 0 & 1 \\ 1 & 1\end{bmatrix}. \]
A. \(\operatorname{rank}(A) = 3\), \(\operatorname{rank}(B) = 2\)
B. \(\operatorname{rank}(A) = 2\), \(\operatorname{rank}(B) = 2\)
C. \(\operatorname{rank}(A) = 2\), \(\operatorname{rank}(B) = 1\)
An invertible \(N \times N\) matrix \(A\) has already been factored as \(A = PLU\).
What is the cost of solving \(Ax=b\) for one new \(b\)?
A. \(O(N^2)\)
B. \(O(N^3)\)
Permute the right-hand side, then solve two triangular systems:
\[ L y = P^{\top} b, \qquad U x = y. \]
Each triangular solve is \(O(N^2)\); the \(O(N^3)\) factorization is not repeated.
Let \(Q\) be orthogonal and \(x\) be a vector.
If \(\lVert x \rVert_2 = 5\), what is \(\lVert Qx \rVert_2\)?
A. \(5\)
B. \(1\)
C. We need to know \(Q\).
Use \(Q^{\top} Q = I\):
\[ \lVert Qx \rVert_2^2 = x^{\top} Q^{\top} Q x = x^{\top} x = \lVert x \rVert_2^2. \]
Orthogonal transformations preserve Euclidean length, so \(\lVert Qx \rVert_2 = 5\).