np.where(condition, x, y) chooses values from x where a condition is True and from y elsewhere. With only the condition, it returns matching index arrays instead. Learn the difference before filtering, replacing missing values or assigning conditional columns.
| Form | Result |
|---|---|
np.where(condition, x, y) | Chosen values; all inputs must broadcast to a common shape |
np.where(condition) | Tuple of matching index arrays; prefer nonzero directly for position finding |
array[mask] | Only matching values, using Boolean indexing |
With three arguments, np.where(condition, x, y) chooses x for True and y for False. It returns an array rather than changing the input. Reassigning the variable only changes which result that variable refers to.
import numpy as np
import pandas as pd
ar = np.arange(6)
result = np.where(ar % 2 == 0, ar * 3, ar)
print("Original:", ar)
print("Chosen values:", result)
np.testing.assert_array_equal(result, [0, 1, 6, 3, 12, 5])
np.testing.assert_array_equal(ar, np.arange(6))Expected output
Original: [0 1 2 3 4 5]
Chosen values: [ 0 1 6 3 12 5]Use np.nan for a floating missing value. Mixing it with integer values produces a floating result. Here zero is also divisible by five, so its position becomes missing. See NaN and dtypes.
ar = np.arange(15)
result = np.where(ar % 5 == 0, np.nan, ar)
print(result)
print("dtype:", result.dtype)
np.testing.assert_array_equal(np.flatnonzero(np.isnan(result)), [0, 5, 10])
assert result.dtype.kind == 'f' Expected output
[nan 1. 2. 3. 4. nan 6. 7. 8. 9. nan 11. 12. 13. 14.]
dtype: float64With only the condition, where returns a tuple containing one index array per dimension. For a 1D input, access its index array with positions[0]. Use np.nonzero() directly for finding nonzero positions, particularly with array subclasses.
ar = np.array([12, 2, 7, 1, 9, 3, 11])
positions = np.where(ar > 5)
print("Index tuple:", positions)
print("Index array:", positions[0])
print("Matching values:", ar[positions])
np.testing.assert_array_equal(positions[0], [0, 2, 4, 6])
np.testing.assert_array_equal(positions[0], np.nonzero(ar > 5)[0])Expected output
Index tuple: (array([0, 2, 4, 6]),)
Index array: [0 2 4 6]
Matching values: [12 7 9 11]Use elementwise & for AND. Parenthesize each comparison so operator precedence matches the intended logic. Python and tries to evaluate the truth value of the entire array and is unsuitable here.
ar = np.array([12, 2, 7, 1, 9, 3, 11])
mask = (ar > 5) & (ar < 10)
positions = np.where(mask)
print("Positions:", positions)
print("Second matching position:", positions[0][1])
print("Matching values:", ar[mask])
np.testing.assert_array_equal(positions[0], [2, 4])Expected output
Positions: (array([2, 4]),)
Second matching position: 4
Matching values: [7 9]Each condition produces a Boolean mask. OR includes a value when either condition is True; NOT reverses a Boolean mask. Keep the original values available while inspecting positions.
ar = np.array([12, 2, 7, 1, 4, 3, 11])
mask = (ar > 5) | (ar % 2 == 0)
print("OR positions:", np.where(mask))
print("NOT values:", ar[~mask])
np.testing.assert_array_equal(np.where(mask)[0], [0, 1, 2, 4, 6])Expected output
OR positions: (array([0, 1, 2, 4, 6]),)
NOT values: [1 3]At each position, the condition chooses one value from the corresponding input. The output preserves the common broadcast shape. For this example all three inputs have shape (2, 2).
condition = np.array([[True, False], [True, True]])
x = np.array([[5, 2], [13, 42]])
y = np.array([[9, 18], [73, 16]])
result = np.where(condition, x, y)
print(result)
np.testing.assert_array_equal(result, [[5, 18], [13, 42]])Expected output
[[ 5 18]
[13 42]]The coordinate arrays describe paired positions, not a rectangular row-by-column selection. For indexing, use the tuple from nonzero or where. argwhere provides pairs that are convenient to display. See indexing and slicing.
a = np.array([[2, 8, 1], [9, 3, 7]])
positions = np.where(a > 5)
print("Rows:", positions[0])
print("Columns:", positions[1])
print("Coordinate pairs:")
print(np.argwhere(a > 5))
print("Values:", a[positions])
np.testing.assert_array_equal(a[positions], [8, 9, 7])Expected output
Rows: [0 1 1]
Columns: [1 0 2]
Coordinate pairs:
[[0 1]
[1 0]
[1 2]]
Values: [8 9 7]condition, x and y must have compatible shapes. A row-level condition should have shape (rows, 1) to expand across columns. A scalar replacement broadcasts to the whole result. Review broadcasting and shape errors.
a = np.arange(12).reshape(3, 4)
keep_rows = np.array([True, False, True])[:, None]
result = np.where(keep_rows, a, -1)
print(result)
assert result.shape == (3, 4)
np.testing.assert_array_equal(result[1], [-1, -1, -1, -1])Expected output
[[ 0 1 2 3]
[-1 -1 -1 -1]
[ 8 9 10 11]]Three-argument where retains a broadcast-shaped result by filling rejected positions. Boolean indexing selects only matching elements. Use filtering when you want fewer records, and replacement when positions must remain aligned.
a = np.array([12, 2, 7, 1, 9])
mask = a > 5
replaced = np.where(mask, a, -1)
filtered = a[mask]
print("Replacement:", replaced)
print("Filter:", filtered)
assert replaced.shape == (5,) and filtered.shape == (3,)Expected output
Replacement: [12 -1 7 -1 9]
Filter: [12 7 9]Even when a condition selects only one branch, the two branch dtypes influence the output type. Use compatible numeric replacements to keep numeric data numeric. A textual status is usually better stored separately. See dtype.
a = np.array([2, -1, 5])
numeric = np.where(a >= 0, a, np.nan)
status = np.where(a >= 0, 'Valid', 'Review')
print("Numeric:", numeric, numeric.dtype)
print("Status:", status)
assert numeric.dtype.kind == 'f'
assert np.isnan(numeric[1])Expected output
Numeric: [ 2. nan 5.] float64
Status: ['Valid' 'Review' 'Valid']Python evaluates both branch expressions before calling where. Therefore a division inside a branch can still divide by zero. For guarded division, initialize an output and use the ufunc where argument of np.divide(). It is a different API from np.where.
numerator = np.array([10.0, 20.0, 30.0])
denominator = np.array([2.0, 0.0, 5.0])
try:
with np.errstate(divide='raise', invalid='raise'):
np.where(denominator != 0, numerator / denominator, np.nan)
except FloatingPointError:
print("The division branch was evaluated before selection.")
else:
raise AssertionError('Expected a division failure')
safe = np.full(numerator.shape, np.nan)
np.divide(numerator, denominator, out=safe, where=denominator != 0)
print("Guarded result:", safe)
np.testing.assert_allclose(safe, [5, np.nan, 6], equal_nan=True)Expected output
The division branch was evaluated before selection.
Guarded result: [ 5. nan 6.]Nested conditions assign 50 for marketing, 65 for production and 45 for planning. Unknown departments retain NaN rather than receiving a guessed allowance. This follows the department mapping used in the Pandas exercise.
my_data = pd.DataFrame({'dept': ['mktg', 'production', 'planning', 'unknown']})
my_data['allowed'] = np.where(my_data['dept'] == 'mktg', 50,
np.where(my_data['dept'] == 'production', 65,
np.where(my_data['dept'] == 'planning', 45, np.nan)))
print(my_data)
np.testing.assert_allclose(my_data['allowed'], [50, 65, 45, np.nan], equal_nan=True)Expected output
dept allowed
0 mktg 50.0
1 production 65.0
2 planning 45.0
3 unknown NaNThese embedded synthetic students make practice independent of a local CSV path. A mark of 60 passes. Missing or out-of-range marks are flagged for review before classification, rather than accidentally treated as failures. See student sample data.
df = pd.DataFrame({'name': ['Alex', 'Ron', 'Ravi', 'King', 'Mira', 'Sam'],
'mark': [60.0, 80.0, 49.0, 70.0, np.nan, 105.0]})
valid = df['mark'].between(0, 100) & np.isfinite(df['mark'])
df['status'] = np.where(valid, np.where(df['mark'] >= 60, 'Pass', 'Fail'), 'Review')
print(df)
assert df['status'].tolist() == ['Pass', 'Pass', 'Fail', 'Pass', 'Review', 'Review']Expected output
name mark status
0 Alex 60.0 Pass
1 Ron 80.0 Pass
2 Ravi 49.0 Fail
3 King 70.0 Pass
4 Mira NaN Review
5 Sam 105.0 ReviewUse the stated grade rule: A from 80, B from 70, C from 50 and D below 50. This is separate from the pass threshold of 60. Check the boundaries, and retain Review for invalid marks.
marks = np.array([49, 50, 69, 70, 79, 80, 100, np.nan])
valid = np.isfinite(marks) & (marks >= 0) & (marks <= 100)
grade = np.where(valid, np.where(marks >= 80, 'A',
np.where(marks >= 70, 'B', np.where(marks >= 50, 'C', 'D'))), 'Review')
print("Marks:", marks)
print("Grades:", grade)
assert grade.tolist() == ['D', 'C', 'C', 'B', 'B', 'A', 'A', 'Review']Expected output
Marks: [ 49. 50. 69. 70. 79. 80. 100. nan]
Grades: ['D' 'C' 'C' 'B' 'B' 'A' 'A' 'Review']For several ordered thresholds, np.select can be easier to read than deeply nested where. The first True condition wins, so put higher thresholds first. For exact department labels, a mapping can be simpler. No selection API removes the need to handle missing values.
marks = np.array([49, 50, 70, 80, np.nan])
valid = np.isfinite(marks) & (marks >= 0) & (marks <= 100)
grade = np.select([~valid, marks >= 80, marks >= 70, marks >= 50],
['Review', 'A', 'B', 'C'], default='D')
print(grade)
assert grade.tolist() == ['D', 'C', 'B', 'A', 'Review']Expected output
['D' 'C' 'B' 'A' 'Review']This synthetic student exercise orders subjects alphabetically for each row. Compute one swap mask, then use separate where calls for text and marks. Keep the original columns until every new value is calculated; mixing text and numbers in one branch bundle can cause dtype coercion. Names are nonmissing and consistently capitalized here.
df = pd.DataFrame({'Name': ['Alex', 'Ron', 'Ravi', 'King'],
'Subject_1': ['Science', 'Social', 'History', 'English'],
'Mark_1': [30, 90, 10, 100],
'Subject_2': ['Chemistry', 'Math', 'Physics', 'Geography'],
'Mark_2': [40, 80, 60, 90]})
before = df.copy()
swap = df['Subject_1'] > df['Subject_2']
result = df.assign(
Subject_1=np.where(swap, df['Subject_2'], df['Subject_1']),
Mark_1=np.where(swap, df['Mark_2'], df['Mark_1']),
Subject_2=np.where(swap, df['Subject_1'], df['Subject_2']),
Mark_2=np.where(swap, df['Mark_1'], df['Mark_2']))
print("Input:")
print(before)
print("Ordered:")
print(result)
assert result['Subject_1'].tolist() == ['Chemistry', 'Math', 'History', 'English']
assert result['Mark_1'].tolist() == [40, 80, 10, 100]
for i in df.index:
original = sorted([(df.loc[i, 'Subject_1'], df.loc[i, 'Mark_1']), (df.loc[i, 'Subject_2'], df.loc[i, 'Mark_2'])])
reordered = [(result.loc[i, 'Subject_1'], result.loc[i, 'Mark_1']), (result.loc[i, 'Subject_2'], result.loc[i, 'Mark_2'])]
assert original == reordered
pd.testing.assert_frame_equal(df, before)Expected output
Input:
Name Subject_1 Mark_1 Subject_2 Mark_2
0 Alex Science 30 Chemistry 40
1 Ron Social 90 Math 80
2 Ravi History 10 Physics 60
3 King English 100 Geography 90
Ordered:
Name Subject_1 Mark_1 Subject_2 Mark_2
0 Alex Chemistry 40 Science 30
1 Ron Math 80 Social 90
2 Ravi History 10 Physics 60
3 King English 100 Geography 90This synthetic example gives a 10 percent discount when quantity is at least five. Missing or invalid quantity/price values remain unknown. The rule checks whole-number nonnegative quantities and finite nonnegative prices. Keep review counts alongside known subtotals.
quantity = np.array([2.0, 5.0, np.nan, -1.0, 6.0, 0.0])
price = np.array([10.0, 20.0, 10.0, 12.0, np.nan, 8.0])
valid = (np.isfinite(quantity) & (quantity >= 0) & (quantity == np.trunc(quantity))
& np.isfinite(price) & (price >= 0))
discount = np.where(quantity >= 5, 0.10, 0.0)
net = np.where(valid, quantity * price * (1 - discount), np.nan)
print("Net amounts:", net)
print("Known subtotal:", np.nansum(net))
print("Orders needing review:", np.count_nonzero(~valid))
np.testing.assert_allclose(net, [20, 90, np.nan, np.nan, np.nan, 0], equal_nan=True)
assert np.nansum(net) == 110 and (~valid).sum() == 3Expected output
Net amounts: [20. 90. nan nan nan 0.]
Known subtotal: 110.0
Orders needing review: 3One-argument where returns coordinates, not filtered values. Three-argument where selects a new array rather than mutating the source. Use & and | with parenthesized comparisons. Check broadcasting and output dtype. Keep missing values separate from known zero. Use guarded ufunc operations when a branch calculation is invalid for some inputs. NumPy results assigned to Pandas columns are positional; verify row order and length before assignment.
For stock [0, 4, 5, 8, NaN, -1], classify nonnegative finite whole counts below five as Reorder and the others as Enough. Invalid counts receive Review. Predict positions 0 and 1 as reorder candidates. Return both status labels and reorder positions.
Build one validity mask and reuse it for classification and positions. This prevents missing or negative stock from being treated as a valid reorder measurement.
stock = np.array([0.0, 4.0, 5.0, 8.0, np.nan, -1.0])
valid = np.isfinite(stock) & (stock >= 0) & (stock == np.trunc(stock))
status = np.where(valid, np.where(stock < 5, 'Reorder', 'Enough'), 'Review')
positions = np.nonzero(valid & (stock < 5))[0]
print("Status:", status)
print("Reorder positions:", positions)
assert status.tolist() == ['Reorder', 'Reorder', 'Enough', 'Enough', 'Review', 'Review']
np.testing.assert_array_equal(positions, [0, 1])Expected output
Status: ['Reorder' 'Reorder' 'Enough' 'Enough' 'Review' 'Review']
Reorder positions: [0 1]Open in Google Colab View on GitHub
All sample data is embedded. Run the examples and practise conditional replacement, indices and data validation. Save a copy in Drive to keep your changes.
Continue with NumPy tutorials, arange(), eye(), ones(), bincount() and linspace(). Reference: NumPy where documentation.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.