Tronquer les flotteurs en Python

110

Je veux supprimer des chiffres d'un flottant pour avoir un nombre fixe de chiffres après le point, comme:

1.923328437452 -> 1.923

J'ai besoin de sortir sous forme de chaîne vers une autre fonction, pas d'imprimer.

Je veux aussi ignorer les chiffres perdus, pas les arrondir.

Joan Venge
la source
4
Devrait-on tronquer -1,233 à -1,23 ou -1,24?
Antony Hatchkins

Réponses:

117

Tout d'abord, la fonction, pour ceux qui veulent juste du code copier-coller:

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

Ceci est valable dans Python 2.7 et 3.1+. Pour les versions plus anciennes, il n'est pas possible d'obtenir le même effet «d'arrondi intelligent» (du moins, non sans beaucoup de code compliqué), mais arrondir à 12 décimales avant la troncature fonctionnera la plupart du temps:

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

Explication

Le cœur de la méthode sous-jacente consiste à convertir la valeur en une chaîne avec une précision maximale, puis à couper tout ce qui dépasse le nombre de caractères souhaité. La dernière étape est facile; cela peut être fait soit avec une manipulation de chaîne

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

ou le decimalmodule

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

La première étape, la conversion en chaîne, est assez difficile car il existe des paires de littéraux à virgule flottante (c'est-à-dire ce que vous écrivez dans le code source) qui produisent à la fois la même représentation binaire et qui doivent cependant être tronquées différemment. Par exemple, considérons 0,3 et 0,29999999999999998. Si vous écrivez 0.3dans un programme Python, le compilateur l'encode en utilisant le format à virgule flottante IEEE dans la séquence de bits (en supposant un flottant de 64 bits)

0011111111010011001100110011001100110011001100110011001100110011

Il s'agit de la valeur la plus proche de 0,3 qui peut être représentée avec précision par un flottant IEEE. Mais si vous écrivez 0.29999999999999998dans un programme Python, le compilateur le traduit exactement dans la même valeur . Dans un cas, vous vouliez qu'il soit tronqué (à un chiffre) comme 0.3, alors que dans l'autre cas, vous vouliez qu'il soit tronqué comme 0.2, mais Python ne peut donner qu'une seule réponse. C'est une limitation fondamentale de Python, ou en fait de tout langage de programmation sans évaluation paresseuse. La fonction de troncature n'a accès qu'à la valeur binaire stockée dans la mémoire de l'ordinateur, et non à la chaîne que vous avez réellement tapée dans le code source. 1

Si vous décodez la séquence de bits en un nombre décimal, à nouveau en utilisant le format à virgule flottante IEEE 64 bits, vous obtenez

0.2999999999999999888977697537484345957637...

donc une implémentation naïve arriverait 0.2même si ce n'est probablement pas ce que vous voulez. Pour plus d'informations sur l'erreur de représentation en virgule flottante, consultez le didacticiel Python .

Il est très rare de travailler avec une valeur à virgule flottante qui est si proche d'un nombre rond et qui n'est pas intentionnellement égale à ce nombre rond. Ainsi, lors de la troncature, il est probablement logique de choisir la "plus belle" représentation décimale parmi tout ce qui pourrait correspondre à la valeur en mémoire. Python 2.7 et plus (mais pas 3.0) inclut un algorithme sophistiqué pour faire exactement cela , auquel nous pouvons accéder via l'opération de formatage de chaîne par défaut.

'{}'.format(f)

La seule mise en garde est que cela agit comme une gspécification de format, dans le sens où il utilise la notation exponentielle ( 1.23e+4) si le nombre est suffisamment grand ou petit. La méthode doit donc saisir ce cas et le gérer différemment. Il y a quelques cas où l'utilisation d'une fspécification de format à la place pose un problème, comme essayer de tronquer 3e-10à 28 chiffres de précision (cela produit 0.0000000002999999999999999980), et je ne suis pas encore sûr de la meilleure façon de les gérer.

Si vous réellement êtes travaillez avec floats qui sont très proches de chiffres ronds , mais intentionnellement ne les égale à (comme ,29999999999999998 ou 99,959999999999994), cela produira des faux positifs, à savoir qu'il y aura des chiffres ronds que vous ne vouliez pas arrondi. Dans ce cas, la solution est de spécifier une précision fixe.

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

Le nombre de chiffres de précision à utiliser ici n'a pas vraiment d'importance, il doit seulement être suffisamment grand pour s'assurer que tout arrondi effectué dans la conversion de chaîne ne "augmente" pas la valeur à sa belle représentation décimale. Je pense que cela sys.float_info.dig + n + 2peut suffire dans tous les cas, mais sinon, il faudra 2peut-être augmenter ce montant, et cela ne fait pas de mal de le faire.

Dans les versions antérieures de Python (jusqu'à 2.6 ou 3.0), le formatage des nombres à virgule flottante était beaucoup plus grossier et produisait régulièrement des choses comme

>>> 1.1
1.1000000000000001

Si tel est votre cas, si vous ne voulez utiliser des représentations décimales « BELLES » pour tronquer, tout ce que vous pouvez faire (pour autant que je sache) est de choisir un certain nombre de chiffres, moins représentable pleine de précision par un float, et autour de la nombre à autant de chiffres avant de le tronquer. Un choix typique est 12,

'%.12f' % f

mais vous pouvez l'ajuster en fonction des nombres que vous utilisez.


1 Eh bien ... j'ai menti. Techniquement, vous pouvez demander à Python de ré-analyser son propre code source et d'extraire la partie correspondant au premier argument que vous passez à la fonction de troncature. Si cet argument est un littéral à virgule flottante, vous pouvez simplement le couper un certain nombre de places après la virgule décimale et le renvoyer. Cependant, cette stratégie ne fonctionne pas si l'argument est une variable, ce qui la rend assez inutile. Ce qui suit est présenté à titre de divertissement uniquement:

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

Généraliser cela pour gérer le cas où vous passez une variable semble être une cause perdue, car vous devrez remonter en arrière tout au long de l'exécution du programme jusqu'à ce que vous trouviez le littéral à virgule flottante qui a donné sa valeur à la variable. S'il y en a même un. La plupart des variables seront initialisées à partir de l'entrée utilisateur ou d'expressions mathématiques, auquel cas la représentation binaire est tout ce qu'il y a.

David Z
la source
Comment appliquer cette fonction à un dataframe?
codeslord
@RohithRNair Du haut de ma tête, de la même manière que vous appliqueriez n'importe quelle autre fonction qui opère sur des éléments individuels (ie applymap()). Il y a peut-être un moyen de rendre l'opération dans son ensemble plus efficace, mais ce serait une question distincte.
David Z
applymap () prend beaucoup de temps car mes dataframes sont vraiment gros. J'essaie de comparer deux dataframes pour les différences, mais la précision en virgule flottante déforme ma sortie de celle souhaitée. Comme vous l'avez dit, je soulèverai une question distincte pour la même chose. Merci.
codeslord
@RohithRNair Ah, eh bien, si vous essayez de comparer deux dataframes pour des différences, demandez plutôt à ce sujet. Tronquer les valeurs (ce qui est l'objet de cette question) n'est pas la meilleure façon de le faire.
David Z
Juste une note, votre code semble couper les nombres négatifs à zéro négatif, ce qui peut prêter à confusion ...
user541686
152
round(1.923328437452, 3)

Consultez la documentation de Python sur les types standard . Vous devrez faire défiler un peu vers le bas pour accéder à la fonction ronde. Essentiellement, le deuxième nombre indique le nombre de décimales à arrondir.

Teifion
la source
49
Je voulais dire que l'arrondi n'est pas ce dont j'ai besoin. J'ai besoin de tronquer, ce qui est différent.
Joan Venge
1
Ahhh, c'est juste. Mon erreur désolé.
Teifion
22
C'est beaucoup de votes positifs pour une solution incorrecte! Une de ces étranges raretés Stackoverflow. Je me demande s'il y a un badge pour ça ...
tumultous_rooster
5
Il est tout simplement épouvantable de constater le nombre de mauvaises réponses (et de votes positifs pour les mauvaises réponses) pour cette question.
nullstellensatz
6
Beaucoup de gens viendront sur cette page à la recherche d'arrondis;)
janjackson
33

Le résultat de roundest un flottant, alors faites attention (l'exemple provient de Python 2.6):

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

Vous serez mieux avec une chaîne formatée:

>>> "%.3f" % 1.923328437452
'1.923'
>>> "%.3f" % 1.23456
'1.235'
Ferdinand Beyer
la source
8
Sur mon Python, cela arrondit: '% .3f'% 1.23456 == '1.235'
David Z
C'est bien plus élégant que le non-sens de formatage manuel des chaînes, bon post!
rsethc
round(1.23456, 3)est 1.235et pas1.2350000000000001
Ahmad
1
@Ahmad pas nécessairement. L'exemple ici est tiré de Python 2.6 (notez la date de la réponse). Le formatage des chaînes a été amélioré dans Python 2.7 / 3.1, c'est probablement pourquoi vous obtenez des résultats différents. Néanmoins, les nombres à virgule flottante auront souvent des représentations de chaîne inattendues, voir: docs.python.org/3.6/tutorial/floatingpoint.html
Ferdinand Beyer
21
n = 1.923328437452
str(n)[:4]
john_dough
la source
3
Simple et pythonique. 4 est la taille du nombre entier, pas seulement les chiffres après le point.
GaTTaCa
4
Donc, si l'utilisateur entre par exemple 2, vous aurez un point décimal .à la fin de la chaîne - pas vraiment une bonne solution je pense.
Zelphir Kaltstahl
Ceci est spécifique à un cas à ce numéro. Comment serait-il généralisé à 11.923328437452?
polariser le
Meilleure réponse! vous pouvez également ajouter float () pour renvoyer un nombre: float (str (n) [: 4])
justSaid
14

À mon invite Python 2.7:

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

Facture
la source
11

Script python simple -

n = 1.923328437452
n = float(int(n * 1000))
n /=1000
Markroxor
la source
3
Réponse propre. Vous manquez juste une étape, pour reconvertir en flottant avant de diviser par 1000. Sinon, vous obtiendrez 1.
Yohan Obadia
9
def trunc(num, digits):
   sp = str(num).split('.')
   return '.'.join([sp[0], sp[1][:digits]])

Cela devrait fonctionner. Cela devrait vous donner la troncature que vous recherchez.

Mat
la source
9

La façon vraiment pythonique de le faire est

from decimal import *

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

ou plus court:

from decimal import Decimal as D, ROUND_DOWN

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

Mettre à jour

Habituellement, le problème n'est pas dans la troncature des flottants lui-même, mais dans l'utilisation incorrecte des nombres flottants avant l' arrondi.

Par exemple: int(0.7*3*100)/100 == 2.09.

Si vous êtes obligé d'utiliser des flottants (par exemple, vous accélérez votre code avec numba), il est préférable d'utiliser des cents comme "représentation interne" des prix: ( 70*3 == 210) et de multiplier / diviser les entrées / sorties.

Antony Hatchkins
la source
Parson me pour demander ça, mais ... pourquoi?
markroxor
@markroxor, je ne sais pas exactement de quoi vous parlez. En passant, le problème n'est généralement pas l'arrondi lui-même, mais l'utilisation incorrecte des nombres flottants avant l' arrondi. Par exemple int(0.7*3*100)/100 == 2.09. Où est passé mon 1 centime?
Antony Hatchkins
cela a du sens, pouvez-vous modifier votre réponse avec cette explication? Merci.
markroxor
Obtenir ImportError: cannot import name 'D', je crois que vous vouliez faire une importation nommée non?
Overdrivr
8

Tant de réponses données à cette question sont complètement fausses. Ils arrondissent les flottants (plutôt que tronqués) ou ne fonctionnent pas dans tous les cas.

C'est le meilleur résultat de Google lorsque je recherche 'Python truncate float', un concept qui est vraiment simple et qui mérite de meilleures réponses. Je suis d'accord avec Hatchkins que l'utilisation du decimalmodule est la manière pythonique de faire cela, donc je donne ici une fonction qui, je pense, répond correctement à la question, et qui fonctionne comme prévu dans tous les cas.

En remarque, les valeurs fractionnaires, en général, ne peuvent pas être représentées exactement par des variables binaires à virgule flottante (voir ici pour une discussion à ce sujet), c'est pourquoi ma fonction renvoie une chaîne.

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
la source
4

J'ai fait quelque chose comme ça:

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
la source
4

Tu peux faire:

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

essai:

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

la source
Cela ne tronque qu'avec des nombres positifs, les nombres négatifs arrondiront vers le bas (loin de zéro).
Aaron D
3

Si vous avez envie de mathémagique, cela fonctionne pour les nombres + cinq:

>>> v = 1.923328437452
>>> v - v % 1e-3
1.923
cs95
la source
Si je comprends bien, 1e-3 sera tronqué à 3 chiffres après le point. J'ai aimé cette réponse mais cela ne semble pas fonctionner pour 4 et 5.
egvo
2

Lors de l'utilisation d'un pandas df cela a fonctionné pour moi

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
la source
1

Je voulais juste mentionner que l’ancienne astuce "make round () with floor ()"

round(f) = floor(f+0.5)

peut être retourné pour créer un sol () à partir de round ()

floor(f) = round(f-0.5)

Bien que ces deux règles ne respectent pas les nombres négatifs, son utilisation est loin d'être idéale:

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
la source
1

int (16,5); cela donnera une valeur entière de 16, c'est-à-dire trunc, ne pourra pas spécifier de décimales, mais je suppose que vous pouvez le faire en

import math;

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

Voici un moyen simple:

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

pour num = 1,923328437452, cela donne 1,923

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

Une fonction générale et simple à utiliser:

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
la source
Génial! Le transtypage en int tronque les nombres positifs et négatifs.
Aaron D
1

Il existe une solution de contournement simple en python 3. Où couper J'ai défini avec une variable d'aide decPlace pour faciliter l'adaptation.

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

Production:

f = 1.1234

J'espère que ça aide.

MBreg
la source
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
la source
Bien mais tu n'as pas arrondi.
Alex
1

Variante courte et facile

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
mégajoe
la source
1

La plupart des réponses sont beaucoup trop compliquées à mon avis, qu'en est-il de cela?

digits = 2  # Specify how many digits you want

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

>>> 122.48

Recherche simplement l'index de «.» et tronquer à volonté (pas d'arrondi). Convertissez la chaîne en flottant comme étape finale.

Ou dans votre cas si vous obtenez un float en entrée et que vous voulez une chaîne en sortie:

fnum = str(122.485221)  # convert float to string first
truncated_float = fnum[:fnum.find('.') + digits + 1]  # string output
H123321
la source
Votre proposition est problématique si le nombre en cours de troncature est petit car vous perdriez beaucoup de précision avec le 0 en tête à droite de la virgule décimale. Mais ce problème est endémique au problème comme indiqué. Ce que j'essaie de dire, c'est que des chiffres significatifs sont la vraie réponse.
overcoil le
1
>>> floor((1.23658945) * 10**4) / 10**4
1.2365

# diviser et multiplier par 10 ** nombre de chiffres souhaités

JohnA
la source
0

utilisez numpy.round

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

Quelque chose d'assez simple pour tenir dans une liste-compréhension, sans bibliothèques ou autres dépendances externes. Pour Python> = 3.6, il est très simple d'écrire avec des chaînes f.

L'idée est de laisser la conversion de chaîne faire l'arrondi à un endroit de plus que nécessaire , puis de couper le dernier chiffre.

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

Bien sûr, il y a un arrondi ici (à savoir pour le quatrième chiffre), mais l'arrondi à un moment donné est inévitable. Dans le cas où la transition entre la troncature et l'arrondi est pertinente, voici un exemple légèrement meilleur:

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

Bonus: supprimer les zéros à droite

>>> 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
la source
0

L'idée de base donnée ici me semble être la meilleure approche pour ce problème. Malheureusement, il a reçu moins de votes tandis que la dernière réponse qui a plus de votes n'est pas complète (comme observé dans les commentaires). Espérons que l'implémentation ci-dessous fournit une solution courte et complète pour la troncature .

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

qui devrait prendre soin de tous les cas de coin trouvés ici et ici .

aak318
la source
-1

Suis aussi un débutant en python et après avoir utilisé quelques morceaux ici, j'offre mes deux cents

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

str (int (time.time ())) prendra le temps epoch comme int et le convertira en string et joindra avec ... str (datetime.now (). microsecond) [: 3] qui renvoie les microsecondes uniquement, convert chaîne et tronquer aux 3 premiers caractères

utilisateur1048839
la source
-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
la source
-3

Si vous voulez dire lors de l'impression, alors ce qui suit devrait fonctionner:

print '%.3f' % number
user44511
la source
2
Cela arrondit le nombre, cela ne tronque pas.
David Z