This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: allow other rounding modes in round()
Type: enhancement Stage:
Components: Interpreter Core Versions: Python 3.3
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: AaronR, ArneBab, mark.dickinson, r.david.murray, skrah, vstinner
Priority: normal Keywords:

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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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 msg145140 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:22adminsetgithub: 57269
2012-05-24 14:30:47ArneBabsetmessages: + msg161510
2011-10-08 08:14:15mark.dickinsonsetmessages: + msg145163
2011-10-07 20:22:45AaronRsetnosy: + AaronR
messages: + msg145140
2011-09-29 19:18:01ArneBabsetmessages: + msg144617
2011-09-29 18:04:50vstinnersetnosy: + vstinner
2011-09-29 17:14:18skrahsetmessages: + msg144602
2011-09-29 16:11:20mark.dickinsonsetmessages: + msg144598
2011-09-29 15:57:03ArneBabsetmessages: + msg144597
title: make round() floating-point errors less hurtful -> allow other rounding modes in round()
2011-09-29 15:44:56mark.dickinsonsetmessages: + msg144596
2011-09-29 15:40:15mark.dickinsonsettype: enhancement
versions: + Python 3.3, - Python 3.2
2011-09-29 15:35:55mark.dickinsonsetmessages: + msg144595
2011-09-29 15:31:28r.david.murraysetnosy: + skrah
messages: + msg144594
2011-09-29 15:25:31ArneBabsetmessages: + msg144593
2011-09-29 15:20:07ArneBabsetmessages: + msg144592
2011-09-29 15:19:40r.david.murraysetnosy: + mark.dickinson, r.david.murray
messages: + msg144591
2011-09-29 15:16:49ArneBabcreate