Strange round() results?

I found that the results of round() are hard to understand:

julia> round(Int, 0.5)
0
julia> round(Int, 1.49999999)
1
julia> round(Int, 1.5)
2
julia> round(Int, 2.5)
2
julia> round(Int, 3.5)
4
julia> round(Int, 4.5)
4
julia> round(Int, 5.5)
6
julia> round(Int, 6.5)
6

See https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/#Rounding-modes-1

It is helpful if you in addition of just listing the results explain what in particular is “hard to understand” and what in the documentation does not explain it well enough.

2 Likes

what’s “hard to understand” is:

if round() is rounding-down (floor), 1.5 should get 1, and 2.5 should get 2
otherwise, if it’s rounding-up (ceiling), 1.5 should get 2, and 2.5 should get 3

on the other hand, the “common sense” rounding is: 0 <= x < 0.5 --> 0, 0.5 <= x <= 1.0 --> 1

now, 1.5 gets 2, and 2.5 gets 2 also

it’s counter-intuitive

So the docs for round say

The RoundingMode r controls the direction of the rounding; the default is RoundNearest, which rounds to the nearest integer, with ties (fractional values of 0.5) being rounded to the nearest even integer

And it has your exact example as an example

 Examples
 ≡≡≡≡≡≡≡≡≡≡

  julia> round(1.7)
  2.0

  julia> round(Int, 1.7)
  2

  julia> round(1.5)
  2.0

  julia> round(2.5)
  2.0

See https://en.wikipedia.org/wiki/Rounding#Round_half_to_even:

This is the default rounding mode used in IEEE 754 floating-point operations

4 Likes

Just briefly, the reasoning for this as compared to the rounding that you learned in school is that it removes a bias towards always making numbers farther away from zero. It’s commonly called banker’s rounding for this reason. Imagine you have a sales tax of 10%. Every time you buy something that ends in 5 cents (like 9.95, 11.25, etc), you’re going to need to round to the nearest cent to determine the tax. If you always rounded up, you’d overestimate the tax. If you always rounded down, you’d underestimate it. This tries to remove the bias and it will if your prices are uniformly distributed.

http://wiki.c2.com/?BankersRounding

12 Likes

While I definitely appreciate the explanation of (some) reasoning behind it (in all honesty – this will be a caveat I will be on the lookout for and probably make the associated mistake/assumption many times, since I am just starting to learn Julia, so – I am grateful for having a way to think about it and realize the “right” way, at least according to the computer)… even just the fact that this method has a special name that you referenced for us as opposed to just, you know, “rounding” seems to me like a pretty huge indication of just an objectively bad and confusing design decision (especially when you have the easily available RoundUp/RoundDown/TowardZero/etc. parameters built in if you wanted to do something weird like that!) that makes me want to run giddily back into Python land – where the only problem is that everything makes too much sense, so much so that it kinda makes me want to never go back to or learn any new language >_<

Seriously though, doesn’t this come up all the time and need to be accounted for? The typical, mathematically standard rounding we are all used to divides the number line into exactly equal-length integer-sized bins, which is a useful (and typically expected/assumed…) property for e.g. random number generation or building histograms, just off the top of my head – whereas this method alternates slightly smaller (open) intervals around the odd numbers with slightly larger (closed) intervals around the evens. Is there a typical/standard way around this quirk for functions which generate or take floats, for them to behave in the normal/expected way? Does one just always use the away from zero rounding flag? Hell I feel like I’d rather redefine round() locally within scope of all my files as a utility, if only for my own sanity lol.

Before you run off, you should check what Python actually does:

In [10]: round(0.5)                                                                                                                                                                                                                                 
Out[10]: 0

In [11]: round(1.5)                                                                                                                                                                                                                                 
Out[11]: 2

In [12]: round(2.5)                                                                                                                                                                                                                                 
Out[12]: 2

In [13]: round(3.5)                                                                                                                                                                                                                                 
Out[13]: 4

Or if you’re more of a numpy person:

In [6]: import numpy as np                                                                                                                                                                                                                          

In [7]: np.round(0.5)                                                                                                                                                                                                                               
Out[7]: 0.0

In [8]: np.round(1.5)                                                                                                                                                                                                                               
Out[8]: 2.0

In [9]: np.round(2.5)                                                                                                                                                                                                                               
Out[9]: 2.0

The way floats behave is defined by a standard (IEEE 754 - Wikipedia). Julia and Python are both implementing the same standard, and they are giving the same result.

12 Likes

You can actually change the RoundingMode, although the docs say it is not safe to change that as a global setting.

If you want to just round up or down, you can simply use ceil() and floor(), both of which require no additional imports (unlike Python). What I really liked about Julia is this, that all common mathematical operations are readily available.

3 Likes