Using a computer in order to do rather complex Math is one of the reasons this machine was originally developed. As long as integer numbers and additions, subtractions, and multiplications are exclusively involved in the calculations, everything is fine. As soon as floating point numbers or fractions, as well as divisions, come into play it enormously complicates the whole matter.
As a regular user, we are not fully aware of these issues that happen behind the scenes and may end up with rather surprising, and possibly inaccurate results for our calculations. As developers, we have to ensure that appropriate measures are taken into account in order to instruct the computer to work in the right way.
In our daily life we use the decimal system that is based on the number 10. The computer uses the binary system, which is base 2, and internally it stores and processes the values as a sequence of 1s and 0s. The values we work with have to be constantly transformed between the two representations. As explained in Python's documentation:
...most decimal fractions cannot be represented exactly as binary fractions. A consequence is that, in general, the decimal floating-point numbers you enter are only approximated by the binary floating-point numbers actually stored in the machine.
This behavior leads to surprising results in simple additions, as shown here:
Listing 1: Inaccuracies with floating point numbers
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
As you can see here, the output is inaccurate, as it should result in 0.9.
Listing 2 shows a similar case for formatting a floating point number for 17 decimal places.
Listing 2: Formatting a floating point number
>>> format(0.1, '.17f')
'0.10000000000000001'
As you may have learned from the examples above, dealing with floating point numbers is a bit tricky, and requires additional measures in order to achieve the correct result, and to minimize computing errors. Rounding the value can solve at least some of the problems. One possibility is the built-in round()
function (for more details regarding its usage see below):
Listing 3: Calculating with rounded values
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
>>> s == 0.9
False
>>> round(0.9, 1) == 0.9
True
As an alternative, you can work with the math module, or explicitly work with fractions stored as two values (numerator and denominator) instead of the rounded, rather inexact floating point values.
In order to store the values like that the two Python modules decimal and fraction come into play (see examples below). But first, let us have a closer look at the term "rounding".
What is Rounding?
In a few words, the process of rounding means:
...replacing [a value] with a different number that is approximately equal to the original, but has a shorter, simpler, or more explicit representation.
Source: https://en.wikipedia.org/wiki/Rounding
Basically, it adds inaccuracy to a precisely calculated value by shortening it. In most cases this is done by removing digits after the decimal point, for example from 3.73 to 3.7, 16.67 to 16.7, or 999.95 to 1000.
Such a reduction is done for several reasons - for example, saving space when storing the value, or simply for removing unused digits. Furthermore, output devices such as analogue displays or clocks can show the computed value with only a limited precision, and require adjusted input data.
In general, two rather simple rules are applied for rounding, you may remember them from school. The digits 0 to 4 lead to rounding down, and the numbers 5 to 9 lead to rounding up. The table below shows a selection of use cases.
| original value | rounded to | result |
|----------------|--------------|--------|
| 226 | the ten | 230 |
| 226 | the hundred | 200 |
| 274 | the hundred | 300 |
| 946 | the thousand | 1,000 |
| 1,024 | the thousand | 1,000 |
| 10h45m50s | the minute | 10h45m |
Rounding Methods
Mathematicians have developed a variety of different rounding methods in order to address the problem of rounding. This includes simple truncation, rounding up, rounding down, rounding half-up, rounding half-down, as well as rounding half away from zero and rounding half to even.
As an example, rounding half away from zero is applied by the European Commission on Economical and Financial Affairs when converting currencies to the Euro. Several countries, such as Sweden, The Netherlands, New Zealand, and South Africa follow the rule named "cash rounding", "penny rounding", or "Swedish rounding".
[Cash rounding] occurs when the minimum unit of account is smaller than the lowest physical denomination of currency. The amount payable for a cash transaction is rounded to the nearest multiple of the minimum currency unit available, whereas transactions paid in other ways are not rounded.
Source: https://en.wikipedia.org/wiki/Cash_rounding
In South Africa, since 2002 cash rounding has been done to the nearest 5 cents. In general, this kind of rounding does not apply to electronic non-cash payments.
In contrast, rounding half to even is the default strategy for Python, NumPy, and Pandas, and is in use by the built-in round()
function that was already mentioned before. It belongs to the category of the round-to-nearest methods, and is also known as convergent rounding, statistician's rounding, Dutch rounding, Gaussian rounding, odd–even rounding, and bankers' rounding. This method is defined in IEEE 754 and works in such a way, that "if the fractional part of x
is 0.5, then y
is the even integer nearest to x
." It is assumed "that the probabilities of a tie in a dataset being rounded down or rounded up are equal" which is usually the case, in practice. Although not fully perfect this strategy leads to appreciable results.
The table below gives practical rounding examples for this method:
| original value | rounded to |
|----------------|------------|
| 23.3 | 23 |
| 23.5 | 24 |
| 24.0 | 24 |
| 24.5 | 24 |
| 24.8 | 25 |
| 25.5 | 26 |
Python Functions
Python comes with the built-in function round()
that is quite useful in our case. It accepts two parameters - the original value, and the number of digits after the decimal point. The listing below illustrates the usage of the method for one, two, and four digits after the decimal point.
Listing 4: Rounding with a specified number of digits
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
>>> round(15.45625, 1)
15.5
>>> round(15.45625, 2)
15.46
>>> round(15.45625, 4)
15.4563
If you call this function without the second parameter the value is rounded to a full integer value.
Listing 5: Rounding without a specified number of digits
>>> round(0.85)
1
>>> round(0.25)
0
>>> round(1.5)
2
Rounded values work fine in case you do not require absolutely precise results. Be aware of the fact that comparing rounded values can also be a nightmare. It will become more obvious in the following example - the comparison of rounded values based on pre-rounding, and post-rounding.
The first calculation of Listing 6 contains pre-rounded values, and describes rounding before adding the values up. The second calculation contains a post-rounded summary which means rounding after the summation. You will notice that the outcome of the comparison is different.
Listing 6: Pre-rounding vs. post-rounding
>>> round(0.3, 10) + round(0.3, 10) + round(0.3, 10) == round(0.9, 10)
False
>>> round(0.3 + 0.3 + 0.3, 10) == round(0.9, 10)
True
Python Modules for Floating Point Calculations
There are four popular modules that can help you properly deal with floating point numbers. This includes the math
module, the numpy
module, the decimal
module, and the fractions
module.
The math
module is centered around mathematical constants, floating point operations, and trigonometric methods. The numpy
module describes itself as "the fundamental package for scientific computing", and is famous for its variety of array methods. The decimal
module covers decimal fixed point and floating point arithmetic, and the fractions
module deals with rational numbers, specifically.
First, we have to try to improve the calculation from Listing 1. As Listing 7 shows, after having imported the math
module we can access the method fsum()
that accepts a list of floating point numbers. For the first calculation there is no difference between the built-in sum()
method, and the fsum()
method from the math
module, but for the second one it is, and returns the correct result we would expect. The precision depends on the underlying IEEE 754 algorithm.
Listing 7: Floating-point calculations with the help of the math
module
>>> import math
>>> sum([0.1, 0.1, 0.1])
0.30000000000000004
>>> math.fsum([0.1, 0.1, 0.1])
0.30000000000000004
>>> sum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
0.9999999999999999
>>> math.fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
1.0
Second, let us have a look at the numpy
module. It comes with the around() method that rounds the values provided as an array. It processes the single values in the same way as the default round()
method.
In order to compare values numpy
offers the equal()
method. Similar to around()
it accepts single values as well as lists of values (so-called vectors) to be processed. Listing 8 shows a comparison for single values as well as rounded values. The observed behavior is quite similar to the previously shown methods.
Listing 8: Comparing values using the equal method from the numpy
module
>>> import numpy
>>> print (numpy.equal(0.3, 0.3))
True
>>> print (numpy.equal(0.3 + 0.3 + 0.3 , 0.9))
False
>>> print (numpy.equal(round(0.3 + 0.3 + 0.3) , round(0.9)))
True
Option three is the decimal
module. It offers exact decimal representation, and preserves the significant digits. The default precision is 28 digits, and you can change this value to a number that is as large as needed for your problem. Listing 9 shows how to use a precision of 8 digits.
Listing 9: Creating decimal numbers using the decimal
module
>>> import decimal
>>> decimal.getcontext().prec = 8
>>> a = decimal.Decimal(1)
>>> b = decimal.Decimal(7)
>>> a / b
Decimal('0.14285714')
Now, comparing float values becomes a lot easier, and leads to the result we were looking for.
Listing 10: Comparisons using the decimal
module
>>> import decimal
>>> decimal.getcontext().prec = 1
>>> a = decimal.Decimal(0.3)
>>> b = decimal.Decimal(0.3)
>>> c = decimal.Decimal(0.3)
>>> a + b + c
Decimal('0.9')
>>> a + b + c == decimal.Decimal('0.9')
True
The decimal
module also comes with a method to round values - quantize(). The default rounding strategy is set to rounding half to even, and can also be changed to a different method if needed. Listing 11 illustrates the usage of the quantize()
method. Please note that the number of digits is specified using a decimal value as a parameter.
Listing 11: Rounding a value using quantize()
>>> d = decimal.Decimal(4.6187)
>>> d.quantize(decimal.Decimal("1.00"))
Decimal('4.62')
Last but not least, we will have a look at the fractions
module. This module allows you to handle floating point values as fractions, for example 0.3
as 3/10. This simplifies the comparison of floating point values, and completely eliminates rounding of values. Listing 12 shows how to use the fractions module.
Listing 12: Storing and comparing floating point values as fractions
>>> import fractions
>>> fractions.Fraction(4, 10)
Fraction(2, 5)
>>> fractions.Fraction(6, 18)
Fraction(1, 3)
>>> fractions.Fraction(125)
Fraction(125, 1)
>>> a = fractions.Fraction(6, 18)
>>> b = fractions.Fraction(1, 3)
>>> a == b
True
Furthermore, the two modules decimal
and fractions
can be combined, as shown in the next example.
Listing 13: Working with decimals and fractions
>>> import fractions
>>> import decimal
>>> a = fractions.Fraction(1,10)
>>> b = fractions.Fraction(decimal.Decimal(0.1))
>>> a,b
(Fraction(1, 10), Fraction(3602879701896397, 36028797018963968))
>>> a == b
False
Conclusion
Storing and processing floating point values correctly is a bit of a mission, and requires a lot of attention for programmers. Rounding the values may help, but be sure to check the correct order of rounding, and the method that you use. This is most important when developing things like financial software, so you'll want to check the rules of local law for rounding.
Python gives you all the tools that are needed, and comes with "batteries included". Happy hacking!
Acknowledgements
The author would like to thank Zoleka Hofmann for her critical comments while preparing this article.