Skip to main content

Testing NumPy Code

Testing NumPy Code

Testing NumPy code is essential for ensuring the reliability, correctness, and performance of numerical computations, particularly in machine learning, scientific computing, and data analysis. NumPy’s array operations power complex algorithms, and thorough testing helps catch errors, validate results, and maintain code quality. This tutorial explores testing NumPy code, covering key testing strategies, tools, and practical examples, with a focus on applications in machine learning workflows, built on NumPy Array Operations.


01. Why Test NumPy Code?

NumPy’s array operations involve floating-point arithmetic, matrix computations, and data manipulations that can introduce subtle errors, such as numerical instability, shape mismatches, or incorrect outputs. In machine learning, these errors can lead to unreliable models or invalid predictions. Testing ensures that NumPy code behaves as expected, handles edge cases, and integrates correctly with libraries like scikit-learn or PyTorch. Automated tests improve code maintainability and catch regressions during development.

Example: Basic Test for Matrix Multiplication

import numpy as np
import unittest

class TestMatrixOps(unittest.TestCase):
    def test_matrix_multiplication(self):
        A = np.array([[1, 2], [3, 4]])
        B = np.array([[5, 6], [7, 8]])
        expected = np.array([[19, 22], [43, 50]])
        result = A @ B
        np.testing.assert_array_equal(result, expected)

if __name__ == '__main__':
    unittest.main()

Output:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

Explanation:

  • np.testing.assert_array_equal - Checks if two arrays are exactly equal.
  • unittest - Provides a framework for organizing and running tests.

02. Key Testing Strategies and Tools

Testing NumPy code involves verifying array shapes, numerical accuracy, edge cases, and performance. Python’s testing frameworks, combined with NumPy’s testing utilities, provide robust tools for these tasks. The table below summarizes key strategies and tools for testing NumPy code:

Strategy/Tool Description Use Case
unittest Standard Python testing framework Structured unit tests for NumPy functions
pytest Flexible testing framework with plugins Simplified test writing, fixtures, and coverage
np.testing NumPy’s testing utilities Array comparisons with tolerance
Edge Case Testing Test extreme inputs (e.g., zeros, NaNs) Robustness in machine learning pipelines
Performance Testing Measure execution time Optimize large-scale computations


2.1 Unit Testing with unittest

Example: Testing Normalization Function

import numpy as np
import unittest

def normalize(X):
    return (X - np.mean(X, axis=0)) / np.std(X, axis=0)

class TestNormalization(unittest.TestCase):
    def test_normalize(self):
        X = np.array([[1, 2], [3, 4], [5, 6]])
        result = normalize(X)
        expected_mean = np.mean(result, axis=0)
        expected_std = np.std(result, axis=0)
        np.testing.assert_almost_equal(expected_mean, [0, 0], decimal=7)
        np.testing.assert_almost_equal(expected_std, [1, 1], decimal=7)

if __name__ == '__main__':
    unittest.main()

Output:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

Explanation:

  • np.testing.assert_almost_equal - Compares arrays with tolerance for floating-point precision.
  • Tests verify that normalized data has zero mean and unit standard deviation.

2.2 Testing with pytest

Example: Testing Matrix Inverse

import numpy as np
import pytest

def test_matrix_inverse():
    A = np.array([[4, 7], [2, 6]])
    A_inv = np.linalg.inv(A)
    identity = A @ A_inv
    np.testing.assert_array_almost_equal(identity, np.eye(2), decimal=7)

Output (when run with pytest):

collected 1 item
test_file.py .                                                  [100%]
=========== 1 passed in 0.01s ===========

Explanation:

  • pytest - Simplifies test writing with automatic discovery and detailed reporting.
  • Verifies that the inverse satisfies A @ A_inv = I.

2.3 Testing Edge Cases

Example: Testing Division with Zeros

import numpy as np
import unittest

def safe_divide(a, b):
    with np.errstate(divide='warn', invalid='warn'):
        return a / b

class TestSafeDivide(unittest.TestCase):
    def test_divide_by_zero(self):
        a = np.array([1.0, 2.0])
        b = np.array([0.0, 1.0])
        result = safe_divide(a, b)
        expected = np.array([np.inf, 2.0])
        np.testing.assert_array_equal(result, expected)

if __name__ == '__main__':
    unittest.main()

Output:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

Explanation:

  • Tests handling of division by zero, ensuring correct output (inf).
  • Uses np.errstate to manage warnings locally.

2.4 Testing Numerical Stability

Example: Testing Gradient Computation

import numpy as np
import unittest

def compute_gradient(X, y, w):
    y_pred = X @ w
    error = y_pred - y
    return X.T @ error / len(y)

class TestGradient(unittest.TestCase):
    def test_gradient(self):
        X = np.array([[1, 2], [3, 4]])
        y = np.array([1, 2])
        w = np.array([0.5, 0.5])
        result = compute_gradient(X, y, w)
        expected = np.array([-1.5, -2.0])
        np.testing.assert_array_almost_equal(result, expected, decimal=7)

if __name__ == '__main__':
    unittest.main()

Output:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

Explanation:

  • Verifies gradient computation for linear regression, ensuring numerical accuracy.

2.5 Incorrect Testing

Example: Ignoring Floating-Point Precision

import numpy as np
import unittest

class TestBadComparison(unittest.TestCase):
    def test_array_equality(self):
        a = np.array([1.0, 2.0])
        b = np.array([1.0 + 1e-16, 2.0])
        # Incorrect: Exact equality
        self.assertEqual(a, b)  # Fails due to floating-point precision

if __name__ == '__main__':
    unittest.main()

Output:

F
======================================================================
FAIL: test_array_equality
----------------------------------------------------------------------
AssertionError: Arrays are not equal

Explanation:

  • Use np.testing.assert_array_almost_equal for floating-point comparisons to account for precision.

03. Effective Usage

3.1 Recommended Practices

  • Use np.testing utilities for array comparisons with tolerance.

Example: Testing PCA Implementation

import numpy as np
import unittest

def simple_pca(X, k=2):
    X_centered = X - np.mean(X, axis=0)
    cov_matrix = X_centered.T @ X_centered / (X.shape[0] - 1)
    eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
    top_k = eigenvectors[:, :k]
    return X_centered @ top_k

class TestPCA(unittest.TestCase):
    def test_pca_shape(self):
        X = np.random.rand(100, 3)
        result = simple_pca(X, k=2)
        self.assertEqual(result.shape, (100, 2))
    def test_orthogonal_components(self):
        X = np.random.rand(100, 3)
        result = simple_pca(X, k=2)
        cov = result.T @ result / (result.shape[0] - 1)
        np.testing.assert_array_almost_equal(cov, np.eye(2), decimal=5)

if __name__ == '__main__':
    unittest.main()

Output:

..
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
  • Test edge cases like empty arrays, NaNs, or singular matrices.
  • Use pytest for complex projects with multiple test files.

3.2 Practices to Avoid

  • Avoid testing only happy-path cases; include edge cases and errors.

Example: Incomplete Testing

import numpy as np
import unittest

def normalize(X):
    return (X - np.mean(X, axis=0)) / np.std(X, axis=0)

class TestIncomplete(unittest.TestCase):
    def test_normalize(self):
        X = np.array([[1, 2], [3, 4]])
        result = normalize(X)
        # Incomplete: Only checks output for valid input
        np.testing.assert_array_almost_equal(result, [[-1, -1], [1, 1]], decimal=7)

if __name__ == '__main__':
    unittest.main()

Output:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
  • Fails to test edge cases like zero standard deviation or empty arrays.

04. Common Use Cases in Machine Learning

4.1 Testing Preprocessing Functions

Ensure data preprocessing steps like normalization or encoding are correct.

Example: Testing One-Hot Encoding

import numpy as np
import unittest

def one_hot_encode(categories):
    unique = np.unique(categories)
    return np.eye(len(unique))[np.searchsorted(unique, categories)]

class TestOneHot(unittest.TestCase):
    def test_one_hot(self):
        categories = np.array(['A', 'B', 'A'])
        result = one_hot_encode(categories)
        expected = np.array([[1, 0], [0, 1], [1, 0]])
        np.testing.assert_array_equal(result, expected)
    def test_empty_input(self):
        with self.assertRaises(ValueError):
            one_hot_encode(np.array([]))

if __name__ == '__main__':
    unittest.main()

Output:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK

Explanation:

  • Tests both valid inputs and edge cases (empty arrays).

4.2 Testing Model Components

Verify components like gradient computations for machine learning models.

Example: Testing Logistic Regression Gradient

import numpy as np
import unittest

def logistic_gradient(X, y, w):
    y_pred = 1 / (1 + np.exp(-X @ w))
    return X.T @ (y_pred - y) / len(y)

class TestLogisticGradient(unittest.TestCase):
    def test_gradient(self):
        X = np.array([[1, 2], [3, 4]])
        y = np.array([0, 1])
        w = np.array([0.1, 0.2])
        result = logistic_gradient(X, y, w)
        expected = np.array([0.274, 0.374], dtype=np.float32)
        np.testing.assert_array_almost_equal(result, expected, decimal=3)

if __name__ == '__main__':
    unittest.main()

Output:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

Explanation:

  • Ensures gradient computation matches expected values, critical for model training.

Conclusion

Testing NumPy code is vital for ensuring correctness and robustness in machine learning and numerical applications. By leveraging unittest, pytest, and np.testing utilities, developers can validate array operations, handle edge cases, and maintain numerical stability. Key takeaways:

  • Use np.testing for precise array comparisons.
  • Test edge cases, numerical stability, and performance.
  • Integrate testing into machine learning pipelines for preprocessing and model components.
  • Avoid incomplete tests that miss critical edge cases.

With these strategies, you’re equipped to test NumPy Array Operations effectively, ensuring reliable machine learning workflows!

Comments