Python Prevent Rounding Error with floats

Question:

I have checked dozens of other questions on stackoverflow about rounding floats in Python. I have learned a lot about how computers store these numbers, which is the root cause of the issue. However, I have not found a solution for my situation.

Imagine the following situation:

amount = 0.053
withdraw = 0.00547849

print(amount - withdraw)

>>> 0.047521509999999996

Here, I would actually like to receive 0.04752151, so a more rounded version.
However, I do not know the number of decimals these numbers should be rounded upon.

If I knew the number of decimals, I could do the following:

num_of_decimals = 8
round((amount - withdraw),num_of_decimals)
>>> 0.04752151

However, I do not know this parameter num_of_decimals. It could be 8,5,10,2,..

Any advice?

Asked By: intStdu

||

Answers:

When decimals are important, then Python offers a decimal module that works best with correct rounding.

from decimal import Decimal

amount = Decimal(0.053)
withdraw = Decimal(0.00547849)

print(amount - withdraw)

# 0.04752150999999999857886789911

num_of_decimals = 8
print(round(amount - withdraw,num_of_decimals))
# 0.04752151

Update: Get number of decimal

num_of_decimals = (amount - withdraw).as_tuple().exponent * -1

#or find
num_of_decimals = str(amount - withdraw)[::-1].find('.')

Instead of round

from decimal import getcontext

...

getcontext().prec = num_of_decimals

print(amount - withdraw)
# 0.047521510

See more decimal documentation

Answered By: Prayson W. Daniel

Concentrating purely on the number of decimal places, something simple, like converting the number to a string might help.

>>> x = 0.00547849
>>> len(str(x).split('.')[1])
8
>>> x = 103.323
>>> len(str(x).split('.')[1])
3

It appears that I’ve offended the gods by offering a dirty but practical solution but here goes nothing, digging myself deeper.

If the number drops into scientific notation via the str function you won’t get an accurate answer.
Picking the bones out of ‘23456e-05’ isn’t simple.
Sadly, the decimal solution also falls at this hurdle, once the number becomes small enough e.g.

>>> withdraw = decimal.Decimal('0.000000123456')
>>> withdraw
Decimal('1.23456E-7')

Also it’s not clear how you’d go about getting the string for input into the decimal function, because if you pump in a float, the result can be bonkers, not to put too fine a point on it.

>>> withdraw = decimal.Decimal(0.000000123456)
>>> withdraw
Decimal('1.23455999999999990576323642514633416311653490993194282054901123046875E-7')

Perhaps I’m just displaying my ignorance.

So roll the dice, as to your preferred solution, being aware of the caveats.
It’s a bit of a minefield.

Last attempt at making this even more hacky, whilst covering the bases, with a default mantissa length, in case it all goes horribly wrong:

>>> def mantissa(n):
...     res_type = "Ok"
...     try:
...         x = str(n).split('.')[1]
...         if x.isnumeric():
...             res = len(x)
...         else:
...             res_type = "Scientific notation "+x
...             res = 4
...     except Exception as e:
...         res_type = "Scientific notation "+str(n)
...         res = 4
...     return res, res_type
... 
>>> print(mantissa(0.11))
(2, 'Ok')
>>> print(mantissa(0.0001))
(4, 'Ok')
>>> print(mantissa(0.0001234567890))
(12, 'Ok')
>>> print(mantissa(0.0001234567890123456789))
(20, 'Ok')
>>> print(mantissa(0.00001))
(4, 'Scientific notation 1e-05')
>>> print(mantissa(0.0000123456789))
(4, 'Scientific notation 23456789e-05')
>>> print(mantissa(0.0010123456789))
(13, 'Ok')
Answered By: Rolf of Saxony

Use decimal with string inputs:

from decimal import Decimal

amount = Decimal("0.053")
withdraw = Decimal("0.00547849")

print(amount - withdraw)

# 0.04752151

Avoid creating floats entirely, and avoid the rounding issues.

If you end up with numbers small enough, and the default representation is now something like 1.2345765645033E-8, you can always get back to decimal notation in your representation:

>>> withdraw = Decimal("0.000000012345765645033")
>>> '{0:f}'.format(withdraw)
'0.000000012345765645033'
Answered By: njzk2
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.