¿Cómo redondear correctamente los números de medio float en Python?

Me enfrento a un extraño comportamiento de la función round() :

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

Este código imprime:

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

Esperaba que los valores flotantes se redondearan siempre, pero en cambio, se redondea al número par más cercano.

¿Por qué este tipo de comportamiento y cuál es la mejor manera de obtener el resultado correcto?

Intenté usar las fractions pero el resultado es el mismo.

La sección Tipos numéricos documenta este comportamiento explícitamente:

round(x[, n])
x redondeado a n dígitos, redondeando la mitad a par. Si se omite n, el valor predeterminado es 0.

Nótese el redondeo a la mitad para igualar . Esto también se llama redondeo de banqueros ; en lugar de redondear siempre hacia arriba o hacia abajo (errores de redondeo de composición), redondeando al número par más cercano a su promedio de errores de redondeo.

Si necesita más control sobre el comportamiento del redondeo, use el módulo decimal , que le permite especificar exactamente qué estrategia de redondeo se debe usar .

Por ejemplo, para redondear desde la mitad:

 >>> 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 

Por ejemplo:

 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) 

round() redondeará hacia arriba o hacia abajo, dependiendo de si el número es par o impar. Una forma simple de solo redondear es:

 int(num + 0.5) 

Si quieres que esto funcione correctamente para números negativos usa:

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

Tenga en cuenta que esto puede estropear grandes números o números realmente precisos como 5000000000000001.0 y 0.49999999999999994 .

Puedes usar esto:

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

Redondeará el número hacia arriba o hacia abajo correctamente.

El comportamiento que está viendo es el típico comportamiento de redondeo IEEE 754. Si tiene que elegir entre dos números que son igualmente diferentes de la entrada, siempre elige el par. La ventaja de este comportamiento es que el efecto de redondeo promedio es cero; igualmente, muchos números se redondean hacia arriba y hacia abajo. Si redondea los números a mitad de camino en una dirección consistente, el redondeo afectará el valor esperado.

El comportamiento que está viendo es correcto si el objective es un redondeo justo, pero eso no siempre es lo que se necesita.

Un truco para obtener el tipo de redondeo que desea es agregar 0.5 y luego tomar la palabra. Por ejemplo, sumr 0.5 a 2.5 da 3, con piso 3.

Versión corta: utilizar el módulo decimal . Puede representar números como 2.675 con precisión, a diferencia de los flotadores de Python, donde 2.675 es realmente 2.67499999999999982236431605997495353221893310546875 (exactamente). Y puede especificar el redondeo que desee: ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP, y ROUND_05UP son todas las opciones.

El redondeo al número par más cercano se ha convertido en una práctica común en las disciplinas numéricas. “Redondear” produce un ligero sesgo hacia resultados más grandes.

Así, desde la perspectiva del establecimiento científico, la round tiene el comportamiento correcto.

Puedes usar:

 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)) 

Me encanta la respuesta fedor2612 . Lo amplié con un argumento opcional de “decimales” para aquellos que quieran usar esta función para redondear cualquier número de decimales (por ejemplo, si desea redondear una moneda de $ 26.455 a $ 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) 

Salida:

26.45

26.46

Aquí hay otra solución. Funcionará como redondeo normal en 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

Un redondeo matemático clásico sin bibliotecas.

 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( (qc)*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 

La siguiente solución logró el “redondeo de la moda escolar” sin utilizar el módulo decimal (que resulta lento).

 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) 

p.ej

 print(round(0.005,2)) # 0 print(school_round(0.005,2)) #0.01