import numpy as np
temps = np.array([[21, 19, 26],
[27, 23, 28],
[27, 18, 30],
[29, 20, 32]])Boolean masks
Before this lesson: P02.3.1 Positions and slices
What you will be able to do
- Build a boolean mask with a comparison and use it to keep the entries, or the rows, where it is True.
- Predict the shape of a masked selection: 1-D in reading order for a full mask, whole rows for a mask along axis 0.
- Count with mask.sum() and find a share with mask.mean(), including a classifier’s accuracy.
- Combine conditions with &, | and ~ inside brackets, and explain why the words and and or fail on arrays.
- Change entries through a mask, and choose between two values entry by entry with np.where.
The question
Which readings were above 27 degrees? On which days was the coast warmer than 25? What share of a model’s predictions were right? None of these names a position; each names a condition. How do you select by a condition without writing a loop, and why does Python refuse the obvious warm and cool?
From the last lesson
1. What are the shapes of temps[:, 1] and temps[:, 1:2]?
Show answer
(4,) and (4, 1). The integer drops the column axis; the slice keeps it with length 1.
2. Which days does temps[1::2] keep?
Show answer
Tuesday and Thursday, positions 1 and 3.
Intuition
The running array is the same: noon temperatures (invented) at the coast, hill and valley stations on Monday to Thursday.
A comparison such as temps > 27 is done entry by entry, like the arithmetic of P02.2.1. The answer is an array of the same shape holding True or False in each cell. Such an array is called a boolean mask: laid over the data, it shows which cells pass the test. Put the mask inside the square brackets and NumPy keeps exactly the cells under a True.
A comparison makes a mask of the same shape. The mask used as an index keeps the four readings above 27 and lines them up in one row, in reading order: along Tuesday, then Wednesday, then Thursday.
The result is 1-D because the kept cells need not form a rectangle: Thursday keeps two readings and Monday none. So NumPy reads the grid row by row, as you read a page, and writes down each kept value in turn.
Build it up
Step 1. A comparison makes a mask. temps > 27 has shape (4, 3) and dtype bool. Every comparison operator works this way: >, >=, <, <=, == and !=.
Step 2. A mask selects entries. temps[temps > 27] is [28, 30, 29, 32], shape (4,). The mask has the same shape as the array, so each cell has its own True or False.
Step 3. A mask along the rows selects whole rows. The coast readings temps[:, 0] give the mask temps[:, 0] > 25, one value per day: [False, True, True, True]. Used as the row index, it keeps every day whose coast reading was above 25, with all three stations.
A mask with one value per row works on axis 0, so it keeps whole rows and the result stays 2-D. This is how a table is filtered: one test per example, and the examples that pass keep every feature.
Step 4. Counting and shares. In arithmetic True counts as 1 and False as 0. So mask.sum() counts the True entries and mask.mean() gives their share. Four of the twelve readings are above 27, so (temps > 27).mean() is \({4/12}\), about 0.333. Three of the four days were warm at the coast, so (temps[:, 0] > 25).mean() is 0.75.
That share is how a classifier is scored. Suppose a rule predicts a hot valley day (valley above 27) whenever the hill is above 19. The predictions are one mask, the true labels another, and pred == y marks the days where they agree.
Accuracy as the mean of a mask. The rule and the truth agree on three days of four; the mean of the agreement mask is the share of correct predictions, 0.75.
Step 5. Writing through a mask. A mask on the left of = changes the cells it marks, in the array itself. capped[capped > 30] = 30 replaces every reading above 30 by 30. To keep temps unchanged, the Code section first makes a separate array with .copy(), which lesson P02.3.4 explains.
Step 6. Choosing entry by entry. np.where(mask, a, b) builds a new array that takes a where the mask is True and b elsewhere. np.where(temps > 27, temps - 27, 0) gives how many degrees each reading was above 27, and 0 for the rest.
The formal statement
A mask whose length does not match its axis raises IndexError. And the three operators &, | and ~ bind more tightly than comparisons, so each comparison needs its own brackets: (temps[:, 0] > 25) & (temps[:, 1] < 20).
Masks combine cell by cell. & is True only where both are, | wherever either is, and ~ flips every entry.
Why a mean is a share. Write \({[\hat{y}_i = y_i]}\) for 1 when prediction \(i\) is right and 0 when it is wrong. The mask pred == y holds exactly these numbers, so its mean is
\[ \begin{aligned} \text{accuracy} &= \frac{1}{n}\sum_{i=1}^{n}\,[\hat{y}_i = y_i] \\ &= \frac{\text{number right}}{n}. \end{aligned} \]
The sum adds a 1 for every correct prediction and a 0 for every wrong one, so it counts the correct ones; dividing by \(n\) turns the count into a share. For the rule above, \({3/4 = 0.75}\). This is the accuracy of lesson ML01.7.1.
Go deeper: why the words and, or and not fail
and, or and not are not operators that can be redefined for arrays. Each asks its operand for one truth value, by calling bool() on it. NumPy refuses to give one truth value for an array of four, because it cannot know whether you mean “all of them” or “any of them”, and raises ValueError. &, | and ~ are ordinary operators, which NumPy defines to work entry by entry. The missing-bracket case fails for a related reason: without brackets, temps[:, 0] > 25 & temps[:, 1] < 20 is read as a chained comparison, x > y < z, which Python expands into (x > y) and (y < z), and that and meets an array.
Worked example
Find the readings above 27 with a loop, then with a mask, and say what share they are.
The loop visits the readings row by row and keeps those that pass:
- Monday, 21, 19, 26: none is above 27.
- Tuesday, 27, 23, 28: 27 is not above 27, but 28 is; keep 28.
- Wednesday, 27, 18, 30: keep 30.
- Thursday, 29, 20, 32: keep 29, then 32.
The list is [28, 30, 29, 32], four values, in the order the loop met them.
The filtering loop as a trace. Reading row by row is the order in which a full mask lists its entries, so the loop and the mask agree value for value.
The mask does the whole loop in one line: temps[temps > 27] is [28, 30, 29, 32]. The count is (temps > 27).sum(), 4, and the share is \({4/12 = 1/3}\), about 0.333 of the readings.
Now whole days. The loop over day positions keeps day \({i}\) when rows[i][0] > 25: Tuesday (27), Wednesday (27) and Thursday (29), but not Monday (21). The mask version is temps[temps[:, 0] > 25], three rows, shape (3, 3).
Code
The filtering loops first, then the masks that replace them:
rows = temps.tolist()
kept = []
for row in rows:
for v in row:
if v > 27:
kept.append(v)
warm_days = [row for row in rows if row[0] > 25]
print(kept, len(warm_days))
mask = temps > 27
print(mask.dtype, mask.shape)
print(temps[mask], mask.sum(), round(mask.mean(), 3))
print(temps[temps[:, 0] > 25])[28, 30, 29, 32] 3
bool (4, 3)
[28 30 29 32] 4 0.333
[[27 23 28]
[27 18 30]
[29 20 32]]
Accuracy as the mean of a mask:
pred = temps[:, 1] > 19 # the rule: hill above 19
y = temps[:, 2] > 27 # the truth: valley above 27
print(pred, y)
print((pred == y).sum(), (pred == y).mean())[False True False True] [False True True True]
3 0.75
Combining masks, writing through one, and np.where:
warm = temps[:, 0] > 25
cool = temps[:, 1] < 20
print(warm & cool, warm | cool, ~warm)
print(temps[warm & cool])
capped = temps.copy() # a separate array (lesson P02.3.4)
capped[capped > 30] = 30
print(capped[3], temps[3])
print(np.where(temps > 27, temps - 27, 0))[False False True False] [ True True True True] [ True False False False]
[[27 18 30]]
[29 20 30] [29 20 32]
[[0 0 0]
[0 0 1]
[0 0 3]
[2 0 5]]
warm & cool keeps only Wednesday: warm at the coast (27) and cool on the hill (18). Writing through the mask changed one cell of capped, Thursday’s valley reading, and temps still says 32.
Explore
This module’s interactive, the index explorer, lives in lesson P02.3.4. Switch it to mask mode and move the threshold: the lit cells, the length of the result and the count mask.sum() move together.
Pitfalls
Wrong beside right. The words and and or need one truth value, and & without brackets binds before the comparisons. Bracket each comparison and join them with &.
“warm and cool combines the two masks.”
and asks each side for a single True or False, and an array of four has none to give, so NumPy raises ValueError. Use & for and, | for or and ~ for not.
“Brackets around each comparison are just style.”
They are needed. & binds more tightly than > and <, so temps[:, 0] > 25 & temps[:, 1] < 20 first computes 25 & temps[:, 1] and then fails as a chained comparison. Write (temps[:, 0] > 25) & (temps[:, 1] < 20).
“A mask keeps the shape of the array.”
A full mask returns a 1-D array, because the kept cells need not form a rectangle. temps[temps > 27] has shape (4,), and you can no longer tell which day each value came from. When you need the positions, keep the mask itself, or use np.where(mask, ..., ...), which does keep the shape.
“A list of 0s and 1s works as a mask.”
Only a list of True and False does. temps[[0, 1, 1, 1]] is read as a list of positions: Monday once and Tuesday three times, the integer indexing of the next lesson. Build masks with comparisons, which give bool arrays.
In machine learning a mask is how a table is filtered before training (examples with a valid label, readings within a sensor’s range) and how a model is scored. Each of these pitfalls turns into a wrong row count or a crash, so print mask.sum() before you use a mask.
Check yourself
Recall
1. What do temps > 27 and temps[temps > 27] return?
Show answer
temps > 27 is a bool array of shape (4, 3). temps[temps > 27] is the 1-D array [28, 30, 29, 32], the readings above 27 in row-by-row order.
2. Which rows does temps[temps[:, 0] > 25] keep, and what is its shape?
Show answer
Tuesday, Wednesday and Thursday, whose coast readings are above 25. Shape (3, 3): whole rows.
3. What do mask.sum() and mask.mean() give for a boolean mask?
Show answer
The number of True entries, and their share of all entries, since True counts as 1 and False as 0.
4. Why does warm and cool raise ValueError?
Show answer
and needs one truth value from each side, and an array of several values does not have one. Use warm & cool.
5. What is np.where(temps > 27, temps - 27, 0)[3]?
Show answer
[2, 0, 5]: Thursday’s readings 29, 20 and 32 are 2 above, not above, and 5 above 27.
Exercises
1. ★ Which readings are below 20? Give them, their count and their share of all twelve.
Show solution
temps[temps < 20] is [19, 18]: the hill on Monday and on Wednesday. Thursday’s hill reading, 20, is not below 20. The count is (temps < 20).sum(), 2, and the share is \({2/12 = 1/6}\), about 0.167.
2. ★★ Write one mask for the days when the valley reached at least 30 or the hill was above 22. Which days pass, and what is temps[mask]?
Show solution
mask = (temps[:, 2] >= 30) | (temps[:, 1] > 22). The valley test gives [False, False, True, True] and the hill test [False, True, False, False]; or-ing them gives [False, True, True, True]. So Tuesday, Wednesday and Thursday pass, and temps[mask] is [[27, 23, 28], [27, 18, 30], [29, 20, 32]], shape (3, 3).
3. ★★★ Write clip(a, lo, hi) with two masks, so that every entry below lo becomes lo and every entry above hi becomes hi, without changing a. What is clip(temps, 20, 30)? Then write it again with np.where alone.
Show solution
def clip(a, lo, hi):
out = a.copy()
out[out < lo] = lo
out[out > hi] = hi
return outThe copy keeps a unchanged, because assignment through a mask writes into the array itself. For temps the values 19 and 18 become 20 and the value 32 becomes 30: [[21, 20, 26], [27, 23, 28], [27, 20, 30], [29, 20, 30]]. With np.where, one call inside another: np.where(a < lo, lo, np.where(a > hi, hi, a)). The inner call caps the top, the outer call lifts the bottom, and np.where always builds a new array. NumPy’s own np.clip(a, lo, hi) gives the same answer.
Summary card
A mask is a grid of True and False; as an index it keeps the True cells.
Compare, then select.
a[a > c]: the entries abovec, 1-D, row by row.a[m]with one value per row keeps whole rows.m.sum()counts,m.mean()is a share: accuracy is(pred == y).mean().- Combine with
(m1) & (m2),|,~; neverand,or,not. a[m] = vwrites in place;np.where(m, a, b)builds a new array.
Sources and next steps
Sources. Each was opened and checked against this lesson on 15 November 2026.
- NumPy developers, Indexing on ndarrays, NumPy user guide, section Advanced indexing, subsection Boolean array indexing: when the mask has the dimensions of the array, the result is 1-D, filled with the entries under
True, in row-major (C-style) order. - NumPy developers, numpy.where, NumPy reference: with three arguments it returns elements chosen from
xwhere the condition isTrueand fromyelsewhere. - Jake VanderPlas, Python Data Science Handbook, Comparisons, Masks, and Boolean Logic: counting
Trueentries withnp.sumandnp.count_nonzero, the masking operation, the need for brackets because of operator precedence, and the aside on the keywordsand/orversus the operators&/|, with theValueErrorthatandraises on an array. - Python Software Foundation, The Python Language Reference, section 6.10, Comparisons: comparisons chain, so
x < y <= zmeansx < y and y <= z; and section 6.17, Operator precedence, where&binds more tightly than the comparisons.
Next. P02.3.3 Picking and ordering with integer arrays selects rows by a list of positions, in any order and with repeats, which is how data are sorted and shuffled.
This lesson unlocks
P02.3.3 Picking and ordering with integer arrays