Как округлить число до значащих цифр в Python

148

Мне нужно округлить число с плавающей точкой для отображения в пользовательском интерфейсе. Например, на одну значимую цифру:

1234 -> 1000

0,12 -> 0,1

0,012 -> 0,01

0,062 -> 0,06

6253 -> 6000

1999 -> 2000

Есть ли хороший способ сделать это с помощью библиотеки Python, или я должен написать это сам?

Питер Грэм
источник
2
Вы просто форматируете вывод? Вы спрашиваете об этом? docs.python.org/library/stdtypes.html#string-formatting или это? docs.python.org/library/string.html#string-formatting
S.Lott
какой выход вы ожидаете для 0,062 и 6253?
Ламирап
Пакет для точности теперь делает это. Мой опубликованный ответ подробно описывает, как это применимо.
Уильям Руснак

Ответы:

146

Вы можете использовать отрицательные числа для округления целых чисел:

>>> round(1234, -3)
1000.0

Таким образом, если вам нужна только самая значимая цифра:

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

Вам, вероятно, придется позаботиться о том, чтобы число с плавающей точкой стало целым, если оно больше 1.

Евгений
источник
3
Это правильное решение. Использование log10- единственный правильный способ определить, как его округлить.
Вольф
73
round_to_n = лямбда x, n: round (x, -int (floor (log10 (x))) + (n - 1))
Рой Хюнцзинь Хан
28
Вы должны использовать log10(abs(x)), в противном случае отрицательные числа потерпят неудачу (И, x == 0конечно, относиться к ним отдельно)
Тобиас Кинцлер
2
Я создал пакет, который делает это сейчас и, вероятно, проще и надежнее, чем этот. Опубликовать ссылку , Репо ссылку . Надеюсь это поможет!
Уильям Руснак
2
round_to_n = lambda x, n: x if x == 0 else round(x, -int(math.floor(math.log10(abs(x)))) + (n - 1))защищает от x==0и x<0Спасибо @RoyHyunjinHan и @TobiasKienzler. Не защищен от неопределенного , как math.inf, или мусор , как никто и т.д.
AJP
98

% g в форматировании строки будет форматировать число с плавающей точкой, округленное до некоторого числа значащих цифр. Иногда он использует научную нотацию 'e', ​​поэтому преобразуйте округленную строку обратно в число с плавающей точкой, а затем через форматирование строки% 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
В 1999 году требовалось, чтобы ФП было отформатировано как 2000, а не как 2000.0. Я не вижу тривиального способа изменить ваш метод для достижения этой цели.
Тим Мартин
1
Это именно то, что я всегда хотел! где ты это нашел?
djhaskin987
12
Обратите внимание, что поведение% g не всегда корректно. В частности, он всегда обрезает конечные нули, даже если они значительны. Число 1.23400 имеет 6 значащих цифр, но «% .6g»% (1.23400) приведет к «1.234», что неверно. Больше подробностей в этом посте: randlet.com/blog/python-significant-figures-format
randlet
3
Так же, как метод в ответе Евгения, это не в состоянии правильно округлить 0.075до 0.08. Это возвращается 0.07вместо.
Габриэль
1
round_sig = lambda f,p: float(('%.' + str(p) + 'e') % f)позволяет настроить количество значащих цифр!
Денизб
49

Если вы хотите иметь значение, отличное от 1 значащей десятичной дроби (в остальном такое же, как у Евгения):

>>> 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
indgar
источник
8
round_sig (-0.0232) -> ошибка математического домена, вы можете добавить туда abs ();)
dgorissen
2
Точно так же, как методы в ответах Евгения и Питера Грэма, это не в состоянии правильно округлить 0.075до 0.08. Это возвращается 0.07вместо.
Габриэль
3
Также это терпит неудачу для round_sig (0).
Юваль Ацмон
2
@Gabriel Это встроенная «особенность» Python, работающая на вашем компьютере, которая проявляется в поведении этой функции round. docs.python.org/2/tutorial/floatingpoint.html#tut-fp-issues
Новичок C
1
@ Габриэль Я добавил ответ, который объясняет, почему вы должны ожидать 0,7 от округления «0,075»! см. stackoverflow.com/a/56974893/1358308
Сэм Мейсон,
30
f'{float(f"{i:.1g}"):g}'
# Or with Python <3.6,
'{:g}'.format(float('{:.1g}'.format(i)))

Это решение отличается от всех остальных тем, что:

  1. это точно решает вопрос ОП
  2. это не нуждается ни в каком дополнительном пакете
  3. для этого не требуется никакой пользовательской вспомогательной функции или математической операции

Для произвольного числа nзначащих цифр вы можете использовать:

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

Тест:

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

Примечание : с этим решением невозможно динамически адаптировать количество значащих цифр от входных данных, потому что не существует стандартного способа различать числа с разными числами конечных нулей ( 3.14 == 3.1400). Если вам нужно сделать это, то вам нужны нестандартные функции, подобные тем, которые представлены в пакете to-precision .

Falken
источник
К вашему сведению: я нашел это решение независимо от eddygeek, когда пытался решить ту же проблему в одном из моих кодов. Теперь я понимаю, что мое решение, очевидно, почти идентично его (я только заметил ошибочный вывод и не удосужился прочитать код, моя ошибка). Возможно, короткого комментария под его ответом было бы достаточно вместо нового ответа ... Единственное (ключевое) отличие - это двойное использование :gформатера, который сохраняет целые числа.
Фалькен
Вау, твой ответ должен быть действительно прочитан сверху донизу;) Этот трюк двойного применения грязный, но аккуратный. (Обратите внимание, что 1999, отформатированный как 2000.0 предлагает 5 значащих цифр, поэтому он должен пройти {:g}снова.) В целом, целые числа с конечными нулями являются неоднозначными в отношении значащих цифр, если только не используется какой-либо метод (например, подчеркивание над последним значащим).
Томаш Гандор
8

Я создал пакет с высокой точностью, который делает то, что вы хотите. Это позволяет вам давать своим цифрам более или менее значимые цифры.

Он также выводит стандартную, научную и инженерную нотацию с указанным количеством значащих цифр.

В принятом ответе есть строка

>>> round_to_1(1234243)
1000000.0

Это на самом деле определяет 8 сиг фиг. Для числа 1234243 моя библиотека отображает только одну значимую цифру:

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

Он также округляет последнюю значащую цифру и может автоматически выбирать, какую нотацию использовать, если нотация не указана:

>>> to_precision(599, 2)
'600'
>>> to_precision(1164, 2)
'1.2e3'
Уильям Руснак
источник
Теперь я ищу то же самое, но применил к
пандам
@mhoff вы можете использовать карту панд с лямбдой. lambda x: to_precision(x, 2)
Уильям Руснак
Добавьте это в (PyPI) [ pypi.org/] . Насколько я могу судить, там нет ничего подобного этому.
Моргот
это отличный пакет, но я думаю, что большинство функций теперь в модуле
sigfig
1
в нем есть ошибка: std_notation (9.999999999999999e-05, 3) дает: «0,00010», что составляет только 2 значащие цифры
Борис Малдер,
5

Чтобы округлить целое число до 1 значащей цифры, основная идея состоит в том, чтобы преобразовать ее в число с плавающей запятой с 1 цифрой перед точкой и округлить ее, а затем преобразовать обратно в исходный целочисленный размер.

Для этого нам нужно знать наибольшую степень на 10 меньше, чем целое число. Для этого мы можем использовать пол функции 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
Плюс один для решения, которое работает без раунда питона (.., цифры) и без прикрепленных строк!
Стив Роджерс
5

Чтобы прямо ответить на вопрос, вот моя версия с использованием именования из функции 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)

Моей главной причиной для публикации этого ответа являются комментарии с жалобами на то, что «0,075» округляется до 0,07, а не 0,08. Это связано с тем, что «Новичок С» указывает на комбинацию арифметики с плавающей запятой, имеющей как конечную точность, так и представление с основанием-2 . Ближайшее к 0,075 число, которое действительно может быть представлено, немного меньше, поэтому округление получается не так, как вы могли наивно ожидать.

Также обратите внимание, что это относится к любому использованию недесятичной арифметики с плавающей запятой, например, C и Java имеют одинаковую проблему.

Чтобы показать более подробно, мы просим Python отформатировать число в шестнадцатеричном формате:

0.075.hex()

который дает нам: 0x1.3333333333333p-4. Причина этого заключается в том, что нормальное десятичное представление часто включает в себя округление и, следовательно, не то, как компьютер фактически «видит» число. Если вы не привыкли к этому формату, пара полезных ссылок - это документы по Python и стандарт Си .

Чтобы показать, как эти числа работают немного, мы можем вернуться к нашей отправной точке, выполнив:

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

который должен распечатать 0.075. 16**13это потому, что после десятичной запятой есть 13 шестнадцатеричных цифр, и 2**-4потому, что шестнадцатеричные показатели являются base-2.

Теперь у нас есть некоторое представление о том, как представлены числа с плавающей точкой, и мы можем использовать decimalмодуль для большей точности, показывая нам, что происходит:

from decimal import Decimal

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

давать: 0.07499999999999999722444243844и, надеюсь, объяснить, почему round(0.075, 2)оценивает0.07

Сэм Мейсон
источник
1
Это отличное объяснение того, почему 0,075 округляется до 0,07 на уровне кода , но нас (в физических науках) учили всегда округлять, а не уменьшать. Таким образом, ожидаемое поведение на самом деле должно иметь 0,08 в результате, несмотря на проблемы точности с плавающей запятой.
Габриэль
1
Я не уверен, где ваша путаница: когда вы вводите 0,075, вы фактически вводите ~ 0,07499 (как указано выше), что округляется в соответствии с обычными математическими правилами. если бы вы использовали тип данных (например, десятичную с плавающей запятой ), который мог бы представлять 0,075, тогда он действительно должен округляться до 0,08
Сэм Мейсон,
Я не смущен. Когда я ввожу 0,075, я на самом деле ввожу 0,075. Что бы ни происходило в математике с плавающей запятой внутри кода, мне все равно.
Габриэль
@Gabriel: А если бы намеренно введен 0.074999999999999999, то , что вы ожидаете получить в этом случае?
Марк Дикинсон
@MarkDickinson, это зависит. Одна значимая цифра: 0,07, две: 0,075.
Габриэль
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

Надеюсь, что мы взяли лучшие из всех ответов, приведенных выше (минус возможность поставить его как одну строку лямбда;)). Еще не изучен, не стесняйтесь редактировать этот ответ:

round_to_n(1e15 + 1, 11)  # 999999999999999.9
AJP
источник
4

Я изменил решение Indgar для обработки отрицательных чисел и небольших чисел (включая ноль).

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)
ryan281
источник
Почему бы просто не проверить x == 0? Если вы любите однострочник, просто return 0 if x==0 else round(...).
pjvandehaar
2
@pjvandehaar, вы правы для общего случая, и я должен был это вставить. Кроме того, для численных расчетов, которые мне нужно выполнить, мы иногда получаем числа, подобные 1e-15. В нашем приложении мы хотим, чтобы сравнение двух небольших чисел (одно из которых могло бы быть нулем) считалось равным. Также некоторые люди хотят округлить небольшое число (это может быть 1e-9, 1e-15 или даже 1e-300) до нуля.
ryan281
1
Интересный. Спасибо за объяснение этого. В таком случае мне очень нравится это решение.
pjvandehaar
@ Моргот Это интересная и сложная проблема. Как вы указали, напечатанное значение не показывает 3 значащих цифры, но значение является правильным (например, 0.970 == 0.97). Я думаю, что вы могли бы использовать некоторые другие решения для печати, например, f'{round_sig(0.9701, sig=3):0.3f}'если вы хотите печатать ноль.
ryan281
3

Если вы хотите округлить без использования строк, ссылка, которую я нашел, похоронена в комментариях выше:

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

мне кажется лучшим Затем, когда вы печатаете с любыми дескрипторами форматирования строки, вы получаете разумный вывод и можете использовать числовое представление для других целей расчета.

Код по ссылке состоит из трех строк: def, doc и return. В нем есть ошибка: вам нужно проверить наличие логарифмов. Это просто. Сравните вход с sys.float_info.min. Полное решение:

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

Это работает для любого скалярного числового значения, и n может быть a, floatесли вам нужно сместить ответ по какой-то причине. Вы можете увеличить предел:

sys.float_info.min*sys.float_info.epsilon

без провокации ошибки, если по какой-то причине вы работаете с минимальными значениями.

getting_sleepy
источник
2

Я не могу придумать ничего такого, что могло бы справиться с этим из коробки. Но это довольно хорошо обрабатывается для чисел с плавающей запятой.

>>> round(1.2322, 2)
1.23

Целые числа сложнее. Они не хранятся в памяти как база 10, поэтому значимые места не являются естественным делом. Это довольно тривиально реализовать, когда они являются строкой.

Или для целых чисел:

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

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

Если вы хотите создать функцию, которая обрабатывает любое число, я бы предпочел преобразовать их в строки и найти десятичное место, чтобы решить, что делать:

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

Другой вариант - проверить тип. Это будет гораздо менее гибко, и, вероятно, не будет хорошо играть с другими числами, такими как Decimalобъекты:

>>> def roundall2(n, sigfigs):
...   if type(n) is int: return intround(n, sigfigs)
...   else: return round(n, sigfigs)
Тим Макнамара
источник
Просто возиться со строками не будет округлять числа. 1999 округляется до 1 значащей цифры - 2000, а не 1000.
Питер Грэм
Существует хорошее обсуждение этой проблемы в архиве ActiveState. Code.activestate.com/lists/python-tutor/70739
Тим Макнамара,
2

Размещенный ответ был наилучшим из имеющихся, когда он был дан, но он имеет ряд ограничений и не дает технически правильных значимых цифр.

numpy.format_float_positional напрямую поддерживает желаемое поведение. Следующий фрагмент возвращает число с плавающей запятой, xотформатированное до 4 значащих цифр, с подавленной научной нотацией.

import numpy as np
x=12345.6
np.format_float_positional(x, precision=4, unique=False, fractional=False, trim='k')
> 12340.
осень
источник
В документации (перенесено на numpy.org/doc/stable/reference/generated/… ) говорится, что эта функция реализует алгоритм Dragon4 (Steele & White 1990, dl.acm.org/doi/pdf/10.1145/93542.93559 ). Это дает раздражающие результаты, например 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'). Я не проверял сам Dragon4.
Rainald62
0

Я тоже столкнулся с этим, но мне нужно было контролировать тип округления. Таким образом, я написал быструю функцию (см. Код ниже), которая может принимать во внимание значение, тип округления и требуемые значащие цифры.

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))
drew.ray
источник
0

Использование форматирования в новом стиле Python 2.6+ (так как стиль% не рекомендуется):

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

В Python 2.7+ вы можете опустить ведущие 0s.

eddygeek
источник
С какой версией python? Python 3.6.3 | Anaconda, Inc. | (по умолчанию 13 октября 2017, 12:02:49) имеет ту же старую проблему округления. «{0}». Format (float («{0: .1g}». Format (0.075))) возвращает «0,07», а не «0,08»
Дон Маклахлан
@DonMclachlan Я добавил объяснение того, почему это ожидается в stackoverflow.com/a/56974893/1358308
Сэм Мейсон,
0

Эта функция выполняет нормальное округление, если число больше 10 ** (- decimal_positions), в противном случае добавляет больше десятичного числа, пока не будет достигнуто количество значащих десятичных позиций:

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)

Надеюсь, поможет.

Этторе Галли
источник
0

https://stackoverflow.com/users/1391441/gabriel , отвечает ли следующее ваше беспокойство по поводу rnd (.075, 1)? Предупреждение: возвращает значение в виде числа с плавающей запятой

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

Это возвращает строку, так что результаты без дробных частей и небольшие значения, которые в противном случае появлялись бы в нотации E, отображаются правильно:

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))
Gnubie
источник
0

На такой вопрос так тщательно ответили, почему бы не добавить еще один

Это подходит моей эстетике немного лучше, хотя многие из вышеперечисленных сопоставимы

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)

Это работает для отдельных чисел и числовых массивов и должно нормально работать для отрицательных чисел.

Есть еще один шаг, который мы могли бы добавить - np.round () возвращает десятичное число, даже если округлено целое число (т. Е. Для значимых значений = 2 мы можем ожидать возврата -460, но вместо этого мы получим -460.0). Мы можем добавить этот шаг, чтобы исправить это:

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

К сожалению, этот последний шаг не будет работать для массива чисел - я оставлю это вам, дорогой читатель, чтобы выяснить, если вам нужно.

зефир
источник
0

Sigfig пакет / библиотека охватывает это. После установки вы можете сделать следующее:

>>> 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
LetzerWille
источник