How to Use Boolean Indexing in NumPy

Boolean indexing is NumPy's mechanism for selecting array elements based on True/False conditions. Instead of writing loops to check each element, you describe *what* you want, and NumPy handles the...

Key Insights

  • Boolean indexing lets you filter and modify NumPy arrays using conditions instead of loops, often achieving 10-100x performance improvements on large datasets.
  • Always use parentheses when combining conditions with &, |, and ~ operators—NumPy’s operator precedence differs from Python’s and/or keywords.
  • Boolean indexing returns a copy of the data, not a view, so modifications to the result won’t affect the original array unless you assign back through the mask.

Introduction to Boolean Indexing

Boolean indexing is NumPy’s mechanism for selecting array elements based on True/False conditions. Instead of writing loops to check each element, you describe what you want, and NumPy handles the how with optimized C code under the hood.

Consider the difference between these two approaches for finding values greater than 5:

import numpy as np

arr = np.array([2, 7, 1, 8, 3, 9, 4, 6])

# The slow way: Python loop
result_loop = []
for x in arr:
    if x > 5:
        result_loop.append(x)
result_loop = np.array(result_loop)

# The fast way: Boolean indexing
result_bool = arr[arr > 5]

print(result_bool)  # [7 8 9 6]

Both produce the same result, but the boolean indexing version is cleaner and dramatically faster. On an array with a million elements, the loop takes about 200 milliseconds on typical hardware. Boolean indexing? Around 5 milliseconds. That’s a 40x speedup with less code.

The performance gap exists because NumPy’s boolean operations execute in compiled C code, processing elements in bulk. Python loops carry interpreter overhead for every single iteration.

Creating Boolean Arrays

Boolean arrays are the foundation of boolean indexing. When you apply a comparison operator to a NumPy array, you get back an array of the same shape filled with True and False values.

arr = np.array([1, 5, 3, 8, 2, 9, 4])

# Each comparison produces a boolean array
print(arr > 5)   # [False False False  True False  True False]
print(arr == 3)  # [False False  True False False False False]
print(arr <= 4)  # [ True False  True False  True False  True]
print(arr != 2)  # [ True  True  True  True False  True  True]

This works element-wise. NumPy compares each element against the value and records the result. The boolean array acts as a mask—True means “include this element,” False means “exclude it.”

You can also compare arrays against each other:

a = np.array([1, 2, 3, 4, 5])
b = np.array([5, 4, 3, 2, 1])

print(a > b)   # [False False False  True  True]
print(a == b)  # [False False  True False False]

The arrays must have compatible shapes. NumPy’s broadcasting rules apply, so you can compare a 2D array against a 1D array if the dimensions align.

Applying Boolean Masks to Arrays

Once you have a boolean array, pass it as an index to extract matching elements:

arr = np.array([10, 25, 3, 42, 15, 8, 31])

mask = arr > 20
print(mask)        # [False  True False  True False False  True]
print(arr[mask])   # [25 42 31]

# Or combine into one line
print(arr[arr > 20])  # [25 42 31]

The result is always a 1D array containing only the elements where the mask is True. This flattening happens even with multidimensional arrays:

matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

print(matrix[matrix > 5])  # [6 7 8 9] - flattened result

If you need to preserve structure, you’ll need different techniques like np.where(), which we’ll cover later.

The boolean mask must match the array’s shape or be broadcastable to it. Mismatched shapes raise an IndexError:

arr = np.array([1, 2, 3, 4, 5])
bad_mask = np.array([True, False, True])  # Wrong length

# arr[bad_mask]  # Raises IndexError

Combining Conditions with Logical Operators

Real filtering often requires multiple conditions. NumPy provides bitwise operators for combining boolean arrays:

  • & — logical AND
  • | — logical OR
  • ~ — logical NOT

Critical warning: You cannot use Python’s and, or, and not keywords. These expect scalar boolean values, not arrays, and will raise a ValueError.

arr = np.array([1, 5, 3, 8, 2, 9, 4, 7])

# Find values between 3 and 7 (inclusive)
result = arr[(arr >= 3) & (arr <= 7)]
print(result)  # [5 3 4 7]

# Find values less than 3 OR greater than 7
result = arr[(arr < 3) | (arr > 7)]
print(result)  # [1 8 2 9]

# Find values NOT equal to 5
result = arr[~(arr == 5)]
print(result)  # [1 3 8 2 9 4 7]

The parentheses are mandatory. Without them, operator precedence breaks your logic:

# This fails or produces wrong results
# arr[arr >= 3 & arr <= 7]  # & binds tighter than >=

# Always use parentheses
arr[(arr >= 3) & (arr <= 7)]  # Correct

The bitwise operators have higher precedence than comparison operators in Python. The expression arr >= 3 & arr <= 7 gets parsed as arr >= (3 & arr) <= 7, which is nonsense.

Modifying Arrays with Boolean Indexing

Boolean indexing isn’t just for reading—you can assign values through a mask to modify arrays in place:

arr = np.array([1, -2, 3, -4, 5, -6])

# Replace negative values with zero
arr[arr < 0] = 0
print(arr)  # [1 0 3 0 5 0]

This pattern is invaluable for data cleaning. Common use cases include:

data = np.array([10.5, -999, 23.1, -999, 15.8, 42.0])

# Replace sentinel values with NaN
data[data == -999] = np.nan
print(data)  # [10.5  nan 23.1  nan 15.8 42. ]

# Cap values at a maximum
scores = np.array([85, 92, 105, 78, 110, 88])
scores[scores > 100] = 100
print(scores)  # [ 85  92 100  78 100  88]

# Apply different values based on condition
arr = np.array([1, 2, 3, 4, 5])
arr[arr % 2 == 0] *= 10  # Multiply even numbers by 10
print(arr)  # [ 1 20  3 40  5]

Remember that boolean indexing on the left side of an assignment modifies the original array. On the right side, it creates a copy.

Practical Applications

Boolean indexing shines in data analysis workflows. Here are patterns you’ll use constantly.

Handling missing or invalid data:

temperatures = np.array([72.1, -999, 68.5, 75.2, -999, 71.8])

# Create a clean dataset
valid_mask = temperatures != -999
clean_temps = temperatures[valid_mask]
print(f"Average temperature: {clean_temps.mean():.1f}")  # 71.9

Outlier detection and removal:

measurements = np.array([10.2, 10.5, 10.1, 50.3, 10.4, 10.3, 0.1])

mean = measurements.mean()
std = measurements.std()

# Remove values more than 2 standard deviations from mean
inliers = measurements[np.abs(measurements - mean) <= 2 * std]
print(inliers)  # [10.2 10.5 10.1 10.4 10.3]

Filtering 2D data based on column conditions:

# Sales data: [product_id, quantity, price]
sales = np.array([
    [101, 5, 29.99],
    [102, 2, 149.99],
    [103, 10, 9.99],
    [104, 1, 299.99],
    [105, 8, 19.99]
])

# Find high-value transactions (price > 100)
high_value = sales[sales[:, 2] > 100]
print(high_value)
# [[102.    2.  149.99]
#  [104.    1.  299.99]]

# Find bulk orders (quantity >= 5) with low prices (< 30)
bulk_cheap = sales[(sales[:, 1] >= 5) & (sales[:, 2] < 30)]
print(bulk_cheap)
# [[101.    5.   29.99]
#  [103.   10.    9.99]
#  [105.    8.   19.99]]

Performance Tips and Common Pitfalls

Avoid chained indexing. This creates intermediate arrays and can cause unexpected behavior:

arr = np.array([[1, 2], [3, 4], [5, 6]])

# Bad: chained indexing
# arr[arr[:, 0] > 2][:, 1] = 0  # May not modify original!

# Good: single expression
mask = arr[:, 0] > 2
arr[mask, 1] = 0  # Modifies original correctly

Use np.where() for conditional selection with alternatives:

arr = np.array([1, 2, 3, 4, 5])

# Boolean indexing: extract values
positives = arr[arr > 3]  # [4, 5]

# np.where: choose between two options
result = np.where(arr > 3, arr, 0)  # [0, 0, 0, 4, 5]
print(result)

# np.where with one argument: get indices
indices = np.where(arr > 3)
print(indices)  # (array([3, 4]),)

np.where(condition, x, y) returns elements from x where condition is True, and from y where it’s False. This preserves array shape, unlike boolean indexing which flattens.

Memory considerations: Boolean indexing creates copies. With large arrays, this matters:

big_array = np.random.randn(10_000_000)

# This creates a copy - uses memory
filtered = big_array[big_array > 0]

# For in-place operations, assign through the mask instead
big_array[big_array < 0] = 0  # Modifies in place, no copy

Use np.any() and np.all() to check conditions:

arr = np.array([1, 2, 3, 4, 5])

if np.any(arr > 4):
    print("At least one value exceeds 4")

if np.all(arr > 0):
    print("All values are positive")

Boolean indexing is one of NumPy’s most powerful features. Master it, and you’ll write cleaner, faster code for any data processing task. The key is thinking in terms of conditions on entire arrays rather than individual elements—let NumPy handle the iteration.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.