Determinant and Inverse¶
Compute matrix determinants and inverses using np.linalg.
Mental Model
The determinant measures how a matrix scales volume -- zero means the matrix is singular and has no inverse. np.linalg.inv computes the inverse, but in practice you should almost always use np.linalg.solve instead, because solving a system directly is faster and more numerically stable than inverting first and multiplying.
The sharper insight: \(\det(A) = 0\) means information collapse — the transformation \(A\) maps some directions to zero, making it impossible to recover the original input. A near-zero determinant (ill-conditioned matrix) means the collapse is almost happening, and numerical results become unreliable.
np.linalg.det¶
1. Basic Usage¶
```python import numpy as np
def main(): A = np.array([[1, 2], [3, 4]])
det = np.linalg.det(A)
print("A =")
print(A)
print(f"det(A) = {det}")
if name == "main": main() ```
Output:
A =
[[1 2]
[3 4]]
det(A) = -2.0
2. Mathematical Form¶
For 2×2 matrix:
```python import numpy as np
def main(): a, b, c, d = 1, 2, 3, 4 A = np.array([[a, b], [c, d]])
det_numpy = np.linalg.det(A)
det_manual = a * d - b * c
print(f"NumPy det: {det_numpy}")
print(f"Manual det: {det_manual}")
if name == "main": main() ```
3. Larger Matrices¶
```python import numpy as np
def main(): A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 10]])
det = np.linalg.det(A)
print("A =")
print(A)
print(f"det(A) = {det:.4f}")
if name == "main": main() ```
Singular Matrices¶
1. Zero Determinant¶
```python import numpy as np
def main(): # Singular matrix (rows are linearly dependent) A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
det = np.linalg.det(A)
print("A =")
print(A)
print(f"det(A) = {det:.2e}") # Essentially zero
if name == "main": main() ```
2. Numerical Precision¶
```python import numpy as np
def main(): A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
det = np.linalg.det(A)
# Check if "essentially zero"
is_singular = np.abs(det) < 1e-10
print(f"det(A) = {det:.2e}")
print(f"Is singular: {is_singular}")
if name == "main": main() ```
3. Condition Number¶
Better way to check near-singularity.
```python import numpy as np
def main(): A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9.0001]]) # Nearly singular
cond = np.linalg.cond(A)
print(f"Condition number: {cond:.2e}")
print("High condition number indicates near-singularity")
if name == "main": main() ```
np.linalg.inv¶
1. Basic Usage¶
```python import numpy as np
def main(): A = np.array([[1, 2], [3, 4]])
A_inv = np.linalg.inv(A)
print("A =")
print(A)
print()
print("A^(-1) =")
print(A_inv)
if name == "main": main() ```
2. Verify Inverse¶
```python import numpy as np
def main(): A = np.array([[1, 2], [3, 4]])
A_inv = np.linalg.inv(A)
# A @ A^(-1) should be identity
product = A @ A_inv
print("A @ A^(-1) =")
print(product)
print()
print("Close to identity:", np.allclose(product, np.eye(2)))
if name == "main": main() ```
3. Singular Matrix Error¶
```python import numpy as np
def main(): A = np.array([[1, 2], [2, 4]]) # Singular (row 2 = 2 * row 1)
try:
A_inv = np.linalg.inv(A)
except np.linalg.LinAlgError as e:
print(f"Error: {e}")
if name == "main": main() ```
np.linalg.pinv¶
1. Pseudo-Inverse¶
Moore-Penrose pseudo-inverse for non-square or singular matrices.
```python import numpy as np
def main(): # Non-square matrix A = np.array([[1, 2], [3, 4], [5, 6]])
A_pinv = np.linalg.pinv(A)
print(f"A shape: {A.shape}")
print(f"A+ shape: {A_pinv.shape}")
print()
print("A+ =")
print(A_pinv)
if name == "main": main() ```
2. Least Squares¶
Pseudo-inverse gives least squares solution.
```python import numpy as np
def main(): # Overdetermined system A = np.array([[1, 1], [1, 2], [1, 3]]) b = np.array([1, 2, 2])
# Least squares via pinv
x = np.linalg.pinv(A) @ b
print(f"x = {x}")
print(f"Residual: {np.linalg.norm(A @ x - b):.4f}")
if name == "main": main() ```
3. Singular Matrix¶
```python import numpy as np
def main(): # Singular matrix A = np.array([[1, 2], [2, 4]])
# inv fails, pinv works
A_pinv = np.linalg.pinv(A)
print("A (singular) =")
print(A)
print()
print("A+ =")
print(A_pinv)
if name == "main": main() ```
Applications¶
1. Linear Transformation¶
```python import numpy as np
def main(): # Transform points A = np.array([[2, 0], [0, 3]])
points = np.array([[1, 0],
[0, 1],
[1, 1]])
# Forward transform
transformed = (A @ points.T).T
# Inverse transform
A_inv = np.linalg.inv(A)
recovered = (A_inv @ transformed.T).T
print("Original points:")
print(points)
print()
print("Transformed:")
print(transformed)
print()
print("Recovered:")
print(recovered)
if name == "main": main() ```
2. Covariance Inverse¶
```python import numpy as np
def main(): # Sample covariance matrix np.random.seed(42) X = np.random.randn(100, 3) cov = np.cov(X.T)
# Precision matrix (inverse covariance)
precision = np.linalg.inv(cov)
print("Covariance:")
print(cov.round(3))
print()
print("Precision:")
print(precision.round(3))
if name == "main": main() ```
3. Matrix Equation¶
Solve \(AXB = C\) for \(X\).
```python import numpy as np
def main(): A = np.array([[1, 2], [3, 4]]) B = np.array([[5, 6], [7, 8]]) C = np.array([[10, 20], [30, 40]])
# X = A^(-1) @ C @ B^(-1)
X = np.linalg.inv(A) @ C @ np.linalg.inv(B)
# Verify
print("X =")
print(X)
print()
print("A @ X @ B =")
print(A @ X @ B)
if name == "main": main() ```
Best Practices¶
1. Avoid Inverse¶
Prefer np.linalg.solve over computing inverse explicitly.
```python import numpy as np
def main(): A = np.random.randn(100, 100) b = np.random.randn(100)
# Bad: compute inverse
x_bad = np.linalg.inv(A) @ b
# Good: solve directly
x_good = np.linalg.solve(A, b)
print(f"Results close: {np.allclose(x_bad, x_good)}")
if name == "main": main() ```
2. Check Condition¶
Always check condition number for numerical stability.
3. Use pinv for Robustness¶
When matrix may be singular or non-square, use pinv.
Exercises¶
Exercise 1.
Create a 3x3 matrix A and compute its determinant. Then compute the determinant of 2 * A and verify that det(2A) = 2^3 * det(A) (the scaling property of determinants for an nxn matrix).
Solution to Exercise 1
import numpy as np
A = np.array([[1, 2, 3], [0, 4, 5], [1, 0, 6]], dtype=float)
det_A = np.linalg.det(A)
det_2A = np.linalg.det(2 * A)
print(f"det(A) = {det_A:.4f}")
print(f"det(2A) = {det_2A:.4f}")
print(f"2^3 * det(A) = {8 * det_A:.4f}")
print(f"Match: {np.allclose(det_2A, 8 * det_A)}")
Exercise 2.
Compute the inverse of A = np.array([[1, 2], [3, 4]]) and verify that A @ A_inv and A_inv @ A both equal the identity matrix (within floating-point tolerance).
Solution to Exercise 2
import numpy as np
A = np.array([[1, 2], [3, 4]], dtype=float)
A_inv = np.linalg.inv(A)
print(f"A @ A_inv = \n{(A @ A_inv).round(10)}")
print(f"Identity: {np.allclose(A @ A_inv, np.eye(2))}")
print(f"Identity: {np.allclose(A_inv @ A, np.eye(2))}")
Exercise 3.
Create a singular matrix A = np.array([[1, 2], [2, 4]]) (rows are linearly dependent). Show that np.linalg.det(A) returns approximately zero and that np.linalg.inv(A) either raises a LinAlgError or returns a matrix with very large elements.
Solution to Exercise 3
import numpy as np
A = np.array([[1, 2], [2, 4]], dtype=float)
det = np.linalg.det(A)
print(f"det(A) = {det:.2e}") # ~0
try:
A_inv = np.linalg.inv(A)
print(f"Inverse has large values: {np.max(np.abs(A_inv)):.2e}")
except np.linalg.LinAlgError as e:
print(f"Error: {e}")