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
How to Modify List Entries During For Loop
How to Measure Elapsed Time in Python
Are For-Loops in Pandas Really Bad? When Should I Care
How to Check If a String Represents an Int, Without Using Try/Except
How to Install a Python Package With a .Whl File
Count the Frequency That a Value Occurs in a Dataframe Column
Setting the Correct Encoding When Piping Stdout in Python
How to Replace Nans by Preceding or Next Values in Pandas Dataframe
Performant Cartesian Product (Cross Join) With Pandas
How to Format a Floating Number to Fixed Width in Python
Pip Install Failing With: Oserror: [Errno 13] Permission Denied on Directory
How to Save/Restore a Model After Training
Retrieve Links from Web Page Using Python and Beautifulsoup
How to Prevent Tensorflow from Allocating the Totality of a Gpu Memory
How to Count the Frequency of the Elements in an Unordered List
Does Python Optimize Tail Recursion
Taking Subarrays from Numpy Array With Given Stride/Stepsize