Come arrotondare un numero a cifre significative in Python


148

Ho bisogno di arrotondare un float per essere visualizzato in un'interfaccia utente. Ad esempio, a una cifra significativa:

1234 -> 1000

0,12 -> 0,1

0,012 -> 0,01

0,062 -> 0,06

6253 -> 6000

1999 -> 2000

C'è un modo carino per farlo usando la libreria Python, o devo scriverlo da solo?



quale uscita ti aspetti per 0,062 e 6253?
lamirap,

Il pacchetto to-precision ora fa questo. La mia risposta postata spiega come questo si applica.
William Rusnack,

Risposte:


146

Puoi usare numeri negativi per arrotondare numeri interi:

>>> round(1234, -3)
1000.0

Quindi se hai bisogno solo della cifra più significativa:

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

Probabilmente dovrai occuparti di trasformare float in numero intero se è maggiore di 1.


3
Questa è la soluzione corretta L'uso log10è l'unico modo corretto per determinare come arrotondarlo.
Wolph,

73
round_to_n = lambda x, n: round (x, -int (floor (log10 (x))) + (n - 1))
Roy Hyunjin Han

28
Dovresti usare log10(abs(x)), altrimenti i numeri negativi falliranno (E tratteranno x == 0separatamente ovviamente)
Tobias Kienzler

2
Ho creato un pacchetto che lo fa ora ed è probabilmente più facile e più robusto di questo. Link post , Link repo . Spero che questo ti aiuti!
William Rusnack,

2
round_to_n = lambda x, n: x if x == 0 else round(x, -int(math.floor(math.log10(abs(x)))) + (n - 1))protegge contro x==0e x<0grazie @RoyHyunjinHan e @TobiasKienzler. Non protetto da undefined come math.inf o spazzatura come None etc
AJP

98

% g nella formattazione delle stringhe formatterà un float arrotondato ad un numero di cifre significative. A volte userà la notazione scientifica 'e', ​​quindi converti la stringa arrotondata in un float e poi attraverso la formattazione della stringa% 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'

7
Il requisito del PO era che il 1999 fosse formattato come "2000", non come "2000.0". Non riesco a vedere un modo banale per cambiare il tuo metodo per raggiungere questo obiettivo.
Tim Martin,

1
È proprio quello che ho sempre desiderato! dove l'hai trovato?
djhaskin987,

12
Si noti che il comportamento di% g non è sempre corretto. In particolare, regola sempre gli zeri finali anche se significativi. Il numero 1.23400 ha 6 cifre significative, ma "% .6g"% (1.23400) produrrà "1.234" che non è corretto. Maggiori dettagli in questo post del blog: randlet.com/blog/python-significant-figures-format
randlet

3
Proprio come il metodo di risposta di Evgeny, questo non riesce a correttamente rotonda 0.075a 0.08. Ritorna 0.07invece.
Gabriel

1
round_sig = lambda f,p: float(('%.' + str(p) + 'e') % f)ti permette di regolare il numero di cifre significative!
denizb,

49

Se vuoi avere un altro decimale significativo (altrimenti uguale a 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

8
round_sig (-0.0232) -> errore del dominio matematico, potresti voler aggiungere un abs () lì;)
dgorissen

2
Proprio come i metodi in risposte di Evgeny e Peter Graham, questo non riesce a correttamente rotonda 0.075a 0.08. Ritorna 0.07invece.
Gabriel

3
Inoltre non riesce per round_sig (0).
Yuval Atzmon,

2
@Gabriel Questa è una "caratteristica" integrata di Python in esecuzione sul tuo computer e si manifesta nel suo comportamento della funzione round. docs.python.org/2/tutorial/floatingpoint.html#tut-fp-issues
Novizio C

1
@Gabriel Ho aggiunto una risposta che spiega perché dovresti aspettarti di recuperare 0.7 dall'arrotondamento di "0,075"! vedi stackoverflow.com/a/56974893/1358308
Sam Mason,

30
f'{float(f"{i:.1g}"):g}'
# Or with Python <3.6,
'{:g}'.format(float('{:.1g}'.format(i)))

Questa soluzione è diversa da tutte le altre perché:

  1. è esattamente risolve il problema OP
  2. esso non ha bisogno di alcun pacchetto aggiuntivo
  3. essa non ha bisogno di alcun definita dall'utente funzione ausiliaria o operazione matematica

Per un numero arbitrario ndi cifre significative, è possibile utilizzare:

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

Test:

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

Nota : con questa soluzione, non è possibile adattare dinamicamente il numero di cifre significative dall'input poiché non esiste un modo standard per distinguere i numeri con numeri diversi di zeri finali ( 3.14 == 3.1400). Se è necessario, sono necessarie funzioni non standard come quelle fornite nel pacchetto di precisione .


Cordiali saluti: Ho trovato questa soluzione indipendentemente da eddygeek mentre stavo cercando di risolvere lo stesso problema in uno dei miei codici. Ora mi rendo conto che la mia soluzione è, ovviamente, quasi identica alla sua (ho appena notato l'output errato e non mi sono preoccupato di leggere il codice, errore mio). Probabilmente sarebbe bastato un breve commento sotto la sua risposta invece di una nuova risposta ... L'unica differenza (chiave) è il doppio uso del :gformattatore che conserva numeri interi.
Falken,

Wow, la tua risposta deve essere davvero letta dall'alto verso il basso;) Questo trucco a doppio cast è sporco, ma pulito. (Si noti che il 1999 è stato formattato come 2000.0 suggerisce 5 cifre significative, quindi deve essere {:g}ripassato.) In generale, i numeri interi con zeri finali sono ambigui per quanto riguarda le cifre significative, a meno che non venga utilizzata una tecnica (come la linea sopra l'ultima significativa).
Tomasz Gandor,

8

Ho creato il pacchetto con precisione che fa quello che vuoi. Ti permette di dare ai tuoi numeri cifre più o meno significative.

Produce anche notazione standard, scientifica e ingegneristica con un numero specificato di cifre significative.

Nella risposta accettata c'è la linea

>>> round_to_1(1234243)
1000000.0

Questo in realtà specifica 8 sig fichi. Per il numero 1234243 la mia biblioteca mostra solo una cifra significativa:

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

Arrotonderà anche l'ultima cifra significativa e potrà scegliere automaticamente quale notazione usare se non viene specificata una notazione:

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

Ora sto cercando lo stesso, ma applicato a un panda df
mhoff il

@mhoff probabilmente puoi usare la mappa dei panda con una lambda. lambda x: to_precision(x, 2)
William Rusnack,

Aggiungi questo a (PyPI) [ pypi.org/] . Non c'è niente di simile che esiste lì, per quanto ne so.
Morgoth,

questo è un ottimo pacchetto ma penso che la maggior parte delle funzionalità siano ora nel modulo
sigfig

1
ha un bug: std_notation (9.999999999999999e-05, 3) dà: '0.00010' che è solo 2 cifre significative
Boris Mulder

5

Per arrotondare un numero intero a 1 cifra significativa, l'idea di base è di convertirlo in un punto mobile con 1 cifra prima del punto e arrotondarlo, quindi riconvertirlo nella sua dimensione intera originale.

Per fare questo dobbiamo conoscere la potenza maggiore di 10 in meno dell'intero. Per questo possiamo usare floor della funzione log 10.

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)

1
Più uno per la soluzione che funziona senza round di Python (.., cifre) e senza stringhe!
Steve Rogers,

5

Per rispondere direttamente alla domanda, ecco la mia versione usando i nomi dalla funzione 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 mia ragione principale per pubblicare questa risposta sono i commenti che lamentano che "0,075" arriva a 0,07 anziché a 0,08. Ciò è dovuto, come sottolineato da "Novizio C", a una combinazione di aritmetica in virgola mobile avente sia una precisione finita che una rappresentazione base-2 . Il numero più vicino a 0,075 che può essere effettivamente rappresentato è leggermente più piccolo, quindi l'arrotondamento esce in modo diverso da quello che potresti aspettarti ingenuamente.

Si noti inoltre che ciò si applica a qualsiasi uso dell'aritmetica in virgola mobile non decimale, ad esempio C e Java hanno entrambi lo stesso problema.

Per mostrare più in dettaglio, chiediamo a Python di formattare il numero in formato "esadecimale":

0.075.hex()

che ci dà: 0x1.3333333333333p-4. La ragione di ciò è che la normale rappresentazione decimale spesso comporta l'arrotondamento e quindi non è il modo in cui il computer "vede" effettivamente il numero. Se non sei abituato a questo formato, un paio di riferimenti utili sono i documenti Python e lo standard C. .

Per mostrare come funzionano questi numeri, possiamo tornare al nostro punto di partenza facendo:

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

che dovrebbe stampare 0.075. 16**13è perché ci sono 13 cifre esadecimali dopo il punto decimale e2**-4 è perché gli esponenti esadecimali sono base-2.

Ora abbiamo un'idea di come sono rappresentati i float, possiamo usare il decimalmodulo per darci un po 'più di precisione, mostrandoci cosa sta succedendo:

from decimal import Decimal

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

dare: 0.07499999999999999722444243844e si spera spiegando perché round(0.075, 2)valuta0.07


1
Questa è una grande spiegazione del perché 0,075 è arrotondato per difetto a 0,07 a livello di codice , ma a noi (nelle scienze fisiche) è stato insegnato a arrotondare sempre non per difetto. Quindi il comportamento previsto è in realtà avere 0,08 come risultato, nonostante i problemi di precisione in virgola mobile.
Gabriel,

1
Non sono sicuro di dove sia la tua confusione: quando inserisci 0,075 stai effettivamente inserendo ~ 0,07499 (come sopra), che viene arrotondato secondo le normali regole matematiche. se stavi utilizzando un tipo di dati (come il virgola mobile decimale ) che potrebbe rappresentare 0,075, dovrebbe effettivamente arrotondare a 0,08
Sam Mason,

Non sono confuso. Quando inserisco 0,075, in realtà inserisco 0,075. Qualunque cosa accada nella matematica in virgola mobile all'interno del codice non mi interessa.
Gabriel,

@Gabriel: E se tu fossi entrato deliberatamente 0.074999999999999999, cosa ti aspetteresti di ottenere in quel caso?
Mark Dickinson,

@MarkDickinson che dipende. Una cifra significativa: 0,07, due: 0,075.
Gabriel,

4
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

Spero che prenda il meglio di tutte le risposte sopra (meno la possibilità di metterlo come una riga lambda;)). Non hai ancora esplorato, sentiti libero di modificare questa risposta:

round_to_n(1e15 + 1, 11)  # 999999999999999.9

4

Ho modificato la soluzione di indgar per gestire numeri negativi e piccoli numeri (incluso zero).

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)

Perché non testare se x == 0? Se ami una linea, basta return 0 if x==0 else round(...).
pjvandehaar,

2
@pjvandehaar, hai ragione per il caso generale e avrei dovuto inserirlo. Inoltre, per i calcoli numerici che devo eseguire occasionalmente otteniamo numeri come 1e-15. Nella nostra applicazione vogliamo che un confronto di due piccoli numeri (uno dei quali potrebbe essere zero) sia considerato uguale. Inoltre alcune persone vogliono arrotondare piccoli numeri (potrebbe essere 1e-9, 1e-15 o anche 1e-300) a zero.
ryan281,

1
Interessante. Grazie per averlo spiegato. In tal caso, mi piace molto questa soluzione.
pjvandehaar,

@Morgoth Questo è un problema interessante e difficile. Come hai sottolineato, il valore stampato non mostra le 3 cifre significative, ma il valore è corretto (ad es 0.970 == 0.97.). Penso che potresti usare alcune delle altre soluzioni di stampa come f'{round_sig(0.9701, sig=3):0.3f}'se volessi stampare lo zero.
ryan281

3

Se vuoi arrotondare senza coinvolgere le stringhe, il link che ho trovato sepolto nei commenti sopra:

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

mi colpisce come meglio. Quindi, quando si stampa con qualsiasi descrittore di formattazione di stringa, si ottiene un risultato ragionevole e è possibile utilizzare la rappresentazione numerica per altri scopi di calcolo.

Il codice al link è un tre righe: def, doc e return. Ha un bug: è necessario verificare la presenza di logaritmi che esplodono. Questo è facile. Confronta l'input con sys.float_info.min. La soluzione completa è:

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

Funziona con qualsiasi valore numerico scalare e n può essere un floatse è necessario spostare la risposta per qualche motivo. Puoi effettivamente spingere il limite a:

sys.float_info.min*sys.float_info.epsilon

senza provocare un errore, se per qualche motivo stai lavorando con valori minuscoli.


2

Non riesco a pensare a nulla che sarebbe in grado di gestire questo fuori dalla scatola. Ma è abbastanza ben gestito per i numeri in virgola mobile.

>>> round(1.2322, 2)
1.23

I numeri interi sono più complicati. Non sono memorizzati come base 10 in memoria, quindi i luoghi significativi non sono una cosa naturale da fare. È abbastanza banale da implementare una volta che sono una stringa.

O per numeri interi:

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

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

Se desideri creare una funzione che gestisca qualsiasi numero, la mia preferenza sarebbe quella di convertirli entrambi in stringhe e cercare un punto decimale per decidere cosa fare:

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

Un'altra opzione è quella di verificare il tipo. Questo sarà molto meno flessibile e probabilmente non giocherà bene con altri numeri come Decimaloggetti:

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

Il solo pasticcio con le stringhe non arrotonda i numeri. Il 1999 arrotondato a 1 cifra significativa è 2000, non 1000.
Peter Graham,

C'è una buona discussione di questo problema archiviato su ActiveState code.activestate.com/lists/python-tutor/70739
Tim McNamara

2

La risposta pubblicata è stata la migliore disponibile quando fornita, ma ha una serie di limitazioni e non produce cifre significative tecnicamente corrette.

numpy.format_float_positional supporta direttamente il comportamento desiderato. Il frammento seguente restituisce il float xformattato su 4 cifre significative, con la notazione scientifica soppressa.

import numpy as np
x=12345.6
np.format_float_positional(x, precision=4, unique=False, fractional=False, trim='k')
> 12340.

La documentazione (spostata su numpy.org/doc/stable/reference/generated/… ) afferma che questa funzione implementa l'algoritmo Dragon4 (di Steele & White 1990, dl.acm.org/doi/pdf/10.1145/93542.93559 ). Produce risultati fastidiosi, ad es print(*[''.join([np.format_float_positional(.01*a*n,precision=2,unique=False,fractional=False,trim='k',pad_right=5) for a in [.99, .999, 1.001]]) for n in [8,9,10,11,12,19,20,21]],sep='\n'). Non ho controllato Dragon4 stesso.
Rainald62,

0

Mi sono imbattuto anche in questo, ma avevo bisogno del controllo sul tipo di arrotondamento. Pertanto, ho scritto una funzione rapida (vedi codice sotto) che può prendere in considerazione valore, tipo di arrotondamento e cifre significative desiderate.

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

Uso della formattazione di nuovo stile di python 2.6+ (poiché% -style è obsoleto):

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

In Python 2.7+ puoi omettere le 0s iniziali .


Con quale versione di Python? Python 3.6.3 | Anaconda, Inc. | (impostazione predefinita, 13 ottobre 2017, 12:02:49) presenta lo stesso vecchio problema di arrotondamento. "{0}". Format (float ("{0: .1g}". Format (0.075))) produce "0,07", non "0,08"
Don Mclachlan,

@DonMclachlan Ho aggiunto una spiegazione del perché questo è previsto in stackoverflow.com/a/56974893/1358308
Sam Mason

0

Questa funzione esegue un round normale se il numero è maggiore di 10 ** (- posizioni decimali), altrimenti aggiunge più decimali fino a raggiungere il numero di posizioni decimali significative:

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)

Spero che sia d'aiuto.


0

https://stackoverflow.com/users/1391441/gabriel , il seguente indirizzo risponde alla tua preoccupazione per rnd (.075, 1)? Avvertenza: restituisce valore come 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

Ciò restituisce una stringa, in modo che i risultati senza parti frazionarie e i valori piccoli che altrimenti apparirebbero nella notazione E sono mostrati correttamente:

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

Data una domanda con una risposta così completa, perché non aggiungerne un'altra

Questo si adatta un po 'meglio alla mia estetica, anche se molti dei precedenti sono comparabili

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)

Questo funziona per singoli numeri e array intorpiditi e dovrebbe funzionare bene per i numeri negativi.

C'è un ulteriore passaggio che potremmo aggiungere: np.round () restituisce un numero decimale anche se arrotondato è un numero intero (vale a dire per significativoFigures = 2 potremmo aspettarci di tornare indietro di -460 ma invece otteniamo -460.0). Possiamo aggiungere questo passaggio per correggere ciò:

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

Sfortunatamente, questo passaggio finale non funzionerà per una serie di numeri - lo lascerò a te caro lettore per capire se ne hai bisogno.


0

Il pacchetto / libreria sigfig copre questo. Dopo l' installazione è possibile effettuare le seguenti operazioni:

>>> from sigfig import round
>>> round(1234, 1)
1000
>>> round(0.12, 1)
0.1
>>> round(0.012, 1)
0.01
>>> round(0.062, 1)
0.06
>>> round(6253, 1)
6000
>>> round(1999, 1)
2000

0
import math

  def sig_dig(x, n_sig_dig):
      num_of_digits = len(str(x).replace(".", ""))
      if n_sig_dig >= num_of_digits:
          return x
      n = math.floor(math.log10(x) + 1 - n_sig_dig)
      result = round(10 ** -n * x) * 10 ** n
      return float(str(result)[: n_sig_dig + 1])


    >>> sig_dig(1234243, 3)
    >>> sig_dig(243.3576, 5)

        1230.0
        243.36
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.