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?

Foi útil?

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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top