Function to determine if two numbers are nearly equal when rounded to n significant decimal digits

Question:

I have been asked to test a library provided by a 3rd party. The library is known to be accurate to n significant figures. Any less-significant errors can safely be ignored. I want to write a function to help me compare the results:

def nearlyequal( a, b, sigfig=5 ):

The purpose of this function is to determine if two floating-point numbers (a and b) are approximately equal. The function will return True if a==b (exact match) or if a and b have the same value when rounded to sigfig significant-figures when written in decimal.

Can anybody suggest a good implementation? I’ve written a mini unit-test. Unless you can see a bug in my tests then a good implementation should pass the following:

assert nearlyequal(1, 1, 5) 
assert nearlyequal(1.0, 1.0, 5) 
assert nearlyequal(1.0, 1.0, 5) 
assert nearlyequal(-1e-9, 1e-9, 5) 
assert nearlyequal(1e9, 1e9 + 1 , 5) 
assert not nearlyequal( 1e4, 1e4 + 1, 5) 
assert nearlyequal( 0.0, 1e-15, 5 ) 
assert not nearlyequal( 0.0, 1e-4, 6 ) 

Additional notes:

  1. Values a and b might be of type int, float or numpy.float64. Values a and b will always be of the same type. It’s vital that conversion does not introduce additional error into the function.
  2. Lets keep this numerical, so functions that convert to strings or use non-mathematical tricks are not ideal. This program will be audited by somebody who is a mathematician who will want to be able to prove that the function does what it is supposed to do.
  3. Speed… I’ve got to compare a lot of numbers so the faster the better.
  4. I’ve got numpy, scipy and the standard-library. Anything else will be hard for me to get, especially for such a small part of the project.
Asked By: Salim Fadhley

||

Answers:

Here’s a take.

def nearly_equal(a,b,sig_fig=5):
    return ( a==b or 
             int(a*10**sig_fig) == int(b*10**sig_fig)
           )
Answered By: Kenan Banks

“Significant figures” in decimal is a matter of adjusting the decimal point and truncating to an integer.

>>> int(3.1415926 * 10**3)
3141
>>> int(1234567 * 10**-3)
1234
>>>
Answered By: S.Lott

There is a function assert_approx_equal in numpy.testing (source here) which may be a good starting point.

def assert_approx_equal(actual,desired,significant=7,err_msg='',verbose=True):
    """
    Raise an assertion if two items are not equal up to significant digits.

    .. note:: It is recommended to use one of `assert_allclose`,
              `assert_array_almost_equal_nulp` or `assert_array_max_ulp`
              instead of this function for more consistent floating point
              comparisons.

    Given two numbers, check that they are approximately equal.
    Approximately equal is defined as the number of significant digits
    that agree.
Answered By: dF.

I believe your question is not defined well enough, and the unit-tests you present prove it:

If by ’round to N sig-fig decimal places’ you mean ‘N decimal places to the right of the decimal point’, then the test assert nearlyequal(1e9, 1e9 + 1 , 5) should fail, because even when you round 1000000000 and 1000000001 to 0.00001 accuracy, they are still different.

And if by ’round to N sig-fig decimal places’ you mean ‘The N most significant digits, regardless of the decimal point’, then the test assert nearlyequal(-1e-9, 1e-9, 5) should fail, because 0.000000001 and -0.000000001 are totally different when viewed this way.

If you meant the first definition, then the first answer on this page (by Triptych) is good.
If you meant the second definition, please say it, I promise to think about it 🙂

Answered By: Oren Shemesh

This is a fairly common issue with floating point numbers. I solve it based on the discussion in Section 1.5 of Demmel[1]. (1) Calculate the roundoff error. (2) Check that the roundoff error is less than some epsilon. I haven’t used python in some time and only have version 2.4.3, but I’ll try to get this correct.

Step 1. Roundoff error

def roundoff_error(exact, approximate):
    return abs(approximate/exact - 1.0)

Step 2. Floating point equality

def float_equal(float1, float2, epsilon=2.0e-9):
    return (roundoff_error(float1, float2) < epsilon)

There are a couple obvious deficiencies with this code.

  1. Division by zero error if the exact value is Zero.
  2. Does not verify that the arguments are floating point values.

Revision 1.

def roundoff_error(exact, approximate):
    if (exact == 0.0 or approximate == 0.0):
        return abs(exact + approximate)
    else:
        return abs(approximate/exact - 1.0)

def float_equal(float1, float2, epsilon=2.0e-9):
    if not isinstance(float1,float):
        raise TypeError,"First argument is not a float."
    elif not isinstance(float2,float):
        raise TypeError,"Second argument is not a float."
    else:
        return (roundoff_error(float1, float2) < epsilon)

That’s a little better. If either the exact or the approximate value is zero, than the error is equal to the value of the other. If something besides a floating point value is provided, a TypeError is raised.

At this point, the only difficult thing is setting the correct value for epsilon. I noticed in the documentation for version 2.6.1 that there is an epsilon attribute in sys.float_info, so I would use twice that value as the default epsilon. But the correct value depends on both your application and your algorithm.

[1] James W. Demmel, Applied Numerical Linear Algebra, SIAM, 1997.

Answered By: tmh

Oren Shemesh got part of the problem with the problem as stated but there’s more:

assert nearlyequal( 0.0, 1e-15, 5 )

also fails the second definition (and that’s the definition I learned in school.)

No matter how many digits you are looking at, 0 will not equal a not-zero. This could prove to be a headache for such tests if you have a case whose correct answer is zero.

Answered By: Loren Pechtel

There are already plenty of great answers, but here’s a think:

def closeness(a, b):
  """Returns measure of equality (for two floats), in unit
     of decimal significant figures."""
  if a == b:
    return float("infinity")
  difference = abs(a - b)
  avg = (a + b)/2
  return math.log10( avg / difference )


if closeness(1000, 1000.1) > 3:
  print "Joy!"

There is a interesting solution to this by B. Dawson (with C++ code)
at “Comparing Floating Point Numbers”. His approach relies on strict IEEE representation of two numbers and the enforced lexicographical ordering when said numbers are represented as unsigned integers.

Answered By: Dan M.

There are lots of ways of comparing two numbers to see if they agree to N significant digits. Roughly speaking you just want to make sure that their difference is less than 10^-N times the largest of the two numbers being compared. That’s easy enough.

But, what if one of the numbers is zero? The whole concept of relative-differences or significant-digits falls down when comparing against zero. To handle that case you need to have an absolute-difference as well, which should be specified differently from the relative-difference.

I discuss the problems of comparing floating-point numbers — including a specific case of handling zero — in this blog post:

http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/

Answered By: Bruce Dawson

As of Python 3.5, the standard way to do this (using the standard library) is with the math.isclose function.

It has the following signature:

isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

An example of usage with absolute error tolerance:

from math import isclose
a = 1.0
b = 1.00000001
assert isclose(a, b, abs_tol=1e-8)

If you want it with precision of n significant digits, simply replace the last line with:

assert isclose(a, b, abs_tol=10**-n)
Answered By: erb

I have been asked to test a library provided by a 3rd party

If you are using the default Python unittest framework, you can use assertAlmostEqual

self.assertAlmostEqual(a, b, places=5)
Answered By: serv-inc
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.