NumPy Indexing

NumPy Indexing and Slicing

1NumPy Indexing: overview

NumPy indexing is how you pick values out of an array: by position and slice (arr[row, col]), by a True/False mask, or by a list of positions, and the way you pick decides whether you get a view of the original data or an independent copy.

GoalBy the end you can select single values, rows, columns and blocks of a 2-D array, tell views from copies, filter and update values with combined masks, and use fancy indexing, np.where and argsort to find and reorder data.

Before this lesson: Python Slicing, NumPy Basics

2NumPy Indexing: diagram

sales = np.array(...) # shape (3, 4)sales = np.array(...) # shape (3, 4)01230121511141252726302891410sales[:, 2] is the whole highlighted column. sales[1, 2] is the one value where row 1 meets column 2: 26.
A 2-D array is a grid addressed as [row, column]. The grey numbers on the left are row positions and the header numbers are column positions, both starting at 0. A colon means "every row" or "every column".

3NumPy Indexing: explained step by step

Once your data is in an array, most of the work is choosing parts of it: one reading, a whole column, only the rows that match a rule. NumPy gives you four ways to choose:

  • by position: arr[2] or arr[1, 3]
  • by slice: arr[1:4] or arr[:, 2]
  • by boolean mask: arr[arr > 10]
  • by a list of positions (called fancy indexing): arr[[0, 3, 3]]

They differ in one detail that causes real bugs: some hand you a view of the original data and some hand you a separate copy. You will see which is which below.

Positions and slices

A 1-D array indexes just like a list: the first item is 0, the last is -1, and a slice arr[start:stop:step] stops before stop.

Python
import numpy as np

temps = np.array([18, 21, 24, 19, 26, 24, 20])
print(temps[0], temps[-1])
print(temps[1:4])
print(temps[::2])
print(temps[::-1])

A 2-D array needs one index per dimension, separated by a comma: grid[row, column]. A slice in either place keeps that dimension, and a lone colon means "everything along this axis".

Python
import numpy as np

grid = np.arange(1, 13).reshape(3, 4)
print(grid[1, 2])
print(grid[1])
print(grid[:, 2])
print(grid[0:2, 1:3])

grid[1][2] also works, but it first builds the whole row and then indexes that row. Write grid[1, 2] instead: it is one step and it is the only form that can slice both axes at once.

A slice is a view, not a copy

Copying a million numbers every time you slice would be slow, so NumPy does not do it. A slice is just another window onto the same memory. Change the window and you change the original.

Python
import numpy as np

prices = np.array([10, 20, 30, 40, 50])
window = prices[1:4]
window[0] = 99
print(prices)

safe = prices[1:4].copy()
safe[0] = -1
print(prices, safe)

Call .copy() whenever you want an independent array you can edit freely. Masks and fancy indexing work the other way round: they always return a copy, so editing the result never touches the original.

Boolean masks in depth

A comparison gives a mask: one True or False per value. Combine masks with & (and), | (or) and ~ (not), and put parentheses around every comparison, because & binds more tightly than > or ==. Never use Python's and / or here: they need a single True or False and raise ValueError: The truth value of an array with more than one element is ambiguous.

A mask on the left of an assignment changes the matching values in place. Because True counts as 1, mask.sum() counts matches and mask.mean() gives their fraction.

Python
import numpy as np

scores = np.array([72, -1, 90, 55, -1, 68, 101])
bad = (scores < 0) | (scores > 100)
print("Invalid:", bad.sum(), "of", scores.size)
scores[bad] = 0
print(scores)
print("Share passing:", (scores >= 60).mean())

On a 2-D array a mask built from one column picks whole rows. With table[table[:, 1] > 60] the mask has one entry per row, so every row where column 1 is above 60 comes back intact.

Python
import numpy as np

marks = np.array([[101, 72], [102, 58], [103, 91]])
print(marks[marks[:, 1] > 60])

Fancy indexing and finding positions

Give an array a list of positions and you get those items, in that order, repeats allowed. Sometimes you need the positions themselves:

  • np.where(mask) returns the positions where the mask is True.
  • arr.argmax() and arr.argmin() return the position of the largest and smallest value.
  • arr.argsort() returns the positions that would sort the array. Use them to reorder another array, such as names ordered by score.
  • np.sort(arr) returns a sorted copy, and arr.sort() sorts in place.
Python
import numpy as np

names = np.array(["Asha", "Ben", "Chen", "Dina"])
scores = np.array([78, 45, 91, 62])
print(names[[2, 0, 2]])
print(np.where(scores > 60)[0])
print(names[scores.argsort()])
print(names[scores.argmax()])

When the index is out of bounds

A single position that does not exist raises IndexError: index 5 is out of bounds for axis 0 with size 4. The message names the axis and the size of that axis, which tells you straight away which of the indexes in grid[row, col] is too big. Slices are forgiving: arr[2:100] quietly stops at the end of the array.

Quick reference: NumPy Indexing

  • Python NumPy Cheat Sheet — NumPy stores numbers in fixed-type arrays and applies operations to every element at once: build with `np.arr…

4NumPy Indexing: syntax

import numpy as np

arr[i]                    # one item (negative i counts from the end)
arr[start:stop:step]      # slice -> VIEW of the same memory
grid[row, col]            # one value in a 2-D array
grid[:, col]              # a whole column
grid[row, :]              # a whole row
grid[r1:r2, c1:c2]        # a block

view = arr[1:4]           # edits to view change arr
safe = arr[1:4].copy()    # independent copy

arr[arr > 10]             # boolean mask -> COPY of the matches
arr[(a > 1) & (a < 9)]   # combine with & | ~ (parentheses!)
arr[mask] = 0             # assign through a mask
mask.sum()                # count the True values

arr[[0, 3, 3]]            # fancy indexing (list of positions) -> COPY
np.where(mask)[0]         # positions where mask is True
arr.argmax(), arr.argmin()
arr.argsort()             # positions that would sort arr
np.sort(arr)              # sorted copy

5NumPy Indexing: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
import numpy as np

# rows = shops, columns = Monday to Friday
sales = np.array([[12, 15, 11, 14, 20],
                  [25, 27, 26, 30, 28],
                  [ 8,  9, 14, 10, 13],
                  [19, 22, 18, 21, 24]])
shops = np.array(["North", "South", "East", "West"])
print("Friday, every shop:", sales[:, 4])
print("East, Wednesday to Friday:", sales[2, 2:])
weekly = sales.sum(axis=1)
print("Busy shops:", shops[weekly > 80])
print("Best first:", shops[np.argsort(weekly)[::-1]])

sales[sales < 12] = 12
print("Lowest day now:", sales.min())
Expected outputFriday, every shop: [20 28 13 24]
East, Wednesday to Friday: [14 10 13]
Busy shops: ['South' 'West']
Best first: ['South' 'West' 'North' 'East']
Lowest day now: 12

7NumPy Indexing: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 3-7A 2-D array: each inner list is one shop (a row) and each position is a weekday (a column). The comment above it says which axis means what, which is worth writing down for any grid.
  2. Line 8shops is a separate 1-D array of names. Row 0 of sales belongs to shops[0], row 1 to shops[1], and so on.
  3. Line 9sales[:, 4] means every row, column 4: Friday's figure for each shop. The result is a 1-D array of 4 numbers.
  4. Line 10sales[2, 2:] is row 2 (East), columns 2 to the end: Wednesday, Thursday and Friday.
  5. Line 11sum(axis=1) adds across each row, giving one weekly total per shop: 72, 136, 54 and 104.
  6. Line 12weekly > 80 is a mask with one True/False per shop. Putting that mask inside shops[...] keeps the names of the shops where it is True.
  7. Line 13argsort returns the shop positions from smallest to largest weekly total. [::-1] reverses them, and using those positions as an index reorders the shop names, best first.
  8. Line 15A mask on the left of an assignment changes the array in place: every value below 12 becomes 12. This is a copy-free way to set a floor.
  9. Line 16min() is now 12 because no value is lower than the floor. The original array was modified, which is why masks on the left of = need care.

8NumPy Indexing: common mistakes

Indexing past the end of an axis

✗ Wrong

import numpy as np
grid = np.zeros((3, 4))
print(grid[3, 0])
IndexError: index 3 is out of bounds for axis 0 with size 3

✓ Right

import numpy as np
grid = np.zeros((3, 4))
print(grid[-1, 0])

The message names the axis and its size. grid has 3 rows (axis 0), so valid row indexes are 0, 1 and 2. Use grid[2, 0] or grid[-1, 0] for the last row. Remember that columns are axis 1, with 4 positions here.

Forgetting the parentheses around each comparison

✗ Wrong

import numpy as np
temps = np.array([12, 18, 25, 31])
print(temps[temps > 15 & temps < 30])
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

✓ Right

import numpy as np
temps = np.array([12, 18, 25, 31])
print(temps[(temps > 15) & (temps < 30)])

& is evaluated before > and <, so Python tries to compute 15 & temps first and then chains the comparisons. Wrap every comparison in its own parentheses.

Editing a slice and changing the original

✗ Wrong

import numpy as np
scores = np.array([50, 60, 70, 80])
first_two = scores[:2]
first_two[0] = 0
print(scores)
No error, but it prints [ 0 60 70 80]: the original array changed too

✓ Right

import numpy as np
scores = np.array([50, 60, 70, 80])
first_two = scores[:2].copy()
first_two[0] = 0
print(scores)

A slice is a view onto the same memory, so writing through it writes to the original. Call .copy() when you need data you can change safely.

Assigning to a mask result in two steps

✗ Wrong

import numpy as np
scores = np.array([40, 75, 30, 90])
scores[scores < 50][0] = 50
print(scores)
No error, but it prints [40 75 30 90]: nothing was changed

✓ Right

import numpy as np
scores = np.array([40, 75, 30, 90])
scores[scores < 50] = 50
print(scores)

scores[scores < 50] makes a copy, and [0] = 50 then edits that temporary copy, which is thrown away. Put the mask directly on the left of the assignment instead, so NumPy writes into the original.

Reading argsort() as if it returned sorted values

✗ Wrong

import numpy as np
scores = np.array([70, 90, 60])
print(scores.argsort())
No error, but it prints [2 0 1]: positions, not scores

✓ Right

import numpy as np
scores = np.array([70, 90, 60])
order = scores.argsort()
print(scores[order])

argsort() returns the positions that would put the array in order. Use them as an index to get the sorted values, or call np.sort(scores) directly.

9NumPy Indexing: exercises

Write your answer, then press Check. The checks run in your browser.

1. Slice a seating grid

Write code Easy

seats is a 2-D array: each row is a row of seats in a hall. Write two functions:

  • back_row(grid) returns the last row of grid.
  • first_two_columns(grid) returns the first two columns of every row, so its shape is (number_of_rows, 2).

Both must work for a grid of any size.

Your code
import numpy as np

seats = np.arange(1, 21).reshape(4, 5)

def back_row(grid):
    pass

def first_two_columns(grid):
    pass

print(back_row(seats))
print(first_two_columns(seats))

2. Predict views, copies and fancy indexing

Predict the output Medium

Read the code and type exactly what it prints. Think about which lines change prices and which only change a copy.

Read this code
import numpy as np

prices = np.array([10, 20, 30, 40, 50])
cheap = prices[:3]
cheap[0] = 99
safe = prices[1:4].copy()
safe[0] = -1
print(prices)
print(safe)
print(prices[[4, 0, 4]])
print(prices[prices > 25].sum())

2 more exercises → 6 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

Why did changing my slice also change the original NumPy array?

Slices are views: they look at the same memory as the original instead of copying it, which keeps slicing fast. Writing through the slice therefore writes to the original. Use arr[1:4].copy() when you want an independent array. Boolean masks and fancy indexing always return copies.

What is the difference between arr[1][2] and arr[1, 2] in NumPy?

Both read the same value in a 2-D array, but arr[1][2] first builds the whole row arr[1] and then indexes it, while arr[1, 2] does it in one step. Only the comma form can slice both axes at once, for example arr[0:2, 1:3], so use it as your default.

Should I use a boolean mask or np.where to find matching values?

Use the mask itself (arr[arr > 5]) when you want the matching values or rows. Use np.where(mask)[0] when you need their positions, for example to look up the same positions in another array. np.where(mask, a, b) is a third form that picks a where the mask is True and b elsewhere.

Further reading

10Next lesson

Up nextNumPy Math and BroadcastingNumPy maths applies functions and summaries to whole arrays, and broadcasting lets arrays of different shapes work toge…

Related:
Python ListsPython SlicingNumPy BasicsNumPy Math and Broadcasting

← NumPy Basics · All Data Analysis lessons · Practise in the IDE