Created on 2018-02-26 15:23 by MJH, last changed 2018-09-28 07:16 by njs. This issue is now closed.
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.
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.
- 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).
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".
I can submit a PR for at least updating the docs about this (as per my previous comment) if that would be welcomed.
> round-to-even at all (as a default)?
> and whether it would therefore be better exposed as an additional function (e.g. "round_to_even" or "round_unbiased"
Changing the default behaviour of `round` again seems highly undesirable to me; I'd be strongly opposed to such a change. Proposals for backwards-compatible changes that make the round-ties-away-from-zero function available again under a different name (or via a keyword argument to `round`) could be worth discussing. And suggestions/PRs for documentation clarification are welcome, of course.
Thanks, Mark. Yes, I saw where Tim said round-half-even should be the default, but I didn't see any proposal to expose it as e.g. math.round_half_even() instead, nor a more complete look at what other languages do. That, along with the subject being 2.6 and not 3, made me think this change in Python 3 must have been discussed more fully elsewhere. Was it not?
3. link to where else this change in Python 3 was discussed more, if anywhere, or else confirm this change was made based on no additional analysis that we can find written down.
It'd also be interesting to hear if this is something we wish we'd done differently now, but that shouldn't distract from 1, 2, and 3.
I spent a few minutes with git blame/checkout/show and so far have found https://bugs.python.org/issue1869 (via https://github.com/python/cpython/commit/e6a076d). Still reading -- looks like there were a number of different changes made to round() at the same time for various reasons -- so maybe changing from round_half_up to round_half_even was necessary for the other improvements, and it couldn't have been exposed as a separate function? Or maybe that was just never proposed?
Sounds reasonable to me; I'm definitely open to documentation improvements. Though it doesn't seem all that buried to me: the round-ties-to-even behaviour is described in the third sentence in the first place I'd look for round documentation (https://docs.python.org/3/library/functions.html#round). It would be misleading to move the information earlier, because the use of round-ties-to-even is specific to the builtin types: user-defined types can do whatever they like via the __round__ magic method.
Sure, a link to a source on bankers rounding could work.
> 3. link to where else this change in Python 3 was discussed more, if anywhere, or else confirm this change was made based on no additional analysis that we can find written down.
I'm not aware of much discussion beyond the thread that Serhiy already pointed to. There's a little bit more (but not much) on rounding the py3k mailing list (try a Google search for "site:mail.python.org/pipermail/python-3000 rounding").
> It'd also be interesting to hear if this is something we wish we'd done differently now, but that shouldn't distract from 1, 2, and 3.
I can't speak for anyone else, but it's certainly not something I think should have been done differently, with one caveat: the silent and subtle change in behaviour from Python 2 to Python 3 was a bit unpleasant, and a possible source of late-discovered (or undiscovered) bugs.
No. The change was independent of other fixes and changes. There _is_ quite a history of round changes: fixes for the single-argument round function in odd corner cases (earlier versions of Python used the simple add-half-and-chop algorithm, with gives the wrong answer for 0.4999999999999999 and for 4503599627370497.0 thanks to FPU-level rounding in the add-half step); making two-argument round correctly-rounded in all cases in Python 2.7 and 3.1 via the same dtoa.c machinery used for str<->float conversions; changing the return type of single-argument round in Python 3; making round generic via the __round__ magic method, etc. But none of these required the change in rounding mode.
2. Conversion of source-code decimal numeric literals (e.g., in "bad_pi = 3.14") to the _nearest_ exactly representable binary float/double; the notion of _nearest_ needs some way to break ties.
Case 2 is really a special case of 1. Though not (usually) FPU-supported: you can think of conversion from decimal string to binary floating-point as another primitive floating-point operation, and it's one that's covered by IEEE 754; round-ties-to-even (or at least, some precision- or algorithm-limited _approximation_ to round-ties-to-even) is again a common default across languages and operating systems.
Case 3 is also covered by IEEE 754, and I believe that "most" languages use round-ties-to-even here, too. C's fprintf (for example) specifies that e-style, f-style, and g-style formatting should be "correctly rounded" (C99 7.19.6.1p13), where "correctly rounded" means "[...] nearest in value, subject to the current rounding mode [...]" (C99 3.9); in practice, that's usually round-ties-to-even. Java's DecimalFormat uses round-ties-to-even by default (source:  https://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html). I haven't checked other languages, but I expect that many of them do something similar.
Like I said, I'm not totally convinced about the numerical benefits of round-ties-to-even for user-level round-to-n-decimal-places operations as opposed to FPU-level rounding (though I'm open to persuasion). That's partly because round-to-two-decimal-places (for example) is actually quite a peculiar operation to be doing on a binary float in the first place, and in practice ties don't really appear or affect the behaviour that often. (It might *look* as though you have a value "2.675" in your dataframe, but on a typical machine that value is actually being stored as "2.67499999999999982236431605997495353221893310546875", so it doesn't matter one whit whether you're using round-ties-to-even or round-ties-to-away: under correct rounding, both are going to give you the surprising result of 2.67 when you round to two decimal places).
What I really like about Python's choice is the consistency. In Python, since Python 3, all five cases of rounding described above use round-ties-to-even. In Python 2, float formatting used round-ties-to-even (most of the time in practice, though for Python 2.6 and earlier the exact behaviour depended on the system), while "round" used round-ties-to-away for a very closely-related operation, and there are bug reports and StackOverflow questions from users surprised by the discrepancy between float formatting and two-argument round. In Python 3, we have the pleasant situation that "round" and string formatting agree.
> the silent and subtle change in behaviour from Python 2 to Python 3 was a bit unpleasant, and a possible source of late-discovered (or undiscovered) bugs.
This change was advertised in the "What’s New In Python 3.0" document.
> This change was advertised in the "What’s New In Python 3.0" document.
In any case, I wasn't referring to documentation. I meant "silent" in the runtime-sense: there was no run-time error or warning associated with the behaviour change (in contrast to the `print` function change, for example). Few people read the "what's new" documents, and someone who's written Python 2 code using round has no particular reason to suspect a behaviour change, and no visible indication that such a change has occurred, when porting their code to Python 3. Nevertheless, their numerical results may be slightly different as a result of the porting. Hence the potential for late-discovered bugs.