• No results found

Precise Maths with decimal Module

2. Fundamentals of Python

2.1. Introduction to Mathematics

2.1.6. Precise Maths with decimal Module

Python’s Standard Library goes one step forward. It grants us access to its another little pearl known as decimal module. Now the game is all about precision of calculations. When we use floating-point mathematical operations usually we do not think how computer represents the floats in its memory. The truth is a bit surprising when you discover that:

>>> r = 0.001

>>> print("r= %1.30f" % r) # display 30 decimal places r= 0.001000000000000000020816681712

instead of

r= 0.001000000000000000000000000000

It is just the way it is: the floats are represented in a binary format that involves a finite number of bits of their representation. When used in calculations, the floats provide us with a formal assurance up to 17 decimal places. However, the rest is not ignored and in case of heavy computations those false decimal digits may propagate. In order to “see” it—run the following code:

r = 0.001

r = 0.001000000000000000020816681712 t = 0.002000000000000000041633363423 r+t = 0.003000000000000000062450045135 operation of the floor division:

from math import trunc from random import random as r x = r()*100 # float

print(x//1 == trunc(x)) True

thought the former returns a float and trunc an integer if x is float.

You may obtain a pure fractional part of the float as follows, e.g:

>>> from math import pi

>>> pi

3.141592653589793

>>> pi - pi//1 0.14159265358979312

Well, I think that in some instances you ought to be (at least) aware

for i in range(1, 10000001):

s = s + r

print("r = %40.30f" % r)

print("E(s) = 10000.000000000000000000000000000000") print("s = %40.30f" % s)

The code returns:

r = 0.001000000000000000020816681712 E(s) = 10000.000000000000000000000000000000 s = 10000.000001578517185407690703868866

From an initial false precision detected at the 20th decimal place in r we end up with an error in our sum at the 6th decimal place! This is an excellent example of how much trust you can put in floats—

not only in Python but in any computer language today.

Is there any cure for that? Of course! This is exactly where Python’s decimal module earns its place in a spotlight.

Briefly speaking, the module has been created to handle imprecise floating-point accuracy. It is very complex in its structure and settings (see its full doc at https://docs.python.org/3.5/library/

decimal.html). However, for our needs of precise computations we are going to use only two functions out of its full package, namely,

Decimal and getcontext.

Here is a modification of the previous code:

from decimal import Decimal as d from decimal import getcontext as df r = 0.001 # float!

for i in range(1, 10000001):

s = s + r

print("r = %40.30f" % rfloat)

print("E(s) = 10000.000000000000000000000000000000") print("s = %40.30f" % s)

print("s' = %40.30f" % float(s))

By now the first two lines should be well understood. We import both functions but shorten their names to d and df, respectively.

Code 2.7

getcontext Decimal

for .. in Another classical loop across various computer languages.

With its help we control the exact number of times the underlying indented block of commands will be executed.

Here we make use of the range function to specify that i will run from 1 to 10000000 or/i.e. the loop will be repeated ten million times.

Code 2.8

You can skip this step if you would like to enforce the use of the full names or control the readability of your Python programs.

Next, we set a fixed precision for r to be considered from now on up to 3 decimal places only. By writing r = d(0.001) we create in Python an object to be viewed as a Decimal class.

From a side note you can understand that Decimal function acting upon specific float, e.g. r = d(0.001), carries float’s “false” comes to derivation of

we do not limit ourselves to the same precision. With decimal

module we can obtain results precisely accurate up to the desired precision. We select 30 decimal places for s. The party begins:

r = 0.001000000000000000020816681712 E(s) = 10000.000000000000000000000000000000

s = 10000.000000000000000000000000000000 # Decimal s' = 10000.000000000000000000000000000000 # float!

Excellent! Please note that s' is a Decimal number converted to float. This sort of result we were expecting from the beginning.

Eventually we’ve got it! Well, yes and no. Do you know why?

I chose the formatting of the output in the print function the same way that magicians make you believe that the impossible is in fact

— possible. The use of the decimal module vouchsafes the precision of computation. However, a simple conversion from Decimal to float number does not do the magic for us. See a side note on number conversion.

You can convince yourself that by running code 2.8 you will notice a much longer execution time. Therefore, the application of

decimal makes sense if you really want to assure the exact precision for all floating-point computations. In finance and accounting it may be of a paramount importance when it comes to reporting. operations keep the precision while computing but a "tricky" conversion from a Decimal to float simply does not work as you might think. Have a look:

>>> x = 0.007

>>> print('%.20f' % x) 0.00700000000000000015

>>> from decimal import Decimal as d

>>> from decimal import getcontext as df

The exact formula for solving this problem is:

or

if we hold our deposit for m periods (i.e. 3650 days) between dates (t-m). The Python code that finds requested compound return utilising both methods is presented below. First, we use floats to solve the puzzles. Next, we re-write everything with the application of the decimal module and compare precision of all results.

from decimal import getcontext as df from decimal import Decimal as d pa = 0.0365 # interest rate p.a. print("dRi = %s\n" % repr(dRi))

# formula (Decimals) df().prec = 30

dcompR0 = (d('1.0')+dRi)**d(str(m)) - d('1.0') tmp = d('1.0')

for i in range(1, m+1):

tmp = tmp * (d('1.0')+dRi)

# compound return (Decimals) dcompR = tmp - d('1.0')

print("(formula) compR = %1.30f" % compR0) print("(loop) compR = %1.30f" % compR) print()

print("(formula) dcompR = %1.30f" % dcompR0) print("(loop) dcompR = %1.30f" % dcompR)

Ri = 0.000099999999999999991239646446 dRi = Decimal('0.00010000')

(formula) compR = 0.440487720760861511948291990848 (loop) compR = 0.440487720760853074253304839658 (formula) dcompR = 0.440487720760919465590177424019 (loop) dcompR = 0.440487720760919465590177424019

Interestingly, the application of floats loses the precision starting at the 14th decimal place and, in addition, reveals a loss of coherence between the exact solution (formula) and the one found using multiplication. In contrast, the values of compound returns computed with the use of Decimals are the same regardless of the method we choose and selected/requested decimal precision. In other words, decimal module ensures precision of computations as displayed, here, with 30 significant decimal digits.

We have found that our $10,000 would grow by a bit more than 44% in 10 years, i.e. up to $14,404.88. The absolute difference between floats and Decimals would be $0.00000000058 then. So, would you care about those fractions of cents? Nah…