Я пытаюсь извлечь из массива все значения в пределах определенного slice
(что-то вроде range
, но с необязательными start
, stop
и step
). И при этом я хочу извлечь выгоду из тяжелой оптимизации, которую range
объекты используют для range.__contains__()
, что означает, что им никогда не нужно создавать экземпляр всего диапазона значений (сравните Почему «1000000000000000 в диапазоне (1000000000000001)» так быстро в Python 3).
Следующий код работает, но он ужасно неэффективен, потому что i
преобразуется в полноценный массив, увеличивая использование памяти и время выполнения.
import numpy as np
arr = np.array([0, 20, 29999999, 10, 30, 40, 50])
M = np.max(arr)
# slice based on values
s = slice(20, None)
i = range(*s.indices(M + 1))
print(arr[np.isin(arr, i)]) # works, but inefficient!
Выход:
[ 20 29999999 30 40 50]
Есть ли функция numpy, чтобы улучшить это напрямую? Должен ли я вместо этого использовать np.vectorize
/np.where
с обратным вызовом с использованием среза (кажется, что возврат от C++ к Python для каждого отдельного элемента тоже может быть медленным [или он не будет этого делать?])? Должен ли я вычесть start
из своих значений, разделить на step
, а затем посмотреть, равны ли значения >= 0 && < (stop - start) / step
? Или я упускаю гораздо лучший способ?
@folengateis хорошая идея, так лучше. Это все еще медленно. Подумайте о том, чтобы иметь массив объемом 4 ГБ и выполнить np.isin(arr, np.arange(65000))
несколько раз...
Связано: stackoverflow.com/questions/13869173/…
Ничего волшебного в range
нет. Это просто способ указать конечные точки, и он откладывает создание экземпляров значений до тех пор, пока они не потребуются (как в цикле for
или list(range...)
). numpy
не может это использовать — обычно ему нужен конкретный массив (ничего «виртуального»). isin
документирует различные стратегии. Чаще всего он сортирует a
и b
вместе и ищет дубликаты. Или, если b
достаточно мал, он может проверить каждый элемент b
и logical_and
результатов. Но в целом, как показывают ответы, лучше всего взять строительный блок.
С slice(20, None)
все, что вам нужно проверить, это np.nonzero(arr>=20)
.
@hpaulj «В дальности нет ничего волшебного» — ну, есть range.__contains__
. Это не волшебство, но оно определенно реализовано на C++ и, таким образом, позволяет избежать перехода от C++ к Python.
range
in
по-прежнему может проверять только одно значение и не быстрее, чем проверка неравенства, такая как 23 >= range.start
. Я предполагаю, что он выполняет множество тестов - является ли оно целым числом, размером относительно начала, остановки, модуля относительно шага. Для тестирования скаляров и элементов списка это полезно, для тестирования массива — нет.
@hpaulj «Для тестирования скаляров и элементов списка это полезно, а для тестирования массива — нет». Я знаю это. Вот почему мой вопрос: есть ли функция numpy, которая это делает? (Кажется, нет.)
Что такое средняя длина вашего массива
@Onyambu все, что загружают мои пользователи. Может быть от 4 байт до 40 гигабайт.
И является ли максимум массива остановкой? или можно выбрать любую остановку?
@Onyambu пользователь может ввести остановку или ее отсутствие, в этом случае остановкой неявно станет максимум массива. Очень похоже на нарезку на основе индексов. Обратите внимание, что stop
может превышать максимальное значение, сравните np.arange(20)[:9999]
, которое является допустимым выражением.
Представьте, что пользователю разрешено вводить все, что вам разрешено вводить вместо ...
в (1D) array[...]
выражении (простое целое число, :
, 1:
, 10:20
, ::2
,...), за исключением того, что выражение среза/диапазона должно применяться к значениям, а не к индексам.
Примените ответ ниже, но вместо этого верните array[mask]
вместо np.nonzero(mask)
В ответе @jared хорошо используется numpy
. numpy
использует slices
для индексации, но малопригоден для «ленивого» range
. Широко используемый arange
создает целый массив. Даже классы index_tricks
, использующие нотацию среза, преобразуют это в вызовы arange
или linspce
. isin
сравнивает целые массивы, а не объекты «ленивого» диапазона. За многие годы подписки на SO [numpy] я не видел такого запроса, как ваш.
@hpaulj «За многие годы подписки на SO [numpy] я не видел такого запроса, как ваш». - это хорошо, правда? Обычно людей вызывают за то, что они задают повторяющийся вопрос, а не новый.
Я хотел подчеркнуть, что вряд ли можно найти стандартную функцию для редко необходимой задачи. Я предполагаю, что чтобы добиться большего, чем Джаред, вам нужно использовать numba
(или cython)
Если я правильно понял вашу проблему - вы желаете вытянуть все значения, включая и после 20
. В таком случае вы можете использовать накопительную сумму, как в:
>>> arr[(arr==20).cumsum()>0]
array([ 20, 30, 40, 50, 29999999])
логическая сумма вернет 1/0, поэтому cumsum
не может быть отрицательным, и как только она хоть раз станет положительной, это означает, что 20
было обнаружено.
----------------РЕДАКТИРОВАТЬ-------------------------
Обобщенное решение по комментариям:
# any condition same rules as above apply - first encounter of matching condition will populate till the end
mask = (arr>20).cumsum()>0
insert
и [:-1]
, если вы не хотите, чтобы она была включена:stop = np.insert((arr==50).cumsum()>0, 0, False)[:-1]
mask
:mask = mask & ~stop
arr = arr[mask][::step]
Я хочу, чтобы это работало для всех возможных диапазонов (или срезов), учитывая начало, остановку и шаг.
Кроме того, не после 20, а больше или равно 20. На самом деле речь идет о значении, а не об индексе. Извините, что мой входной массив был отсортирован.
@bers, попробуйте сейчас - я обобщил решение (принципы остаются - поэтому я бы сохранил первоначальный ответ - я чувствую, что ваш конкретный вопрос довольно специфичен - стоит также оставить ответ высокого уровня)
Я думаю, вы неправильно понимаете мой вопрос. Пожалуйста, помогите мне переформулировать это. Что я хочу от массива, так это (индексы) всех значений, находящихся в определенном диапазоне. Эти значения не обязательно находятся в одном последовательном фрагменте. Если бы мой массив был np.array([0, 20, 29999999, 10, 30, 40, 50])
, я бы хотел [ 20 29999999 30 40 50]
.
arr[arr>=20]
тогда сделаю
Как я писал выше: «Я хочу, чтобы это работало для всех возможных диапазонов (или срезов), учитывая начало, остановку и шаг».
Основываясь на этого комментария, я думаю, вам просто нужно проверить >
и <
и вернуть ненулевые индексы из этого результата.
import numpy as np
def get_indices_within_range(arr, start, stop, step=None):
mask = np.ones_like(arr, dtype=bool)
if start is None and stop is None:
raise ValueError("At least one of start and stop must not be None.")
if start is not None:
mask *= arr >= start
if stop is not None:
mask *= arr < stop
if step is not None and step != 1:
if start is not None:
mask *= (arr - start)%step == 0
else:
mask *= arr%step == 0
if len(arr.shape) > 1:
return np.nonzero(mask)
else:
return np.nonzero(mask)[0]
rng = np.random.default_rng(2)
arr = rng.integers(0, 100, size=(20,))
print(arr)
indices = get_indices_within_range(arr, 20, 70)
print(indices)
indices = get_indices_within_range(arr, 20, None)
print(indices)
indices = get_indices_within_range(arr, None, 70)
print(indices)
indices = get_indices_within_range(arr, 20, 70, 10)
print(indices)
indices = get_indices_within_range(arr, None, 70, 10)
print(indices)
Полученные результаты:
[83 26 10 29 41 81 45 9 33 60 81 72 99 18 88 5 55 27 20 65]
[ 1 3 4 6 8 9 16 17 18 19]
[ 0 1 3 4 5 6 8 9 10 11 12 14 16 17 18 19]
[ 1 2 3 4 6 7 8 9 13 15 16 17 18 19]
[ 9 18]
[ 2 9 18]
Да, это работает. Недостаток в том, что не учитывается step
; и даже текущее решение должно дважды перебирать весь массив. В некоторых языках minmax
необходимо вычислять min
и max
за одну итерацию, сравните en.cppreference.com/w/cpp/algorithm/minmax. Что-то вроде этого для проверки диапазона было бы здорово в numpy.
Как будет использоваться step
? Мне непонятно, откуда это берется.
Ну, мой вопрос касается объектов slice
и range
, так что, возможно, посмотрите, как это там работает. docs.python.org/3.12/library/stdtypes.html#range и docs.python.org/3.12/library/functions.html#slice
Я знаю, как step
работает для этих объектов, но неясно, как это применимо к этой проблеме.
Мне нужны индексы значений в диапазоне. Если мой диапазон равен range(0, 10, 3)
, мне нужны индексы значений в {0, 3, 6, 9}
.
Обновленный код теперь делает то, что вы хотите?
Да, это так. Это не то, чего я хотел (я мог бы написать такой код сам), но я приму ответ, пока не появится лучший. В конце концов, я думаю, что numpy
еще не реализовал это, и мне не удалось найти более эффективную реализацию, даже в виде расширения модуля C.
x in range(...)
эффективен, поскольку может делать утверждения о том, какими будут значения в диапазоне. По сути, это реализация f(x) = mx+c
где x ∈ Z and N <= x < M
.
Как правило, чтобы сделать это в numpy, вы должны создать набор масок, а затем &
объединить их, чтобы вычислить битовую маску интересующего вас массива значений, а затем индексировать ваши значения с помощью этой битовой маски. Это приведет к тому, что вы вычислите множество избыточных значений, но это компенсируется эффективностью, которую предлагает numpy. например. отсутствие накладных расходов на объект Python для каждого значения и возможность использования инструкций SIMD.
Итак, вы можете сделать что-то вроде:
values = np.array([[0, 20, 29999999, 10, 30, 40, 50]])
# these values should produce [10, 30, 50]
start = 10 # excludes 0
stop = 100 # excludes 29999999
step = 20 # excludes 20 and 40 (in combination with start=10)
start_mask = start <= values
stop_mask = values < stop
step_mask = (values - start) % step == 0
final_mask = start_mask & stop_mask & step_mask
result = values[final_mask]
assert result.tolist() == [10, 30, 50]
В приведенный выше код еще предстоит внести оптимизацию. Один из них — избегать вычислений и использования step_mask
When step == 1
(поскольку созданная битовая маска будет полностью истинной). Другие могут получить numpy для повторного использования массива битовых масок, созданного в результате вычисления start_mask
, вместо создания нового массива для каждого промежуточного результата.
почему ты используешь
range
вместоnp.arange
? это уже выигрыш на порядок