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