Change in Python 3's "round" behavior
I recently found out about Python 3's round-to-even change (via https://github.com/cosmologicon/pywat!) and am having trouble finding where that change was discussed. I did find the revealingly-invalid bug report https://bugs.python.org/issue32956 ("python 3 round bug"), so I asked there, but wanted to invite anyone else on this list who might be interested to help. If interested, please see the comments there (copy/pasted below for convenience), and +nosy or comment on that issue. Thanks! Joshua Bronson added the comment: This was so surprising to me that I had to check some other languages that I had handy. It turns out that not one of JavaScript, Ruby, Perl, C++, Java, Go, or Rust agrees with Python. In fact they all agreed with one another that 2.5 should round to 3. Examples below. I understand from https://github.com/cosmologicon/pywat/pull/40#discussion_r219962259 that "to always round up... can theoretically skew the data" but it's not clear why that's a good enough reason to differ from the "round" function in all these other languages (as opposed to e.g. offering this alternative behavior in some additional "round_unskewed" function). I assume the rationale for having Python 3's "round" differ from that of so many other languages was written down when this decision was made, but I searched and couldn't find it. Could anyone link to it in a comment here? And would it be worth including rationale and a larger callout in the https://docs.python.org/3/library/functions.html#round docs? The documentation of this behavior is a bit buried among other things, and the rationale for it is missing entirely. $ node -e 'console.log(Math.round(2.5))' 3 $ ruby -e 'puts (2.5).round()' 3 $ perl -e 'use Math::Round; print round(2.5)' 3 $ cat test_round.cpp #include <stdio.h> #include <math.h> int main(void) { printf("%f\n", round(2.5)); } $ g++ test_round.cpp && ./a.out 3.000000 $ cat TestRound.java class TestRound { public static void main(String[] args) { System.out.println(Math.round(2.5)); } } $ javac TestRound.java && java TestRound 3 $ cat test_round.go package main import "fmt" import "math" func main() { fmt.Println(math.Round(2.5)) } $ go build test_round.go && ./test_round 3 $ cat test_round.rs fn main() { println!("{}", (2.5_f64).round()); } $ rustc test_round.rs && ./test_round 3 Serhiy Storchaka added the comment: See the discussion on the Python-Dev mailing list: https://mail.python.org/pipermail/python-dev/2008-January/075863.html. For C look at the rint() function. It is a common knowledge that rounding half-to-even is what users want in most cases, but it is a tiny bit more expensive in C. In Python the additional cost of such rounding is insignificant. Joshua Bronson added the comment: Thanks Serhiy, I read the Python-Dev thread you linked to, but that doesn't resolve the issues: - Its topic is Python 2.6 (where this behavior does not occur) rather than Python 3 (where it does). - A few messages into the thread Guido does address Python 3, but in fact says "I think the consensus is against round-to-even in 3.0" (see https://mail.python.org/pipermail/python-dev/2008-January/075897.html). - There is no discussion of the fact that this behavior differs from the function named "round" in all the other programming languages I mentioned, and whether it would therefore be better exposed as an additional function (e.g. "round_to_even" or "round_unbiased", and in the math or statistics package rather than builtins). Surprisingly, Excel is the only other programming environment I saw discussed in the thread. (And round(2.5) == 3 there.) So that all suggests there must be some other thread or issue where this change for Python 3 have been discussed, but I looked again and could not find it. The C "rint" example you gave just seems to prove the point that this behavior should have a distinct name from "round". Regarding:
It is a common knowledge that rounding half-to-even is what users want in most cases
I don't think that's common knowledge; seems like citation needed? Based on all the other languages where this differs (not to mention Python 2), it's not clear users would want Python 3 to be the only different one. And this is definitely a surprise for the majority of programmers, whose experience with "round" is how it works everywhere else. (This is making it into pywat after all: https://github.com/cosmologicon/pywat/pull/40) I can submit a PR for at least updating the docs about this (as per my previous comment) if that would be welcomed.
On Wed, Sep 26, 2018 at 07:26:17AM -0400, jab@math.brown.edu wrote:
I did find the revealingly-invalid bug report https://bugs.python.org/issue32956 ("python 3 round bug"), so I asked there, but wanted to invite anyone else on this list who might be interested to help.
What about those of us who are interested in hindering? But seriously, even if round-to-even was a mistake, Python 3.x has used it for seven releases now, about a decade. Backwards compatibility means we cannot just change it. By now, there are people relying on this behaviour. Changing it would need to go through a deprecation cycle, which probably means one release with a silent warning, a second release with warning enabled, and not until 3.10 would the default change. That's a lot of inconvenience just for the sake of almost-but-not-quite matching the behaviour of some other programming languages, while breaking compatibility with others: julia> round(2.5) 2.0 julia> round(3.5) 4.0 In any case, I would oppose any proposal to revert this change. Round- to-even ("banker's rounding") is generally mathematically better, and its been said (half in jest) that if you're not using it, you're probably up to shenanigans :-) For users who don't specifically care about the rounding mode, round-to- even generally makes the safest default, even if it is surprising to those used to the naive technique taught in primary schools. For those who care about compatibility with some other language, well, there are a lot of languages and we can't match them *all* by default: # Javascript js> Math.round(-2.5) -2 # Ruby irb(main):001:0> (-2.5).round() => -3 so you probably need your own custom round function. On the other hand, I wouldn't object out of hand to a feature request to support the same eight rounding modes as the decimal module. But as always, the Devil is in the details. -- Steve
jab@math.brown.edu wrote:
I understand from https://github.com/cosmologicon/pywat/pull/40#discussion_r219962259 that "to always round up... can theoretically skew the data"
*Very* theoretically. If the number is even a whisker bigger than 2.5 it's going to get rounded up regardless:
round(2.500000000000001) 3
That difference is on the order of the error you expect from representing decimal fractions in binary, so I would be surprised if anyone can actually measure this bias in a real application.
It is a common knowledge that rounding half-to-even is what users want in most cases
I don't think that's common knowledge; seems like citation needed?
It's not common enough for me to have heard of it before. (BTW, how do you provide a citation for "common knowledge"?-) -- Greg
On Thu, Sep 27, 2018 at 05:55:07PM +1200, Greg Ewing wrote:
jab@math.brown.edu wrote:
I understand from https://github.com/cosmologicon/pywat/pull/40#discussion_r219962259 that "to always round up... can theoretically skew the data"
*Very* theoretically. If the number is even a whisker bigger than 2.5 it's going to get rounded up regardless:
round(2.500000000000001) 3
That difference is on the order of the error you expect from representing decimal fractions in binary, so I would be surprised if anyone can actually measure this bias in a real application.
I think you may have misunderstood the nature of the bias. It's not about individual roundings and it definitely has nothing to do with binary representation. Any one round operation will introduce a bias. You had a number, say 2.3, and it gets rounded down to 2.0, introducing an error of -0.3. But if you have lots of rounds, some will round up, and some will round down, and we want the rounding errors to cancel. The errors *almost* cancel using the naive rounding algorithm as most of the digits pair up: .1 rounds down, error = -0.1 .9 rounds up, error = +0.1 .2 rounds down, error = -0.2 .8 rounds up, error = +0.2 etc. If each digit is equally likely, then on average they'll cancel and we're left with *almost* no overall error. The problem is that while there are four digits rounding down (.1 through .4) there are FIVE which round up (.5 through .9). Two digits don't pair up: .0 stays unchanged, error = 0 .5 always rounds up, error = +0.5 Given that for many purposes, our data is recorded only to a fixed number of decimal places, we're dealing with numbers like 0.5 rather than 0.5000000001, so this can become a real issue. Every ten rounding operations will introduce an average error of +0.05 instead of cancelling out. Rounding introduces a small but real bias. The most common (and, in many experts' opinion, the best default behaviour) is Banker's Rounding, or round-to-even. All the other digits round as per the usual rule, but .5 rounds UP half the time and DOWN the rest of the time: 0.5, 2.5, 3.5 etc round down, error = -0.5 1.5, 3.5, 5.5 etc round up, error = +0.5 thus on average the .5 digit introduces no error and the bias goes away. -- Steve
-----Original Message----- From: Python-Dev <python-dev-bounces+tritium- list=sdamon.com@python.org> On Behalf Of Steven D'Aprano Sent: Thursday, September 27, 2018 9:54 AM To: python-dev@python.org Subject: Re: [Python-Dev] Change in Python 3's "round" behavior
On Thu, Sep 27, 2018 at 05:55:07PM +1200, Greg Ewing wrote:
jab@math.brown.edu wrote:
I understand from https://github.com/cosmologicon/pywat/pull/40#discussion_r219962259 that "to always round up... can theoretically skew the data"
*Very* theoretically. If the number is even a whisker bigger than 2.5 it's going to get rounded up regardless:
round(2.500000000000001) 3
That difference is on the order of the error you expect from representing decimal fractions in binary, so I would be surprised if anyone can actually measure this bias in a real application.
I think you may have misunderstood the nature of the bias. It's not about individual roundings and it definitely has nothing to do with binary representation.
Any one round operation will introduce a bias. You had a number, say 2.3, and it gets rounded down to 2.0, introducing an error of -0.3. But if you have lots of rounds, some will round up, and some will round down, and we want the rounding errors to cancel.
The errors *almost* cancel using the naive rounding algorithm as most of the digits pair up:
.1 rounds down, error = -0.1 .9 rounds up, error = +0.1
.2 rounds down, error = -0.2 .8 rounds up, error = +0.2
etc. If each digit is equally likely, then on average they'll cancel and we're left with *almost* no overall error.
The problem is that while there are four digits rounding down (.1 through .4) there are FIVE which round up (.5 through .9). Two digits don't pair up:
.0 stays unchanged, error = 0 .5 always rounds up, error = +0.5
Given that for many purposes, our data is recorded only to a fixed number of decimal places, we're dealing with numbers like 0.5 rather than 0.5000000001, so this can become a real issue. Every ten rounding operations will introduce an average error of +0.05 instead of cancelling out. Rounding introduces a small but real bias.
The most common (and, in many experts' opinion, the best default behaviour) is Banker's Rounding, or round-to-even. All the other digits round as per the usual rule, but .5 rounds UP half the time and DOWN the rest of the time:
0.5, 2.5, 3.5 etc round down, error = -0.5 1.5, 3.5, 5.5 etc round up, error = +0.5
thus on average the .5 digit introduces no error and the bias goes away.
...and we have a stats module that would be a great place for a round function that needs to cancel rounding errors. The simple case should be the intuitive case for most users. My experience and that of many users of the python irc channel on freenode is that round-half-to-even is not the intuitive, or even desired, behavior - round-half-up is. This wouldn't be frustrating to the human user if the round built-in let you pick the method, instead you have to use the very complicated decimal module with a modified context to get intuitive behavior. I could live with `round(2.5) -> 2.0` if `round(2.5, method='up') -> 3.0` (or some similar spelling) was an option. As it stands, this is a wart on the language. "Statistically balanced rounding" is a special case, not the default case.
-- Steve _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/tritium- list%40sdamon.com
I don't really get the statistical argument. If you're doing something like calculating an average and care about accuracy, why are you rounding the values before averaging? Why not average first and then round the result if you need to? -- Greg
-----Original Message----- From: Python-Dev <python-dev-bounces+tritium- list=sdamon.com@python.org> On Behalf Of Greg Ewing Sent: Saturday, September 29, 2018 9:50 PM To: python-dev@python.org Subject: Re: [Python-Dev] Change in Python 3's "round" behavior
I don't really get the statistical argument. If you're doing something like calculating an average and care about accuracy, why are you rounding the values before averaging? Why not average first and then round the result if you need to?
Other use case is finance, where you can end up with interest calculations that are fractional of the base unit of currency. US$2.345 is impossible to represent in real currency, so it has to be rounded. With half-towards-even, that rounds to $2.34, and $2.355 rounds to $2.36. It evens out in the long run. While that is very helpful for finance calculations, if you are doing finance with that level of precision, you should be using decimal instead of float anyways and decimal's round has configurable round method.
-- Greg
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/tritium- list%40sdamon.com
Alex Walters wrote:
Other use case is finance, where you can end up with interest calculations that are fractional of the base unit of currency. US$2.345 is impossible to represent in real currency, so it has to be rounded.
This brings us back to my original point about floating point accuracy. If you do your interest calculation in floating point binary, first it's very unlikely that it will come out ending in exactly 0.5 of a cent, and secondly if you care about the details that much, you should be calculating in decimal, and being explicit about exactly what kind of rounding you're doing. -- Greg
On Mon, Oct 01, 2018 at 10:50:36AM +1300, Greg Ewing wrote:
Alex Walters wrote:
Other use case is finance, where you can end up with interest calculations that are fractional of the base unit of currency. US$2.345 is impossible to represent in real currency, so it has to be rounded.
This brings us back to my original point about floating point accuracy. If you do your interest calculation in floating point binary, first it's very unlikely that it will come out ending in exactly 0.5 of a cent,
And yet people (Alex, and he says others) are complaining about this change in behaviour. If getting exactly 0.5 is as unlikely as you claim, how would they notice?
and secondly if you care about the details that much, you should be calculating in decimal, and being explicit about exactly what kind of rounding you're doing.
Why should people using float have a biased round just because "they should be using Decimal"? The choice to use Decimal is not up to us and there's nothing wrong with using float for many purposes. Those who do shouldn't be burdened with a biased round. Regardless of whether it meets with the approval of the mathematically naive who think that primary school rounding is the "intuitive" (or only) way to round, the change was made something like a decade ago. It matches the behaviour of Julia, .Net, VBScript and I expect other languages and makes for a technically better default rounding mode. With no overwhelmingly strong case for reverting to a biased rounding mode, I think this discussion is dead. If people want to discuss something more productive, we could talk about adding an optional argument to round() to take a rounding mode, or adding an equivalent to the math library. I'll start off... How about we move the rounding mode constants out of the decimal module and into the math module? That makes them more easily discoverable and importable (the math module is lightweight, the decimal module is not). The decimal module would then import the constants from math (it already imports math so that's no extra dependency). Then we can add a keyword only argument to round: round(number, ndigits=0, *, mode=ROUND_HALF_EVEN) To use it, you can import the rounding mode you want from math: from math import ROUND_CEILING round(x, 3, mode=ROUND_CEILING) and everyone is happy (he says optimistically). It's a bit funny to have constants in the math module not actually used there, for the benefit of a builtin and Decimal, but I prefer that to either importing them from decimal or making them builtins. Thoughts? -- Steve
On Mon, Oct 1, 2018 at 9:36 AM Steven D'Aprano <steve@pearwood.info> wrote:
Then we can add a keyword only argument to round:
round(number, ndigits=0, *, mode=ROUND_HALF_EVEN)
To use it, you can import the rounding mode you want from math:
from math import ROUND_CEILING round(x, 3, mode=ROUND_CEILING)
I have no problem with this.
and everyone is happy (he says optimistically).
And I am as dubious as you are about this :) IMO, the biggest problem with round() is that it's TOO discoverable. People reach for it when what they really should be using is string formatting ("I want to display all these values to three decimal places"), and then sometimes get bitten when something doesn't actually display the way they think it will. When it's used correctly, it's usually fine. ChrisA
On Sat, Sep 29, 2018 at 09:40:03PM -0400, Alex Walters wrote:
...and we have a stats module that would be a great place for a round function that needs to cancel rounding errors.
This has nothing to do with statistics. You should consider that this is often called "Banker's Rounding" and what that tells you. (It's also called Dutch Rounding.)
The simple case should be the intuitive case for most users.
Should it? I think that having the most correct behaviour should be the default. Who decides what is "intuitive"? I asked my three year old nephew whether 1.5 should round to down to 1 or up to 2, and he said that he didn't care about numbers because he was sailing across the ocean and I was standing in the way of his boat.
My experience and that of many users of the python irc channel on freenode is that round-half-to-even is not the intuitive, or even desired, behavior - round-half-up is.
It would be very informative to ask *why* they want round-half-up. I expect that the reason given will boil down to "because it is the rounding method I learned in school" even if they can't articulate it that way, and start going on about it being "intuitive" as if rounding ties upwards was more intuitive than rounding ties downward. Compatibility with "other languages" isn't the answer, because other languages differ in how they do rounding and we can't match them all: # Javascript js> Math.round(2.5) + Math.round(-2.5) 1 # Ruby steve@orac ~ $ ruby -e 'puts (2.5).round() + (-2.5).round()' 0 VBScript is another language which uses Bankers Rounding: https://blogs.msdn.microsoft.com/ericlippert/2003/09/26/bankers-rounding/ although the example given (calculating an average) is misleading, because as I said this is not about statistics. Bankers Rounding produces better *averages* because it produces better *sums* (to quote one of the comments). Similarly for differences. If you perform many subtractions (let's say you are paying off a loan, and calculating interest, then rounding to the nearest cent) you have to care about bias. If each rounding introduces a 0.5 cent bias (as round-half-up does) then the total bias increases as the number of transactions increases.
This wouldn't be frustrating to the human user
Did you intend to imply I'm not human, or was it an accident? -- Steve
On Sun, Sep 30, 2018 at 10:18 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Sat, Sep 29, 2018 at 09:40:03PM -0400, Alex Walters wrote:
My experience and that of many users of the python irc channel on freenode is that round-half-to-even is not the intuitive, or even desired, behavior - round-half-up is.
It would be very informative to ask *why* they want round-half-up.
I expect that the reason given will boil down to "because it is the rounding method I learned in school" even if they can't articulate it that way, and start going on about it being "intuitive" as if rounding ties upwards was more intuitive than rounding ties downward.
Let's start by assuming that real numbers are a perfectly continuous space of values, and that every actually-recorded value is *already* the result of rounding some number to fit within our available space (rather than assuming that recorded values are perfectly precise and correct). Further, assume that representable numbers are equally spaced - not strictly true, but extremely hard to compensate for. That means that any representable number actually has to indicate a range of values centered on that value. For the sake of argument, pretend we can represent one digit before the decimal and one after; in actual usage, this would occur at the extreme of precision, 53 bits down the line. So the number 2.0 actually means the range (1.95, 2.05), the number 2.1 really means (2.05, 2.15), 2.5 means (2.45, 2.55), 2.9 means (2.85, 2.95), 3.0 means (2.95, 3.05). Now we take our values and round them to integer. If we round all 0.5 values up, that means that the rounded value 2 will now catch all values in the range (1.45, 2.45), and the rounded value 3 catches (2.45, 3.45). In effect, our values are being skewed low by half a ULP. By using "round to even", you make the rounded value 2 catch all values in the range (1.45, 2.55), and the rounded value 3 now catches (2.55, 3.45). Values are now evenly spread around the stated value, but there is an entire ULP of discrepancy between the span of even numbers and the span of odd numbers. Which is more important? For a number's effective range to be centered around it, or for its range to be the same size as the range of every other number? ChrisA
Chris Angelico wrote:
]That means that any representable number actually has to indicate a range of values centered on that value.
That's not always true -- it depends on the source of the information. For example, a reading of 5 seconds on a clock with 1 second resolution actually represents a value between 5 and 6 seconds. So if you're fussy about rounding, you might want to round clock readings differently from measurements on a ruler. -- Greg
On Mon, Oct 1, 2018 at 8:17 AM Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Chris Angelico wrote:
]That means that any representable number actually has to indicate a range of values centered on that value.
That's not always true -- it depends on the source of the information. For example, a reading of 5 seconds on a clock with 1 second resolution actually represents a value between 5 and 6 seconds.
So if you're fussy about rounding, you might want to round clock readings differently from measurements on a ruler.
True. I gave a number of assumptions, and if those assumptions don't hold, you may need to vary things. If you have something like you describe here, you probably want to round-to-zero or something. ChrisA
On 9/30/18 6:15 PM, Greg Ewing wrote:
Chris Angelico wrote:
]That means that any representable number actually has to indicate a range of values centered on that value.
That's not always true -- it depends on the source of the information. For example, a reading of 5 seconds on a clock with 1 second resolution actually represents a value between 5 and 6 seconds.
So if you're fussy about rounding, you might want to round clock readings differently from measurements on a ruler.
Actually it could be from 4+ to 6- seconds, say the first reading is 1, that could be anything from 1.000 to 1.999 and the second reading be 6, that could be from 6.000 to 6.999, thus the interval be from 6.000 - 1.999 = 4.001 tp 6.999 - 1.000 = 5.999 seconds. Now if you waited for the start time to roll over so you knew you were near 1.000, that would be different, but from just sampling you get ranges. Now if it was a stop watch that started at the beginning it depends on how it presents the time, it might respond 5 for 5.000 to 5.999 seconds, or it might intentionally round the data and say 5 from about 4.5 to 5.5. Now, one case where there is an intentional bias to the bottom is Map Grid Coordinate system, where you specify 1 meter resolution within a grid with 5 digits, but if you want to specify to less precision, the specification it to ALWAYS truncate so map coordinate 1234 represent the range from 12340.0000 to 12349.9999 -- Richard Damon
Steven D'Aprano wrote:
(It's also called Dutch Rounding.)
Oh, so *that's* why Python does it! Fair enough. :-)
Similarly for differences. If you perform many subtractions (let's say you are paying off a loan, and calculating interest, then rounding to the nearest cent) you have to care about bias.
If I'm paying off a loan, it's what the bank calculates that matters, not what I calculate. And I hope the bank isn't relying on the vagaries of Python floating point arithmetic for its critical financial calculations. -- Greg
On 9/30/2018 2:17 PM, Steven D'Aprano wrote:
(It's also called Dutch Rounding.)
Ah - as to why - and from school! (as so-called intuitive! rather desired!). A test score goes from 5.5 to 6.0 - which becomes passing. Oh, do I recall my children's frustrations when they had a X.4Y score - that became X.0. Tears!
On 01/10/18 21:45, Michael Felt wrote:
On 9/30/2018 2:17 PM, Steven D'Aprano wrote:
(It's also called Dutch Rounding.)
Ah - as to why - and from school! (as so-called intuitive! rather desired!).
A test score goes from 5.5 to 6.0 - which becomes passing.
Oh, do I recall my children's frustrations when they had a X.4Y score - that became X.0. Tears!
Please do not reply to any message from Steven D'Aprano as you are also likely to get banned by the incompetent moderators. -- My fellow Pythonistas, ask not what our language can do for you, ask what you can do for our language. Mark Lawrence
On Wed, Sep 26, 2018 at 7:29 AM <jab@math.brown.edu> wrote:
I recently found out about Python 3's round-to-even change (via https://github.com/cosmologicon/pywat!) and am having trouble finding where that change was discussed.
That GitHub project is hilarious especially the NaN stuff... Rounding is from engineering so there is more than one definition, and one is not more correct than the others, it just depends on the specific application. Functions like ceiling and floor do have mathematical definitions. Whichever definition of rounding the Python standard library adopts, it should be very explicitly defined in the documentation in terms of ceiling and floor. In applications where rounding is actually important, it's a good idea to do calculations with one rounding function, and again with another, and compare results.
On 9/26/2018 7:26 AM, jab@math.brown.edu wrote: To paraphrase: 1. Where was the 3.0 change discussed? 2. What was the rationale? I think these have been answered as well as possible. 3. Can the change be reverted? It 'could be', but will not be reverted? 4. Should something be added to the doc? Maybe, but I don't see any enthusiasm from core devs. This list is for development of future Python and CPython. The continued discussion of what other languages do and how to best use rounding are off-topic here (and given the above, on python-ideas). Please take these comparison and use discussions to python-list. -- Terry Jan Reedy
participants (11)
-
Alex Walters
-
Chris Angelico
-
Greg Ewing
-
jab@math.brown.edu
-
Mansour Moufid
-
Mark Lawrence
-
Michael Felt
-
Richard Damon
-
Stephen J. Turnbull
-
Steven D'Aprano
-
Terry Reedy