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

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

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

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

В нашей повседневной жизни мы используем десятичную систему, основанную на числе 10. Компьютер использует двоичную систему с основанием 2, а внутри он хранит и обрабатывает значения как последовательность единиц и нулей. Ценности, с которыми мы работаем, должны постоянно трансформироваться между двумя представлениями. Как объяснено в документации 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: десятичная и дробная (см. Примеры ниже). Но сначала давайте подробнее рассмотрим термин «округление».

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

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

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

[Источник: https://en.wikipedia.org/wiki/Rounding]{.small}

По сути, он увеличивает неточность точно рассчитанного значения, сокращая его. В большинстве случаев это делается путем удаления цифр после десятичной точки, например от 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 | 

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

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

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

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

[Источник: https://en.wikipedia.org/wiki/Cash_rounding]{.small}

В Южной Африке с 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: Вычисления с плавающей запятой с помощью math модуля

 >>> 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: Создание десятичных чисел с помощью модуля decimal

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

Теперь сравнение значений с плавающей запятой стало намного проще и привело к желаемому результату.

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

 >>> 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: Округление значения с помощью quantize()

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

И последнее, но не менее важное: мы рассмотрим модуль fractions Этот модуль позволяет обрабатывать значения с плавающей запятой как дроби, например 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 предоставляет вам все необходимые инструменты и поставляется с «батареями в комплекте». Удачного взлома!

Благодарности

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus