Python Arithmetic With Small Numbers
Solution 1:
The precision of floats is higher near 0 than it is near 1.
- There are 4591870180066957722 floats between 0.0 and 0.1.
- There are 900719925474099 floats between 0.9 and 1.0, fewer by far.
Float density halves at regular intervals, it looks something like this:
Here is the next float after 1.
, in the direction of 0.
:
>>>import math>>>math.nextafter(1., 0.)
0.9999999999999999
>>>format(math.nextafter(1., 0.), ".32f") # let's see more decimal places
'0.99999999999999988897769753748435'
The mathematically correct value of 1 - 10 is 0.99999999999999999 (there are seventeen nines), I'll call this number n. Like almost all numbers, n can't be represented exactly with a float.
0.99999999999999999 # n
0.00000000000000001 # distance between n and 1, i.e. 10^-17
0.00000000000000010102230246251565... # distance between n and nextafter(1., 0.)
So you see, 1 - 10 is about 10 times further from nextafter(1., 0.)
than it is from 1.
. When the expression 1. - 1.e-17
is evaluated by the interpreter it gives you back the closest possible result, which is 1.
exactly. It wouldn't make sense to return any other float, that would be even further away from the "real" result (pardon the pun).
Note:math.nextafter
is available in Python 3.9+. In earlier versions you can use numpy.nextafter
similarly.
Related question -> Increment a Python floating point value by the smallest possible amount
Solution 2:
First, let's review what epsilon
really is in the return value of sys.float_info
.
Epsilon (or 𝟄
) is the smallest number such that 0.5 + 𝟄 ≠ 0.5 AND 0.5 - 𝟄 ≠ 0.5
Python is telling you that the smallest number that will cause 0.5
to increment or decrement repeatably is epsilon=2.220446049250313e-16
-- but this is only for the value 0.5. You are attempting to increment 1.0
by 1.0e-17
. This is a larger value (1.0 vs 0.5) being incremented by a smaller number than the 𝟄 for 0.5 (1.0e-17 vs 2.2e-16). You are off by an order of magnitude roughly, since the increment value of 1.0e-17 is an order of magnitude smaller than the relative epsilon for 1.0.
You can see this here:
These change the value of 0.5
>>>0.5+sys.float_info.epsilon
0.5000000000000002
>>>0.5-sys.float_info.epsilon
0.4999999999999998
These values do not:
>>>0.5+sys.float_info.epsilon/10.0
0.5
>>>0.5-sys.float_info.epsilon/10.0
0.5
>>>5.0+sys.float_info.epsilon
5.0
>>>5.0-sys.float_info.epsilon
5.0
Explanation:
IEEE 754 defines the floating point format in use today on most standard computers (specialty computers or libraries may use a different format.) The 64 bit format of IEEE 754 uses 53 bits of precision to calculate and 52 to store to the mantissa of a floating point value. Since you have a fixed 52/53 bits to work with, the magnitude and accuracy of the mantissa changes for larger / smaller values. So then the 𝟄 changes as the relative magnitude of a floating point number changes. The value of 𝟄 for 0.5 is different that the value for 1.0 and for 100.0.
For a variety of very good and platform-specific reasons (storage and representation, rounding, etc), even though you could use a smaller number, epsilon is defined as using 52 bits of precision for the 64 bit float format. Since most Python implementations use a C double float for float, this can be demonstrated:
>>> 2**-52==sys.float_info.epsilon
True
See how many bits your platform will do:
>>>0.5 + 2.0**-53
0.5000000000000001
>>>0.5 - 2.0**-53
0.4999999999999999
>>>0.5 + 2.0**-54
0.5 # fail for 0.5 + 54 bits...
>>>0.5 - 2.0**-54
0.49999999999999994 # OK for minus
>>>0.5 - 2.0**-55
0.5 # fail for 0.5 minus 55 bits...
There are several work arounds for your issue:
- You can use the C99 concept of nextafter to calculate the value appropriate epsilon. For Python, either use numpy or the Decimal class to calculate
nextafter
. More onnextafter
in my previous answer HERE - Use integers. A 64 bit integer will clearly handle an epsilon value in the 17th order of magnitude without rounding.
- Use an arbitrary precision math library. Decimal is in the standard Python distribution.
The important concept is that the value of 𝟄 is relative to value (and if you are incrementing or decrementing).
This can be seen here:
>>> numpy.nextafter(0.0,1.0)-0.04.9406564584124654e-324# a relative epsilon value of 4.94e-324>>> numpy.nextafter(0.01,1.0)-0.011.7347234759768071e-18# 1e-17 would still work...>>> numpy.nextafter(0.1,1.0)-0.11.3877787807814457e-17# 1e-17 would >>barely<< work...>>> numpy.nextafter(0.5,1.0)-0.51.1102230246251565e-16# a relative epsilon value of 1.1e-16>>> numpy.nextafter(500.0,501.0)-500.05.6843418860808015e-14# relative epsilon of 5.6e-14>>> numpy.nextafter(1e17,1e18)-1e1716.0# the other end of the spectrum...
So you can see that 1e-17 will work handily to increment values between 0.0 and 0.1 but not many values greater than that. As you can see above, the relative 𝟄 for 1e17 is 16.
Solution 3:
it should be able to handle "large" small numbers like 1e-17, shouldn't it?
Not necessarily (it depends on the numbers). A float
cannot exactly represent either 1e-17
or 1-(1e-17)
. In the case of the latter, the nearest number that it can represent is 1
.
I suggest you read What Every Computer Scientist Should Know About Floating-Point Arithmetic.
Solution 4:
If you need this level of precision, consider the Decimal module
>>> decimal.Decimal(1.0)-decimal.Decimal('1.0e-17')
Decimal('0.999999999999999990')
>>> decimal.Decimal(1.0)-decimal.Decimal('1.0e-17')<decimal.Decimal(1.0)
True
And:
>>> decimal.Decimal(1.0)-decimal.Decimal('1.0e-17')<1.0True
Careful with the last one though because you can get conversion errors.
Others have suggested What Every Computer Scientist Should Know About Floating-Point Arithmetic. and I also recommend Don’t Store That in a Float
Solution 5:
you can handle those. note that
>>> 1.e-17 == 0False
and
>>>1.e-17 + 1.e-18
1.1e-17
you simply cannot handle 1-1e-17, because the mantissa won't fit in the finite precision
Post a Comment for "Python Arithmetic With Small Numbers"