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 12

Time 9:10–10:00

Diagonalization and matrix powers

Repeating the same rule over and over is more common than you might think. You predict tomorrow's weather from today's, then the day after from that prediction. You compute next month's active and dormant users from this month's, then the month after that. Game economies, population changes and web page rankings are all "multiplying a vector by the same matrix again and again", that is, Aⁿx.

The trouble starts when n is large. Multiplying by a matrix 100 times is a lot of work, and it is hard to tell in advance where the result is heading. The key to this lesson is to use the eigenvectors from Lesson 11 as a basis (Lesson 10). Seen from the eigenvector directions, the matrix is just "stretching along each axis", and stretching 100 times only takes raising a number to the 100th power.

This one idea also explains why the golden ratio comes out of the Fibonacci sequence, and why the weather settles into fixed proportions in the long run. At the end you will write power iteration yourself, a way to find an eigenvalue by nothing more than repeated multiplication.

12-1. In the eigenvector basis it is just stretching (8 min)

Let's look again at A=[2112]A = \begin{bmatrix} 2 & 1 \\ 1 & 2 \end{bmatrix} from Lesson 11. Its eigenvectors were e1=(1,1)\mathbf{e}_1 = (1, 1) (eigenvalue 3) and e2=(1,−1)\mathbf{e}_2 = (1, -1) (eigenvalue 1). The two directions are different, so together they can build every vector in the plane (Lesson 4). In other words, the two eigenvectors form a basis.

Written in this basis, v=(3,1)\mathbf{v} = (3, 1) is v=2 e1+1 e2\mathbf{v} = 2\,\mathbf{e}_1 + 1\,\mathbf{e}_2. Now multiply by AA. All AA does to each eigenvector is "multiply by λ\lambda", so the calculation is very simple.

Av=2 (Ae1)+1 (Ae2)=2⋅3 e1+1⋅1 e2=[66]+[1−1]=[75]A\mathbf{v} = 2\,(A\mathbf{e}_1) + 1\,(A\mathbf{e}_2) = 2 \cdot 3\,\mathbf{e}_1 + 1 \cdot 1\,\mathbf{e}_2 = \begin{bmatrix} 6 \\ 6 \end{bmatrix} + \begin{bmatrix} 1 \\ -1 \end{bmatrix} = \begin{bmatrix} 7 \\ 5 \end{bmatrix}

The eigenvector coordinates (2,1)(2, 1) became (6,1)(6, 1): each coordinate was simply multiplied by its own eigenvalue. The directions do not mix. Split into three steps, it looks like this.

  1. Change coordinates. Turn the standard coordinates (3,1)(3, 1) into the eigenvector coordinates (2,1)(2, 1). As you saw in Lesson 10, this means multiplying by P−1P^{-1}, the inverse of the basis matrix PP (columns = eigenvectors).
  2. Stretch along each axis. Multiply the eigenvector coordinates by λ1\lambda_1 and λ2\lambda_2 respectively. This means multiplying by a diagonal matrix DD with only the eigenvalues on its diagonal.
  3. Change coordinates back. Turn the eigenvector coordinates back into standard coordinates. This means multiplying by PP.

As you learned in Lesson 6, matrix products are read from right to left, so chaining the three steps gives this.

A=P D P−1,P=[111−1],D=[3001]A = P\,D\,P^{-1}, \qquad P = \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix}, \quad D = \begin{bmatrix} 3 & 0 \\ 0 & 1 \end{bmatrix}

Writing a matrix this way is called diagonalization. It means "seen in the right coordinate system, this matrix is a diagonal matrix".

What the book writesRead it asMeaningIn Python
PPPthe matrix with the eigenvectors as its columnsvecs
D=diag(λ1,λ2)D = \mathrm{diag}(\lambda_1, \lambda_2)D equals diag of lambda one, lambda twoeigenvalues on the diagonal, 0 elsewherenp.diag(vals)
A=PDP−1A = PDP^{-1}A equals P D P inversethe diagonalization of AP @ D @ np.linalg.inv(P)
AnA^nA to the nA multiplied n timesnp.linalg.matrix_power(A, n)
xn=Anx0\mathbf{x}_n = A^n\mathbf{x}_0x n equals A to the n times x zerothe state after n steps from the start x0\mathbf{x}_0matrix_power(A, n) @ x0

12-2. Powers become easy (6 min)

Write A2A^2 using the diagonalization, and P−1P=IP^{-1}P = I meets in the middle and disappears.

A2=(PDP−1)(PDP−1)=PD (P−1P) DP−1=PD2P−1A^2 = (PDP^{-1})(PDP^{-1}) = PD\,(P^{-1}P)\,DP^{-1} = PD^2P^{-1}

The same happens however many times you multiply, so An=PDnP−1A^n = PD^nP^{-1}. And to raise a diagonal matrix to a power, you just raise each diagonal entry to that power.

Dn=[λ1n00λ2n]D^n = \begin{bmatrix} \lambda_1^n & 0 \\ 0 & \lambda_2^n \end{bmatrix}

In words: "switch to eigenvector coordinates, multiply each coordinate by λn\lambda^n, and switch back". Multiply v=2 e1+1 e2\mathbf{v} = 2\,\mathbf{e}_1 + 1\,\mathbf{e}_2 from 12-1 by AA ten times and you get this.

A10v=2⋅310 e1+1⋅110 e2=118098[11]+[1−1]=[118099118097]A^{10}\mathbf{v} = 2 \cdot 3^{10}\,\mathbf{e}_1 + 1 \cdot 1^{10}\,\mathbf{e}_2 = 118098\begin{bmatrix} 1 \\ 1 \end{bmatrix} + \begin{bmatrix} 1 \\ -1 \end{bmatrix} = \begin{bmatrix} 118099 \\ 118097 \end{bmatrix}

Instead of ten matrix products, one 3103^{10} does it. More importantly, you can see where the result is heading. 3n3^n grows fast while 1n1^n stays put, so the larger nn gets, the more the e1\mathbf{e}_1 term dominates. That is why the direction of AnvA^n\mathbf{v} gets closer and closer to (1,1)(1, 1).

In general, it works like this.

The eigenvector of the eigenvalue with the largest absolute value dominates the long-run behavior. If that eigenvalue is greater than 1, things grow in that direction; if it is less than 1, they shrink to 0; if it is exactly 1, they settle at some state in that direction.

Not every matrix can be diagonalized. When there is only one eigenvector line, like the shear matrix in Lesson 11, you cannot build a basis, so there is no P−1P^{-1}. When the eigenvalues are complex, like a rotation matrix's, the matrix can be diagonalized in the world of complex numbers, but in this course we will only read it as a picture: "there is a rotation mixed in, so it spins while growing or shrinking".

The lab below starts from the vector x₀ (the teal handle) and plots x₀, Ax₀, A²x₀, …, Aⁿx₀ as points in order, joined by a line. Fainter points are earlier steps and darker ones are later, and the crimson arrow is the last xₙ. The yellow dotted line is the eigenvector direction of the eigenvalue with the largest absolute value. Move the slider n from 0 to 20 and watch whether the points get pulled toward the yellow dotted line.

x₀

Matrix

A0.90.10.50.5
x50.840.17

In the long run it settles at 83.3% sunny : 16.7% cloudy

Direction of the largest eigenvalue (dotted): λ = 1

Drawn 3.5× larger (the numbers are the real values)

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([[0.9, 0.5], [0.1, 0.5]])
>>> x0 = np.array([1, 0])
>>> np.linalg.matrix_power(A, 5)
array([[0.83504, 0.8248 ],
       [0.16496, 0.1752 ]])
>>> np.linalg.matrix_power(A, 5) @ x0
array([0.83504, 0.16496])
>>> np.round(np.linalg.eigvals(A), 6)
array([1. +0.j, 0.4+0.j])
In 'Weather Markov chain', raise n from 0 to 20 and watch the points gather at one point on the yellow dotted line. Then switch to 'Fibonacci' to see whether the ratio approaches 1.61803, and to 'Spiral inward' to see the points swirl into the origin.

In 'Weather Markov chain' and 'Fibonacci' the numbers are small, so a note tells you the picture is drawn 3.5× larger. The numbers on the right and in the NumPy panel below are the real values.

12-3. Markov chains: what will the weather be tomorrow? (7 min)

Suppose the weather is only ever "sunny" or "cloudy", and it follows these rules.

Today's weatherChance of sun tomorrowChance of clouds tomorrow
Sunny today0.90.1
Cloudy today0.50.5

If you write today's state as a probability vector x\mathbf{x} = (chance of sun, chance of clouds), you get tomorrow's state by multiplying by one matrix.

xn+1=A xn,A=[0.90.50.10.5]\mathbf{x}_{n+1} = A\,\mathbf{x}_n, \qquad A = \begin{bmatrix} 0.9 & 0.5 \\ 0.1 & 0.5 \end{bmatrix}

Column 1 of the matrix says where you go tomorrow starting from "sunny today", and column 2 says where you go starting from "cloudy today" (Lesson 5: the columns are where the basis vectors land). That is why each column adds up to 1. If today is certainly sunny, x0=(1,0)\mathbf{x}_0 = (1, 0); tomorrow is Ax0=(0.9,0.1)A\mathbf{x}_0 = (0.9, 0.1), and the day after is (0.86,0.14)(0.86, 0.14). A process like this, where "the next state depends only on the current state", is called a Markov chain.

Let's find the eigenvalues. The trace is 1.41.4 and the determinant is 0.45−0.05=0.40.45 - 0.05 = 0.4.

λ2−1.4λ+0.4=0⟹(λ−1)(λ−0.4)=0⟹λ1=1,  λ2=0.4\lambda^2 - 1.4\lambda + 0.4 = 0 \quad\Longrightarrow\quad (\lambda - 1)(\lambda - 0.4) = 0 \quad\Longrightarrow\quad \lambda_1 = 1,\ \ \lambda_2 = 0.4

Reading it with the rule from 12-2, the 0.4n0.4^n direction quickly fades away and only the direction of eigenvalue 1 remains. Since that eigenvalue is exactly 1, a vector in that direction does not change when multiplied by AA. This is called the steady state. The first row of (A−I)v=0(A - I)\mathbf{v} = \mathbf{0} is −0.1s+0.5c=0-0.1s + 0.5c = 0, that is s=5cs = 5c. Scaling it so the probabilities add up to 1 gives this.

v=(56, 16)≈(0.833, 0.167)\mathbf{v} = \left(\tfrac{5}{6},\ \tfrac{1}{6}\right) \approx (0.833,\ 0.167)

The lab's "In the long run it settles at 83.3% sunny : 16.7% cloudy" is exactly this value. At n = 5 it has nearly arrived at (0.835,0.165)(0.835, 0.165), and at n = 20 it agrees to seven decimal places.

The surprising part is that it does not depend on where you start. Whether today is certainly cloudy or a fifty-fifty, in the long run you arrive at the same 83.3% : 16.7%. Every difference in the starting point is carried in the 0.4n0.4^n direction and fades away. PageRank, which ranks web pages, is a steady state with the same structure: "the share of time a user randomly following links spends on each page in the long run".

If you drag x₀ in the lab to a vector whose components do not add up to 1, the point it converges to changes in proportion to the sum. For example, with a sum of 2 it goes to (1.667,0.333)(1.667, 0.333). It is still on the yellow dotted line, with the same 5 : 1 ratio of sun to clouds.

12-4. Fibonacci and the golden ratio, plus a spiral (5 min)

The Fibonacci sequence 1,1,2,3,5,8,13,…1, 1, 2, 3, 5, 8, 13, \dots follows the rule "add the previous two numbers to get the next". Bundle two consecutive numbers into a vector, and this rule also becomes a single matrix multiplication.

[Fn+1Fn]=[1110][FnFn−1]\begin{bmatrix} F_{n+1} \\ F_n \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}\begin{bmatrix} F_n \\ F_{n-1} \end{bmatrix}

The first row is Fn+1=Fn+Fn−1F_{n+1} = F_n + F_{n-1}, and the second just copies FnF_n down one slot. The eigenvalues of this matrix are the solutions of λ2−λ−1=0\lambda^2 - \lambda - 1 = 0.

λ1=1+52≈1.618,λ2=1−52≈−0.618\lambda_1 = \frac{1 + \sqrt{5}}{2} \approx 1.618, \qquad \lambda_2 = \frac{1 - \sqrt{5}}{2} \approx -0.618

λ1\lambda_1 is the golden ratio. Since ∣λ2∣<1\lvert\lambda_2\rvert < 1, that direction fades away, and the vector (Fn+1,Fn)(F_{n+1}, F_n) lines up with the eigenvector direction of λ1\lambda_1. That eigenvector points along (λ1,1)(\lambda_1, 1), so the ratio of the two components, Fn+1/FnF_{n+1} / F_n, approaches the golden ratio 1.61803…1.61803\dots. In the lab's 'Fibonacci', raise n and the "Ratio x_n[0] / x_n[1]" wobbles through 1, 2, 1.5, 1.667, 1.6, 1.625, … as it closes in on 1.61803. It wobbles up and down because λ2\lambda_2 is negative, so the sign of the remaining error flips every step.

In 'Fibonacci', 'Show direction only (scale to length 1)' is on from the start. The components grow by a factor of 1.618n1.618^n and would fly off the screen, so each step is scaled to length 1 to show only the direction. Turn it off and the points race off the screen along the yellow dotted line in no time.

The 'Spiral inward' matrix [0.8−0.50.50.8]\begin{bmatrix} 0.8 & -0.5 \\ 0.5 & 0.8 \end{bmatrix} has the complex eigenvalues 0.8±0.5i0.8 \pm 0.5i. As you saw in Lesson 11, complex eigenvalues signal a rotation. The size of the complex number, 0.82+0.52≈0.943\sqrt{0.8^2 + 0.5^2} \approx 0.943, is less than 1, so each multiplication shrinks the length to about 94%. That is why the points circle the origin and get sucked in like a whirlpool. There are no real eigenvectors, so no yellow dotted line appears either.

12-5. Diagonalization and powers with NumPy (11 min)

Reading the lab's NumPy panel

On the lab's starting screen ('Weather Markov chain', n = 5), the NumPy panel shows this code.

>>> import numpy as np
>>> A = np.array([[0.9, 0.5], [0.1, 0.5]])
>>> x0 = np.array([1, 0])
>>> np.linalg.matrix_power(A, 5)
array([[0.83504, 0.8248 ],
       [0.16496, 0.1752 ]])
>>> np.linalg.matrix_power(A, 5) @ x0
array([0.83504, 0.16496])
>>> np.round(np.linalg.eigvals(A), 6)
array([1. +0.j, 0.4+0.j])

np.linalg.matrix_power(A, n) is the matrix Aⁿ, A multiplied n times. It has the same value as A @ A @ A @ A @ A, but it computes it with fewer multiplications by repeated squaring. With n = 0 you get the identity matrix. Add @ x0 and you have x₅, the weather probabilities five days later. Stepping forward one step at a time gives the same answer.

>>> A @ x0
array([0.9, 0.1])
>>> A @ (A @ x0)
array([0.86, 0.14])
>>> np.linalg.matrix_power(A, 2) @ x0
array([0.86, 0.14])
>>> np.linalg.matrix_power(A, 0)
array([[1., 0.],
       [0., 1.]])

As you saw in Lesson 11, np.linalg.eigvals(A) returns only the eigenvalues, and the panel wraps it in np.round(…, 6) to show six decimal places. So when you pick 'Fibonacci' you see only six digits, as in array([ 1.618034+0.j, -0.618034+0.j]). The space in 1. +0.j is only there to line it up with 0.4+0.j.

Raise n to 20 or 50 and it sticks to the steady state (5/6, 1/6). Starting from a cloudy day lands in the same place. Starting from a vector whose sum is 2 lands at the place with the same ratio, twice as far out.

>>> np.linalg.matrix_power(A, 20) @ x0
array([0.83333334, 0.16666666])
>>> np.linalg.matrix_power(A, 50) @ x0
array([0.83333333, 0.16666667])
>>> np.linalg.matrix_power(A, 50) @ np.array([0, 1])
array([0.83333333, 0.16666667])
>>> np.linalg.matrix_power(A, 30) @ np.array([2, 0])
array([1.66666667, 0.33333333])

np.diag: building a diagonal matrix and taking out the diagonal

np.diag does two different jobs depending on what you pass it. Pass a 1-D array (a list of numbers) and it builds a diagonal matrix with those numbers on the diagonal; pass a 2-D array (a matrix) and it takes out just the diagonal.

>>> np.diag([3, 1])
array([[3, 0],
       [0, 1]])
>>> np.diag(np.array([[1, 2], [3, 4]]))
array([1, 4])

Checking that P D P⁻¹ gives the matrix back

Let's write out the diagonalization from 12-1 exactly and check it. The columns of P are the eigenvectors (1, 1) and (1, −1).

>>> S = np.array([[2, 1], [1, 2]])
>>> P = np.array([[1, 1], [1, -1]])
>>> D = np.diag([3, 1])
>>> P @ D @ np.linalg.inv(P)
array([[2., 1.],
       [1., 2.]])
>>> S @ np.array([3, 1])
array([7, 5])

np.linalg.inv works in real numbers, so the result came out as floats like 2.. Now do the same with the eigenvectors NumPy finds. S is symmetric, so use eigh from Lesson 11.

>>> vals, vecs = np.linalg.eigh(S)
>>> P = vecs
>>> D = np.diag(vals)
>>> np.allclose(P @ D @ np.linalg.inv(P), S)
True

Now use Aⁿ = P Dⁿ P⁻¹ from 12-2. Only the diagonal needs raising to the power, so vals ** 10 does it. ** on an array raises each element to the power (the element-wise operations from Lesson 2).

>>> P @ np.diag(vals ** 10) @ np.linalg.inv(P)
array([[29525., 29524.],
       [29524., 29525.]])
>>> np.linalg.matrix_power(S, 10)
array([[29525, 29524],
       [29524, 29525]])
>>> np.linalg.matrix_power(S, 10) @ np.array([3, 1])
array([118099, 118097])

The (118099, 118097) you found by hand in 12-2 comes out exactly.

One more thing as a bonus. The eigenvectors of a symmetric matrix are perpendicular and have length 1, so the inverse of P equals its transpose P.T. A matrix like this is called an orthogonal matrix, and it is the star of the next lesson.

>>> np.allclose(np.linalg.inv(P), P.T)
True

Finding the steady state directly from the eigenvector

The steady state of a Markov chain is the eigenvector for eigenvalue 1. np.argmax tells you which slot holds the largest value. Take out the column with that index (Lesson 11: vecs[:, k]) and divide by the sum of its components so it becomes probabilities.

>>> vals, vecs = np.linalg.eig(A)
>>> k = np.argmax(vals.real)
>>> k
np.int64(0)
>>> s = vecs[:, k].real
>>> s
array([0.98058068, 0.19611614])
>>> s / s.sum()
array([0.83333333, 0.16666667])
>>> A @ (s / s.sum())
array([0.83333333, 0.16666667])

The eigenvector NumPy gives is (0.98, 0.196), scaled to length 1, so it does not look like probabilities, but the ratio is 5 : 1. Divide by the sum and you get (5/6, 1/6), which does not change when multiplied by A. That is the steady state.

You check that each column adds up to 1 with sum(axis=0). axis=0 means "add along axis 0 (down the rows), top to bottom", that is, add up each column. Lesson 15 sums up axes again.

>>> A.sum(axis=0)
array([1., 1.])

Fibonacci

>>> F = np.array([[1, 1], [1, 0]])
>>> np.linalg.matrix_power(F, 10)
array([[89, 55],
       [55, 34]])
>>> x = np.array([1, 0])
>>> for n in range(1, 8):
...     x = F @ x
...     print(n, x, x[0] / x[1])
1 [1 1] 1.0
2 [2 1] 2.0
3 [3 2] 1.5
4 [5 3] 1.6666666666666667
5 [8 5] 1.6
6 [13  8] 1.625
7 [21 13] 1.6153846153846154
>>> np.linalg.eigvals(F)
array([ 1.61803399+0.j, -0.61803399+0.j])
>>> (1 + np.sqrt(5)) / 2
np.float64(1.618033988749895)

The entries of F¹⁰ are the Fibonacci numbers 89, 55 and 34. The ratio in the for loop wobbling up and down as it approaches the golden ratio is exactly what 12-4 described.

12-6. Power iteration: finding an eigenvalue by repetition alone (5 min)

Turn "the direction of the largest eigenvalue wins in the end" from 12-2 around, and you can find the largest eigenvalue without solving the characteristic equation. Start from any vector and keep multiplying by A: that is power iteration.

But if you only multiply, the numbers either grow without end, as with Fibonacci, or shrink to 0. So you scale to length 1 each time (the same thing as the lab's 'Show direction only (scale to length 1)'). How many times longer the vector gets in one multiplication is an approximation of the eigenvalue.

>>> x = np.array([1.0, 0.0])
>>> for k in range(1, 11):
...     y = F @ x
...     lam = np.linalg.norm(y) / np.linalg.norm(x)
...     x = y / np.linalg.norm(y)
...     print(k, round(float(lam), 6))
1 1.414214
2 1.581139
3 1.612452
4 1.617215
5 1.617914
6 1.618017
7 1.618031
8 1.618034
9 1.618034
10 1.618034

round(float(lam), 6) turns the value into a plain Python number, so each line does not get np.float64(…) attached, and shows six decimal places. After eight rounds the golden ratio is correct to six places. At that point x is an approximation of the eigenvector.

>>> x
array([0.85067229, 0.52569636])
>>> np.round(x[0] / x[1], 4)
np.float64(1.6182)

How fast it converges is set by the ratio of the sizes of the two eigenvalues, |λ₂| / |λ₁|. For Fibonacci it is 0.618 / 1.618 ≈ 0.38, so each step shrinks the error to about 0.38 times, and it converges quickly. When the two eigenvalues are close in size, it gets very slow.

In practice np.linalg.eig finds all the eigenvalues at once with a much more sophisticated method (the QR algorithm). Even so, power iteration is still used today when a matrix is so large, millions by millions, that eig is out of the question, and PageRank was originally computed this way. Calculations like this, which "repeat to close in on the answer", are covered properly in numerical analysis, the course after this one.

12-7. Common mistakes (3 min)

A ** n is not a matrix power. ** on an array raises each element to the power. It is the same story as A * B not being matrix multiplication in Lesson 6.

>>> S ** 3
array([[8, 1],
       [1, 8]])
>>> np.linalg.matrix_power(S, 3)
array([[14, 13],
       [13, 14]])

Integer overflow happens silently. NumPy's int64 only holds numbers up to about 9.2 × 10¹⁸. The Fibonacci number F₁₀₁ is far bigger than that, and you get a nonsense number with no error and no warning.

>>> np.linalg.matrix_power(F, 90) @ np.array([1, 0])
array([4660046610375530309, 2880067194370816120])
>>> np.linalg.matrix_power(F, 100) @ np.array([1, 0])
array([1298777728820984005, 3736710778780434371])

Up to the 90th power it is correct, but in the 100th power result the second number is bigger than the first, so these are not Fibonacci numbers. If you need very large integers, dtype=object makes the array hold Python integers (slow, but exact).

>>> Fo = np.array([[1, 1], [1, 0]], dtype=object)
>>> np.linalg.matrix_power(Fo, 100) @ np.array([1, 0])
array([573147844013817084101, 354224848179261915075], dtype=object)

Matrices that cannot be diagonalized. For the shear matrix, the two columns of the P that eig returns lie on the same line (Lesson 11), so its determinant is close to 0. Compute its inverse and instead of an error you get huge numbers around 10¹⁵. Do not trust results like that.

>>> vals, vecs = np.linalg.eig(np.array([[1, 1], [0, 1]]))
>>> np.linalg.det(vecs)
np.complex128(2.2204460492503185e-16+0j)

Exercises (5 min)

  1. Use the diagonalization from 12-1 to find [2112]5\begin{bmatrix} 2 & 1 \\ 1 & 2 \end{bmatrix}^5 by hand. (Hint: 35=2433^5 = 243. The entries of AnA^n are 3n+12\tfrac{3^n + 1}{2} and 3n−12\tfrac{3^n - 1}{2}.)
  2. Suppose app users are in one of two states, "active" and "dormant". Next month 80% of active users stay active, and 30% of dormant users come back. Set up the matrix TT and find what share of users is active in the long run.
  3. Predict which eigenvalue and eigenvector power iteration from 12-6 approaches when run 30 times on [2112]\begin{bmatrix} 2 & 1 \\ 1 & 2 \end{bmatrix}, then check.

Solutions

Question 1: A5=P diag(243,1) P−1A^5 = P\,\mathrm{diag}(243, 1)\,P^{-1}, and the entries are (243+1)/2=122(243 + 1)/2 = 122 and (243−1)/2=121(243 - 1)/2 = 121.

Question 2: the columns say "where you go next month starting from this month's state", so T=[0.80.30.20.7]T = \begin{bmatrix} 0.8 & 0.3 \\ 0.2 & 0.7 \end{bmatrix}. With trace 1.5 and determinant 0.56−0.06=0.50.56 - 0.06 = 0.5, the eigenvalues are 1 and 0.5. The eigenvector for eigenvalue 1 comes from −0.2a+0.3b=0-0.2a + 0.3b = 0, so a=1.5ba = 1.5b, and scaling it to add up to 1 gives (0.6,0.4)(0.6, 0.4). In the long run, 60% are active and 40% dormant.

Question 3: it approaches the largest eigenvalue 3 and its eigenvector direction (1,1)(1, 1), that is (0.707,0.707)(0.707, 0.707) scaled to length 1.

>>> np.linalg.matrix_power(S, 5)
array([[122, 121],
       [121, 122]])
>>> T = np.array([[0.8, 0.3], [0.2, 0.7]])
>>> np.linalg.eigvals(T)
array([1. +0.j, 0.5+0.j])
>>> vals, vecs = np.linalg.eig(T)
>>> s = vecs[:, np.argmax(vals.real)].real
>>> s / s.sum()
array([0.6, 0.4])
>>> np.linalg.matrix_power(T, 30) @ np.array([0, 1])
array([0.6, 0.4])
>>> x = np.array([1.0, 0.0])
>>> for _ in range(30):
...     x = S @ x
...     x = x / np.linalg.norm(x)
>>> x
array([0.70710678, 0.70710678])
>>> x @ S @ x
np.float64(3.0000000000000004)

The last line for question 2 confirms that even starting with every user dormant, you arrive at (0.6, 0.4) after 30 months. In question 3, x @ S @ x measures "how many times S stretches x" for an x of length 1, and it came out as the eigenvalue 3 with a little floating-point residue attached. For an exact comparison, use np.isclose(x @ S @ x, 3).

Lesson recap

  • Use the eigenvectors as a basis and the matrix becomes a diagonal matrix D that multiplies each axis by λ: A = P D P⁻¹ (columns of P = eigenvectors).

  • Powers are Aⁿ = P Dⁿ P⁻¹. For Dⁿ, just raise each diagonal entry to the nth power.

  • The direction of the eigenvalue with the largest absolute value dominates the long run. A Markov chain heads to the eigenvector of eigenvalue 1 (the steady state); Fibonacci heads to the golden ratio direction.

  • np.linalg.matrix_power(A, n) is Aⁿ. A ** n is an element-wise power, something completely different.

  • np.diag turns a list into a diagonal matrix and a matrix into its diagonal. Check a diagonalization with P @ D @ np.linalg.inv(P).

  • Power iteration: repeat 'multiply, then scale to length 1' and you close in on the largest eigenvalue and its eigenvector.

In the next lesson you will see why orthogonal vectors are so convenient, and learn least squares, which finds the line that best fits a set of data points. It is a way to find "the closest answer" to a system of equations that has no exact solution.

← Previous

Lesson 11 · Eigenvalues and eigenvectors

Next →

Lesson 13 · Orthogonality, projection and least squares

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