Redondeo de números en Python

Usar una computadora para hacer matemáticas bastante complejas es una de las razones por las que esta máquina se desarrolló originalmente. Siempre que sean números enteros y sumas, restas...

Usar una computadora para hacer matemáticas bastante complejas es una de las razones por las que esta máquina se desarrolló originalmente. Mientras los números enteros y las sumas, restas y multiplicaciones estén exclusivamente involucrados en los cálculos, todo está bien. Tan pronto como entran en juego los números de coma flotante o las fracciones, así como las divisiones, todo se complica enormemente.

Como usuario habitual, no somos plenamente conscientes de estos problemas que ocurren detrás de escena y pueden terminar con resultados bastante sorprendentes y posiblemente inexactos para nuestros cálculos. Como desarrolladores, tenemos que asegurarnos de que se toman en cuenta las medidas apropiadas para indicarle a la computadora que funcione de la manera correcta.

En nuestra vida diaria usamos el sistema decimal que se basa en el número 10. La computadora usa el sistema binario, que es base 2, e internamente almacena y procesa los valores como una secuencia de 1 y 0. Los valores con los que trabajamos tienen que transformarse constantemente entre las dos representaciones. Como se explica en Documentación de Python:

...la mayoría de las fracciones decimales no se pueden representar exactamente como fracciones binarias. Una consecuencia es que, en general, los números decimales de coma flotante que ingresa solo se aproximan a los números binarios de coma flotante realmente almacenados en la máquina.

Este comportamiento conduce a resultados sorprendentes en adiciones simples, como se muestra aquí:

Listado 1: Inexactitudes con números de punto flotante

1
2
3
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999

Como puede ver aquí, la salida es inexacta, ya que debería resultar en 0.9.

Listado 2 muestra un caso similar para formatear un número de punto flotante para 17 lugares decimales.

Listado 2: Formatear un número de punto flotante

1
2
>>> format(0.1, '.17f')
'0.10000000000000001'

Como habrá aprendido de los ejemplos anteriores, tratar con números de punto flotante es un poco complicado y requiere medidas adicionales para lograr el resultado correcto y minimizar los errores de cálculo. Redondear el valor puede resolver al menos algunos de los problemas. Una posibilidad es la función integrada round() (para obtener más detalles sobre su uso, consulte a continuación):

Listado 3: Cálculo con valores redondeados

1
2
3
4
5
6
7
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
>>> s == 0.9
False
>>> round(0.9, 1) == 0.9
True

Como alternativa, puede trabajar con el módulo de matemáticas, o trabajar explícitamente con fracciones almacenadas como dos valores (numerador y denominador) en lugar del flotante redondeado, bastante inexacto valores de puntos.

Para almacenar los valores así los dos módulos de Python decimal y [fracción](https://docs.python.org/3 /library/fractions.html) entran en juego (ver ejemplos a continuación). Pero primero, echemos un vistazo más de cerca al término "redondeo".

¿Qué es el redondeo? {#lo que está rondando}

En pocas palabras, el proceso de redondeo significa:

...reemplazando [un valor] con un número diferente que es aproximadamente igual al original, pero tiene una representación más corta, más simple o más explícita.

[Fuente: https://en.wikipedia.org/wiki/Redondeo]{.pequeño}

Básicamente, agrega inexactitud a un valor calculado con precisión al acortarlo. En la mayoría de los casos, esto se hace eliminando dígitos después del punto decimal, por ejemplo, de 3,73 a 3,7, de 16,67 a 16,7 o de 999,95 a 1000.

Dicha reducción se realiza por varias razones, por ejemplo, para ahorrar espacio al almacenar el valor o simplemente para eliminar dígitos no utilizados. Además, los dispositivos de salida, como las pantallas analógicas o los relojes, pueden mostrar el valor calculado con una precisión limitada y requieren datos de entrada ajustados.

En general, se aplican dos reglas bastante simples para el redondeo, tal vez las recuerdes de la escuela. Los dígitos del 0 al 4 llevan al redondeo hacia abajo, y los números del 5 al 9 llevan a [redondeando](https://www.factmonster.com/math-science/mathematics/rounding-numbers-rules-examples-for-fractions- sumas). La siguiente tabla muestra una selección de casos de uso.

1
2
3
4
5
6
7
8
| original value | rounded to   | result |
|----------------|--------------|--------|
| 226            | the ten      | 230    |
| 226            | the hundred  | 200    |
| 274            | the hundred  | 300    |
| 946            | the thousand | 1,000  |
| 1,024          | the thousand | 1,000  |
| 10h45m50s      | the minute   | 10h45m |

Métodos de redondeo

Los matemáticos han desarrollado una variedad de diferentes métodos de redondeo para abordar el problema del redondeo. Esto incluye el truncamiento simple, el redondeo hacia arriba, el redondeo hacia abajo, el redondeo por la mitad hacia arriba, el redondeo por la mitad hacia abajo, así como el redondeo de la mitad desde cero y el redondeo de la mitad a la par.

Como ejemplo, el redondeo a la mitad de cero es aplicado por la Comisión Europea de Asuntos Económicos y Financieros al convertir monedas al euro. Varios países, como Suecia, los Países Bajos, Nueva Zelanda y Sudáfrica, siguen la regla denominada "redondeo en efectivo", "redondeo en centavos" o "redondeo en sueco".

El [redondeo de efectivo] ocurre cuando la unidad de cuenta mínima es más pequeña que la denominación física más baja de la moneda. El monto a pagar por una transacción en efectivo se redondea al múltiplo más cercano de la unidad monetaria mínima disponible, mientras que las transacciones pagadas de otra manera no se redondean.

[Fuente: https://en.wikipedia.org/wiki/Cash_rounding]{.small}

En Sudáfrica, desde 2002, el redondeo de efectivo se realiza a los 5 centavos más cercanos. En general, este tipo de redondeo no se aplica a los pagos electrónicos que no sean en efectivo.

En cambio, redondear de la mitad a par es la estrategia por defecto de Python, entumecido, y [pandas](/tutorial-para-principiantes-sobre -la-biblioteca-pandas-python/), y está en uso por la función integrada round() que ya se mencionó anteriormente. Pertenece a la categoría de los métodos de redondeo al más cercano y también se conoce como redondeo convergente, redondeo estadístico, redondeo holandés, redondeo gaussiano, redondeo par-impar y redondeo bancario. Este método está definido en IEEE754 y funciona de tal manera que "si la parte fraccionaria de x es 0,5, entonces y es el entero par más cercano a x." Se supone que "las probabilidades de que un empate en un conjunto de datos se redondee hacia abajo o hacia arriba son iguales", lo que suele ser el caso, en la práctica. Aunque no es del todo perfecta, esta estrategia conduce a resultados apreciables.

La siguiente tabla proporciona ejemplos prácticos de redondeo para este método:

1
2
3
4
5
6
7
8
| original value | rounded to |
|----------------|------------|
| 23.3           | 23         |
| 23.5           | 24         |
| 24.0           | 24         |
| 24.5           | 24         |
| 24.8           | 25         |
| 25.5           | 26         |

Funciones de Python {#funciones de Python}

Python viene con la función integrada round() que es bastante útil en nuestro caso. Acepta dos parámetros: el valor original y el número de dígitos después del punto decimal. La siguiente lista ilustra el uso del método para uno, dos y cuatro dígitos después del punto decimal.

Listado 4: Redondeo con un número específico de dígitos

1
2
3
4
5
6
>>> round(15.45625, 1)
15.5
>>> round(15.45625, 2)
15.46
>>> round(15.45625, 4)
15.4563

Si llama a esta función sin el segundo parámetro, el valor se redondea a un valor entero completo.

Listado 5: Redondeo sin un número específico de dígitos

1
2
3
4
5
6
>>> round(0.85)
1
>>> round(0.25)
0
>>> round(1.5)
2

Los valores redondeados funcionan bien en caso de que no necesite resultados absolutamente precisos. Tenga en cuenta que comparar valores redondeados también puede ser una pesadilla. Se volverá más obvio en el siguiente ejemplo: la comparación de valores redondeados basados ​​en el redondeo previo y el redondeo posterior.

El primer cálculo del Listado 6 contiene valores prerredondeados y describe el redondeo antes de sumar los valores. El segundo cálculo contiene un resumen posredondeado, lo que significa redondeo después de la suma. Notarás que el resultado de la comparación es diferente.

Listado 6: Redondeo previo vs. redondeo posterior

1
2
3
4
>>> round(0.3, 10) + round(0.3, 10) + round(0.3, 10) == round(0.9, 10)
False
>>> round(0.3 + 0.3 + 0.3, 10) == round(0.9, 10)
True

Módulos de Python para cálculos de punto flotante {#módulos de Python para cálculos de punto flotante}

Hay cuatro módulos populares que pueden ayudarlo a manejar correctamente los números de coma flotante. Esto incluye el módulo matemáticas, el módulo Numpy, el módulo decimal y el módulo fracciones.

El módulo matemáticas se centra en constantes matemáticas, operaciones de punto flotante y métodos trigonométricos. El módulo Numpy se describe a sí mismo como "el paquete fundamental para la computación científica", y es famoso por su variedad de métodos de matriz. El módulo ‘decimal’ cubre la aritmética de punto fijo decimal y punto flotante, y el módulo de ‘fracciones’ se ocupa específicamente de los números racionales.

Primero, tenemos que intentar mejorar el cálculo del Listado 1. Como muestra el Listado 7, después de haber importado el módulo math podemos acceder al método fsum() que acepta una lista de números de punto flotante. Para el primer cálculo no hay diferencia entre el método integrado sum() y el método fsum() del módulo math, pero para el segundo sí lo es, y devuelve el resultado correcto. suponer. La precisión depende del algoritmo IEEE 754 subyacente.

Listado 7: Cálculos de coma flotante con la ayuda del módulo math

1
2
3
4
5
6
7
8
9
>>> import math
>>> sum([0.1, 0.1, 0.1])
0.30000000000000004
>>> math.fsum([0.1, 0.1, 0.1])
0.30000000000000004
>>> sum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
0.9999999999999999
>>> math.fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
1.0

En segundo lugar, echemos un vistazo al módulo Numpy. Viene con el método around() que redondea los valores proporcionados como una matriz. Procesa los valores individuales de la misma manera que el método round() por defecto.

Para comparar valores, Numpy ofrece el método equal(). Similar a around(), acepta valores individuales así como listas de valores (los llamados vectores) para ser procesados. Listado 8 muestra una comparación de valores individuales así como valores redondeados. El comportamiento observado es bastante similar a los métodos mostrados anteriormente.

Listado 8: Comparando valores usando el método equal del módulo Numpy

1
2
3
4
5
6
7
>>> import numpy
>>> print (numpy.equal(0.3, 0.3))
True
>>> print (numpy.equal(0.3 + 0.3 + 0.3 , 0.9))
False
>>> print (numpy.equal(round(0.3 + 0.3 + 0.3) , round(0.9)))
True

La opción tres es el módulo decimal. Ofrece una representación decimal exacta y conserva los dígitos significativos. La precisión predeterminada es de 28 dígitos y puede cambiar este valor a un número tan grande como sea necesario para su problema. Listado 9 muestra cómo usar una precisión de 8 dígitos.

Listado 9: Creando números decimales usando el módulo decimal

1
2
3
4
5
6
>>> import decimal
>>> decimal.getcontext().prec = 8
>>> a = decimal.Decimal(1)
>>> b = decimal.Decimal(7)
>>> a / b
Decimal('0.14285714')

Ahora, la comparación de valores flotantes se vuelve mucho más fácil y conduce al resultado que estábamos buscando.

Listado 10: Comparaciones utilizando el módulo decimal

1
2
3
4
5
6
7
8
9
>>> import decimal
>>> decimal.getcontext().prec = 1
>>> a = decimal.Decimal(0.3)
>>> b = decimal.Decimal(0.3)
>>> c = decimal.Decimal(0.3)
>>> a + b + c
Decimal('0.9')
>>> a + b + c == decimal.Decimal('0.9')
True

El módulo decimal también viene con un método para redondear valores - cuantificar(). La estrategia de redondeo predeterminada está configurada para redondear la mitad a la par, y también se puede cambiar a un método diferente si es necesario. El Listado 11 ilustra el uso del método quantize(). Tenga en cuenta que el número de dígitos se especifica utilizando un valor decimal como parámetro.

Listado 11: Redondeando un valor usando quantize()

1
2
3
>>> d = decimal.Decimal(4.6187)
>>> d.quantize(decimal.Decimal("1.00"))
Decimal('4.62')

Por último, pero no menos importante, echaremos un vistazo al módulo fracciones. Este módulo le permite manejar valores de punto flotante como fracciones, por ejemplo, 0.3 como 3/10. Esto simplifica la comparación de valores de coma flotante y elimina por completo el redondeo de valores. Listado 12 muestra cómo usar el módulo de fracciones.

Listado 12: Almacenamiento y comparación de valores de punto flotante como fracciones

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> import fractions
>>> fractions.Fraction(4, 10)
Fraction(2, 5)
>>> fractions.Fraction(6, 18)
Fraction(1, 3)
>>> fractions.Fraction(125)
Fraction(125, 1)
>>> a = fractions.Fraction(6, 18)
>>> b = fractions.Fraction(1, 3)
>>> a == b
True

Además, los dos módulos decimal y fractions se pueden combinar, como se muestra en el siguiente ejemplo.

Listado 13: Trabajando con decimales y fracciones

1
2
3
4
5
6
7
8
>>> import fractions
>>> import decimal
>>> a = fractions.Fraction(1,10)
>>> b = fractions.Fraction(decimal.Decimal(0.1))
>>> a,b
(Fraction(1, 10), Fraction(3602879701896397, 36028797018963968))
>>> a == b
False

Conclusión

Almacenar y procesar valores de punto flotante correctamente es una misión y requiere mucha atención por parte de los programadores. Redondear los valores puede ayudar, pero asegúrese de verificar el orden correcto de redondeo y el método que utiliza. Esto es más importante cuando se desarrollan cosas como software financiero, por lo que querrá verificar las reglas de la ley local para redondear.

Python le brinda todas las herramientas necesarias y viene con "baterías incluidas". ¡Feliz pirateo!

Agradecimientos

El autor desea agradecer a Zoleka Hofmann por sus comentarios críticos mientras preparaba este artículo.

Licensed under CC BY-NC-SA 4.0