NumPy Indexing and Slicing
Select, filter, and update array elements using integer indexing, slices, boolean masks, and fancy indexing.
Real-World Scenario
An ML engineer working with a dataset of 50,000 images needs to extract specific pixel regions from every image, select all samples where the label equals 3, and replace corrupted values above a threshold. These are indexing and masking operations — the core data access patterns you’ll use in every NumPy workflow.
Basic Indexing
import numpy as np
arr = np.array([10, 20, 30, 40, 50])
# Single element — 0-based, like Python lists
print(arr[0]) # 10
print(arr[-1]) # 50 — negative indices count from the end
print(arr[-2]) # 40
# 2-D indexing — [row, col]
matrix = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
print(matrix[0, 0]) # 1 — top-left
print(matrix[2, 3]) # 12 — bottom-right
print(matrix[-1, -1]) # 12 — same element, negative indices
# Selecting an entire row or column
print(matrix[1]) # [5 6 7 8] — row 1
print(matrix[:, 2]) # [ 3 7 11] — column 2 (all rows, col index 2)
Slicing
NumPy slice syntax is start:stop:step, identical to Python list slicing but extended to multiple dimensions. Slices return views — not copies.
import numpy as np
arr = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80, 90])
print(arr[2:6]) # [20 30 40 50] — elements at index 2, 3, 4, 5
print(arr[:4]) # [ 0 10 20 30] — from start to index 3
print(arr[6:]) # [60 70 80 90] — from index 6 to end
print(arr[::2]) # [ 0 20 40 60 80] — every other element
print(arr[::-1]) # [90 80 70 ... 0] — reversed
# 2-D slicing — extract a submatrix
matrix = np.arange(16).reshape(4, 4)
print(matrix)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]
# [12 13 14 15]]
# Rows 1–2, columns 1–3
sub = matrix[1:3, 1:3]
print(sub)
# [[ 5 6]
# [ 9 10]]
# Views share memory — modifying sub modifies matrix
sub[0, 0] = 99
print(matrix[1, 1]) # 99
# Make an independent copy to avoid this
safe_sub = matrix[1:3, 1:3].copy()
Boolean Masking
Boolean masking is the most readable way to filter arrays. Comparison operators applied to an array return a boolean array of the same shape. Passing that boolean array as an index selects only the True elements.
import numpy as np
temperatures = np.array([22.1, 35.4, 18.9, 41.2, 28.7, 15.3, 38.1])
# Step 1: comparison produces a boolean array
hot_mask = temperatures > 30
print(hot_mask) # [False True False True False False True]
# Step 2: use the mask as an index — returns a 1-D array of matching values
hot_temps = temperatures[hot_mask]
print(hot_temps) # [35.4 41.2 38.1]
# Combine in one line
print(temperatures[temperatures > 30])
# Multiple conditions — use & (and) and | (or), not Python's 'and'/'or'
comfortable = temperatures[(temperatures >= 20) & (temperatures <= 30)]
print(comfortable) # [22.1 28.7]
# np.where — like a vectorized ternary: where(condition, if_true, if_false)
labels = np.where(temperatures > 30, "hot", "normal")
print(labels) # ['normal' 'hot' 'normal' 'hot' 'normal' 'normal' 'hot']
# Replace values in-place using a mask
temperatures[temperatures > 40] = 40.0 # clamp outliers
print(temperatures) # max is now 40.0
Fancy Indexing
Use an array of integer indices to select non-contiguous elements in arbitrary order. This always returns a copy.
import numpy as np
prices = np.array([9.99, 14.99, 4.99, 24.99, 7.49, 19.99])
# Select specific elements by position
selected = prices[[0, 2, 5]]
print(selected) # [ 9.99 4.99 19.99]
# Reorder elements
shuffled = prices[[3, 0, 4, 1]]
print(shuffled) # [24.99 9.99 7.49 14.99]
# 2-D fancy indexing — select specific rows
data = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12]
])
# Select rows 0 and 2
print(data[[0, 2]])
# [[1 2 3]
# [7 8 9]]
# Select specific (row, col) pairs — extracts diagonal-style elements
rows = np.array([0, 1, 2])
cols = np.array([2, 0, 1])
print(data[rows, cols]) # [3 4 8] — data[0,2], data[1,0], data[2,1]
Real-World: Selecting Training Samples by Label
import numpy as np
rng = np.random.default_rng(42)
# Simulate a dataset: 1000 samples, 10 features each
X = rng.standard_normal((1000, 10))
y = rng.integers(0, 5, size=1000) # labels 0–4
# Select all samples belonging to class 3
class_3_mask = y == 3
X_class3 = X[class_3_mask]
print(f"Class 3 samples: {X_class3.shape[0]}") # ~200
# Get indices where label == 3 (useful when you need positions, not values)
class_3_indices = np.where(y == 3)[0]
print(class_3_indices[:5]) # [4 7 9 ...]
# Sample 50 random class-3 examples
sample_idx = rng.choice(class_3_indices, size=50, replace=False)
X_sample = X[sample_idx]
print(X_sample.shape) # (50, 10)
Common Mistakes
1. Modifying a slice when you need a copy:
arr = np.array([1, 2, 3, 4, 5])
view = arr[1:4]
view[0] = 99 # also modifies arr[1] — unexpected if you forgot view is a view
2. Using Python and/or on boolean arrays:
# Wrong — raises ValueError
# mask = (arr > 2) and (arr < 5)
# Correct — element-wise bitwise operators
mask = (arr > 2) & (arr < 5)
3. Off-by-one in stop index:
arr = np.arange(10)
print(arr[2:5]) # [2 3 4] — stop=5 is excluded, like Python range