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
Time 4:10–5:00
Before a game character is drawn on screen, each of its corner points is moved several times. It is stretched to the character's size, turned to face the way the character is looking, and turned again to match the camera's view. If there are a million corners, do you have to do three transformations a million times each, three million calculations in all?
You don't. Doing two linear transformations one after another gives another linear transformation, so it can be written as a single matrix. Combine the three transformations into one matrix ahead of time, and each corner needs only one multiplication. That is how graphics engines actually work.
The calculation that combines two matrices into the matrix of "one after the other" is matrix multiplication. In this lesson you won't start by memorizing a formula. Instead you will derive matrix multiplication from the picture in Lesson 5 ("the columns are where î and ĵ land"). Then the formula, and the rule that "you can't swap the order", make sense on their own.
Do transformation first, then transformation on the result, and a vector moves like this.
When is done, the grid lines are still straight, parallel and evenly spaced. Applying to that grid keeps them that way too. So the two steps together are also a linear transformation, and by Lesson 5 they can be written as a single matrix. We write that matrix .
There is one thing here you really need to get used to. The transformation done first, , goes on the right. You read starting from the side nearest the vector , that is, from right to left. It is the same as function composition from math class meaning "first , then ". In Python it is A(B(v)), where the inner parentheses are evaluated first.
In the lab below, the defaults are Rotate 90° for "First transformation B" and Shear for "Then transformation A". B and A sit side by side on the right, and below them the "B first, then A = AB" box shows the combined matrix.
Swapping the order changes the result: AB ≠ BA
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([[1, 1], [0, 1]])
>>> B = np.array([[0, -1], [1, 0]])
>>> A @ B
array([[ 1, -1],
[ 1, 0]])
>>> B @ A
array([[ 0, -1],
[ 1, 1]])
>>> np.allclose(A @ B, B @ A)
False
>>> A * B
array([[ 0, -1],
[ 0, 0]])Press 'Play in order' and the grid first turns 90° counterclockwise (B), pauses briefly, then gets pushed sideways at a slant (A). In the final picture, teal î is at (1, 1) and crimson ĵ is at (−1, 0). As you learned in Lesson 5, stand these two up as columns side by side and you have AB.
Let's follow the numbers to make sure.
So column 1 of AB is (1, 1) and column 2 is (−1, 0), the same as the box on the right of the lab and A @ B in the NumPy panel.
Put what you just did into general words and you have the recipe for matrix multiplication.
Column 1 of is "where is after ", so applying to it gives the final landing spot. In one sentence: the columns of are times (the columns of ).
Call column 1 of and column 2 , and it looks like this.
Let's apply it to the example from 6-1. Compute by combining columns, as in Lesson 5.
By hand, Lesson 5's "dot products with the rows" is faster. The entry in row , column of is the dot product of row of with column of . For a general it looks like this.
For example, the top-left entry is the dot product of row 1 of , , with column 1 of , . A handy way to remember it: your left finger runs across a row of , your right finger runs down a column of , and you multiply and add as you go.
Matrices that are not square can be multiplied too. There is one condition: the number of columns of must equal the number of rows of . To take the dot product of a row and a column, the two must have the same length.
The two inner numbers must match, and the result has the shape of the two outer numbers, . In Lesson 5, multiplying a matrix by the matrix holding 4 corners and getting a followed this rule.
Turn on 'Swap the order (A first, then B)' in the lab and the "A first, then B = BA" box appears. Shear first and rotate afterward, and î goes to (0, 1) and ĵ to (−1, 1), so the result is [[0, −1], [1, 1]]. That is different from the earlier [[1, −1], [1, 0]]. The status message also changes to "Swapping the order changes the result: AB ≠ BA".
With numbers, 2 × 3 = 3 × 2, but with matrices swapping the order usually changes the result. It happens all the time in everyday life. Putting on socks and then shoes is not the same as putting on shoes and then socks. In photo editing, "rotate, then stretch sideways" and "stretch sideways, then rotate" also give different pictures.
Here are all the pairs you can make with the lab's five chips.
| The pair B and A | Swap the order and it is | Why |
|---|---|---|
| Identity + anything | the same | 'Identity' does nothing |
| Stretch sideways + Mirror left-right | the same | both only touch the horizontal direction |
| Rotate 90° + Rotate 90° | the same | a transformation always commutes with itself |
| Rotate 90° + Stretch sideways | different | rotate first, and the stretch hits what was the vertical direction |
| Rotate 90° + Shear | different | the default example |
| Rotate 90° + Mirror left-right | different | after a mirror, the turn goes the opposite way |
| Stretch sideways + Shear | different | whether the amount of shear gets stretched depends on the order |
| Shear + Mirror left-right | different | the direction of the shear flips depending on the order |
Some pairs give the same result in either order, but those are special cases. When you meet a matrix product, assume by default that "you can't swap the order".
The identity matrix is the lab's 'Identity', the transformation that changes nothing. goes to and goes to , so its columns are and . Whether you do a do-nothing transformation before or after, the result is the same.
plays the role for matrices that 1 plays in multiplying numbers. In Lesson 7 the inverse is defined as "the matrix that gives when you multiply by it".
What about three transformations? Doing first, then , and finally is . Whether you compute it as or as , the result is the same. This is the associative law.
Wherever you put the parentheses, the order "first , then , finally " stays the same. The only thing that changes is "which two you combine first". Keep these apart: you must not swap the order ( versus ), but you may move the parentheses freely. This is exactly the graphics engine trick from the introduction. Instead of computing for every corner, you build once and then only compute .
Let's widen the lab's 'Rotate 90°' to a general angle . We use from Lesson 3 and its partner . Take an arrow of length 1 from the origin and turn it counterclockwise by from the horizontal axis; its tip is at .
Stand the landing spots up as columns side by side and you have the rotation matrix.
Put in : since and , you get the lab's .
Rotate twice and the angles add. Turning by and then by more is turning by .
Choose 'Rotate 90°' for both B and A in the lab and the result is , a 180° rotation. This formula also shows right away that rotations give the same result in either order, because .
The lab's NumPy panel builds A and B, then shows A @ B, B @ A, np.allclose(A @ B, B @ A) and A * B in turn. Let's try them one line at a time.
@: the matrix productThe @ you used in Lesson 5 to multiply a matrix by a vector also works for multiplying matrices together.
>>> import numpy as np
>>> A = np.array([[1, 1], [0, 1]]) # shear
>>> B = np.array([[0, -1], [1, 0]]) # rotate 90°
>>> A @ B
array([[ 1, -1],
[ 1, 0]])
>>> B @ A
array([[ 0, -1],
[ 1, 1]])A @ B means "first B, then A". The Python code is written in the same order as the book's notation, so you read it the same way, from the right. Everything after # is a Python comment and does not affect what runs.
Let's also check 6-2's "the columns of AB are A times (the columns of B)". Take the columns of B out with B[:, 0] from Lesson 5, move them with A, stack them back as columns with np.column_stack, and you get A @ B.
>>> np.column_stack([A @ B[:, 0], A @ B[:, 1]])
array([[ 1, -1],
[ 1, 0]])Moving a vector in two steps and moving it once with the combined matrix also give the same thing.
>>> v = np.array([2, 1])
>>> A @ (B @ v)
array([1, 2])
>>> (A @ B) @ v
array([1, 2])@ does the same job as the function np.matmul. In older code you will also often see np.dot(A, B). For 2-D arrays, all three give the same result.
>>> np.matmul(A, B)
array([[ 1, -1],
[ 1, 0]])np.allclose: are two matrices equal?Write A @ B == B @ A and you get a matrix of True/False, one comparison per slot. When you want to know "are they all equal?" in one go, use np.allclose. It applies Lesson 3's np.isclose to every slot and returns True only if all of them are True.
>>> A @ B == B @ A
array([[False, True],
[ True, False]])
>>> np.allclose(A @ B, B @ A)
FalseFor integer matrices you could compare exactly with np.array_equal, but once decimals are involved, as with rotation matrices, tiny errors appear, so np.allclose is the safe choice. You will see an example shortly.
* is not the matrix productThe last line of the lab's NumPy panel, A * B, is a trap put there on purpose. * is the element-wise product you have used since Lesson 2: it multiplies the numbers in the same position.
>>> A * B
array([[ 0, -1],
[ 0, 0]])The top left is 1 × 0 = 0 and the top right is 1 × (−1) = −1; it only multiplied slot by slot, which has nothing to do with doing one transformation after another. If you pick 'Mirror left-right' for B and 'Stretch sideways' for A in the lab, A * B and A @ B happen to come out the same. That is because both matrices are 0 everywhere off the diagonal. Coincidences like this can let a wrong * slip through your tests, so always write matrix products with @.
np.eye: the identity matrixYou make an identity matrix with np.eye(n). The name comes from how "I", the letter for identity, sounds: "eye".
>>> I = np.eye(2)
>>> I
array([[1., 0.],
[0., 1.]])
>>> A @ I
array([[1., 1.],
[0., 1.]])
>>> np.allclose(A @ I, A)
Truenp.eye makes a real-number (float64) array by default, so A @ I also comes out as real numbers like 1.. The values are the same as A. If you need integers, write np.eye(2, dtype=int).
Let's turn the rotation matrix from 6-5 into a function. NumPy's np.cos and np.sin take angles in radians, not degrees (°). So we convert first with np.radians (the opposite direction of the np.degrees you used in Lesson 3).
>>> def rot(deg):
... t = np.radians(deg)
... return np.array([[np.cos(t), -np.sin(t)],
... [np.sin(t), np.cos(t)]])
>>> rot(30)
array([[ 0.8660254, -0.5 ],
[ 0.5 , 0.8660254]])
>>> rot(90)
array([[ 6.123234e-17, -1.000000e+00],
[ 1.000000e+00, 6.123234e-17]])In rot(90), 6.123234e-17 showed up where there should be a 0. e-17 means "times 10 to the power −17", so this is about 0.00000000000000006, zero for all practical purposes. It is the floating-point error from Lesson 3, which appears because the computer cannot hold π exactly. With the error mixed in, the numbers have wildly different sizes, so NumPy shows every slot in e notation to keep them aligned. To see it nicely, round with np.round.
>>> np.round(rot(90), 10)
array([[ 0., -1.],
[ 1., 0.]])Now check "turn 30° and then 60° more = turn 90°" and "90° twice = 180°". Because of the error you cannot check this with ==, so use np.allclose.
>>> np.allclose(rot(60) @ rot(30), rot(90))
True
>>> np.allclose(rot(90) @ rot(90), rot(180))
True
>>> np.allclose(rot(60) @ rot(30), rot(30) @ rot(60))
TrueThe last line is the conclusion from 6-5: rotations give the same result in either order.
Chain @ several times and Python computes from the left, as (A @ B) @ C. Thanks to the associative law, the result is the same as A @ (B @ C).
>>> C = np.array([[2, 0], [0, 1]]) # stretch sideways
>>> A @ B @ C
array([[ 2, -1],
[ 2, 0]])
>>> np.array_equal((A @ B) @ C, A @ (B @ C))
TrueA @ B @ C means "first C (stretch sideways), then B (rotate 90°), finally A (shear)". Build the combined matrix once, and you can move a matrix holding many points in one go, like the square's corners in Lesson 5.
>>> M = A @ B @ C
>>> square = np.array([[0, 1, 1, 0],
... [0, 0, 1, 1]])
>>> M @ square
array([[ 0, 2, 1, -1],
[ 0, 2, 2, 0]])Writing the order backwards. Write "rotate first, then shear" in the order you say it, as rotate @ shear, and you get the opposite order. What happens first goes on the right: shear @ rotate. If you get confused, attach a vector v at the right end and read it as shear @ (rotate @ v). Whatever is closest to v happens first.
* instead of @. As you saw in 6-6, it gives a wrong answer without any error. For matrices that are 0 off the diagonal it happens to give the right answer, which makes the mistake even harder to spot.
Multiplying mismatched shapes. If the number of columns of the first matrix differs from the number of rows of the second, you cannot multiply them. np.ones((2, 3)) is a 2 × 3 matrix filled with 1s.
>>> X = np.ones((2, 3))
>>> (X @ np.ones((3, 4))).shape
(2, 4)
>>> X @ X
Traceback (most recent call last):
...
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)(2 × 3)(3 × 4) has matching inner numbers, 3, so it becomes (2 × 4). But (2 × 3)(2 × 3) has inner numbers 3 and 2, which differ, so you get an error. That is what (size 2 is different from 3) at the end of the message means.
[[1, 1], [0, 1]], stretch sideways is [[2, 0], [0, 1]].)[[-1, 0], [0, 1]] twice? Predict it from the picture first.np.allclose to check whether rot(45) multiplied by itself equals rot(90).>>> shear = np.array([[1, 1], [0, 1]])
>>> stretch = np.array([[2, 0], [0, 1]])
>>> stretch @ shear
array([[2, 2],
[0, 1]])
>>> shear @ stretch
array([[2, 1],
[0, 1]])
>>> flip = np.array([[-1, 0], [0, 1]])
>>> flip @ flip
array([[1, 0],
[0, 1]])
>>> np.allclose(rot(45) @ rot(45), rot(90))
True
>>> (np.ones((3, 2)) @ np.ones((2, 5))).shape
(3, 5)For question 1: the shear happens first, so it goes on the right: stretch @ shear. Follow î and ĵ as in 6-2. î = (1, 0) is unchanged by the shear and becomes (2, 0) under the stretch. ĵ = (0, 1) becomes (1, 1) under the shear, and the stretch doubles only its horizontal component, giving (2, 1). Read the output [[2, 2], [0, 1]] downward and column 1 is (2, 0) and column 2 is (2, 1), a perfect match. In the swapped shear @ stretch, ĵ meets the stretch first, but its horizontal component is 0, so it stays put, and then the shear takes it to (1, 1). So column 2 is (1, 1), and the amount of shear is not doubled.
For question 2: reflecting a mirror image again brings it back, so you get the identity matrix I.
For question 3: 45° + 45° = 90°, so it is True.
For question 4: the inner numbers, 2, match, so the result is 3 × 5. Swap the order and you get (2 × 5)(3 × 2), whose inner numbers 5 and 3 differ, so you cannot multiply. With matrices of different shapes, swapping the order can make the product impossible altogether.
Lesson recap
Doing B first and then A is also a linear transformation, written as the matrix AB. What happens first is on the right: read from the right.
The columns of AB are A times (the columns of B). By hand, the entry in row i, column j of AB is the dot product of row i of A with column j of B.
Usually AB ≠ BA (socks and shoes). But (AB)C = A(BC): you may move the parentheses, but you must not swap the order.
The identity matrix I is 'Identity' (AI = IA = A), and rotation matrices satisfy R(α)R(β) = R(α + β).
NumPy: the matrix product is @ (= np.matmul), * is element-wise. The identity is np.eye; compare with np.allclose when there is rounding error.
In the next lesson you will learn the determinant, which tells you how many times a transformation scales areas, and the inverse, which undoes a transformation. It is the story of finding "the matrix that gives I when you multiply by it".
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.