NumPy - np.allclose() - Compare with Tolerance

• `np.allclose()` compares arrays element-wise within absolute and relative tolerance thresholds, solving floating-point precision issues that break exact equality checks

Key Insights

np.allclose() compares arrays element-wise within absolute and relative tolerance thresholds, solving floating-point precision issues that break exact equality checks • The function uses the formula absolute(a - b) <= (atol + rtol * absolute(b)) for each element pair, where default values are rtol=1e-05 and atol=1e-08 • Understanding when to adjust tolerance parameters prevents false negatives in numerical computations, scientific calculations, and testing scenarios where minor differences are acceptable

Why Exact Equality Fails for Floating-Point Numbers

Floating-point arithmetic introduces rounding errors that make exact equality comparisons unreliable. Operations that should theoretically produce identical results often differ by tiny amounts.

import numpy as np

# Mathematical identity that fails with floating-point
a = 0.1 + 0.2
b = 0.3

print(f"a = {a}")
print(f"b = {b}")
print(f"a == b: {a == b}")  # False
print(f"Difference: {a - b}")  # 5.551115123125783e-17

# Array comparison with accumulated errors
arr1 = np.array([0.1 + 0.2, 0.4 + 0.5, 0.7 + 0.8])
arr2 = np.array([0.3, 0.9, 1.5])

print(f"\nDirect equality: {np.array_equal(arr1, arr2)}")  # False
print(f"Using allclose: {np.allclose(arr1, arr2)}")  # True

This demonstrates why np.allclose() is essential for real-world numerical comparisons where mathematical equivalence matters more than bit-level identity.

Basic Usage and Default Behavior

The np.allclose() function accepts two arrays and returns a single boolean indicating whether all elements are within tolerance.

import numpy as np

# Simple comparison with default tolerances
x = np.array([1.0, 2.0, 3.0])
y = np.array([1.0000001, 2.0000001, 3.0000001])

print(np.allclose(x, y))  # True

# Larger differences exceed default tolerance
z = np.array([1.001, 2.001, 3.001])
print(np.allclose(x, z))  # False

# Works with different shapes through broadcasting
scalar = 5.0
array = np.array([5.0, 5.0, 5.0])
print(np.allclose(scalar, array))  # True

# Multi-dimensional arrays
matrix1 = np.array([[1.0, 2.0], [3.0, 4.0]])
matrix2 = np.array([[1.0000001, 2.0000001], [3.0000001, 4.0000001]])
print(np.allclose(matrix1, matrix2))  # True

The default tolerance works well for most general-purpose comparisons but requires adjustment for specific use cases.

Understanding Relative and Absolute Tolerance

The tolerance formula combines both relative and absolute components: absolute(a - b) <= (atol + rtol * absolute(b)). This dual approach handles both small and large numbers appropriately.

import numpy as np

# Demonstrating relative tolerance (rtol)
# For large numbers, relative difference matters
large1 = np.array([1000000.0])
large2 = np.array([1000000.1])

print(f"Difference: {large2[0] - large1[0]}")  # 0.1
print(f"Default rtol (1e-05): {np.allclose(large1, large2)}")  # True
print(f"Stricter rtol (1e-08): {np.allclose(large1, large2, rtol=1e-08)}")  # False

# Demonstrating absolute tolerance (atol)
# For small numbers near zero, absolute difference matters
small1 = np.array([1e-10])
small2 = np.array([2e-10])

print(f"\nDifference: {small2[0] - small1[0]}")  # 1e-10
print(f"Default atol (1e-08): {np.allclose(small1, small2)}")  # True
print(f"Stricter atol (1e-11): {np.allclose(small1, small2, atol=1e-11)}")  # False

# Combined effect
mixed = np.array([1e-9, 1000.0])
reference = np.array([2e-9, 1000.001])

print(f"\nWith defaults: {np.allclose(mixed, reference)}")  # True
print(f"Only rtol=1e-05: {np.allclose(mixed, reference, atol=0)}")  # False (fails for small values)
print(f"Only atol=1e-08: {np.allclose(mixed, reference, rtol=0)}")  # False (fails for large values)

Practical Applications in Testing

Unit tests for numerical algorithms require tolerance-based comparisons to avoid brittle tests that fail due to implementation details rather than actual errors.

import numpy as np

def matrix_multiply_custom(A, B):
    """Custom matrix multiplication implementation."""
    return np.dot(A, B)

def test_matrix_operations():
    """Test suite using allclose for numerical stability."""
    
    # Test 1: Identity matrix property
    A = np.random.rand(3, 3)
    I = np.eye(3)
    result = matrix_multiply_custom(A, I)
    
    assert np.allclose(result, A), "Identity matrix test failed"
    
    # Test 2: Matrix inverse
    B = np.random.rand(4, 4)
    B_inv = np.linalg.inv(B)
    product = matrix_multiply_custom(B, B_inv)
    expected_identity = np.eye(4)
    
    assert np.allclose(product, expected_identity), "Inverse test failed"
    
    # Test 3: Transpose property
    C = np.random.rand(2, 3)
    D = np.random.rand(3, 2)
    result1 = matrix_multiply_custom(C, D).T
    result2 = matrix_multiply_custom(D.T, C.T)
    
    assert np.allclose(result1, result2), "Transpose property test failed"
    
    print("All tests passed!")

test_matrix_operations()

Handling Special Values

The function handles NaN and infinity values according to the equal_nan parameter, which is crucial for datasets with missing or undefined values.

import numpy as np

# NaN handling
arr_with_nan1 = np.array([1.0, 2.0, np.nan])
arr_with_nan2 = np.array([1.0, 2.0, np.nan])

print(f"Default (equal_nan=False): {np.allclose(arr_with_nan1, arr_with_nan2)}")  # False
print(f"With equal_nan=True: {np.allclose(arr_with_nan1, arr_with_nan2, equal_nan=True)}")  # True

# Infinity handling
arr_inf1 = np.array([1.0, np.inf, -np.inf])
arr_inf2 = np.array([1.0, np.inf, -np.inf])

print(f"\nInfinity comparison: {np.allclose(arr_inf1, arr_inf2)}")  # True

# Mixed special values
mixed1 = np.array([0.0, np.inf, np.nan, -np.inf])
mixed2 = np.array([1e-10, np.inf, np.nan, -np.inf])

print(f"Mixed without equal_nan: {np.allclose(mixed1, mixed2)}")  # False
print(f"Mixed with equal_nan: {np.allclose(mixed1, mixed2, equal_nan=True)}")  # True

Domain-Specific Tolerance Configuration

Different domains require different tolerance levels. Scientific computing, financial calculations, and graphics processing each have distinct precision requirements.

import numpy as np

# Scientific computing: moderate tolerance
def compare_physical_measurements(measured, expected):
    """Compare experimental results with 0.1% tolerance."""
    return np.allclose(measured, expected, rtol=1e-3, atol=1e-6)

measured_force = np.array([9.81, 19.62, 29.43])
expected_force = np.array([9.80, 19.60, 29.40])
print(f"Physical measurements match: {compare_physical_measurements(measured_force, expected_force)}")

# Financial calculations: strict tolerance
def compare_financial_values(calculated, expected):
    """Compare monetary values with cent precision."""
    return np.allclose(calculated, expected, rtol=0, atol=0.01)

calculated_prices = np.array([99.991, 199.995, 299.989])
expected_prices = np.array([100.00, 200.00, 300.00])
print(f"Financial values match: {compare_financial_values(calculated_prices, expected_prices)}")

# Graphics/rendering: loose tolerance
def compare_pixel_values(rendered, reference):
    """Compare rendered output with 1% tolerance."""
    return np.allclose(rendered, reference, rtol=1e-2, atol=1.0)

rendered_rgb = np.array([[255, 128, 64], [200, 150, 100]])
reference_rgb = np.array([[254, 127, 65], [201, 151, 99]])
print(f"Pixel values match: {compare_pixel_values(rendered_rgb, reference_rgb)}")

Performance Considerations

When comparing large arrays repeatedly, understanding np.allclose() performance characteristics helps optimize computational workflows.

import numpy as np
import time

# Performance comparison for large arrays
size = 10_000_000
large_array1 = np.random.rand(size)
large_array2 = large_array1 + np.random.rand(size) * 1e-6

# Timing allclose
start = time.time()
result = np.allclose(large_array1, large_array2)
allclose_time = time.time() - start
print(f"allclose time: {allclose_time:.4f}s, result: {result}")

# Manual tolerance check (less efficient)
start = time.time()
diff = np.abs(large_array1 - large_array2)
manual_result = np.all(diff <= 1e-5 * np.abs(large_array2) + 1e-8)
manual_time = time.time() - start
print(f"Manual check time: {manual_time:.4f}s, result: {manual_result}")

# Early termination benefit
almost_identical = large_array1.copy()
almost_identical[0] = 999.0  # Single large difference

start = time.time()
result_early = np.allclose(almost_identical, large_array1)
early_time = time.time() - start
print(f"Early termination time: {early_time:.4f}s, result: {result_early}")

The np.allclose() function provides optimized C-level implementation that outperforms manual Python loops while offering the flexibility needed for robust numerical comparisons across diverse application domains.

Liked this? There's more.

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