Python 3.X Rounding Behavior

Python 3.x rounding behavior

Python 3's way (called "round half to even" or "banker's rounding") is considered the standard rounding method these days, though some language implementations aren't on the bus yet.

The simple "always round 0.5 up" technique results in a slight bias toward the higher number. With large numbers of calculations, this can be significant. The Python 3.0 approach eliminates this issue.

There is more than one method of rounding in common use. IEEE 754, the international standard for floating-point math, defines five different rounding methods (the one used by Python 3.0 is the default). And there are others.

This behavior is not as widely known as it ought to be. AppleScript was, if I remember correctly, an early adopter of this rounding method. The round command in AppleScript offers several options, but round-toward-even is the default as it is in IEEE 754. Apparently the engineer who implemented the round command got so fed up with all the requests to "make it work like I learned in school" that he implemented just that: round 2.5 rounding as taught in school is a valid AppleScript command. :-)

Python Rounding (Properly)

Python uses "banker's rounding" for x.5. Even numbers round down, odd numbers round up. You are saying it "should" round up, but that's just your definition. It's not the only definition.

If you always want rounding up, do:

result = int(x+0.5)

Weird rounding using numpy.floor() to every 0.02

This happens due to the finite resolution of floating-point numbers on a computer leading to slightly inaccurate results when you perform arithmetic operations on them (Roundoff error caused by floating-point arithmetic). You can see what happens by just performing the parts of your algorithm separately:

>>> 32.16*50
1607.9999999999998

So you see this is slightly off from the exactly accurate result of 1608.0 and will result in 1607.0 after applying np.floor().

What you could do to fix this is put a np.round() inside the np.floor() call, i.e.:

def cpt_rnd(x):
return np.floor(np.round(x*50))/50

By default, np.round() rounds off to the nearest integer, which is exactly what we want here.

Python 3.x rounding half up

Rounding is surprisingly hard to do right, because you have to handle floating-point calculations very carefully. If you are looking for an elegant solution (short, easy to understand), what you have like like a good starting point. To be correct, you should replace decimal.Decimal(str(number)) with creating the decimal from the number itself, which will give you a decimal version of its exact representation:

d = Decimal(number).quantize(...)...

Decimal(str(number)) effectively rounds twice, as formatting the float into the string representation performs its own rounding. This is because str(float value) won't try to print the full decimal representation of the float, it will only print enough digits to ensure that you get the same float back if you pass those exact digits to the float constructor.

If you want to retain correct rounding, but avoid depending on the big and complex decimal module, you can certainly do it, but you'll still need some way to implement the exact arithmetics needed for correct rounding. For example, you can use fractions:

import fractions, math

def round_half_up(number, dec_places=0):
sign = math.copysign(1, number)
number_exact = abs(fractions.Fraction(number))
shifted = number_exact * 10**dec_places
shifted_trunc = int(shifted)
if shifted - shifted_trunc >= fractions.Fraction(1, 2):
result = (shifted_trunc + 1) / 10**dec_places
else:
result = shifted_trunc / 10**dec_places
return sign * float(result)

assert round_half_up(1.49) == 1
assert round_half_up(1.5) == 2
assert round_half_up(1.51) == 2
assert round_half_up(2.49) == 2
assert round_half_up(2.5) == 3
assert round_half_up(2.51) == 3

Note that the only tricky part in the above code is the precise conversion of a floating-point to a fraction, and that can be off-loaded to the as_integer_ratio() float method, which is what both decimals and fractions do internally. So if you really want to remove the dependency on fractions, you can reduce the fractional arithmetic to pure integer arithmetic; you stay within the same line count at the expense of some legibility:

def round_half_up(number, dec_places=0):
sign = math.copysign(1, number)
exact = abs(number).as_integer_ratio()
shifted = (exact[0] * 10**dec_places), exact[1]
shifted_trunc = shifted[0] // shifted[1]
difference = (shifted[0] - shifted_trunc * shifted[1]), shifted[1]
if difference[0] * 2 >= difference[1]: # difference >= 1/2
shifted_trunc += 1
return sign * (shifted_trunc / 10**dec_places)

Note that testing these functions brings to spotlight the approximations performed when creating floating-point numbers. For example, print(round_half_up(2.175, 2)) prints 2.17 because the decimal number 2.175 cannot be represented exactly in binary, so it is replaced by an approximation that happens to be slightly smaller than the 2.175 decimal. The function receives that value, finds it smaller than the actual fraction corresponding to the 2.175 decimal, and decides to round it down. This is not a quirk of the implementation; the behavior derives from properties of floating-point numbers and is also present in the round built-in of Python 3 and 2.

How to properly round-up half float numbers?

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

Why is Python: 3.8 rounding Decimal 1120.50 down instead of up?

The round built-in function rounds towards the even solution when between two values.

This base round function definition is documented here: https://docs.python.org/3.7/library/functions.html#round

As the decimal package is a builtin, but not a basic type, it implements its own __round__ function, which follows the built-in standard of rounding to the even solution when between two values.

This isn't well documented, but I verified in the code itself: https://github.com/python/cpython/blob/1f0cde678406749524d11e852a16bf243cef5c5f/Lib/_pydecimal.py#L1890

There is no way to override the way the round function works for decimal objects.

Use quantize instead

The decimal package provides a function quantize which allows you to round and set the round type.

Quantize is documented here: https://docs.python.org/3/library/decimal.html#decimal.Decimal.quantize

and the round types are here: https://docs.python.org/3/library/decimal.html#rounding-modes



Related Topics



Leave a reply



Submit