import numpy as npMasking: Practice with the Cardboard
Module 3: NumPy (extra practice)
Masking: Practice with the Cardboard
This page is extra practice for Masking: Sets and Logic. If masks still feel slippery, think of a piece of cardboard with holes laid over your array:
Truecuts a hole.Falseleaves the cardboard.- You can only read the numbers that show through the holes:
A[mask]. - You can only write through the holes:
A[mask] = value. The covered numbers do not change.
Three Kinds of Mask
A mask must fit the array, like cardboard cut to the size of the sheet. What “fit” means depends on what you want to select.
| Mask | Shape of the mask | Read | Write |
|---|---|---|---|
| One hole per cell | the same shape as A |
A[mask] |
A[mask] = v |
| One slot per row | (rows,) |
A[mask] |
A[mask] = v |
| One slot per column | (columns,) |
A[:, mask] |
A[:, mask] = v |
We already know cell masks from Masking: Sets and Logic. Row and column masks are new, and they are the reason this page exists. They come from np.any() and np.all() with an axis:
A = np.array([[2, 7, 4, 9],
[5, 1, 8, 3],
[6, 0, 12, 10]])
cell_mask = A > 5
print(cell_mask)
print(A[cell_mask]) # a 1D array: the values that show through the holes[[False True False True]
[False False True False]
[ True False True True]]
[ 7 9 8 6 12 10]
Now a row mask: which rows contain a value greater than 8? We ask the question along axis=1, the axis that runs across each row, so every row collapses to one answer:
row_mask = np.any(A > 8, axis=1)
print(row_mask) # one value per row, shape (3,)
print(A[row_mask]) # whole rows[ True False True]
[[ 2 7 4 9]
[ 6 0 12 10]]
And a column mask: which columns have only positive numbers? axis=0 runs down every column:
col_mask = np.all(A > 0, axis=0)
print(col_mask) # one value per column, shape (4,)
print(A[:, col_mask]) # all the rows (the colon), only the selected columns[ True False True True]
[[ 2 4 9]
[ 5 8 3]
[ 6 12 10]]
The colon in A[:, col_mask] says “all the rows”. The mask goes in the second position because columns are the second axis.
np.any(..., axis=1) and np.all(..., axis=1) give one answer per row. axis=0 gives one answer per column. The axis you name is the one that disappears.
If you remember only one rule: a row mask has as many values as the array has rows, and a column mask as many as it has columns.
Writing works the same way. The mask chooses the rows or columns, and the assignment fills all of them:
B = A.copy()
B[:, col_mask] = 0 # zero the columns that passed the test
print(B)[[0 7 0 0]
[0 1 0 0]
[0 0 0 0]]
Practice
Use these grades for the exercises. Each row is a student and each column is an exam.
names = np.array(["Alice", "Bruno", "Chloe", "Diego", "Emma"])
grades = np.array([[7, 9, 6],
[5, 8, 10],
[4, 6, 5],
[9, 9, 8],
[3, 5, 7]]) # 5 students (rows), 3 exams (columns)Without running the code, write what it prints. Then check it in the lab: use the array [[3, 8, 1], [6, 2, 9]] and the mask A % 3 == 0.
A = np.array([[3, 8, 1],
[6, 2, 9]])
mask = A % 3 == 0
print(A[mask])
A[mask] = 0
print(A)A = np.array([[3, 8, 1],
[6, 2, 9]])
mask = A % 3 == 0
print(A[mask]) # [3 6 9]: the multiples of 3, as a 1D array
A[mask] = 0
print(A)[3 6 9]
[[0 8 1]
[0 2 0]]
The mask has the shape of A, so it is a cell mask. A[mask] is always 1D, even when A is 2D: the holes are not in a rectangle. Writing through the holes leaves the covered numbers (8, 1, 2) untouched.
A student passes when every grade is at least 5. Use a row mask to print the names of the students who passed all three exams. Hint: names is an array, so it can be indexed with the same mask as grades.
passed_all = np.all(grades >= 5, axis=1)
print(passed_all) # one value per student
print(names[passed_all]) # ['Alice' 'Bruno' 'Diego'][ True True False True False]
['Alice' 'Bruno' 'Diego']
grades >= 5 is a cell mask with the shape (5, 3). np.all(..., axis=1) asks “is every exam above 5?” for each row, and returns a mask with the shape (5,). That mask fits both grades (5 rows) and names (5 elements).
- Which exams did nobody fail? Build a column mask and select those exams:
grades[:, mask]. - The exams whose average is below
7were too hard. Give+1point to everybody in those exams, usinggrades[:, hard] += 1. Work on a copy sogradesstays as it is.
nobody_failed = np.all(grades >= 5, axis=0)
print(nobody_failed) # [False True True]
print(grades[:, nobody_failed]) # exams 1 and 2, for every student[False True True]
[[ 9 6]
[ 8 10]
[ 6 5]
[ 9 8]
[ 5 7]]
curved = grades.copy()
hard = grades.mean(axis=0) < 7 # the average of every exam: a column mask
print(grades.mean(axis=0)) # [5.6 7.4 7.2]
curved[:, hard] += 1
print(curved)[5.6 7.4 7.2]
[[ 8 9 6]
[ 6 8 10]
[ 5 6 5]
[10 9 8]
[ 4 5 7]]
grades.mean(axis=0) has one average per column, so < 7 gives a column mask directly.
This code wants to set to zero the grades of every student who failed an exam. Predict the error message. Then fix it.
grades[np.any(grades < 5, axis=0)] = 0IndexError: boolean index did not match indexed array along axis 0; size of axis is 5 but size of corresponding boolean axis is 3
axis=0 runs down the columns, so we get one answer per exam (3 values). But we use that mask to choose students, and there are 5 of them. The cardboard has three slots for five rows. To ask the question for each student, the axis that disappears must be the columns, axis=1:
zeroed = grades.copy()
zeroed[np.any(grades < 5, axis=1)] = 0
print(zeroed)[[ 7 9 6]
[ 5 8 10]
[ 0 0 0]
[ 9 9 8]
[ 0 0 0]]
Print the grades of the students whose total is above the average total. Hint: first grades.sum(axis=1).
totals = grades.sum(axis=1) # one total per student
print(totals, totals.mean()) # [22 23 15 26 15] 20.2
print(grades[totals > totals.mean()])[22 23 15 26 15] 20.2
[[ 7 9 6]
[ 5 8 10]
[ 9 9 8]]
The comparison totals > totals.mean() is already a row mask: one value per student. There is no need for np.any() or np.all() here, because the sum already collapsed each row.
Create a 6 x 6 array of zeros and write 1 on its border, using one Boolean vector as a row mask and as a column mask. Hint: np.arange(6) and the positions 0 and 5.
img = np.zeros((6, 6), dtype=int)
position = np.arange(6)
edge = (position == 0) | (position == 5) # True for 0 and 5
img[edge, :] = 1 # the first and the last row
img[:, edge] = 1 # the first and the last column
print(img)[[1 1 1 1 1 1]
[1 0 0 0 0 1]
[1 0 0 0 0 1]
[1 0 0 0 0 1]
[1 0 0 0 0 1]
[1 1 1 1 1 1]]
The same edge mask works as a row mask and as a column mask because the array is square. For a non-square array we would need one mask per axis.
Things to Try in the Lab
- A mask with a hole too many. Load
A > 2 & A < 9. Read the error and explain it with the precedence rule from Masking: Sets and Logic. Then add the parentheses. - A square array. Load
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]with the masknp.any(A > 6, axis=0), then withaxis=1. Both masks have 3 values: which one selects rows and which one selects columns? - Integers do not become floats. Select some cells with an integer array and write
0.5through them. What does the lab report? Compare witha = np.array([1, 2, 3]); a[a > 1] = 0.5. Thedtypeof an array is fixed when the array is created. - Edit the mask by hand. Switch some
TrueandFalsevalues in the table under the board, and predict the output ofA[mask]before pressing the button.
Summary
- A Boolean mask is cardboard with holes: you read and write only through them.
- A cell mask has the shape of
A, andA[mask]is always 1D. np.any()andnp.all()with anaxisproduce a row mask (axis=1) or a column mask (axis=0). The axis you name is the one that disappears.- Select rows with
A[mask]and columns withA[:, mask]. - A mask that does not fit the axis it is used on raises an
IndexError. - Assigning through a mask never changes the
dtypeof the array.
The cardboard explanation and the first interactive version were created by Manuel Morante for his section of the course.