Boolean Masking
In one line: build a boolean array the same shape as your data, then use it to select, count or assign — this is how filtering is done without loops.
The pattern
counts = np.array([5, 120, 3, 88, 41])
mask = counts > 50 # array([False, True, False, True, False])
counts[mask] # array([120, 88]) ← a COPY
counts[mask] = 50 # clip in place
mask.sum() # 2 — True is 1, so sum counts matches
mask.mean() # 0.4 — proportion passing
mask.sum() and mask.mean() are the idiomatic "how many" and "what fraction" — no need for np.count_nonzero although that exists too and is marginally faster.
Combining conditions
Use &, |, ~ — never and, or, not.
(counts > 10) & (counts < 100) # elementwise AND
(counts < 10) | (counts > 100) # elementwise OR
~(counts > 50) # elementwise NOT
The parentheses are mandatory. & binds tighter than > in Python, so counts > 10 & counts < 100 parses as counts > (10 & counts) < 100 and raises a confusing error.
The Python keywords fail because and/or call bool() on their operands, and calling bool() on a multi-element array raises:
ValueError: The truth value of an array with more than one element is ambiguous.
That error message, when you see it, almost always means you wrote and where you needed &.
Any, all, and where
mask.any() # is anything True?
mask.all() # is everything True?
mask.any(axis=1) # per row — see [[Axes and Reductions]]
np.where(mask) # tuple of index arrays where True
np.where(mask, a, b) # vectorised ternary: a where True, b where False
np.flatnonzero(mask) # flat indices of True — cleaner than np.where(mask)[0]
np.argwhere(mask) # (n, ndim) array of coordinate tuples
np.where(cond, x, y) is the vectorised if/else and it is worth memorising:
direction = np.where(log2fc > 0, "up", "down")
capped = np.where(counts > 1000, 1000, counts)
Both branches are evaluated eagerly, so np.where(x != 0, 1/x, 0) still emits a divide-by-zero warning. Use np.divide(1, x, out=np.zeros_like(x), where=x != 0) instead.
Masks on multi-dimensional arrays
A boolean mask of the same shape flattens the result:
expr[expr > 10] # 1-D array of all values above 10 — loses shape
To keep the matrix shape, select along one axis with a 1-D mask:
expressed = (expr > 1).sum(axis=1) >= 3 # gene passes in ≥3 samples
expr[expressed] # (n_passing, n_samples) — shape kept
expr[:, sample_mask] # subset columns
expr[np.ix_(expressed, sample_mask)] # both at once
This gene-filtering idiom — "expressed above threshold in at least N samples" — is the standard first step of any RNA-seq analysis.
Boolean masking returns a copy
b = a[a > 5]
b[0] = 0 # does NOT affect a
Unlike slicing, mask selection cannot be expressed as a stride pattern, so NumPy must gather the elements into new memory. See Views vs Copies.
But assignment through a mask does write to the original:
a[a > 5] = 0 # this DOES modify a
a[mask] calls __getitem__ (gather → copy); a[mask] = x calls __setitem__ (scatter → in-place).
Bioinformatics examples
# gene filtering
keep = (counts.sum(axis=1) > 10) & ((counts > 0).sum(axis=1) >= 3)
filtered = counts[keep]
kept_gene_names = gene_names[keep] # ← apply the SAME mask to the labels
# quality filtering on reads
good = (quals.mean(axis=1) >= 30) & (~np.isnan(quals).any(axis=1))
# significant hits
sig = (padj < 0.05) & (np.abs(log2fc) > 1)
print(f"{sig.sum()} significant of {sig.size} ({sig.mean():.1%})")
# masking NaNs before a mean
valid = ~np.isnan(x)
x[valid].mean() # or just np.nanmean(x)
The critical habit is in line 3: whenever you filter a data matrix, apply the identical mask to the label array in the same breath. Losing the correspondence between rows and gene names is the most common — and most invisible — bug in matrix-based bioinformatics. This is precisely the problem Pandas solves with an index.
Common mistakes
and/orinstead of&/|→ "truth value is ambiguous".- Missing parentheses around comparisons combined with
&. arr == np.nanis alwaysFalse. Usenp.isnan(arr). See Missing Data in NumPy.- Filtering the matrix but not the labels. Silent, permanent misattribution of results.
- Expecting
expr[expr > 10]to keep the matrix shape. It returns 1-D. - Using a mask of the wrong length. NumPy raises for boolean masks of mismatched length — but a mask that is 1 short of the axis length used to be tolerated in old versions. Be exact.
np.where(cond, f(x), g(x))whereforgerrors on the other branch. Both are computed.
See also
Indexing and Slicing · Fancy Indexing · Views vs Copies · Missing Data in NumPy · Axes and Reductions · Filtering and query · Applied - Expression Matrices with NumPy
Test cases · 2
| # | via | input | expected stdout |
|---|---|---|---|
| 1 | stdin | 10 12 15 9 4 30 22 15 | 3/7 positions at >=15x |
| 2 | stdin | 5 5 5 5 10 | 0/4 positions at >=10x |
Hints · 2
01Hint
depths >= threshold produces a boolean array of the same shape.
02Hint
Summing a boolean array counts the True values.