Cómo redondear un número a cifras significativas en Python

148

Necesito redondear un flotador para que se muestre en una interfaz de usuario. Por ejemplo, a una cifra significativa:

1234 -> 1000

0.12 -> 0.1

0.012 -> 0.01

0,062 -> 0,06

6253 -> 6000

1999 -> 2000

¿Hay una buena manera de hacer esto usando la biblioteca Python, o tengo que escribirlo yo mismo?

Peter Graham
fuente
2
¿Estás formateando la salida? ¿Estás preguntando sobre esto? docs.python.org/library/stdtypes.html#string-formatting o esto? docs.python.org/library/string.html#string-formatting
S.Lott
¿Qué salida esperas para 0.062 y 6253?
lamirap
El paquete a precisión ahora hace esto. Mi respuesta publicada detalla cómo se aplica esto.
William Rusnack

Respuestas:

146

Puede usar números negativos para redondear enteros:

>>> round(1234, -3)
1000.0

Por lo tanto, si solo necesita el dígito más significativo:

>>> from math import log10, floor
>>> def round_to_1(x):
...   return round(x, -int(floor(log10(abs(x)))))
... 
>>> round_to_1(0.0232)
0.02
>>> round_to_1(1234243)
1000000.0
>>> round_to_1(13)
10.0
>>> round_to_1(4)
4.0
>>> round_to_1(19)
20.0

Probablemente tenga que encargarse de convertir el flotador en entero si es mayor que 1.

Evgeny
fuente
3
Esta es la solución correcta. Usar log10es la única forma adecuada de determinar cómo redondearlo.
Wolph
73
round_to_n = lambda x, n: round (x, -int (floor (log10 (x))) + (n - 1))
Roy Hyunjin Han
28
Debe usar log10(abs(x)), de lo contrario los números negativos fallarán (y tratarán por x == 0separado, por supuesto)
Tobias Kienzler
2
He creado un paquete que hace esto ahora y probablemente sea más fácil y más robusto que este. Enlace de publicación , enlace de repositorio . ¡Espero que esto ayude!
William Rusnack
2
round_to_n = lambda x, n: x if x == 0 else round(x, -int(math.floor(math.log10(abs(x)))) + (n - 1))protege x==0y x<0gracias @RoyHyunjinHan y @TobiasKienzler. No protege contra indefinidos como math.inf o basura como None, etc.
AJP
98

% g en el formato de cadena formateará un flotante redondeado a un número de cifras significativas. A veces usará la notación científica 'e', ​​por lo tanto, vuelva a convertir la cadena redondeada en flotante mediante el formato de cadena% s.

>>> '%s' % float('%.1g' % 1234)
'1000'
>>> '%s' % float('%.1g' % 0.12)
'0.1'
>>> '%s' % float('%.1g' % 0.012)
'0.01'
>>> '%s' % float('%.1g' % 0.062)
'0.06'
>>> '%s' % float('%.1g' % 6253)
'6000.0'
>>> '%s' % float('%.1g' % 1999)
'2000.0'
Peter Graham
fuente
77
El requisito del PO era que 1999 se formateara como '2000', no como '2000.0'. No puedo ver una manera trivial de cambiar su método para lograr esto.
Tim Martin
1
¡Es justo lo que siempre quise! donde encontraste esto
djhaskin987
12
Tenga en cuenta que el comportamiento de% g no siempre es correcto. En particular, siempre recorta los ceros finales, incluso si son significativos. El número 1.23400 tiene 6 dígitos significativos, pero "% .6g"% (1.23400) dará como resultado "1.234" que es incorrecto. Más detalles en esta publicación de blog: randlet.com/blog/python-significant-figures-format
randlet
3
Al igual que el método de la respuesta de Evgeny, esto no funciona correctamente a ronda 0.075a 0.08. Regresa en su 0.07lugar.
Gabriel
1
round_sig = lambda f,p: float(('%.' + str(p) + 'e') % f)le permite ajustar la cantidad de dígitos significativos!
denizb
49

Si desea tener otro decimal distinto de 1 (de lo contrario, lo mismo que Evgeny):

>>> from math import log10, floor
>>> def round_sig(x, sig=2):
...   return round(x, sig-int(floor(log10(abs(x))))-1)
... 
>>> round_sig(0.0232)
0.023
>>> round_sig(0.0232, 1)
0.02
>>> round_sig(1234243, 3)
1230000.0
indgar
fuente
8
round_sig (-0.0232) -> error de dominio matemático, es posible que desee agregar un abs () allí;)
dgorissen
2
Al igual que los métodos de respuestas de Evgeny y Peter Graham, esto no funciona correctamente a ronda 0.075a 0.08. Regresa en su 0.07lugar.
Gabriel
3
También falla para round_sig (0).
Yuval Atzmon
2
@Gabriel Esa es una "característica" incorporada de Python que se ejecuta en su computadora, y se manifiesta en su comportamiento de la función round. docs.python.org/2/tutorial/floatingpoint.html#tut-fp-issues
Novato C
1
@Gabriel ¡Agregué una respuesta que explica por qué debería esperar obtener 0.7 al redondear "0.075"! ver stackoverflow.com/a/56974893/1358308
Sam Mason
30
f'{float(f"{i:.1g}"):g}'
# Or with Python <3.6,
'{:g}'.format(float('{:.1g}'.format(i)))

Esta solución es diferente de todas las demás porque:

  1. es exactamente resuelve la cuestión OP
  2. no necesita ningún paquete adicional
  3. no necesita ningún definida por el usuario función auxiliar o operación matemática

Para un número arbitrario nde cifras significativas, puede usar:

print('{:g}'.format(float('{:.{p}g}'.format(i, p=n))))

Prueba:

a = [1234, 0.12, 0.012, 0.062, 6253, 1999, -3.14, 0., -48.01, 0.75]
b = ['{:g}'.format(float('{:.1g}'.format(i))) for i in a]
# b == ['1000', '0.1', '0.01', '0.06', '6000', '2000', '-3', '0', '-50', '0.8']

Nota : con esta solución, no es posible adaptar el número de cifras significativas dinámicamente desde la entrada porque no hay una forma estándar de distinguir números con diferentes números de ceros finales ( 3.14 == 3.1400). Si necesita hacerlo, entonces se necesitan funciones no estándar como las proporcionadas en el paquete de precisión .

Falken
fuente
FYI: Encontré esta solución independientemente de eddygeek mientras intentaba resolver el mismo problema en uno de mis códigos. Ahora me doy cuenta de que mi solución es, obviamente, casi idéntica a la suya (acabo de notar la salida errónea y no me molesté en leer el código, mi error). Probablemente un breve comentario debajo de su respuesta hubiera sido suficiente en lugar de una nueva respuesta ... La única diferencia (clave) es el doble uso del :gformateador que preserva los enteros.
Falken
Wow, tu respuesta necesita ser realmente leída de arriba a abajo;) Este truco de doble reparto es sucio, pero ordenado. (Tenga en cuenta que 1999 formateado como 2000.0 sugiere 5 dígitos significativos, por lo que tiene que pasar de {:g}nuevo.) En general, los enteros con ceros finales son ambiguos con respecto a las cifras significativas, a menos que se utilice alguna técnica (como el renglón anterior al último significativo).
Tomasz Gandor
8

He creado el paquete con precisión que hace lo que quieres. Le permite dar a sus números cifras más o menos significativas.

También genera notación estándar, científica y de ingeniería con un número específico de cifras significativas.

En la respuesta aceptada está la línea.

>>> round_to_1(1234243)
1000000.0

Eso realmente especifica 8 higos sig. Para el número 1234243, mi biblioteca solo muestra una cifra significativa:

>>> from to_precision import to_precision
>>> to_precision(1234243, 1, 'std')
'1000000'
>>> to_precision(1234243, 1, 'sci')
'1e6'
>>> to_precision(1234243, 1, 'eng')
'1e6'

También redondeará la última cifra significativa y puede elegir automáticamente qué notación usar si no se especifica una notación:

>>> to_precision(599, 2)
'600'
>>> to_precision(1164, 2)
'1.2e3'
William Rusnack
fuente
Ahora estoy buscando lo mismo pero aplicado a un pandas df
mhoff
@mhoff probablemente puedas usar el mapa de pandas con una lambda. lambda x: to_precision(x, 2)
William Rusnack
Agregue esto a (PyPI) [ pypi.org/] . No hay nada como esto que exista allí, por lo que puedo decir.
Morgoth
este es un gran paquete, pero creo que la mayoría de las características están ahora en el módulo
sigfig
1
tiene un error: std_notation (9.999999999999999e-05, 3) da: '0.00010' que son solo 2 dígitos significativos
Boris Mulder
5

Para redondear un número entero a 1 cifra significativa, la idea básica es convertirlo a un punto flotante con 1 dígito antes del punto y redondearlo, luego convertirlo nuevamente a su tamaño entero original.

Para hacer esto, necesitamos saber la potencia más grande de 10 menos que el número entero. Podemos usar el piso de la función log 10 para esto.

from math import log10, floor
def round_int(i,places):
    if i == 0:
        return 0
    isign = i/abs(i)
    i = abs(i)
    if i < 1:
        return 0
    max10exp = floor(log10(i))
    if max10exp+1 < places:
        return i
    sig10pow = 10**(max10exp-places+1)
    floated = i*1.0/sig10pow
    defloated = round(floated)*sig10pow
    return int(defloated*isign)
Cris Stringfellow
fuente
1
¡Más uno para la solución que funciona sin la ronda de Python (.., dígitos) y sin condiciones!
Steve Rogers
5

Para responder directamente a la pregunta, aquí está mi versión usando nombres de la función R :

import math

def signif(x, digits=6):
    if x == 0 or not math.isfinite(x):
        return x
    digits -= math.ceil(math.log10(abs(x)))
    return round(x, digits)

Mi razón principal para publicar esta respuesta son los comentarios quejándose de que "0.075" se redondea a 0.07 en lugar de 0.08. Esto se debe, como lo señaló "Novato C", a una combinación de aritmética de coma flotante que tiene precisión finita y una representación de base 2 . El número más cercano a 0.075 que puede representarse realmente es un poco más pequeño, por lo tanto, el redondeo sale de manera diferente de lo que cabría esperar ingenuamente.

También tenga en cuenta que esto se aplica a cualquier uso de aritmética de coma flotante no decimal, por ejemplo, C y Java tienen el mismo problema.

Para mostrar con más detalle, le pedimos a Python que formatee el número en formato "hexadecimal":

0.075.hex()

lo que nos da: 0x1.3333333333333p-4. La razón para hacer esto es que la representación decimal normal a menudo implica redondeo y, por lo tanto, no es cómo la computadora realmente "ve" el número. Si no está acostumbrado a este formato, un par de referencias útiles son las documentación de Python y el C estándar .

Para mostrar cómo funcionan un poco estos números, podemos volver a nuestro punto de partida haciendo lo siguiente:

0x13333333333333 / 16**13 * 2**-4

que debería imprimirse 0.075. 16**13es porque hay 13 dígitos hexadecimales después del punto decimal, y 2**-4es porque los exponentes hexadecimales son base-2.

Ahora tenemos una idea de cómo se representan los flotadores, podemos usar el decimalmódulo para darnos más precisión, mostrándonos lo que está sucediendo:

from decimal import Decimal

Decimal(0x13333333333333) / 16**13 / 2**4

dando: 0.07499999999999999722444243844y con suerte explicando por qué round(0.075, 2)evalúa a0.07

Sam Mason
fuente
1
Esta es una gran explicación de por qué 0.075 se redondea a 0.07 en el nivel de código , pero a nosotros (en las ciencias físicas) se nos ha enseñado a redondear siempre hacia abajo y no hacia abajo. Por lo tanto, el comportamiento esperado es tener 0.08 como resultado, a pesar de los problemas de precisión de coma flotante.
Gabriel
1
No estoy seguro de dónde está su confusión: cuando ingresa 0.075 en realidad está ingresando ~ 0.07499 (como se indicó anteriormente), que se redondea de acuerdo con las reglas matemáticas normales. si estaba usando un tipo de datos (como coma flotante decimal ) que podría representar 0.075, entonces debería redondear a 0.08
Sam Mason
No estoy confundido Cuando ingreso 0.075 en realidad estoy ingresando 0.075. Pase lo que pase en las matemáticas de coma flotante dentro del código, no me importa.
Gabriel
@Gabriel: Y si hubiera entrado deliberadamente 0.074999999999999999, lo que se puede esperar para conseguir en ese caso?
Mark Dickinson
@ MarkDickinson eso depende. Una cifra significativa: 0.07, dos: 0.075.
Gabriel
4
def round_to_n(x, n):
    if not x: return 0
    power = -int(math.floor(math.log10(abs(x)))) + (n - 1)
    factor = (10 ** power)
    return round(x * factor) / factor

round_to_n(0.075, 1)      # 0.08
round_to_n(0, 1)          # 0
round_to_n(-1e15 - 1, 16) # 1000000000000001.0

Con suerte, tomando la mejor de todas las respuestas anteriores (menos poder ponerlo como una lambda de una línea;)). Todavía no lo he explorado, siéntase libre de editar esta respuesta:

round_to_n(1e15 + 1, 11)  # 999999999999999.9
AJP
fuente
4

Modifiqué la solución de indgar para manejar números negativos y números pequeños (incluido cero).

from math import log10, floor
def round_sig(x, sig=6, small_value=1.0e-9):
    return round(x, sig - int(floor(log10(max(abs(x), abs(small_value))))) - 1)
ryan281
fuente
¿Por qué no solo probar si x == 0? Si amas una frase, solo return 0 if x==0 else round(...).
pjvandehaar
2
@pjvandehaar, tiene razón para el caso general y debería haberlo puesto. Además, para los cálculos numéricos que necesito realizar, ocasionalmente obtenemos números como 1e-15. En nuestra aplicación, queremos que una comparación de dos números pequeños (uno de los cuales podría ser cero) se considere igual. Además, algunas personas quieren redondear números pequeños (podría ser 1e-9, 1e-15 o incluso 1e-300) a cero.
ryan281
1
Interesante. Gracias por explicar eso. En ese caso, me gusta mucho esta solución.
pjvandehaar
@ Morgan Este es un problema interesante y difícil. Como señaló, el valor impreso no muestra los 3 dígitos significativos, pero el valor es correcto (por ejemplo 0.970 == 0.97). Creo que podría usar algunas de las otras soluciones de impresión, como f'{round_sig(0.9701, sig=3):0.3f}'si quisiera imprimir el cero.
ryan281
3

Si desea redondear sin involucrar cadenas, el enlace que encontré enterrado en los comentarios anteriores:

http://code.activestate.com/lists/python-tutor/70739/

me parece lo mejor. Luego, cuando imprime con cualquier descriptor de formato de cadena, obtiene un resultado razonable y puede usar la representación numérica para otros fines de cálculo.

El código en el enlace tiene tres líneas: def, doc y return. Tiene un error: debe verificar la existencia de logaritmos explosivos. Eso es fácil. Compare la entrada a sys.float_info.min. La solución completa es:

import sys,math

def tidy(x, n):
"""Return 'x' rounded to 'n' significant digits."""
y=abs(x)
if y <= sys.float_info.min: return 0.0
return round( x, int( n-math.ceil(math.log10(y)) ) )

Funciona para cualquier valor numérico escalar, y n puede ser un floatsi necesita cambiar la respuesta por alguna razón. En realidad, puede empujar el límite para:

sys.float_info.min*sys.float_info.epsilon

sin provocar un error, si por alguna razón está trabajando con valores minúsculos.

me está dando sueño
fuente
2

No puedo pensar en nada que pueda manejar esto fuera de la caja. Pero está bastante bien manejado para números de coma flotante.

>>> round(1.2322, 2)
1.23

Los enteros son más complicados. No se almacenan como base 10 en la memoria, por lo que los lugares importantes no son algo natural. Sin embargo, es bastante trivial implementar una vez que son una cadena.

O para enteros:

>>> def intround(n, sigfigs):
...   n = str(n)
...   return n[:sigfigs] + ('0' * (len(n)-(sigfigs)))

>>> intround(1234, 1)
'1000'
>>> intround(1234, 2)

Si desea crear una función que maneje cualquier número, mi preferencia sería convertirlos en cadenas y buscar un lugar decimal para decidir qué hacer:

>>> def roundall1(n, sigfigs):
...   n = str(n)
...   try:
...     sigfigs = n.index('.')
...   except ValueError:
...     pass
...   return intround(n, sigfigs)

Otra opción es verificar el tipo. Esto será mucho menos flexible y probablemente no jugará bien con otros números como los Decimalobjetos:

>>> def roundall2(n, sigfigs):
...   if type(n) is int: return intround(n, sigfigs)
...   else: return round(n, sigfigs)
Tim McNamara
fuente
Solo jugar con las cuerdas no redondeará los números. 1999 redondeado a 1 cifra significativa es 2000, no 1000.
Peter Graham
Hay una buena discusión sobre este problema archivado en ActiveState code.activestate.com/lists/python-tutor/70739
Tim McNamara
2

La respuesta publicada fue la mejor disponible cuando se dio, pero tiene una serie de limitaciones y no produce cifras significativas técnicamente correctas.

numpy.format_float_positional admite el comportamiento deseado directamente. El siguiente fragmento devuelve el flotador xformateado a 4 cifras significativas, con la notación científica suprimida.

import numpy as np
x=12345.6
np.format_float_positional(x, precision=4, unique=False, fractional=False, trim='k')
> 12340.
Otoño
fuente
La documentación (movida a numpy.org/doc/stable/reference/generated/… ) establece que esta función implementa el algoritmo Dragon4 (de Steele & White 1990, dl.acm.org/doi/pdf/10.1145/93542.93559 ). Produce resultados molestos, por ejemplo print(*[''.join([np.format_float_positional(.01*a*n,precision=2,unique=False,fractional=False,trim='k',pad_right=5) for a in [.99, .999, 1.001]]) for n in [8,9,10,11,12,19,20,21]],sep='\n'). No revisé Dragon4 en sí mismo.
Rainald62
0

Me encontré con esto también, pero necesitaba control sobre el tipo de redondeo. Por lo tanto, escribí una función rápida (vea el código a continuación) que puede tener en cuenta el valor, el tipo de redondeo y los dígitos significativos deseados.

import decimal
from math import log10, floor

def myrounding(value , roundstyle='ROUND_HALF_UP',sig = 3):
    roundstyles = [ 'ROUND_05UP','ROUND_DOWN','ROUND_HALF_DOWN','ROUND_HALF_UP','ROUND_CEILING','ROUND_FLOOR','ROUND_HALF_EVEN','ROUND_UP']

    power =  -1 * floor(log10(abs(value)))
    value = '{0:f}'.format(value) #format value to string to prevent float conversion issues
    divided = Decimal(value) * (Decimal('10.0')**power) 
    roundto = Decimal('10.0')**(-sig+1)
    if roundstyle not in roundstyles:
        print('roundstyle must be in list:', roundstyles) ## Could thrown an exception here if you want.
    return_val = decimal.Decimal(divided).quantize(roundto,rounding=roundstyle)*(decimal.Decimal(10.0)**-power)
    nozero = ('{0:f}'.format(return_val)).rstrip('0').rstrip('.') # strips out trailing 0 and .
    return decimal.Decimal(nozero)


for x in list(map(float, '-1.234 1.2345 0.03 -90.25 90.34543 9123.3 111'.split())):
    print (x, 'rounded UP: ',myrounding(x,'ROUND_UP',3))
    print (x, 'rounded normal: ',myrounding(x,sig=3))
drew.ray
fuente
0

Usando el formato de estilo nuevo de Python 2.6+ (ya que el estilo% está en desuso):

>>> "{0}".format(float("{0:.1g}".format(1216)))
'1000.0'
>>> "{0}".format(float("{0:.1g}".format(0.00356)))
'0.004'

En python 2.7+ puedes omitir los 0s principales .

eddygeek
fuente
¿Con qué versión de python? Python 3.6.3 | Anaconda, Inc. | (predeterminado, 13 de octubre de 2017, 12:02:49) tiene el mismo viejo problema de redondeo. "{0}". Format (float ("{0: .1g}". Format (0.075))) produce '0.07', no '0.08'
Don Mclachlan
@DonMclachlan He agregado una explicación de por qué esto se espera en stackoverflow.com/a/56974893/1358308
Sam Mason
0

Esta función realiza una ronda normal si el número es mayor que 10 ** (- decimales_posiciones), de lo contrario agrega más decimales hasta que se alcanza el número de posiciones decimales significativas:

def smart_round(x, decimal_positions):
    dp = - int(math.log10(abs(x))) if x != 0.0 else int(0)
    return round(float(x), decimal_positions + dp if dp > 0 else decimal_positions)

Espero eso ayude.

Ettore Galli
fuente
0

https://stackoverflow.com/users/1391441/gabriel , ¿aborda lo siguiente su preocupación sobre rnd (.075, 1)? Advertencia: devuelve el valor como flotante

def round_to_n(x, n):
    fmt = '{:1.' + str(n) + 'e}'    # gives 1.n figures
    p = fmt.format(x).split('e')    # get mantissa and exponent
                                    # round "extra" figure off mantissa
    p[0] = str(round(float(p[0]) * 10**(n-1)) / 10**(n-1))
    return float(p[0] + 'e' + p[1]) # convert str to float

>>> round_to_n(750, 2)
750.0
>>> round_to_n(750, 1)
800.0
>>> round_to_n(.0750, 2)
0.075
>>> round_to_n(.0750, 1)
0.08
>>> math.pi
3.141592653589793
>>> round_to_n(math.pi, 7)
3.141593
Don mclachlan
fuente
0

Esto devuelve una cadena, de modo que los resultados sin partes fraccionarias, y los valores pequeños que de otro modo aparecerían en la notación E se muestran correctamente:

def sigfig(x, num_sigfig):
    num_decplace = num_sigfig - int(math.floor(math.log10(abs(x)))) - 1
    return '%.*f' % (num_decplace, round(x, num_decplace))
Gnubie
fuente
0

Ante una pregunta tan bien respondida, ¿por qué no agregar otra?

Esto se adapta un poco mejor a mi estética, aunque muchos de los anteriores son comparables

import numpy as np

number=-456.789
significantFigures=4

roundingFactor=significantFigures - int(np.floor(np.log10(np.abs(number)))) - 1
rounded=np.round(number, roundingFactor)

string=rounded.astype(str)

print(string)

Esto funciona para números individuales y matrices numpy, y debería funcionar bien para números negativos.

Hay un paso adicional que podríamos agregar: np.round () devuelve un número decimal incluso si redondeado es un entero (es decir, para cifras significativas = 2 podríamos esperar obtener -460, pero en su lugar obtenemos -460.0). Podemos agregar este paso para corregir eso:

if roundingFactor<=0:
    rounded=rounded.astype(int)

Desafortunadamente, este paso final no funcionará para una variedad de números; se lo dejaré a usted, querido lector, para que lo descubra si lo necesita.

céfiro
fuente
0

El paquete / biblioteca sigfig cubre esto. Después de la instalación , puede hacer lo siguiente:

>>> from sigfig import round
>>> round(1234, 1)
1000
>>> round(0.12, 1)
0.1
>>> round(0.012, 1)
0.01
>>> round(0.062, 1)
0.06
>>> round(6253, 1)
6000
>>> round(1999, 1)
2000
Hiperactivo
fuente
0
import math

  def sig_dig(x, n_sig_dig):
      num_of_digits = len(str(x).replace(".", ""))
      if n_sig_dig >= num_of_digits:
          return x
      n = math.floor(math.log10(x) + 1 - n_sig_dig)
      result = round(10 ** -n * x) * 10 ** n
      return float(str(result)[: n_sig_dig + 1])


    >>> sig_dig(1234243, 3)
    >>> sig_dig(243.3576, 5)

        1230.0
        243.36
LetzerWille
fuente