Truncar flotantes en Python

110

Quiero eliminar dígitos de un flotante para tener un número fijo de dígitos después del punto, como:

1.923328437452 -> 1.923

Necesito enviar como una cadena a otra función, no imprimir.

También quiero ignorar los dígitos perdidos, no redondearlos.

Joan Venge
fuente
4
¿Debería truncar -1,233 a -1,23 o -1,24?
Antony Hatchkins

Respuestas:

116

Primero, la función, para aquellos que solo quieren un código de copiar y pegar:

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

Esto es válido en Python 2.7 y 3.1+. Para las versiones anteriores, no es posible obtener el mismo efecto de "redondeo inteligente" (al menos, no sin mucho código complicado), pero el redondeo a 12 lugares decimales antes del truncamiento funcionará la mayor parte del tiempo:

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

Explicación

El núcleo del método subyacente es convertir el valor en una cadena con total precisión y luego simplemente cortar todo más allá del número deseado de caracteres. El último paso es sencillo; se puede hacer con manipulación de cuerdas

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

o el decimalmodulo

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

El primer paso, convertir a una cadena, es bastante difícil porque hay algunos pares de literales de punto flotante (es decir, lo que escribe en el código fuente) que producen la misma representación binaria y, sin embargo, deben truncarse de manera diferente. Por ejemplo, considere 0.3 y 0.29999999999999998. Si escribe 0.3en un programa Python, el compilador lo codifica utilizando el formato de punto flotante IEEE en la secuencia de bits (asumiendo un flotante de 64 bits)

0011111111010011001100110011001100110011001100110011001100110011

Este es el valor más cercano a 0.3 que se puede representar con precisión como un flotante IEEE. Pero si escribe 0.29999999999999998en un programa de Python, el compilador lo traduce exactamente al mismo valor . En un caso, quería que se truncara (a un dígito) como 0.3, mientras que en el otro caso, quería que se truncase como 0.2, pero Python solo puede dar una respuesta. Esta es una limitación fundamental de Python, o de cualquier lenguaje de programación sin evaluación perezosa. La función de truncamiento solo tiene acceso al valor binario almacenado en la memoria de la computadora, no a la cadena que realmente ingresó en el código fuente. 1

Si decodifica la secuencia de bits en un número decimal, nuevamente usando el formato de punto flotante IEEE de 64 bits, obtiene

0.2999999999999999888977697537484345957637...

por lo que se produciría una implementación ingenua 0.2, aunque probablemente eso no sea lo que desea. Para obtener más información sobre el error de representación de punto flotante, consulte el tutorial de Python .

Es muy raro trabajar con un valor de punto flotante que está tan cerca de un número redondo y, sin embargo, no es intencionalmente igual a ese número redondo. Entonces, al truncar, probablemente tenga sentido elegir la representación decimal "más bonita" de todas las que podrían corresponder al valor en la memoria. Python 2.7 y versiones posteriores (pero no 3.0) incluye un algoritmo sofisticado para hacer precisamente eso , al que podemos acceder a través de la operación de formato de cadena predeterminada.

'{}'.format(f)

La única advertencia es que esto actúa como una gespecificación de formato, en el sentido de que usa notación exponencial ( 1.23e+4) si el número es lo suficientemente grande o pequeño. Entonces, el método tiene que detectar este caso y manejarlo de manera diferente. Hay algunos casos en los que el uso de una fespecificación de formato causa un problema, como intentar truncar 3e-10a 28 dígitos de precisión (produce 0.0000000002999999999999999980), y todavía no estoy seguro de cuál es la mejor manera de manejarlos.

Si realmente está trabajando con floats que están muy cerca de los números redondeados pero intencionalmente no son iguales a ellos (como 0.29999999999999998 o 99.959999999999994), esto producirá algunos falsos positivos, es decir, redondeará los números que no desea redondear. En ese caso, la solución es especificar una precisión fija.

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

El número de dígitos de precisión a usar aquí realmente no importa, solo necesita ser lo suficientemente grande para garantizar que cualquier redondeo realizado en la conversión de cadena no "aumente" el valor a su agradable representación decimal. Creo que sys.float_info.dig + n + 2puede ser suficiente en todos los casos, pero si no es así, 2podría ser necesario aumentarlo, y no está de más hacerlo.

En versiones anteriores de Python (hasta 2.6 o 3.0), el formato de número de punto flotante era mucho más burdo y producía regularmente cosas como

>>> 1.1
1.1000000000000001

Si ésta es su situación, si usted no desea utilizar "buenos" representaciones decimales para truncar, todo lo que puede hacer (por lo que yo sé) es elegir un número de dígitos, menos de lo representable precisión completa por una float, y alrededor de la número a esa cantidad de dígitos antes de truncarlo. Una elección típica es 12,

'%.12f' % f

pero puedes ajustar esto para que se adapte a los números que estás usando.


1 Bueno ... mentí. Técnicamente, puede indicarle a Python que vuelva a analizar su propio código fuente y extraiga la parte correspondiente al primer argumento que pase a la función de truncamiento. Si ese argumento es un literal de punto flotante, puede cortarlo un cierto número de lugares después del punto decimal y devolverlo. Sin embargo, esta estrategia no funciona si el argumento es una variable, lo que la hace bastante inútil. Lo siguiente se presenta solo con fines de entretenimiento:

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 esto para manejar el caso en el que pasa una variable parece una causa perdida, ya que tendría que rastrear hacia atrás a través de la ejecución del programa hasta encontrar el literal de punto flotante que le dio a la variable su valor. Si es que hay uno. La mayoría de las variables se inicializarán a partir de la entrada del usuario o expresiones matemáticas, en cuyo caso la representación binaria es todo lo que hay.

David Z
fuente
¿Cómo podemos aplicar esta función a un marco de datos?
codelord
@RohithRNair Fuera de mi cabeza, de la misma manera que aplicaría cualquier otra función que opere en elementos individuales (es decir applymap()). Tal vez haya una manera de hacer que toda la operación sea más eficiente, pero ese sería un tema para una pregunta separada.
David Z
applymap () está tomando mucho tiempo ya que mis marcos de datos son realmente grandes. Estoy tratando de comparar dos marcos de datos en busca de diferencias, pero la precisión del punto flotante está sesgando mi salida del deseado. Como dijiste, plantearé una pregunta separada para el mismo. Gracias.
codelord
@RohithRNair Ah, bueno, si está tratando de comparar dos marcos de datos en busca de diferencias, pregunte sobre eso. Truncar los valores (que es de lo que trata esta pregunta) no es la mejor manera de hacerlo.
David Z
Solo una nota, su código parece cortar números negativos a cero negativo, lo que puede
resultar
152
round(1.923328437452, 3)

Consulte la documentación de Python sobre los tipos estándar . Deberá desplazarse un poco hacia abajo para llegar a la función de ronda. Básicamente, el segundo número dice a cuántos lugares decimales redondearlo.

Teifion
fuente
49
Me refiero a que el redondeo no es lo que necesito. Necesito truncar, que es diferente.
Joan Venge
1
Ahhh, bastante justo. Mi error lo siento.
Teifion
22
¡Son muchos votos positivos por una solución incorrecta! Una de esas extrañas rarezas de Stackoverflow. Me pregunto si hay una tarjeta de identificación a que ...
tumultous_rooster
5
Es simplemente espantoso cuántas respuestas incorrectas (y votos a favor por las respuestas incorrectas) hay para esta pregunta.
nullstellensatz
6
Mucha gente vendrá a esta página en busca de redondeo;)
janjackson
33

El resultado de roundes un flotador, así que ten cuidado (el ejemplo es de Python 2.6):

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

Estará mejor cuando utilice una cadena formateada:

>>> "%.3f" % 1.923328437452
'1.923'
>>> "%.3f" % 1.23456
'1.235'
Ferdinand Beyer
fuente
8
En mi Python, eso redondea: '% .3f'% 1.23456 == '1.235'
David Z
Esto es mucho más elegante que las tonterías del formato manual de cadenas, ¡buena publicación!
rsethc
round(1.23456, 3)es 1.235y no1.2350000000000001
Ahmad
1
@Ahmad no necesariamente. El ejemplo aquí es de Python 2.6 (tenga en cuenta la fecha de la respuesta). El formato de cadena se mejoró en Python 2.7 / 3.1, probablemente por eso obtiene resultados diferentes. Sin embargo, los números de punto flotante a menudo tendrán representaciones de cadenas inesperadas, consulte: docs.python.org/3.6/tutorial/floatingpoint.html
Ferdinand Beyer
21
n = 1.923328437452
str(n)[:4]
john_dough
fuente
3
Simple y pitónico. Sin embargo, 4 es el tamaño del número entero, no solo los dígitos después del punto.
GaTTaCa
4
Entonces, si el usuario ingresa, por ejemplo 2, tendrá un punto decimal .al final de la cadena; no creo que sea una buena solución.
Zelphir Kaltstahl
Esto es específico para un caso de este número. ¿Cómo se generalizaría a 11,923328437452?
polarizar el
¡La mejor respuesta! también puede agregar float () para devolver un número: float (str (n) [: 4])
justSaid
14

En mi indicador de Python 2.7:

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

Cuenta
fuente
11

Secuencia de comandos de Python simple -

n = 1.923328437452
n = float(int(n * 1000))
n /=1000
Markroxor
fuente
3
Respuesta limpia. Solo pierde un paso para volver a flotar antes de dividir por 1000. De lo contrario, obtendrá 1.
Yohan Obadia
9
def trunc(num, digits):
   sp = str(num).split('.')
   return '.'.join([sp[0], sp[1][:digits]])

Esto debería funcionar. Debería darle el truncamiento que está buscando.

Mate
fuente
9

La forma verdaderamente pitónica de hacerlo es

from decimal import *

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

o más corto:

from decimal import Decimal as D, ROUND_DOWN

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

Actualizar

Por lo general, el problema no está en truncar los flotantes en sí, sino en el uso inadecuado de los números flotantes antes del redondeo.

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

Si se ve obligado a usar flotadores (digamos, está acelerando su código con numba), es mejor usar centavos como "representación interna" de los precios: ( 70*3 == 210) y multiplicar / dividir las entradas / salidas.

Antony Hatchkins
fuente
Parson me por preguntar esto, pero ... ¿por qué?
Markroxor
@markroxor, no estoy seguro de qué es exactamente lo que está preguntando. Como nota al margen, normalmente el problema no está en el redondeo en sí, sino en el uso inadecuado de números flotantes antes del redondeo. Ej int(0.7*3*100)/100 == 2.09. ¿A dónde se fue mi 1 centavo?
Antony Hatchkins
eso tiene sentido, ¿puedes editar tu respuesta con esta explicación? Gracias.
Markroxor
Obteniendo ImportError: cannot import name 'D', creo que querías hacer una importación con nombre, ¿no?
Overdrivr
8

Muchas de las respuestas dadas a esta pregunta son completamente incorrectas. O redondean flotantes (en lugar de truncar) o no funcionan para todos los casos.

Este es el principal resultado de Google cuando busco 'Python truncate float', un concepto que es realmente sencillo y que merece mejores respuestas. Estoy de acuerdo con Hatchkins en que usar el decimalmódulo es la forma pitónica de hacer esto, así que doy aquí una función que creo que responde a la pregunta correctamente y que funciona como se esperaba para todos los casos.

Como nota al margen, los valores fraccionarios, en general, no se pueden representar exactamente mediante variables binarias de punto flotante (consulte aquí para una discusión sobre esto), razón por la cual mi función devuelve una cadena.

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
fuente
4

Hice algo como esto:

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
Álvaro
fuente
4

Tu puedes hacer:

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

pruebas:

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

fuente
Esto solo se trunca con números positivos, los números negativos se redondearán hacia abajo (alejándose de cero).
Aaron D
3

Si te apetece algo de matemática, esto funciona para + ve números:

>>> v = 1.923328437452
>>> v - v % 1e-3
1.923
cs95
fuente
Según tengo entendido, 1e-3 se truncará a 3 dígitos después del punto. Me gustó esta respuesta, pero parece que no funciona para 4 y 5.
egvo
2

Cuando uso un pandas df esto funcionó para mí

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
fuente
1

Solo quería mencionar que el viejo truco "hacer redondeo () con piso ()" de

round(f) = floor(f+0.5)

se puede dar la vuelta para hacer piso () de redondo ()

floor(f) = round(f-0.5)

Aunque estas dos reglas rompen con números negativos, usarlas es menos 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
fuente
1

int (16,5); esto dará un valor entero de 16, es decir, trunc, no podrá especificar decimales, pero supongo que puede hacerlo por

import math;

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

He aquí una forma sencilla:

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

para num = 1.923328437452, esto da como resultado 1.923

Sarang
fuente
1
def trunc(f,n):
  return ('%.16f' % f)[:(n-16)]
Ross Cartlidge
fuente
1

Una función general y simple 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
fuente
¡Excelente! La conversión a int trunca los números positivos y negativos.
Aaron D
1

Hay una solución fácil en Python 3. Dónde cortar Definí con una variable de ayuda decPlace para facilitar la adaptación.

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

Salida:

f = 1.1234

Espero eso ayude.

MBreg
fuente
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
fuente
Agradable pero no estás redondeando.
Alex
1

Variante corta y 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
fuente
1

La mayoría de las respuestas son demasiado complicadas en mi opinión, ¿qué tal esto?

digits = 2  # Specify how many digits you want

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

>>> 122.48

Simplemente escaneando el índice de '.' y truncar como se desee (sin redondeo). Convierta la cuerda en flotante como paso final.

O en su caso, si obtiene un float como entrada y desea una cadena como salida:

fnum = str(122.485221)  # convert float to string first
truncated_float = fnum[:fnum.find('.') + digits + 1]  # string output
H123321
fuente
Su propuesta es problemática si el número que se está truncando es pequeño, ya que desperdiciaría mucha precisión con el 0 inicial a la derecha del punto decimal. Pero este problema es endémico del problema como se dijo. Lo que intento decir es que cifras significativas es la verdadera respuesta.
overcoil
1
>>> floor((1.23658945) * 10**4) / 10**4
1.2365

# dividir y multiplicar por 10 ** número de dígitos deseados

JohnA
fuente
0

use numpy.round

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

Algo lo suficientemente simple como para caber en una lista de comprensión, sin bibliotecas u otras dependencias externas. Para Python> = 3.6, es muy sencillo escribir con f-strings.

La idea es dejar que la conversión de cadenas realice el redondeo a un lugar más de lo necesario y luego cortar el ú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']

Por supuesto, aquí se está produciendo un redondeo (es decir, para el cuarto dígito), pero el redondeo en algún momento es inevitable. En caso de que la transición entre el truncamiento y el redondeo sea relevante, aquí hay un ejemplo un poco mejor:

>>> 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']

Bono: eliminar ceros a la derecha

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

La idea central que se ofrece aquí me parece el mejor enfoque para este problema. Desafortunadamente, ha recibido menos votos mientras que la respuesta posterior que tiene más votos no está completa (como se observa en los comentarios). Con suerte, la siguiente implementación proporciona una solución corta y completa para el truncamiento .

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

que debería ocuparse de todos los casos de esquina que se encuentran aquí y aquí .

aak318
fuente
-1

También soy un novato en Python y después de hacer uso de algunas piezas aquí, ofrezco mis dos centavos

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

str (int (time.time ())) tomará el tiempo epoch como int y lo convertirá en una cadena y se unirá con ... str (datetime.now (). microsecond) [: 3] que devuelve solo los microsegundos, convert para encadenar y truncar a los primeros 3 caracteres

usuario1048839
fuente
-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
fuente
-3

Si se refiere a imprimir, lo siguiente debería funcionar:

print '%.3f' % number
user44511
fuente
2
Eso redondea el número, no se trunca.
David Z