NumPy
03 / 03

Math, Linear Algebra & Performance

NumPy: Math, Linear Algebra & Performance

Universal Functions (ufuncs)

a = np.array([1.0, 4.0, 9.0, 16.0])

# Element-wise math (vectorized — no Python loop needed)
np.sqrt(a)              # [1. 2. 3. 4.]
np.square(a)            # [1. 16. 81. 256.]
np.abs(np.array([-1, -2, 3]))  # [1 2 3]
np.exp(a)               # e^x
np.log(a)               # natural log
np.log2(a)
np.log10(a)
np.sin(a)
np.cos(a)
np.floor(a)
np.ceil(a)
np.round(a, decimals=2)
np.clip(a, 0, 5)        # clamp values to [0, 5]

# Aggregations
a.sum()                  # sum all elements
a.sum(axis=0)            # sum along rows (result: shape per column)
a.sum(axis=1)            # sum along columns (result: shape per row)
a.mean()
a.std()
a.var()
a.min()
a.max()
a.argmin()               # index of minimum
a.argmax()               # index of maximum
a.cumsum()               # cumulative sum
np.median(a)
np.percentile(a, [25, 50, 75])
np.unique(a)
np.sort(a)               # returns sorted copy
a.sort()                 # sorts in-place

Linear Algebra

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

# Matrix multiplication
A @ B                    # preferred (PEP 465)
np.matmul(A, B)          # same
np.dot(A, B)             # also works for 2D

# Element-wise (NOT matrix multiply)
A * B                    # [[ 5,12],[21,32]]

# Linear algebra functions
np.linalg.det(A)         # determinant
np.linalg.inv(A)         # inverse
np.linalg.trace(A)       # sum of diagonal
np.linalg.norm(A)        # Frobenius norm by default
np.linalg.norm(A, axis=1)  # row norms

# Eigenvalues and eigenvectors
eigenvalues, eigenvectors = np.linalg.eig(A)

# Singular Value Decomposition
U, S, Vt = np.linalg.svd(A)

# Solve linear system Ax = b
b = np.array([1, 2])
x = np.linalg.solve(A, b)     # faster than inv(A) @ b

# Least squares (overdetermined system)
A_tall = np.random.rand(100, 3)
b_tall = np.random.rand(100)
x, residuals, rank, sv = np.linalg.lstsq(A_tall, b_tall, rcond=None)

Performance & Best Practices

  • Vectorize: never write Python loops over array elements. Use ufuncs, array operations, and built-in aggregations.

  • Avoid copies: slices return views; fancy indexing and boolean indexing return copies. Profile with a.base to check.

  • Memory layout: C-contiguous (row-major) for row operations; F-contiguous for column operations. Use np.ascontiguousarray() if needed.

  • dtype matters: float32 is 2× faster than float64 on GPU and many CPU SIMD operations. Use smallest dtype that fits.

  • np.einsum: Einstein summation notation for complex tensor operations — often faster than matmul chains.

  • numba: JIT-compiles Python/NumPy to LLVM — near-C speed for loops that can't be vectorized.

  • Use out= parameter to write results into pre-allocated array: np.add(a, b, out=result) — avoids allocation.

# Benchmark: Python loop vs NumPy vectorization
import time
n = 1_000_000
a = np.random.rand(n)

# Python loop — slow
start = time.time()
result = [x ** 2 for x in a]
print(f"Loop: {time.time() - start:.3f}s")  # ~0.3s

# NumPy vectorized — fast
start = time.time()
result = a ** 2
print(f"NumPy: {time.time() - start:.4f}s") # ~0.002s — 150× faster

# np.einsum examples
A = np.random.rand(100, 50)
B = np.random.rand(50, 30)
np.einsum('ij,jk->ik', A, B)   # matrix multiply (same as A @ B)
np.einsum('ij->i', A)          # row sums (same as A.sum(axis=1))
np.einsum('ii->', A[:50, :50]) # trace

# Save/load arrays
np.save('array.npy', a)                   # single array
np.savez('arrays.npz', a=a, b=B)         # multiple arrays
loaded = np.load('arrays.npz')
loaded['a']                               # retrieve by key
np.savetxt('data.csv', A, delimiter=',', fmt='%.4f')

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free