Skillversitas

Indexing and Slicing

Indexing into arrays is how data gets out, and it comes in three flavors with different performance and copy semantics: basic indexing (integers and slices), fancy indexing (integer arrays), and boolean masking. The flavor matters - basic slicing returns a view (writes pass through), while fancy indexing and boolean masks return copies (writes do not). Mastering this chapter is mastering the single most-used part of the API.

Basic Indexing: 1-D

import numpy as np

a = np.arange(10)          # [0 1 2 3 4 5 6 7 8 9]

print(a[0], a[3])          # 0 3
print(a[-1])               # 9 - negative indexes count from the end
print(a[2:5])              # [2 3 4] - start:stop, end exclusive
print(a[::2])              # [0 2 4 6 8] - step of 2
print(a[7:])               # [7 8 9] - open end
print(a[:])                # the whole array (a view)
print(a[::-1])             # reversed - a view, no copy

Slice syntax [start:stop:step] is Python's, with open ends allowed. One critical property: basic slicing returns a view - a window over the same memory. Mutating the slice mutates the original:

b = a[2:5]
b[0] = 99
print(a[2])                # 99 - the original changed

Basic Indexing: 2-D and Beyond

m = np.arange(12).reshape(3, 4)
print(m)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(m[1])           # row 1: [4 5 6 7]
print(m[1, 2])        # element: 6
print(m[0, 1:3])      # row 0, columns 1-2: [1 2]
print(m[:, 1])        # all rows, column 1: [1 5 9]
print(m[1:, 2:])      # rows 1+, cols 2+: [[6 7] [10 11]]
print(m[..., 0])      # ellipsis: all leading axes, column 0

One index expression per axis, in axis order; : means "all of this axis"; the ... (ellipsis) abbreviates the remaining leading axes and is the clean way to index the last axis of an array of unknown dimension. Every basic expression above returns a view when it is a pure slice - the m[1:, 2:] block shares memory with m.

Fancy Indexing (Integer Arrays)

a = np.arange(10)
print(a[[0, 2, 4]])            # [0 2 4] - pick specific positions
print(a[np.array([1, 3, 5])])  # same, via an explicit array

m = np.arange(12).reshape(3, 4)
print(m[[0, 2]])               # rows 0 and 2 (a copy)
print(m[[0, 1], 2])            # (row 0, col 2) and (row 1, col 2): [2 6]

# paired indexing: one row index per column index
rows = np.array([0, 1, 2])
cols = np.array([3, 1, 0])
print(m[rows, cols])           # [3 5 8] - element at (0,3), (1,1), (2,0)

Passing an integer array selects positions; the result is a copy, so assignments do not write back: a[[0, 2]][0] = 99 changes nothing in a (to assign, use a[[0, 2]] = [99, 98] in one step). The paired form m[rows, cols] is the standard "gather these cells" operation - one result per (row, col) pair - and it is how scatter/gather, sampling, and permutation patterns are written.

Boolean Masking

a = np.arange(10)
print(a[a > 5])                 # [6 7 8 9] - elements passing the mask
print(a[a % 2 == 0])            # [0 2 4 6 8]

m = np.arange(12).reshape(3, 4)
big = m > 5                     # a boolean array, same shape
print(m[big])                   # all elements > 5, flattened: [6 7 9 10 11]
print(big.sum())                # 5 - counting is just a mask sum

# assignment through a mask - the workhorse mutation pattern
a[a < 5] = 0
print(a)                        # [0 0 0 0 0 5 6 7 8 9]

A boolean array of the same shape acts as a mask: arr[mask] returns the True positions as a 1-D copy, and arr[mask] = value writes through to the original - the conditional-assignment idiom that replaces loops. Compound conditions use &, |, ~ with parentheses (operators bind tighter than the bitwise forms, and Python's and does not work elementwise).

The View/Copy Rule, Summarized

OperationReturnsWrites pass through?
pure slice a[1:5], m[1:, 2:]viewyes
integer/fancy a[[1,3]], m[rows, cols]copyno (use one-step assignment)
boolean mask a[a > 5]copyassignment form writes through

When a view surprises you (a change you did not make), the diagnosis is x.base is not None or a np.shares_memory(x, y) check; when a copy surprises you, the fix is .copy() explicitly. The rule above covers 99% of cases - learn it as a reflex.

Summary

Index with one expression per axis: slices for ranges (views, writes pass through), integer arrays for picked positions (copies), boolean masks for conditional selection and the arr[mask] = value mutation idiom, and the ellipsis for the trailing axis of open-dimensional code. Keep the view/copy rule in muscle memory - it explains more NumPy "mystery changes" than any other fact - and use np.shares_memory when the memory relationship itself needs confirming.