Skip to content
Educora
University25 min30 / 42

NumPy: vectorization and linear algebra

Replace loops with array expressions, master the broadcasting rules, reshape arrays, and use `np.linalg` to solve linear systems and find determinants and eigenvalues.

Check yourself
In this lesson you will learn
  • Rewrite loop-based code as vectorized NumPy expressions and explain the speed difference
  • Predict the result shape of an operation on two arrays with the broadcasting rules
  • Apply @, np.linalg.solve, det, inv and eigh together with their mathematical meaning

Murad is processing one million sales of an online shop: for every sale he must compute the amount, apply a discount and find the daily change. With a for loop this takes seconds; with NumPy it takes milliseconds. The secret is vectorization: we write the operation once for the whole array instead of once per element. In this lesson we will also work with linear algebra, the mathematical language of data science.

Vectorization: computing without loops

Definition
Vectorization

Replacing a Python loop over elements with operations applied to whole arrays. The loop still exists, but it runs inside NumPy in compiled C code. Functions that work element by element like this are called ufuncs (universal functions): +, *, np.sqrt, np.exp, np.where and so on.

Why is a Python loop slow? On every iteration the interpreter reads an instruction, checks the element's type and creates a new float object for the result. NumPy checks the type once for numbers of the same type lying next to each other in memory and uses the processor's vector (SIMD) instructions. Run the code and see the difference for yourself:

Python
import time
import numpy as np

x = np.random.default_rng(0).random(1_000_000)

t0 = time.perf_counter()
total = 0.0
for v in x:
    total += v * v
t_loop = time.perf_counter() - t0

t0 = time.perf_counter()
total_vec = np.dot(x, x)
t_vec = time.perf_counter() - t0

print(f'loop:       {t_loop:.4f} s')
print(f'vectorized: {t_vec:.4f} s')
print('same result:', bool(np.isclose(total, total_vec)))
print(f'speed-up: about {t_loop / t_vec:.0f} times')
The times depend on your computer and browser, so no fixed output is given here. The speed-up is usually tens or even hundreds of times.
With a loopVectorized
a running total in a variablea.sum(), a @ b
if / else for each elementnp.where(cond, x, y)
counting elements that match a condition(a > 0).sum()
a cumulative sumnp.cumsum(a)
differences between neighboursnp.diff(a)
the maximum and its positiona.max(), a.argmax()
Python
import numpy as np

prices = np.array([12.0, 45.5, 8.9, 120.0, 64.0])
qty = np.array([3, 1, 10, 1, 2])

revenue = prices * qty
discount = np.where(revenue > 100, 0.10, 0.0)
to_pay = (revenue * (1 - discount)).round(2)
print(revenue)
print(to_pay, to_pay.sum())

daily = np.array([520.0, 610.0, 580.0, 700.0, 655.0])
print(np.diff(daily))
print((np.diff(daily) / daily[:-1] * 100).round(1))
print(np.cumsum(daily))
▸ Expected output
[ 36.   45.5  89.  120.  128. ]
[ 36.   45.5  89.  108.  115.2] 393.7
[ 90. -30. 120. -45.]
[17.3 -4.9 20.7 -6.4]
[ 520. 1130. 1710. 2410. 3065.]
A 10% discount on purchases above 100 manat is applied in one line. Then the daily change in revenue (in manat and in percent) and the cumulative total are computed.

Broadcasting: arrays of different shapes

When you write a * 10, the number 10 is as if “stretched” to an array of the same shape as a. This mechanism is called broadcasting, and it works not only with single numbers but also with arrays of different shapes. NumPy compares the shapes from right to left:

  1. If one shape has fewer dimensions, 1s are added on its left: (4,) → (1, 4).
  2. Along each axis the two lengths must be equal, or one of them must be 1.
  3. An axis of length 1 is “stretched” to the other array's length (without copying memory); the result's shape is the maximum along each axis.
  4. If along some axis the lengths differ and neither is 1, you get a ValueError.
ABResult
(3, 4)(4,)(3, 4)
(3, 1)(1, 4)(3, 4)
(2, 1, 5)(3, 1)(2, 3, 5)
(3, 4)(3,)error: 4 ≠ 3

A classic use of broadcasting is standardization: from each column we subtract its mean and divide by its standard deviation, so that features on different scales (height in cm, weight in kg) become comparable. A mean of shape (2,) is subtracted from an array of shape (4, 2) — rules 1 and 3 do the work.

zᵢⱼ = (xᵢⱼ − μⱼ) / σⱼzᵢⱼ = (xᵢⱼ − μⱼ) / σⱼ
where:
  • xᵢⱼfeature j of example i
  • μⱼ, σⱼmean and standard deviation of column j
  • zᵢⱼstandardized value (z-score): mean 0, standard deviation 1
Python
import numpy as np

X = np.array([[170.0, 65.0],
              [160.0, 52.0],
              [182.0, 80.0],
              [175.0, 71.0]])
mean = X.mean(axis=0)
std = X.std(axis=0)
Z = (X - mean) / std
print(mean, std.round(3))
print(Z.round(2))
print(np.allclose(Z.mean(axis=0), 0), Z.std(axis=0).round(6))
▸ Expected output
[171.75  67.  ] [ 8.012 10.173]
[[-0.22 -0.2 ]
 [-1.47 -1.47]
 [ 1.28  1.28]
 [ 0.41  0.39]]
True [1. 1.]

Combining a column vector with a row vector gives an “each with each” table. a[:, np.newaxis] (or a[:, None]) turns an array of shape (3,) into a (3, 1) column:

Python
import numpy as np

a = np.arange(1, 4)
b = np.arange(1, 6)
table = a[:, np.newaxis] * b
print(table.shape)
print(table)

M = np.ones((3, 4))
v = np.array([10.0, 20.0, 30.0])
try:
    M + v
except ValueError as e:
    print('Error:', str(e).strip())
print((M + v[:, None])[:, 0])
▸ Expected output
(3, 5)
[[ 1  2  3  4  5]
 [ 2  4  6  8 10]
 [ 3  6  9 12 15]]
Error: operands could not be broadcast together with shapes (3,4) (3,)
[11. 21. 31.]
Example 1: find the result shape

Find the result shape of each operation or show that it fails: a) (5, 1, 3) + (4, 3); b) (8, 1) · (1, 6); c) (2, 3) + (2,); d) (256, 256, 3) · (3,).

Show solution
a) (4, 3) → (1, 4, 3); axis by axis: 5 and 1 → 5, 1 and 4 → 4, 3 and 3 → 3. Result (5, 4, 3).
b) 8 and 1 → 8, 1 and 6 → 6. Result (8, 6) — an “each with each” table.
c) From the right: 3 and 2 — not equal and neither is 1 → ValueError.
d) The last axes 3 and 3 match: result (256, 256, 3). This multiplies the R, G, B channels of every pixel of an image by three different factors.

Reshaping

reshape shows the same elements in a different shape; the number of elements must not change, and one dimension may be -1 — NumPy works it out. .T transposes, ravel() flattens to one dimension (returning a view when possible), and np.vstack, np.hstack and np.concatenate glue arrays together.

Python
import numpy as np

a = np.arange(12)
print(a.reshape(3, 4))
print(a.reshape(2, -1).shape)
m = a.reshape(3, 4)
print(m.T.shape, m.ravel()[:5])
print(np.vstack([m[0], m[1]]))
print(np.concatenate([m, m], axis=1).shape)
▸ Expected output
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
(2, 6)
(4, 3) [0 1 2 3 4]
[[0 1 2 3]
 [4 5 6 7]]
(3, 8)

Linear algebra: np.linalg

The product of matrices is not element-wise multiplication. A * B multiplies matching elements, while A @ B (or np.dot) computes the mathematical matrix product: a row of A is multiplied by a column of B as a dot product. For this, the number of columns of A must equal the number of rows of B.

cᵢⱼ = ∑ₖ₌₁ⁿ aᵢₖ · bₖⱼ (m × n) @ (n × p) → (m × p)
where:
  • aᵢₖelement in row i, column k of A
  • bₖⱼelement in row k, column j of B
  • nthe shared number of columns of A and rows of B
Python
import numpy as np

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A * B)
print(A @ B)
print(np.dot(A, B).shape, (np.ones((2, 3)) @ np.ones((3, 5))).shape)
▸ Expected output
[[ 5 12]
 [21 32]]
[[19 22]
 [43 50]]
(2, 2) (2, 5)
Check: (A @ B)₀₀ = 1 · 5 + 2 · 7 = 19. The element-wise product gives 1 · 5 = 5 instead.

A system of linear equations is written in matrix form as Ax = b. If det A ≠ 0, the system has a unique solution and A has an inverse: x = A⁻¹b. For a 2 × 2 matrix everything can be computed by hand:

A = [[a, b], [c, d]]: det A = a·d − b·c A⁻¹ = (1 / det A) · [[d, −b], [−c, a]]A = [[a, b], [c, d]]: det A = a·d − b·c A⁻¹ = (1 / det A) · [[d, −b], [−c, a]]
where:
  • det Adeterminant; if it is 0, the matrix is singular and has no inverse
  • A⁻¹inverse matrix: A · A⁻¹ = I (identity matrix)
Example 2: notebooks and pens

3 notebooks and 2 pens cost 5.5 manat; 1 notebook and 4 pens cost 3.5 manat. Find the price of a notebook and a pen with the matrix method.

Show solution
A = [[3, 2], [1, 4]], b = (5.5, 3.5).
det A = 3 · 4 − 2 · 1 = 10 ≠ 0 — there is a unique solution.
A⁻¹ = (1/10) · [[4, −2], [−1, 3]].
x = A⁻¹b = (1/10) · (4 · 5.5 − 2 · 3.5, −5.5 + 3 · 3.5) = (1/10) · (15, 5) = (1.5, 0.5).
A notebook costs 1.5 manat, a pen 0.5 manat. Check: 3 · 1.5 + 2 · 0.5 = 5.5 ✓.

With three unknowns (notebook, pen, ruler) the hand calculation gets long, but NumPy solves it in one line:

Python
import numpy as np

A = np.array([[2.0, 1.0, 3.0],
              [1.0, 3.0, 1.0],
              [4.0, 2.0, 0.0]])
b = np.array([9.5, 5.0, 7.0])

x = np.linalg.solve(A, b)
print(x.round(2))
print(np.allclose(A @ x, b))
print(round(np.linalg.det(A), 2))
x_inv = np.linalg.inv(A) @ b
print(np.allclose(x, x_inv))
▸ Expected output
[1.5 0.5 2. ]
True
-30.0
True
Notebook 1.5, pen 0.5, ruler 2 manat. np.allclose checks that the solution satisfies the equations; the determinant shown is rounded.

An eigenvector is a non-zero vector v that the matrix A only stretches or shrinks without changing its direction; the stretch factor λ is the eigenvalue. We find the λ values from the characteristic equation. Applications: principal component analysis (PCA), Google's PageRank algorithm, the natural vibration frequencies of bridges and buildings, and the stability of dynamic systems.

A · v = λ · v det(A − λ · I) = 0
where:
  • veigenvector (v ≠ 0)
  • λeigenvalue
  • Iidentity matrix

The second equation is the characteristic equation: the system (A − λI)v = 0 has a non-zero solution only when the determinant is 0.

Example 3: eigenvalues of a 2 × 2 matrix

Find the eigenvalues and eigenvectors of A = [[2, 1], [1, 2]].

Show solution
det(A − λI) = (2 − λ)² − 1 = 0 → 2 − λ = ±1 → λ₁ = 1, λ₂ = 3.
λ = 3: (A − 3I)v = 0 → −v₁ + v₂ = 0 → v = (1, 1).
λ = 1: v₁ + v₂ = 0 → v = (1, −1).
The matrix stretches the direction (1, 1) three times and leaves the direction (1, −1) unchanged.
Python
import numpy as np

A = np.array([[2.0, 1.0], [1.0, 2.0]])
values, vectors = np.linalg.eigh(A)
print(values.round(4))
v = vectors[:, 1]
print(np.allclose(A @ v, values[1] * v))
print(round(np.linalg.det(A), 4), round(values.prod(), 4))
print(round(np.trace(A), 4), round(values.sum(), 4))
▸ Expected output
[1. 3.]
True
3.0 3.0
4.0 4.0
Use eigh for symmetric matrices: it returns the eigenvalues in ascending order. The vectors are normalised to length 1, but their signs may differ between libraries.

Linear algebra leads straight to machine learning. Fitting the best straight line through points (the least-squares method) leads to the normal equation. If we add a column of ones to the matrix X, the intercept is found automatically as well:

w = (Xᵀ X)⁻¹ Xᵀ y
where:
  • Xn × d feature matrix (the first column is ones)
  • yvector of target values
  • wcoefficients that minimise the sum of squared errors
Python
import numpy as np

rng = np.random.default_rng(7)
x = rng.uniform(0, 10, 50)
y = 2.5 * x + 1.0 + rng.normal(0, 1, 50)
X = np.column_stack([np.ones_like(x), x])
w = np.linalg.solve(X.T @ X, X.T @ y)
print(w.round(3))
w2, *_ = np.linalg.lstsq(X, y, rcond=None)
print(w2.round(3))
▸ Expected output
[1.236 2.458]
[1.236 2.458]
The data comes from the line y = 2.5x + 1 plus noise; the fitted intercept is ≈ 1.24 and the slope ≈ 2.46. In practice the more robust np.linalg.lstsq is used; scikit-learn's LinearRegression solves the same problem.
Exercise

Scale every column of the matrix X to the range [0, 1] with broadcasting and no loop (min–max normalisation): (X − column minimum) / (column maximum − column minimum). Print the result rounded to 2 decimal places.

Exercise · Python
import numpy as np

X = np.array([[2.0, 50.0, 1.0],
              [4.0, 80.0, 3.0],
              [6.0, 65.0, 2.0],
              [8.0, 95.0, 5.0]])
# min-max scale every column
▸ Expected output
[[0.   0.   0.  ]
 [0.33 0.67 0.5 ]
 [0.67 0.33 0.25]
 [1.   1.   1.  ]]
Exercise

At a market: 2 kg apples + 1 kg pears + 1 kg grapes = 9.5 manat; 1 kg apples + 2 kg pears + 1 kg grapes = 10 manat; 1 kg apples + 1 kg pears + 3 kg grapes = 13.5 manat. Use np.linalg.solve to find the price of 1 kg of apples, pears and grapes and print it rounded to 2 decimals, then print the matrix determinant rounded to 1 decimal.

Exercise · Python
import numpy as np

# A: kilograms in each purchase, b: amounts paid (manat)
A = np.array([[2.0, 1.0, 1.0],
              [1.0, 2.0, 1.0],
              [1.0, 1.0, 3.0]])
b = np.array([9.5, 10.0, 13.5])
# solve A x = b and print the prices, then the determinant
▸ Expected output
[2.  2.5 3. ]
7.0

Key points

  • Vectorization replaces Python loops with ufuncs that run in C and speeds code up tens or hundreds of times.
  • Broadcasting compares shapes from the right: the lengths must be equal or one of them must be 1.
  • v[:, None] and keepdims=True make shapes compatible by turning a vector into a column.
  • * is element-wise and @ is the matrix product: (m × n) @ (n × p) → (m × p).
  • Solve Ax = b with np.linalg.solve; when det A = 0 there is no unique solution.
  • Av = λv; the eigenvalues add up to the trace and multiply to the determinant.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
What is the result shape when an array of shape (4, 1) is added to an array of shape (3,)?