Bem representando um número de ponto flutuante no Python
-
27-09-2019 - |
Pergunta
Quero representar um número de ponto flutuante como uma corda arredondada para algum número de dígitos significativos e nunca usando o formato exponencial. Essencialmente, quero exibir qualquer número de ponto flutuante e verifique se "parece bom".
Existem várias partes neste problema:
- Preciso especificar o número de dígitos significativos.
- O número de dígitos significativos precisa ser variável, o que não pode ser feito com o Operador de formatação da string. [Editar] Fui corrigido; O operador de formatação da string pode fazer isso.
- Eu preciso que seja arredondado da maneira que uma pessoa esperaria, não algo como 1.999999999999
Eu descobri uma maneira de fazer isso, embora pareça uma rodada de trabalho e não seja perfeita. (A precisão máxima é de 15 dígitos significativos.)
>>> def f(number, sigfig):
return ("%.15f" % (round(number, int(-1 * floor(log10(number)) + (sigfig - 1))))).rstrip("0").rstrip(".")
>>> print f(0.1, 1)
0.1
>>> print f(0.0000000000368568, 2)
0.000000000037
>>> print f(756867, 3)
757000
Existe uma maneira melhor de fazer isso? Por que o Python não tem uma função interna para isso?
Solução
Parece que não há truque de formatação de cordas embutido que permita (1) imprimir carros alegóricos cujo primeiro dígito significativo aparece após o 15º lugar decimal e (2) não em notação científica. Então isso deixa manipulação manual de cordas.
Abaixo eu uso o decimal
módulo para extrair os dígitos decimais do flutuador. o float_to_decimal
A função é usada para converter o flutuador para um Decimal
objeto. O caminho óbvio decimal.Decimal(str(f))
está errado porque str(f)
pode perder dígitos significativos.
float_to_decimal
foi retirado do Documentação do módulo decimal.
Depois que os dígitos decimais são obtidos como uma tupla de INTs, o código abaixo faz a coisa óbvia: corte o número desejado de dígitos sigificantes, arredondar se necessário, junte -se aos dígitos em uma corda, aderência em um sinal, coloque um decimal ponto e zeros para a esquerda ou direita, conforme apropriado.
Na parte inferior, você encontrará alguns casos que eu costumava testar o f
função.
import decimal
def float_to_decimal(f):
# http://docs.python.org/library/decimal.html#decimal-faq
"Convert a floating point number to a Decimal with no loss of information"
n, d = f.as_integer_ratio()
numerator, denominator = decimal.Decimal(n), decimal.Decimal(d)
ctx = decimal.Context(prec=60)
result = ctx.divide(numerator, denominator)
while ctx.flags[decimal.Inexact]:
ctx.flags[decimal.Inexact] = False
ctx.prec *= 2
result = ctx.divide(numerator, denominator)
return result
def f(number, sigfig):
# http://stackoverflow.com/questions/2663612/nicely-representing-a-floating-point-number-in-python/2663623#2663623
assert(sigfig>0)
try:
d=decimal.Decimal(number)
except TypeError:
d=float_to_decimal(float(number))
sign,digits,exponent=d.as_tuple()
if len(digits) < sigfig:
digits = list(digits)
digits.extend([0] * (sigfig - len(digits)))
shift=d.adjusted()
result=int(''.join(map(str,digits[:sigfig])))
# Round the result
if len(digits)>sigfig and digits[sigfig]>=5: result+=1
result=list(str(result))
# Rounding can change the length of result
# If so, adjust shift
shift+=len(result)-sigfig
# reset len of result to sigfig
result=result[:sigfig]
if shift >= sigfig-1:
# Tack more zeros on the end
result+=['0']*(shift-sigfig+1)
elif 0<=shift:
# Place the decimal point in between digits
result.insert(shift+1,'.')
else:
# Tack zeros on the front
assert(shift<0)
result=['0.']+['0']*(-shift-1)+result
if sign:
result.insert(0,'-')
return ''.join(result)
if __name__=='__main__':
tests=[
(0.1, 1, '0.1'),
(0.0000000000368568, 2,'0.000000000037'),
(0.00000000000000000000368568, 2,'0.0000000000000000000037'),
(756867, 3, '757000'),
(-756867, 3, '-757000'),
(-756867, 1, '-800000'),
(0.0999999999999,1,'0.1'),
(0.00999999999999,1,'0.01'),
(0.00999999999999,2,'0.010'),
(0.0099,2,'0.0099'),
(1.999999999999,1,'2'),
(1.999999999999,2,'2.0'),
(34500000000000000000000, 17, '34500000000000000000000'),
('34500000000000000000000', 17, '34500000000000000000000'),
(756867, 7, '756867.0'),
]
for number,sigfig,answer in tests:
try:
result=f(number,sigfig)
assert(result==answer)
print(result)
except AssertionError:
print('Error',number,sigfig,result,answer)
Outras dicas
Se você quer precisão do ponto flutuante, você precisa usar o decimal
módulo, que faz parte do Biblioteca padrão Python:
>>> import decimal
>>> d = decimal.Decimal('0.0000000000368568')
>>> print '%.15f' % d
0.000000000036857
Aqui está um trecho que formata um valor de acordo com as barras de erro fornecidas.
from math import floor, log10, round
def sigfig3(v, errplus, errmin):
i = int(floor(-log10(max(errplus,errmin)) + 2))
if i > 0:
fmt = "%%.%df" % (i)
return "{%s}^{%s}_{%s}" % (fmt % v,fmt % errplus, fmt % errmin)
else:
return "{%d}^{%d}_{%d}" % (round(v, i),round(errplus, i), numpy.round(i))
Exemplos:
5268685 (+1463262,-2401422) becomes 5300000 (+1500000,-2400000)
0.84312 +- 0.173124 becomes 0.84 +- 0.17
São necessários flutuações de precisão arbitrária para responder adequadamente a esta pergunta. Portanto, usando o módulo decimal é uma obrigação. Não há método para converter uma decimal para uma string sem nunca usar o formato exponencial (parte da pergunta original), então escrevi uma função para fazer exatamente isso:
def removeExponent(decimal):
digits = [str(n) for n in decimal.as_tuple().digits]
length = len(digits)
exponent = decimal.as_tuple().exponent
if length <= -1 * exponent:
zeros = -1 * exponent - length
digits[0:0] = ["0."] + ["0"] * zeros
elif 0 < -1 * exponent < length:
digits.insert(exponent, ".")
elif 0 <= exponent:
digits.extend(["0"] * exponent)
sign = []
if decimal.as_tuple().sign == 1:
sign = ["-"]
print "".join(sign + digits)
O problema está tentando arredondar para números significativos. O método "quantize ()" de Decimal não arredonde mais alto que o ponto decimal, e a função "rodada ()" sempre retorna um flutuador. Não sei se esses são bugs, mas isso significa que a única maneira de arredondar números de ponto flutuante infinito é analisá -lo como uma lista ou string e fazer o arredondamento manualmente. Em outras palavras, não há resposta sã para esta pergunta.