Issue 13060: allow other rounding modes in round() (original) (raw)
Issue13060
Created on 2011-09-29 15:16 by ArneBab, last changed 2022-04-11 14:57 by admin.
Messages (14) | ||
---|---|---|
msg144590 - (view) | Author: Arne Babenhauserheide (ArneBab) * | Date: 2011-09-29 15:16 |
Hi, I just stumbled over round() errors. I read the FAQ¹, and even though the FAQ states that this is no bug, its implementation is less than ideal. To illustrate: >>> round(0.5, 1) 0.0 and >>> round(5, -1) 0 This is mathematically wrong and hits for comparisions which humans put in. As alternate I use the following hack myself where ever I have to round numbers: def roundexact(a, *args): ... return round(a+0.000000000000001*max(1.0, (2*a)//10), *args) This has correct behavior for *5 on my hardware: >>> roundexact(0.5, 0) 1.0 Its errors only appear in corner cases: >>> roundexact(0.4999999999999999, 0) 0.0 >>> roundexact(0.49999999999999998, 0) 1.0 This implementation shields me from implementation details of my hardware in all but very few extreme cases, while the current implementation of round() exhibits the hardware-imposed bugs in cases which actually matter to humans. Maybe round could get a keyword argument roundup5 or such, which exhibits the behavior that any *5 number is rounded up. Note: The decimal module is no alternative, because it is more than factor 100 slower than floats (at least for simple computations): >>> from timeit import timeit >>> timeit("float(1.0)+float(0.1)") 0.30365920066833496 >>> timeit("Decimal(1.0)+Decimal(0.1)", setup="from decimal import Decimal, getcontext; getcontext().prec=17") 49.96972298622131 ¹: http://docs.python.org/library/functions.html?highlight=round#round | ||
msg144591 - (view) | Author: R. David Murray (r.david.murray) * ![]() |
Date: 2011-09-29 15:19 |
Note that a C accelerator for Decimal is in the works. | ||
msg144592 - (view) | Author: Arne Babenhauserheide (ArneBab) * | Date: 2011-09-29 15:20 |
Better comparision of decimal and float: >>> timeit("a+a", setup="from decimal import Decimal, getcontext; getcontext().prec=17; a = Decimal(1.0)") 21.125790119171143 >>> timeit("a+a", setup="a = float(1.0)") 0.05324697494506836 | ||
msg144593 - (view) | Author: Arne Babenhauserheide (ArneBab) * | Date: 2011-09-29 15:25 |
If the C accelerator for decimal gets decimal performance close to floats (which I doubt, because it has to do much more), it could be very useful for me. What is its estimated time to completion? Even when it is finished, it does not change the problem that round(0.5, 0) gives mathematically wrong results, so python requires hacks to be correct in this very simple case - instead of being right for most of the simple cases and only get it wrong in corner cases. My hack isn’t really clean, though, because it depends on the internal representation of floats in the hardware. A real fix would have to get the real size of floats and adjust the added value accordingly. | ||
msg144594 - (view) | Author: R. David Murray (r.david.murray) * ![]() |
Date: 2011-09-29 15:31 |
It seems to me that saying "floating point" and "mathematically correct" in the same breath is...optimistic :) But that's why I added Mark to nosy, he knows far more about this stuff than I do. As far as I know the accelerator is feature complete at this point. I think the goal is to ship it with 3.3, but I'm not sure where we are at in the process of making that a reality. | ||
msg144595 - (view) | Author: Mark Dickinson (mark.dickinson) * ![]() |
Date: 2011-09-29 15:35 |
> This is mathematically wrong ... No, it's not 'mathematically wrong'. There are many different rounding conventions in use, and no single universally agreed convention for rounding halfway cases. Python chooses to use unbiased rounding[1] here, which matches the rounding used for all other basic arithmetic operations. Other comments: (1) I agree that round-half-up might be a useful convention to have available. But... (2) Depending on any sort of predictable rounding behaviour for *decimal* halfway cases when using *binary* floats is fraught with peril. If you really care about these halfway cases going in a particular direction (whether it's away from zero, towards +infinity, towards even, towards odd, etc.) then you should really be using Decimal. [1] http://en.wikipedia.org/wiki/Rounding#Round_half_to_even | ||
msg144596 - (view) | Author: Mark Dickinson (mark.dickinson) * ![]() |
Date: 2011-09-29 15:44 |
Classifying this as a feature request: the behaviour of round isn't going to change here, but there might be community support for adding a mechanism for round to allow other rounding modes. It might be worth taking this to the python-ideas mailing list to hash out what that mechanism should be (extra keyword to round, ...). | ||
msg144597 - (view) | Author: Arne Babenhauserheide (ArneBab) * | Date: 2011-09-29 15:57 |
I did not know about rounding to even, so maybe there should be a warning in the 2.7 documentation, that the behavior changed in python 3 (I just checked that: python2.7 is in line with the documentation). The first time I stumbled over these issues was when implementing a game, where I wanted to map pixels to hexfields, where I had to get the borderline cases right: https://bitbucket.org/ArneBab/hexbattle/src/38ad49c04836/hexgrid.py#cl-24 This likely won’t hit me in the game, but it really hurts in the doctests. PS: I like the naming as “allow other rounding modes”, so I changed the title of the bug. I hope that’s OK. PPS: Thank you for all your replies! The bugtracker feels really welcoming and helpful, even when reporting something as a bug, which is just a difference in goal definition. I hope it will turn out to be useful for the community! | ||
msg144598 - (view) | Author: Mark Dickinson (mark.dickinson) * ![]() |
Date: 2011-09-29 16:11 |
> maybe there should be a warning in the 2.7 documentation Well, such a warning really belongs in the Python 3 documentation rather than the Python 2 documentation. (Or at least, AFAIK that's the convention that's been followed to date: the Python 2 docs don't 'know' about Python 3 in general.) There's a note in the 'What's new in Python 3' documentation that covers this and other changes in round: http://docs.python.org/dev/whatsnew/3.0.html#builtins | ||
msg144602 - (view) | Author: Stefan Krah (skrah) * ![]() |
Date: 2011-09-29 17:14 |
> If the C accelerator for decimal gets decimal performance close to > floats (which I doubt, because it has to do much more), it could be > very useful for me. What is its estimated time to completion? It is finished and awaiting review (See #7652). The version in http://hg.python.org/features/cdecimal#py3k-cdecimal is the same as the version that will be released as cdecimal-2.3 in a couple of weeks. Benchmarks for cdecimal-2.2 are over here: http://www.bytereef.org/mpdecimal/benchmarks.html#pi-64-bit Typically cdecimal is 2-3 times slower than floats. With further aggressive optimizations one *might* get that down to 1.5-2 times for a fixed width Decimal64 type, but this is pure speculation at this point. If you look at http://www.bytereef.org/mpdecimal/benchmarks.html#mandelbrot-64-bit , you'll see that the Intel library performs very well for that specific type. Exact calculations are performed in binary, then converted to decimal for rounding. Note that this strategy _only_ works for relatively low precisions. | ||
msg144617 - (view) | Author: Arne Babenhauserheide (ArneBab) * | Date: 2011-09-29 19:18 |
cdecimal sounds great! when is it scheduled for inclusion? | ||
msg145140 - (view) | Author: Aaron Robson (AaronR) | Date: 2011-10-07 20:22 |
When i run into I have to bodge around it in ways like the below code. I've only ever used round half up, has anyone here even used Bankers Rounding by choice before? For reference here are the other options: http://en.wikipedia.org/wiki/Rounding#Tie-breaking def RoundHalfUp(number): '''http://en.wikipedia.org/wiki/Rounding#Round_half_up 0.5 and above round up else round down. ''' trunc = int(number) fractionalPart = number - trunc if fractionalPart < 0.5: return trunc else: ceil = trunc + 1 return ceil | ||
msg145163 - (view) | Author: Mark Dickinson (mark.dickinson) * ![]() |
Date: 2011-10-08 08:14 |
I'm warming to this idea. We already have several round-to-integer functions (but not round-to-an-arbitrary-number-of-decimal-places) available in the math module (under the names floor, ceil and trunc). This *does* seem to be a common need, and it's easy to get roll-your-own implementations wrong (e.g., check what the implementation in does for negative numbers). I suspect that once we get more people shifting to py3k we're going to get more complaints about round doing round-half-to-even. Rather than expanding the signature of round, it might be worth considering a new math-module function (with name to be determined) that does round-half-up for floats. We might later extend it to other types in the same way as is currently done for floor and ceil (with __floor__ and __ceil__ magic methods); introduction of such magic methods would probably require a PEP though. At issue: *which* round-half-up function do we want? The one that rounds halfway cases away from zero (what Wikipedia calls "Round half away from zero"), or the one that rounds halfway cases towards +infinity? I'm inclined towards the former. I don't think it's worth implementing both. I guess we should follow floor / ceil's lead of returning integer output for float input in the case where number of places to round to isn't given (though personally I would have been happier if floor / ceil had continued to return float output for float input, as in Python 2.x). | ||
msg161510 - (view) | Author: Arne Babenhauserheide (ArneBab) * | Date: 2012-05-24 14:30 |
I also think that rounding half away from zero would be the most obvious choice, as it does not introduce a bias for random numbers distributed around 0 while being close to what I would expect from school mathematics. The case of n*(random() - 0.5) which I assume as common, this should work well, which is not the case for rounding towards +infinity. |
History | |||
---|---|---|---|
Date | User | Action | Args |
2022-04-11 14:57:22 | admin | set | github: 57269 |
2012-05-24 14:30:47 | ArneBab | set | messages: + |
2011-10-08 08:14:15 | mark.dickinson | set | messages: + |
2011-10-07 20:22:45 | AaronR | set | nosy: + AaronRmessages: + |
2011-09-29 19🔞01 | ArneBab | set | messages: + |
2011-09-29 18:04:50 | vstinner | set | nosy: + vstinner |
2011-09-29 17:14:18 | skrah | set | messages: + |
2011-09-29 16:11:20 | mark.dickinson | set | messages: + |
2011-09-29 15:57:03 | ArneBab | set | messages: + title: make round() floating-point errors less hurtful -> allow other rounding modes in round() |
2011-09-29 15:44:56 | mark.dickinson | set | messages: + |
2011-09-29 15:40:15 | mark.dickinson | set | type: enhancementversions: + Python 3.3, - Python 3.2 |
2011-09-29 15:35:55 | mark.dickinson | set | messages: + |
2011-09-29 15:31:28 | r.david.murray | set | nosy: + skrahmessages: + |
2011-09-29 15:25:31 | ArneBab | set | messages: + |
2011-09-29 15:20:07 | ArneBab | set | messages: + |
2011-09-29 15:19:40 | r.david.murray | set | nosy: + mark.dickinson, r.david.murraymessages: + |
2011-09-29 15:16:49 | ArneBab | create |