[Python-ideas] Way to check for floating point "closeness"?

Steven D'Aprano steve at pearwood.info
Sat Jan 17 11:01:18 CET 2015


On Fri, Jan 16, 2015 at 11:09:16PM -0500, Terry Reedy wrote:
> On 1/12/2015 12:02 PM, Chris Barker wrote:

> >but it:
> >
> >A) is buried in the unittest.TestCase class
> >
> >B) is an assertion, so you can't use it as a general test (easily)
> >
> >C) uses number of decimal digits or an absolute delta, but does not
> >provide a significant figures comparison, which is most likely what's
> >wanted (and a bit harder to write yourself)
> 
> assertAlmostEqual((a-b)/d, 0, delta = tol)
> where d is a, b, and (a+b)/2 as one thinks is appropriate.

That does nothing to solve problems A) and B), and I'm dubious that it 
provides a "significant figures comparison" (whatever that means, I'm 
pretty sure it doesn't mean "relative error", which is what you're 
calculating in a round-about fashion).


> >numpy provides allclose()
> 
> According to Neil Girdhar,
> absolute(/a/ - /b/) <= (/atol/ + /rtol/ * absolute(/b/))
> which I presume means, in Python,
> abs(a-b) <= atol + rtol * abs(b)
> where atol and rtol are assume >= 0.0

Adding the error tolerances together is a dubious thing to do. I don't 
understand the reasoning between that. Given two values a and b, there 
are two definitions of the error between them:

absolute = abs(a - b)
relative = abs(a - b)/abs(b)

[Note: I'm sticking to numpy's unconditional use of "b" for the 
denominator, which is not symmetric. In actuality, I would use 
min(abs(a), abs(b)) for the denominator.]

In the case where we only specify absolute or relative tolerance, it is 
obvious what to do: calculate the appropriate error, and if it is less 
than the given tolerance, return True:


def approx_equal(a, b, allowed_absolute_error, allowed_relative_error):
    # For simplicity, ignore NANs, INFs, and assume b != 0
    actual_error = abs(a - b)
    if allowed_absolute_error is None:
        # Only perform a check on relative error.
        return actual_error <= allowed_relative_error*abs(b)
    elif allowed_relative_error is None:
        # Only perform a check on absolute error.
        return actual_error <= allowed_absolute_error
    else:
        # We have specified *both* abs and rel error.


How should we handle the third case? Two obvious ways come to mind: 
require that *both* individual tests pass:

        return (actual_error <= allowed_relative_error*abs(b)
                and
                actual_error <= allowed_absolute_error)
        # equivalent to:
        # actual_error <= max(allowed_relative_error*abs(b), 
        #                     allowed_absolute_error)


or require that *either* test pass:

        return (actual_relative_error <= allowed_relative_error
                or
                actual_absolute_error <= allowed_absolute_error)
        # equivalent to:
        # actual_error <= min( ... )


But what numpy does is to add the tolerances together, that is, it uses 
*twice* the average of them, equivalent to this:

        allowed_error = (
            allowed_absolute_error + allowed_relative_error*abs(b)
            )
        return actual_absolute_error <= allowed_error


This means that numpy will claim that two numbers are close even though 
*both* the absolute and relative error tests fail:

py> numpy.allclose([1.2], [1.0], 0.0, 0.1)  # Fails absolute error test.
False
py> numpy.allclose([1.2], [1.0], 0.1, 0.0)  # Fails relative error test.
False
py> numpy.allclose([1.2], [1.0], 0.1, 0.1)  # Passes!
True


I cannot think of a good justification for that. Either I am missing 
something, or this goes to show that numpy can mess up even something as 
simple and straightforward as an error calculation. If I'm right, that's 
further evidence that getting this "right" and putting it in the 
standard library is a good thing to do.


[...]
> Consider the problem of finding the (unique) 0 crossing (root) of a 
> monotonic function f.  One is looking for a value x* such that f(x*) is 
> 'near' 0.  (Someone claimed that 'nothing is close to zero'.  This is 
> nonsensical both in applied math and everyday life.)

It isn't nonsensical, it just needs to be understood in context of 
relative errors. All non-zero numbers are infinitely far from zero in 
terms of relative error.


> A standard 
> approach is to compute successive approximations until one finds such a 
> point.  But unthinking application of such a method may not get one the 
> desired result.  The following are two examples with opposite problems.
[snip anecdote]

I didn't think that the well-known difficulties in root-finding has 
anything to do with the usefulness of a standard way to compare numbers 
for approximate equality.



-- 
Steven


More information about the Python-ideas mailing list