5.4.11 · D2Scientific Computing (Python)

Visual walkthrough — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

2,148 words10 min readBack to topic

Step 1 — What "least squares" even asks

WHAT. We have more equations than unknowns. Picture a machine (a matrix ) that eats a small arrow and spits out an arrow in a big space. We have a target arrow out there. We ask: which input makes the output land as close to as possible?

WHY this question. In real data (fitting a line to 100 noisy points) there is no exact solution — the points don't lie on one line. So "solve exactly" is impossible; the honest question becomes "get as close as you can."

PICTURE. Below, is the orange target. As varies, can only reach points on the blue column space — the flat plane of everything the machine can produce. The closest reachable point is (green). The leftover gap is the red residual.

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

So the whole problem is: — make the red arrow as short as possible.


Step 2 — The geometric answer: drop a perpendicular

WHAT. Before any algebra, here is the answer in one idea: the shortest red arrow from the target down to the blue plane is the perpendicular one.

WHY. Think of standing at point above a flat floor (the column space). The closest floor point to you is straight down — any slanted path is longer. That "straight down" is the perpendicular drop. So at the best , the residual must be perpendicular to the whole plane.

PICTURE. The green foot of the perpendicular is ; the red meets the plane at a right angle (see the little square).

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

Everything after this is just: how do we compute that perpendicular drop cleanly?


Step 3 — Length is what we must protect (enter orthogonal matrices)

WHAT. Our whole problem is about length (). So the perfect tool is a transformation that does not change lengths. That tool is an orthogonal matrix .

WHY this tool and not another? We are free to look at the problem "through a rotated pair of glasses." But we must not distort the picture — if the glasses stretched things, the shortest arrow through them would be a different arrow, and we'd solve the wrong problem. Orthogonal matrices are exactly the rotations/reflections: they spin and flip space but never stretch it.

PICTURE. Left: an arrow and its length. Right: after applying (a rotation), the arrow points elsewhere but the ruler reads the same length.

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

Step 4 — Build and : straightening the columns of

WHAT. We factor the machine as :

  • = an orthonormal set of directions (clean perpendicular axes),
  • = an upper-triangular recipe saying how each original column of is mixed from those axes.

WHY. 's columns are usually slanted and unequal in length — awkward. Gram–Schmidt straightens them into perpendicular unit arrows one at a time, and bookkeeps the straightening in . We separate the machine into "a clean rotation" () times "a simple triangular stretch" ().

PICTURE. Two slanted columns (blue) become perpendicular unit vectors (green). (orange) is the small triangular table of coefficients: , and .

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

Step 5 — Look at the problem through 's glasses

WHAT. Substitute into the residual, then rotate the whole picture by (the reverse rotation).

WHY. From Step 3, is orthogonal too, so it doesn't change any lengths — the shortest arrow stays the shortest arrow. But it aligns everything to 's clean axes, where the algebra falls apart into something trivial.

PICTURE. Same target/plane as before, but now viewed after applying : the slanted column space becomes axis-aligned, and the residual measurement is unchanged (same red length).

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

Step 6 — Read off the answer:

WHAT. In the rotated view, part of lands inside the reachable space (the top coordinates, matched by ) and part lands outside it (the leftover bottom coordinates, which can never touch).

WHY. We can drive the reachable part exactly to zero by choosing so that equals the top part of . The unreachable bottom part is the irreducible error — the length of the perpendicular from Step 2. Nothing we do to shrinks it.

PICTURE. split into a "reachable" top block (green, cancellable) and an "unreachable" bottom block (red, the minimum residual). The best zeroes the green.

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

Step 7 — Edge and degenerate cases (never leave a scenario unshown)

WHAT & WHY. A derivation you can trust must survive the weird inputs.

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

The figure shows all three: (A) perpendicular meets a point → exact; (B) a collapsed axis → non-unique foot; (C) target on the plane → zero residual.


The one-picture summary

Figure — scipy.linalg — more stable than numpy.linalg, lu, qr, schur

The full journey on one canvas: target (orange) → drop the perpendicular to the column space (green foot, red residual) → rotate by so the space becomes axis-aligned → the reachable part is cancelled by , leaving the unreachable block as the minimum miss → solve the triangular by back-substitution.

Recall Feynman retelling — say it to a friend

You've got a target dot floating above a flat table. You can only place your pencil on the table, and you want to be as close to the target as possible. Obviously: stand directly beneath it — straight down. That "straight down" is the least-squares answer, and the leftover height is the error you simply can't remove.

Doing "straight down" with messy slanted table-legs is annoying. So we first straighten the table's edges into clean perpendicular rulers (that's ), writing down how the old edges were made from the new ones (that's ). Because rotating the whole scene with clean rulers never changes any distances, the closest-point problem stays identical but now reads off instantly: match the reachable part exactly, accept the unreachable leftover. What's left is a tiny triangular puzzle you solve by walking one row at a time.

The tempting shortcut — squaring into — secretly doubles your error, because it squares the condition number. QR refuses to do that. Same answer on paper, far more trustworthy on a computer. In practice you never hand-crank it: x, res, rank, sv = scipy.linalg.lstsq(A, b).

Prereqs revisited: Least Squares, Gram-Schmidt Orthogonalization, Condition Number, LAPACK and BLAS, numpy.linalg.