Как избежать неправильного округления с помощью numpy.round? - PullRequest
0 голосов
/ 16 мая 2018

Я работаю с числами с плавающей запятой.Если я сделаю:

import numpy as np
np.round(100.045, 2)

Я получу:

Out[15]: 100.04

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

Мой вопрос: как мне избежать это ошибка?

Ответы [ 3 ]

0 голосов
/ 16 мая 2018

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

>>> a = 100.045
>>> a.as_integer_ratio()  # not exact
(7040041011254395, 70368744177664)

>>> a = 0.25
>>> a.as_integer_ratio()  # exact
(1, 4)

Также важно знать, что вы не можете восстановить использованный литерал (100.045) из полученного плавающего числаномер точки.Поэтому единственное, что вы можете сделать, - это использовать тип данных произвольной точности вместо литерала.Например, вы можете использовать Fraction или Decimal (просто упомянуть два встроенных типа).

Я упоминал, что вы не можете восстановить литерал, когда он анализируется как float - поэтому вы должны ввести егов виде строки или чего-то еще, представляющего число в точности и поддерживаемого этими типами данных:

>>> from fractions import Fraction
>>> f = Fraction(100045, 100)
>>> f
Fraction(20009, 20)

>>> f = Fraction("100.045")
>>> f
Fraction(20009, 20)

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

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

>>> import numpy as np

>>> a = np.array([Decimal("100.045") for _ in range(1000)])
>>> np.round(a)
AttributeError: 'decimal.Decimal' object has no attribute 'rint'

Вначале я сказал, что вы правы лишь отчасти.Есть еще один поворот!

Вы упомянули, что округление 100.045 очевидно даст 100.05.Но это совсем не очевидно, в вашем случае это даже неправильно (в контексте математики с плавающей запятой в программировании - это было бы верно для «обычных вычислений»).Во многих языках программирования «половинное» значение (где число после десятичного числа, которое вы округляете, равно 5) не всегда с округлением в большую сторону - например, Python (и NumPy) используют округление "подход "половина к четному" , потому что он менее предвзят.Например, 0.5 будет округлено до 0, а 1.5 будет округлено до 2.

Таким образом, даже если 100.045 можно представить в точности как число с плавающей точкой - оно все равно будет округлено до 100.04 из-за этого правила округления!

>>> round(Fraction("100.045"), 1)
Fraction(5002, 5)

>>> 5002 / 5
1000.4

>>> d = Decimal("100.045")
>>> round(d, 2)
Decimal('100.04')

Это даже упоминается в документах NumPy для numpy.around:

Примечания

Для значений, находящихся точно посередине между округленными десятичными значениями, NumPy округляет до ближайшего четного значения .Таким образом, 1,5 и 2,5 округляют до 2,0, -0,5 и 0,5 округляют до 0,0 и т. Д. Результаты также могут быть удивительными из-за неточного представления десятичных дробей в стандарте IEEE с плавающей запятой [R1011] и ошибок, возникающих при масштабировании на степени десяти.

(выделение мое.)

Единственный (по крайней мере, я знаю) числовой тип в Python, позволяющий установить правило округления вручную, - Decimal - через ROUND_HALF_UP:

>>> from decimal import Decimal, getcontext, ROUND_HALF_UP
>>> dc = getcontext()
>>> dc.rounding = ROUND_HALF_UP
>>> d = Decimal("100.045")
>>> round(d, 2)
Decimal('100.05')

Сводка

Поэтому, чтобы избежать "ошибки", вы должны:

  • Запретить Python анализировать его как плавающую точкузначение и
  • используют тип данных, который может точно представлять его
  • , тогда вам придется вручную переопределить режим округления по умолчанию, чтобы вы получили округление для "половин".
  • (откажитесь от NumPy, потому что у него нет типов данных произвольной точности)
0 голосов
/ 07 июня 2019

Это не ошибка, а фича)))

Вы можете просто использовать этот трюк:

def myround(val):
"Fix pythons round"
d,v = math.modf(val)
if d==0.5:
    val += 0.000000001
return round(val)
0 голосов
/ 16 мая 2018

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

In [24]: dec, integ = np.modf(100.045)

In [25]: integ + np.round(dec, 2)
Out[25]: 100.05

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

В данном случае вот что такое dec:

In [30]: dec
Out[30]: 0.045000000000001705

И вы можете проверить, что раунд дает тот же результат с 0.045:

In [31]: round(0.045, 2)
Out[31]: 0.04

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

In [37]: dec, i = np.modf(100.0333)

In [38]: dec
Out[38]: 0.033299999999997

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

...