Рубрики
Без рубрики

Округление чисел в Python

Автор оригинала: Frank Hofmann.

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

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

В нашей повседневной жизни мы используем десятичную систему счисления, основанную на числе 10. Компьютер использует двоичную систему, которая является базой 2, и внутренне он хранит и обрабатывает значения в виде последовательности 1 и 0. Ценности, с которыми мы работаем, должны постоянно трансформироваться между этими двумя представлениями. Как описано в документации Python :

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

Такое поведение приводит к удивительным результатам в простых добавлениях, как показано здесь:

Листинг 1: Неточности с числами с плавающей запятой

>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999

Как вы можете видеть здесь, вывод неточен, так как он должен привести к 0.9.

В листинге 2 показан аналогичный случай форматирования числа с плавающей запятой для 17 знаков после запятой.

Листинг 2: Форматирование числа с плавающей запятой

>>> format(0.1, '.17f')
'0.10000000000000001'

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

Листинг 3: Расчет с округленными значениями

>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
>>> s == 0.9
False
>>> round(0.9, 1) == 0.9
True

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

Для хранения таких значений в игру вступают два модуля Python decimal и fraction (см. Примеры ниже). Но сначала давайте подробнее рассмотрим термин “округление”.

Что такое Округление?

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

…замена [значения] другим числом, которое приблизительно равно исходному, но имеет более короткое, простое или более явное представление.

Источник: Источник:

По сути, он добавляет неточность к точно рассчитанному значению, сокращая его. В большинстве случаев это делается путем удаления цифр после десятичной точки, например, от 3,73 до 3,7, от 16,67 до 16,7 или от 999,95 до 1000.

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

В общем, для округления применяются два довольно простых правила, вы, наверное, помните их со школы. Цифры от 0 до 4 приводят к округлению вниз, а цифры от 5 до 9-к округлению вверх . В таблице ниже показан выбор вариантов использования.

| original value | rounded to   | result |
|----------------|--------------|--------|
| 226            | the ten      | 230    |
| 226            | the hundred  | 200    |
| 274            | the hundred  | 300    |
| 946            | the thousand | 1,000  |
| 1,024          | the thousand | 1,000  |
| 10h45m50s      | the minute   | 10h45m |

Методы округления

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

Например, округление наполовину от нуля применяется Европейской комиссией по экономическим и финансовым вопросам при конвертации валют в евро. Некоторые страны, такие как Швеция, Нидерланды, Новая Зеландия и Южная Африка, следуют правилу, называемому “округление наличных денег”, “округление пенни” или “шведское округление”.

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

Источник: Источник:

В Южной Африке с 2002 года округление наличных производится до ближайших 5 центов. Как правило, такого рода округление не применяется к электронным безналичным платежам.

Напротив, округление половины до четности является стратегией по умолчанию для Python, Numpy и Pandas и используется встроенной функцией round () , которая уже упоминалась ранее. Он относится к категории методов округления до ближайшего и также известен как конвергентное округление, статистическое округление, голландское округление, гауссовское округление, нечетно–четное округление и округление банкиров. Этот метод определен в IEEE 754 и работает таким образом, что “если дробная часть x равна 0,5, то y является четным целым числом, ближайшим к x .” Предполагается, что “вероятности связи в наборе данных, округленном вниз или округленном вверх, равны”, что обычно и происходит на практике. Хотя и не полностью совершенная, эта стратегия приводит к заметным результатам.

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

| original value | rounded to |
|----------------|------------|
| 23.3           | 23         |
| 23.5           | 24         |
| 24.0           | 24         |
| 24.5           | 24         |
| 24.8           | 25         |
| 25.5           | 26         |

Функции Python

Python поставляется со встроенной функцией round () , которая весьма полезна в нашем случае. Он принимает два параметра – исходное значение и количество цифр после десятичной точки. Приведенный ниже список иллюстрирует использование метода для одной, двух и четырех цифр после десятичной точки.

Листинг 4: Округление с заданным числом цифр

>>> round(15.45625, 1)
15.5
>>> round(15.45625, 2)
15.46
>>> round(15.45625, 4)
15.4563

Если вы вызываете эту функцию без второго параметра, то значение округляется до полного целого числа.

Листинг 5: Округление без заданного количества цифр

>>> round(0.85)
1
>>> round(0.25)
0
>>> round(1.5)
2

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

Первое вычисление Листинг 6 содержит предварительно округленные значения и описывает округление перед добавлением значений. Второе вычисление содержит сводку после округления, что означает округление после суммирования. Вы заметите, что результат сравнения отличается.

Листинг 6: Предварительное округление и последующее округление

>>> round(0.3, 10) + round(0.3, 10) + round(0.3, 10) == round(0.9, 10)
False
>>> round(0.3 + 0.3 + 0.3, 10) == round(0.9, 10)
True

Модули Python для вычислений с плавающей запятой

Есть четыре популярных модуля, которые помогут вам правильно работать с числами с плавающей запятой. Это включает в себя модуль math , модуль Numpy , модуль decimal и модуль fractions .

Модуль math сосредоточен вокруг математических констант, операций с плавающей запятой и тригонометрических методов. Модуль Numpy описывает себя как “фундаментальный пакет для научных вычислений” и известен своим разнообразием методов массива. Модуль decimal охватывает десятичную арифметику с фиксированной и плавающей точками, а модуль fractions имеет дело, в частности, с рациональными числами.

Во-первых, мы должны попытаться улучшить расчет из Листинга 1 . Как показывает Листинг 7 , после импорта модуля math мы можем получить доступ к методу fsum () , который принимает список чисел с плавающей запятой. Для первого расчета нет никакой разницы между встроенным методом sum() и методом fsum() из модуля math , но для второго он есть и возвращает правильный результат, который мы ожидали бы. Точность зависит от базового алгоритма IEEE 754.

Листинг 7: Вычисления с плавающей запятой с помощью математика модуль

>>> import math
>>> sum([0.1, 0.1, 0.1])
0.30000000000000004
>>> math.fsum([0.1, 0.1, 0.1])
0.30000000000000004
>>> sum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
0.9999999999999999
>>> math.fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
1.0

Во-вторых, давайте взглянем на модуль Numpy . Он поставляется с методом around () , который округляет значения, предоставленные в виде массива. Он обрабатывает отдельные значения так же, как и метод по умолчанию round () .

Для сравнения значений Numpy предлагает метод equal () . Подобно around() он принимает одиночные значения, а также списки значений (так называемые векторы) для обработки. Листинг 8 показывает сравнение как одиночных, так и округленных значений. Наблюдаемое поведение весьма похоже на ранее показанные методы.

Листинг 8: Сравнение значений с помощью метода equal из Numpy модуль

>>> import numpy
>>> print (numpy.equal(0.3, 0.3))
True
>>> print (numpy.equal(0.3 + 0.3 + 0.3 , 0.9))
False
>>> print (numpy.equal(round(0.3 + 0.3 + 0.3) , round(0.9)))
True

Третий вариант-это модуль decimal . Он предлагает точное десятичное представление и сохраняет значащие цифры. Точность по умолчанию составляет 28 цифр, и вы можете изменить это значение на число, которое является настолько большим, насколько это необходимо для вашей проблемы. Листинг 9 показывает, как использовать точность 8 цифр.

Листинг 9: Создание десятичных чисел с помощью десятичный модуль

>>> import decimal
>>> decimal.getcontext().prec = 8
>>> a = decimal.Decimal(1)
>>> b = decimal.Decimal(7)
>>> a / b
Decimal('0.14285714')

Теперь сравнение значений float становится намного проще и приводит к тому результату, который мы искали.

Листинг 10: Сравнение с использованием десятичный модуль

>>> import decimal
>>> decimal.getcontext().prec = 1
>>> a = decimal.Decimal(0.3)
>>> b = decimal.Decimal(0.3)
>>> c = decimal.Decimal(0.3)
>>> a + b + c
Decimal('0.9')
>>> a + b + c == decimal.Decimal('0.9')
True

Модуль decimal также поставляется с методом округления значений – quantize() . Стратегия округления по умолчанию настроена на округление половины до четности, а также может быть изменена на другой метод, если это необходимо. Листинг 11 иллюстрирует использование метода quantize () . Обратите внимание, что количество цифр задается с использованием десятичного значения в качестве параметра.

Листинг 11: Округление значения с помощью квантование()

>>> d = decimal.Decimal(4.6187)
>>> d.quantize(decimal.Decimal("1.00"))
Decimal('4.62')

И последнее, но не менее важное: мы рассмотрим модуль фракции . Этот модуль позволяет обрабатывать значения с плавающей запятой в виде дробей, например 0.3 как 3/10. Это упрощает сравнение значений с плавающей запятой и полностью исключает округление значений. Листинг 12 показывает, как использовать модуль фракций.

Листинг 12: Хранение и сравнение значений с плавающей запятой в виде дробей

>>> import fractions
>>> fractions.Fraction(4, 10)
Fraction(2, 5)
>>> fractions.Fraction(6, 18)
Fraction(1, 3)
>>> fractions.Fraction(125)
Fraction(125, 1)
>>> a = fractions.Fraction(6, 18)
>>> b = fractions.Fraction(1, 3)
>>> a == b
True

Кроме того, два модуля decimal и fractions могут быть объединены, как показано в следующем примере.

Листинг 13: Работа с десятичными дробями и дробями

>>> import fractions
>>> import decimal
>>> a = fractions.Fraction(1,10)
>>> b = fractions.Fraction(decimal.Decimal(0.1))
>>> a,b
(Fraction(1, 10), Fraction(3602879701896397, 36028797018963968))
>>> a == b
False

Вывод

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

Python дает вам все необходимые инструменты и поставляется с “включенными батарейками”. Счастливого взлома!

Признание

Автор хотел бы поблагодарить Золеку Хофманн за ее критические замечания при подготовке этой статьи.