Truncando flutuadores em Python


110

Quero remover dígitos de um ponto flutuante para ter um número fixo de dígitos após o ponto, como:

1.923328437452 -> 1.923

Preciso dar saída como string para outra função, não imprimir.

Também quero ignorar os dígitos perdidos, não arredondá-los.


4
Deve -1,233 ser truncado para -1,23 ou -1,24?
— Antony Hatchkins

Respostas:


117

Primeiro, a função, para aqueles que querem apenas um código de copiar e colar:

def truncate(f, n):
    '''Truncates/pads a float f to n decimal places without rounding'''
    s = '{}'.format(f)
    if 'e' in s or 'E' in s:
        return '{0:.{1}f}'.format(f, n)
    i, p, d = s.partition('.')
    return '.'.join([i, (d+'0'*n)[:n]])

Isso é válido no Python 2.7 e 3.1+. Para versões mais antigas, não é possível obter o mesmo efeito de "arredondamento inteligente" (pelo menos, não sem muitos códigos complicados), mas arredondar para 12 casas decimais antes do truncamento funcionará na maior parte do tempo:

def truncate(f, n):
    '''Truncates/pads a float f to n decimal places without rounding'''
    s = '%.12f' % f
    i, p, d = s.partition('.')
    return '.'.join([i, (d+'0'*n)[:n]])

Explicação

O núcleo do método subjacente é converter o valor em uma string com precisão total e, em seguida, cortar tudo além do número desejado de caracteres. A última etapa é fácil; pode ser feito com manipulação de string

i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])

ou o decimalmódulo

str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))

A primeira etapa, a conversão para uma string, é bastante difícil porque existem alguns pares de literais de ponto flutuante (ou seja, o que você escreve no código-fonte) que produzem a mesma representação binária e, ainda assim, devem ser truncados de forma diferente. Por exemplo, considere 0,3 e 0,29999999999999998. Se você escrever 0.3em um programa Python, o compilador o codifica usando o formato de ponto flutuante IEEE na sequência de bits (assumindo um float de 64 bits)

0011111111010011001100110011001100110011001100110011001100110011

Este é o valor mais próximo de 0,3 que pode ser representado com precisão como um flutuador IEEE. Mas se você escrever 0.29999999999999998em um programa Python, o compilador o traduzirá exatamente no mesmo valor . Em um caso, você pretendia que fosse truncado (para um dígito) como 0.3, enquanto no outro caso você pretendia que fosse truncado como 0.2, mas Python pode dar apenas uma resposta. Esta é uma limitação fundamental do Python, ou mesmo de qualquer linguagem de programação sem avaliação preguiçosa. A função de truncamento só tem acesso ao valor binário armazenado na memória do computador, não à string que você realmente digitou no código-fonte. 1

Se você decodificar a sequência de bits de volta para um número decimal, novamente usando o formato de ponto flutuante IEEE de 64 bits, você obtém

0.2999999999999999888977697537484345957637...

portanto, uma implementação ingênua surgiria, 0.2mesmo que provavelmente não seja o que você deseja. Para obter mais informações sobre o erro de representação de ponto flutuante, consulte o tutorial Python .

É muito raro trabalhar com um valor de ponto flutuante tão próximo a um número redondo e, ainda assim, intencionalmente não igual a esse número redondo. Portanto, ao truncar, provavelmente faz sentido escolher a representação decimal "mais adequada" de todas as que podem corresponder ao valor na memória. O Python 2.7 e superior (mas não 3.0) inclui um algoritmo sofisticado para fazer exatamente isso , que podemos acessar por meio da operação de formatação de string padrão.

'{}'.format(f)

A única ressalva é que isso funciona como uma gespecificação de formato, no sentido de que usa notação exponencial ( 1.23e+4) se o número for grande ou pequeno o suficiente. Portanto, o método deve capturar esse caso e tratá-lo de maneira diferente. Existem alguns casos em que o uso de uma fespecificação de formato causa um problema, como tentar truncar 3e-10para 28 dígitos de precisão (produz 0.0000000002999999999999999980), e ainda não tenho certeza da melhor forma de lidar com isso.

Se você realmente estiver trabalhando com floats que são muito próximos de números redondos, mas intencionalmente não iguais a eles (como 0,29999999999999998 ou 99,959999999999994), isso produzirá alguns falsos positivos, ou seja, arredondará números que você não deseja arredondar. Nesse caso, a solução é especificar uma precisão fixa.

'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)

O número de dígitos de precisão a ser usado aqui não importa realmente, ele só precisa ser grande o suficiente para garantir que qualquer arredondamento executado na conversão de string não "aumente" o valor para sua bela representação decimal. Acho que sys.float_info.dig + n + 2pode ser suficiente em todos os casos, mas, se não, 2talvez seja necessário aumentar, e não faz mal fazê-lo.

Em versões anteriores do Python (até 2.6 ou 3.0), a formatação do número de ponto flutuante era muito mais rudimentar e costumava produzir coisas como

>>> 1.1
1.1000000000000001

Se esta é sua situação, se você não quiser usar "nice" representações decimais para truncagem, tudo o que você pode fazer (tanto quanto eu sei) é escolher um número de dígitos, menos do que o representável precisão total por um float, e volta a número com essa quantidade de dígitos antes de truncá-lo. Uma escolha típica é 12,

'%.12f' % f

mas você pode ajustar isso para se adequar aos números que está usando.


1 Bem ... eu menti. Tecnicamente, você pode instruir o Python a analisar novamente seu próprio código-fonte e extrair a parte correspondente ao primeiro argumento que você passa para a função de truncamento. Se esse argumento for um literal de ponto flutuante, você pode simplesmente cortá-lo com um certo número de casas após o ponto decimal e retornar isso. No entanto, essa estratégia não funciona se o argumento for uma variável, o que o torna bastante inútil. O seguinte é apresentado apenas para valor de entretenimento:

def trunc_introspect(f, n):
    '''Truncates/pads the float f to n decimal places by looking at the caller's source code'''
    current_frame = None
    caller_frame = None
    s = inspect.stack()
    try:
        current_frame = s[0]
        caller_frame = s[1]
        gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline)
        for token_type, token_string, _, _, _ in gen:
            if token_type == tokenize.NAME and token_string == current_frame[3]:
                next(gen) # left parenthesis
                token_type, token_string, _, _, _ = next(gen) # float literal
                if token_type == tokenize.NUMBER:
                    try:
                        cut_point = token_string.index('.') + n + 1
                    except ValueError: # no decimal in string
                        return token_string + '.' + '0' * n
                    else:
                        if len(token_string) < cut_point:
                            token_string += '0' * (cut_point - len(token_string))
                        return token_string[:cut_point]
                else:
                    raise ValueError('Unable to find floating-point literal (this probably means you called {} with a variable)'.format(current_frame[3]))
                break
    finally:
        del s, current_frame, caller_frame

Generalizar isso para lidar com o caso em que você passa uma variável parece uma causa perdida, já que você teria que rastrear a execução do programa até encontrar o literal de ponto flutuante que deu à variável seu valor. Se houver um. A maioria das variáveis ​​será inicializada a partir da entrada do usuário ou de expressões matemáticas, caso em que a representação binária é tudo o que existe.


Como podemos aplicar essa função a um dataframe?
— codeslord

@RohithRNair Em cima da minha cabeça, da mesma forma que você aplicaria qualquer outra função que opere em elementos individuais (ou seja applymap()). Talvez haja uma maneira de tornar toda a operação mais eficiente, mas isso seria um assunto para outra questão.
— David Z

applymap () está demorando muito, pois meus dataframes são realmente grandes. Estou tentando comparar dois dataframes para diferenças, mas a precisão do ponto flutuante está distorcendo minha saída do desejado. Como você disse, levantarei uma questão separada para o mesmo. Obrigado.
— codeslord

@RohithRNair Ah, se você está tentando comparar dois dataframes para diferenças, pergunte sobre isso. Truncar os valores (que é o assunto desta pergunta) não é a melhor maneira de fazer isso.
— David Z

Apenas uma observação, seu código parece reduzir os números negativos a zero negativo, o que pode ficar confuso ...
— user541686

152
round(1.923328437452, 3)

Consulte a documentação do Python sobre os tipos padrão . Você precisará rolar um pouco para baixo para chegar à função round. Essencialmente, o segundo número diz para quantas casas decimais ele deve ser arredondado.


49
Eu quis dizer que arredondar não é o que eu preciso. Eu preciso truncar, o que é diferente.
— Joan Venge

1
Ahhh, muito justo. Meu erro, desculpe.
— Teifion

22
São muitos votos positivos para uma solução incorreta! Uma daquelas raridades de Stackoverflow estranhas. Eu me pergunto se há um emblema para isso ...
— tumultous_rooster

5
É simplesmente espantoso quantas respostas erradas (e votos positivos para as respostas erradas) existem para esta pergunta.
— nullstellensatz

6
Muitas pessoas virão para esta página em busca de arredondamento;)
— janjackson

33

O resultado de roundé um float, então fique atento (o exemplo é do Python 2.6):

>>> round(1.923328437452, 3)
1.923
>>> round(1.23456, 3)
1.2350000000000001

Você ficará melhor ao usar uma string formatada:

>>> "%.3f" % 1.923328437452
'1.923'
>>> "%.3f" % 1.23456
'1.235'

8
No meu Python, isso arredonda: '% .3f'% 1.23456 == '1.235'
— David Z

Isso é muito mais elegante do que a bobagem de formatação manual de strings, bom post!
— rsethc

round(1.23456, 3)é 1.235e não é1.2350000000000001
— Ahmad

1
@Ahmad não necessariamente. O exemplo aqui é do Python 2.6 (observe a data da resposta). A formatação da string foi melhorada no Python 2.7 / 3.1, provavelmente por isso você obtém resultados diferentes. No entanto, os números de ponto flutuante costumam ter representações de string inesperadas, consulte: docs.python.org/3.6/tutorial/floatingpoint.html
— Ferdinand Beyer

21
n = 1.923328437452
str(n)[:4]

3
Simples e pitônico. 4 é o tamanho do número inteiro, não apenas os dígitos após o ponto.
— GaTTaCa

4
Portanto, se o usuário inserir, por exemplo 2, você terá um ponto decimal .no final da string - não é realmente uma boa solução, eu acho.
— Zelphir Kaltstahl

Isso é específico para um caso desse número. Como seria generalizado para 11.923328437452?
— polarizar

Melhor resposta! você também pode adicionar float () para retornar um número: float (str (n) [: 4])
— justSaid

14

No meu prompt Python 2.7:

>>> int(1.923328437452 * 1000)/1000.0 1.923


11

Script python simples -

n = 1.923328437452
n = float(int(n * 1000))
n /=1000

3
Resposta limpa. Você apenas perde uma etapa, para converter de volta para flutuar antes de dividir por 1000. Caso contrário, você obterá 1.
— Yohan Obadia

9
def trunc(num, digits):
   sp = str(num).split('.')
   return '.'.join([sp[0], sp[1][:digits]])

Isso deve funcionar. Ele deve fornecer o truncamento que você está procurando.


9

A maneira verdadeiramente pythônica de fazer isso é

from decimal import *

with localcontext() as ctx:
    ctx.rounding = ROUND_DOWN
    print Decimal('1.923328437452').quantize(Decimal('0.001'))

ou mais curto:

from decimal import Decimal as D, ROUND_DOWN

D('1.923328437452').quantize(D('0.001'), rounding=ROUND_DOWN)

Atualizar

Normalmente, o problema não está em truncar os próprios flutuadores, mas no uso impróprio de números flutuantes antes do arredondamento.

Por exemplo: int(0.7*3*100)/100 == 2.09.

Se você for forçado a usar floats (digamos, você está acelerando seu código com numba), é melhor usar centavos como "representação interna" de preços: ( 70*3 == 210) e multiplicar / dividir as entradas / saídas.


Parson me por perguntar isso, mas ... por quê?
— markroxor

@markroxor, não tenho certeza do que exatamente você está perguntando. Como nota lateral, geralmente o problema não é com o arredondamento em si, mas com o uso impróprio de números flutuantes antes do arredondamento. Ex int(0.7*3*100)/100 == 2.09. Para onde foi meu 1 centavo?
— Antony Hatchkins

isso faz sentido, você pode editar sua resposta com esta explicação? obrigado.
— markroxor

Entendendo ImportError: cannot import name 'D', acredito que você queria fazer uma importação nomeada, não?
— Overdrivr

8

Muitas das respostas dadas para esta pergunta estão completamente erradas. Eles arredondam os flutuadores (em vez de truncar) ou não funcionam para todos os casos.

Este é o principal resultado do Google quando procuro por 'Python truncate float', um conceito realmente direto e que merece respostas melhores. Eu concordo com Hatchkins que usar o decimalmódulo é a maneira pítônica de fazer isso, então eu dou aqui uma função que acho que responde a pergunta corretamente e que funciona conforme o esperado para todos os casos.

Como uma nota lateral, valores fracionários, em geral, não podem ser representados exatamente por variáveis ​​binárias de ponto flutuante (veja aqui uma discussão sobre isso), e é por isso que minha função retorna uma string.

from decimal import Decimal, localcontext, ROUND_DOWN

def truncate(number, places):
    if not isinstance(places, int):
        raise ValueError("Decimal places must be an integer.")
    if places < 1:
        raise ValueError("Decimal places must be at least 1.")
    # If you want to truncate to 0 decimal places, just do int(number).

    with localcontext() as context:
        context.rounding = ROUND_DOWN
        exponent = Decimal(str(10 ** - places))
        return Decimal(str(number)).quantize(exponent).to_eng_string()

4

Eu fiz algo assim:

from math import trunc


def truncate(number, decimals=0):
    if decimals < 0:
        raise ValueError('truncate received an invalid value of decimals ({})'.format(decimals))
    elif decimals == 0:
        return trunc(number)
    else:
        factor = float(10**decimals)
        return trunc(number*factor)/factor

4

Você pode fazer:

def truncate(f, n):
    return math.floor(f * 10 ** n) / 10 ** n

testando:

>>> f=1.923328437452
>>> [truncate(f, n) for n in range(5)]
[1.0, 1.9, 1.92, 1.923, 1.9233]

Isso só trunca com números positivos, os números negativos serão arredondados para baixo (longe de zero).
— Aaron D

3

Se você gosta de matemática, isso funciona para + cinco números:

>>> v = 1.923328437452
>>> v - v % 1e-3
1.923

Pelo que entendi 1e-3 irá truncar para 3 dígitos após o ponto. Gostei desta resposta, mas não parece funcionar para 4 e 5.
— egvo

2

Ao usar um pandas df, isso funcionou para mim

import math
def truncate(number, digits) -> float:
    stepper = 10.0 ** digits
    return math.trunc(stepper * number) / stepper

df['trunc'] = df['float_val'].apply(lambda x: truncate(x,1))
df['trunc']=df['trunc'].map('{:.1f}'.format)

1

Só queria mencionar que o velho truque de "arredondar () com chão ()"

round(f) = floor(f+0.5)

pode ser girado para tornar o piso () de redondo ()

floor(f) = round(f-0.5)

Embora ambas as regras quebrem em torno de números negativos, usá-las é menos do que ideal:

def trunc(f, n):
    if f > 0:
        return "%.*f" % (n, (f - 0.5*10**-n))
    elif f == 0:
        return "%.*f" % (n, f)
    elif f < 0:
        return "%.*f" % (n, (f + 0.5*10**-n))

1

int (16,5); isso dará um valor inteiro de 16, ou seja, trunc, não será capaz de especificar decimais, mas acho que você pode fazer isso

import math;

def trunc(invalue, digits):
    return int(invalue*math.pow(10,digits))/math.pow(10,digits);

1

Esta é uma maneira fácil:

def truncate(num, res=3):
    return (floor(num*pow(10, res)+0.5))/pow(10, res)

para num = 1,923328437452, resulta em 1,923



1

Uma função geral e simples de usar:

def truncate_float(number, length):
    """Truncate float numbers, up to the number specified
    in length that must be an integer"""

    number = number * pow(10, length)
    number = int(number)
    number = float(number)
    number /= pow(10, length)
    return number

Ótimo! A conversão para int trunca os números positivos e negativos.
— Aaron D

1

Existe uma solução alternativa fácil em python 3. Onde cortar eu defini com uma variável de ajuda decPlace para facilitar a adaptação.

f = 1.12345
decPlace= 4
f_cut = int(f * 10**decPlace) /10**decPlace

Resultado:

f = 1.1234

Espero que ajude.


1
def precision(value, precision):
    """
    param: value: takes a float
    param: precision: int, number of decimal places
    returns a float
    """
    x = 10.0**precision
    num = int(value * x)/ x
    return num
precision(1.923328437452, 3)

1.923


Legal, mas você não está arredondando.
— Alex

1

Variante curta e fácil

def truncate_float(value, digits_after_point=2):
    pow_10 = 10 ** digits_after_point
    return (float(int(value * pow_10))) / pow_10

>>> truncate_float(1.14333, 2)
>>> 1.14

>>> truncate_float(1.14777, 2)
>>> 1.14


>>> truncate_float(1.14777, 4)
>>> 1.1477

1

A maioria das respostas é muito complicada na minha opinião, que tal isso?

digits = 2  # Specify how many digits you want

fnum = '122.485221'
truncated_float = float(fnum[:fnum.find('.') + digits + 1])

>>> 122.48

Simplesmente digitalizando o índice de '.' e truncar conforme desejado (sem arredondamento). Converta a string em flutuante como etapa final.

Ou, no seu caso, se você receber um float como entrada e quiser uma string como saída:

fnum = str(122.485221)  # convert float to string first
truncated_float = fnum[:fnum.find('.') + digits + 1]  # string output

Sua proposta é problemática se o número que está sendo truncado for pequeno, pois você desperdiçaria muita precisão com o 0 à direita do ponto decimal. Mas este problema é endêmico para o problema conforme declarado. O que estou tentando dizer é que algarismos significativos são a resposta real.
— overcoil

1
>>> floor((1.23658945) * 10**4) / 10**4
1.2365

# divida e multiplique por 10 ** número de dígitos desejados


0

use numpy.round

import numpy as np
precision = 3
floats = [1.123123123, 2.321321321321]
new_float = np.round(floats, precision)

0

Algo simples o suficiente para caber em uma compreensão de lista, sem bibliotecas ou outras dependências externas. Para Python> = 3.6, é muito simples escrever com strings f.

A ideia é permitir que a conversão de string faça o arredondamento para um lugar a mais do que você precisa e, em seguida, corte o último dígito.

>>> nout = 3  # desired number of digits in output
>>> [f'{x:.{nout+1}f}'[:-1] for x in [2/3, 4/5, 8/9, 9/8, 5/4, 3/2]]
['0.666', '0.800', '0.888', '1.125', '1.250', '1.500']

Claro, há é arredondamento acontecendo aqui (nomeadamente para o quarto dígito), mas o arredondamento em algum momento é unvoidable. Caso a transição entre truncamento e arredondamento seja relevante, aqui está um exemplo um pouco melhor:

>>> nacc = 6  # desired accuracy (maximum 15!)
>>> nout = 3  # desired number of digits in output
>>> [f'{x:.{nacc}f}'[:-(nacc-nout)] for x in [2.9999, 2.99999, 2.999999, 2.9999999]]
>>> ['2.999', '2.999', '2.999', '3.000']

Bônus: remover zeros à direita

>>> nout = 3  # desired number of digits in output
>>> [f'{x:.{nout+1}f}'[:-1].rstrip('0') for x in [2/3, 4/5, 8/9, 9/8, 5/4, 3/2]]
['0.666', '0.8', '0.888', '1.125', '1.25', '1.5']

0

A ideia central apresentada aqui me parece ser a melhor abordagem para esse problema. Infelizmente, ele recebeu menos votos, enquanto a resposta posterior que tem mais votos não está completa (conforme observado nos comentários). Felizmente, a implementação a seguir fornece uma solução curta e completa para truncamento .

def trunc(num, digits):
    l = str(float(num)).split('.')
    digits = min(len(l[1]), digits)
    return (l[0]+'.'+l[1][:digits])

que deve cuidar de todos os casos extremos encontrados aqui e aqui .


-1

Também sou um novato em python e depois de usar alguns pedaços aqui, ofereço meus dois centavos

print str(int(time.time()))+str(datetime.now().microsecond)[:3]

str (int (time.time ())) pegará o período de tempo como int e o converterá em string e se unirá com ... str (datetime.now (). microssegundo) [: 3] que retorna apenas os microssegundos, converter para string e truncar para os primeiros 3 caracteres


-1
# value  value to be truncated
# n  number of values after decimal

value = 0.999782
n = 3
float(int(value*1en))*1e-n

-3

Se você quer dizer ao imprimir, o seguinte deve funcionar:

print '%.3f' % number

2
Isso arredonda o número, não trunca.
— David Z
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.