NumPy · lesson 4 of 13
NumPy Indexing and Slicing
Index, slice, mask and fancy-index NumPy arrays, and learn exactly which operations return views and which return copies, proven with np.shares_memory.
By SkillByExample · Updated
Single Elements, Rows and Columns
An index per axis, separated by commas, picks one element; a colon keeps a whole axis.
import numpy as np
m = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(m[0, 0], m[2, 3], m[-1, -1])
print(m[1])
print(m[:, 2])
$ python basic_index.py
1 12 12
[5 6 7 8]
[ 3 7 11]
m[1] is row 1 and m[:, 2] is column 2 — both come back 1-D. Negative indices count from the
end, so m[-1, -1] and m[2, 3] are the same corner element.
Slicing
start:stop:step works on every axis, with stop excluded as in Python.
import numpy as np
arr = np.arange(0, 100, 10)
print(arr[2:6], arr[::3], arr[::-1][:3])
m = np.arange(16).reshape(4, 4)
print(m)
print(m[1:3, 1:3])
$ python slicing.py
[20 30 40 50] [ 0 30 60 90] [90 80 70]
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]
[[ 5 6]
[ 9 10]]
m[1:3, 1:3] takes rows 1–2 and columns 1–2, giving the 2 × 2 block from the middle. A negative
step reverses: arr[::-1][:3] is the last three values, largest first.
Views and Copies
Whether an indexing result shares memory with the original is the single most important thing to know about NumPy indexing.
import numpy as np
m = np.arange(16).reshape(4, 4)
sliced = m[1:3, 1:3]
fancy = m[[1, 2]]
masked = m[m > 10]
print("slice shares memory:", np.shares_memory(m, sliced))
print("fancy shares memory:", np.shares_memory(m, fancy))
print("mask shares memory:", np.shares_memory(m, masked))
sliced[0, 0] = 99
fancy[0, 0] = -1
print(m[1])
$ python views_copies.py
slice shares memory: True
fancy shares memory: False
mask shares memory: False
[ 4 99 6 7]
The slice is a view, so writing 99 into it changed m[1, 1]. The fancy-indexed and masked
results are copies: -1 went into fancy only, and m’s row 1 shows no trace of it. The rule:
integers and slices give views; integer lists and boolean masks give copies.
Other Views: reshape, ravel and transpose
import numpy as np
a = np.arange(6)
r = a.reshape(2, 3)
t = r.T
flat = t.ravel()
print(np.shares_memory(a, r), np.shares_memory(a, t))
print(np.shares_memory(a, flat))
print(np.shares_memory(a, a.flatten()))
$ python other_views.py
True True
False
False
reshape and .T only change how the same bytes are read, so both are views. ravel returns
a view when it can, but t is transposed — its elements aren’t in memory order — so here it had
to copy. flatten always copies. When you need independence, call .copy() rather than
guessing.
Writing Through a Copy by Accident
import numpy as np
arr = np.array([5, 12, 3, 18, 7])
arr[arr > 10][0] = 0
print(arr)
arr[arr > 10] = 0
print(arr)
$ python chained.py
[ 5 12 3 18 7]
[5 0 3 0 7]
The first assignment silently did nothing. arr[arr > 10] built a copy, [0] = 0 wrote into
that copy, and the copy was discarded. A single indexing operation on the left-hand side — the
second form — assigns into arr itself.
Boolean Masks
import numpy as np
temps = np.array([22.1, 35.4, 18.9, 41.2, 28.7, 15.3, 38.1])
print(temps > 30)
print(temps[temps > 30])
print(temps[(temps >= 20) & (temps <= 30)])
print(np.where(temps > 30, "hot", "ok"))
temps[temps > 40] = 40.0
print(temps.max())
$ python masks.py
[False True False True False False True]
[35.4 41.2 38.1]
[22.1 28.7]
['ok' 'hot' 'ok' 'hot' 'ok' 'ok' 'hot']
40.0
A comparison produces a boolean array of the same shape, and indexing with it keeps the True
positions. np.where(cond, a, b) picks elementwise between two values. Assigning through a mask
(the last two lines) modifies the original, clamping 41.2 down to 40.0.
and/or Raise an Error
import numpy as np
temps = np.array([22.1, 35.4, 18.9])
try:
print(temps[(temps > 20) and (temps < 30)])
except ValueError as e:
print("ValueError:", e)
$ python and_error.py
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
and needs a single True or False, and a three-element boolean array has no single truth
value. & and | compare element by element; the parentheses matter because & binds more
tightly than >.
Fancy Indexing
import numpy as np
prices = np.array([9.99, 14.99, 4.99, 24.99, 7.49, 19.99])
print(prices[[0, 2, 5]])
print(prices[[3, 0, 3]])
data = np.arange(1, 13).reshape(4, 3)
rows = np.array([0, 1, 2])
cols = np.array([2, 0, 1])
print(data[rows, cols])
$ python fancy.py
[ 9.99 4.99 19.99]
[24.99 9.99 24.99]
[3 4 8]
An index list can pick any positions in any order, including repeats (3 twice). With two
index arrays, NumPy pairs them up — data[0, 2], data[1, 0], data[2, 1] — rather than
taking every combination.
Repeated Indices and +=
import numpy as np
counts = np.zeros(3, dtype=int)
counts[[0, 0, 2]] += 1
print(counts)
counts = np.zeros(3, dtype=int)
np.add.at(counts, [0, 0, 2], 1)
print(counts)
$ python add_at.py
[1 0 1]
[2 0 1]
Index 0 appears twice, yet += only counted it once. The statement expands to
counts[idx] = counts[idx] + 1, which writes the same value to position 0 twice. np.add.at
applies each increment separately, which is what you want for histograms and scatter-adds.
Selecting Samples by Label
import numpy as np
rng = np.random.default_rng(42)
X = rng.standard_normal((1000, 10))
y = rng.integers(0, 5, size=1000)
X3 = X[y == 3]
idx3 = np.flatnonzero(y == 3)
sample = X[rng.choice(idx3, size=50, replace=False)]
print(X3.shape, idx3[:5], sample.shape)
$ python by_label.py
(216, 10) [ 1 2 4 10 18] (50, 10)
X[y == 3] uses a mask built from a different array of the same length, the everyday way to
filter features by label. np.flatnonzero returns the positions rather than the values, which
you need for sampling indices. With seed 42 roughly a fifth of the 1000 rows are class 3.
Practice
- From
np.arange(20).reshape(4, 5), take every other column of the last two rows. Is the result a view? - Replace every negative value in
np.array([3, -1, 4, -1, 5, -9])with 0 in place. - Count how many times each value 0–4 appears in
[1, 3, 3, 0, 3, 1]usingnp.add.at. - Fix
arr[arr % 2 == 0][0] = 100so it sets the first even element ofarrto 100.
Solution 1
import numpy as np
m = np.arange(20).reshape(4, 5)
part = m[-2:, ::2]
print(part)
print(np.shares_memory(m, part))
$ python practice_slice.py
[[10 12 14]
[15 17 19]]
True
Slices with steps are still basic slicing, so the result is a view.
Solution 2
import numpy as np
a = np.array([3, -1, 4, -1, 5, -9])
a[a < 0] = 0
print(a)
$ python practice_clip.py
[3 0 4 0 5 0]
A mask on the left-hand side of an assignment writes into the original array.
Solution 3
import numpy as np
counts = np.zeros(5, dtype=int)
np.add.at(counts, [1, 3, 3, 0, 3, 1], 1)
print(counts)
$ python practice_counts.py
[1 2 0 3 0]
Each of the three 3s was counted; counts[[...]] += 1 would have given 1 at index 3.
Solution 4
import numpy as np
arr = np.array([7, 4, 9, 6, 2])
arr[np.flatnonzero(arr % 2 == 0)[0]] = 100
print(arr)
$ python practice_first_even.py
[ 7 100 9 6 2]
np.flatnonzero turns the mask into positions, and a single integer index on the left-hand
side writes into arr itself.