Вот пользовательская функция, которая позволяет переходить через десятичные приращения:
def my_range(start, stop, step):
i = start
while i < stop:
yield i
i += step
Это работает так:
out = list(my_range(0, 1, 0.1))
print(out)
[0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, 0.8999999999999999, 0.9999999999999999]
Теперь нет ничего удивительного в этом. Понятно, что это происходит из-за неточностей с плавающей запятой и что 0.1
не имеет точного представления в памяти. Итак, эти ошибки точности понятны.
Возьмите numpy
с другой стороны:
import numpy as np
out = np.arange(0, 1, 0.1)
print(out)
array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
Интересно то, что здесь нет видимых неточностей неточности. Я подумал, что это может быть связано с тем, что показывает __repr__
, поэтому для подтверждения я попробовал это:
x = list(my_range(0, 1.1, 0.1))[-1]
print(x.is_integer())
False
x = list(np.arange(0, 1.1, 0.1))[-1]
print(x.is_integer())
True
Итак, моя функция возвращает неверное верхнее значение (оно должно быть 1.0
, но на самом деле это 1.0999999999999999
), но np.arange
делает это правильно.
Мне известно о математике с плавающей запятой? но смысл этого вопроса :
Как NumPy делает это?
3 ответа
Разница в конечных точках заключается в том, что NumPy вычисляет длину впереди, а не ad hoc, потому что ей нужно предварительно выделить массив. Вы можете увидеть это в numpy.arange(0.0, 2.1, 0.3)
:
In [46]: numpy.arange(0.0, 2.1, 0.3)
Out[46]: array([ 0. , 0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 2.1])
Гораздо безопаснее использовать numpy.linspace
, где вместо размера шага вы говорите, сколько элементов вы хотите и хотите ли вы включить правильную конечную точку.
Может показаться, что при вычислении элементов NumPy не испытывал ошибок округления, но это только из-за другой логики отображения. NumPy усекает отображаемую точность более агрессивно, чем float.__repr__
. Если вы используете tolist
для получения обычного списка обычных скаляров Python (и, следовательно, обычной float
логики отображения), вы можете увидеть, что NumPy также столкнулся с ошибкой округления:
In [47]: numpy.arange(0, 1, 0.1).tolist()
Out[47]:
[0.0,
0.1,
0.2,
0.30000000000000004,
0.4,
0.5,
0.6000000000000001,
0.7000000000000001,
0.8,
0.9]
Он испытал немного различную ошибку округления - например, в .6 и .7 вместо .8 и .9 - потому что он также использует другие средства вычисления элементов, реализованные в <
Реализация функции fill
имеет то преимущество, что использует start + i*step
вместо многократного добавления шага, что позволяет избежать накопления ошибок при каждом добавлении. Однако у него есть недостаток, заключающийся в том, что (без видимой на то причины) он пересчитывает шаг из первых двух элементов вместо того, чтобы принять шаг в качестве аргумента, поэтому он может потерять большую точность на шаг впереди.
Хотя arange
выполняет переход по диапазону немного по-другому, он все еще имеет проблему с представлением с плавающей точкой:
In [1358]: np.arange(0,1,0.1)
Out[1358]: array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
Печать скрывает это; преобразовать его в список, чтобы увидеть кровавые подробности:
In [1359]: np.arange(0,1,0.1).tolist()
Out[1359]:
[0.0,
0.1,
0.2,
0.30000000000000004,
0.4,
0.5,
0.6000000000000001,
0.7000000000000001,
0.8,
0.9]
Или с другой итерацией
In [1360]: [i for i in np.arange(0,1,0.1)] # e.g. list(np.arange(...))
Out[1360]:
[0.0,
0.10000000000000001,
0.20000000000000001,
0.30000000000000004,
0.40000000000000002,
0.5,
0.60000000000000009,
0.70000000000000007,
0.80000000000000004,
0.90000000000000002]
В этом случае каждый отображаемый элемент представляет собой np.float64
, где, как и в первом, каждый представляет собой float
.
Помимо различного представления списков и массивов NumPys arange
работает путем умножения вместо повторного добавления. Это больше похоже на:
def my_range2(start, stop, step):
i = 0
while start+(i*step) < stop:
yield start+(i*step)
i += 1
Тогда вывод полностью равен:
>>> np.arange(0, 1, 0.1).tolist() == list(my_range2(0, 1, 0.1))
True
При повторном добавлении вы «накапливаете» ошибки округления с плавающей запятой. На умножение все еще влияет округление, но ошибка не накапливается.
Как указано в комментариях, это не совсем то, что происходит. Насколько я вижу, это больше похоже на:
def my_range2(start, stop, step):
length = math.ceil((stop-start)/step)
# The next two lines are mostly so the function really behaves like NumPy does
# Remove them to get better accuracy...
next = start + step
step = next - start
for i in range(length):
yield start+(i*step)
Но не уверен, что это правильно, потому что в NumPy происходит гораздо больше.
Похожие вопросы
Связанные вопросы
Новые вопросы
python
Python - это многопарадигмальный, динамически типизированный, многоцелевой язык программирования. Он разработан для быстрого изучения, понимания и использования, а также для обеспечения чистого и единообразного синтаксиса. Обратите внимание, что Python 2 официально не поддерживается с 01.01.2020. Тем не менее, для вопросов о Python, связанных с версией, добавьте тег [python-2.7] или [python-3.x]. При использовании варианта Python (например, Jython, PyPy) или библиотеки (например, Pandas и NumPy) включите его в теги.