Exercises — scipy.linalg — more stable than numpy.linalg, lu, qr, schur
This page is a graded workout for the parent topic. Work each problem before opening its solution. Levels climb from recognition to mastery. Every symbol is built from the ground up as we go — nothing is assumed.
Before we start, some words we will use constantly. Picture a matrix as a machine that moves arrows (vectors).
Level 1 — Recognition
Goal: can you spot which factorization/tool a situation calls for, and read scipy's output correctly?
L1.1 — Name the tool
For each task, name the single best scipy.linalg routine.
(a) Solve one system for a general square . (b) Find where is tall (, more equations than unknowns). (c) Get eigenvalues of a possibly defective matrix (recall: one with too few independent eigenvectors to diagonalize), using only perfectly-conditioned steps. (d) Solve the generalized eigenproblem .
Recall Solution
(a) solve(A, b) — internally one LU factorization + two triangular solves.
(b) lstsq(A, b) (QR/SVD under the hood). "Tall + minimize the leftover length" = least squares.
(c) schur(A) — always exists, uses only unitary steps (), eigenvalues appear on the diagonal of . Diagonalization would fail here because a defective matrix has no invertible eigenvector matrix .
(d) eig(A, b=B) — scipy's eig accepts a second matrix ; numpy.linalg cannot do this.
L1.2 — Read the lu output
scipy.linalg.lu(A) returns P, L, U. Which identity is correct?
Recall Solution
Both (ii) and (iii) are correct — they are the same statement. SciPy's convention is . Multiplying on the left by and using (a permutation matrix is orthogonal) gives . Statement (i) is the common wrong reading.
Level 2 — Application
Goal: run a factorization by hand, then confirm it is exactly what the code returns.
L2.1 — LU by hand, then solve
Let
Do partial-pivoted LU by hand, then solve via forward + back substitution.
The figure below walks the two stages of this exact problem. Left panel: a bar chart comparing the first-column magnitudes (row 1) and (row 2); the taller amber bar (row 2, value ) wins and becomes the pivot, so we swap rows. Right panel: the substitution ladder — the cyan block does the forward pass from the top down (, then ), and the amber block does the back pass from the bottom up (, then ), landing on the solution .

Recall Solution
Pivot. Compare the first-column entries and . We pivot on the largest to avoid dividing by something small (small pivots amplify rounding). Since , swap rows: Eliminate. The multiplier is ==== (the "how many pivot-rows fit into the entry below" number). Do :
L=\begin{pmatrix}1&0\\ \tfrac23 & 1\end{pmatrix}.$$ $L$ just **stores the multiplier** — undoing "subtract $\tfrac23$" is "add $\tfrac23$", so no new computation. **Permute $b$.** We swapped rows, so $Pb=\begin{pmatrix}12\\10\end{pmatrix}$. **Forward-solve $Ly=Pb$** (top-down through the lower triangle): $$y_1=12,\qquad \tfrac23 y_1 + y_2 = 10 \Rightarrow y_2 = 10 - \tfrac23(12)=2.$$ **Back-solve $Ux=y$** (bottom-up through the upper triangle): $$1\cdot x_2 = 2 \Rightarrow x_2 = 2,\qquad 6x_1 + 3x_2 = 12 \Rightarrow 6x_1 = 6 \Rightarrow x_1 = 1.$$ **Answer $x=(1,\,2)$.** Check in the original $A$: row 1 $= 4(1)+3(2)=10$ ✓, row 2 $=6(1)+3(2)=12$ ✓.L2.2 — QR of a by hand
Let
Use Gram–Schmidt on the columns to build (orthonormal columns) and (upper triangular) with .
Recall Solution
Call the columns , . Gram–Schmidt turns these two arrows into two perpendicular unit arrows. Step 1 — normalise . Its length (norm) is . So The first entry is (how much of builds ). Step 2 — remove the -part of . The overlap is the dot product . The leftover perpendicular piece: Its length is . Normalise: Assemble. Check (columns perpendicular unit) and . (SciPy may flip a sign on a whole column and the matching row — both are valid QRs since and are both unit and perpendicular.)
Level 3 — Analysis
Goal: reason about why one method is more accurate than another.
L3.1 — Why not the normal equations?
To solve least squares , a classic route is the normal equations . QR instead reduces the problem to . Using the condition number (a number measuring how much can amplify input error — see Condition Number), explain in one sentence why QR is safer, and give the numeric blow-up factor when .
Recall Solution
Forming squares the condition number: . QR never forms ; it works with directly, so it keeps . With , the normal equations behave like a system of condition — you lose about decimal digits instead of . On standard double precision ( digits) the normal-equations answer can be garbage, while QR still keeps good digits.
L3.2 — Pivot choice changes the error
Solve two ways: (a) no pivoting (use the tiny as pivot), (b) partial pivoting (swap so is the pivot). Which one is safe, and why?
Recall Solution
True answer: , . So .
(a) No pivot. Multiplier . Row 2 becomes (the original is swallowed by rounding — it is orders smaller). Back-substitution then divides by this and you recover with total loss of the small digits — a catastrophic error.
(b) Partial pivoting. Swap first: pivot is , multiplier , tiny and harmless. Elimination gives a well-behaved and the computed answer matches to full precision.
Moral: partial pivoting picks the largest-magnitude available pivot precisely so the multiplier and nothing gets swamped. This is exactly why scipy.linalg.solve/lu pivot by default — the same lesson as Gaussian Elimination.
Level 4 — Synthesis
Goal: combine factorizations, or connect them across problems.
L4.1 — Reuse one factorization for many right-hand sides
You must solve for the same but three different vectors , , . Why is lu_factor + lu_solve better than calling solve three times, and what are the three answers?
Recall Solution
Why factor once: the expensive part of solving is the LU factorization itself (about operations). Each right-hand side then costs only two triangular solves (). Calling solve(A, b_k) three times redoes the factorization every time. lu_factor(A) once, then lu_solve per , pays the cubic cost once.
Answers (reuse the from L2.1, where we already found 's action):
- → (done in L2.1).
- : ; ; back: . So .
- : ; ; back: . So .
from scipy.linalg import lu_factor, lu_solve
lup = lu_factor(A) # factor ONCE
xs = [lu_solve(lup, bk) for bk in (b1, b2, b3)]L4.2 — Schur to read eigenvalues off the diagonal
For , the matrix is already upper triangular. Explain why its Schur form is trivial, read off the eigenvalues, and confirm they match eig. Why is Schur a safer engine than diagonalization here?
Recall Solution
Already-triangular case. The Schur decomposition is with unitary and upper triangular. If is already upper triangular, one valid choice is (the identity is unitary) and . The eigenvalues sit on the diagonal of , so they are the diagonal of : .
Check: the characteristic polynomial is , giving . eig(A) returns exactly [2., 3.]. See Eigenvalues and Eigenvectors.
Why Schur is safer: diagonalization needs invertible and can have a huge (blows up for near-defective matrices). Schur uses only the unitary with — no ill-conditioned inverse ever appears, and it always exists. That is why expm/sqrtm are built on it.
Level 5 — Mastery
Goal: pick and justify the whole pipeline, including degenerate cases.
L5.1 — Choose the right tool under constraints
For each scenario, name the routine AND one sentence of justification.
(a) is symmetric with all eigenvalues positive (SPD), size , and you must solve it fast.
(b) is , and you want plus the numerical rank of .
(c) You need (matrix exponential) for a non-symmetric .
(d) is singular (columns dependent) and you call solve(A,b) — what happens, what should you do?
Recall Solution
(a) cho_factor / cho_solve — SPD matrices admit Cholesky , which needs no pivoting and costs half of LU. Fastest and stable. (Cholesky Decomposition.)
(b) lstsq(A, b) — it returns x, residual, rank, singular_values; the SVD-based driver gives a reliable numerical rank, which plain QR does not report cleanly. (Least Squares.)
(c) expm(A) — built on the Schur decomposition (unitary, well-conditioned), so it stays accurate even when is not diagonalizable.
(d) solve(A, b) on an exactly singular raises LinAlgError ("singular matrix"); on a merely near-singular it returns a wildly amplified, wrong answer with no warning. Fix: switch to lstsq(A, b), which returns the minimum-norm least-squares solution and reports the numerical rank, so it does not choke on rank deficiency and tells you the matrix is degenerate.
L5.2 — Degenerate LU: a zero pivot
Try LU on without pivoting, then with partial pivoting. Show why the naive path breaks and the pivoted path succeeds.
Recall Solution
No pivoting. The would-be first pivot is . The multiplier is undefined — division by zero. Naive LU simply cannot start. This is the degenerate case that pivoting exists to handle.
Partial pivoting. Compare column-1 entries and ; the largest is , so swap rows:
Now this is already upper triangular: , so and . Factorization succeeds. This is exactly what scipy.linalg.lu does automatically — the permutation guarantees a usable pivot whenever the matrix is nonsingular. If a column were entirely zero (truly singular), no swap could help — and that is precisely when you switch to lstsq.