web-dev-qa-db-fra.com

Comment arrondir un nombre à des chiffres significatifs dans Python

J'ai besoin d'arrondir un float pour être affiché dans une interface utilisateur. E.g, à un chiffre significatif:

1234 -> 1000

0,12 -> 0,1

0,012 -> 0,01

0,062 -> 0,06

6253 -> 6000

1999 -> 2000

Existe-t-il un bon moyen de faire cela en utilisant la bibliothèque Python, ou dois-je l'écrire moi-même?

122
Peter Graham

Vous pouvez utiliser des nombres négatifs pour arrondir des nombres entiers:

>>> round(1234, -3)
1000.0

Donc, si vous n'avez besoin que du chiffre le plus significatif:

>>> from math import log10, floor
>>> def round_to_1(x):
...   return round(x, -int(floor(log10(abs(x)))))
... 
>>> round_to_1(0.0232)
0.02
>>> round_to_1(1234243)
1000000.0
>>> round_to_1(13)
10.0
>>> round_to_1(4)
4.0
>>> round_to_1(19)
20.0

Vous devrez probablement vous occuper de transformer float en entier s'il est supérieur à 1.

130
Evgeny

% g dans le format de chaîne formatera un float arrondi à un certain nombre de chiffres significatifs. Il utilisera parfois la notation scientifique 'e', ​​aussi reconvertissez la chaîne arrondie en float puis en formatant la chaîne% s.

>>> '%s' % float('%.1g' % 1234)
'1000'
>>> '%s' % float('%.1g' % 0.12)
'0.1'
>>> '%s' % float('%.1g' % 0.012)
'0.01'
>>> '%s' % float('%.1g' % 0.062)
'0.06'
>>> '%s' % float('%.1g' % 6253)
'6000.0'
>>> '%s' % float('%.1g' % 1999)
'2000.0'
93
Peter Graham

Si vous voulez avoir autre chose qu'une décimale significative (sinon identique à Evgeny):

>>> from math import log10, floor
>>> def round_sig(x, sig=2):
...   return round(x, sig-int(floor(log10(abs(x))))-1)
... 
>>> round_sig(0.0232)
0.023
>>> round_sig(0.0232, 1)
0.02
>>> round_sig(1234243, 3)
1230000.0
46
indgar
print('{:g}'.format(float('{:.1g}'.format(12.345))))

Cette solution est différente de toutes les autres car:

  1. il résout exactement la question
  2. il n'a pas besoin d'un paquet supplémentaire
  3. non ne nécessite aucune fonction auxiliaire définie par l'utilisateur ou opération mathématique

Pour un nombre arbitraire n de chiffres significatifs, vous pouvez utiliser:

print('{:g}'.format(float('{:.{p}g}'.format(i, p=n))))

Tester:

a = [1234, 0.12, 0.012, 0.062, 6253, 1999, -3.14, 0., -48.01, 0.75]
b = ['{:g}'.format(float('{:.1g}'.format(i))) for i in a]
# b == ['1000', '0.1', '0.01', '0.06', '6000', '2000', '-3', '0', '-50', '0.8']

Remarque: avec cette solution, il n'est pas possible d'adapter dynamiquement le nombre de chiffres significatifs à partir de l'entrée car il n'existe pas de méthode standard pour distinguer les nombres avec des nombres différents de zéros de fin (3.14 == 3.1400). Si vous avez besoin de le faire, des fonctions non standard telles que celles fournies dans le package to-precision sont nécessaires.

13
Falken

J'ai créé le package à la précision qui fait ce que vous voulez. Cela vous permet de donner à vos chiffres des chiffres plus ou moins significatifs.

Il génère également des notations standard, scientifiques et techniques avec un nombre spécifié de chiffres significatifs.

Dans la réponse acceptée, il y a la ligne

>>> round_to_1(1234243)
1000000.0

Cela spécifie en fait 8 fig. Pour le numéro 1234243, ma bibliothèque n'affiche qu'un seul chiffre significatif:

>>> from to_precision import to_precision
>>> to_precision(1234243, 1, 'std')
'1000000'
>>> to_precision(1234243, 1, 'sci')
'1e6'
>>> to_precision(1234243, 1, 'eng')
'1e6'

Il arrondira également le dernier chiffre significatif et pourra choisir automatiquement quelle notation utiliser si aucune notation n'est spécifiée:

>>> to_precision(599, 2)
'600'
>>> to_precision(1164, 2)
'1.2e3'
6
William Rusnack

Pour arrondir un entier à 1 chiffre significatif, l’idée de base est de le convertir en une virgule flottante avec 1 chiffre avant le point et de l’arrondir, puis de le reconvertir à sa taille d’entier originale.

Pour ce faire, nous devons connaître la plus grande puissance de 10 de moins que le nombre entier. Pour cela, nous pouvons utiliser la fonction log 10 de l’étage.

from math import log10, floor
def round_int(i,places):
    if i == 0:
        return 0
    isign = i/abs(i)
    i = abs(i)
    if i < 1:
        return 0
    max10exp = floor(log10(i))
    if max10exp+1 < places:
        return i
    sig10pow = 10**(max10exp-places+1)
    floated = i*1.0/sig10pow
    defloated = round(floated)*sig10pow
    return int(defloated*isign)
5

J'ai modifié la solution d'Indgar pour gérer les nombres négatifs et les petits nombres (y compris zéro).

from math import log10, floor
def round_sig(x, sig=6, small_value=1.0e-9):
    return round(x, sig - int(floor(log10(max(abs(x), abs(small_value))))) - 1)
4
ryan281
def round_to_n(x, n):
    if not x: return 0
    power = -int(math.floor(math.log10(abs(x)))) + (n - 1)
    factor = (10 ** power)
    return round(x * factor) / factor

round_to_n(0.075, 1)      # 0.08
round_to_n(0, 1)          # 0
round_to_n(-1e15 - 1, 16) # 1000000000000001.0

J'espère que nous prendrons la meilleure de toutes les réponses ci-dessus (moins de pouvoir le formuler comme un lambda à une ligne;)). N'avez pas encore exploré, n'hésitez pas à modifier cette réponse:

round_to_n(1e15 + 1, 11)  # 999999999999999.9
4
AJP

Si vous voulez arrondir sans utiliser de chaînes, le lien que j'ai trouvé enterré dans les commentaires ci-dessus:

http://code.activestate.com/lists/python-tutor/70739/

me frappe comme le meilleur. Ensuite, lorsque vous imprimez avec des descripteurs de format de chaîne, vous obtenez une sortie raisonnable et vous pouvez utiliser la représentation numérique à d'autres fins de calcul.

Le code sur le lien est un trois lignes: def, doc et return. Il y a un bug: vous devez vérifier les logarithmes qui explosent. C'est facile. Comparez l'entrée à sys.float_info.min. La solution complète est:

import sys,math

def tidy(x, n):
"""Return 'x' rounded to 'n' significant digits."""
y=abs(x)
if y <= sys.float_info.min: return 0.0
return round( x, int( n-math.ceil(math.log10(y)) ) )

Cela fonctionne pour n'importe quelle valeur numérique scalaire, et n peut être un float si vous devez décaler la réponse pour une raison quelconque. Vous pouvez réellement pousser la limite à:

sys.float_info.min*sys.float_info.epsilon

sans provoquer d'erreur, si pour une raison quelconque vous travaillez avec des valeurs minuscules.

3
getting_sleepy

Pour répondre directement à la question, voici ma version utilisant l'attribution de nom à partir de fonction R :

import math

def signif(x, digits=6):
    if x == 0 or not math.isfinite(x):
        return x
    digits -= math.ceil(math.log10(abs(x)))
    return round(x, digits)

La raison principale pour laquelle j'ai posté cette réponse est les commentaires qui se plaignent que "0,075" arrondit à 0,07 au lieu de 0,08. Ceci est dû, comme indiqué par "Novice C", à une combinaison d'arithmétique en virgule flottante ayant les deux précision finie et représentation en base 2 . Le nombre le plus proche de 0,075 que vous pouvez réellement représenter est légèrement plus petit. Par conséquent, l'arrondi est différent de celui auquel vous vous attendiez naïvement.

Notez également que cela s'applique à toute utilisation d'arithmétique en virgule flottante non décimale, par ex. C et Java ont tous deux le même problème.

Pour montrer plus en détail, nous demandons à Python de formater le nombre au format "hex":

0.075.hex()

ce qui nous donne: 0x1.3333333333333p-4. La raison en est que la représentation décimale normale implique souvent l’arrondissement et n’est donc pas la façon dont l’ordinateur "voit" le nombre. Si vous n'êtes pas habitué à ce format, quelques références utiles sont Python docs et C standard .

Pour montrer comment ces chiffres fonctionnent un peu, nous pouvons revenir à notre point de départ en faisant:

0x13333333333333 / 16**13 * 2**-4

qui devrait imprimer 0.075. 16**13 Est dû au fait qu'il y a 13 chiffres hexadécimaux après le point décimal et 2**-4 Au fait que les exposants hexadécimaux sont en base-2.

Maintenant que nous avons une idée de la façon dont les flottants sont représentés, nous pouvons utiliser le module decimal pour nous donner plus de précision, nous montrant ce qui se passe:

from decimal import Decimal

Decimal(0x13333333333333) / 16**13 / 2**4

donnant: 0.07499999999999999722444243844 et en espérant expliquer pourquoi round(0.075, 2) est évalué à 0.07

2
Sam Mason

Je ne peux imaginer quoi que ce soit qui serait capable de gérer cela en dehors de la boîte. Mais c'est assez bien géré pour les nombres à virgule flottante.

>>> round(1.2322, 2)
1.23

Les entiers sont plus difficiles. Ils ne sont pas stockés en mémoire 10 dans la base 10, de sorte que les lieux importants ne sont pas une chose naturelle à faire. C’est assez simple à implémenter une fois qu’ils sont une chaîne.

Ou pour les entiers:

>>> def intround(n, sigfigs):
...   n = str(n)
...   return n[:sigfigs] + ('0' * (len(n)-(sigfigs)))

>>> intround(1234, 1)
'1000'
>>> intround(1234, 2)

Si vous souhaitez créer une fonction qui gère n'importe quel nombre, ma préférence serait de les convertir en chaînes et de rechercher un point décimal pour décider quoi faire:

>>> def roundall1(n, sigfigs):
...   n = str(n)
...   try:
...     sigfigs = n.index('.')
...   except ValueError:
...     pass
...   return intround(n, sigfigs)

Une autre option consiste à vérifier le type. Ce sera beaucoup moins flexible et ne jouera probablement pas bien avec d'autres nombres tels que Decimal objets:

>>> def roundall2(n, sigfigs):
...   if type(n) is int: return intround(n, sigfigs)
...   else: return round(n, sigfigs)
2
Tim McNamara

Étant donné une question si bien répondu, pourquoi ne pas ajouter un autre

Cela convient un peu mieux à mon esthétique, même si beaucoup de ce qui précède sont comparables

import numpy as np

number=-456.789
significantFigures=4

roundingFactor=significantFigures - int(np.floor(np.log10(np.abs(number)))) - 1
rounded=np.round(number, roundingFactor)

string=rounded.astype(str)

print(string)

Cela fonctionne pour les nombres individuels et les tableaux numpy, et devrait fonctionner correctement pour les nombres négatifs.

Nous pourrions ajouter une étape supplémentaire - np.round () renvoie un nombre décimal, même si arrondi est un entier (par exemple, pour significatifFigures = 2, nous pourrions nous attendre à -460 mais à -460.0). Nous pouvons ajouter cette étape pour corriger cela:

if roundingFactor<=0:
    rounded=rounded.astype(int)

Malheureusement, cette dernière étape ne fonctionnera pas pour toute une série de chiffres - je vous laisse le soin de vous expliquer, cher lecteur, si vous en avez besoin.

0
Zephyr

Cette fonction effectue un tour normal si le nombre est supérieur à 10 ** (- décimales_positions), sinon ajoute plus de décimale jusqu'à ce que le nombre de positions décimales significatives soit atteint:

def smart_round(x, decimal_positions):
    dp = - int(math.log10(abs(x))) if x != 0.0 else int(0)
    return round(float(x), decimal_positions + dp if dp > 0 else decimal_positions)

J'espère que ça aide.

0
Ettore Galli

Utilisation de python 2.6+ nouvelle mise en forme du style (% -style est obsolète):

>>> "{0}".format(float("{0:.1g}".format(1216)))
'1000.0'
>>> "{0}".format(float("{0:.1g}".format(0.00356)))
'0.004'

Dans python 2.7+, vous pouvez omettre le 0s.

0
eddygeek

Cela retourne une chaîne, de sorte que les résultats sans parties décimales, ainsi que les petites valeurs qui apparaissent autrement en notation E sont affichées correctement:

def sigfig(x, num_sigfig):
    num_decplace = num_sigfig - int(math.floor(math.log10(abs(x)))) - 1
    return '%.*f' % (num_decplace, round(x, num_decplace))
0
Gnubie

https://stackoverflow.com/users/1391441/gabriel , le texte suivant répond-il à vos préoccupations concernant rnd (.075, 1)? Avertissement: renvoie la valeur sous forme de float

def round_to_n(x, n):
    fmt = '{:1.' + str(n) + 'e}'    # gives 1.n figures
    p = fmt.format(x).split('e')    # get mantissa and exponent
                                    # round "extra" figure off mantissa
    p[0] = str(round(float(p[0]) * 10**(n-1)) / 10**(n-1))
    return float(p[0] + 'e' + p[1]) # convert str to float

>>> round_to_n(750, 2)
750.0
>>> round_to_n(750, 1)
800.0
>>> round_to_n(.0750, 2)
0.075
>>> round_to_n(.0750, 1)
0.08
>>> math.pi
3.141592653589793
>>> round_to_n(math.pi, 7)
3.141593
0
Don Mclachlan

Je me suis heurté à cela aussi, mais j'avais besoin de contrôler le type d'arrondi. Ainsi, j'ai écrit une fonction rapide (voir le code ci-dessous) qui peut prendre en compte la valeur, le type d'arrondi et les chiffres significatifs souhaités.

import decimal
from math import log10, floor

def myrounding(value , roundstyle='ROUND_HALF_UP',sig = 3):
    roundstyles = [ 'ROUND_05UP','ROUND_DOWN','ROUND_HALF_DOWN','ROUND_HALF_UP','ROUND_CEILING','ROUND_FLOOR','ROUND_HALF_EVEN','ROUND_UP']

    power =  -1 * floor(log10(abs(value)))
    value = '{0:f}'.format(value) #format value to string to prevent float conversion issues
    divided = Decimal(value) * (Decimal('10.0')**power) 
    roundto = Decimal('10.0')**(-sig+1)
    if roundstyle not in roundstyles:
        print('roundstyle must be in list:', roundstyles) ## Could thrown an exception here if you want.
    return_val = decimal.Decimal(divided).quantize(roundto,rounding=roundstyle)*(decimal.Decimal(10.0)**-power)
    nozero = ('{0:f}'.format(return_val)).rstrip('0').rstrip('.') # strips out trailing 0 and .
    return decimal.Decimal(nozero)


for x in list(map(float, '-1.234 1.2345 0.03 -90.25 90.34543 9123.3 111'.split())):
    print (x, 'rounded UP: ',myrounding(x,'ROUND_UP',3))
    print (x, 'rounded normal: ',myrounding(x,sig=3))
0
drew.ray