classification
Title: Getting incorrect results in rounding procedures
Type: behavior Stage: resolved
Components: Interpreter Core Versions: Python 3.9, Python 3.8, Python 3.7
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: docs@python Nosy List: AVicennA, docs@python, ned.deily, paul.moore, ronaldoussoren, steven.daprano, terry.reedy, tim.golden, zach.ware
Priority: normal Keywords:

Created on 2019-12-16 08:50 by AVicennA, last changed 2019-12-17 14:17 by mark.dickinson. This issue is now closed.

Messages (8)
msg358467 - (view) Author: AVicennA (AVicennA) * Date: 2019-12-16 08:50
This is about rounding process and getting incorrect results. In documentation written
that, "This is not a bug: it’s a result of the fact that most decimal fractions can’t be 
represented exactly as a float". - https://docs.python.org/3/library/functions.html?highlight=round#round
It is also related with hardware. I wrote some code parts that shows it and used decimal value
as in documentation sample:

''' 2.675(4) - (4) or (3) or (2) etc. I have given range 2, and the result is influenced not 
    just by one number after those 2 ranges, but also the another number consistently. '''

>>> round(2.675, 2)
2.67
>>>
>>> round(5.765, 2)
5.76
>>>
>>> round(2.6754, 2)
2.68
>>>
>>> round(5.7652, 2)
5.77

''' "format" is also not working properly. Gives incorrect results. '''

>>> format(2.675, ".2f")  
'2.67'
>>>
>>> format(2.678, ".2f")
'2.68'
>>>
>>> '{:0.2f}'.format(2.675)
'2.67'
>>>
>>> '{:0.2f}'.format(2.678)
'2.68'

''' Because, when the decimal string is converted to a binary floating-point number, it's 
    again replaced with a binary approximation:

    Whose exact value is 5.765 --> 5.76499999999999968025576890795491635799407958984375
                                                 &&
                         2.675 --> 2.67499999999999982236431605997495353221893310546875

    It means that, the 76(5) --> 5 replaced in a memory as 4.(999999999999)
                                                 &&
                       67(5) --> 5 replaced in a memory as 4.(999999999999) '''

>>> from decimal import Decimal
>>> Decimal(2.675)
Decimal('2.67499999999999982236431605997495353221893310546875')
>>>
>>> Decimal(5.765)
Decimal('5.76499999999999968025576890795491635799407958984375')

''' Used float point precision(FPU) with math lib to apply a certain correct form. 
    I propose to use some tricks. But again incorrect result in third sample: '''

>>> import math
>>> math.ceil(2.675 * 100) / 100
2.68
>>>
>>> print("%.2f" % (math.ceil(2.675 * 100) / 100))
2.68
>>> math.ceil(2.673 * 100) / 100
2.68

''' The most correct form is using by round: '''

>>> round(2.675 * 100) / 100
2.68
>>>
>>> round(2.673 * 100) / 100
2.67
>>> round(2.674 * 100) / 100
2.67
>>> round(2.676 * 100) / 100
2.68

''' In this case, whatever the range the full right result is a return.
    Mostly can be using in fraction side correctness. '''

>>> def my_round(val, n):
...     return round(val * 10 ** n) / 10 ** n
...
>>> my_round(2.675, 2)
2.68
>>>
>>> my_round(2.676, 2)
2.68
>>>
>>> my_round(2.674, 2)
2.67
>>>
>>> my_round(2.673, 2)
2.67
>>>
>>> my_round(2.674, 3)
2.674
>>>
>>> my_round(55.37678, 3)
55.377
>>>
>>> my_round(55.37678, 2)
55.38
>>>
>>> my_round(55.37478, 2)
55.37
>>>
>>> my_round(224.562563, 2)
224.56
>>>
>>> my_round(224.562563, 3)
224.563
>>>
>>> my_round(224.562563, 4)
224.5626
>>>
>>> my_round(224.562563, 5)
224.56256
>>>
>>> my_round(224.562563, 7)
224.562563
>>>
>>> my_round(224.562563, 11)
224.562563

''' my_round - function tested on Windows and Linux platforms(x64). This can be added in Python
    next releases to solve this problem which related with the IEEE 754 and PEP 754 problems. '''
msg358470 - (view) Author: Steven D'Aprano (steven.daprano) * (Python committer) Date: 2019-12-16 10:02
You've spent a lot of time demonstrating behaviour, but have not given us any reason why you think the results given are wrong.

As far as I can tell, every single example you show is correct. You have even quoted one of the relevant sections from the docs. "This is not a bug..."

You've flagged this is a documentation bug, an IDLE bug, an interpreter core bug, a library bug, etc. It isn't all of those things! I don't know what you actually want: do you want to improve the documentation? Something else?

By the way, your function "my_round" is buggy. This is wrong:

my_round(2.675, 2)
2.68

The binary float 2.675 is exactly equal to 3011782250804019/1125899906842624, or in decimal, exactly 

2.67499999999999982236431605997495353221893310546875

Rounding to two decimal places is 2.67, not 2.68.

Can you explain why we shouldn't close this as Not A Bug?
msg358471 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2019-12-16 10:11
@AVicennA: as the docs you linked to explain, this is not a bug: `round` is giving the correct result in all cases (or at least, as correct as is possible given the use of binary floating-point).

Let's take just the first case, `round(2.675, 2)`. `2.675` is a numeric literal in the source that's converted to a Python object of type `float` whose value is stored[*] using the IEEE 754 binary64 format. The exact value of that object is then 2.67499999999999982236431605997495353221893310546875.

So the value that Python sees when rounding is *less* than the halfway case 2.675, so it rounds down to 2.67. If you think that's not the right thing to do, I have a question for you: what result would you expect `round(2.6749999999999998, 2)` to give?

Your proposed my_round replacement is not a fix: unlike *round*, it does *not* do correct rounding in all cases, and does not always give the naively expected result in all cases either. To give just one example of many:

    >>> my_round(4.395, 2)
    4.39

So I don't really understand what action you're proposing here. `round` is as good as it can reasonably be, and the documentation already explains the weaknesses and links to further reading. Unless you're proposing that Python adopt decimal floating-point for its core float type, or that two-argument round be deprecated, there not really anything to be done here.

[*] Disclaimer: use of IEEE 754 is not guaranteed, but is overwhelmingly likely on common platforms.
msg358473 - (view) Author: AVicennA (AVicennA) * Date: 2019-12-16 11:37
@steven.daprano, yes, my_round(2.675, 2) --> gave 2.68 and it's not buggy and not wrong. This is correct in this case. I advise you look at 5th class math book.
msg358511 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2019-12-16 18:46
The flaw in my_round is that it rounds twice, not once.  The first rounding is caused by multiplying by a factor that is not a power of 2.  

In the case of 2.675, that rounding is up enough to affect the second rounding.
>>> format(2.675, ".17f")
'2.67499999999999982'
>>> format(2.675 *100, ".17f")
'267.50000000000000000'

In the case of 4.395, the first rounding is down.
>>> format(4.395, ".17f")
'4.39499999999999957'
>>> format(4.395 *100, ".17f")
'439.49999999999994316'
Even if it had been up, it might not have been enough to affect the outcome, as 57 is a lot farther from 100 than 82.

If you want to discuss floating point approximations and rounding further, please post to python-list, not here.
msg358514 - (view) Author: AVicennA (AVicennA) * Date: 2019-12-16 19:04
@mark.dickinson,

1) Where is your "`round` is giving the correct result in all cases"??

>>> round(4.395, 2)
4.39

2) I wrote it in my post using decimal punct:
''' Because, when the decimal string is converted to a binary floating-point number, it's 
    again replaced with a binary approximation:

    Whose exact value is 5.765 --> 5.76499999999999968025576890795491635799407958984375
                                                 &&
                         2.675 --> 2.67499999999999982236431605997495353221893310546875

    It means that, the 76(5) --> 5 replaced in a memory as 4.(999999999999)
                                                 &&
                       67(5) --> 5 replaced in a memory as 4.(999999999999) '''

3) round(2.6749999999999998, 2) --> this is the same with round(2.675, 2).

4) I also have a question for you.. Do you have any suggestion to solve this problem?
msg358516 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2019-12-16 19:37
@AVicennA: 4.39 *is* the correctly-rounded result for `round(4.395, 2)`. Modulo (as-yet unreported) bugs, `round` does correct-rounding (in the IEEE 754 sense) in all cases. I was pointing out that your `my_round` does not solve the problem you think it does: presumably you'd consider the "correct" result for `round(4.395, 2)` to be `4.4`, so you'd like `my_round(4.395, 2)` to return `4.4`?

> Do you have any suggestion to solve this problem?

The "problem" presumably being that two-argument `round` gives surprising results, to those who haven't thought about the implications of binary floating-point?

Unfortunately, there's no easy solution here. Discouraging use of two-argument round, and perhaps eventually deprecating it, is one possibility; there are few really good use-cases for two-argument round - most of the common use-cases involve rounding for reporting purposes, and that's better handled by string formatting. Keeping two-argument round and documenting the issue is another, and that's the solution the Python core devs have chosen. A more drastic solution would be to have Python's numeric literals use decimal floating-point by default instead of binary floating-point, so that we finally have a What You See Is What You Get behaviour for floating-point. That would be a huge change with many implications, and it's definitely way out of scope for this issue.

If you want to discuss this further, please feel free to do so, but not here: this isn't the right forum for that discussion.
msg358542 - (view) Author: AVicennA (AVicennA) * Date: 2019-12-17 11:39
@mark.dickinson:

You asked some questions, then, I also asked you about solving this problem. It seems to me you just love to talk.. I just answered to your questions. If "this isn't the right forum for that discussion" as you mean, it concerns you too. When you ask it's right, but when I answer it's wrong place to discuss ?! I can change this line of my post - "In this case, whatever the range the full right result is a return" to " In this case, sometimes right result can be return like a round". round function also not work properly, my_round too. Each of them must be used in its place. In the end I wanna say that, if you want to discuss this further,  please, we can continue conversation in mailing list or where you want. Thanks.
History
Date User Action Args
2019-12-17 14:17:04mark.dickinsonsetnosy: - mark.dickinson
2019-12-17 11:39:46AVicennAsetmessages: + msg358542
2019-12-17 01:00:35koobssetnosy: - koobs
2019-12-16 19:38:00mark.dickinsonsetmessages: + msg358516
2019-12-16 19:07:57steve.dowersetnosy: - steve.dower
2019-12-16 19:04:09AVicennAsetmessages: + msg358514
2019-12-16 18:46:59terry.reedysetmessages: + msg358511
components: - Documentation, IDLE, Library (Lib), macOS, Tests, Windows, FreeBSD
versions: - Python 3.6
2019-12-16 11:37:52AVicennAsetmessages: + msg358473
2019-12-16 10:12:03mark.dickinsonsetstatus: open -> closed
resolution: not a bug
stage: resolved
2019-12-16 10:11:16mark.dickinsonsetnosy: + mark.dickinson
messages: + msg358471
2019-12-16 10:02:03steven.dapranosetnosy: + steven.daprano
messages: + msg358470
2019-12-16 08:50:51AVicennAcreate