NumPy - np.logical_and/or/not/xor

NumPy's logical functions provide element-wise boolean operations on arrays. While Python's `&`, `|`, `~`, and `^` operators work on NumPy arrays, the explicit logical functions offer better control,...

Key Insights

  • NumPy’s logical functions (np.logical_and, np.logical_or, np.logical_not, np.logical_xor) operate element-wise on arrays and handle edge cases like NaN values more reliably than Python’s native operators
  • These functions accept where and out parameters for conditional operations and memory-efficient in-place modifications, critical for large-scale data processing
  • Understanding broadcasting rules and performance characteristics enables efficient multi-dimensional boolean indexing and complex filtering operations on arrays

Understanding NumPy Logical Operations

NumPy’s logical functions provide element-wise boolean operations on arrays. While Python’s &, |, ~, and ^ operators work on NumPy arrays, the explicit logical functions offer better control, clearer intent, and handle broadcasting more predictably.

import numpy as np

# Basic comparison: operators vs functions
a = np.array([True, True, False, False])
b = np.array([True, False, True, False])

# Using operators
result_op = a & b

# Using functions
result_func = np.logical_and(a, b)

print(result_op)    # [True False False False]
print(result_func)  # [True False False False]

The key difference emerges when working with non-boolean arrays. NumPy’s logical functions automatically convert inputs to boolean values:

x = np.array([0, 1, 2, 3])
y = np.array([0, 0, 1, 1])

# Logical functions treat non-zero as True
result = np.logical_and(x, y)
print(result)  # [False False True True]

# This differs from bitwise AND
bitwise = x & y
print(bitwise)  # [0 0 0 1]

np.logical_and: Intersection Logic

np.logical_and returns True where both input arrays have truthy values. This is essential for compound filtering conditions.

# Real-world example: filtering data
temperatures = np.array([15, 22, 28, 35, 18, 25])
humidity = np.array([45, 60, 75, 80, 50, 65])

# Find comfortable conditions: temp between 20-30 and humidity < 70
temp_good = np.logical_and(temperatures >= 20, temperatures <= 30)
humidity_good = humidity < 70

comfortable = np.logical_and(temp_good, humidity_good)
print(comfortable)  # [False True False False False True]

# Apply filter
comfortable_temps = temperatures[comfortable]
print(comfortable_temps)  # [22 25]

Multi-dimensional arrays showcase broadcasting capabilities:

# 2D sensor data: rows=sensors, cols=time points
sensor_data = np.array([
    [1.2, 0.8, 1.5, 2.1],
    [0.5, 1.1, 0.9, 1.8],
    [2.0, 1.9, 2.2, 2.5]
])

# Check multiple conditions across different axes
above_threshold = sensor_data > 1.0
below_max = sensor_data < 2.0

valid_readings = np.logical_and(above_threshold, below_max)
print(valid_readings)
# [[True False True False]
#  [False True False True]
#  [False True False False]]

np.logical_or: Union Logic

np.logical_or returns True when at least one input is truthy. Critical for inclusive filtering.

# Alert system: trigger on either condition
pressure = np.array([980, 1013, 1020, 950, 1015])
wind_speed = np.array([5, 15, 8, 45, 12])

# Alert if pressure < 970 OR wind > 40
low_pressure = pressure < 970
high_wind = wind_speed > 40

alert = np.logical_or(low_pressure, high_wind)
print(alert)  # [False False False True False]
print(f"Alert indices: {np.where(alert)[0]}")  # Alert indices: [3]

Combining with reduce for checking conditions across arrays:

# Check if any sensor in a group exceeds threshold
sensor_groups = np.array([
    [0.5, 0.6, 0.7],
    [1.2, 0.8, 0.9],
    [0.4, 0.3, 0.5]
])

threshold = 1.0

# Check each group (row) for any value > threshold
any_exceeded = np.logical_or.reduce(sensor_groups > threshold, axis=1)
print(any_exceeded)  # [False True False]

np.logical_not: Negation and Inversion

np.logical_not inverts boolean values, useful for finding complementary sets.

# Data validation: find invalid entries
data = np.array([1.5, np.nan, 2.3, np.inf, -1.2, 4.5])

# Identify valid numbers (not NaN, not infinite)
is_valid = np.logical_and(~np.isnan(data), ~np.isinf(data))
# Or using logical_not
is_valid = np.logical_and(
    np.logical_not(np.isnan(data)),
    np.logical_not(np.isinf(data))
)

print(is_valid)  # [True False True False True True]
clean_data = data[is_valid]
print(clean_data)  # [1.5 2.3 -1.2 4.5]

Practical application in mask inversion:

# Image processing: invert selection mask
image_mask = np.array([
    [True, True, False],
    [False, True, False],
    [True, False, False]
])

inverted_mask = np.logical_not(image_mask)
print(inverted_mask)
# [[False False True]
#  [True False True]
#  [False True True]]

# Count selected vs unselected pixels
selected = np.sum(image_mask)
unselected = np.sum(inverted_mask)
print(f"Selected: {selected}, Unselected: {unselected}")
# Selected: 4, Unselected: 5

np.logical_xor: Exclusive Or Logic

np.logical_xor returns True only when inputs differ. Useful for change detection and toggle operations.

# Change detection between states
previous_state = np.array([True, True, False, False, True])
current_state = np.array([True, False, True, False, False])

changes = np.logical_xor(previous_state, current_state)
print(changes)  # [False True True False True]
print(f"Changed indices: {np.where(changes)[0]}")
# Changed indices: [1 2 4]

Real-world application in feature comparison:

# Compare feature flags between versions
version_a_features = np.array([1, 1, 0, 1, 0, 1])
version_b_features = np.array([1, 0, 1, 1, 0, 0])

# Features that changed (added or removed)
feature_diff = np.logical_xor(version_a_features, version_b_features)
print(f"Features changed: {np.sum(feature_diff)}")  # Features changed: 3

# Identify specifically added vs removed
added = np.logical_and(feature_diff, version_b_features)
removed = np.logical_and(feature_diff, version_a_features)

print(f"Added features at indices: {np.where(added)[0]}")    # [2]
print(f"Removed features at indices: {np.where(removed)[0]}")  # [1 5]

Performance Optimization with out and where Parameters

NumPy logical functions support out for in-place operations and where for conditional execution.

# Memory-efficient in-place operations
large_array = np.random.rand(1000000) > 0.5
another_array = np.random.rand(1000000) > 0.5

# Preallocate output
result = np.empty(1000000, dtype=bool)

# Compute in-place
np.logical_and(large_array, another_array, out=result)

# Conditional computation with 'where'
mask = np.random.rand(1000000) > 0.3
output = np.zeros(1000000, dtype=bool)

np.logical_and(large_array, another_array, out=output, where=mask)
# Only elements where mask=True are computed

Combining Logical Operations for Complex Filters

Chain logical functions for sophisticated filtering:

# Multi-condition data filtering
data = {
    'temperature': np.array([15, 22, 28, 35, 18, 25, 30]),
    'pressure': np.array([1010, 1013, 1020, 995, 1015, 1018, 990]),
    'humidity': np.array([45, 60, 75, 80, 50, 65, 85])
}

# Complex condition: (temp 20-30 AND humidity < 70) OR pressure < 1000
temp_range = np.logical_and(
    data['temperature'] >= 20,
    data['temperature'] <= 30
)
low_humidity = data['humidity'] < 70
low_pressure = data['pressure'] < 1000

condition1 = np.logical_and(temp_range, low_humidity)
final_filter = np.logical_or(condition1, low_pressure)

print(f"Matching indices: {np.where(final_filter)[0]}")
# Matching indices: [1 3 5 6]

Broadcasting with Logical Operations

Leverage broadcasting for dimension-agnostic comparisons:

# 3D data: (samples, features, time)
measurements = np.random.randn(5, 3, 10)

# Check if all features exceed threshold at any time point
threshold_low = -1.0
threshold_high = 1.0

within_range = np.logical_and(
    measurements > threshold_low,
    measurements < threshold_high
)

# Check per sample if all features stay within range for entire duration
all_valid = np.all(within_range, axis=(1, 2))
print(f"Valid samples: {np.where(all_valid)[0]}")

NumPy’s logical functions provide the foundation for efficient boolean operations on arrays. Their element-wise nature, broadcasting support, and optimization parameters make them indispensable for data filtering, validation, and complex conditional logic in numerical computing pipelines.

Liked this? There's more.

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