0

The error below occurs on the 14th decimal:

>>> 1001*.2
200.20000000000002

Here* the error occurs on the 18th decimal digit:

>>> from decimal import Decimal
>>> Decimal.from_float(.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
#                           ^ 
#                           |_ here 

*Note: I used Fraction since >>> 0.1 is displayed as 0.1 in the console, but I think this is related to how it's printed, not how it's stored.

Questions:

  • Is there a way to determine on which exactly decimal digit the error will occur?
  • Is there a difference between Python 2 and Python 3?
user
  • 5,370
  • 8
  • 47
  • 75
  • 15th digit is the last one that is guaranteed to be correct on any single decimal number. The errors accumulate, however. However if your number is a negative power of 2 it can be much more precise... – Antti Haapala -- Слава Україні Sep 04 '16 at 14:21
  • @AnttiHaapala Are you sure? In the case of `1001*.2` the last correct digit is the 13th. Also, the `size_hint` is provided by the user and so powers of 2 aren't an option. – user Sep 04 '16 at 14:34
  • @Fermiparadox the last digit `2` is the *17th* significant digit. – Antti Haapala -- Слава Україні Sep 04 '16 at 14:46
  • @AnttiHaapala 17th significant? Shouldn't the `0` preceding the last `2` be considered significant so that `2` is rounded to `0`? I m having a hard time understanding what you mean by 15th digit. In my 2 examples, where do i start counting to 15, so that I detect the first "incorrect" (that is, that should be rounded) digit? – user Sep 04 '16 at 15:45
  • @AnttiHaapala I think i got it. You meant to say that up to 15 _significant_ digits or more will be correct. – user Sep 05 '16 at 07:21
  • 15 significant digits are always correct. However, `decimal.Decimal.from_float(2 ** -53)` -> `Decimal('1.1102230246251565404236316680908203125E-16')` is exact result. – Antti Haapala -- Слава Україні Sep 05 '16 at 07:44

3 Answers3

2

If we assume that the size of the widget is stored exactly, then there are 2 sources of error: the conversion of size_hint from decimal -> binary, and the multiplication. In Python, these should both be correctly rounded to nearest, so each should have relative error of half an ulp (unit in the last place). Since the second operation is a multiplication we can just add the bounds to get a total relative error which will be bounded 1 ulp, or 2-53.

Converting to decimal:

>>> math.trunc(math.log10(2.0**-53))
-15

this means you should be accurate to 15 significant figures.

There shouldn't be any difference between Python 2 and 3: Python has long been fairly strict about floating point behaviour, the only change I'm aware of is the behaviour of the round function, which isn't used here.

Simon Byrne
  • 7,694
  • 1
  • 26
  • 50
  • Doesn't this contradict the 2 examples in my question? First one has an error on the 18th decimal. Unless i misunderstood something. – user Sep 04 '16 at 13:14
  • 1
    ah, I misunderstood. The 15 is just a lower bound, to figure out the exact number you would have to do something similar to your above calculation. – Simon Byrne Sep 04 '16 at 20:43
2

To answer the decimal to double-precision floating-point conversion part of your question...

The conversion of decimal fractions between 0.0 and 0.1 will be good to 15-16 decimal digits (Note: you start counting at the first non-zero digit after the point.)

0.1 = 0.1000000000000000055511151231257827021181583404541015625 is good to 16 digits (rounded to 17 it is 0.10000000000000001; rounded to 16 it is 0.1).

0.2 = 0.200000000000000011102230246251565404236316680908203125 is also good to 16 digits.

(An example only good to 15 digits:

0.81 = 0.810000000000000053290705182007513940334320068359375)

Rick Regan
  • 3,407
  • 22
  • 28
  • _"..will be good to 15-16 decimal digits"_ - I m assuming you mean "significant" digits. Also, does the same hold true for floats in the `0.1` to `10000.` range? – user Sep 05 '16 at 07:19
  • Yes, significant digits; and yes, 15-16 digits over that range too. – Rick Regan Sep 05 '16 at 16:42
0

I'd recommend you take a read to pep485

Using == operator to compare floating-point values is not the right way to go, instead consider using math.isclose or cmath.isclose, here's a little example using your values:

try:
    from math import isclose

    v1 = 101 * 1 / 5
    v2 = 101 * (1 / 5)
except:
    v1 = float(101) * float(1) / float(5)
    v2 = float(101) * (float(1) / float(5))

    def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
        return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

print("v1==v2 {0}".format(v1 == v2))
print("isclose(v1,v2) {0}".format(isclose(v1, v2)))

As you can see, I'm explicitly casting to float in python 2.x and using the function provided in the documentation, with python 3.x I just use directly your values and the provided function from math module.

BPL
  • 9,632
  • 9
  • 59
  • 117
  • I m afraid this does not answer either of my questions. The example at the top was provided just to show an issue i encountered, but if it is confusing i can remove it. – user Sep 04 '16 at 12:45
  • @Fermiparadox The issue you found is because you're not comparing correctly floating point values, as I've already mentioned don't use `==` operator. Regarding to your questions 1) you could compare the string representation of your floating-point values to know the exact digit (unrecommended & slow), nobody does that and 2) read [this](https://docs.python.org/3/tutorial/floatingpoint.html) – BPL Sep 04 '16 at 12:53
  • @Fermiparadox Also, I'd recommend you take a read to [this](http://gamedevs.org/uploads/numerical-robustness-geometric-calculations.pdf), it isn't python related though – BPL Sep 04 '16 at 12:55
  • Thanks for the links, but as i already said the example was somewhat irrelevant of the actual goals of my question. I removed it in order to prevent further confusion. – user Sep 04 '16 at 13:00
  • Relax dude, i only dved because this doesn't answer the question, not to punish you or anything. You can (being a grown adult) remove your answer, or change it so that it _does_ answer the question. I don't care about rep, i ve deleted even my upvoted posts if they are bad. Future visitors get deceived if votes are used for revenge. – user Sep 04 '16 at 14:34
  • @Fermiparadox I don't know where your comment is coming from but just to say I've downvoted your question because several reasons: a) Your question proves you haven't taken your time to learn about floating-point maths b) Your question isn't adding any real value to anybody and c) Your attitude of downvoting an answer which provides real value to understand floating-point maths to the general public is not cool at all, just commenting is not useful for you was ok. That's pretty much, you're talking big here accusing of revenge to someone who wasted his valuable time trying to help you. – BPL Sep 04 '16 at 14:46
  • You are taking it personally, and i told you i have nothing against you. The problem is that *this answer does not address any of the 2 key points under the "Question" section of the OP*. Also, if u thought it was not useful, u would have downvoted 2 hours earlier. Lastly, code of my question originates from your first link, meaning _i did do_ my homework before asking. I will not bother you anymore. Have a nice day. – user Sep 04 '16 at 14:50