Proposal: support for closeness comparison
See original GitHub issueDue to the limitations of floating point arithmetic, comparing floating point values for bitwise equality is only required in very few situations. In usual sitatuations, for example comparing the output of a function against an expected result, it has thus become best practice to compare the values for closeness rather than equality. Python added built-in support for closeness comparisons (math.isclose
) with PEP485 which was introduced in Python 3.5.
With this I’m proposing to add an elementwise isclose
operator:
def isclose(x1, x2, *, rtol: float, atol: float):
pass
Similar to equal
, x1
and x2
as well as the return value are arrays. The returned array will be of type bool
.
The relative tolerance rtol
and absolute tolerance atol
should have default values which are discussed below.
Status quo
All actively considered libraries already at least partially support closeness comparisons. In addition to the elementwise isclose
operation, usually also allclose
is defined. Since allclose(a, b) == all(isclose(a, b))
and all
is already part of the standard, I don’t think adding allclose
is helpful. Otherwise, we would also need to consider allequal
and so on.
Library | isclose |
allclose |
---|---|---|
NumPy | numpy.isclose |
numpy.allclose |
TensorFlow | tensorflow.experimental.numpy.isclose |
tensorflow.experimental.numpy.allclose |
PyTorch | torch.isclose |
torch.allclose |
MXNet | mxnet.contrib.ndarray.allclose |
|
JAX | jax.numpy.isclose |
jax.numpy.allclose |
Dask | dask.array.isclose |
dask.array.allclose |
CuPy | cupy.isclose |
cupy.allclose |
Closeness definition
All the libraries above define closeness like this:
abs(actual - expected) <= atol + rtol * abs(expected)
PEP485 states about this:
In this approach, the absolute and relative tolerance are added together, rather than the or method used in [
math.isclose
]. This is computationally more simple, and if relative tolerance is larger than the absolute tolerance, then the addition will have no effect. However, if the absolute and relative tolerances are of similar magnitude, then the allowed difference will be about twice as large as expected. […] Even more critically, if the values passed in are small compared to the absolute tolerance, then the relative tolerance will be completely swamped, perhaps unexpectedly.
math.isclose
overcomes this and additionally is symmetric:
abs(actual - expected) <= max(atol, rtol * max(abs(actual, expected)))
Thus, in addition to adding the isclose
operator, I think it should stick to the objectively better definition of math.isclose
.
Non-finite numbers
In addition to finite numbers, the standard should also define how non-finite numbers (NaN
, inf
, and -inf
) are to be handled. Again, I propose to stick to the rationale of PEP485, which in turn is based on IEEE 754:
NaN
is never close to anything. All library implementations add aequal_nan: bool = False
flag to the functions. IfTrue
twoNaN
values are considered close. Still, comparison between any other value and aNaN
is never considered close.inf
, and-inf
are only close to themselves.
Default tolerances
In addition to fixed default values (math.isclose
: rtol=1e-9, atol=0.0
, all libraries: rtol=1e-5, atol=1e-8
) the default tolerances could also be varied by the promoted dtype. For example, arrays of dtype float64
could use stricter default tolerances as float32
.
For integer dtypes, I propose using rtol = atol = 0.0
which would be identical to comparing them for equality. For floating point dtypes I would use the rationale of PEP485 as base:
rtol
: Approximately half the precision of the promoted dtypeatol
:0.0
Issue Analytics
- State:
- Created 2 years ago
- Reactions:1
- Comments:13 (8 by maintainers)
Top GitHub Comments
Thanks @pmeier!
equal_nan
is quite useful for testing. There you typically compare against expected values; if the expected result of some function call is[1.5, 2.5, nan]
then clearly you wantequal_nan=True
otherwise you have to special-case nan’s everywhere.For library code though, it’s typically the opposite - you want the regular IEEE 754 behavior where
nan
s are not equal.That does of course make it a little questionable to have
equal_nan
inisclose
/allclose
rather than only in testing functions likeassert_allclose
. Maybe for statistical algorithms like the hypothesis tests inscipy.stats
it still makes sense.To investigate the impact of the proposed change, I’ve implemented it for
numpy
and run thenumpy
,scipy
, andscikit-learn
test suite against this patched version.numpy
Only a single test fails with respect to the numerics (there are < 10 failures for tests of the test function that break due to my PoC implementation):
Since
rtol
andatol
have a similar magnitude, the addition of both tolerances made this test pass. Individually the maximum absolute difference is greater thanatol
(0.00528 > 0.005
) and the maximum relative difference is greater thanrtol
(0.00228 > 0.001
).scipy
Internally
scipy
relies at one point on the asymmetry ofnp.isclose
. I think this is not intended, but I opened scipy/scipy#14081 to make sure. After patching this we have 10 failing tests left with 3 of them again related to the PoC implementation. 3 fall same category as the failingnumpy
test:4 more tests have very strict tolerances which are manually set:
For example:
scikit-learn
No failing tests.
IMO, this means two things: