NumPy Masked Arithmetic: Define Invalid Results Before Division
Build auditable numerical masks, initialize skipped outputs, and distinguish input validity from finite arithmetic results.
Reviewed October 9, 2026. Examples target Python 3.12 and NumPy 2.3 and use synthetic floating-point values.
A rate calculation needs more than a formula. A zero denominator, a missing numerator, and an arithmetic overflow represent different failures, and replacing them all with zero can make an invalid record look like a valid absence of activity. A reliable numerical workflow defines which inputs are accepted and how an uncomputed result is represented before performing the division.
This article builds a small division contract around explicit Boolean masks and an initialized output array. It also explains why selecting between two expressions does not necessarily prevent those expressions from being evaluated. The examples use invented values to expose edge cases; they do not establish operational thresholds or rates for a real dataset.
1. Write the domain rule in ordinary language
Suppose a record contains a completed count and an available count. For this example, both values must be finite, the numerator must be nonnegative, and the denominator must be strictly positive. These are chosen application rules. Another application could legitimately allow negative denominators, so a generic numerical helper should not smuggle this specific policy into every ratio.
Keep the validity rule close to the operation. Naming the mask eligible or valid_input makes the intended boundary easier to inspect than embedding a long expression inside a call. If the rule changes, the reviewer can compare the old and new masks directly and update fixtures for the affected cases.
A missing output should remain distinguishable from a genuine zero result. In this tutorial, NaN marks an uncomputed floating-point position, and a Boolean mask records whether computation was attempted. A production export may need a separate status column as well, especially when its file format cannot preserve that distinction directly.
2. Initialize the output before applying a mask
The where argument to a NumPy universal function controls which output locations are computed. Locations where the condition is false retain their existing output values. Therefore an initialized out array is part of the contract. Asking NumPy to allocate an output while skipping positions does not give those positions a meaningful application-defined value.
import numpy as np
completed = np.array([8.0, 0.0, 3.0, np.nan, 5.0, -1.0])
available = np.array([10.0, 5.0, 0.0, 2.0, np.inf, 2.0])
assert completed.shape == available.shape
eligible = (
np.isfinite(completed)
& np.isfinite(available)
& (completed >= 0)
& (available > 0)
)
ratios = np.full(completed.shape, np.nan, dtype=np.float64)
with np.errstate(divide="raise", invalid="raise", over="raise"):
np.divide(completed, available, out=ratios, where=eligible)
np.testing.assert_array_equal(eligible, [True, True, False, False, False, False])
np.testing.assert_allclose(ratios[:2], [0.8, 0.0])
assert np.isnan(ratios[2:]).all()
assert eligible.sum() == 2
print(ratios)
The second valid record has a zero numerator and a positive denominator, so its computed zero is meaningful under our rule. The record with a zero denominator is skipped. Treating both results as zero would discard information about the calculation's validity.
| Input situation | Eligible under this fixture? | Declared output | Interpretation |
|---|---|---|---|
| Finite positive denominator and positive numerator | Yes | Computed ratio | Valid calculation |
| Zero numerator and finite positive denominator | Yes | Zero | Valid absence of completions |
| Zero denominator | No | NaN |
Ratio not defined by this contract |
| Missing or infinite input | No | NaN |
Rejected non-finite input |
| Negative numerator | No | NaN |
Rejected application value |
The table is a policy explanation for the synthetic count example. It does not classify every mathematical division problem, and it does not imply that infinity is always invalid in every numerical application.
3. Understand why selection can still evaluate unsafe arithmetic
A tempting expression is np.where(denominator != 0, numerator / denominator, np.nan). Python evaluates the arguments before calling np.where, so the division expression has already been evaluated. The final selection can hide a problematic result without preventing the problematic arithmetic from occurring.
Using the division ufunc's own where parameter is different because the mask is supplied to the operation that computes output locations. This distinction matters when floating-point errors should raise an exception, when warnings are evidence of a defect, or when an unnecessary intermediate would be expensive.
Do not treat suppression of a warning as a validity policy. An error-state context changes how floating-point conditions are reported; it does not decide whether a zero denominator was permitted by the application. Put that decision in the mask, then choose reporting behavior for the computation that remains.
4. Check the result as well as the inputs
Finite inputs can produce an infinite floating-point result through overflow. A positive denominator can be so small relative to its numerator that the representable output range is exceeded. Input eligibility and result acceptability are therefore different masks, even when they often agree on ordinary values.
The following complete program demonstrates both the eager-selection trap and a deliberately classified overflow. Its overflow handling is restricted to an example that immediately checks the result; silently ignoring overflow without checking would create a misleading success path.
import numpy as np
numerator = np.array([1.0, 1.0])
denominator = np.array([1.0, 0.0])
raised = False
try:
with np.errstate(divide="raise", invalid="raise"):
np.where(denominator != 0, numerator / denominator, np.nan)
except FloatingPointError:
raised = True
assert raised
huge = np.array([1e308, 4.0])
small = np.array([1e-308, 2.0])
eligible = np.isfinite(huge) & np.isfinite(small) & (small > 0)
output = np.full(huge.shape, np.nan, dtype=np.float64)
with np.errstate(over="ignore", divide="raise", invalid="raise"):
np.divide(huge, small, out=output, where=eligible)
finite_result = eligible & np.isfinite(output)
overflowed = eligible & ~np.isfinite(output)
assert eligible.all()
np.testing.assert_array_equal(finite_result, [False, True])
np.testing.assert_array_equal(overflowed, [True, False])
assert output[1] == 2.0
output[overflowed] = np.nan
assert np.isnan(output[0])
print(output, finite_result)
The values are intentionally extreme, not representative observations. Their purpose is to make a boundary visible. The program labels the first result as non-finite after accepted inputs; it then replaces that result with the declared missing representation. A real workflow should preserve that reason in its validation report rather than merging it with an input rejection.
5. Preserve reason codes and row alignment
A single validity mask is enough to choose computation locations, but often insufficient for an audit. Keep separate masks for a missing input, a forbidden denominator, and an unacceptable computed result. Define whether these reasons can overlap or whether one reason has precedence when a record violates several rules.
Retain the original row order and an identifier when attaching results to records. A correct numeric array can still be joined to the wrong rows if a filtering step changes positions. Assign results through the same index or explicitly checked identifier mapping used to construct the input arrays.
Report counts alongside the final data. For example, record how many rows were eligible, how many produced finite outputs, and how many were rejected for each documented reason. These counts should reconcile with the input row count under the chosen overlap policy. The resulting audit is much more useful than a final column filled with unexplained zeros.
6. Decide aggregation semantics explicitly
Skipping invalid ratios in a mean answers a different question from treating them as zero. Neither operation can be justified merely because a function makes it convenient. Decide which population the aggregate describes, report its denominator, and keep rejected observations visible in the quality summary.
The average of individual ratios also differs from the ratio of totals. Two rows with very different available counts receive equal weight in an ordinary average of row ratios but different weight in a pooled rate. Write the intended estimand in words before choosing the aggregation. This is a reasoning step about the dataset, not a broadcasting or masking feature.
For production data, establish tolerances and accepted ranges from the domain rather than copying the fixture's thresholds. If a denominator represents measurement noise near zero, an exact positivity check may not capture the uncertainty. Any threshold should have a documented rationale and tests around its boundary.
7. Build an adversarial numerical review
Test a valid positive ratio, a genuine zero, a zero denominator, a negative domain value, a missing input, and a non-finite result from finite inputs. Use explicit expected masks, rather than checking only that the function runs without warnings. Also assert that skipped locations keep the declared output representation.
Verify dtype and shape at the boundary. An integer output buffer is inappropriate for ratios and missing floating-point markers. Unexpected broadcasting can expand an operation or align values incorrectly, so equal input shape is a reasonable rule when the contract promises one ratio for each paired record.
The result should be explainable row by row: the inputs passed or failed a known rule, the operation computed only eligible positions, and a separate check accepted or classified its outputs. That explanation makes numerical cleanup reproducible without concealing why some values were never calculated.
Primary sources
- NumPy 2.3
divide: division, output buffers, and masked computation locations. - NumPy 2.3
isfinite: finite-value classification. - NumPy 2.3
errstate: scoped floating-point error reporting.