NumPy·Lesson 8·12 min·0/4 exercises

Fancy indexing

Indexing with a list of positions — and why two lists zip instead of crossing.

a[[0, 2]]a[rows, cols]np.ix_np.takenp.add.atalways copies

Watch it happen

Play it through, or step back and forth yourself.

v
0
1
2
3
10
20
30
40
shape (4,)
[3, 0, 0, 1]
3
0
0
1
positions, not values
v[[3, 0, 0, 1]]
0
1
2
3
40
10
10
20
shape (4,)

Instead of a range, hand the brackets a list of positions. v[[3, 0, 0, 1]] takes position 3, then 0, then 0 again, then 1. Any order you like, repeats allowed — and the result has the shape of the index list, not of v.

The idea

The third way to index. Instead of a single position or a range, hand the brackets an array of positions:

v[[3, 0, 0, 1]]     # position 3, then 0, then 0 again, then 1

Any order you like, repeats allowed, and the result takes the shape of the index array rather than of v. That's what makes it the tool for reordering, sampling and shuffling: x[rng.permutation(len(x))] shuffles an array, and x[np.argsort(keys)] sorts one array by another.

Two index arrays zip — they don't cross

This is the part everyone gets wrong once. On a 2-D array:

a[[0, 2]]            # rows 0 and 2 — shape (2, 4)
a[[0, 2], [1, 0]]    # elements (0,1) and (2,0) — shape (2,)

The second one is not "rows 0 and 2, columns 1 and 0". The two lists are paired up positionally, like zip: first row index with first column index, second with second. You asked for two coordinates, so you get two values.

If you wanted the 2×2 sub-grid, say so:

a[np.ix_([0, 2], [1, 0])]    # shape (2, 2)
a[[0, 2]][:, [1, 0]]         # same result, one extra copy
Hand-drawn notes showing that two index lists are zipped into pairs of coordinates giving two elements, while np.ix_ crosses them to give a block.

Why zipping is the sensible rule

It looks arbitrary until you know the mechanism: the index arrays broadcast against each other, by the same rules as lesson 13. Two (2,) arrays broadcast to (2,), which pairs them elementwise. Give the first one a second axis and it stretches into a grid instead:

rows = np.array([0, 2])
a[rows[:, None], [1, 0]]     # (2,1) against (2,) -> (2,2)

That's precisely what np.ix_ builds for you. Once you see it as broadcasting, the behaviour stops being a special case and becomes the only consistent option.

It always copies

Unlike basic slicing, fancy indexing always returns a copy. Rows 0 and 2 aren't evenly spaced, so no stride describes them — there is nothing to make a view out of.

Which leads to a subtle trap when the same position appears twice:

x = np.zeros(3)
x[[0, 0, 0]] += 1
x                      # [1. 0. 0.]  — not [3. 0. 0.]

x[idx] += 1 is really "fetch, add one, write back". The fetch happens once, produces [0, 0, 0], adds one to give [1, 1, 1], and writes all three into position 0 — so the last write wins. When you genuinely want to accumulate, use the unbuffered form:

np.add.at(x, [0, 0, 0], 1)   # [3. 0. 0.]

That comes up whenever you're building a histogram by hand or scattering values into bins.

Hand-drawn notes showing that repeating an index in an in-place add only applies once, and that np.add.at is what accumulates.

Assignment

Fancy indexing works on the left-hand side too, and there it does write into the original:

a[[0, 2]] = 0            # zero out rows 0 and 2
a[[0, 2], [1, 0]] = -1   # set two specific elements

Reading gives you a copy; writing edits the array. Not a contradiction — the copy is the result of a read, and there's no result to copy when you're assigning.

Practice

Write it yourself. The answer is there when you want it.

Putting the kettle on…

Starting up…

Write it yourself

not graded

Index v with the list [3, 0, 0, 1], and a with [0, 2], printing both. Then show the difference between a[[0, 2], [1, 0]], which zips the lists into two elements, and np.ix_, which crosses them into a block. Finish with the duplicate trap: zeros of length 3, x[[0, 0, 0]] += 1, then print x.

Write something and press Run — the output appears here.

Your turn

4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

From v, return the values at positions 2, 0 and 3, in that order.

your answer

Return rows 0 and 3 of cups, stacked.

your answer

Return just two elements of a: the one at (0, 3) and the one at (2, 0). The result should be a 1-D array of length 2.

your answer

Now the sub-grid: rows 0 and 2 crossed with columns 3 and 0, shape (2, 2).

your answer