NumPy Fundamentals
The backbone of numerical computing in Python. Learn arrays, broadcasting, vectorization, and linear algebra — everything you need before diving into ML.
NumPy Arrays — Creation & Properties
A NumPy ndarray (N-dimensional array) is the core data structure of NumPy. Unlike Python lists, arrays store elements of the same data type in contiguous memory — making them extremely fast for numerical computations.
There are many ways to create arrays. The most common methods are shown below:
import numpy as np # From Python list arr1 = np.array([1, 2, 3, 4, 5]) # 2D array from nested list arr2 = np.array([[1, 2, 3], [4, 5, 6]]) # Filled arrays zeros = np.zeros((3, 4)) # 3x4 array of 0.0 ones = np.ones((2, 3)) # 2x3 array of 1.0 full = np.full((2, 2), 7) # 2x2 filled with 7 eye = np.eye(3) # 3x3 identity matrix # Range-based arrays range_arr = np.arange(0, 10, 2) # [0,2,4,6,8] linear = np.linspace(0, 1, 5) # 5 evenly spaced from 0→1 print(arr2.shape) # (2, 3) print(arr2.ndim) # 2 print(arr2.dtype) # int64 print(arr2.size) # 6
| Function | Description |
|---|---|
| np.array() | Create array from list or nested list |
| np.zeros(shape) | Array filled with 0.0 |
| np.ones(shape) | Array filled with 1.0 |
| np.full(shape, val) | Array filled with specific value |
| np.eye(n) | n×n identity matrix |
| np.arange(start, stop, step) | Like Python range() but returns array |
| np.linspace(start, stop, n) | n evenly spaced values between start and stop |
| np.empty(shape) | Uninitialized array (fast, but values are garbage) |
Indexing & Slicing
NumPy supports powerful indexing — regular indexing, slicing, boolean masking, and fancy indexing. These are essential for selecting data in ML pipelines.
import numpy as np arr = np.array([10, 20, 30, 40, 50]) # ── 1D Indexing ── print(arr[0]) # 10 (first element) print(arr[-1]) # 50 (last element) print(arr[1:4]) # [20 30 40] print(arr[::2]) # [10 30 50] (every 2nd) # ── 2D Indexing ── mat = np.array([[1,2,3],[4,5,6],[7,8,9]]) print(mat[1, 2]) # 6 (row 1, col 2) print(mat[0:2, 1:3]) # [[2,3],[5,6]] print(mat[:, 1]) # [2 5 8] (entire column 1) # ── Boolean Masking ── data = np.array([5, 12, 3, 18, 7]) mask = data > 8 print(mask) # [False True False True False] print(data[mask]) # [12 18] # ── Fancy Indexing (pick specific indices) ── print(data[[0, 2, 4]]) # [5 3 7]
.copy() when you need an independent array: arr[1:3].copy()
Reshaping Arrays
Reshaping is one of the most used operations in ML — converting between 1D, 2D, and 3D arrays when passing data between models and layers.
import numpy as np arr = np.arange(12) # [0,1,2,...,11] # reshape: total elements must match mat = arr.reshape(3, 4) # 3 rows, 4 cols mat2 = arr.reshape(2, -1) # -1 = NumPy figures out cols # Flatten back to 1D flat1 = mat.ravel() # returns view when possible flat2 = mat.flatten() # always returns a copy # Transpose: swap rows and columns print(mat.T.shape) # (4, 3) # Add/remove dimensions x = np.array([1, 2, 3]) # shape (3,) x_col = np.expand_dims(x, axis=1) # shape (3,1) column vector x_back = np.squeeze(x_col) # back to (3,) print(mat) print(mat2.shape)
reshape() requires the total number of elements to stay the same. Reshaping a (12,) array to (4, 4) will raise a ValueError since 4×4=16 ≠ 12.
Stacking & Splitting
import numpy as np a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) # Stack horizontally (side by side) print(np.hstack([a, b])) # [1 2 3 4 5 6] # Stack vertically (on top of each other) print(np.vstack([a, b])) # [[1 2 3] # [4 5 6]] # Concatenate along axis print(np.concatenate([a, b], axis=0)) # [1 2 3 4 5 6] # Split array into parts arr = np.arange(9) parts = np.split(arr, 3) # 3 equal parts print(parts) # Split 2D array mat = np.arange(16).reshape(4,4) h = np.hsplit(mat, 2) # split into 2 column halves v = np.vsplit(mat, 2) # split into 2 row halves
Broadcasting
Broadcasting is NumPy's ability to perform operations on arrays of different shapes without copying data. NumPy automatically "stretches" the smaller array to match the larger one.
Two arrays are compatible for broadcasting if, for each dimension pair (aligned from the right), the sizes are either equal or one of them is 1.
import numpy as np # Scalar with array (simplest broadcast) arr = np.array([1, 2, 3, 4]) print(arr * 2) # [2 4 6 8] # 1D array with 2D array mat = np.array([[1,2,3], [4,5,6]]) # shape (2,3) row = np.array([10, 20, 30]) # shape (3,) print(mat + row) # [[11 22 33] row added to EACH row of mat # [14 25 36]] # Column broadcast — shape (2,1) + (1,3) col = np.array([[100], [200]]) # shape (2,1) print(mat + col) # [[101 102 103] # [204 205 206]]
Vectorization
Vectorization means replacing Python for loops with NumPy operations that run in optimized C code. This is a fundamental concept in writing efficient ML code.
import numpy as np import time arr = np.arange(1_000_000) # ── Slow: Python loop ── t0 = time.time() result = [] for x in arr: result.append(x ** 2) print(f"Loop: {time.time()-t0:.3f}s") # ── Fast: Vectorized ── t1 = time.time() result = arr ** 2 print(f"Vectorized: {time.time()-t1:.4f}s") # ── np.vectorize() — apply custom function element-wise ── def my_func(x): return x ** 2 + 1 vfunc = np.vectorize(my_func) print(vfunc(np.array([1, 2, 3]))) # [2 5 10]
for loop over a NumPy array, there's almost always a vectorized alternative. Always prefer operations like arr * 2, np.sum(arr), arr[arr > 0] over loops.
Math Functions — Basic & Aggregate
import numpy as np a = np.array([4, 9, 16, 25]) # Element-wise math print(np.sqrt(a)) # [2. 3. 4. 5.] print(np.abs([-3, 4])) # [3 4] print(np.power(a, 2)) # [16 81 256 625] # ── Aggregate functions ── mat = np.array([[1,2,3], [4,5,6]]) print(np.sum(mat)) # 21 (total) print(np.sum(mat, axis=0)) # [5 7 9] column sums print(np.sum(mat, axis=1)) # [6 15] row sums print(np.mean(mat)) # 3.5 print(np.median(mat)) # 3.5 print(np.std(mat)) # 1.708 print(np.var(mat)) # 2.916 print(np.min(mat), np.max(mat)) # 1 6
axis=0 means operate down the rows (column-wise result). axis=1 means operate across columns (row-wise result). Think of axis as "which dimension collapses".
Sorting & Searching
import numpy as np arr = np.array([3, 1, 4, 1, 5, 9, 2, 6]) print(np.sort(arr)) # [1 1 2 3 4 5 6 9] — returns sorted copy print(np.argsort(arr)) # [1 3 6 0 2 4 7 5] — indices that would sort print(np.argmin(arr)) # 1 — index of minimum value print(np.argmax(arr)) # 5 — index of maximum value print(np.unique(arr)) # [1 2 3 4 5 6 9] — unique values # np.where — conditional selection data = np.array([-2, 5, -1, 8, -3]) result = np.where(data > 0, data, 0) # replace negatives with 0 print(result) # [0 5 0 8 0] # unique with counts vals, counts = np.unique(arr, return_counts=True) print(dict(zip(vals, counts))) # {1:2, 2:1, 3:1, ...}
Linear Algebra — np.linalg
Linear algebra is the language of machine learning. Matrix multiplication, eigenvalues, and norms appear constantly in ML algorithms.
import numpy as np A = np.array([[1, 2], [3, 4]]) B = np.array([[5, 6], [7, 8]]) # Dot product / Matrix multiplication print(np.dot(A, B)) # [[19 22] [43 50]] print(A @ B) # Same as np.dot — @ is preferred in modern Python # Determinant print(np.linalg.det(A)) # -2.0 # Inverse print(np.linalg.inv(A)) # [[-2. 1. ] # [ 1.5 -0.5]] # Eigenvalues and Eigenvectors vals, vecs = np.linalg.eig(A) print("Eigenvalues:", vals) # [-0.372 5.372] # Norm (length of a vector) v = np.array([3, 4]) print(np.linalg.norm(v)) # 5.0 (Euclidean distance)
@ operator for matrix multiplication is used constantly in deep learning (weight matrices). np.linalg.norm() appears in regularization (L2 norm). Eigenvalues appear in PCA.
Random Module — np.random
The np.random module is used for generating random data, initializing weights, and creating synthetic datasets for testing.
import numpy as np # Always set seed for reproducibility! np.random.seed(42) # Uniform distribution [0, 1) print(np.random.rand(3, 2)) # 3x2 matrix of random floats # Normal (Gaussian) distribution — mean=0, std=1 print(np.random.randn(5)) # 5 values from standard normal # Random integers print(np.random.randint(0, 10, size=(2,3))) # 2x3 ints from 0-9 # Random choice from array options = ['cat', 'dog', 'bird'] print(np.random.choice(options, size=5)) # random picks with replacement # Shuffle an array in-place arr = np.arange(10) np.random.shuffle(arr) # modifies arr in-place print(arr) # Permutation — returns shuffled copy print(np.random.permutation(10)) # shuffled [0..9]
np.random.seed(), your results change every run — making experiments non-reproducible. Set the seed at the top of every notebook or script.
Rounding, Clipping & Trigonometry
import numpy as np arr = np.array([1.7, 2.3, 3.9, -1.2]) # Rounding print(np.round(arr, 1)) # [ 1.7 2.3 3.9 -1.2] print(np.floor(arr)) # [ 1. 2. 3. -2.] round down print(np.ceil(arr)) # [ 2. 3. 4. -1.] round up # Clipping — cap values within a range data = np.array([-5, 2, 15, 7, -1]) print(np.clip(data, 0, 10)) # [0 2 10 7 0] — useful for activation clipping # Exponential & Logarithm print(np.exp([0, 1, 2])) # [1. 2.718 7.389] — used in softmax! print(np.log([1, np.e])) # [0. 1.] — natural log # Trigonometry (angles in radians) angles = np.array([0, np.pi/2, np.pi]) print(np.sin(angles).round(2)) # [0. 1. 0.] print(np.cos(angles).round(2)) # [1. 0. -1.]
np.exp() is used in softmax and sigmoid activations. np.log() is used in cross-entropy loss. np.clip() prevents numerical overflow in log computations.