While slicing lets you select contiguous blocks of elements and boolean indexing filters elements based on conditions, there are times when you need to select specific elements based on their index positions, even if those positions are not sequential. This is where fancy indexing comes in. Fancy indexing uses NumPy arrays (or Python lists) containing integer indices to pick out elements from another array.
The core idea is straightforward: you pass an array or list of indices to the array you want to select from.
Let's start with a simple one-dimensional array:
import numpy as np
# Create a 1D array
arr = np.arange(10) * 5
print(f"Original array:\n{arr}")
# Output:
# Original array:
# [ 0 5 10 15 20 25 30 35 40 45]
# Define the indices we want to select
indices = np.array([1, 5, 8, 2])
print(f"Indices to select: {indices}")
# Output:
# Indices to select: [1 5 8 2]
# Use fancy indexing
selected_elements = arr[indices]
print(f"Selected elements:\n{selected_elements}")
# Output:
# Selected elements:
# [ 5 25 40 10]
Notice a few things:
indices
, a NumPy array containing [1, 5, 8, 2]
, inside the square brackets []
.arr
.selected_elements
contains the elements arr[1]
, arr[5]
, arr[8]
, and arr[2]
.indices
), not the original array (arr
). The order of elements in the result also matches the order in the index array.You can use the same index multiple times if needed:
indices_repeated = np.array([3, 3, 1, 8, 1])
print(f"Indices with repetition: {indices_repeated}")
# Output:
# Indices with repetition: [3 3 1 8 1]
repeated_selection = arr[indices_repeated]
print(f"Selection with repeated indices:\n{repeated_selection}")
# Output:
# Selection with repeated indices:
# [15 15 5 40 5]
Fancy indexing becomes even more versatile with multi-dimensional arrays. You can pass multiple index arrays to select elements based on coordinates across different axes.
Consider a 2D array:
arr2d = np.array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
print(f"Original 2D array:\n{arr2d}")
Selecting Specific Rows: If you provide a single array of indices, it selects entire rows corresponding to those indices:
# Select rows 0 and 2
row_indices = np.array([0, 2])
selected_rows = arr2d[row_indices]
print(f"Selected rows (0 and 2):\n{selected_rows}")
# Output:
# Selected rows (0 and 2):
# [[ 0 1 2 3]
# [ 8 9 10 11]]
Selecting Specific Elements (Coordinates):
To select individual elements using their coordinates, you need to provide one index array for each dimension. The arrays should typically have the same length. NumPy pairs up the indices element-wise: the first element selected is at (row_indices[0], col_indices[0])
, the second at (row_indices[1], col_indices[1])
, and so on.
# Indices for rows
row_indices = np.array([0, 3, 1, 2])
# Indices for columns
col_indices = np.array([1, 2, 0, 3])
# Select elements at coordinates (0,1), (3,2), (1,0), (2,3)
selected_coords = arr2d[row_indices, col_indices]
print(f"Selected elements by coordinates:\n{selected_coords}")
# Output:
# Selected elements by coordinates:
# [ 1 14 4 11]
Here, the selected elements are arr2d[0, 1]
(which is 1), arr2d[3, 2]
(which is 14), arr2d[1, 0]
(which is 4), and arr2d[2, 3]
(which is 11). The result is a 1D array because we provided specific coordinates.
Combining Fancy Indexing with Slicing or Basic Indexing: You can also mix fancy indexing with slicing or basic indexing. For instance, to select specific rows and specific columns, you might combine them:
# Select rows 0 and 2, and columns 1 and 3
row_indices = np.array([0, 2])
col_indices = np.array([1, 3])
# Method 1: Select rows first, then columns from the result
selected_block_1 = arr2d[row_indices][:, col_indices]
print(f"Selected block (Method 1):\n{selected_block_1}")
# Output:
# Selected block (Method 1):
# [[ 1 3]
# [ 9 11]]
# Method 2: Using np.ix_ for broadcasting indices (more advanced)
# This helper function creates indexers for selecting a rectangular region
selected_block_2 = arr2d[np.ix_(row_indices, col_indices)]
print(f"Selected block (Method 2 with np.ix_):\n{selected_block_2}")
# Output:
# Selected block (Method 2 with np.ix_):
# [[ 1 3]
# [ 9 11]]
Both methods yield the sub-array containing elements at the intersections of rows 0 & 2 and columns 1 & 3. The first method is often more intuitive for beginners, selecting rows first and then columns from that intermediate result.
You can also select specific rows and all columns, or specific columns from all rows:
# Select rows 1 and 3, all columns
print(f"Rows 1 and 3, all columns:\n{arr2d[[1, 3]]}")
# Output:
# Rows 1 and 3, all columns:
# [[ 4 5 6 7]
# [12 13 14 15]]
# Select all rows, columns 0 and 2
print(f"All rows, columns 0 and 2:\n{arr2d[:, [0, 2]]}")
# Output:
# All rows, columns 0 and 2:
# [[ 0 2]
# [ 4 6]
# [ 8 10]
# [12 14]]
Just like basic indexing and slicing, fancy indexing can be used to modify array elements. You place the fancy index expression on the left side of an assignment:
arr = np.arange(10) * 5
print(f"Original array: {arr}")
indices = np.array([1, 5, 8, 2])
arr[indices] = 99 # Assign a single value to all selected elements
print(f"Array after modifying elements at {indices}: {arr}")
# Output:
# Original array: [ 0 5 10 15 20 25 30 35 40 45]
# Array after modifying elements at [1 5 8 2]: [ 0 99 99 15 20 99 30 35 99 45]
# You can also assign an array of values, matching the index array shape
arr[indices] = np.array([-1, -2, -3, -4])
print(f"Array after assigning multiple values: {arr}")
# Output:
# Array after assigning multiple values: [ 0 -1 -4 15 20 -2 30 35 -3 45]
The same applies to multi-dimensional arrays:
print(f"Original 2D array:\n{arr2d}")
# Modify elements at (0,1), (3,2), (1,0), (2,3)
row_indices = np.array([0, 3, 1, 2])
col_indices = np.array([1, 2, 0, 3])
arr2d[row_indices, col_indices] = 0
print(f"2D array after setting specific elements to 0:\n{arr2d}")
# Output:
# Original 2D array:
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]
# [12 13 14 15]]
# 2D array after setting specific elements to 0:
# [[ 0 0 2 3]
# [ 0 5 6 7]
# [ 8 9 10 0]
# [12 13 0 15]]
A significant difference between slicing and fancy indexing is that fancy indexing almost always returns a copy of the data, not a view. This means changes made to the array returned by fancy indexing will not affect the original array.
arr = np.arange(10)
print(f"Original array: {arr}")
# Slice (creates a view)
slice_view = arr[2:5]
print(f"Slice view: {slice_view}")
slice_view[0] = 99 # Modify the view
print(f"Original array after modifying slice view: {arr}") # Original is changed
# Fancy Indexing (creates a copy)
indices = np.array([6, 8])
fancy_copy = arr[indices]
print(f"Fancy copy: {fancy_copy}")
fancy_copy[0] = -1 # Modify the copy
print(f"Original array after modifying fancy copy: {arr}") # Original is unchanged
# Output:
# Original array: [0 1 2 3 4 5 6 7 8 9]
# Slice view: [2 3 4]
# Original array after modifying slice view: [ 0 1 99 3 4 5 6 7 8 9]
# Fancy copy: [6 8]
# Original array after modifying fancy copy: [ 0 1 99 3 4 5 6 7 8 9]
Remembering this copy behavior is important, especially when you intend to modify the original array data. If you need to modify the original array using index arrays, use the fancy indexing expression directly on the left side of the assignment, as shown previously.
Fancy indexing provides a powerful and flexible way to access and manipulate array elements based on their positions, complementing the capabilities of basic slicing and boolean indexing.
© 2025 ApX Machine Learning