How to properly round-up half float numbers?
Asked Answered
I

24

132

I am facing a strange behavior of the round() function:

for i in range(1, 15, 2):
    n = i / 2
    print(n, "=>", round(n))

This code prints:

0.5 => 0
1.5 => 2
2.5 => 2
3.5 => 4
4.5 => 4
5.5 => 6
6.5 => 6

I expected the floating values to be always rounded up, but instead, it is rounded to the nearest even number.

Why such behavior, and what is the best way to get the correct result?

I tried to use the fractions but the result is the same.

Interlanguage answered 8/10, 2015 at 15:11 Comment(4)
can't explain the behaviour of round() but you could use math.ceil() if you always want to round upAeolic
@Aeolic I would like 1.3 to be rounded down to 1, so I can not use ceil().Interlanguage
Possible duplicate of Limiting floats to two decimal pointsAeolic
Many days have passed since I studied error analysis. However If I recall correctly, the rounding of 5*10**-k depends on the digit preceding it. By rounding up for uneven digits and down for even digits, you get a positive error half the time and an even error half the time (in theory). When you perform many additions, those errors can cancel each-otherKanzu
R
111

The Numeric Types section documents this behaviour explicitly:

round(x[, n])
x rounded to n digits, rounding half to even. If n is omitted, it defaults to 0.

Note the rounding half to even. This is also called bankers rounding; instead of always rounding up or down (compounding rounding errors), by rounding to the nearest even number you average out rounding errors.

If you need more control over the rounding behaviour, use the decimal module, which lets you specify exactly what rounding strategy should be used.

For example, to round up from half:

>>> from decimal import localcontext, Decimal, ROUND_HALF_UP
>>> with localcontext() as ctx:
...     ctx.rounding = ROUND_HALF_UP
...     for i in range(1, 15, 2):
...         n = Decimal(i) / 2
...         print(n, '=>', n.to_integral_value())
...
0.5 => 1
1.5 => 2
2.5 => 3
3.5 => 4
4.5 => 5
5.5 => 6
6.5 => 7
Rubierubiginous answered 8/10, 2015 at 15:24 Comment(3)
IEEE 754 rounding half to even is also described at en.wikipedia.org/wiki/Rounding#Round_half_to_evenPinfold
In your example, is there a benefit to modifying the local context as opposed to just using the rounding argument as in: n.to_integral_value(rounding=ROUND_HALF_UP)?Halfcocked
@dhobbs: setting the context once is clearer in intent, but from a technical point of view there is no difference.Rubierubiginous
H
60

For example:

from decimal import Decimal, ROUND_HALF_UP

Decimal(1.5).quantize(0, ROUND_HALF_UP)

# This also works for rounding to the integer part:
Decimal(1.5).to_integral_value(rounding=ROUND_HALF_UP)
Halfcocked answered 8/10, 2015 at 15:28 Comment(0)
A
33

You can use this:

import math
def normal_round(n):
    if n - math.floor(n) < 0.5:
        return math.floor(n)
    return math.ceil(n)

It will round number up or down properly.

Alverson answered 18/12, 2016 at 7:20 Comment(2)
Time and time I am shocked there is not internal function like this. I mean, it is not like people nowadays use a lot of numpy and math in python to implement numerical algorithms....Include
It does round 'up or down', but unfortunately doesn't work with negative numbers.Zaller
P
28

round() will round either up or down, depending on if the number is even or odd. A simple way to only round up is:

int(num + 0.5)

If you want this to work properly for negative numbers use:

((num > 0) - (num < 0)) * int(abs(num) + 0.5)

Note, this can mess up for large numbers or really precise numbers like 5000000000000001.0 and 0.49999999999999994.

Profluent answered 31/7, 2017 at 19:51 Comment(3)
There are some subtleties that aren't addressed by this solution. E.g., what result does this give if num = -2.4? What about num = 0.49999999999999994? num = 5000000000000001.0? On a typical machine using IEEE 754 format and semantics, this solution gives the wrong answer for all three of these cases.Highkeyed
@Mark Dickinson I've updated the post to mention this. ThanksProfluent
Strictly speaking, both 0.49999999999999994 and 5000000000000001.0 are problematic due to precision. In both cases, adding 0.5 causes necessary precision bits to "fall off" the right hand side of the IEEE 754 double (64bit) mantissa (52 fraction bits + implicit 1.0). The first case basically doubles the value, pushing the (set) LSB out, while the second case is so large that the 0.5 is smaller than the existing LSB value. In fact, for 2^52 <= num < 2^53, it will round integers to even (prob. due to math chip rounding 80bit internal back to 64bit output). num >= 2^53 adding 0.5 does nothing.Verbosity
H
15

Why make it so complicated? (Only works for positive numbers)

def HalfRoundUp(value):
    return int(value + 0.5)

You could of course make it into a lambda which would be:

HalfRoundUp = lambda value: int(value + 0.5)

Unfortunately, this simple answer doesn't work with negative numbers, but it can be fixed with the floor function from math: (This works for both positive and negative numbers too)

from math import floor
def HalfRoundUp(value):
    return floor(value + 0.5)
Heelpiece answered 20/6, 2021 at 22:50 Comment(3)
Does not add any value to other answers.Nuptial
best answer in this page! can't believe python doesn't have built-in solutions.Malemute
@Malemute Python's round() function used to round up in older versions, but they switched the rounding strategy to half to even in Python 3. It was a deliberate choice because this produces better results. See: docs.python.org/3/whatsnew/3.0.html#builtinsAnnikaanniken
C
12

The behavior you are seeing is typical IEEE 754 rounding behavior. If it has to choose between two numbers that are equally different from the input, it always picks the even one. The advantage of this behavior is that the average rounding effect is zero - equally many numbers round up and down. If you round the half way numbers in a consistent direction the rounding will affect the expected value.

The behavior you are seeing is correct if the objective is fair rounding, but that is not always what is needed.

One trick to get the type of rounding you want is to add 0.5 and then take the floor. For example, adding 0.5 to 2.5 gives 3, with floor 3.

Carrigan answered 8/10, 2015 at 15:24 Comment(0)
S
12

Love the fedor2612 answer. I expanded it with an optional "decimals" argument for those who want to use this function to round any number of decimals (say for example if you want to round a currency $26.455 to $26.46).

import math

def normal_round(n, decimals=0):
    expoN = n * 10 ** decimals
    if abs(expoN) - abs(math.floor(expoN)) < 0.5:
        return math.floor(expoN) / 10 ** decimals
    return math.ceil(expoN) / 10 ** decimals

oldRounding = round(26.455,2)
newRounding = normal_round(26.455,2)

print(oldRounding)
print(newRounding)

Output:

26.45

26.46

Superadd answered 2/10, 2018 at 23:47 Comment(1)
Great function! Never thought I'll face such an issue with rounding 133.125 with 2 digits to 133.13 nor to 133.12 . Thanks, Man!Eccles
I
3

Short version: use the decimal module. It can represent numbers like 2.675 precisely, unlike Python floats where 2.675 is really 2.67499999999999982236431605997495353221893310546875 (exactly). And you can specify the rounding you desire: ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP, and ROUND_05UP are all options.

Ila answered 8/10, 2015 at 15:20 Comment(0)
G
2

Rounding to the nearest even number has become common practice in numerical disciplines. "Rounding up" produces a slight bias towards larger results.

So, from the perspective of the scientific establishment, round has the correct behavior.

Gemoets answered 8/10, 2015 at 15:19 Comment(1)
Sure, it's the correct way if you are processing measured data, running simulations etc. But it's incorrect, for example, if you would like to calculate grades in school. In Hungary, we have a 5-grade grading system, and an average of 4.5 is rounded up to 5. I used this as an example when teaching my son Python, and I was stunned when round(4.5) gave 5. I had a hard time explaining to him why Python rounding works differently from what he learned in school about rounding...Quaver
T
2

Here is another solution. It will work as normal rounding in excel.

from decimal import Decimal, getcontext, ROUND_HALF_UP

round_context = getcontext()
round_context.rounding = ROUND_HALF_UP

def c_round(x, digits, precision=5):
    tmp = round(Decimal(x), precision)
    return float(tmp.__round__(digits))

c_round(0.15, 1) -> 0.2, c_round(0.5, 0) -> 1

Tallie answered 15/11, 2018 at 23:18 Comment(1)
Anyone wondering about tmp.__round__, see Python built-in function source code - round() or Python 3 Decimal rounding half down with ROUND_HALF_UP context.Verbosity
B
2

In the question this is basically an issue when dividing a positive integer by 2. The easisest way is int(n + 0.5) for individual numbers.

However we cannot apply this to series, therefore what we then can do for example for a pandas dataframe, and without going into loops, is:

import numpy as np
df['rounded_division'] = np.where(df['some_integer'] % 2 == 0, round(df['some_integer']/2,0), round((df['some_integer']+1)/2,0))
Burghley answered 11/7, 2019 at 9:40 Comment(0)
R
2

A small addition as the rounding half up with some of the solutions might not work as expected in some cases.

Using the function from above for instance:

from decimal import Decimal, ROUND_HALF_UP
def round_half_up(x: float, num_decimals: int) -> float:
    if num_decimals < 0:
        raise ValueError("Num decimals needs to be at least 0.")
    target_precision = "1." + "0" * num_decimals
    rounded_x = float(Decimal(x).quantize(Decimal(target_precision), ROUND_HALF_UP))
    return rounded_x
round_half_up(1.35, 1)
1.4
round_half_up(4.35, 1)
4.3

Where I was expecting 4.4. What did the trick for me was converting x into a string first.

from decimal import Decimal, ROUND_HALF_UP
def round_half_up(x: float, num_decimals: int) -> float:
    if num_decimals < 0:
        raise ValueError("Num decimals needs to be at least 0.")
    target_precision = "1." + "0" * num_decimals
    rounded_x = float(Decimal(str(x)).quantize(Decimal(target_precision), ROUND_HALF_UP))
    return rounded_x

round_half_up(4.35, 1)
4.4
Redroot answered 15/6, 2021 at 7:50 Comment(0)
A
1

The following solution achieved "school fashion rounding" without using the decimal module (which turns out to be slow).

def school_round(a_in,n_in):
''' python uses "banking round; while this round 0.05 up" '''
    if (a_in * 10 ** (n_in + 1)) % 10 == 5:
        return round(a_in + 1 / 10 ** (n_in + 1), n_in)
    else:
        return round(a_in, n_in)

e.g.

print(round(0.005,2)) # 0
print(school_round(0.005,2)) #0.01
Alkane answered 20/12, 2018 at 15:51 Comment(0)
B
1

So just to make sure there is a crystal clear working example here, I wrote a small convenience function

def round_half_up(x: float, num_decimals: int) -> float:
    """Use explicit ROUND HALF UP. See references, for an explanation.

    This is the proper way to round, as taught in school.

    Args:
        x:
        num_decimals:

    Returns:
            https://mcmap.net/q/65015/-how-to-properly-round-up-half-float-numbers

    """

    if num_decimals < 0:
        raise ValueError("Num decimals needs to be at least 0.")
    target_precision = "1." + "0" * num_decimals
    rounded_x = float(Decimal(x).quantize(Decimal(target_precision), ROUND_HALF_UP))
    return rounded_x

And an appropriate set of test cases

def test_round_half_up():
    x = 1.5
    y = round_half_up(x, 0)
    assert y == 2.0

    y = round_half_up(x, 1)
    assert y == 1.5

    x = 1.25
    y = round_half_up(x, 1)
    assert y == 1.3

    y = round_half_up(x, 2)
    assert y == 1.25

Bosh answered 19/12, 2020 at 15:43 Comment(0)
H
1

I'd like to share my solution to the problem. Using decimal library.

import decimal

def round_number(number, decimal_places):
    decimal.getcontext().rounding = decimal.ROUND_HALF_UP
    decimal_number = decimal.Decimal(str(number))
    rounded_number = decimal_number.quantize(decimal.Decimal("0." + "0" * decimal_places))
    rounded_float = float(rounded_number)
    return rounded_float
Hub answered 2/7, 2023 at 14:7 Comment(1)
This is exactly what I also came up with, this seems to be the only way to "clean" a number in float format.Muckworm
I
0

You can use:

from decimal import Decimal, ROUND_HALF_UP

for i in range(1, 15, 2):
    n = i / 2
    print(n, "=>", Decimal(str(n)).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
Insouciance answered 8/10, 2015 at 15:29 Comment(0)
B
0

A classical mathematical rounding without any libraries

def rd(x,y=0):
''' A classical mathematical rounding by Voznica '''
m = int('1'+'0'*y) # multiplier - how many positions to the right
q = x*m # shift to the right by multiplier
c = int(q) # new number
i = int( (q-c)*10 ) # indicator number on the right
if i >= 5:
    c += 1
return c/m

Compare:

print( round(0.49), round(0.51), round(0.5), round(1.5), round(2.5), round(0.15,1))  # 0  1  0  2  2  0.1

print( rd(0.49), rd(0.51), rd(0.5), rd(1.5), rd(2.5), rd(0.15,1))  # 0  1  1  2  3  0.2
Bans answered 16/12, 2018 at 14:12 Comment(0)
P
0

Knowing that round(9.99,0) rounds to int=10 and int(9.99) rounds to int=9 brings success:

Goal: Provide lower and higher round number depending on value

    def get_half_round_numers(self, value):
        """
        Returns dict with upper_half_rn and lower_half_rn
        :param value:
        :return:
        """
        hrns = {}
        if not isinstance(value, float):
            print("Error>Input is not a float. None return.")
            return None

        value = round(value,2)
        whole = int(value) # Rounds 9.99 to 9
        remainder = (value - whole) * 100

        if remainder >= 51:
            hrns['upper_half_rn'] = round(round(value,0),2)  # Rounds 9.99 to 10
            hrns['lower_half_rn'] = round(round(value,0) - 0.5,2)
        else:
            hrns['lower_half_rn'] = round(int(value),2)
            hrns['upper_half_rn'] = round(int(value) + 0.5,2)

        return hrns

Some testing:

enter image description here

yw

Painless answered 9/11, 2019 at 18:54 Comment(0)
R
0
import math
# round tossing n digits from the end
def my_round(n, toss=1):

    def normal_round(n):
        if isinstance(n, int):
            return n
        intn, dec = str(n).split(".")
        if int(dec[-1]) >= 5:
            if len(dec) == 1:
                return math.ceil(n)
            else:
                return float(intn + "." + str(int(dec[:-1]) + 1))
        else:
            return float(intn + "." + dec[:-1])

    while toss >= 1:
        n = normal_round(n)
        toss -= 1
    return n


for n in [1.25, 7.3576, 30.56]:
    print(my_round(n, 2))

1.0
7.36
31
Robbi answered 26/3, 2020 at 11:6 Comment(0)
U
0
import math
def round_half_up(x: float) -> int:
    if x < 0:
        return math.trunc(x) if -x % 1 < 0.5 else math.floor(x)
    else:
        return math.trunc(x) if  x % 1 < 0.5 else math.ceil(x)

This even works for corner cases like 0.49999999999999994 and 5000000000000001.0.

Unionize answered 28/10, 2022 at 0:58 Comment(0)
M
0

This is a function that takes the number of decimal places as an argument. It also rounds up half decimal.

import math
def normal_round(n, decimal_places):
    if int((str(n)[-1])) < 5:
        return round(n, decimal_places)
    return round(n + 10**(-1 * (decimal_places+1)), decimal_places)

Test cases:

>>> normal_round(5.12465, 4)
5.1247
>>> normal_round(5.12464, 4)
5.1246
>>> normal_round(5.12467, 4)
5.1247
>>> normal_round(5.12463, 4)
5.1246
>>> normal_round(5.1241, 4)
5.1241
>>> normal_round(5.1248, 4)
5.1248
>>> normal_round(5.1248, 3)
5.125
>>> normal_round(5.1242, 3)
5.124
Multinational answered 25/1, 2023 at 8:1 Comment(1)
normal_round(5.124644, 4) gives 5.1246 but normal_round(5.124645, 4) gives 5.1247 It is not a good solution to just check the very last number and then add a small amount at the last decimal place +1, since the last number may be not relevant at all. e.g. normal_round(0.4000000005, 0) will look at the 5 and therefore add 0.1Muckworm
M
0

Another similar solution:

import decimal

def round_normal(number, digits=0):
    decimal.getcontext().rounding = decimal.ROUND_HALF_UP
    return round(decimal.Decimal(str(number)), digits)

or if you are going for speed

import decimal

decimal.getcontext().rounding = decimal.ROUND_HALF_UP

def round_normal(number, digits=0):
    return round(decimal.Decimal(str(number)), digits)

if you need a result in float, then put float around the whole part.


  • Why not using quantize?

It takes longer to construct the string format for quantize.

  • Why not use local context?

If you prefer to change the context just locally, you could also do:

def round_normal(number, digits=0):
    with decimal.localcontext() as ctx:
        ctx.rounding = decimal.ROUND_HALF_UP
        return round(decimal.Decimal(str(number)), digits)

but the time to execute this is again much higher. same for the abs math.floor else math.ceil solution (a bit more runtime)


So, why is this cast from float to string needed? didn't the other solutions without input -> str -> decimal work faster?

Yes, it is faster with less conversions, however, as mentioned there is a problem with some float numbers. here an example:

> a = Decimal(1.125); b = Decimal("1.125"); a == b
True
> a = Decimal(1.255); b = Decimal("1.255"); a == b
False
> a
Decimal('1.25499999999999989341858963598497211933135986328125')
> b
Decimal('1.255')

This means that even if we convert to Decimal, if our input is a float and already changed to a close but not fully matching number, then it does not help at all!

Therefore, we can go the approach to convert to str and then to Decimal:

> c = Decimal("1.255")
> c
Decimal('1.255')

if we just go with the float 1.255 or use this in a Decimal (so it keeps its error), then the calculation goes wrong!

Here the example:

> round(1.255, 2)
1.25
> round(Decimal(1.255), 2)
Decimal('1.25')
> round(Decimal(str(1.255)), 2)
Decimal('1.26')

But keep in mind, the float -> str -> Decimal works, since the string representation of a float has not such a high precision, so the number is kind of rounded.

The solution helps to not need to change number formats in your other code, but cleanest way would be to work with Decimal everywhere! Then there would also no need for those conversions

Muckworm answered 13/7, 2023 at 12:15 Comment(0)
H
0

No modules to import, simple:

if (a / b) - (a // b) == 0.5:
    c = (a // b) + 1
else:
    c = round(a / b)
Hanshaw answered 6/11, 2023 at 18:28 Comment(0)
A
-1

You can try this

def round(num):
    return round(num + 10**(-9))

it will work since num = x.5 will always will be x.5 + 0.00...01 in the process which its closer to x+1 hence the round function will work properly and it will round x.5 to x+1

Astra answered 5/7, 2019 at 7:32 Comment(1)
Now x.499999999 will be subject to half-even rounding, and will (half the time, assuming floating point precision issues don't force it one way or the other) get rounded up. That's worse than the initial scenario, as you're now rounding to the more distant number.Macguiness

© 2022 - 2025 — McMap. All rights reserved.