Как округлять числа в Python

484
Как округлять числа в Python
Как округлять числа в Python

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

Каждый специалист по data science должен помнить о том, что набор данных может быть необъективным. Выводы, сделанные на основе необъективных данных, могут привести к дорогостоящим ошибкам.

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

В этой статье вы узнаете:

  • Почему способ округления чисел важен
  • Как округлить число в соответствии с различными стратегиями округления и как реализовать каждый метод на чистом Python
  • Как округление влияет на данные, и какая стратегия округления минимизирует этот эффект
  • Как округлять числа в массивах NumPy и Pandas DataFrames
  • Когда следует применять различные стратегии округления

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

Начнем с рассмотрения встроенного в Python механизма округления.

Встроенная функция Round() в Python

В Python есть встроенная функция round(), которая принимает два числовых аргумента, n и ndigits, и возвращает число n, округленное до ndigits. Аргумент ndigits по умолчанию равен нулю, поэтому если его не указывать, то получится число, округленное до целого. Как вы увидите, round() может работать не совсем так, как вы ожидаете.

Большинство людей учат округлять числа примерно так:

Округлите число n до p десятичных знаков, сначала сдвинув десятичную точку в n на p знаков путем умножения n на 10ᵖ (10 в степень p), чтобы получить новое число m.

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

Наконец, сместите десятичную точку на p мест назад, разделив m на 10ᵖ.

Это простой алгоритм! Например, число 2,5, округленное до ближайшего целого числа, равно 3. Число 1,64, округленное до одного десятичного знака, равно 1,6.

Теперь откройте сеанс интерпретатора и округлите 2,5 до ближайшего целого числа с помощью встроенной в Python функции round():

>>> round(2.5)
2

Внимание!

Как Round() обрабатывает число 1,5?

>>> round(1.5)
2

Итак, round() округляет 1,5 до 2, а 2,5 до 2!

Прежде чем вы напишете о проблеме в Python, позвольте заверить вас, что round(2.5) должна возвращать 2. Есть веская причина, по которой round() ведет себя именно так.

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

Возможно, вы задаетесь вопросом: “Может ли способ округления чисел действительно иметь такое большое влияние?”. Давайте посмотрим, насколько серьезным может быть влияние округления.

Насколько сильно может повлиять округление?

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

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

Давайте проведем небольшой эксперимент. Представим, что общая стоимость купленных вами акций колеблется каждую секунду на какое-то небольшое случайное число, скажем, между $0,05 и -$0,05. Это колебание может быть не обязательно красивым значением с двумя знаками после запятой. Например, общая стоимость может увеличиться на $0,031286 в одну секунду и уменьшиться в следующую секунду на $0,028476.

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

Чтобы провести наш эксперимент с помощью Python, давайте начнем с написания функции truncate(), которая усекает число до трех знаков после запятой:

>>> def truncate(n):
...     return int(n * 1000) / 1000

Функция truncate() работает, сначала сдвигая десятичную точку в числе n на три места вправо путем умножения n на 1000. Целая часть этого нового числа берется с помощью функции int(). Наконец, десятичная точка сдвигается на три позиции назад влево путем деления n на 1000.

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

Начните с инициализации этих переменных значением 100:

>>> actual_value, truncated_value = 100, 100

Теперь запустим моделирование в течение 1 000 000 секунд (примерно 11,5 дней). Для каждой секунды генерируйте случайное значение в диапазоне от -0,05 до 0,05 с помощью функции uniform() в модуле random, а затем обновите фактическое и усеченное значения:

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239

Основная часть моделирования происходит в цикле for, который перебирает диапазон(1000000) чисел от 0 до 999 999. Значение, полученное из range() на каждом шаге, хранится в variable _, которую мы используем здесь, потому что на самом деле нам не нужно это значение внутри цикла.

На каждом шаге цикла с помощью random.randn() генерируется новое случайное число в диапазоне от -0,05 до 0,05, которое присваивается переменной randn. Новое значение вашей инвестиции рассчитывается путем добавления randn к actual_value, а усеченное итоговое значение рассчитывается путем добавления randn к truncated_value и последующего усечения этого значения с помощью truncate().

Как вы видите, взглянув на переменную actual_value после выполнения цикла, вы потеряли всего $3,55. Однако если бы вы смотрели на truncated_value, вы бы решили, что потеряли почти все свои деньги!

Примечание: В приведенном выше примере функция random.seed() используется для запуска генератора псевдослучайных чисел, чтобы вы могли воспроизвести показанный здесь результат.

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

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258

Какая разница!

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

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

Множество методов

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

Усечение

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

Значение Усеченный до Результат
12.345 Tens place 10
12.345 Ones place 12
12.345 Tenths place 12.3
12.345 Hundredths place 12.34

 

Вы уже видели один способ реализации этого в функции truncate() из раздела выше “Насколько сильно может повлиять округление?”. В этой функции входное число усекалось до трех десятичных знаков:

  • Умножение числа на 1000 для смещения десятичной точки на три позиции вправо
  • Получение целой части нового числа с помощью функции int()
  • Сдвиг десятичной точки на три позиции влево при делении на 1000

Вы можете обобщить этот процесс, заменив 1000 на число 10ᵖ (10, возведенное в степень p), где p – количество десятичных знаков, до которых нужно усекать:

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier

В этой версии функции truncate() второй аргумент по умолчанию равен 0, поэтому если второй аргумент не передан функции, то truncate() возвращает целую часть любого переданного ей числа.

Функция truncate() хорошо работает как для положительных, так и для отрицательных чисел:

>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62

Вы даже можете передать отрицательное число в  decimals для усечения до цифр слева от десятичной точки:

>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0

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

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

Округление

Вторая стратегия округления, которую мы рассмотрим, называется “округление в большую сторону”. Эта стратегия всегда округляет число до определенного количества цифр. В следующей таблице приводится краткое описание этой стратегии:

Значение Округлить до Результат
12.345 Tens place 20
12.345 Ones place 13
12.345 Tenths place 12.4
12.345 Hundredths place 12.35

 

Для реализации стратегии “округления в большую сторону” в Python мы воспользуемся функцией ceil() из модуля math.

Функция ceil()  получила свое название от термина ” ceiling “, который используется в математике для описания ближайшего целого числа, которое больше или равно заданному числу.

Every number that is not an integer lies between two consecutive integers. For example, the number 1.2 lies in the interval between 1 and 2. The “ceiling” is the greater of the two endpoints of the interval. The lesser of the two endpoints in called the “floor.” Thus, the ceiling of 1.2 is 2, and the floor of 1.2 is 1.

Каждое число, которое не является целым, лежит между двумя последовательными целыми числами. Например, число 1.2 лежит в интервале между 1 и 2. Потолок “ceiling” – это большая из двух конечных точек интервала. Меньшая из двух конечных точек называется дном “floor”. Таким образом, потолок числа 1.2 равен 2, а дно числа 1.2 равно 1.

В математике специальная функция, называемая ceiling function, отображает каждое число на его потолок. Чтобы функция потолка могла принимать целые числа, потолок целого числа определяется как само целое число. Таким образом, потолок числа 2 равен 2.

В Python функция math.ceil()реализует функцию потолка и всегда возвращает ближайшее целое число, которое больше или равно введенному:

>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0

Обратите внимание, что потолок -0,5 равен 0, а не -1. Это имеет смысл, потому что 0 – это ближайшее целое число к -0,5, которое больше или равно -0,5.

Давайте напишем функцию round_up() , которая реализует стратегию “округления в большую сторону”:

def round_up(n, decimals=0):
    multiplier = 10 ** decimals
    return math.ceil(n * multiplier) / multiplier

Вы можете заметить, что round_up()очень похожа на truncate(). Сначала десятичная точка в n сдвигается на нужное количество мест вправо путем умножения n на  10 **  decimals (десятичных знаков). Это новое значение округляется до ближайшего целого числа с помощью math.ceil(), а затем десятичная точка смещается обратно влево путем деления на  10 ** decimals (10 ** десятичных) дробей.

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

Давайте посмотрим, насколько хорошо работает round_up()  для различных входных данных:

>>> round_up(1.1)
2.0

>>> round_up(1.23, 1)
1.3

>>> round_up(1.543, 2)
1.55

Подобно функции truncate(), вы можете передавать  decimals отрицательное значение:

>>> round_up(22.45, -1)
30.0

>>> round_up(1352, -2)
1400

Когда вы передаете отрицательное число в decimals, число в первом аргументе round_up() округляется до нужного количества цифр слева от десятичной точки.

Попробуйте угадать, что возвращает round_up(-1.5):

>>> round_up(-1.5)
-1.0

Является ли -1.0тем, что вы ожидали?

Если вы изучите логику, использованную при определении функции round_up()– в частности, то, как работает функция math.ceil() – тогда логично, что round_up(-1.5) возвращает -1.0 Однако некоторые люди, ожидают симметрии вокруг нуля при округлении чисел, так что если 1.5 округляется до  2, то – -1.5  должно округляться до -2.

Давайте определимся с терминологией. Для наших целей мы будем использовать термины “округление вверх” и “округление вниз” в соответствии со следующей схемой:

Округляйте вправо в большую сторону и влево в меньшую.
Округляйте вправо в большую сторону и влево в меньшую.

Округление в большую сторону всегда округляет число вправо на числовой прямой, а округление в меньшую сторону всегда округляет число влево на числовой прямой.

Округление в меньшую сторону

Противоположностью “округления в большую сторону” является стратегия “округления в меньшую сторону”, которая всегда округляет число до определенного количества цифр. Вот несколько примеров, иллюстрирующих эту стратегию:

Значение Округляется в меньшую сторону Результат
12.345 Tens place 10
12.345 Ones place 12
12.345 Tenths place 12.3
12.345 Hundredths place 12.34

 

Чтобы реализовать стратегию “округления в меньшую сторону” в Python, мы можем следовать тому же алгоритму, который мы использовали для trunctate()и round_up(). Сначала сдвиньте десятичную точку, затем округлите до целого числа и, наконец, сдвиньте десятичную точку обратно.

В  round_up()мы использовали  math.ceil() для округления до предела числа после сдвига десятичной точки. Однако для стратегии “округления в меньшую сторону” нам нужно округлить до нижней границы числа после сдвига десятичной точки.

К счастью для нас, в модуле mathесть функция floor(), которая возвращает нижнее значение входных данных:

>>> math.floor(1.2)
1

>>> math.floor(-0.5)
-1

Вот описание функции round_down():

def round_down(n, decimals=0):
    multiplier = 10 ** decimals
    return math.floor(n * multiplier) / multiplier

Это выглядит так же, как round_up(), только math.ceil() заменен на math.floor().

Вы можете проверить round_down() на нескольких различных значениях:

>>> round_down(1.5)
1

>>> round_down(1.37, 1)
1.3

>>> round_down(-0.5)
-1

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

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

Вступление: Предвзятость округления

Сейчас вы познакомились с тремя методами округления: truncate()round_up(), и round_down(). Все эти три метода довольно грубы, когда речь идет о сохранении разумной точности для данного числа.

Существует одно важное отличие между  truncate() и round_up() и round_down(), которое подчеркивает важный аспект округления: симметрия вокруг нуля.

Вспомните, что round_up() не симметрична вокруг нуля. В математических терминах функция f(x) симметрична вокруг нуля, если для любого значения x, f(x) + f(-x) = 0. Например, round_up(1.5) возвращает 2, а round_up(-1.5) возвращает -1. Функция round_down() также не симметрична вокруг 0.

С другой стороны, функция truncate() симметрична относительно нуля. Это происходит потому, что после сдвига десятичной точки вправо функция truncate() обрезает оставшиеся цифры. Если исходное значение положительное, это равносильно округлению числа в меньшую сторону. Отрицательные числа округляются в большую сторону. Таким образом, truncate(1.5) возвращает 1, а truncate(-1.5) – -1.

Концепция симметрии вводит понятие смещения округления, которое описывает, как округление влияет на числовые данные в наборе данных.

Стратегия “округления в большую сторону” имеет смещение в сторону положительной бесконечности, поскольку значение всегда округляется в сторону положительной бесконечности. Аналогично, стратегия “округления в меньшую сторону” имеет смещение в сторону отрицательной бесконечности.

Стратегия ” truncation” демонстрирует смещение в сторону отрицательной бесконечности для положительных значений и смещение в сторону положительной бесконечности для отрицательных значений. Функции округления с таким поведением, как правило, имеют смещение в сторону нуля.

Давайте посмотрим, как это работает на практике. Рассмотрим следующий список плавающих чисел:

>>> data = [1.25, -2.67, 0.43, -1.79, 4.32, -8.19]

Let’s compute the mean value of the values in data using the statistics.mean() function:

Вычислим среднее значение значений в data  с помощью функции statistics.mean():

>>> import statistics

>>> statistics.mean(data)
-1.1083333333333332

Теперь примените каждую из функций round_up()round_down() и truncate()  в списке comprehension для округления каждого числа в данных до одного десятичного знака и вычисления нового среднего значения:

>>> ru_data = [round_up(n, 1) for n in data]
>>> ru_data
[1.3, -2.6, 0.5, -1.7, 4.4, -8.1]
>>> statistics.mean(ru_data)
-1.0333333333333332

>>> rd_data = [round_down(n, 1) for n in data]
>>> statistics.mean(rd_data)
-1.1333333333333333

>>> tr_data = [truncate(n, 1) for n in data]
>>> statistics.mean(tr_data)
-1.0833333333333333

После округления каждого числа в data в большую сторону новое среднее значение составляет примерно -1.033, что больше, чем фактическое среднее значение, равное примерно 1.108. Округление в меньшую сторону сдвигает среднее значение вниз до  -1.133. Среднее значение усеченных значений составляет около -1.08 и наиболее близко к фактическому среднему значению.

Этот пример не означает, что вы всегда должны усекать, когда вам нужно округлить отдельные значения, сохранив при этом среднее значение как можно точнее. Список данных содержит равное количество положительных и отрицательных значений. Функция truncate() будет вести себя так же, как round_up() для списка всех положительных значений, и так же, как round_down() для списка всех отрицательных значений.

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

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

Например, если кто-то попросит вас округлить числа 1.23 и 1.28 до одного десятичного знака, вы, вероятно, быстро ответите 1.2 и 1.3. Функции truncate(), round_up() и round_down() не делают ничего подобного.

А как насчет числа 1,25? Вы, вероятно, сразу же подумаете, что нужно округлить его до 1,3, но в действительности 1,25 равноудалено от 1,2 и 1,3. В некотором смысле, 1,2 и 1,3 – это оба ближайших числа к 1,25 с точностью до одного десятичного знака. Число 1,25 называется “равным” по отношению к 1,2 и 1,3. В подобных случаях необходимо назначить тайбрейкер.

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

Округление половины в большую сторону

Стратегия “округление половины в большую сторону” округляет каждое число до ближайшего числа с заданной точностью, и разрывает равенства путем округления в большую сторону. Вот несколько примеров:

Значение Округлить до Результат
13.825 Tens place 10
13.825 Ones place 14
13.825 Tenths place 13.8
13.825 Hundredths place 13.83

Чтобы реализовать стратегию “округления половины в большую сторону” в Python, вы начинаете, как обычно, со сдвига десятичной точки вправо на нужное количество мест. Однако на этом этапе вам нужен способ определить, является ли цифра сразу после сдвинутой десятичной точки меньше или больше или равна 5.

Один из способов сделать это – добавить 0,5 к сдвинутому значению, а затем округлить в меньшую сторону с помощью math.floor(). Это работает, потому что:

  • Если цифра в первом десятичном знаке сдвинутого значения меньше пяти, то добавление 0,5 не изменит целочисленную часть сдвинутого значения, поэтому нижняя часть равна целочисленной части.
  • Если первая цифра после десятичного знака больше или равна 5, то добавление 0,5 увеличит целую часть сдвинутого значения на 1, так что полученное значение будет равно этому большему целому числу.

Вот как это выглядит в Python:

def round_half_up(n, decimals=0):
    multiplier = 10 ** decimals
    return math.floor(n*multiplier + 0.5) / multiplier

Обратите внимание, что round_half_up() очень похожа на round_down(). Это может быть несколько неинтуитивно, но внутри round_half_up() округляет только в меньшую сторону. Хитрость заключается в добавлении 0,5 после сдвига десятичной точки, чтобы результат округления в меньшую сторону соответствовал ожидаемому значению.

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

>>> round_half_up(1.23, 1)
1.2

>>> round_half_up(1.28, 1)
1.3

>>> round_half_up(1.25, 1)
1.3

Отлично! Теперь вы наконец-то можете получить тот результат, в котором вам отказала встроенная функция round():

>>> round_half_up(2.5)
3.0

Однако прежде чем вы обрадуетесь, давайте посмотрим, что произойдет, если вы попытаетесь округлить -1,225 до 2 знаков после запятой:

>>> round_half_up(-1.225, 2)
-1.23

Подождите. Мы только что обсуждали, как округляются значения в большую из двух возможных величин. -1.225 находится посередине между -1.22 и -1.23. Поскольку -1.22 больше из этих двух, round_half_up(-1.225, 2) должен вернуть -1.22. Но вместо этого мы получили -1,23.

Возможно, в функции round_half_up() есть ошибка?

Когда round_half_up() округляет -1,225 до двух знаков после запятой, первое, что она делает, это умножает -1,225 на 100. Давайте убедимся, что это работает так, как ожидается:

>>> -1.225 * 100
-122.50000000000001

Ну… это неправильно! Но это объясняет, почему round_half_up(-1.225, 2) возвращает -1.23. Давайте продолжим алгоритм round_half_up() шаг за шагом, используя _ в REPL, чтобы вспомнить последнее значение, выведенное на каждом шаге:

>>> _ + 0.5
-122.00000000000001

>>> math.floor(_)
-123

>>> _ / 100
-1.23

Даже если -122.00000000000001 действительно близко к -122, ближайшее целое число, которое меньше или равно ему, -123. Когда десятичная точка сдвигается назад влево, окончательное значение равно -1,23.

Ну, теперь вы знаете, как round_half_up(-1.225, 2) возвращает -1.23, хотя логической ошибки нет, но почему Python говорит, что -1.225 * 100 – это -122.50000000000001? Есть ли в Python ошибка?

Тот факт, что Python говорит, что -1.225 * 100 это -122.50000000000001, является следствием ошибки представления с плавающей точкой. Вы можете спросить себя: “Хорошо, но есть ли способ исправить это?”. Лучше задать себе вопрос: “Нужно ли мне это исправлять?”.

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

Если вы решили, что стандартного класса float в Python достаточно для вашего приложения, некоторые случайные ошибки в round_half_up() из-за ошибки представления с плавающей точкой не должны вызывать беспокойства.

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

Округление половины в меньшую сторону

Стратегия “округления половины в меньшую сторону” округляет до ближайшего числа с требуемой точностью, как и метод “округления половины в большую сторону”, за исключением того, что она разрывает равенство, округляя до меньшего из двух чисел. Вот несколько примеров:

Значение Округлить до Результат
13.825 Tens place 10
13.825 Ones place 14
13.825 Tenths place 13.8
13.825 Hundredths place 13.82

Вы можете реализовать стратегию “округления наполовину в меньшую сторону” в Python, заменив math.floor() в функции round_half_up() на math.ceil() и вычитая 0,5 вместо сложения:

def round_half_down(n, decimals=0):
    multiplier = 10 ** decimals
    return math.ceil(n*multiplier - 0.5) / multiplier

Давайте проверим round_half_down() на нескольких тестовых примерах:

>>> round_half_down(1.5)
1.0

>>> round_half_down(-1.5)
-2.0

>>> round_half_down(2.25, 1)
2.2

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

>>> data = [-2.15, 1.45, 4.35, -12.75]

Вычислим среднее значение этих чисел:

>>> statistics.mean(data)
-2.275

Затем вычислите среднее значение данных после округления до одного десятичного знака с помощью функций round_half_up() и round_half_down():

>>> rhu_data = [round_half_up(n, 1) for n in data]
>>> statistics.mean(rhu_data)
-2.2249999999999996

>>> rhd_data = [round_half_down(n, 1) for n in data]
>>> statistics.mean(rhd_data)
-2.325

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

Все остальные стратегии округления, которые мы обсудим, пытаются смягчить эти смещения различными способами.

Округление в половину от нуля

Если вы внимательно изучите функции round_half_up() и round_half_down(), то заметите, что ни одна из них не является симметричной относительно нуля:

>>> round_half_up(1.5)
2.0

>>> round_half_up(-1.5)
-1.0

>>> round_half_down(1.5)
1.0

>>> round_half_down(-1.5)
-2.0

Один из способов ввести симметрию – всегда округлять от нуля. В следующей таблице показано, как это работает:

Значение Округление Результат
15.25 Tens place 20
15.25 Ones place 15
15.25 Tenths place 15.3
-15.25 Tens place -20
-15.25 Ones place -15
-15.25 Tenths place -15.3

Чтобы применить стратегию “округления половины от нуля” к числу n, начните, как обычно, со сдвига десятичной точки вправо на заданное количество мест. Затем вы смотрите на цифру d, расположенную непосредственно справа от десятичного знака в этом новом числе. На этом этапе можно рассмотреть четыре случая:

  1. Если n положительно и d >= 5, округлите в большую сторону
  2. Если n положительно и d < 5, округлите в меньшую сторону
  3. Если n отрицательно и d >= 5, округлите в меньшую сторону
  4. Если n отрицательно и d < 5, округлите в большую сторону

После округления в соответствии с одним из вышеуказанных четырех правил вы сдвигаете десятичный знак обратно влево.

Учитывая число n и значение десятичных дробей, вы можете реализовать это в Python с помощью функций round_half_up() и round_half_down():

if n >= 0:
    rounded = round_half_up(n, decimals)
else:
    rounded = round_half_down(n, decimals)

Это достаточно просто, но на самом деле есть более простой способ!

Если вы сначала возьмете абсолютное значение n с помощью встроенной в Python функции abs(), вы можете просто использовать round_half_up() для округления числа. Затем нужно только придать округленному числу тот же знак, что и n. Один из способов сделать это – использовать функцию math.copysign().

math.copysign() берет два числа a и b и возвращает a со знаком b:

>>> math.copysign(1, -2)
-1.0

Обратите внимание, что функция math.copysign() возвращает число float, хотя оба ее аргумента были целыми числами.

Используя abs(), round_half_up() и math.copysign(), вы можете реализовать стратегию “округления половины от нуля” всего в двух строках Python:

def round_half_away_from_zero(n, decimals=0):
    rounded_abs = round_half_up(abs(n), decimals)
    return math.copysign(rounded_abs, n)

В функции round_half_away_from_zero() абсолютное значение n округляется до десятичных знаков после запятой с помощью функции round_half_up(), и этот результат присваивается переменной rounded_abs. Затем исходный знак n применяется к rounded_abs с помощью math.copysign(), и это конечное значение с правильным знаком возвращается функцией.

Проверка функции round_half_away_from_zero() на нескольких различных значениях показывает, что функция ведет себя так, как ожидалось:

>>> round_half_away_from_zero(1.5)
2.0

>>> round_half_away_from_zero(-1.5)
-2.0

>>> round_half_away_from_zero(-12.75, 1)
-12.8

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

Давайте проверим, насколько хорошо round_half_away_from_zero() устраняет смещение округления на примере из предыдущего раздела:

>>> data = [-2.15, 1.45, 4.35, -12.75] 
>>> statistics.mean(data)
-2.275

>>> rhaz_data = [round_half_away_from_zero(n, 1) for n in data]
>>> statistics.mean(rhaz_data)
-2.2750000000000004

Среднее значение чисел в данных сохраняется почти точно при округлении каждого числа в данных до одного десятичного знака с помощью функции round_half_away_from_zero()!

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

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

Округление половины до целого

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

Значение Округление половины до целого Результат
15.255 Tens place 20
15.255 Ones place 15
15.255 Tenths place 15.3
15.255 Hundredths place 15.26

Стратегия “округления половины до четного” – это стратегия, используемая встроенной в Python функцией round() и являющаяся правилом округления по умолчанию в стандарте IEEE-754. Эта стратегия работает в предположении, что вероятности того, что ничья в наборе данных будет округлена вниз или вверх, равны. На практике это обычно так и есть.

Теперь вы знаете, почему round(2.5) возвращает 2. Это не ошибка. Это осознанное проектное решение, основанное на надежных рекомендациях.

Чтобы убедиться в том, что round() действительно округляет до четного значения, попробуйте использовать ее для нескольких разных значений:

>>> round(4.5)
4

>>> round(3.5)
4

>>> round(1.75, 1)
1.8

>>> round(1.65, 1)
1.6

Функция round() почти свободна от смещения, но она не идеальна. Например, погрешность округления может возникнуть, если большинство связей в вашем наборе данных округляются до четного значения вместо того, чтобы округляться в меньшую сторону. Стратегии, смягчающие погрешность даже лучше, чем “округление половины до четного”, существуют, но они несколько неясны и нужны только в экстремальных обстоятельствах.

Наконец, round() страдает от тех же проблем, которые вы видели в round_half_up(), из-за ошибки представления с плавающей точкой:

>>> # Expected value: 2.68
>>> round(2.675, 2)
2.67

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

Если точность имеет первостепенное значение, следует использовать класс Decimal в Python.

The Decimal Class

Десятичный модуль Python – это одна из тех возможностей языка, о которых вы можете не знать, если вы новичок в Python. Руководство по работе с десятичным модулем можно найти в документации:

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

Преимущества десятичного модуля включают:

  • Точное десятичное представление: 0.1 на самом деле 0.1, а 0.1 + 0.1 + 0.1 – 0.3 возвращает 0, как и следовало ожидать.
  • Сохранение значащих цифр: При сложении 1,20 и 2,50 в результате получается 3,70 с сохранением нуля в конце для обозначения значимости.
  • Изменяемая пользователем точность: По умолчанию точность десятичного модуля составляет двадцать восемь цифр, но пользователь может изменять это значение в зависимости от конкретной задачи.

Давайте изучим, как работает округление в модуле decimal. Начните с ввода следующего текста в Python REPL:

>>> import decimal
>>> decimal.getcontext()
Context(
    prec=28,
    rounding=ROUND_HALF_EVEN,
    Emin=-999999,
    Emax=999999,
    capitals=1,
    clamp=0,
    flags=[],
    traps=[
        InvalidOperation,
        DivisionByZero,
        Overflow
    ]
)

decimal.getcontext() возвращает объект Context, представляющий контекст по умолчанию модуля decimal. Контекст включает точность по умолчанию и стратегию округления по умолчанию, среди прочего.

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

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

>>> from decimal import Decimal
>>> Decimal("0.1")
Decimal('0.1')

Просто для развлечения, давайте проверим утверждение, что Decimal поддерживает точное десятичное представление:

>>> Decimal('0.1') + Decimal('0.1') + Decimal('0.1')
Decimal('0.3')

А-а-а. Приятно, не правда ли?

Округление десятичной дроби выполняется с помощью метода .quantize():

>>> Decimal("1.65").quantize(Decimal("1.0"))
Decimal('1.6')

Хорошо, это, вероятно, выглядит немного странно, поэтому давайте разберем это. Аргумент Decimal(“1.0”) в .quantize() определяет количество десятичных знаков для округления числа. Поскольку 1.0 имеет один десятичный знак, число 1.65 округляется до одного десятичного знака. Стратегия округления по умолчанию – “округление половины до четного”, поэтому результат равен 1,6.

Вспомните, что функция round(), которая также использует стратегию “округления половины до четного”, не смогла правильно округлить 2,675 до двух знаков после запятой. Вместо 2,68, round(2.675, 2) возвращает 2,67. Благодаря модулям точного десятичного представления у вас не возникнет такой проблемы с классом Decimal:

>>> Decimal("2.675").quantize(Decimal("1.00"))
Decimal('2.68')

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

>>> decimal.getcontext().prec = 2
>>> Decimal("1.23") + Decimal("2.32")
Decimal('3.6')

Чтобы изменить точность, нужно вызвать функцию decimal.getcontext() и установить атрибут .prec. Если установка атрибута при вызове функции кажется вам странной, вы можете сделать это, потому что .getcontext() возвращает специальный объект Context, который представляет текущий внутренний контекст, содержащий параметры по умолчанию, используемые модулем decimal.

Точное значение 1,23 плюс 2,32 равно 3,55. Поскольку точность теперь двухзначная, а стратегия округления установлена по умолчанию “округление половины до четного”, значение 3,55 автоматически округляется до 3,6.

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

Флаг Стратегия округления
decimal.ROUND_CEILING Rounding up
decimal.ROUND_FLOOR Rounding down
decimal.ROUND_DOWN Truncation
decimal.ROUND_UP Rounding away from zero
decimal.ROUND_HALF_UP Rounding half away from zero
decimal.ROUND_HALF_DOWN Rounding half towards zero
decimal.ROUND_HALF_EVEN Rounding half to even
decimal.ROUND_05UP Rounding up and rounding towards zero

Первое, что следует заметить, это то, что схема именования, используемая модулем decimal, отличается от той, о которой мы договорились ранее в статье. Например, decimal.ROUND_UP реализует стратегию “округления от нуля”, которая на самом деле округляет отрицательные числа в меньшую сторону.

Во-вторых, некоторые стратегии округления, упомянутые в таблице, могут показаться незнакомыми, поскольку мы их не обсуждали. Вы уже видели, как работает decimal.ROUND_HALF_EVEN, поэтому давайте рассмотрим каждую из них в действии.

Стратегия decimal.ROUND_CEILING работает так же, как функция round_up(), которую мы определили ранее:

>>> decimal.getcontext().rounding = decimal.ROUND_CEILING

>>> Decimal("1.32").quantize(Decimal("1.0"))
Decimal('1.4')

>>> Decimal("-1.32").quantize(Decimal("1.0"))
Decimal('-1.3')

Обратите внимание, что результаты decimal.ROUND_CEILING не симметричны вокруг нуля.

Стратегия decimal.ROUND_FLOOR работает так же, как и наша функция round_down():

>>> decimal.getcontext().rounding = decimal.ROUND_FLOOR

>>> Decimal("1.32").quantize(Decimal("1.0"))
Decimal('1.3')

>>> Decimal("-1.32").quantize(Decimal("1.0"))
Decimal('-1.4')

Как и decimal.ROUND_CEILING, стратегия decimal.ROUND_FLOOR не является симметричной относительно нуля.

Стратегии decimal.ROUND_DOWN и decimal.ROUND_UP имеют несколько обманчивые названия. И ROUND_DOWN, и ROUND_UP симметричны вокруг нуля:

>>> decimal.getcontext().rounding = decimal.ROUND_DOWN

>>> Decimal("1.32").quantize(Decimal("1.0"))
Decimal('1.3')

>>> Decimal("-1.32").quantize(Decimal("1.0"))
Decimal('-1.3')

>>> decimal.getcontext().rounding = decimal.ROUND_UP

>>> Decimal("1.32").quantize(Decimal("1.0"))
Decimal('1.4')

>>> Decimal("-1.32").quantize(Decimal("1.0"))
Decimal('-1.4')

Стратегия decimal.ROUND_DOWN округляет числа в сторону нуля, как и функция truncate(). С другой стороны, decimal.ROUND_UP округляет все в сторону от нуля. Это явный отход от терминологии, о которой мы договорились ранее в этой статье, поэтому имейте это в виду, когда будете работать с модулем decimal.

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

>>> decimal.getcontext().rounding = decimal.ROUND_HALF_UP

>>> Decimal("1.35").quantize(Decimal("1.0"))
Decimal('1.4')

>>> Decimal("-1.35").quantize(Decimal("1.0"))
Decimal('-1.4')

Обратите внимание, что decimal.ROUND_HALF_UP работает так же, как наша round_half_away_from_zero(), а не как round_half_up().

Существует также стратегия decimal.ROUND_HALF_DOWN, которая разрушает равенство путем округления в сторону нуля:

>>> decimal.getcontext().rounding = decimal.ROUND_HALF_DOWN

>>> Decimal("1.35").quantize(Decimal("1.0"))
Decimal('1.3')

>>> Decimal("-1.35").quantize(Decimal("1.0"))
Decimal('-1.3')

Стратегия окончательного округления, доступная в десятичном модуле, сильно отличается от всего, что мы видели до сих пор:

>>> decimal.getcontext().rounding = decimal.ROUND_05UP

>>> Decimal("1.38").quantize(Decimal("1.0"))
Decimal('1.3')

>>> Decimal("1.35").quantize(Decimal("1.0"))
Decimal('1.3')

>>> Decimal("-1.35").quantize(Decimal("1.0"))
Decimal('-1.3')

В приведенных выше примерах кажется, что decimal.ROUND_05UP округляет все в сторону нуля. На самом деле, именно так и работает decimal.ROUND_05UP, если только результат округления не заканчивается на 0 или 5. В этом случае число округляется в сторону от нуля:

>>> Decimal("1.49").quantize(Decimal("1.0"))
Decimal('1.4')

>>> Decimal("1.51").quantize(Decimal("1.0"))
Decimal('1.6')

В первом примере число 1,49 сначала округляется до нуля во втором десятичном разряде, в результате чего получается 1,4. Поскольку 1,4 не оканчивается ни на 0, ни на 5, его оставляют как есть. С другой стороны, 1,51 округляется до нуля во втором десятичном разряде, в результате чего получается число 1,5. Оно оканчивается на 5, поэтому первый десятичный знак округляется от нуля до 1,6.

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

Более подробную информацию о Decimal можно найти в кратком учебнике в документации Python.

Далее давайте обратим внимание на два основных компонента Python в стеках научных вычислений и науки о данных: NumPy и Pandas.

Округление массивов NumPy

В области науки о данных и научных вычислений вы часто храните свои данные в виде массива NumPy. Одной из самых мощных возможностей NumPy является использование векторизации и трансляции для применения операций ко всему массиву сразу, а не к одному элементу за раз.

Давайте сгенерируем некоторые данные, создав массив NumPy 3×4 из псевдослучайных чисел:

>>> import numpy as np
>>> np.random.seed(444)

>>> data = np.random.randn(3, 4)
>>> data
array([[ 0.35743992,  0.3775384 ,  1.38233789,  1.17554883],
       [-0.9392757 , -1.14315015, -0.54243951, -0.54870808],
       [ 0.20851975,  0.21268956,  1.26802054, -0.80730293]])

Сначала мы создаем модуль np.random, чтобы вы могли легко воспроизвести результат. Затем с помощью np.random.randn() создается массив NumPy 3×4 чисел с плавающей точкой.

Чтобы округлить все значения в массиве данных, можно передать данные в качестве аргумента функции np.around(). Желаемое количество знаков после запятой задается с помощью аргумента decimals ключевого слова. Используется стратегия округления от половины до четного, как и встроенная в Python функция round().

Например, в следующем примере все значения в данных округляются до трех знаков после запятой:

>>> np.around(data, decimals=3)
array([[ 0.357,  0.378,  1.382,  1.176],
       [-0.939, -1.143, -0.542, -0.549],
       [ 0.209,  0.213,  1.268, -0.807]])

np.around() находится во власти ошибки представления с плавающей точкой, так же как и round().

Например, значение в третьей строке первого столбца массива данных равно 0,20851975. Когда вы округляете его до трех знаков после запятой, используя стратегию “округления половины до четного”, вы ожидаете, что значение будет равно 0,208. Но в выводе np.around() видно, что значение округляется до 0,209. Однако значение 0,3775384 в первой строке второго столбца правильно округляется до 0,378.

Если вам нужно округлить данные в массиве до целых чисел, NumPy предлагает несколько вариантов:

Функция np.ceil() округляет каждое значение в массиве до ближайшего целого числа, большего или равного исходному значению:

>>> np.ceil(data)
array([[ 1.,  1.,  2.,  2.],
       [-0., -1., -0., -0.],
       [ 1.,  1.,  2., -0.]])

Эй, мы открыли новое число! Отрицательный ноль!

На самом деле, стандарт IEEE-754 требует реализации как положительного, так и отрицательного нуля. Как можно использовать нечто подобное? Википедия знает ответ:

Неформально можно использовать обозначение “-0” для отрицательного значения, которое было округлено до нуля. Это обозначение может быть полезно, когда отрицательный знак имеет значение; например, при табулировании температур по Цельсию, где отрицательный знак означает ниже нуля. (Источник)

Чтобы округлить каждое значение до ближайшего целого числа, используйте np.floor():

>>> np.floor(data)
array([[ 0.,  0.,  1.,  1.],
       [-1., -2., -1., -1.],
       [ 0.,  0.,  1., -1.]])

Вы также можете усечь каждое значение до его целочисленной составляющей с помощью np.trunc():

>>> np.trunc(data)
array([[ 0.,  0.,  1.,  1.],
       [-0., -1., -0., -0.],
       [ 0.,  0.,  1., -0.]])

Наконец, чтобы округлить до ближайшего целого числа, используя стратегию “округления половины до четного”, используйте np.rint():

>>> np.rint(data)
array([[ 0.,  0.,  1.,  1.],
       [-1., -1., -1., -1.],
       [ 0.,  0.,  1., -1.]])

Вы могли заметить, что многие стратегии округления, которые мы обсуждали ранее, здесь отсутствуют. Для подавляющего большинства ситуаций функция around() – это все, что вам нужно. Если вам нужно реализовать другую стратегию, например, round_half_up(), вы можете сделать это с помощью простой модификации:

def round_half_up(n, decimals=0):
    multiplier = 10 ** decimals
    # Replace math.floor with np.floor
    return np.floor(n*multiplier + 0.5) / multiplier

Благодаря векторным операциям NumPy это работает именно так, как вы ожидаете:

>>> round_half_up(data, decimals=2)
array([[ 0.36,  0.38,  1.38,  1.18],
       [-0.94, -1.14, -0.54, -0.55],
       [ 0.21,  0.21,  1.27, -0.81]])

Теперь, когда вы стали мастером округления в NumPy, давайте посмотрим на другой тяжеловес Python для науки о данных – библиотеку Pandas.

Округление в Pandas и DataFrame

Библиотека Pandas стала основным инструментом для специалистов по обработке данных и аналитиков данных, работающих на Python. По словам Джо Уиндхэма из Real Python:

Pandas – это геймчейнджер для науки о данных и аналитики, особенно если вы пришли к Python, потому что искали что-то более мощное, чем Excel и VBA.

Две основные структуры данных Pandas – это DataFrame, который в очень свободном смысле похож на таблицу Excel, и Series, который можно представить как столбец в электронной таблице. Объекты Series и DataFrame можно также эффективно округлять с помощью методов Series.round() и DataFrame.round():

>>> import pandas as pd

>>> # Re-seed np.random if you closed your REPL since the last example
>>> np.random.seed(444)

>>> series = pd.Series(np.random.randn(4))
>>> series
0    0.357440
1    0.377538
2    1.382338
3    1.175549
dtype: float64

>>> series.round(2)
0    0.36
1    0.38
2    1.38
3    1.18
dtype: float64

>>> df = pd.DataFrame(np.random.randn(3, 3), columns=["A", "B", "C"])
>>> df
          A         B         C
0 -0.939276 -1.143150 -0.542440
1 -0.548708  0.208520  0.212690
2  1.268021 -0.807303 -3.303072

>>> df.round(3)
       A      B      C
0 -0.939 -1.143 -0.542
1 -0.549  0.209  0.213
2  1.268 -0.807 -3.303

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

>>> # Specify column-by-column precision with a dictionary
>>> df.round({"A": 1, "B": 2, "C": 3})
     A     B      C
0 -0.9 -1.14 -0.542
1 -0.5  0.21  0.213
2  1.3 -0.81 -3.303

>>> # Specify column-by-column precision with a Series
>>> decimals = pd.Series([1, 2, 3], index=["A", "B", "C"])
>>> df.round(decimals)
     A     B      C
0 -0.9 -1.14 -0.542
1 -0.5  0.21  0.213
2  1.3 -0.81 -3.303

Если вам нужна большая гибкость в округлении, вы можете применить функции NumPy floor(), ceil() и rint() к объектам Pandas Series и DataFrame:

>>> np.floor(df)
     A    B    C
0 -1.0 -2.0 -1.0
1 -1.0  0.0  0.0
2  1.0 -1.0 -4.0

>>> np.ceil(df)
     A    B    C
0 -0.0 -1.0 -0.0
1 -0.0  1.0  1.0
2  2.0 -0.0 -3.0

>>> np.rint(df)
     A    B    C
0 -1.0 -1.0 -1.0
1 -1.0  0.0  0.0
2  1.0 -1.0 -3.0

Здесь также будет работать модифицированная функция round_half_up() из предыдущего раздела:

>>> round_half_up(df, decimals=2)
      A     B     C
0 -0.94 -1.14 -0.54
1 -0.55  0.21  0.21
2  1.27 -0.81 -3.30

Поздравляем, вы на пути к мастерству округления! Теперь вы знаете, что существует большое количество способов округлить число. (Вы можете реализовать множество стратегий округления в чистом Python, и вы отточили свои навыки округления массивов NumPy и объектов Pandas Series и DataFrame.

Остался еще один шаг: знать, когда применить нужную стратегию.

Применение и лучшие практики

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

Хранить больше и округлять позже

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

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

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

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

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

Наконец, при вычислении среднесуточной температуры следует вычислять ее с полной точностью и округлять окончательный ответ.

Соблюдайте местные валютные правила

Когда вы заказываете в кофейне чашку кофе за 2,40 доллара, продавец обычно добавляет обязательный налог. Размер этого налога во многом зависит от того, где вы находитесь географически, но для примера предположим, что он составляет 6%. Налог, который необходимо добавить, составляет $0,144. Следует ли округлять эту сумму в большую сторону до $0,15 или в меньшую до $0,14? Ответ, вероятно, зависит от правил, установленных местными властями!

Подобные ситуации могут возникать и при конвертации одной валюты в другую. В 1999 году Европейская комиссия по экономическим и финансовым вопросам кодифицировала использование стратегии “округления половины от нуля” при конвертации валют в евро, однако в других валютах могут быть приняты иные правила.

Другой сценарий, “шведское округление“, возникает, когда минимальная единица валюты на уровне бухгалтерского учета в стране меньше, чем самая низкая единица физической валюты. Например, если чашка кофе стоит 2,54 доллара после уплаты налогов, но в обращении нет монет достоинством 1 цент, что делать? У покупателя не будет точной суммы, а торговец не сможет сделать точную сдачу.

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

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

Если есть сомнения, округляйте до четного значения

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

Из всех методов, которые мы обсуждали в этой статье, стратегия “округление половины до четного” лучше всего минимизирует погрешность округления. К счастью, Python, NumPy и Pandas по умолчанию используют эту стратегию, поэтому, используя встроенные функции округления, вы уже хорошо защищены!

Итоги

Какое это было путешествие!

В этой статье вы узнали, что:

  • Существуют различные стратегии округления, которые вы теперь знаете, как реализовать на чистом Python.
  • Каждая стратегия округления по своей природе вносит погрешность в округление, и стратегия “округление половины до четного” хорошо смягчает эту погрешность в большинстве случаев.
  • Способ, которым компьютеры хранят в памяти числа с плавающей точкой, естественно, вносит тонкую ошибку округления, но вы узнали, как ее обойти с помощью модуля decimal в стандартной библиотеке Python.
  • Вы можете округлять массивы NumPy и объекты Pandas Series и DataFrame.
  • Существуют лучшие практики округления при работе с реальными данными.