Worked examples — NumPy linear algebra — np.linalg.solve, eig, svd, norm, det
This page is the exercise gym for the parent note. We do not re-teach the theory — we stress-test it. The goal: after this page there is no case — no sign, no zero, no collapse, no weird shape — that surprises you.
If any symbol below feels unfamiliar, the parent note and these prerequisites build it: Gaussian elimination and LU decomposition, Eigenvalues and diagonalization, Vector norms and metric spaces, Condition number and numerical stability, Least squares regression, NumPy arrays and broadcasting.
The scenario matrix
Every one of the five tools has normal cases and edge cases. The table below lists every class of scenario this topic can throw at you. Each worked example that follows is tagged with the cell(s) it covers. By the last example, every cell is green.
| # | Tool | Case class | What makes it special |
|---|---|---|---|
| C1 | solve |
Well-behaved square system | Unique answer, positive determinant |
| C2 | solve |
Singular system () | No unique solution — the machine collapsed |
| C3 | solve / cond |
Nearly singular (ill-conditioned) | Answer exists but is untrustworthy |
| C4 | det |
Negative determinant | Orientation flip (mirror) — the sign matters |
| C5 | norm |
Vector norms, Frobenius and matrix operator norm | Same object, different "size" answers |
| C6 | eig |
Real distinct eigenvalues (symmetric) | Clean stretch directions |
| C7 | eig |
Complex eigenvalues (rotation) | Machine rotates → no real fixed direction |
| C8 | svd |
Non-square matrix | eig would crash; SVD still works |
| C9 | svd / word problem |
Rank-deficient / compression | A real singular value hits zero |
| C10 | lstsq |
Non-square / over-determined system | solve crashes; least-squares gives best fit |
| C11 | mixed | Exam twist — combine tools | Choose the right tool + verify |
Figures accompany the geometric cells (C1, C4, C7, C8, C10).
C1 — Well-behaved square system

Look at the figure: each equation is a line in the – plane. Two non-parallel lines cross at exactly one point — that crossing is the solution.
Steps.
- Write it as with , . Why this step? NumPy needs the coefficient grid and the right-hand column separated — the unknowns live in the vector we solve for.
- Check the machine is invertible first: . Why this step? A nonzero determinant means the two column-arrows span real area (the figure's lines are not parallel), so a unique crossing exists.
- Eliminate : gives , i.e. .
Why this step? Subtracting a multiple of row 1 zeroes the first column below the pivot — exactly what Gaussian elimination and LU decomposition does inside
solve. - Back-substitute: . Why this step? Once the system is triangular, the bottom row already gave ; we walk upward, plugging the known into the top row to release the last unknown .
A = np.array([[3.,2.],[1.,2.]]); b = np.array([12.,8.])
np.linalg.solve(A, b) # array([2., 3.])Recall Verify
Plug back: ✓ and ✓. Also np.allclose(A @ [2,3], b) is True.
C2 — Singular system ()
Steps.
- Form .
- Determinant: . Why this step? Row 2 is exactly half of row 1's coefficients (). The two column-arrows are parallel — the parallelogram has zero area, so the machine squishes the plane onto a line.
- Because ,
np.linalg.solveraisesLinAlgError: Singular matrix. Why? There is no unique crossing. Here the lines are parallel but not identical ( says , but the second says ) → no solution at all.
A = np.array([[2.,4.],[1.,2.]])
np.linalg.det(A) # 0.0 (up to rounding)
# np.linalg.solve(A, [6.,5.]) -> raises LinAlgError: Singular matrixRecall Verify
✓. The system is inconsistent: cannot equal both and .
C3 — Nearly singular (ill-conditioned)
Steps.
- — nonzero, so a unique answer exists on paper.
Why this step? We always test invertibility first (as in C1): a nonzero determinant promises a single crossing, so it is legitimate to ask
solvefor the answer. - Solve:
np.linalg.solvereturns . Why this step? With invertibility confirmed, onesolvecall runs the Gaussian elimination of Gaussian elimination and LU decomposition and hands back the unique — but the tiny pivot warns us to keep our guard up. - Measure danger with the condition number (the symbol introduced at the top of the page), which asks "how much can a small error in blow up in ?"
Why this tool and not
det? A tinydetcan just mean small scale, not instability. is scale-independent — it directly measures error amplification (see Condition number and numerical stability). - Here : a change of in can shift by order . The answer is mathematically fine but numerically fragile.
- Watch the amplification for real. Nudge by a hair to — a change of only in one entry. Re-solving gives : the answer jumped by a whole unit. A wobble in the input produced an swing in the output — that is exactly what predicted. Why this step? Quoting a condition number is abstract; re-solving a perturbed system makes the fragility concrete and visible.
A = np.array([[1.,1.],[1.,1.0001]])
b = np.array([2., 2.0001])
b2 = np.array([2., 2.0002]) # perturb b by 1e-4
np.linalg.solve(A, b) # array([1., 1.])
np.linalg.solve(A, b2) # array([0., 2.]) <- huge jump from a tiny nudge!
np.linalg.cond(A) # ~ 4.0e4 -> large: distrust this solveRecall Verify
✓. Plug back: ✓, ✓. Perturbed: ✓, ✓.
C4 — Negative determinant (orientation flip)

Steps.
- .
- Magnitude : areas are preserved (a unit square still has area 1). Why this step? is the volume-scaling factor from the parent note — here it scales by 1, no growth.
- Sign : the transformation is a reflection. In the figure, the labelled corners run counter-clockwise before, and clockwise after — the orientation flipped, like a mirror. Why does sign encode this? is a signed area; swapping the two column-arrows (which this matrix does — it swaps the axes) reverses the "handedness".
np.linalg.det(np.array([[0.,1.],[1.,0.]])) # -1.0 (area kept, orientation flipped)Recall Verify
✓.
C5 — All the norms of one object (vector, Frobenius, and operator)
Steps.
- . Why: is the total taxicab distance — add up every step regardless of sign.
- . Why: straight-line (Euclidean) length; the zero entry contributes nothing.
- . Why: the single biggest component — the worst offender.
- Ordering: always, so ✓.
- Frobenius of : flatten the whole matrix into one long vector and take its length: . Why this tool? Frobenius treats the matrix as raw stored "energy" of all entries — cheap, but it ignores what the matrix does to vectors.
- Operator (induced-2) norm of : this answers a different question — "what is the biggest stretch factor applies to any unit vector?" That number is the largest singular value (see C8 for singular values). For our symmetric , equals the largest . Its eigenvalues solve , so . Why a separate norm? When you care about worst-case amplification of a system (stability, error bounds), Frobenius is the wrong ruler — the operator norm is the honest one. Notice the two matrix norms differ ( vs ): they measure genuinely different things.
v = np.array([-3.,4.,0.,12.])
np.linalg.norm(v, 1) # 19.0
np.linalg.norm(v) # 13.0 (default ord=2)
np.linalg.norm(v, np.inf) # 12.0
M = np.array([[1.,-2.],[-2.,2.]])
np.linalg.norm(M, 'fro') # 3.6055... 'fro' is the API shorthand for the Frobenius norm
np.linalg.norm(M, 2) # 3.5615... ord=2 on a MATRIX = operator/induced-2 norm = largest singular valueRecall Verify
, ✓. Ordering ✓. Frobenius ✓; operator norm ✓.
C6 — Real distinct eigenvalues (symmetric matrix)
Steps.
- Characteristic equation , where is the identity matrix defined at the top of this page: Why this step? From the parent note: a fixed stretch direction exists only when collapses (is singular), i.e. its determinant is zero. We subtract (a scaled do-nothing machine) so the equation becomes .
- Solve or . Why this step? Taking the square root of both sides is how we undo the squaring in step 1 — and a square equals in exactly two ways ( and ), which is precisely why a matrix has (up to) two eigenvalues.
- For : . For : . Why: we seek the direction the machine only stretches; solving finds it.
- Check perpendicular: ✓ — symmetric matrices always give orthogonal eigenvectors (Eigenvalues and diagonalization).
A = np.array([[2.,1.],[1.,2.]])
w, V = np.linalg.eig(A) # w = [3., 1.] (order may vary)
np.allclose(A @ V[:,0], w[0]*V[:,0]) # TrueRecall Verify
Product of eigenvalues : ✓. Sum trace: ✓.
C7 — Complex eigenvalues (pure rotation)

Steps.
- Build the characteristic determinant . Why this step? Same recipe as C6: eigenvalues are the that make singular, and singular ⇔ zero determinant. We subtract from and take the determinant .
- (imaginary!). Why complex? Look at the figure: every real arrow gets turned 90°, so is never a real multiple of . No real eigenvector can exist — the algebra confesses this by producing imaginary .
- This is exactly the parent note's warning:
eigon a rotation returns complex numbers. NumPy givesw = [0.+1.j, 0.-1.j].
R = np.array([[0.,-1.],[1.,0.]])
np.linalg.eigvals(R) # array([0.+1.j, 0.-1.j])Recall Verify
at : ✓. Product ✓; sum ✓.
C8 — Non-square matrix (eig would crash, SVD saves us)

Steps.
eig(A)is undefined — eigenvalues need a square machine that maps a space to itself. sends 2-D input to 3-D output. Why SVD instead? SVD (parent §5) works for any shape: , read right-to-left as rotate () → stretch () → rotate ().- Singular values are the stretches. Since scales the first axis by 3, the second by 2, and adds a zero row (no third input direction), . Why: are eigenvalues of , giving , so .
- How and set the ellipse's axes. In the figure, the input unit circle maps to an ellipse. The columns of are the input directions that get stretched cleanly; the columns of are the output directions those land on — the ellipse's principal axes. Concretely: 's first column is sent to times 's first column, giving the long (red) semi-axis of length 3; 's second column maps to times 's second column, the short semi-axis of length 2. So says which input arrows, says how far they stretch, says where they point in output space. Why this matters: this is the exact geometric engine behind Principal Component Analysis (PCA) — the biggest picks the direction of greatest spread.
A = np.array([[3.,0.],[0.,2.],[0.,0.]])
U, s, Vt = np.linalg.svd(A)
s # array([3., 2.])
Sig = np.zeros_like(A); np.fill_diagonal(Sig, s)
np.allclose(U @ Sig @ Vt, A) # TrueRecall Verify
Eigenvalues of are ; square roots match s ✓.
C9 — Rank-deficient matrix / compression (word problem)
Steps.
- Notice column 2 column 1. So all data lies on one line — effectively 1-D. Why care? This is the heart of Principal Component Analysis (PCA) and compression: redundant directions carry zero singular value.
- Take the SVD. , whose determinant is → one eigenvalue is .
- Eigenvalues of : trace , so they are and . Singular values , . Why: a zero singular value = a direction with no stretch = a collapsed dimension → the matrix has rank 1.
- Answer: exactly one independent pattern. We could store this data with a single number per day (times the fixed ratio) — that's compression.
A = np.array([[1.,2.],[2.,4.],[3.,6.]])
U, s, Vt = np.linalg.svd(A)
s # array([8.3666, 0.]) -> rank 1
np.linalg.matrix_rank(A) # 1Recall Verify
(trace of ), ✓; hence rank 1 ✓.
C10 — Non-square / over-determined system (lstsq, not solve)

Steps.
- Write one equation per point: , i.e. with , . Why this step? Each data point is a demand on the line; stacking them makes tall (3 rows, 2 columns) — a non-square, over-determined system.
np.linalg.solverefuses this: it requires a square . There is no exact hitting all three points (they are not collinear), so no true inverse exists. Why this step?solveanswers "undo the machine exactly"; here no exact undo exists, so we must change the question.- Change the question to least squares: find minimizing the total squared miss — the tool for this is
np.linalg.lstsq(Least squares regression). Why this tool and notsolve? When you cannot satisfy every equation, the next-best is to be as close as possible in Euclidean distance; that is precisely what least squares minimizes. - The minimizer solves the normal equations :
, . Solving this square system: , giving .
Why this step? is small and square — safe for
solve— and its solution is provably the closest fit. In the figure, the red line threads between the points, minimizing the vertical gaps.
A = np.array([[0.,1.],[1.,1.],[2.,1.]]); b = np.array([1.,2.,2.])
# np.linalg.solve(A, b) -> LinAlgError: Last 2 dims must be square
sol, *_ = np.linalg.lstsq(A, b, rcond=None) # array([0.5, 1.1666...])Recall Verify
Normal equations: , ; solving gives . Residuals sum to ✓ (least-squares fits pass through the centroid).
C11 — Exam twist: pick the right tool, then verify
Steps.
- One number, two answers. Compute . Why this step? ⇒ unique solution for every (answer a); ⇒ areas scale by 6, and the minus sign ⇒ orientation flips (answer b). One determinant settles both sub-questions.
- Now solve for with
solve, neverinv: . Subtract row 1 from row 2: . Then . Why this order? Confirm invertibility (step 1) before asking for a solution — no point solving a collapsed system; and elimination gives the answer directly.
A = np.array([[4.,3.],[6.,3.]]); b = np.array([1.,0.])
np.linalg.det(A) # -6.0
np.linalg.solve(A, b) # array([-0.5, 1.])Recall Verify
✓ and ✓. ✓.
Quick self-test
Which tool detects a rotation-only matrix has no real fixed direction?
np.linalg.eig — it returns complex eigenvalues (C7).A tiny nonzero det but a huge cond — trust the solve?
Column 2 = 2×column 1: what is the smallest singular value?
Non-square matrix — eig or svd?
svd (C8); eig needs a square matrix.Over-determined system (more equations than unknowns) — solve or lstsq?
lstsq (C10); solve needs a square matrix, and no exact solution exists.On a matrix, what does np.linalg.norm(M, 2) return vs np.linalg.norm(M, 'fro')?
ord=2 = operator norm (largest singular value, worst-case stretch); 'fro' = Frobenius (entrywise Euclidean length). Different questions (C5).