Hướng dẫn python sigfig module

You can use negative numbers to round integers:

>>> round(1234, -3)
1000.0

Thus if you need only most significant digit:

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

You'll probably have to take care of turning float to integer if it's bigger than 1.

answered Aug 5, 2010 at 2:57

13

%g in string formatting will format a float rounded to some number of significant figures. It will sometimes use 'e' scientific notation, so convert the rounded string back to a float then through %s string formatting.

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

answered Aug 5, 2010 at 4:24

Peter GrahamPeter Graham

10.9k7 gold badges38 silver badges42 bronze badges

10

If you want to have other than 1 significant decimal (otherwise the same as 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

Hướng dẫn python sigfig module

Stephen Rauch

45.6k30 gold badges105 silver badges126 bronze badges

answered Aug 5, 2010 at 9:49

indgarindgar

6794 silver badges2 bronze badges

8

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

This solution is different from all of the others because:

  1. it exactly solves the OP question
  2. it does not need any extra package
  3. it does not need any user-defined auxiliary function or mathematical operation

For an arbitrary number n of significant figures, you can use:

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

Note: with this solution, it is not possible to adapt the number of significant figures dynamically from the input because there is no standard way to distinguish numbers with different numbers of trailing zeros (3.14 == 3.1400). If you need to do so, then non-standard functions like the ones provided in the to-precision package are needed.

Andy Jones

4,4252 gold badges17 silver badges21 bronze badges

answered Feb 15, 2018 at 17:10

FalkenFalken

7495 silver badges8 bronze badges

9

I have created the package to-precision that does what you want. It allows you to give your numbers more or less significant figures.

It also outputs standard, scientific, and engineering notation with a specified number of significant figures.

In the accepted answer there is the line

>>> round_to_1(1234243)
1000000.0

That actually specifies 8 sig figs. For the number 1234243 my library only displays one significant figure:

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

It will also round the last significant figure and can automatically choose what notation to use if a notation isn't specified:

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

offby1

6,36829 silver badges45 bronze badges

answered May 23, 2017 at 11:59

7

To directly answer the question, here's my version using naming from the R function:

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)

My main reason for posting this answer are the comments complaining that "0.075" rounds to 0.07 rather than 0.08. This is due, as pointed out by "Novice C", to a combination of floating point arithmetic having both finite precision and a base-2 representation. The nearest number to 0.075 that can actually be represented is slightly smaller, hence rounding comes out differently than you might naively expect.

Also note that this applies to any use of non-decimal floating point arithmetic, e.g. C and Java both have the same issue.

To show in more detail, we ask Python to format the number in "hex" format:

0.075.hex()

which gives us: 0x1.3333333333333p-4. The reason for doing this is that the normal decimal representation often involves rounding and hence is not how the computer actually "sees" the number. If you're not used to this format, a couple of useful references are the Python docs and the C standard.

To show how these numbers work a bit, we can get back to our starting point by doing:

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

which should should print out 0.075. 16**13 is because there are 13 hexadecimal digits after the decimal point, and 2**-4 is because hex exponents are base-2.

Now we have some idea of how floats are represented we can use the decimal module to give us some more precision, showing us what's going on:

from decimal import Decimal

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

giving: 0.07499999999999999722444243844 and hopefully explaining why round(0.075, 2) evaluates to 0.07

answered Jul 10, 2019 at 16:21

Sam MasonSam Mason

13.5k1 gold badge35 silver badges51 bronze badges

7

To round an integer to 1 significant figure the basic idea is to convert it to a floating point with 1 digit before the point and round that, then convert it back to its original integer size.

To do this we need to know the largest power of 10 less than the integer. We can use floor of the log 10 function for this.

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)

answered Mar 4, 2012 at 18:18

0

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

Hopefully taking the best of all the answers above (minus being able to put it as a one line lambda ;) ). Haven't explored yet, feel free to edit this answer:

round_to_n(1e15 + 1, 11)  # 999999999999999.9

I modified indgar's solution to handle negative numbers and small numbers (including 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)

answered Mar 26, 2016 at 19:50

4

I can't think of anything that would be able to handle this out of the box. But it's fairly well handled for floating point numbers.

>>> round(1.2322, 2)
1.23

Integers are trickier. They're not stored as base 10 in memory, so significant places isn't a natural thing to do. It's fairly trivial to implement once they're a string though.

Or for integers:

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

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

If you would like to create a function that handles any number, my preference would be to convert them both to strings and look for a decimal place to decide what to do:

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

Another option is to check for type. This will be far less flexible, and will probably not play nicely with other numbers such as Decimal objects:

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

answered Aug 5, 2010 at 1:35

Tim McNamaraTim McNamara

17.6k4 gold badges51 silver badges81 bronze badges

2

If you want to round without involving strings, the link I found buried in the comments above:

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

strikes me as best. Then when you print with any string formatting descriptors, you get a reasonable output, and you can use the numeric representation for other calculation purposes.

The code at the link is a three liner: def, doc, and return. It has a bug: you need to check for exploding logarithms. That is easy. Compare the input to sys.float_info.min. The complete solution is:

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

It works for any scalar numeric value, and n can be a float if you need to shift the response for some reason. You can actually push the limit to:

sys.float_info.min*sys.float_info.epsilon

without provoking an error, if for some reason you are working with miniscule values.

answered Apr 13, 2018 at 6:49

The posted answer was the best available when given, but it has a number of limitations and does not produce technically correct significant figures.

numpy.format_float_positional supports the desired behaviour directly. The following fragment returns the float x formatted to 4 significant figures, with scientific notation suppressed.

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

answered Oct 21, 2019 at 17:01

AutumnAutumn

2,6781 gold badge17 silver badges32 bronze badges

1

The sigfig package/library covers this. After installing you can do the following:

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

answered Nov 30, 2019 at 17:32

HyperActiveHyperActive

8939 silver badges11 bronze badges

Using python 2.6+ new-style formatting (as %-style is deprecated):

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

In python 2.7+ you can omit the leading 0s.

answered Jan 8, 2018 at 13:34

eddygeekeddygeek

3,8212 gold badges22 silver badges29 bronze badges

2

I adapted one of the answers. I like this:

def sigfiground(number:float, ndigits=3)->float:
    return float(f"{number:.{ndigits}g}")

I use it when I still want a float (I do formatting elsewhere).

answered Dec 17, 2021 at 15:21

MYKMYK

9843 silver badges11 bronze badges

1

I ran into this as well but I needed control over the rounding type. Thus, I wrote a quick function (see code below) that can take value, rounding type, and desired significant digits into account.

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

answered Sep 28, 2017 at 16:52

drew.raydrew.ray

211 silver badge3 bronze badges

This function does a normal round if the number is bigger than 10**(-decimal_positions), otherwise adds more decimal until the number of meaningful decimal positions is reached:

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)

Hope it helps.

answered Jul 19, 2018 at 9:38

https://stackoverflow.com/users/1391441/gabriel, does the following address your concern about rnd(.075, 1)? Caveat: returns value as a 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

answered Feb 22, 2019 at 18:46

This returns a string, so that results without fractional parts, and small values which would otherwise appear in E notation are shown correctly:

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

answered Mar 15, 2019 at 12:04

GnubieGnubie

2,5574 gold badges24 silver badges38 bronze badges

Given a question so thoroughly answered why not add another

This suits my aesthetic a little better, though many of the above are comparable

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)

This works for individual numbers and numpy arrays, and should function fine for negative numbers.

There's one additional step we might add - np.round() returns a decimal number even if rounded is an integer (i.e. for significantFigures=2 we might expect to get back -460 but instead we get -460.0). We can add this step to correct for that:

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

Unfortunately, this final step won't work for an array of numbers - I'll leave that to you dear reader to figure out if you need.

answered Aug 3, 2019 at 8:59

zephyrzephyr

2152 silver badges13 bronze badges

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

answered Mar 26, 2020 at 21:29

LetzerWilleLetzerWille

5,2034 gold badges22 silver badges26 bronze badges

4

Most of these answers involve the math, decimal and/or numpy imports or output values as strings. Here is a simple solution in base python that handles both large and small numbers and outputs a float:

def sig_fig_round(number, digits=3):
    power = "{:e}".format(number).split('e')[1]
    return round(number, -(int(power) - digits))

answered Dec 16, 2020 at 18:23

A simple variant using the standard decimal library

from decimal import Decimal

def to_significant_figures(v: float, n_figures: int) -> str:
    d = Decimal(v)
    d = d.quantize(Decimal((0, (), d.adjusted() - n_figures + 1)))
    return str(d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize())

Testing it

>>> to_significant_figures(1.234567, 3)
'1.23'
>>> to_significant_figures(1234567, 3)
'1230000'
>>> to_significant_figures(1.23, 7)
'1.23'
>>> to_significant_figures(123, 7)
'123'

answered Sep 4, 2021 at 21:47

This function takes both positive and negative numbers and does the proper significant digit rounding.

from math import floor

def significant_arithmetic_rounding(n, d):
    '''
    This function takes a floating point number and the no. of significant digit d, perform significant digits
    arithmetic rounding and returns the floating point number after rounding
    '''
    if n == 0:
        return 0
    else:
        # Checking whether the no. is negative or positive. If it is negative we will take the absolute value of it and proceed
        neg_flag = 0
        if n < 0:
            neg_flag = 1
            n = abs(n)
        
        n1 = n   
        # Counting the no. of digits to the left of the decimal point in the no.
        ld = 0
        while(n1 >= 1):
            n1 /= 10
            ld += 1
        
        n1 = n
        # Counting the no. of zeros to the right of the decimal point and before the first significant digit in the no.
        z = 0
        if ld == 0:
            while(n1 <= 0.1):
                n1 *= 10
                z += 1
        
        n1 = n
        # No. of digits to be considered after decimal for rounding
        rd = (d - ld) + z
        n1 *= 10**rd
    
        # Increase by 0.5 and take the floor value for rounding
        n1 = floor(n1+0.5)
        # Placing the decimal point at proper position
        n1 /= 10 ** rd
        # If the original number is negative then make it negative
        if neg_flag == 1:
            n1 = 0 - n1

        return n1

Testing:

>>> significant_arithmetic_rounding(1234, 3)
1230.0
>>> significant_arithmetic_rounding(123.4, 3)
123.0
>>> significant_arithmetic_rounding(0.0012345, 3)
0.00123
>>> significant_arithmetic_rounding(-0.12345, 3)
-0.123
>>> significant_arithmetic_rounding(-30.15345, 3)
-30.2

answered Dec 7, 2021 at 15:08

Easier to know an answer works for your needs when it includes examples. The following is built on previous solutions, but offers a more general function which can round to 1, 2, 3, 4, or any number of significant digits.

import math

# Given x as float or decimal, returns as string a number rounded to "sig" significant digts
# Return as string in order to control significant digits, could be a float or decimal 
def round_sig(x, sig=2):
  r = round(x, sig-int(math.floor(math.log10(abs(x))))-1)
  floatsig = "%." + str(sig) + "g"
  return "%d"%r if abs(r) >= 10**(sig-1) else '%s'%float(floatsig % r) 

>>> a = [1234, 123.4, 12.34, 1.234, 0.1234, 0.01234, 0.25, 1999, -3.14, -48.01, 0.75]
>>> [print(i, "->", round_sig(i,1), round_sig(i), round_sig(i,3), round_sig(i,4)) for i in a]

1234 -> 1000 1200 1230 1234
123.4 -> 100 120 123 123.4
12.34 -> 10 12 12.3 12.34
1.234 -> 1 1.2 1.23 1.234
0.1234 -> 0.1 0.12 0.123 0.1234
0.01234 -> 0.01 0.012 0.0123 0.01234
0.25 -> 0.2 0.25 0.25 0.25
1999 -> 2000 2000 2000 1999
-3.14 -> -3 -3.1 -3.14 -3.14
-48.01 -> -50 -48 -48.0 -48.01
0.75 -> 0.8 0.75 0.75 0.75

answered Apr 22 at 20:01

chadnchadn

2491 gold badge2 silver badges7 bronze badges

in very cases, the number of significant is depend on to the evaluated process, e.g. error. I wrote the some codes which returns a number according to it's error (or with some desired digits) and also in string form (which doesn't eliminate right side significant zeros)

import numpy as np

def Sig_Digit(x, *N,):
    if abs(x) < 1.0e-15:
        return(1)
    N = 1 if N ==() else N[0]
    k = int(round(abs(N)-1))-int(np.floor(np.log10(abs(x))))
    return(k);

def Sig_Format(x, *Error,):
    if abs(x) < 1.0e-15:
        return('{}')
    Error = 1 if Error ==() else abs(Error[0])
    k = int(np.floor(np.log10(abs(x))))
    z = x/10**k
    k = -Sig_Digit(Error, 1)
    m = 10**k
    y = round(x*m)/m
    if k < 0:
        k = abs(k)
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.'+'{}'.format(k-1)+'f}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.'+'{}'.format(k)+'f}'
    elif k == 0:
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.0e}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.0f}'
    else:
        FMT = '{:'+'{}'.format(2+k)+'.'+'{}'.format(k)+'e}'
    return(FMT)

def Sci_Format(x, *N):
    if abs(x) < 1.0e-15:
        return('{}')
    N = 1 if N ==() else N[0]
    N = int(round(abs(N)-1))
    y = abs(x)
    k = int(np.floor(np.log10(y)))
    z = x/10**k
    k = k-N
    m = 10**k
    y = round(x/m)*m
    if k < 0:
        k = abs(k)
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.'+'{}'.format(k-1)+'f}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.'+'{}'.format(k)+'f}'
    elif k == 0:
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.0e}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.0f}'
    else:
        FMT = '{:'+'{}'.format(2+N)+'.'+'{}'.format(N)+'e}'
    return(FMT)

def Significant(x, *Error):
    N = 0 if Error ==() else Sig_Digit(abs(Error[0]), 1)
    m = 10**N
    y = round(x*m)/m
    return(y)

def Scientific(x, *N):
    m = 10**Sig_Digit(x, *N)
    y = round(x*m)/m
    return(y)

def Scientific_Str(x, *N,): 
    FMT = Sci_Format(x, *N)
    return(FMT.format(x))

def Significant_Str(x, *Error,):    
    FMT = Sig_Format(x, *Error)
    return(FMT.format(x))

test code:

X = [19.03345607, 12.075, 360.108321344, 4325.007605343]
Error = [1.245, 0.1245, 0.0563, 0.01245, 0.001563, 0.0004603]
for x in X:
    for error in Error:
        print(x,'+/-',error, end='   \t==> ')
        print(' (',Significant_Str(x, error), '+/-', Scientific_Str(error),')')

  

print out:

19.03345607 +/- 1.245       ==>  ( 19 +/-  1 )

19.03345607 +/- 0.1245      ==>  ( 19.0 +/- 0.1 )

19.03345607 +/- 0.0563      ==>  ( 19.03 +/- 0.06 )

19.03345607 +/- 0.01245     ==>  ( 19.03 +/- 0.01 )

19.03345607 +/- 0.001563    ==>  ( 19.033 +/- 0.002 )

19.03345607 +/- 0.0004603       ==>  ( 19.0335 +/- 0.0005 )

12.075 +/- 1.245    ==>  ( 12 +/-  1 )

12.075 +/- 0.1245       ==>  ( 12.1 +/- 0.1 )

12.075 +/- 0.0563       ==>  ( 12.07 +/- 0.06 )

12.075 +/- 0.01245      ==>  ( 12.07 +/- 0.01 )

12.075 +/- 0.001563     ==>  ( 12.075 +/- 0.002 )

12.075 +/- 0.0004603    ==>  ( 12.0750 +/- 0.0005 )

360.108321344 +/- 1.245     ==>  ( 360 +/-  1 )

360.108321344 +/- 0.1245    ==>  ( 360.1 +/- 0.1 )

360.108321344 +/- 0.0563    ==>  ( 360.11 +/- 0.06 )

360.108321344 +/- 0.01245       ==>  ( 360.11 +/- 0.01 )

360.108321344 +/- 0.001563      ==>  ( 360.108 +/- 0.002 )

360.108321344 +/- 0.0004603     ==>  ( 360.1083 +/- 0.0005 )

4325.007605343 +/- 1.245    ==>  ( 4325 +/-  1 )

4325.007605343 +/- 0.1245       ==>  ( 4325.0 +/- 0.1 )

4325.007605343 +/- 0.0563       ==>  ( 4325.01 +/- 0.06 )

4325.007605343 +/- 0.01245      ==>  ( 4325.01 +/- 0.01 )

4325.007605343 +/- 0.001563     ==>  ( 4325.008 +/- 0.002 )

4325.007605343 +/- 0.0004603    ==>  ( 4325.0076 +/- 0.0005 )

answered Sep 17 at 16:11