math.isclose() example: Newton's method

Introducing math.isclose()

The math.isclose() function (new in Python 3.5) is the one obvious way™ to solve a common problem in floating point math: how to safely compare two values when we can't get exact equality due to the limitations of floating point arithmetic.

For example, consider this:


In [1]:
three_tenths = .1 + .1 + .1
three_tenths


Out[1]:
0.30000000000000004

In [2]:
three_tenths == .3


Out[2]:
False

The issue is the efficient but approximate binary representation of some numbers (like .1) in modern CPUs using the IEEE-754 floating point standard. There are several ways of making such a comparison with a practical tolerance, but since Python 3.5 the canonical way is to use math.isclose(), like this:


In [3]:
import math

math.isclose(three_tenths, .3)


Out[3]:
True

The math.isclose() function has two additional arguments to fine tune the tolerance, but I'll not cover them here.

Now let's see a real example using isclose().

Using math.isclose() with Newton's method

Newton's method of succesive approximations can be used to compute the square root.

How Newton's method works

To compute sqrt(n), the algorithm starts with guess=n/2 and computes a better_guess: the average of guess and n/guess. If those two guesses are equal or very close, the square root is the better_guess. If not, the better_guess is used as the guess, and a new better_guess is computed as the average of that and n/guess. This process quickly converges to a very good approximation of the square root.

The implementation below uses math.is_close() to test whether the better_guess is close to the current one, which means further approximations will not be useful and the better_guess is an acceptable result.


In [4]:
import math

def newton_sqrt(n, verbose=False):
    guess = n / 2
    while True:
        if verbose: print('guess ->', guess)
        better_guess = (guess + n/guess) / 2
        if math.isclose(guess, better_guess):
            return better_guess
        guess = better_guess

Sample use:


In [5]:
newton_sqrt(100)


Out[5]:
10.0

Using verbose=True, we can see how quickly Newton's algorithm converges to the solution:


In [6]:
newton_sqrt(100, True)


guess -> 50.0
guess -> 26.0
guess -> 14.923076923076923
guess -> 10.812053925455988
guess -> 10.030495203889796
guess -> 10.000046356507898
guess -> 10.000000000107445
Out[6]:
10.0

Applying newton_sqrt() to numbers from 9 to 16, we see that some results are a little different from those from math.sqrt(). However, the results are all considered close enough by isclose() with the default tolerance.


In [7]:
for n in range(9, 17):
    computed = newton_sqrt(n)
    expected = math.sqrt(n)
    close = math.isclose(computed, expected)
    delta = computed - expected
    print('sqrt({:2d}): {:.20f} {:.20f} {} {:.20f}'.format(n, computed, expected, close, delta))


sqrt( 9): 3.00000000000000000000 3.00000000000000000000 True 0.00000000000000000000
sqrt(10): 3.16227766016837907870 3.16227766016837952279 True -0.00000000000000044409
sqrt(11): 3.31662479035539980998 3.31662479035539980998 True 0.00000000000000000000
sqrt(12): 3.46410161513775438635 3.46410161513775438635 True 0.00000000000000000000
sqrt(13): 3.60555127546398956895 3.60555127546398912486 True 0.00000000000000044409
sqrt(14): 3.74165738677394132949 3.74165738677394132949 True 0.00000000000000000000
sqrt(15): 3.87298334620741702139 3.87298334620741702139 True 0.00000000000000000000
sqrt(16): 4.00000000000000000000 4.00000000000000000000 True 0.00000000000000000000

Since Python already has a math.sqrt() function, our newton_sqrt() is only a didactic example. However, it is an elegant algorithm to know, and a fine example of the utility of math.isclose().

Bonus topic: the Decimal type

Another way of dealing with floating point issues is to completely avoid them by using the decimal.Decimal type, which can represent 0.1 with exact precision:


In [8]:
from decimal import Decimal

one_tenth = Decimal('.1')
three_tenths = one_tenth + one_tenth + one_tenth
three_tenths == Decimal('.3')


Out[8]:
True

Note the use of string arguments in the Decimal constructors above. Using a float to build a Decimal is often a bad idea, because, the Decimal value will reflect the imprecision of the float:


In [9]:
Decimal(.1)


Out[9]:
Decimal('0.1000000000000000055511151231257827021181583404541015625')

In [10]:
Decimal(.1) == one_tenth


Out[10]:
False

Again, the isclose function can be helpful:


In [11]:
math.isclose(Decimal(.1), one_tenth)


Out[11]:
True

But if you need exact precision you should only use Decimal numbers built from strings or integers.


In [12]:
Decimal('.1') * 10


Out[12]:
Decimal('1.0')

In general, calculations with money should use Decimal values to represent money amounts.