NumPy Advanced — Indexing, Broadcasting & Linear Algebra
np.dot, the @ operator, and np.linalg.inv.
Picking Up Where Lesson 5 Left Off
You already know how to create arrays and apply simple element-wise operations. This lesson covers the four ideas that let you actually select and combine data inside arrays the way real analysis requires: masking, fancy indexing, 2D slicing, and broadcasting — plus a first taste of linear algebra, the backbone of the machine learning you'll meet in Section 5.
Slicing 2D Arrays
A 2D array is indexed with two values separated by a comma: array[rows, columns]. A bare colon : means "every element along that axis."
import numpy as np grid = np.array([ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], ]) print(grid[1, 2]) # 7 — row 1, column 2 print(grid[0, :]) # [1 2 3 4] — the entire first row print(grid[:, 1]) # [ 2 6 10] — the entire second column print(grid[0:2, 1:3]) # top-right 2x2 block: [[2 3] [6 7]] print(grid[-1, :]) # [ 9 10 11 12] — last row
.copy() — e.g. grid[0:2, 1:3].copy() — when you specifically need an independent copy.Boolean Indexing & Masking
A comparison on an array — like arr > 10 — produces an array of True/False values called a boolean mask. Using that mask to index the original array selects only the elements where the mask is True. This is one of the most useful patterns in all of NumPy.
import numpy as np scores = np.array([55, 88, 92, 40, 75, 61]) mask = scores >= 60 print(mask) # [False True True False True True] print(scores[mask]) # [88 92 75 61] — only the passing scores # Written in one line, without naming the mask passing = scores[scores >= 60] # Combining conditions needs & / | (not "and"/"or"), and parentheses around each condition mid_range = scores[(scores >= 60) & (scores < 90)] print(mid_range) # [88 75 61] # Masking to modify values in place — a fast way to "clip" or clean data cleaned = scores.copy() cleaned[cleaned < 60] = 0 # replace every failing score with 0 print(cleaned) # [ 0 88 92 0 75 61]
and/or expect single boolean values, but a comparison like scores >= 60 produces a whole array of booleans. NumPy overloads & (and), | (or), and ~ (not) to work element-wise instead — and each individual condition needs its own parentheses.Fancy Indexing
Fancy indexing means indexing an array with a list or array of specific positions, rather than a single index or a slice.
import numpy as np names = np.array(["Amara", "Ben", "Cleo", "Dev", "Eli"]) # Selecting a specific, non-contiguous set of positions print(names[[0, 2, 4]]) # ['Amara' 'Cleo' 'Eli'] # Works with 2D arrays too — selecting specific rows grid = np.array([[1, 2], [3, 4], [5, 6], [7, 8]]) print(grid[[0, 3]]) # [[1 2] [7 8]] — rows 0 and 3 # Combining fancy indexing with np.argsort() to get the top-N scores = np.array([55, 88, 92, 40, 75]) top_two_idx = np.argsort(scores)[-2:] # indices of the 2 highest scores print(names[top_two_idx]) # the names with the top 2 scores
np.argsort() to find positions, then fancy-indexing with them.Broadcasting Rules
Broadcasting is the set of rules NumPy uses to let arrays of different shapes work together in an operation, without you having to manually resize either one. You already used the simplest case in Lesson 5 — array + 5 — where the single number 5 was "broadcast" to match every element.
The rule, informally
Compare the two shapes from the right, dimension by dimension. Two dimensions are compatible if they're equal, or if one of them is 1 (in which case it gets stretched to match). If neither condition holds for some dimension, the operation fails.
import numpy as np # Case 1: a (3, 4) matrix + a (4,) vector — the vector is broadcast across every row matrix = np.array([ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], ]) row_adjust = np.array([100, 200, 300, 400]) # shape (4,) print(matrix + row_adjust) # [[101 202 303 404] # [105 206 307 408] # [109 210 311 412]] — row_adjust was applied to EVERY row # Case 2: a (3, 1) column + a (1, 4) row → broadcasts into a full (3, 4) result col = np.array([[1], [2], [3]]) # shape (3, 1) row = np.array([[10, 20, 30, 40]]) # shape (1, 4) print(col + row) # [[11 21 31 41] # [12 22 32 42] # [13 23 33 43]]
(3, 4) array and a (3,) array can NOT broadcast together — comparing from the right, 4 vs 3 are neither equal nor is either one 1, so NumPy raises a ValueError. Reshaping the vector to (3, 1) would make it broadcast down the columns instead of across the rows.matrix - matrix.mean(axis=0) — instead of a manual nested loop. You'll use this pattern constantly once you reach normalization and feature scaling in Section 5.Basic Linear Algebra
NumPy's linalg module and the @ operator cover the core linear algebra operations that machine learning is built on. You don't need to be a linear algebra expert yet — just recognize these three operations.
import numpy as np # Dot product of two vectors — multiply matching elements, then sum v1 = np.array([1, 2, 3]) v2 = np.array([4, 5, 6]) print(np.dot(v1, v2)) # 32 → (1*4 + 2*5 + 3*6) # Matrix multiplication with np.dot() or the @ operator (they're equivalent here) A = np.array([[1, 2], [3, 4]]) # shape (2, 2) B = np.array([[5, 6], [7, 8]]) # shape (2, 2) print(np.dot(A, B)) print(A @ B) # same result — @ is the standard, more readable syntax # [[19 22] # [43 50]] # The inverse of a square matrix (only defined when the matrix is invertible) inv_A = np.linalg.inv(A) print(inv_A) print(A @ inv_A) # approximately the identity matrix: [[1 0] [0 1]]
A @ B to work, the number of columns in A must match the number of rows in B. A (2, 3) matrix can multiply a (3, 4) matrix — producing a (2, 4) result — but not a (2, 4) matrix directly.@ and np.dot() matters, because this is exactly the machinery running underneath Scikit-Learn when it fits a linear regression model in Section 5.Lesson Summary
Let's recap everything you learned in this lesson:
array[rows, columns]; slices are views, not copies.arr[arr > 10]) selects elements matching a condition — combine conditions with & and |, not and/or.arr[[0, 2, 4]]) selects elements at specific known positions.np.dot() / @ compute dot products and matrix multiplication; np.linalg.inv() finds a matrix's inverse.grid[0:2, 1:3] select from a 2D array?names[[0, 2, 4]] demonstrate?Combine masking, fancy indexing, and broadcasting in one small analysis.
You have a 2D array of exam scores for 3 students across 4 exams:
[[62, 75, 58, 80], [90, 85, 95, 88], [45, 50, 60, 55]]. Write a script that: (1) computes the mean score of each column (each exam) using .mean(axis=0), (2) uses broadcasting to add a flat +5 curve to every score, (3) uses boolean masking to find and print how many curved scores are 90 or above.
Rules: Don't write a manual loop for the curve — use array + scalar broadcasting. Use a boolean mask and
.sum() on the mask (True counts as 1) to count scores ≥ 90.
💡 Show hints if you're stuck
- Column means:
scores.mean(axis=0)— axis=0 means "collapse down the rows" - Curve:
curved = scores + 5 - Count:
(curved >= 90).sum()— summing a boolean array counts the True values