How to round a number to significant figures in Py

2019-01-01 13:00发布

问题:

I need to round a float to be displayed in a UI. E.g, to one significant figure:

1234 -> 1000

0.12 -> 0.1

0.012 -> 0.01

0.062 -> 0.06

6253 -> 6000

1999 -> 2000

Is there a nice way to do this using the Python library, or do I have to write it myself?

回答1:

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.



回答2:

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


回答3:

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


回答4:

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)


回答5:

I modified indgar\'s solution to handle negative numbers and small numbers (including zero).

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)


回答6:

print(\'{:g}\'.format(float(\'{:.1g}\'.format(12.345))))

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.



回答7:

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


回答8:

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


回答9:

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)


回答10:

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


回答11:

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.



回答12:

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.



回答13:

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.