R

ReflexKR

Reflex Docs

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

Linear Algebra You Can See, with NumPy
Lesson 6

Time 4:10–5:00

Matrix multiplication = one transformation after another

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.

6-1. Two transformations, one after another (6 min)

Do transformation BB first, then transformation AA on the result, and a vector v\mathbf{v} moves like this.

v → B  Bv → A  A(Bv)\mathbf{v} \ \xrightarrow{\ B\ } \ B\mathbf{v} \ \xrightarrow{\ A\ } \ A(B\mathbf{v})

When BB is done, the grid lines are still straight, parallel and evenly spaced. Applying AA 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 ABAB.

A(Bv)=(AB)vA(B\mathbf{v}) = (AB)\mathbf{v}

There is one thing here you really need to get used to. The transformation done first, BB, goes on the right. You read ABvAB\mathbf{v} starting from the side nearest the vector v\mathbf{v}, that is, from right to left. It is the same as function composition f(g(x))f(g(x)) from math class meaning "first gg, then ff". 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.

îĵ

First transformation B

Then transformation A

B01-10A1011

B first, then A = AB

11-10

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' to watch the grid move in two steps, I → B → AB. Then turn on 'Swap the order (A first, then B)', play again and see whether the result matrix changes, and switch the chips around to find pairs that give the same result in either order.

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.

  • î = (1, 0) becomes (0, 1) under B (Rotate 90°), then (1, 1) under A (Shear). A shear pushes a point that is 1 step up 1 step to the right.
  • ĵ = (0, 1) becomes (−1, 0) under B, and stays (−1, 0) under A. Points on the floor (the horizontal axis) do not move under a shear.

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.

6-2. Computing a matrix product (9 min)

Put what you just did into general words and you have the recipe for matrix multiplication.

  • Column 1 of ABAB = where ı^\hat{\imath} lands after both steps = (column 1 of BB) moved by AA
  • Column 2 of ABAB = where ȷ^\hat{\jmath} lands after both steps = (column 2 of BB) moved by AA

Column 1 of BB is "where ı^\hat{\imath} is after BB", so applying AA to it gives the final landing spot. In one sentence: the columns of ABAB are AA times (the columns of BB).

Call column 1 of BB b1\mathbf{b}_1 and column 2 b2\mathbf{b}_2, and it looks like this.

B=[b1b2]⟹AB=[Ab1Ab2]B = \begin{bmatrix} \mathbf{b}_1 & \mathbf{b}_2 \end{bmatrix} \quad \Longrightarrow \quad AB = \begin{bmatrix} A\mathbf{b}_1 & A\mathbf{b}_2 \end{bmatrix}

Let's apply it to the example from 6-1. Compute Ab1A\mathbf{b}_1 by combining columns, as in Lesson 5.

A=[1101],B=[0−110]A = \begin{bmatrix} 1 & 1 \\ 0 & 1 \end{bmatrix}, \quad B = \begin{bmatrix} 0 & -1 \\ 1 & 0 \end{bmatrix} Ab1=0[10]+1[11]=[11],Ab2=−1[10]+0[11]=[−10]A\mathbf{b}_1 = 0 \begin{bmatrix} 1 \\ 0 \end{bmatrix} + 1 \begin{bmatrix} 1 \\ 1 \end{bmatrix} = \begin{bmatrix} 1 \\ 1 \end{bmatrix}, \qquad A\mathbf{b}_2 = -1 \begin{bmatrix} 1 \\ 0 \end{bmatrix} + 0 \begin{bmatrix} 1 \\ 1 \end{bmatrix} = \begin{bmatrix} -1 \\ 0 \end{bmatrix} AB=[1−110]AB = \begin{bmatrix} 1 & -1 \\ 1 & 0 \end{bmatrix}

By hand, Lesson 5's "dot products with the rows" is faster. The entry in row ii, column jj of ABAB is the dot product of row ii of AA with column jj of BB. For a general 2×22 \times 2 it looks like this.

[abcd][efgh]=[ae+bgaf+bhce+dgcf+dh]\begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} e & f \\ g & h \end{bmatrix} = \begin{bmatrix} ae + bg & af + bh \\ ce + dg & cf + dh \end{bmatrix}

For example, the top-left entry ae+bgae + bg is the dot product of row 1 of AA, (a,b)(a, b), with column 1 of BB, (e,g)(e, g). A handy way to remember it: your left finger runs across a row of AA, your right finger runs down a column of BB, and you multiply and add as you go.

The shape rule

Matrices that are not square can be multiplied too. There is one condition: the number of columns of AA must equal the number of rows of BB. To take the dot product of a row and a column, the two must have the same length.

A⏟m×n B⏟n×p=AB⏟m×p\underbrace{A}_{m \times n} \ \underbrace{B}_{n \times p} = \underbrace{AB}_{m \times p}

The two inner numbers nn must match, and the result has the shape of the two outer numbers, m×pm \times p. In Lesson 5, multiplying a 2×22 \times 2 matrix by the 2×42 \times 4 matrix holding 4 corners and getting a 2×42 \times 4 followed this rule.

6-3. Order matters: AB ≠ BA (5 min)

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 ASwap the order and it isWhy
Identity + anythingthe same'Identity' does nothing
Stretch sideways + Mirror left-rightthe sameboth only touch the horizontal direction
Rotate 90° + Rotate 90°the samea transformation always commutes with itself
Rotate 90° + Stretch sidewaysdifferentrotate first, and the stretch hits what was the vertical direction
Rotate 90° + Sheardifferentthe default example
Rotate 90° + Mirror left-rightdifferentafter a mirror, the turn goes the opposite way
Stretch sideways + Sheardifferentwhether the amount of shear gets stretched depends on the order
Shear + Mirror left-rightdifferentthe 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".

6-4. The identity matrix and associativity (4 min)

The identity matrix II is the lab's 'Identity', the transformation that changes nothing. ı^\hat{\imath} goes to ı^\hat{\imath} and ȷ^\hat{\jmath} goes to ȷ^\hat{\jmath}, so its columns are (1,0)(1, 0) and (0,1)(0, 1). Whether you do a do-nothing transformation before or after, the result is the same.

I=[1001],AI=IA=AI = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}, \qquad AI = IA = A

II plays the role for matrices that 1 plays in multiplying numbers. In Lesson 7 the inverse is defined as "the matrix that gives II when you multiply by it".

What about three transformations? Doing CC first, then BB, and finally AA is ABCABC. Whether you compute it as (AB)C(AB)C or as A(BC)A(BC), the result is the same. This is the associative law.

(AB)C=A(BC)(AB)C = A(BC)

Wherever you put the parentheses, the order "first CC, then BB, finally AA" stays the same. The only thing that changes is "which two you combine first". Keep these apart: you must not swap the order (ABAB versus BABA), but you may move the parentheses freely. This is exactly the graphics engine trick from the introduction. Instead of computing A(B(Cv))A(B(C\mathbf{v})) for every corner, you build M=ABCM = ABC once and then only compute MvM\mathbf{v}.

6-5. Rotation matrices (5 min)

Let's widen the lab's 'Rotate 90°' to a general angle θ\theta. We use cos⁡θ\cos\theta from Lesson 3 and its partner sin⁡θ\sin\theta. Take an arrow of length 1 from the origin and turn it counterclockwise by θ\theta from the horizontal axis; its tip is at (cos⁡θ,sin⁡θ)(\cos\theta, \sin\theta).

  • Turn ı^=(1,0)\hat{\imath} = (1, 0) by θ\theta and you get (cos⁡θ,sin⁡θ)(\cos\theta, \sin\theta)
  • ȷ^=(0,1)\hat{\jmath} = (0, 1) is 90° ahead of ı^\hat{\imath}, so turning it gives (−sin⁡θ,cos⁡θ)(-\sin\theta, \cos\theta)

Stand the landing spots up as columns side by side and you have the rotation matrix.

R(θ)=[cos⁡θ−sin⁡θsin⁡θcos⁡θ]R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}

Put in θ=90°\theta = 90°: since cos⁡90°=0\cos 90° = 0 and sin⁡90°=1\sin 90° = 1, you get the lab's [0−110]\begin{bmatrix} 0 & -1 \\ 1 & 0 \end{bmatrix}.

Rotate twice and the angles add. Turning by β\beta and then by α\alpha more is turning by α+β\alpha + \beta.

R(α) R(β)=R(α+β),R(θ) R(θ)=R(2θ)R(\alpha)\,R(\beta) = R(\alpha + \beta), \qquad R(\theta)\,R(\theta) = R(2\theta)

Choose 'Rotate 90°' for both B and A in the lab and the result is [−100−1]\begin{bmatrix} -1 & 0 \\ 0 & -1 \end{bmatrix}, a 180° rotation. This formula also shows right away that rotations give the same result in either order, because α+β=β+α\alpha + \beta = \beta + \alpha.

6-6. Matrix multiplication in NumPy (11 min)

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 product

The @ 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)
False

For 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 product

The 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 matrix

You 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)
True

np.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).

Building a rotation matrix

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))
True

The last line is the conclusion from 6-5: rotations give the same result in either order.

Multiplying three or more

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))
True

A @ 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]])

6-7. Common mistakes (3 min)

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.

Exercises (7 min)

  1. Work out the matrix for "shear first, then stretch sideways" by hand, then check with NumPy. How does the result change if you swap the order? (Shear is [[1, 1], [0, 1]], stretch sideways is [[2, 0], [0, 1]].)
  2. What matrix do you get by doing the left-right mirror [[-1, 0], [0, 1]] twice? Predict it from the picture first.
  3. Use np.allclose to check whether rot(45) multiplied by itself equals rot(90).
  4. Multiply a 3 × 2 matrix by a 2 × 5 matrix: what shape is the result? Can you swap the order and multiply a 2 × 5 matrix by a 3 × 2 matrix?

Solutions

>>> 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".

← Previous

Lesson 5 · A matrix is a rule for moving space

Next →

Lesson 7 · Determinants and inverses

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

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
한국어English

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex