A mask gives you the matching values. np.where gives you an array the same size, built from two of them.
import numpy as np scores = np.array([10, 20, 30, 40]) scores[scores >= 30] # -> [30, 40] two values np.where(scores >= 30, scores + 5, scores) # -> [10, 20, 35, 45] four # Read it as: where the condition holds take from the second argument, # otherwise take from the third. All three are the same shape, or # broadcast to it. # Both branches are computed in full, whatever the condition says: np.where(scores > 0, 1 / scores, 0) # 1 / scores is evaluated for every element, including any zeros — # an `if` would have skipped them, and this does not.
- where chooses; a mask selects
Indexing with a mask returns only the matching values, and it flattens, so the result no longer lines up with anything else you have. np.where returns the same shape as the condition, which means it can be assigned back to a column or compared position by position with the original. When you want to *change* some values, where is the tool; when you want to *look at* some values, the mask is.
- Both branches are evaluated, because it is a function call
Python evaluates the arguments before np.where ever runs, so the 'otherwise' branch is computed for the elements that did not need it and vice versa. That is harmless for arithmetic and matters when a branch would divide by zero, take a log of a negative, or index out of range — it will do so, and warn, even though its values are then discarded. An `if` short-circuits; np.where cannot.
- Anything broadcastable can be a branch
np.where(mask, scores + 5, scores) uses two arrays, and np.where(mask, 0, scores) uses a scalar for one of them — the same broadcasting rules as everywhere else. So 'set the failures to zero' and 'add five to the passes' are the same call with different arguments, which is why one function covers so much ground.
One call. You need a condition, what to use where it holds, and what to use where it does not — and the third of those is the scores themselves.
np.where(scores >= threshold, scores + amount, scores). The condition is a mask, the two branches are whole arrays, and the result is the same shape as all three.
Resist the mask version — scores[mask] += amount modifies the caller’s array, and building a copy first to mask into is two more lines for the same answer. np.where produces a new array by construction, which is what the contract wants.
import numpy as np def bonus(scores, threshold, amount): return np.____(scores >= ____, scores + ____, ____)
import numpy as np
def bonus(scores, threshold, amount):
# Where the condition holds take scores + amount, otherwise take the
# score unchanged. A new array, the same shape as the input.
return np.where(scores >= threshold, scores + amount, scores)
scores + amount builds a whole array — for every element, including the ones that will not be used — and np.where builds the result. On a large column that is two passes and two allocations where an in-place masked assignment would be one. The in-place version is faster and modifies the caller’s data; this one is clear and does not. Knowing which you are choosing is the point.
It does not have to be a comparison against the same array: np.where(other_column > 0, a, b) chooses between two arrays based on a third, which is how you apply a rule from one column to another. All three arguments broadcast, so scalars work in any position.
Called with only a condition, np.where returns the *positions* where it holds — a tuple of index arrays, not values. It is a genuinely different operation sharing a name, it is what you want when you need to know where rather than what, and np.nonzero is the clearer spelling of it.
For clamping to a range, np.clip(values, low, high) says it in one call and is clearer than two nested wheres. For more than two outcomes, nesting np.where gets unreadable fast — np.select takes a list of conditions and a list of choices and is the right tool for a grade band or a tax bracket. And if a branch would be expensive or unsafe to compute for the elements that do not need it, np.where is the wrong shape entirely, because it will compute it anyway: use a mask and assign into a result you have already allocated.
- Rewrite it with a mask and an in-place assignment on a copy, and compare the two for readability and for how many arrays each one builds.
- Add a second band — a larger bonus above a higher threshold — first with nested wheres and then with np.select, and decide which you would keep.
- Try np.where(scores > 0, 100 / scores, 0) on an array containing a zero and read the warning: it computed the division it was never going to use.