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?
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
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')
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'
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?
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
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')
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'