Como arredondar um número para números significativos em Python

148

Preciso arredondar um flutuador para ser exibido em uma interface do usuário. Por exemplo, para uma figura significativa:

1234 -> 1000

0,12 -> 0,1

0,012 -> 0,01

0,062 -> 0,06

6253 -> 6000

1999 -> 2000

Existe uma boa maneira de fazer isso usando a biblioteca Python, ou eu mesmo tenho que escrever?

Peter Graham
fonte
2
Você está apenas formatando a saída? Você está perguntando sobre isso? docs.python.org/library/stdtypes.html#string-formatting ou isso? docs.python.org/library/string.html#string-formatting
S.Lott
que saída você espera para 0,062 e 6253?
lamirap
O pacote com precisão agora faz isso. Minha resposta postada detalha como isso se aplica.
William Rusnack

Respostas:

146

Você pode usar números negativos para arredondar números inteiros:

>>> round(1234, -3)
1000.0

Portanto, se você precisar apenas do dígito mais 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

Você provavelmente terá que cuidar de transformar float em número inteiro se for maior que 1.

Evgeny
fonte
3
Esta é a solução correta. Usar log10é a única maneira adequada de determinar como arredondá-lo.
Wolph 5/08/10
73
round_to_n = lambda x, n: round (x, -int (floor (log10 (x))) + (n - 1))
Roy Hyunjin Han
28
Você deve usar log10(abs(x)), caso contrário, os números negativos irá falhar (E tratar x == 0separadamente, é claro)
Tobias KIENZLER
2
Eu criei um pacote que faz isso agora e provavelmente é mais fácil e mais robusto que este. Link de publicação , Link de repositório . Espero que isto ajude!
William Rusnack 23/05
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==0e x<0obrigado @RoyHyunjinHan e @TobiasKienzler. Não protegidos contra indefinido como math.inf, ou lixo como Nenhum etc
AJP
98

% g na formatação de string formatará um float arredondado para um número significativo de números. Às vezes, ele usa a notação científica 'e'; portanto, converta a string arredondada de volta em um float e, em seguida, através da formatação da string% 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
fonte
7
O requisito do OP era para 1999 ser formatado como '2000', não como '2000.0'. Não vejo uma maneira trivial de mudar seu método para conseguir isso.
Tim Martin
1
É exatamente o que eu sempre quis! onde você achou isso?
djhaskin987
12
Observe que o comportamento de% g nem sempre é correto. Em particular, sempre apara os zeros à direita, mesmo que sejam significativos. O número 1.23400 possui 6 dígitos significativos, mas "% .6g"% (1.23400) resultará em "1.234" incorreto. Mais detalhes nesta publicação do blog: randlet.com/blog/python-significant-figures-format
randlet
3
Assim como o método na resposta de Evgeny, isso falha ao arredondar corretamente 0.075para 0.08. Ele retorna em 0.07vez disso.
Gabriel
1
round_sig = lambda f,p: float(('%.' + str(p) + 'e') % f)permite ajustar o número de dígitos significativos!
Denizb
49

Se você quiser ter um número decimal diferente de 1 (caso contrário, o mesmo que o 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
fonte
8
round_sig (-0,0232) -> erro de domínio de matemática, você pode querer adicionar uma abs () lá;)
dgorissen
2
Assim como os métodos das respostas de Evgeny e Peter Graham, isso falha ao arredondar corretamente 0.075para 0.08. Ele retorna em 0.07vez disso.
Gabriel
3
Também falha para round_sig (0).
Yuval Atzmon
2
@ Gabriel Esse é um "recurso" embutido do python em execução no seu computador e se manifesta no comportamento da função round. docs.python.org/2/tutorial/floatingpoint.html#tut-fp-issues
Iniciante C
1
@ Gabriel Adicionei uma resposta que explica por que você deveria esperar obter 0,7 de volta do arredondamento "0,075"! Veja 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 solução é diferente de todas as outras porque:

  1. -lo exatamente resolve a questão OP
  2. ele não precisa de qualquer pacote adicional
  3. ele não precisa de nenhuma função auxiliar definida pelo usuário ou operação matemática

Para um número arbitrário nde números significativos, você pode usar:

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

Teste:

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 : com esta solução, não é possível adaptar dinamicamente o número de figuras significativas a partir da entrada, porque não existe uma maneira padrão de distinguir números com diferentes números de zeros à direita ( 3.14 == 3.1400). Se você precisar fazer isso, serão necessárias funções não padrão, como as fornecidas no pacote de precisão .

Falken
fonte
FYI: Encontrei esta solução independentemente do eddygeek enquanto tentava resolver o mesmo problema em um dos meus códigos. Agora eu percebo que minha solução é, obviamente, quase idêntica à dele (eu apenas notei a saída incorreta e não me preocupei em ler o código, meu erro). Provavelmente, um breve comentário abaixo da resposta seria suficiente em vez de uma nova resposta ... A única diferença (chave) é o uso duplo do :gformatador que preserva números inteiros.
Falken
Uau, sua resposta precisa ser realmente lida de cima para baixo;) Esse truque de elenco duplo é sujo, mas limpo. (Observe que 1999 formatado como 2000.0 sugere 5 dígitos significativos, portanto é necessário {:g}repetir.) Em geral, números inteiros com zeros à direita são ambíguos em relação a números significativos, a menos que alguma técnica (como overline acima da última significativa) seja usada.
Tomasz Gandor 6/04
8

Eu criei o pacote com precisão que faz o que você deseja. Ele permite que você forneça números mais ou menos significativos.

Ele também gera notação padrão, científica e de engenharia com um número especificado de números significativos.

Na resposta aceita, há a linha

>>> round_to_1(1234243)
1000000.0

Na verdade, isso especifica 8 sig figs. Para o número 1234243, minha biblioteca exibe apenas uma figura 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'

Ele também arredondará o último número significativo e poderá escolher automaticamente qual notação usar se uma notação não for especificada:

>>> to_precision(599, 2)
'600'
>>> to_precision(1164, 2)
'1.2e3'
William Rusnack
fonte
Agora eu estou procurando o mesmo, mas aplicada a uma pandas df
mhoff
@ mhoff você provavelmente pode usar o mapa de pandas com um lambda. lambda x: to_precision(x, 2)
William Rusnack 7/02/19
Adicione isso a (PyPI) [ pypi.org/] . Não existe nada assim lá, até onde eu sei.
Morgoth
este é um grande pacote, mas eu acho que a maioria dos recursos estão agora no módulo sigfig
hiperativa
1
tem um erro: std_notation (9.999999999999999e-05, 3) fornece: '0.00010', que possui apenas 2 dígitos significativos
Boris Mulder
5

Para arredondar um número inteiro para 1 figura significativa, a idéia básica é convertê-lo em um ponto flutuante com 1 dígito antes do ponto e arredondá-lo e depois convertê-lo novamente em seu tamanho inteiro original.

Para fazer isso, precisamos conhecer a maior potência de 10 a menos que o número inteiro. Podemos usar o piso da função log 10 para isso.

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
fonte
1
Mais um para a solução que funciona sem arredondar o python (.., dígitos) e sem amarras!
9788 Steve Rogers
5

Para responder diretamente à pergunta, aqui está minha versão usando a nomeação da função 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)

Meu principal motivo para postar esta resposta são os comentários reclamando que "0,075" arredonda para 0,07 em vez de 0,08. Isso se deve, como apontado por "Iniciante C", a uma combinação de aritmética de ponto flutuante com precisão finita e uma representação de base 2 . O número mais próximo a 0,075 que pode realmente ser representado é um pouco menor; portanto, o arredondamento sai de maneira diferente do que você pode esperar ingenuamente.

Observe também que isso se aplica a qualquer uso de aritmética de ponto flutuante não decimal, por exemplo, C e Java têm o mesmo problema.

Para mostrar com mais detalhes, pedimos ao Python que formate o número no formato "hexadecimal":

0.075.hex()

o que nos dá: 0x1.3333333333333p-4. A razão para fazer isso é que a representação decimal normal geralmente envolve arredondamentos e, portanto, não é como o computador "vê" o número. Se você não está acostumado a este formato, um par de referências úteis são os docs Python eo padrão C .

Para mostrar como esses números funcionam um pouco, podemos voltar ao nosso ponto de partida fazendo:

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

que deve ser impresso 0.075. 16**13é porque existem 13 dígitos hexadecimais após o ponto decimal e 2**-4porque os expoentes hexadecimais são base-2.

Agora, temos uma idéia de como os carros alegóricos são representados. Podemos usar o decimalmódulo para fornecer mais precisão, mostrando o que está acontecendo:

from decimal import Decimal

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

dando: 0.07499999999999999722444243844e esperançosamente explicando por que round(0.075, 2)avalia0.07

Sam Mason
fonte
1
Esta é uma ótima explicação de por que 0,075 é arredondado para 0,07 no nível do código , mas nós (nas ciências físicas) fomos ensinados a sempre arredondar para não arredondar para baixo. Portanto, o comportamento esperado é ter 0,08 como resultado, apesar dos problemas de precisão de ponto flutuante.
Gabriel
1
Não sei ao certo onde está sua confusão: quando você digita 0,075, está digitando ~ 0,07499 (como acima), que arredonda de acordo com as regras normais de matemática. se você estivesse usando um tipo de dados (como ponto flutuante decimal ), que poderia representar 0,075, então ele deve de fato rodada a 0,08
Sam Mason
Eu não estou confuso. Quando insiro 0,075, estou inserindo 0,075. O que quer que aconteça na matemática de ponto flutuante dentro do código, eu não me importo.
Gabriel
@ Gabriel: E se você tivesse entrado deliberadamente 0.074999999999999999, o que esperaria obter nesse caso?
22619 Mark
@ MarkDickinson isso depende. Um número significativo: 0,07, dois: 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

Espero ter o melhor de todas as respostas acima (menos poder colocá-lo como uma linha lambda;)). Ainda não explorou, sinta-se à vontade para editar esta resposta:

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

Modifiquei a solução da indgar para lidar com números negativos e números pequenos (incluindo zero).

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
fonte
Por que não apenas testar se x == 0? Se você gosta de uma frase, apenas return 0 if x==0 else round(...).
pjvandehaar
2
@pjvandehaar, você está correto no caso geral e eu deveria ter colocado isso. Além disso, para os cálculos numéricos que preciso executar, ocasionalmente obtemos números como 1e-15. Em nossa aplicação, queremos que uma comparação de dois números pequenos (um dos quais possa ser zero) seja considerada igual. Além disso, algumas pessoas desejam arredondar números pequenos (pode ser 1e-9, 1e-15 ou até 1e-300) para zero.
ryan281
1
Interessante. Obrigado por explicar isso. Nesse caso, eu realmente gosto desta solução.
precisa saber é o seguinte
@Orgoth Este é um problema interessante e difícil. Como você apontou, o valor impresso não mostra os três dígitos significativos, mas o valor está correto (por exemplo 0.970 == 0.97). Eu acho que você poderia usar algumas das outras soluções de impressão, como f'{round_sig(0.9701, sig=3):0.3f}'se você quiser imprimir o zero.
ryan281
3

Se você deseja arredondar sem envolver cordas, o link que encontrei enterrado nos comentários acima:

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

me parece melhor. Então, quando você imprime com qualquer descritor de formatação de sequência, obtém uma saída razoável e pode usar a representação numérica para outros fins de cálculo.

O código no link é de três linhas: def, doc e return. Ele tem um bug: você precisa verificar se há logaritmos explosivos. Isso é fácil. Compare a entrada com sys.float_info.min. A solução completa é:

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

Ele funciona para qualquer valor numérico escalar e n pode ser um floatse você precisar alterar a resposta por algum motivo. Você pode realmente empurrar o limite para:

sys.float_info.min*sys.float_info.epsilon

sem provocar um erro, se por algum motivo você estiver trabalhando com valores minúsculos.

ficando com sono
fonte
2

Não consigo pensar em nada que possa resolver isso imediatamente. Mas é razoavelmente bem tratado para números de ponto flutuante.

>>> round(1.2322, 2)
1.23

Inteiros são mais complicados. Eles não são armazenados como base 10 na memória, portanto lugares significativos não são uma coisa natural a se fazer. É bastante trivial de implementar uma vez que eles são uma string.

Ou para números inteiros:

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

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

Se você deseja criar uma função que lida com qualquer número, minha preferência seria convertê-los em strings e procurar uma casa decimal para decidir o que fazer:

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

Outra opção é verificar o tipo. Isso será muito menos flexível e provavelmente não funcionará bem com outros números, como Decimalobjetos:

>>> def roundall2(n, sigfigs):
...   if type(n) is int: return intround(n, sigfigs)
...   else: return round(n, sigfigs)
Tim McNamara
fonte
Apenas mexer com cordas não arredondará os números. 1999 arredondado para 1 figura significativa é de 2000, não 1000.
Peter Graham
Há uma boa discussão sobre esse problema arquivado em ActiveState code.activestate.com/lists/python-tutor/70739
Tim McNamara
2

A resposta postada foi a melhor disponível quando fornecida, mas possui várias limitações e não produz números significativos tecnicamente corretos.

numpy.format_float_positional suporta diretamente o comportamento desejado. O fragmento a seguir retorna o float xformatado para 4 números significativos, com a notação científica suprimida.

import numpy as np
x=12345.6
np.format_float_positional(x, precision=4, unique=False, fractional=False, trim='k')
> 12340.
Outono
fonte
A documentação (movida para numpy.org/doc/stable/reference/generated/… ) afirma que esta função implementa o algoritmo Dragon4 (de Steele & White 1990, dl.acm.org/doi/pdf/10.1145/93542.93559 ). Produz resultados irritantes, por exemplo 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'). Eu não verifiquei o próprio Dragon4.
Rainald62
0

Encontrei isso também, mas precisava de controle sobre o tipo de arredondamento. Assim, escrevi uma função rápida (veja o código abaixo) que pode levar em consideração o valor, o tipo de arredondamento e os dígitos significativos desejados.

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

Usando a formatação de novo estilo do python 2.6+ (como% -style está obsoleto):

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

No python 2.7+, você pode omitir os 0s principais .

eddygeek
fonte
Com qual versão do python? Anaconda, Inc. | (padrão, 13 de outubro de 2017, 12:02:49) tem o mesmo problema de arredondamento antigo. O formato "{0}". (Float ("{0: .1g}". Formato (0,075))) produz '0,07', não '0,08'
Don Mclachlan
@DonMclachlan Eu adicionei uma explicação de por que isso é esperado em stackoverflow.com/a/56974893/1358308
Sam Mason
0

Esta função executa uma rodada normal se o número for maior que 10 ** (- posições_ decimais); caso contrário, adiciona mais casas decimais até que o número de casas decimais significativas seja atingido:

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 que ajude.

Ettore Galli
fonte
0

https://stackoverflow.com/users/1391441/gabriel , o seguinte endereço aborda sua preocupação com rnd (0,075, 1)? Advertência: retorna valor como um float

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

Isso retorna uma string, para que resultados sem partes fracionárias e pequenos valores que, de outra forma, aparecessem na notação E sejam mostrados corretamente:

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

Dada uma pergunta tão bem respondida, por que não adicionar outra

Isso combina com minha estética um pouco melhor, embora muitos dos itens acima sejam comparáveis

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)

Isso funciona para números individuais e matrizes numpy e deve funcionar bem para números negativos.

Há uma etapa adicional que poderíamos adicionar - np.round () retorna um número decimal, mesmo que arredondado seja um número inteiro (por exemplo, para significantFigures = 2, podemos esperar voltar -460, mas obtemos -460,0). Podemos adicionar esta etapa para corrigir isso:

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

Infelizmente, esta etapa final não funcionará para uma série de números - deixarei isso para você, querido leitor, para descobrir se você precisa.

zéfiro
fonte
0

O pacote / biblioteca sigfig cobre isso. Após a instalação, você pode fazer o seguinte:

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