Python 3 rounding problem

I started taking a 100-day course in learning python on Udemy and came across an interesting rounding bug or feature. I tried it on 3 different python platforms with the same results, but interesting enough python 2.7 on the pi doesn’t have this problem. Every other ascending half number rounds incorrectly. 0.5 rounds to 0, 1.5 rounds to 2, 2.5 rounds to 2. etc… Also has the same behaviour with negative numbers. Guess this is the way it is handling floating point numbers but seems like a bug to me. Here is a screenshot example. Python 3.11.1 on my windows machine has the same problem as shown with 3.7.3 version on the pi.

3 Likes

Python 3 uses a different rounding behaviour compared to Python 2: it now uses so-called “banker’s rounding” (Wikipedia): when the integer part is odd, the number is rounded away from zero; when the integer part is even, is it rounded towards zero.

The reason for this is to avoid a bias, when all values at .5 are rounded away from zero (and then e.g. summed).

This is the behaviour you are seeing, and it is in fact consistent. It’s perhaps just different than what you are used to.

Definitely unsettling, even if it makes sense on a statistical point of view
If you want to enforce the behaviour maybe you can test of the decimal part is higher than 5 (or “higher or equal” if you will), and use math.ceil() if it is, math.floor() otherwise

It appears there’s a “decimal” library that could fit, but I don’t write python on a regular basis so…

Decimal('2.675').quantize(Decimal('1.11'), rounding=ROUND_HALF_UP)
# output: 2.68

Decimal('2.5').quantize(Decimal('1.'), rounding=ROUND_HALF_DOWN)
# output: 2
2 Likes

Thanks for info on this. I had never heard of that method. After having that info, I also found this link on it. Guess if I had searched a little more carefully with google, I would have found this. Python 3.x rounding behavior - Stack Overflow

1 Like

Well the real problem with these is that you don’t even see it most of the time…
If this hadn’t been part of a course, I think any developer would oversee this particular behaviour, and this could cause serious issues thousands of lines of code later, very hard to track down…

Having the possibility to do “banker’s rounding” is great, but making this the default? mmmh…
I would understand this method being the default in a statistics library like “pandas”, but as an “all around” language, this is a very strange choice…

The more I see this kind of particularity/discrepancy on python, nodejs or go, the more I’m happy to stick with my old “declining” java language… it may not be the smartest or the prettiest, but at least it’s sane and predictible…
NB: had a total crackdown this week trying to understand date formatting in Go… the thing is… just crazy…

1 Like

One of my math teachers a long time ago demonstrated something like this to tell us not to trust computers just because. He made some sort of spreadsheet or used the calculator to repeat the problem and it was something simple 10-.1+.1 and pretty quickly it would fail.

Learning the little oddities like this are pretty crazy.

2 Likes

It does seem like they should at least have an optional parameter for this with the default behavior the same as 2.7. Since python 2.7 did always round up the .5, seems like it would have better to leave that function the same and make a different banker’s round function. Can’t go backwards though. Now that I know this, it will certainly sink into my memory easily.

[Edit]
Here is a little openscad script that rounds the way I expected it to.

for (i = [0:20])
{
a = i + 0.5;
num = round(a);
echo("a = “, a,” num = ", num);
}

If my logic is correct, you should be able to add a very small number to it before rounding like this.
round(2.5 + 0.0000000001)

int (variableName + .5)

Or, for 2 decimal places

int (variableName * 100 + .5) / 100

Just use the old way before programming languages had these new-fangled rounding functions. :rofl: and while yer at it… get off my lawn!

2 Likes

.NET has this BS logic by default too.

Doc for Math.Round(…) variants are long, and full of notes on ‘rounding to even’, and banker’s rounding based behavior.

imo, a long doc full of notes and caveats like this is an unwitting confession that the default behavior is wrong…

  • “Rounding away from zero is the most widely known form of rounding, while rounding to nearest even is the standard in financial and statistical operations. It conforms to IEEE Standard 754, section 4. When used in multiple rounding operations, rounding to nearest even reduces the rounding error that is caused by consistently rounding midpoint values in a single direction. In some cases, this rounding error can be significant.”

They rationalize the default behavior as following https://en.wikipedia.org/wiki/IEEE_754#Roundings_to_nearest

Yeah, that’s great and all, but… The default expectation of most Dev Customers wanting to round to an integer requires them to mindfully remember to use a cumbersome non default overload… e.g.

  • :heavy_check_mark: Math.Round(1.5) and Math.Round(1.5, MidpointRounding.AwayFromZero) both returns 2
  • :heavy_check_mark: Math.Round(2.5, MidpointRounding.AwayFromZero) returns 3
    -:poop:Math.Round(2.5) returns 2

:man_shrugging:

.NET source code for those interested …

1 Like

At least there’s an overload that is easy to see and “catch” if you use VS ou VSCode for completion :slight_smile:

I think this tells a lot about how the language is targeted too…
An awful lot of the workforce in development is located in banks, insurances, trade markets and such…
So, if you’re responsible for the design of a language, maybe it makes sense to adopt a “financial” logic by default…

Once again, if you take Java as an example, this kind of decision would be subject to an RFC, not decided upon or implemented before 3 major versions (4-5 years), and we’d probably wouldn’t change the default behaviour that’s been standard for the past 21 versions, just add another method or overload and/or some kind of flag to change the behaviour…

I think a simple solution to this problem in python 3 is to just use the int() function instead of the round() function like this:
int(2.5 + 0.5)
You could even define a new function called rnd()

def rnd(a):
    x = int(a + 0.5)
    return x

I seem to recall using this method in Fortan many years ago.

2 Likes

I think that comes under the heading of “That’s what I said…”

I think the problem though is changing the default behaviour, which is a problem. Mind you nobody should be just converting Python 2.x to 3.x without checks, because there are other (sometimes major) differences in how the code operates. Python 2 and Python 3 can almost be thought of as different syntactically similar languages. There is a reason why I keep python 2.7 around on my servers, even though I do most new projects using python3.

2 Likes

So sorry I missed that, or maybe I subconsciously saw it & it didn’t register in my conscious. Thanks for pointing that out for my conscious mind.

1 Like

If I was just doing statistics, I doubt I would ever notice (and I haven’t so far :sweat_smile:).

Something like converting from a decimal amount of days to the nearest night would definitely screw me up. Having Monday and Wednesday put 12pm in the am would be a hard to track down bug.

But c’mon, it isn’t worth the punishment of Java! :smiley:

com.this.was.a.joke.please.dont.start.a.turf.war

I haven’t actually used Java since I stopped doing android programming. That was a long time ago and I think a lot of new apps are written in JavaScript.

:rofl:

The whole Android stack/studio was indeed awful :slight_smile:
But just like the rest of this language, it’s not the language itself that counts, but the many many libraries it has

Right now, no one would ever write “native” android apps, there are plenty frameworks and products to make your life easier :slight_smile:

1 Like

You forgot to catch the declared exceptions :wink:

2 Likes