Linear Algebra You Can See, with NumPy
·
Course overview
1
Writing the world as lists of numbers: vectors and NumPy arrays
2
Vector = arrow = list of numbers: addition and scaling
3
Length, angle and the dot product
4
Linear combinations and span
5
A matrix is a rule for moving space
6
Matrix multiplication = one transformation after another
7
Determinants and inverses
8
Solving Ax = b
9
Column space, null space and rank
10
Changing basis
11
Eigenvalues and eigenvectors
12
Diagonalization and matrix powers
13
Orthogonality, projection and least squares
14
SVD: rotate, stretch, rotate
15
PCA, and the next step
Since Lesson 5 we have looked at a matrix as "a rule that moves space". Multiply by a matrix and almost every vector changes direction, and its length changes too. But many matrices hide special directions that do not change. A vector along such a direction stays on the same line after the transformation; only its length gets multiplied.
These special directions are called eigenvectors, and the factors are called eigenvalues. The names sound intimidating, but they are used everywhere. How a search engine ranks the importance of web pages (PageRank), the natural shapes in which a bridge or building vibrates, and principal component analysis (PCA), which we meet in Lesson 15, are all "finding the eigenvectors of some matrix". Eigenvalues also hold the answer to next lesson's question: "what happens if you repeat the same transformation 100 times?"
You caught a glimpse of the "eigenvector dashed lines" in the Lesson 5 lab. In this lesson you will find out for yourself what those lines are, then learn how to compute them by hand and with NumPy.
Suppose you multiply a vector by a matrix , and the result is exactly some multiple of the original vector.
A (nonzero) that satisfies this equation is called an eigenvector of , and the number is called an eigenvalue. In words, it means this.
Even after moves it, stays on its original line, and only its length becomes times as large.
| What the book writes | Read it as | Meaning | In Python |
|---|---|---|---|
| lambda | an eigenvalue: the factor the eigenvector is stretched by | vals[0] | |
| A v equals lambda v | v is an eigenvector of A with factor lambda | np.allclose(A @ v, lam * v) | |
| lambda one, lambda two | the first and second eigenvalues | vals[0], vals[1] | |
| I | the identity matrix (Lesson 6) | np.eye(2) | |
| det of A minus lambda I equals zero | the characteristic equation, which finds the eigenvalues | in 11-3 |
"Eigen" is German for "own" or "characteristic". Think of it as each matrix having directions of its own.
For example, let's feed a few vectors to .
went to , so its direction changed. But became 3 times as long in the same direction, and did not change at all. So the eigenvectors of this matrix are the direction (eigenvalue 3) and the direction (eigenvalue 1).
Find them yourself in the lab below. The teal arrow is v and the crimson arrow is Av. At first v = (1.5, 0.5), so it says "Not yet: v and Av are 17° apart". Drag the tip of the teal v until it lines up with the crimson Av on one line, and both arrows turn bold and the message tells you the factor too, like "Eigenvector! Av = 3·v".
Not yet: v and Av are 17° apart
Check it with NumPy
Each time you settle on a value in the lab, the server runs NumPy on the same numbers.
>>> A = np.array([[2, 1], [1, 2]])
>>> v = np.array([1.5, 0.5])
>>> A @ v
array([3.5, 2.5])
>>> vals, vecs = np.linalg.eig(A)
>>> np.round(vals, 6)
array([3.+0.j, 1.+0.j])
>>> np.round(vecs, 6)
array([[ 0.707107+0.j, -0.707107+0.j],
[ 0.707107+0.j, 0.707107+0.j]])
>>> np.round(A @ vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])
>>> np.round(vals[0] * vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])Press 'Spin v all the way around' and v travels around a circle of radius 1.5 while the crimson Av traces an ellipse. The points on the circle have been carried onto the ellipse by the matrix. The moments during the spin when v and Av line up on one line are exactly the eigenvector directions. With 'Symmetric', the directions of the ellipse's longest and shortest diameters are the two eigenvectors. You will meet this fact again with SVD in Lesson 14.
Turn on 'Answer (2×2 formula)' and the eigenvector directions are drawn as yellow dotted lines, along with the λ values and the eigenvectors scaled to length 1. How to compute this answer is the topic of 11-3.
One point worth stressing: change v from (1, 1) to (2, 2) or (−1, −1) and it is still an eigenvector. It is more accurate to think of an eigenvector not as "one arrow" but as one line through the origin. Every vector on that line except the zero vector is an eigenvector. The zero vector is left out because A0 = λ0 holds for any λ at all, so it tells you nothing.
Along an eigenvector direction, the matrix does only one thing: "multiply by ". So the value of alone tells you what happens in that direction.
| Value of | What happens in that direction | Example |
|---|---|---|
| stretched, same direction | : 3 times as long | |
| unchanged | an axis that stays put | |
| shrunk, same direction | : half as long | |
| squashed flat onto the origin | the null space direction from Lesson 9 | |
| flipped, then times as long | : reversed, as in a mirror |
The row is especially important. Since , an eigenvector with eigenvalue 0 is a direction of the null space you saw in Lesson 9. So if 0 is an eigenvalue, then , and there is no inverse (Lesson 7).
The lab's status message says "Eigenvector!" whenever v and Av lie on one line, whether they point the same way or opposite ways. That is because a direction with a negative eigenvalue is an eigenvector too.
Spinning v around in the lab gives you intuition, but to get exact values you need to calculate. The key is to tie together what you learned in earlier lessons into a single line.
Step 1: move everything to one side. In , move the right side to the left. Writing as lets you factor out .
Step 2: read it as a picture. A nonzero went through the matrix and vanished into the origin. That means squashes the plane flat (Lesson 9: the null space is more than just the origin). A matrix that squashes flat has determinant 0 (Lesson 7).
This is called the characteristic equation. The only unknown is , so you can solve it.
Step 3: compute it for 2×2. If , then , and the formula from Lesson 7 gives this.
, the sum of the diagonal entries, is called the trace, and is the determinant. So the two eigenvalues of a 2×2 matrix add up to the trace and multiply to the determinant. This is very handy for checking your work.
Example: . The trace is 4 and the determinant is .
Step 4: find an eigenvector for each eigenvalue. Plug in and you get , a system of equations (Lesson 8).
The two rows are the same equation, so there are infinitely many solutions (the "infinitely many" case from Lesson 8). That is how it should be, because an eigenvector is a whole line. Pick as a representative. In the same way, gives , so . This matches exactly what you got by multiplying directly in 11-1.
Check: (the trace), (the determinant). Correct.
Triangular matrices are even easier. For the upper triangular , the characteristic equation is , so the eigenvalues are just the diagonal entries, 3 and 2.
The four matrices in the lab were picked on purpose to behave differently. Switch the chips one by one and compare with the table below.
| Chip | Matrix | Characteristic equation | Eigenvalues | Eigenvectors |
|---|---|---|---|---|
| Symmetric | 3, 1 | , : perpendicular | ||
| Upper triangular | 3, 2 | , : not perpendicular | ||
| Rotation | no real solutions | none | ||
| Shear | 1 (twice) | only the direction |
Rotation. A 90° rotation turns every vector by 90°, so no vector can keep its direction. However you turn v in the lab, the message stays at "90° apart", and the path of Av is a circle, not an ellipse. The characteristic equation has no real solutions; you need the imaginary number to solve it, giving . So you can read it this way: when the eigenvalues come out complex, the matrix has a rotation in it.
Shear. A horizontal shear does not move the points on the x-axis. So the direction is an eigenvector with eigenvalue 1. Every other direction gets tilted to the right, though, so there is only one eigenvector line. A matrix like this, where the eigenvalue 1 appears "twice" but there is only one line, cannot describe the whole plane with eigenvectors alone. It is the classic case where next lesson's diagonalization does not work.
Symmetric. A matrix that equals its own (the transpose from Lesson 5) is called a symmetric matrix. A symmetric matrix always has real eigenvalues, and eigenvectors for different eigenvalues are always perpendicular. You can check it: . Covariance matrices from data are symmetric, which is why PCA in Lesson 15 relies on exactly this property.
The "Check it with NumPy" panel under the lab starts out with this code. Type it in line by line as you read along.
>>> import numpy as np
>>> A = np.array([[2, 1], [1, 2]])
>>> v = np.array([1.5, 0.5])
>>> A @ v
array([3.5, 2.5])A @ v is the matrix-vector product from Lesson 5. (1.5, 0.5) went to (3.5, 2.5). Since 3.5 / 1.5 and 2.5 / 0.5 are different, it was not "stretched by one common factor", so it is not an eigenvector.
The eigenvalues and eigenvectors come out together from a single call to np.linalg.eig. There are two results, so you receive them under two names, as in vals, vecs = … (Python's tuple unpacking). The lab's NumPy panel wraps the next lines in np.round(…, 6) to make them easier to read, rounding to six decimal places.
>>> vals, vecs = np.linalg.eig(A)
>>> np.round(vals, 6)
array([3.+0.j, 1.+0.j])
>>> np.round(vecs, 6)
array([[ 0.707107+0.j, -0.707107+0.j],
[ 0.707107+0.j, 0.707107+0.j]])np.round(array, 6) gives a new array with every element rounded to six decimal places. The original vals and vecs are not changed. Here are the unrounded values.
>>> vals
array([3.+0.j, 1.+0.j])
>>> vecs
array([[ 0.70710678+0.j, -0.70710678+0.j],
[ 0.70710678+0.j, 0.70710678+0.j]])There are three new things here.
① +0.j: complex numbers. Python writes the imaginary unit as j. 3.+0.j means "real part 3, imaginary part 0", which is just 3. NumPy 2.5's eig does not assume the matrix is symmetric, so it allows for complex eigenvalues like a rotation matrix's and always returns the answer as a complex array. If you only want the real part, add .real.
>>> vals.dtype
dtype('complex128')
>>> vals.real
array([3., 1.])② The columns of vecs are the eigenvectors. vecs is a 2×2 matrix; column 0 is the eigenvector for vals[0], and column 1 is the eigenvector for vals[1]. Take them out with M[:, 0] from Lesson 4 (slot 0 of every row = column 0). vecs[0] is row 0, which is something else entirely.
>>> vecs[:, 0]
array([0.70710678+0.j, 0.70710678+0.j])
>>> vecs[0]
array([ 0.70710678+0.j, -0.70710678+0.j])③ They are scaled to length 1. You got (0.7071…, 0.7071…) instead of the (1, 1) you found by hand because NumPy normalizes eigenvectors into unit vectors (Lesson 3). 0.70710678 is 1/√2. Every vector on the same line is an eigenvector, so divide by the first component and you get back the shape you found by hand.
>>> 1 / np.sqrt(2)
np.float64(0.7071067811865475)
>>> vecs[:, 0] / vecs[0, 0]
array([1.+0.j, 1.+0.j])
>>> np.linalg.norm(vecs[:, 0])
np.float64(0.9999999999999999)
>>> np.isclose(np.linalg.norm(vecs[:, 0]), 1)
np.True_The length came out as 0.9999999999999999, not exactly 1. That is the floating-point error from Lesson 3. Compare results of real-number calculations with np.isclose instead of ==.
Notice also that column 1 is (−0.707, 0.707) rather than (1, −1), the opposite sign from what you found by hand. It is on the same line, so both are correct answers. The sign of an eigenvector is not fixed. It can differ between programs and versions, so avoid writing code that expects the signs to match.
The last two lines of the NumPy panel test the definition directly. On the left, A @ vecs[:, 0] is Av; on the right, vals[0] * vecs[:, 0] is λv. The panel wraps both in np.round(…, 6).
>>> np.round(A @ vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])
>>> np.round(vals[0] * vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])
>>> A @ vecs[:, 0]
array([2.12132034+0.j, 2.12132034+0.j])
>>> vals[0] * vecs[:, 0]
array([2.12132034+0.j, 2.12132034+0.j])
>>> np.allclose(A @ vecs[:, 0], vals[0] * vecs[:, 0])
TrueThe rounded 2.12132 (that is 2.121320 with the trailing 0 dropped) and the original 2.12132034 are the same value with a different number of digits. Rather than comparing the two sides by eye, in code you check with np.allclose, as in the last line.
You can also check both eigenvectors at once. vecs * vals uses broadcasting (Lesson 2) to multiply column 0 by vals[0] and column 1 by vals[1]. So it must equal A @ vecs.
>>> np.allclose(A @ vecs, vecs * vals)
Truenp.linalg.eighWhen you know the matrix is symmetric, use eigh. It is made only for symmetric matrices, so it is faster and more stable, and it returns a real array with the eigenvalues sorted from smallest to largest (ascending).
>>> vals2, vecs2 = np.linalg.eigh(A)
>>> vals2
array([1., 3.])
>>> vecs2
array([[-0.70710678, 0.70710678],
[ 0.70710678, 0.70710678]])
>>> vecs2[:, 0] @ vecs2[:, 1]
np.float64(0.0)eig made no promise about order (this time it was 3, 1), but eigh gives 1, 3. The last line confirms that the dot product of the two eigenvectors is 0, which means they are perpendicular, as 11-4 said. Remember that the largest eigenvalue is at the end (vals2[-1]). PCA in Lesson 15 uses exactly that.
>>> R = np.array([[0, -1], [1, 0]])
>>> np.linalg.eigvals(R)
array([0.+1.j, 0.-1.j])When you only need the eigenvalues, use np.linalg.eigvals. The rotation matrix gives 1.j and -1.j, that is ±i, as 11-4 said. This time the imaginary part is not 0, so you must not throw it away with .real.
>>> S = np.array([[1, 1], [0, 1]])
>>> vals, vecs = np.linalg.eig(S)
>>> vals
array([1.+0.j, 1.+0.j])
>>> vecs
array([[ 1.00000000e+00+0.j, -1.00000000e+00+0.j],
[ 0.00000000e+00+0.j, 2.22044605e-16+0.j]])For the shear matrix NumPy returns two columns, but the second is (−1, 0.000…0002), which is practically on the same line as the first. 2.22044605e-16 is 2.22 × 10⁻¹⁶, a zero at the level of floating-point error. Getting two columns from eig does not mean there are two eigenvector directions. You can check whether the two columns are linearly independent with the rank from Lesson 4.
>>> np.linalg.matrix_rank(vecs)
np.int64(1)The rank is 1, so the eigenvectors form only one line. If you pick 'Shear' in the lab and turn on 'Answer (2×2 formula)', only one yellow dotted line is drawn.
With the upper triangular matrix, let's check "add up to the trace, multiply to the determinant" from 11-3. np.trace is the sum of the diagonal entries.
>>> B = np.array([[3, 1], [0, 2]])
>>> vals = np.linalg.eigvals(B)
>>> vals
array([3.+0.j, 2.+0.j])
>>> np.trace(B)
np.int64(5)
>>> np.linalg.det(B)
np.float64(6.0)
>>> vals.sum()
np.complex128(5+0j)
>>> vals.prod()
np.complex128(6+0j)np.complex128(5+0j) is also just 5. The sum matches the trace 5, and the product matches the determinant 6.
Taking an eigenvector as a row. vecs[0] is not an eigenvector. Eigenvectors are columns, so it is vecs[:, 0]. As you saw above, with a symmetric matrix vecs[0] happens to produce plausible-looking numbers, which makes the mistake hard to notice. When in doubt, check with np.allclose(A @ v, lam * v).
Using eigh on a matrix that is not symmetric. eigh trusts that the matrix is symmetric and reads only its lower triangle. Give it a non-symmetric matrix and it returns a wrong answer without any error. The true eigenvalues of the matrix below are 5 and 2, but eigh gives something unrelated.
>>> C = np.array([[4, 1], [2, 3]])
>>> np.linalg.eigvals(C).real
array([5., 2.])
>>> np.linalg.eigh(C).eigenvalues
array([1.43844719, 5.56155281])The results of eig and eigh are named tuples, so you can also take them out with .eigenvalues and .eigenvectors. If you are not sure whether a matrix is symmetric, check first with np.allclose(C, C.T), and if it is not, use eig.
Non-square matrices. Eigenvalues ask "does a direction stay the same within the same space", so only square matrices have them.
>>> np.linalg.eig(np.array([[1, 2, 3], [4, 5, 6]]))
Traceback (most recent call last):
...
numpy.linalg.LinAlgError: Last 2 dimensions of the array must be squareThe tool that does a similar job for non-square matrices is SVD, in Lesson 14.
@ to check that .Question 1: the trace is and the determinant is , so , that is , giving , . Check: , .
Question 2: for , the first row of is , so . For , the first row is , so .
>>> C @ np.array([1, 1])
array([5, 5])
>>> C @ np.array([1, -2])
array([ 2, -4])
>>> D = np.array([[2, 0], [0, 0.5]])
>>> vals, vecs = np.linalg.eig(D)
>>> vals.real
array([2. , 0.5])
>>> vecs.real
array([[1., 0.],
[0., 1.]])(5, 5) = 5·(1, 1) and (2, −4) = 2·(1, −2), so both are eigenvectors.
Question 3: a diagonal matrix "stretches and shrinks along the axes", doubling the x direction and halving the y direction. So the eigenvalues are the diagonal entries 2 and 0.5, and the eigenvectors are the standard basis î = (1, 0) and ĵ = (0, 1). Next lesson turns this around and uses the fact that any matrix looks like a diagonal matrix like this when seen from its eigenvector directions.
Lesson recap
An eigenvector is a direction that stays on the same line after the matrix, and the eigenvalue λ is the factor in that direction: Av = λv.
An eigenvector is a line through the origin. Every vector on it (except the zero vector) is an eigenvector, and its sign is not fixed.
Find eigenvalues with the characteristic equation det(A − λI) = 0. For 2×2 it is λ² − (trace)λ + det = 0.
A rotation has no real eigenvectors (complex eigenvalues), a shear has only one, and a symmetric matrix has perpendicular eigenvectors.
vals, vecs = np.linalg.eig(A): the eigenvectors are vecs[:, i] (columns!). NumPy 2.5's eig always returns complex numbers, so use .real when needed.
For symmetric matrices use np.linalg.eigh: real, ascending. Use it on a non-symmetric matrix and you silently get a wrong answer.
In the next lesson you will use the eigenvectors as a basis (Lesson 10) and learn diagonalization, which turns a matrix into "stretching along the axes". Then multiplying by a matrix 100 times becomes raising a few numbers to the 100th power, and you will see why the golden ratio pops out of the Fibonacci sequence.
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.