A slice and an index array, doing superficially similar things with entirely different consequences.
import numpy as np grid = np.array([[1, 2], [3, 4], [5, 6]]) grid[0:2] # -> [[1, 2], [3, 4]] a VIEW, evenly spaced grid[0:2].base is grid # -> True grid[[2, 0]] # -> [[5, 6], [1, 2]] a COPY, any order grid[[2, 0]].base is None # -> True, it owns its data grid[[1, 1, 0]] # -> [[3, 4], [3, 4], [1, 2]] repeats are fine # three rows out of a three-row array, one of them twice — which is why # the result cannot possibly be a window onto the original.
- A view has to be describable as a stride, and this is not
grid[0:2] is 'start here, step by one, stop there' — numpy can serve that by changing how it reads the same memory. grid[[2, 0]] is an arbitrary permutation, and grid[[1, 1, 0]] has more rows than the original: neither can be expressed as a stride, so numpy allocates. That is the whole rule, and it means fancy indexing is never a trap the way slicing is.
- The result is as long as the order, not as long as the array
Ten positions out of a three-row array gives ten rows. That is what makes bootstrap sampling a one-liner — sample with replacement by generating random positions — and it is also why the output shape depends on the index rather than on the input.
- It composes with argsort, which is where it earns its keep
grid[np.argsort(grid[:, 1])] sorts the rows by their second column: argsort gives the order and fancy indexing applies it. Nearly everything you want to do to the rows of an array is 'compute an order, then index by it', which is why these two functions are usually seen together.
One expression. numpy will take an array of positions wherever it takes a single one, and the result is built for you.
grid[order] indexes the first axis with every position in order. It already copies, already supports repeats and negatives, and already raises for a position that does not exist.
Do not loop and stack the rows yourself — that is slower, loses the dtype if you go via lists, and does not support negative positions. And do not add a .copy(): fancy indexing has already allocated, so it would be a second copy for nothing.
import numpy as np def reorder(grid, order): return grid[____]
import numpy as np
def reorder(grid, order):
# Indexing the first axis with an array of positions. It copies, so the
# result is already independent — no .copy() needed, unlike a slice.
return grid[order]
The views exercise needed an explicit .copy() because slicing hands back a window; this needs none, because an arbitrary permutation cannot be a window. Same syntax, opposite guarantee — and the way to tell them apart is not the brackets but what is inside them: a slice object is a view, an array of positions is a copy.
Adding one would be harmless and would double the memory traffic of a function whose whole job is to move rows around. Knowing which operations already copied is what stops defensive copying from quietly becoming the expensive part of a pipeline.
grid[rng.permutation(len(grid))] shuffles, grid[np.argsort(grid[:, 1])] sorts by a column, and grid[rng.integers(0, len(grid), len(grid))] is a bootstrap sample. All three are this function with a different order — which is why numpy has no separate shuffle-rows or sort-rows call.
If you want an evenly spaced selection — every other row, the first hundred — use a slice: it is free where this allocates. If you are indexing both axes with arrays, read the broadcasting rules for fancy indexing first, because grid[[0, 1], [0, 1]] gives you two single elements rather than a 2×2 block, which surprises everybody once. np.ix_ builds the block version. And for a table with named columns, pandas' .iloc says the same thing with the axis made explicit, which is worth the dependency as soon as somebody else has to read it.
- Sort the rows by their second column using argsort and this function, and check that a stable sort keeps equal values in their original order.
- Take a bootstrap sample — positions drawn with replacement — and confirm some rows appear twice and others not at all.
- Try grid[[0, 1], [0, 1]] and np.ix_([0, 1], [0, 1]) on the same array and work out why they return different shapes.