NumPy advanced indexing is the family of subscript operations that select array elements using a boolean mask or an integer array of indices, often built with helpers like np.where and np.logical_and. It differs from basic indexing (slicing, integer indexing, negative indexing) in one way that matters: advanced indexing returns a copy of the data, basic slicing returns a view of the original array. That copy-vs-view split is the whole reason NumPy draws the basic/advanced line. Get it wrong and assignments either silently fail to propagate or silently mutate something you didn’t mean to.
For the slicing/integer/negative-index forms that are basic (and return views), see NumPy array slicing.
Basic vs advanced indexing (the contract)
| Indexer | Category | Returns |
|---|---|---|
arr[2], arr[:, 1:3], arr[::-1], arr[-2:] | Basic (slicing, integer, negative) | View — shares memory with arr |
arr[mask] where mask.dtype == bool | Advanced (boolean) | Copy — independent array |
arr[[0, 2, 5]], arr[idx] where idx is an integer ndarray | Advanced (integer-array, “fancy”) | Copy — independent array |
Mix: arr[1:3, [0, 2]] | Advanced (any advanced part promotes the whole expression) | Copy |
Practical consequence:
import numpy as np
arr = np.arange(10)
# Basic slicing — view
b = arr[2:5]
b[0] = 99
print(arr) # [ 0 1 99 3 4 5 6 7 8 9] — original modified
# Advanced (boolean) indexing — copy
c = arr[arr > 4]
c[0] = -1
print(arr) # [ 0 1 99 3 4 5 6 7 8 9] — original untouched
# In-place write through a boolean mask still works
arr[arr > 4] = 0
print(arr) # [0 1 99 3 4 0 0 0 0 0] — direct subscript-assign goes backThe last case is the subtle one: arr[mask] = … (assignment) writes through to the original, but c = arr[mask]; c[...] = … (binding then mutating) does not, because c is a fresh copy. Subscript-assignment is special syntax that NumPy intercepts.
Boolean indexing
Apply a comparison to an array to get a boolean mask of the same shape:
greater_than_five = test_data > 5greater_than_five[i, j] is True if test_data[i, j] > 5, else False.
Use the mask to select elements:
test_data[greater_than_five]The result is a 1D array containing only the elements where the mask is True. The original 2D structure is lost — boolean indexing always flattens to 1D.
Shape-preserving selection: np.where
To preserve the original array shape, use np.where(condition, value_if_true, value_if_false):
drop_under_five = np.where(test_data > 5, test_data, 0)- Where the condition is true, take the value from
test_data. - Where false, replace with
0.
The result has the same shape as the input. This is useful for “keep some, replace others” operations.
Multiple conditions
Combine boolean masks with np.logical_and (or & for boolean arrays):
mask = np.logical_and(test_data > 5, test_data < 20)
test_data[mask]The result is a 1D array of elements satisfying both conditions: .
Equivalent operators: np.logical_or, np.logical_not, np.logical_xor. Or use bitwise operators on boolean arrays: &, |, ~. Don’t use Python’s and/or/not — they don’t broadcast over arrays.
When to use each
- Boolean masking (
arr[arr > 5]): filter by an arbitrary condition; flattens to 1D; returns a copy. - Integer-array (fancy) indexing (
arr[[0, 3, 7]],arr[idx]): pick out specified positions in a chosen order; returns a copy. np.where(cond, a, b): shape-preserving “replace where condition” — useful for “keep some, replace others” without flattening.np.logical_and/np.logical_or/np.logical_not(or&/|/~on boolean arrays): combine masks before indexing. Don’t use Python’sand/or/not— they don’t broadcast.- For consecutive subsets, reversal, and column/row picks (the basic-indexing forms), use NumPy array slicing. Slicing returns a view, which is the right tool when you want changes to propagate back to the original.