Есть ли функция Numpy для подмножества массива на основе значений (а не индексов) в срезе или диапазоне?

Я пытаюсь извлечь из массива все значения в пределах определенного 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? Или я упускаю гораздо лучший способ?

почему ты используешь range вместо np.arange? это уже выигрыш на порядок

folen gateis 29.06.2024 11:29

@folengateis хорошая идея, так лучше. Это все еще медленно. Подумайте о том, чтобы иметь массив объемом 4 ГБ и выполнить np.isin(arr, np.arange(65000)) несколько раз...

bers 29.06.2024 15:09

Связано: stackoverflow.com/questions/13869173/…

bers 29.06.2024 16:08

Ничего волшебного в range нет. Это просто способ указать конечные точки, и он откладывает создание экземпляров значений до тех пор, пока они не потребуются (как в цикле for или list(range...)). numpy не может это использовать — обычно ему нужен конкретный массив (ничего «виртуального»). isin документирует различные стратегии. Чаще всего он сортирует a и b вместе и ищет дубликаты. Или, если b достаточно мал, он может проверить каждый элемент b и logical_and результатов. Но в целом, как показывают ответы, лучше всего взять строительный блок.

hpaulj 29.06.2024 18:11

С slice(20, None) все, что вам нужно проверить, это np.nonzero(arr>=20).

hpaulj 29.06.2024 18:16

@hpaulj «В дальности нет ничего волшебного» — ну, есть range.__contains__. Это не волшебство, но оно определенно реализовано на C++ и, таким образом, позволяет избежать перехода от C++ к Python.

bers 29.06.2024 18:20
rangein по-прежнему может проверять только одно значение и не быстрее, чем проверка неравенства, такая как 23 >= range.start. Я предполагаю, что он выполняет множество тестов - является ли оно целым числом, размером относительно начала, остановки, модуля относительно шага. Для тестирования скаляров и элементов списка это полезно, для тестирования массива — нет.
hpaulj 29.06.2024 22:02

@hpaulj «Для тестирования скаляров и элементов списка это полезно, а для тестирования массива — нет». Я знаю это. Вот почему мой вопрос: есть ли функция numpy, которая это делает? (Кажется, нет.)

bers 29.06.2024 23:03

Что такое средняя длина вашего массива

Onyambu 30.06.2024 15:48

@Onyambu все, что загружают мои пользователи. Может быть от 4 байт до 40 гигабайт.

bers 30.06.2024 16:21

И является ли максимум массива остановкой? или можно выбрать любую остановку?

Onyambu 30.06.2024 16:38

@Onyambu пользователь может ввести остановку или ее отсутствие, в этом случае остановкой неявно станет максимум массива. Очень похоже на нарезку на основе индексов. Обратите внимание, что stop может превышать максимальное значение, сравните np.arange(20)[:9999], которое является допустимым выражением.

bers 30.06.2024 19:52

Представьте, что пользователю разрешено вводить все, что вам разрешено вводить вместо ... в (1D) array[...] выражении (простое целое число, :, 1:, 10:20, ::2,...), за исключением того, что выражение среза/диапазона должно применяться к значениям, а не к индексам.

bers 30.06.2024 19:54

Примените ответ ниже, но вместо этого верните array[mask] вместо np.nonzero(mask)

Onyambu 30.06.2024 19:59

В ответе @jared хорошо используется numpy. numpy использует slices для индексации, но малопригоден для «ленивого» range. Широко используемый arange создает целый массив. Даже классы index_tricks, использующие нотацию среза, преобразуют это в вызовы arange или linspce. isin сравнивает целые массивы, а не объекты «ленивого» диапазона. За многие годы подписки на SO [numpy] я не видел такого запроса, как ваш.

hpaulj 30.06.2024 23:12

@hpaulj «За многие годы подписки на SO [numpy] я не видел такого запроса, как ваш». - это хорошо, правда? Обычно людей вызывают за то, что они задают повторяющийся вопрос, а не новый.

bers 01.07.2024 06:09

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

hpaulj 01.07.2024 06:18
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
17
156
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Если я правильно понял вашу проблему - вы желаете вытянуть все значения, включая и после 20. В таком случае вы можете использовать накопительную сумму, как в:

>>> arr[(arr==20).cumsum()>0] 
array([      20,       30,       40,       50, 29999999])

логическая сумма вернет 1/0, поэтому cumsum не может быть отрицательным, и как только она хоть раз станет положительной, это означает, что 20 было обнаружено.

----------------РЕДАКТИРОВАТЬ-------------------------

Обобщенное решение по комментариям:

  1. создайте маску, закрывающую отправную точку:
# any condition same rules as above apply - first encounter of matching condition will populate till the end
mask = (arr>20).cumsum()>0
  1. вырезка условия остановки (включена строка условия остановки — удалите insert и [:-1], если вы не хотите, чтобы она была включена:
stop = np.insert((arr==50).cumsum()>0, 0, False)[:-1]
  1. объедините mask:
mask  = mask & ~stop
  1. применить шаг (то же, что и выше — вопрос о том, где вы применяете шаг — предполагается, что с момента первого совпадения внутри маски):
arr = arr[mask][::step]

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

bers 29.06.2024 11:07

Кроме того, не после 20, а больше или равно 20. На самом деле речь идет о значении, а не об индексе. Извините, что мой входной массив был отсортирован.

bers 29.06.2024 11:49

@bers, попробуйте сейчас - я обобщил решение (принципы остаются - поэтому я бы сохранил первоначальный ответ - я чувствую, что ваш конкретный вопрос довольно специфичен - стоит также оставить ответ высокого уровня)

Georgina Skibinski 29.06.2024 11:55

Я думаю, вы неправильно понимаете мой вопрос. Пожалуйста, помогите мне переформулировать это. Что я хочу от массива, так это (индексы) всех значений, находящихся в определенном диапазоне. Эти значения не обязательно находятся в одном последовательном фрагменте. Если бы мой массив был np.array([0, 20, 29999999, 10, 30, 40, 50]), я бы хотел [ 20 29999999 30 40 50].

bers 29.06.2024 15:05
arr[arr>=20] тогда сделаю
Georgina Skibinski 29.06.2024 18:28

Как я писал выше: «Я хочу, чтобы это работало для всех возможных диапазонов (или срезов), учитывая начало, остановку и шаг».

bers 29.06.2024 19:45
Ответ принят как подходящий

Основываясь на этого комментария, я думаю, вам просто нужно проверить > и < и вернуть ненулевые индексы из этого результата.

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.

bers 29.06.2024 16:07

Как будет использоваться step? Мне непонятно, откуда это берется.

jared 29.06.2024 16:20

Ну, мой вопрос касается объектов slice и range, так что, возможно, посмотрите, как это там работает. docs.python.org/3.12/library/stdtypes.html#range и docs.python.org/3.12/library/functions.html#slice

bers 29.06.2024 19:48

Я знаю, как step работает для этих объектов, но неясно, как это применимо к этой проблеме.

jared 29.06.2024 19:50

Мне нужны индексы значений в диапазоне. Если мой диапазон равен range(0, 10, 3), мне нужны индексы значений в {0, 3, 6, 9}.

bers 29.06.2024 23:06

Обновленный код теперь делает то, что вы хотите?

jared 29.06.2024 23:22

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

bers 30.06.2024 19:57

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, вместо создания нового массива для каждого промежуточного результата.

Другие вопросы по теме