📊 Section 1 · Foundations 🟡 Intermediate MODULE 06

NumPy Advanced — Indexing, Broadcasting & Linear Algebra

⏱️ 26 min read
📖 Theory + Code
🧩 5 Quiz Questions
🏗️ 1 Challenge
Your progress in Section 186%
🎯 What you'll learn: Boolean indexing and masking, fancy indexing, slicing 2D arrays by row and column, the broadcasting rules that let arrays of different shapes work together, and a first look at linear algebra with 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."

slicing_2d.py
PYTHON
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
⚠️
Slices are views, not copies
Slicing a NumPy array (unlike slicing a Python list) returns a view into the original data, not a new copy. Modifying the sliced result also modifies the original array. Use .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.

boolean_indexing.py
PYTHON
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]
📝
Why & and |, not and/or
Python's 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.

fancy_indexing.py
PYTHON
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
Boolean vs. fancy indexing
Boolean indexing selects elements based on a condition (values where something is true). Fancy indexing selects elements based on specific positions you already know. They're often combined — for example, using 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.

broadcasting.py
PYTHON
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]]
⚠️
When broadcasting fails
A (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.
Why this matters in practice
Broadcasting is exactly what makes an operation like "subtract the column mean from every row" a single readable line — 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.

linear_algebra_basics.py
PYTHON
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]]
📝
Shapes must line up for matrix multiplication
For 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.
🔮
Where this shows up later
You won't need to compute matrix inverses by hand in this course — but recognizing @ 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:

2D arrays are indexed as array[rows, columns]; slices are views, not copies.
Boolean masking (arr[arr > 10]) selects elements matching a condition — combine conditions with & and |, not and/or.
Fancy indexing (arr[[0, 2, 4]]) selects elements at specific known positions.
Broadcasting lets differently-shaped arrays combine when their trailing dimensions match, or one of them is 1.
np.dot() / @ compute dot products and matrix multiplication; np.linalg.inv() finds a matrix's inverse.
🧩 Knowledge Check — Lesson 6
Answer all 5 questions to test your understanding. Instant feedback on every answer.
1. What does grid[0:2, 1:3] select from a 2D array?
2. To combine two boolean conditions on a NumPy array, you should use:
3. What does names[[0, 2, 4]] demonstrate?
4. Can a (3, 4) array and a (4,) array be broadcast together?
5. Which operator performs matrix multiplication between two NumPy arrays?
💪
Coding Challenge — Lesson 6
Apply what you learned · Intermediate Level

Combine masking, fancy indexing, and broadcasting in one small analysis.

Challenge: Grade Curve 📈

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
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 6 Complete!

Masking, fancy indexing, broadcasting, and basic linear algebra are all in your toolkit now. One lesson left in Section 1 — the checkpoint quiz covering everything from Lessons 1–6.

Module 06 of 7 Section 1 — Python for Data Science Foundations