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.

Joan Venge
fonte
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.

David Z
fonte
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.

Teifion
fonte
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'
Ferdinand Beyer
fonte
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]
john_dough
fonte
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

Conta
fonte
11

Script python simples -

n = 1.923328437452
n = float(int(n * 1000))
n /=1000
Markroxor
fonte
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.

Matt
fonte
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.

Antony Hatchkins
fonte
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()
nullstellensatz
fonte
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
Alvaro
fonte
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]

fonte
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
cs95
fonte
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)
Bart Cubrich
fonte
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))
itsadok
fonte
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);
Pieter
fonte
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

Sarang
fonte
1
def trunc(f,n):
  return ('%.16f' % f)[:(n-16)]
Ross Cartlidge
fonte
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
Yohan Obadia
fonte
Ó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.

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

Andrew Olson
fonte
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
megajoe
fonte
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
H123321
fonte
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

JohnA
fonte
0

use numpy.round

import numpy as np
precision = 3
floats = [1.123123123, 2.321321321321]
new_float = np.round(floats, precision)
Rafaelvalle
fonte
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']
Axel
fonte
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 .

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

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

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

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

print '%.3f' % number
user44511
fonte
2
Isso arredonda o número, não trunca.
David Z