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)

IndexerCategoryReturns
arr[2], arr[:, 1:3], arr[::-1], arr[-2:]Basic (slicing, integer, negative)View — shares memory with arr
arr[mask] where mask.dtype == boolAdvanced (boolean)Copy — independent array
arr[[0, 2, 5]], arr[idx] where idx is an integer ndarrayAdvanced (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 back

The 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 > 5

greater_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’s and/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.