Indexing and Slicing

Difficulty: Beginner

Indexing and slicing are fundamental operations for accessing and manipulating data within NumPy arrays. Like Python lists, NumPy arrays use zero-based indexing. For a 1D array, arr[0] retrieves the first element and arr[-1] retrieves the last. However, NumPy extends this concept significantly for multi-dimensional arrays, where you separate indices for each dimension with commas: arr[row, col] for 2D arrays. This comma-separated syntax is more efficient and readable than the chained bracket notation arr[row][col].

Slicing extracts a contiguous subset of an array using the start:stop:step syntax, just like Python lists. For 1D arrays, arr[1:4] returns elements at indices 1, 2, and 3 (the stop index is exclusive). For 2D arrays, you can slice along each dimension independently: arr[0:2, 1:3] selects rows 0 and 1 and columns 1 and 2. A crucial difference from Python lists is that NumPy slices return views, not copies. Modifying a slice modifies the original array. If you need an independent copy, use arr[1:4].copy().

Boolean indexing uses an array of True/False values to select elements. When you write arr[arr > 5], NumPy evaluates arr > 5 to produce a boolean array, then returns only the elements where the boolean value is True. This is one of the most common and powerful patterns in NumPy and Pandas. You can create complex conditions by combining boolean expressions with & (and), | (or), and ~ (not), always wrapping each condition in parentheses due to operator precedence rules.

Fancy indexing (also called advanced indexing) lets you access multiple non-contiguous elements by passing an array of indices. For example, arr[[0, 3, 5]] returns the elements at indices 0, 3, and 5. Unlike slicing, fancy indexing always returns a copy, not a view. For 2D arrays, you can pass separate index arrays for rows and columns: arr[[0, 2], [1, 3]] returns the elements at positions (0,1) and (2,3). This is useful for extracting specific elements from scattered positions in a large array.

Understanding the distinction between views and copies is critical for avoiding subtle bugs. Slicing creates a view that shares memory with the original array, so changes propagate both ways. Fancy indexing and boolean indexing create copies, so changes to the result do not affect the original. When in doubt, use the .copy() method explicitly to ensure you are working with independent data.

Code examples

Basic 1D indexing and slicing

import numpy as np

arr = np.array([10, 20, 30, 40, 50, 60, 70])

print("First:", arr[0])
print("Last:", arr[-1])
print("Slice [1:4]:", arr[1:4])
print("Every other:", arr[::2])
print("Reversed:", arr[::-1])

Index 0 gets the first element, -1 gets the last. Slicing [1:4] gets indices 1, 2, 3. Step of 2 skips every other element. Step of -1 reverses the array.

2D array indexing

import numpy as np

arr = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]])

print("Element [0,2]:", arr[0, 2])
print("Row 1:", arr[1])
print("Column 0:", arr[:, 0])
print("Subarray [0:2, 1:3]:")
print(arr[0:2, 1:3])

arr[0, 2] accesses row 0, column 2. arr[1] returns the entire second row. arr[:, 0] selects all rows but only column 0. arr[0:2, 1:3] slices both dimensions to get a 2x2 subarray.

Boolean indexing for filtering

import numpy as np

scores = np.array([85, 42, 91, 67, 73, 55, 98])

passing = scores[scores >= 70]
print("Passing scores:", passing)

# Combined condition
mid_range = scores[(scores >= 50) & (scores <= 80)]
print("Mid-range (50-80):", mid_range)

# Count
print("Failing count:", np.sum(scores < 70))

scores >= 70 creates a boolean mask that selects only passing scores. Combined conditions use & with parentheses. np.sum on a boolean array counts True values.

Fancy indexing

import numpy as np

arr = np.array([10, 20, 30, 40, 50, 60])

# Select specific indices
selected = arr[[0, 2, 5]]
print("Indices [0,2,5]:", selected)

# 2D fancy indexing
matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

print("Diagonal:", matrix[[0, 1, 2], [0, 1, 2]])
print("Corners:", matrix[[0, 0, 2, 2], [0, 2, 0, 2]])

Fancy indexing with a list of indices returns those specific elements. For 2D arrays, passing two index arrays selects elements at the paired positions: (0,0), (1,1), (2,2) for the diagonal.

Key points

Concepts covered

basic indexing, slicing, boolean indexing, fancy indexing