Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

NumPy & SciPy for Scientific Computing  Module 2 – Indexing, reshaping and vectorised operations

Boolean masks and fancy indexing

Filter NumPy arrays with boolean masks and np.where, pick rows, pairs and grids with integer arrays and take_along_axis, and count repeats with np.add.at.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, numpy 2.5.3, scipy 1.18.1 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Filter, count and replace values with boolean masks combined with &, | and ~
  • Find positions with np.nonzero and choose values element by element with np.where
  • Pick rows, pairs and sub-grids with integer arrays and np.ix_, and predict the result's shape
  • Sort and pick values within each row with argsort, take_along_axis and put_along_axis
  • Count and sum with repeated indices correctly using np.add.at or np.bincount

Before you start

What you’ll learn · Before you start

What you will learn

  • Filter, count and replace values with boolean masks combined with &, | and ~
  • Find positions with np.nonzero and choose values element by element with np.where
  • Pick rows, pairs and sub-grids with integer arrays and np.ix_, and predict the result's shape
  • Sort and pick values within each row with argsort, take_along_axis and put_along_axis
  • Count and sum with repeated indices correctly using np.add.at or np.bincount

Before you start

On this page

Twelve customers rated a product from 1 to 5 stars, and counts[ratings] += 1 produced a histogram whose bars add up to 5, not 12. Indexing with an array of positions, or with a mask of True and False, is one of NumPy’s most useful tools, and this is its best-known trap: each star value was counted once, however often it appeared.

You will filter and replace values with masks, combine conditions without the errors Python’s and causes, find positions with np.nonzero, pick rows, pairs and grids with integer arrays, choose a different position in each row, and count repeats correctly with np.add.at and np.bincount.

A boolean mask selects, counts and replaces values

A comparison on an array compares every element at once and returns an array of True and False with the same shape. Used as an index, that boolean mask: An array of True and False, usually from a comparison such as a > 50, used as an index: a[mask] gives the values where it is True as a new 1-D array, and a[mask] = v writes v there. keeps the elements where it is True. Here are eight readings from a temperature sensor, one a minute, with three glitches among them. Step through the comparisons, the mask and what it gives:

Interactive · step by step Eight readings through one mask Step through the comparisons and the mask. Each left box holds eight values, one per reading; T is True and F is False.
Eight readings through one mask, step 7 of 7. Showing readings (21 nan 22 nan 23 24 nan 22), readings < -10 (F F F T F F F F), readings > 50 (F T F F F F T F), glitch = low | high (F T F T F F T F), readings[glitch] (85 -40 99, a copy), glitch.sum() (3 glitches; good mean 22.4). In focus: readings, glitch = low | high.readings21 nan 22 nan 23 24 nan 22readings < -10F F F T F F F Freadings > 50F T F F F F T Fglitch = low | highF T F T F F T Freadings[glitch]85 -40 99, a copyglitch.sum()3 glitches; good mean 22.4

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Eight readings. One reading a minute from a sensor. Three of them, 85, -40 and 99, are glitches: no room is that hot or that cold.
  2. One comparison. readings < -10 compares every reading at once and gives a new array of True and False, one value for each reading. Only position 3, the -40, is True.
  3. A second comparison. readings > 50 is True at positions 1 and 6, the 85 and the 99.
  4. Combined with |. low | high is True wherever either is True: positions 1, 3 and 6. This array of True and False is the mask.
  5. Selecting. readings[glitch] keeps the readings where the mask is True, in order, in a new 1-D array: 85, -40 and 99. It shares no memory with readings.
  6. Counting. True counts as 1 and False as 0, so glitch.sum() is the number of glitches, 3. The other five, readings[~glitch], have a mean of 22.4.
  7. Replacing. readings[glitch] = np.nan is an assignment, so it writes into readings itself: the three glitches become NaN and the rest stay as they were.

The run does the same in NumPy:

Glitches found, counted and replaced with one mask Python · masks/glitches.py
"""Find, count and replace glitches in sensor readings with a boolean mask."""
import numpy as np

readings = np.array([21.0, 85.0, 22.0, -40.0, 23.0, 24.0, 99.0, 22.0])  # degrees C, one a minute
low = readings < -10
high = readings > 50
glitch = low | high  # True where either is True
print("low:   ", low)
print("high:  ", high)
print("glitch:", glitch)
print("readings[glitch]:", readings[glitch], "| glitch.sum():", glitch.sum())
print("mean of the good readings:", readings[~glitch].mean())
print("shares memory with readings:", np.shares_memory(readings, readings[glitch]))
readings[glitch] = np.nan  # an assignment: it writes into readings
print("after readings[glitch] = np.nan:", readings)

Output

low:    [False False False  True False False False False]
high:   [False  True False False False False  True False]
glitch: [False  True False  True False False  True False]
readings[glitch]: [ 85. -40.  99.] | glitch.sum(): 3
mean of the good readings: 22.4
shares memory with readings: False
after readings[glitch] = np.nan: [21. nan 22. nan 23. 24. nan 22.]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 glitches.py

readings[glitch] is a new 1-D array of the three glitches: reading through a mask is advanced indexing: Indexing with an array of integers or of booleans, such as a[[0, 2]] or a[a > 5]. Reading with it always makes a copy; on the left of =, it writes into the array. More in “Indexing, slicing and views”, so it copies, and the run printed shares memory with readings: False. Because True counts as 1 and False as 0, glitch.sum() counts them, and ~glitch turns the mask round, so readings[~glitch].mean() is the mean of the five good readings, 22.4. On the left of = the same mask writes into readings: the three glitches became NaN in place.

A mask of the array’s own shape always gives a 1-D result, whatever the array’s shape, because the selected values need not form rows or columns. NumPy’s guide states it for a mask with as many dimensions as the array: the result is “a 1-dimensional array”. When the shape must stay, choose values with np.where instead, two sections below.

Key idea

A mask is an array of True and False: read through it for a copy of the selected values, sum it to count them, and assign through it to change them where they are.

Checkpoint

Check: Boolean masks

  1. Question 1 of 2 x = np.array([12, 55, 31, 60, 8]). What is (x > 30).sum()?

    Type a number.

    A hint

    Write down the mask first, then count its True values.

    Show the answer: Question 1 of 2

    Answer: 3 values

    The mask is [False, True, True, True, False]: 55, 31 and 60 are above 30. True counts as 1 in a sum, so the result is 3.

  2. Question 2 of 2 x = np.array([4, -2, 7, -5]), then x[x < 0] = 0. What is x?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [4, 0, 7, 0]

    Assignment through a mask changes the original in place and keeps its shape.

Combine conditions with bitwise operators, in parentheses

Masks combine element by element with & (and), | (or) and ~ (not), and each comparison needs parentheses of its own. The run shows three combinations that work and the two ways it usually goes wrong:

Three combined masks, and two lines that fail Python · combine/conditions.py
"""Conditions combine with &, | and ~, each comparison in parentheses; and, or and missing parentheses fail."""
import numpy as np

readings = np.array([21.0, 85.0, 22.0, -40.0, 23.0, 24.0, 99.0, 22.0])
print("in 0 to 30:      ", readings[(readings >= 0) & (readings <= 30)])
print("outside 0 to 30: ", readings[(readings < 0) | (readings > 30)])
print("not above 50:    ", readings[~(readings > 50)])
try:
    readings[(readings >= 0) and (readings <= 30)]
except ValueError as error:
    print("with and:", type(error).__name__ + ":", error)
try:
    readings[readings >= 0 & readings <= 30]
except TypeError as error:
    print("without parentheses:", type(error).__name__ + ":", error)

Output

in 0 to 30:       [21. 22. 23. 24. 22.]
outside 0 to 30:  [ 85. -40.  99.]
not above 50:     [ 21.  22. -40.  23.  24.  22.]
with and: ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
without parentheses: TypeError: ufunc 'bitwise_and' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conditions.py

and fails because Python’s and asks for one True or False of the array on its left, and an array of eight booleans has no single truth value: hence the ValueError that suggests a.any() or a.all(). Without parentheses the error is a different one. In Python’s table of operator precedence & binds more tightly than >= and <=, so readings >= 0 & readings <= 30 means readings >= (0 & readings) <= 30, and 0 & readings asks for a bitwise and of an integer with floats, which NumPy refuses with a TypeError. On an integer array the & would get through and the line would fail later, with the ambiguous truth value error of a chained comparison.

Writing a range test as Python would

0 <= r <= 30 and (r >= 0) and (r <= 30) both raise the ambiguous truth value error on an array, because each hides an and. Write (r >= 0) & (r <= 30); np.logical_and(r >= 0, r <= 30) says the same in words.

Checkpoint

Check: Combining conditions

  1. Question 1 of 2 Which line raises "The truth value of an array with more than one element is ambiguous"?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: x[(x > 0) and (x < 5)]

    and, or and not work on single truth values. For masks use &, | and ~, with each comparison in parentheses.

  2. Question 2 of 2 r is a float array. Why does r[r >= 0 & r <= 30] raise a TypeError about bitwise_and?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Python applies & before the comparisons, so it computes 0 & r, a bitwise and of an int with floats

    Write (r >= 0) & (r <= 30). With integer arrays the same mistake gets past the & and then fails as a chained comparison, with the "truth value is ambiguous" ValueError.

np.nonzero gives positions, and np.where chooses values

A mask says which elements; sometimes you need where they are. np.nonzero(mask) returns the positions of the True values, as a tuple with one array of indexes per axis. np.where does two jobs: given only a condition it is a shorthand for nonzero (its reference says to prefer nonzero then), and given three arguments, np.where(cond, x, y) builds a new array that takes x where the condition is True and y elsewhere:

Positions, a choice of values, and a value that does not fit Python · where/positions.py
"""Positions with np.nonzero, an element-wise choice with np.where, and a Python int that does not fit."""
import numpy as np

readings = np.array([21.0, 85.0, 22.0, -40.0, 23.0, 24.0, 99.0, 22.0])
glitch = (readings < -10) | (readings > 50)
print("np.nonzero(glitch):", np.nonzero(glitch))
print("np.where(glitch):  ", np.where(glitch))
cleaned = np.where(glitch, np.nan, readings)  # a new array: readings keeps its values
print("np.where(glitch, np.nan, readings):", cleaned)
print("readings[1] is still", readings[1])

grid = np.array([[3, 9, 1], [7, 2, 8]])
rows, cols = np.nonzero(grid > 5)
print("grid > 5 at rows", rows, "and columns", cols, "->", grid[rows, cols])

small = np.zeros(3, dtype=np.int8)  # int8 holds -128 to 127
try:
    np.where([True, False, True], 300, small)
except OverflowError as error:
    print("OverflowError:", error)

Output

np.nonzero(glitch): (array([1, 3, 6]),)
np.where(glitch):   (array([1, 3, 6]),)
np.where(glitch, np.nan, readings): [21. nan 22. nan 23. 24. nan 22.]
readings[1] is still 85.0
grid > 5 at rows [0 1 1] and columns [1 0 2] -> [9 7 8]
OverflowError: Python integer 300 out of bounds for int8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 positions.py

The glitches are at positions 1, 3 and 6, and both calls printed the same tuple. On a 2-D array there is one array per axis: the values above 5 in grid are at rows [0 1 1] and columns [1 0 2], and those two arrays used together as an index, grid[rows, cols], give the values back, [9 7 8]. np.where(glitch, np.nan, readings) keeps all eight positions and leaves readings as it was: readings[1] is still 85.0.

np.where refuses a Python int that does not fit, from NumPy 2.5

np.where(mask, 300, small) gives an int8 result, which holds -128 to 127, so NumPy 2.5.3 raised OverflowError: Python integer 300 out of bounds for int8. The 2.5 release notes say such values used to be “silently truncated”. Pyodide in the browser ships NumPy 2.4.6, so pass an array of the dtype you want, or check the range first.

Checkpoint

Check: np.nonzero and np.where

  1. Question 1 of 2 m has shape (3, 4). What are the shapes of m[m > 5] and np.where(m > 5, m, 0)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: (6,) and (3, 4)

    Use a mask to pull values out, and np.where(cond, x, y) to keep the array's shape while replacing values.

  2. Question 2 of 2 What does np.nonzero(np.array([[3, 9, 1], [7, 2, 8]]) > 5) return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Two arrays: rows [0, 1, 1] and columns [1, 0, 2]

    nonzero returns a tuple of index arrays, one per axis, which can be used straight back as an index.

An array of integers picks positions in any order

An index array lists the positions you want, in the order you want them, repeats allowed: m[[2, 0, 2]] gives rows 2, 0 and 2 again, and m[:, [3, 0]] gives every row with column 3 first. That is fancy indexing: Indexing with arrays of integers, such as a[[2, 0, 2]]: the result holds the values at those positions, in that order and with repeats, as a new array. One index array per axis pairs the positions up.. When you give one index array per axis, NumPy pairs them up position by position, which surprises nearly everyone the first time. Step through the same two lists used as pairs and as a grid:

Interactive · step by step Two index arrays, as pairs or as a grid Step through the two indexes. The left column holds the rows of m; the right column shows what each index picks from them.
Two index arrays, as pairs or as a grid, step 5 of 5. Showing row 0, row 1, row 2, m[[0, 2], [1, 3]] ([1, 11]: two pairs), np.ix_ grid ([[1, 3], [9, 11]]).col 1col 3row 00 1 2 3row 14 5 6 7row 28 9 10 11m[[0, 2], [1, 3]][1, 11]: two pairsnp.ix_ grid[[1, 3], [9, 11]]

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Two lists of positions. m[[0, 2], [1, 3]] gives one list of rows, [0, 2], and one list of columns, [1, 3]. NumPy reads them side by side, as pairs of positions.
  2. The first pair. The first pair is row 0 with column 1: the value 1.
  3. The second pair. The second pair is row 2 with column 3: the value 11. Two pairs give two values, [1, 11], a 1-D array and not a 2 by 2 block.
  4. Every row with every column. np.ix_([0, 2], [1, 3]) turns the lists into shapes (2, 1) and (1, 2), so every row is paired with every column: four positions instead of two.
  5. The shape follows the index. Pairs gave the shape of the lists, (2,); the grid gave (2, 2), rows 0 and 2 by columns 1 and 3. Row 1 is in neither result. Both are new arrays, copies.

The run shows the pairs, the grid and two rules about shapes:

Rows in any order, pairs, a grid with np.ix_, and two shapes Python · fancy/pick.py
"""Integer arrays pick positions in any order, pair up across axes, or make a grid with np.ix_."""
import numpy as np

m = np.arange(12, dtype=np.int64).reshape(3, 4)
print(m)
print("m[[2, 0, 2]]:", m[[2, 0, 2]].tolist())
print("m[:, [3, 0]]:", m[:, [3, 0]].tolist())
print("m[[0, 2], [1, 3]]:", m[[0, 2], [1, 3]].tolist())
print("m[np.ix_([0, 2], [1, 3])]:", m[np.ix_([0, 2], [1, 3])].tolist())
idx = np.array([[0, 1], [1, 2]])
print("m[idx].shape:", m[idx].shape)
x = np.zeros((2, 3, 4))
print("x[0, :, [0, 1]].shape:", x[0, :, [0, 1]].shape)
print("x[0][:, [0, 1]].shape:", x[0][:, [0, 1]].shape)

Output

[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
m[[2, 0, 2]]: [[8, 9, 10, 11], [0, 1, 2, 3], [8, 9, 10, 11]]
m[:, [3, 0]]: [[3, 0], [7, 4], [11, 8]]
m[[0, 2], [1, 3]]: [1, 11]
m[np.ix_([0, 2], [1, 3])]: [[1, 3], [9, 11]]
m[idx].shape: (2, 2, 4)
x[0, :, [0, 1]].shape: (2, 3)
x[0][:, [0, 1]].shape: (3, 2)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pick.py

m[[0, 2], [1, 3]] is [m[0, 1], m[2, 3]], the two values 1 and 11. np.ix_ turns the lists into a column and a row of positions, so every listed row meets every listed column: [[1, 3], [9, 11]]. In general the result takes the shape of the index: m[idx] with a (2, 2) index gives (2, 2, 4), a row of 4 for each position of idx.

The last two lines show a rule from NumPy’s guide that catches people out. When advanced indexes are separated by a slice, as in x[0, :, [0, 1]], “the dimensions resulting from the advanced indexing operation come first”: the integer 0 and the list [0, 1] combine into one axis of length 2 at the front, so the shape is (2, 3). x[0][:, [0, 1]] indexes in two steps and gives (3, 2).

Checkpoint

Check: Integer index arrays

  1. Question 1 of 2 For the lesson's m (0 to 11 in 3 rows of 4), what is m[[1, 2], [0, 3]]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: [4, 11]

    One index array per axis means pairs of positions. For every combination, use m[np.ix_(rows, cols)].

  2. Question 2 of 2 What is the shape of m[np.ix_([0, 1, 2], [0, 3])]?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: (3, 2)

    np.ix_ gives the row list shape (3, 1) and the column list shape (1, 2), so together they reach (3, 2).

Pick a different position in each row

Sometimes every row needs its own positions: each player’s rounds sorted from worst to best, or each player’s two best rounds. np.argsort(scores, axis=1) gives each row the positions that sort it, and np.take_along_axis pairs row i of those positions with row i of the data. np.put_along_axis writes the same way:

Each row sorted, each row's top two, each row's best marked Python · along/per_row.py
"""Reorder each row, take each row's top two, and mark each row's best, with one index per row."""
import numpy as np

scores = np.array([[3, 9, 1, 7], [8, 2, 6, 4], [5, 5, 9, 1]])  # 3 players, 4 rounds each
order = np.argsort(scores, axis=1)  # for each row, the positions that sort it
print("argsort, axis=1:\n", order)
print("take_along_axis:\n", np.take_along_axis(scores, order, axis=1))
top2 = np.argsort(-scores, axis=1, kind="stable")[:, :2]  # largest first; a tie goes to the lower position
print("top two positions:\n", top2)
print("top two scores:\n", np.take_along_axis(scores, top2, axis=1))
best = np.argmax(scores, axis=1, keepdims=True)  # shape (3, 1)
marks = np.zeros_like(scores)
np.put_along_axis(marks, best, 1, axis=1)
print("1 at each row's best:\n", marks)
print("scores[:, top2].shape:", scores[:, top2].shape)

Output

argsort, axis=1:
 [[2 0 3 1]
 [1 3 2 0]
 [3 0 1 2]]
take_along_axis:
 [[1 3 7 9]
 [2 4 6 8]
 [1 5 5 9]]
top two positions:
 [[1 3]
 [0 2]
 [2 0]]
top two scores:
 [[9 7]
 [8 6]
 [9 5]]
1 at each row's best:
 [[0 1 0 0]
 [1 0 0 0]
 [0 0 1 0]]
scores[:, top2].shape: (3, 3, 2)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 per_row.py

The first player’s rounds [3, 9, 1, 7] sort through the positions [2, 0, 3, 1] into [1, 3, 7, 9]. For the top two, the run sorts -scores, so the largest come first, with kind="stable" so that equal values keep their order: the third player’s two 5s tie, and position 0 comes before position 1, giving [2, 0]. np.argmax(..., keepdims=True) keeps one position per row in a (3, 1) array, which put_along_axis uses to write a 1 at each row’s best round.

Indexing every row with every row's positions

scores[:, top2] looks right but has shape (3, 3, 2): the whole (3, 2) array of positions is applied to every row, 18 values instead of 6. Use np.take_along_axis(scores, top2, axis=1), which matches the rows up.

Checkpoint

Check: One position per row

  1. One question scores has shape (3, 4) and top2 shape (3, 2). What are the shapes of np.take_along_axis(scores, top2, axis=1) and scores[:, top2]?

    Choose one answer.

    Show the answer: One question

    Answer: (3, 2) and (3, 3, 2)

    When each row needs its own positions, use take_along_axis; plain indexing with an index array on one axis applies the whole array of positions to every row.

Repeated indices count once, unless you use np.add.at

Back to the ratings. counts[ratings] += 1 reads the selected values, adds 1 and writes them back, and NumPy’s guide says what that means for a repeated index: the new value “is assigned to x[1] three times, rather than being incremented 3 times”. Here is that order of events in plain Python, on four of the ratings:

Interactive · line by line What counts[ratings] += 1 does, step by step Step through the three loops. The flags under ratings and got are the positions each loop reads next; watch position 5 of counts on the last loop.

Program · trace/buffered_add.py

counts = [0, 0, 0, 0, 0, 0]
ratings = [5, 4, 5, 3]
# 1. read every position: a copy
got = []
for r in ratings:
    got.append(counts[r])
# 2. add 1 to each value read
for k in range(len(got)):
    got[k] += 1
# 3. write back, one by one
for r, g in zip(ratings, got):
    counts[r] = g
print(counts)

Variables

After line 13: counts is [0, 0, 0, 1, 1, 1]; ratings is [5, 4, 5, 3]; got is [1, 1, 1, 1]; r is 3; k is 3; g is 1.counts000102131415ratings50415233got10111213r3k3g1
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped
  • the stop value, never given

Printed so far

[0, 0, 0, 1, 1, 1]

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 32 steps are listed below.

Line run 13
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. counts = [0, 0, 0, 0, 0, 0] makes counts a list of 6 items.
  3. Line 2. ratings = [5, 4, 5, 3] makes ratings a list of 4 items.
  4. Line 4. got = [] makes got a list of 0 items.
  5. Line 5. for r in ratings: takes the item at position 0 of ratings: r is 5.
  6. Line 6. got.append(counts[r]) adds 0 to the end of got.
  7. Line 5. for r in ratings: takes the item at position 1 of ratings: r is 4.
  8. Line 6. got.append(counts[r]) adds 0 to the end of got.
  9. Line 5. for r in ratings: takes the item at position 2 of ratings: r is 5.
  10. Line 6. got.append(counts[r]) adds 0 to the end of got. Key point The third rating is 5 again. Nothing has been written yet, so counts[5] is still 0 and the copy holds 0 for position 5 a second time.
  11. Line 5. for r in ratings: takes the item at position 3 of ratings: r is 3.
  12. Line 6. got.append(counts[r]) adds 0 to the end of got.
  13. Line 5. for r in ratings: finds no item at position 4 of ratings (it has 4 items), so the loop ends.
  14. Line 8. for k in range(len(got)): gives its first number: k is 0.
  15. Line 9. got[k] += 1 changes got: it is now [1, 0, 0, 0]. Each value of the copy goes up by 1 on its own. Neither copy of position 5 knows about the other.
  16. Line 8. for k in range(len(got)): gives the next number: k is 1.
  17. Line 9. got[k] += 1 changes got: it is now [1, 1, 0, 0].
  18. Line 8. for k in range(len(got)): gives the next number: k is 2.
  19. Line 9. got[k] += 1 changes got: it is now [1, 1, 1, 0].
  20. Line 8. for k in range(len(got)): gives the next number: k is 3.
  21. Line 9. got[k] += 1 changes got: it is now [1, 1, 1, 1].
  22. Line 8. for k in range(len(got)): has no number left: a range stops before its stop value, 4, so the loop ends.
  23. Line 11. for r, g in zip(ratings, got): takes the items at position 0 of ratings and got together: r is 5 and g is 1.
  24. Line 12. counts[r] = g changes counts: it is now [0, 0, 0, 0, 0, 1]. The first write puts 1 at position 5.
  25. Line 11. for r, g in zip(ratings, got): takes the items at position 1 of ratings and got together: r is 4 and g is 1.
  26. Line 12. counts[r] = g changes counts: it is now [0, 0, 0, 0, 1, 1].
  27. Line 11. for r, g in zip(ratings, got): takes the items at position 2 of ratings and got together: r is 5 and g is 1.
  28. Line 12. counts[r] = g leaves counts as it was. The bug The second write to position 5 stores 1 again. It replaces the first write instead of adding to it, so a rating given twice is counted once.
  29. Line 11. for r, g in zip(ratings, got): takes the items at position 3 of ratings and got together: r is 3 and g is 1.
  30. Line 12. counts[r] = g changes counts: it is now [0, 0, 0, 1, 1, 1].
  31. Line 11. for r, g in zip(ratings, got): stops: ratings and got have no item at position 4, so zip makes no more pairs and the loop ends.
  32. Line 13. print(counts) prints [0, 0, 0, 1, 1, 1]. That was the last line, so the program ends. counts adds up to 3 for 4 ratings, one count for each different rating. np.add.at and np.bincount count all 4.

Both copies of position 5 were read as 0 before anything was written, so both writes stored 1. Press Edit, add another 4 to ratings and run it again: position 4 still ends at 1. np.add.at does the same addition unbuffered, one occurrence at a time, so that a position indexed more than once collects every addition. np.bincount counts the values of an integer array in one call. Here are four ways to count all twelve ratings:

A histogram of twelve ratings, four ways Python · repeats/histogram.py
"""A histogram of ratings four ways: a loop, counts[ratings] += 1, np.add.at and np.bincount."""
import numpy as np

ratings = np.array([5, 4, 5, 3, 5, 4, 2, 5, 4, 5, 1, 4])  # 12 ratings from 1 to 5 stars

loop = np.zeros(6, dtype=np.int64)
for r in ratings:
    loop[r] += 1
print("Python loop:         ", loop, "total", loop.sum())

fancy = np.zeros(6, dtype=np.int64)
fancy[ratings] += 1  # each star value is counted once, however often it appears
print("counts[ratings] += 1:", fancy, "total", fancy.sum())

at = np.zeros(6, dtype=np.int64)
np.add.at(at, ratings, 1)  # unbuffered: every occurrence adds 1
print("np.add.at:           ", at, "total", at.sum())

counts = np.bincount(ratings, minlength=6)
print("np.bincount:         ", counts, "total", counts.sum())
print("stars per value, with weights:", np.bincount(ratings, weights=ratings))
try:
    np.bincount(ratings.astype(np.float64))
except TypeError as error:
    print("np.bincount of floats: TypeError:", error)

Output

Python loop:          [0 1 1 1 4 5] total 12
counts[ratings] += 1: [0 1 1 1 1 1] total 5
np.add.at:            [0 1 1 1 4 5] total 12
np.bincount:          [0 1 1 1 4 5] total 12
stars per value, with weights: [ 0.  1.  2.  3. 16. 25.]
np.bincount of floats: TypeError: Cannot cast array data from dtype('float64') to dtype('int64') according to the rule 'safe'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 histogram.py

The loop, np.add.at and np.bincount agree, [0 1 1 1 4 5] with a total of 12, while counts[ratings] += 1 kept one count per star value, a total of 5. minlength=6 makes sure the result has a position for every star value, present or not, and weights adds a number per occurrence instead of 1: here the stars themselves, 16 for the four 4-star ratings and 25 for the five 5-star ones. Floats are refused with a TypeError, which the NumPy 2.5 release notes list as the end of a deprecation that began in 2.1.

Count Unique Values (Frequency Counter) Paste the twelve ratings, one per line, and compare its counts with the run's np.bincount: 1, 1, 1, 4 and 5 for one to five stars.

Key idea

With repeated indices, x[idx] += v adds once for each different position. Use np.bincount for counts and sums by integer key, and np.add.at (or another ufunc’s at) when every occurrence must count.

Checkpoint

Check: Repeated indices

  1. Question 1 of 2 x = np.zeros(4), idx = np.array([1, 1, 1, 3]), then x[idx] += 1. What is x.sum()?

    Type a number.

    A hint

    Each distinct position is written once, with the value it had plus 1.

    Show the answer: Question 1 of 2

    Answer: 2 counts

    Positions 1 and 3 each end up as 1, whatever the repeats, so the sum is 2. np.add.at(x, idx, 1) would give 4.

  2. Question 2 of 2 Which call counts every occurrence of idx = [1, 1, 1, 3] into a length-4 array, giving [0, 3, 0, 1]?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: np.bincount(idx, minlength=4)

    For counts and sums by position, np.bincount; for any other operation with repeated indices, ufunc.at, such as np.add.at or np.maximum.at.

Practise: count values and rank within rows

Two functions in plain Python rebuild what this lesson’s NumPy calls do: a counter that treats repeats as np.bincount does, and the top-k positions per row that argsort and take_along_axis give. Their sample tests run right here:

Code lab · 2 steps

Count repeats and find each row's best

Count every value as np.bincount does, then find the positions of each row's k largest values with ties to the lower position.

  1. Count every value, repeats included To do
  2. Find the positions of each row's k largest values To do

Exercise · Easy · Python

Count every value, repeats included

Write count_values(values, minlength=0) in counting.py, with Python's own lists (no NumPy needed). It does what np.bincount does: position v of the result holds how many times v appears in values, so every occurrence counts, repeats included.

  • The result has max(values) + 1 positions, or minlength if that is more; for no values at all it has minlength positions.
  • A negative value raises ValueError, and a value that is not an int, such as 2.0, raises TypeError, as NumPy 2.5 does.

The lesson's ratings give the first check: count_values([5, 4, 5, 3, 5, 4, 2, 5, 4, 5, 1, 4]) is [0, 1, 1, 1, 4, 5], twelve ratings in all, where counts[ratings] += 1 gave five.

Starter code · counting.py

def count_values(values, minlength=0):
    """Return a list whose position v holds how many times v appears in values (like np.bincount)."""
    counts = [0] * max(minlength, max(values, default=-1) + 1)
    for v in set(values):
        counts[v] = 1
    return counts
The sample tests · test_counting.py
from counting import count_values


def test_the_lessons_ratings():
    """counts all twelve ratings, repeats included"""
    assert count_values([5, 4, 5, 3, 5, 4, 2, 5, 4, 5, 1, 4]) == [0, 1, 1, 1, 4, 5]


def test_minlength():
    """pads the result to minlength, and grows past it when a value needs more room"""
    assert count_values([0, 0, 1, 3, 3, 3], minlength=5) == [2, 1, 0, 3, 0]
    assert count_values([2], minlength=1) == [0, 0, 1]
    assert count_values([], minlength=3) == [0, 0, 0]
    assert count_values([]) == []


def test_bad_values_are_refused():
    """refuses a negative value with ValueError and a float with TypeError"""
    try:
        count_values([1, -1])
    except ValueError:
        pass
    else:
        assert False, "a negative value must raise ValueError"
    try:
        count_values([1.0, 2.0])
    except TypeError:
        return
    assert False, "a float must raise TypeError"
A hint

Check the values first (isinstance(v, int) and v >= 0), then make a list of zeros of the right length and add 1 at position v once for each value, in a plain for loop: a loop handles repeats one at a time, which is what np.add.at does too.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Medium · Python

Find the positions of each row's k largest values

Write top_k_per_row(scores, k) in topk.py, with Python's own lists. scores is a list of rows of numbers; return, for each row, the column positions of its k largest values, largest first. When two values are equal, the lower column position comes first. This is what np.argsort(-scores, axis=1, kind="stable")[:, :k] gives, and those are the positions np.take_along_axis needs.

  • k = 0 gives an empty list for every row, and k equal to the row length gives the whole row's order.
  • A k below 0 or above the length of a row raises ValueError.

The lesson's players give the first check: for [[3, 9, 1, 7], [8, 2, 6, 4], [5, 5, 9, 1]] and k = 2 the result is [[1, 3], [0, 2], [2, 0]].

Starter code · topk.py

def top_k_per_row(scores, k):
    """Return, for each row, the positions of its k largest values, largest first, ties by the lower position."""
    return [sorted(range(len(row)), key=lambda j: row[j])[:k] for row in scores]
The sample tests · test_topk.py
from topk import top_k_per_row

PLAYERS = [[3, 9, 1, 7], [8, 2, 6, 4], [5, 5, 9, 1]]  # the lesson's scores


def test_the_lessons_top_two():
    """gives the positions that the lesson's run printed"""
    assert top_k_per_row(PLAYERS, 2) == [[1, 3], [0, 2], [2, 0]]


def test_ties_go_to_the_lower_position():
    """puts the lower column first when two values are equal"""
    assert top_k_per_row([[1, 1, 1]], 2) == [[0, 1]]
    assert top_k_per_row([[4, 7, 7, 2]], 3) == [[1, 2, 0]]


def test_k_of_zero_and_k_of_all():
    """returns empty lists for k = 0 and the whole order for k = the row length"""
    assert top_k_per_row(PLAYERS, 0) == [[], [], []]
    assert top_k_per_row(PLAYERS, 4) == [[1, 3, 0, 2], [0, 2, 3, 1], [2, 0, 1, 3]]
    assert top_k_per_row([], 2) == []


def test_k_out_of_range():
    """refuses a k below 0 or longer than a row"""
    for k in (-1, 5):
        try:
            top_k_per_row(PLAYERS, k)
        except ValueError:
            continue
        assert False, f"k = {k} must raise ValueError"
A hint

sorted(range(len(row)), key=...) sorts the positions of a row instead of its values. A key of (-row[j], j) puts larger values first and, among equal values, the lower position first. Keep the first k.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

The self-check mixes all six ideas, starting with the one your answers so far show you know least well:

Practice that adapts to you

Practise this lesson

18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 18 m has shape (3, 4). What shape does m[m > 5] have?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: (6,)

    NumPy's guide says a mask with the array's number of dimensions gives a 1-dimensional result.

  2. Question 2 of 18 For the readings 21, 85, 22, -40, 23, 24, 99 and 22 with the glitches 85, -40 and 99, what is readings[~glitch].mean()?

    Type a number.

    Show the answer: Question 2 of 18

    Answer: 22.4 degrees C (anything from 22.39 to 22.41 counts)

    ~glitch keeps 21, 22, 23, 24 and 22, which add up to 112; 112 / 5 = 22.4, as the run printed.

  3. Question 3 of 18 sel = readings[glitch], then sel[:] = 0. What happens to readings?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: Nothing; sel is a copy

    To change the original, assign through the mask itself: readings[glitch] = 0.

  4. Question 4 of 18 What does ~(x > 50) select from x?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: Every value that is not above 50, including 50 itself

    ~ is element-wise not for boolean arrays, as in readings[~glitch].

  5. Question 5 of 18 Which mask selects the readings outside 0 to 30?

    Choose one answer.

    Show the answer: Question 5 of 18

    Answer: (r < 0) | (r > 30)

    Outside a range is one condition or the other: (r < 0) | (r > 30), or ~((r >= 0) & (r <= 30)).

  6. Question 6 of 18 0 <= x <= 30 works for a Python number. What happens when x is an array, and what do you write instead?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: It raises the "truth value is ambiguous" ValueError; write (0 <= x) & (x <= 30)

    Chained comparisons are and in disguise, so they fail on arrays like and does.

  7. Question 7 of 18 What does np.where(mask) return when it is given only the condition?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: The positions where mask is True, the same as np.nonzero(mask)

    np.where has two jobs: positions with one argument, a choice of values with three.

  8. Question 8 of 18 cleaned = np.where(glitch, np.nan, readings). What is readings[1] afterwards?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: 85.0

    np.where is the way to replace values without touching the original; readings[glitch] = np.nan is the way to change it in place.

  9. Question 9 of 18 On NumPy 2.5.3, what does np.where([True, False, True], 300, np.zeros(3, dtype=np.int8)) do?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: It raises OverflowError: Python integer 300 out of bounds for int8

    Older NumPy versions truncated such values silently, so code that must run on both should pass an array of the wanted dtype, or check the range first.

  10. Question 10 of 18 What does m[[2, 0, 2]] give?

    Choose one answer.

    Show the answer: Question 10 of 18

    Answer: Rows 2, 0 and 2 again, in that order, as a new (3, 4) array

    Index arrays are how you reorder, repeat or sample rows; the result is always a copy.

  11. Question 11 of 18 What is m[:, [3, 0]]?

    Choose one answer.

    Show the answer: Question 11 of 18

    Answer: [[3, 0], [7, 4], [11, 8]]

    A slice on one axis and an index array on the other keep the rows and pick the columns in the listed order.

  12. Question 12 of 18 x has shape (2, 3, 4). Why is x[0, :, [0, 1]].shape (2, 3) and not (3, 2)?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: The two advanced indexes, 0 and [0, 1], are separated by a slice, so their axis comes first

    Index the first axis on its own first, x[0][:, [0, 1]], or use a slice, x[0, :, 0:2], to keep the order you expect.

  13. Question 13 of 18 What does np.argsort(scores, axis=1) return?

    Choose one answer.

    Show the answer: Question 13 of 18

    Answer: For each row, the column positions that would sort that row

    Positions per row are what take_along_axis and put_along_axis expect.

  14. Question 14 of 18 For scores = [[3, 9, 1, 7], [8, 2, 6, 4], [5, 5, 9, 1]], which positions does np.put_along_axis(marks, np.argmax(scores, axis=1, keepdims=True), 1, axis=1) set to 1?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: Column 1 of row 0, column 0 of row 1 and column 2 of row 2

    keepdims=True gives argmax the shape (3, 1) that put_along_axis needs: one position for each row.

  15. Question 15 of 18 Row [5, 5, 9, 1]: which two positions does np.argsort(-row, kind="stable")[:2] give?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: [2, 0]

    Negating and sorting stably gives largest first with ties to the lower position, the rule the exercise uses.

  16. Question 16 of 18 What does NumPy 2.5.3 do with np.bincount(np.array([1.0, 2.0]))?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: It raises TypeError, because bincount needs integers

    bincount counts non-negative integers. Floats raise TypeError and negative numbers ValueError.

  17. Question 17 of 18 How many positions does np.bincount([1, 3, 3], minlength=6) have?

    Type a number.

    Show the answer: Question 17 of 18

    Answer: 6 positions

    Without minlength it would have max + 1 = 4 positions; minlength=6 adds positions 4 and 5, as zeros.

  18. Question 18 of 18 codes holds a shop code per sale and amounts the amount of each sale. Which line gives the total amount per shop code in one call?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: np.bincount(codes, weights=amounts)

    np.bincount with weights is a grouped sum by integer key; np.add.at(totals, codes, amounts) gives the same.

Interview questions

Warm-up (fresher): What does a[a > 0] return, and how is it different from np.where(a > 0, a, 0)?

a > 0 is a boolean mask, an array of True and False with the shape of a. a[a > 0] uses it as an index and returns a new 1-D array holding only the positive values, in order, so a (3, 4) array with five positive values gives shape (5,); it is a copy, so changing it leaves a alone. np.where(a > 0, a, 0) also builds a new array, but with the full shape of a: it takes the value of a where the mask is True and 0 everywhere else. Use the mask to pull values out, for example to average them, and np.where to replace values while keeping every position. To change a itself, assign through the mask: a[a < 0] = 0.

Two interview questions build on this lesson, one about a histogram that counts too little and one about an index whose shape comes out swapped; their first steps are on the module’s question page:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A teammate counts 10 million event codes, integers from 0 to 999, with counts[codes] += 1. No error is raised, but the counts add up to 1,000 and every code looks equally rare on the dashboard. Explain what happened, replace the line, and say which replacement you would choose and why.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    frames has shape (100, 480, 640). frames[7, :, [10, 20, 30]] should give 480 rows by 3 columns, but its shape is (3, 480), and the code that follows quietly works on transposed data. Explain the rule behind the shape and give two ways to get (480, 3).

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A boolean mask selects values into a new 1-D array, counts them with sum(), and changes them in place when it is used on the left of =.
  • Conditions combine with &, | and ~, each comparison in parentheses; and, or and chained comparisons raise on arrays, and a missing parenthesis lets & bind before the comparison.
  • np.nonzero returns the positions of the True values, one array per axis, while np.where(cond, x, y) builds a new array of the original shape from two choices.
  • One index array per axis pairs positions up, np.ix_ makes a grid, and the result takes the shape of the index, with separated advanced indexes moving their axis to the front.
  • take_along_axis and put_along_axis use a different position in each row, as argsort and argmax with keepdims=True produce them.
  • x[idx] += 1 counts a repeated index once; np.add.at and np.bincount count every occurrence.

Terms in this lesson

boolean mask
An array of True and False, usually from a comparison such as a > 50, used as an index: a[mask] gives the values where it is True as a new 1-D array, and a[mask] = v writes v there.
fancy indexing
Indexing with arrays of integers, such as a[[2, 0, 2]]: the result holds the values at those positions, in that order and with repeats, as a new array. One index array per axis pairs the positions up.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.