Мне нужно найти индекс первого значения в массиве 1d NumPy или числовой серии Pandas, удовлетворяющем условию. Массив большой, и индекс может быть около начала или конца массива, или условие может вообще не выполняться. Я не могу сказать заранее, что более вероятно. Если условие не выполняется, возвращаемое значение должно быть -1
. Я рассмотрел несколько подходов.
# func(arr) returns a Boolean array
idx = next(iter(np.where(func(arr))[0]), -1)
Но это часто бывает слишком медленно, поскольку func(arr)
применяет векторизованную функцию к массиву весь, а не останавливается при выполнении условия. В частности, это дорого, когда условие выполняется рядом с Начните массива.
np.argmax
немного быстрее, но не может определить, когда выполняется условие никогда:
np.random.seed(0)
arr = np.random.rand(10**7)
assert next(iter(np.where(arr > 0.999999)[0]), -1) == np.argmax(arr > 0.999999)
%timeit next(iter(np.where(arr > 0.999999)[0]), -1) # 21.2 ms
%timeit np.argmax(arr > 0.999999) # 17.7 ms
np.argmax(arr > 1.0)
возвращает 0
, то есть экземпляр, когда выполняется условие нет.
# func(arr) returns a Boolean scalar
idx = next((idx for idx, val in enumerate(arr) if func(arr)), -1)
Но это слишком медленно, когда условие выполняется рядом с конец массива. Предположительно это связано с тем, что выражение генератора имеет дорогостоящие накладные расходы из-за большого количества вызовов __next__
.
Является ли этот всегда компромиссом или есть способ для общего func
эффективно извлечь первый индекс?
Для сравнительного анализа предположим, что func
находит индекс, когда значение больше заданной константы:
# Python 3.6.5, NumPy 1.14.3, Numba 0.38.0
import numpy as np
np.random.seed(0)
arr = np.random.rand(10**7)
m = 0.9
n = 0.999999
# Start of array benchmark
%timeit next(iter(np.where(arr > m)[0]), -1) # 43.5 ms
%timeit next((idx for idx, val in enumerate(arr) if val > m), -1) # 2.5 µs
# End of array benchmark
%timeit next(iter(np.where(arr > n)[0]), -1) # 21.4 ms
%timeit next((idx for idx, val in enumerate(arr) if val > n), -1) # 39.2 ms
numba
С numba
можно оптимизировать сценарии оба. Синтаксически вам нужно только построить функцию с помощью простого цикла for
:
from numba import njit
@njit
def get_first_index_nb(A, k):
for i in range(len(A)):
if A[i] > k:
return i
return -1
idx = get_first_index_nb(A, 0.9)
Numba повышает производительность за счет JIT ("Just In Time") компиляции кода и использования Оптимизация на уровне ЦП. Цикл обычныйfor
без декоратора @njit
обычно будет помедленнее, чем методы, которые вы уже пробовали, для случая, когда условие выполняется поздно.
Для числовой серии df['data']
Pandas вы можете просто передать представление NumPy в JIT-скомпилированную функцию:
idx = get_first_index_nb(df['data'].values, 0.9)
Поскольку numba
разрешает функции как аргументы, и предполагая, что переданная функция также может быть JIT-скомпилирована, вы можете получить метод для вычисления индекса пth, где выполняется условие для произвольного func
.
@njit
def get_nth_index_count(A, func, count):
c = 0
for i in range(len(A)):
if func(A[i]):
c += 1
if c == count:
return i
return -1
@njit
def func(val):
return val > 0.9
# get index of 3rd value where func evaluates to True
idx = get_nth_index_count(arr, func, 3)
Для 3-го значения последний вы можете передать обратное, arr[::-1]
, и отрицать результат len(arr) - 1
, - 1
, необходимый для учета 0-индексации.
# Python 3.6.5, NumPy 1.14.3, Numba 0.38.0
np.random.seed(0)
arr = np.random.rand(10**7)
m = 0.9
n = 0.999999
@njit
def get_first_index_nb(A, k):
for i in range(len(A)):
if A[i] > k:
return i
return -1
def get_first_index_np(A, k):
for i in range(len(A)):
if A[i] > k:
return i
return -1
%timeit get_first_index_nb(arr, m) # 375 ns
%timeit get_first_index_np(arr, m) # 2.71 µs
%timeit next(iter(np.where(arr > m)[0]), -1) # 43.5 ms
%timeit next((idx for idx, val in enumerate(arr) if val > m), -1) # 2.5 µs
%timeit get_first_index_nb(arr, n) # 204 µs
%timeit get_first_index_np(arr, n) # 44.8 ms
%timeit next(iter(np.where(arr > n)[0]), -1) # 21.4 ms
%timeit next((idx for idx, val in enumerate(arr) if val > n), -1) # 39.2 ms
Я тоже хотел сделать что-то подобное и обнаружил, что решения, представленные в этом вопросе, мне не очень помогают. В частности, решение numba
было для меня намного медленнее, чем более традиционные методы, представленные в самом вопросе. У меня есть список times_all
, обычно состоящий из десятков тысяч элементов, и я хочу найти индекс первого элемента times_all
, который больше, чем time_event
. И у меня тысячи time_event
. Мое решение состоит в том, чтобы разделить times_all
на куски, например, по 100 элементов, сначала решить, какому временному сегменту принадлежит time_event
, сохранить индекс первого элемента этого сегмента, затем найти, какой индекс в этом сегменте, и сложить два индекса. Вот минимальный код. Для меня он работает на порядки быстрее, чем другие решения на этой странице.
def event_time_2_index(time_event, times_all, STEPS=100):
import numpy as np
time_indices_jumps = np.arange(0, len(times_all), STEPS)
time_list_jumps = [times_all[idx] for idx in time_indices_jumps]
time_list_jumps_idx = next((idx for idx, val in enumerate(time_list_jumps)\
if val > time_event), -1)
index_in_jumps = time_indices_jumps[time_list_jumps_idx-1]
times_cropped = times_all[index_in_jumps:]
event_index_rel = next((idx for idx, val in enumerate(times_cropped) \
if val > time_event), -1)
event_index = event_index_rel + index_in_jumps
return event_index
Данные, над которыми я работаю, являются экспериментальными данными, и на данном этапе я не могу ими поделиться. Но у меня есть отсортированный массив временных шагов с шагом 1/320 секунды и прибл. 1e5 выборок и другой массив времен событий, обычно порядка тысяч. И мне нужен индекс этих событий, чтобы использовать его в инструменте анализа ЭЭГ. Используя этот трюк сегментации, для выборки 1e5 максимальное количество сравнений составляет 1000 + 100, но без этого сегментации может быть что угодно до 1e5-1. Я использовал генератор next
, потому что в ваших тестах он был самым быстрым, и к тому же это всего одна строчка.
Кроме того, для меня numba
работал медленнее, чем другие решения, чего я не ожидал. Хотя я должен сказать, что запускаю свой код на Spyder, который, как я знаю, плохо влияет на управление памятью, так что, возможно, это сыграло свою роль: stackoverflow.com/questions/57409470/…
I have a sorted array of time steps
- это дополнительное предположение, которое нельзя вывести из вопроса. Я понимаю, к чему вы клоните, но считаю, что ваш ответ, возможно, хороший на вопрос другой. Если бы вы написали свои собственные вопросы и ответы с дополнительными критериями, он, вероятно, получил бы лучший прием. [Хотя вы, должен, моделируете пример входных данных, как я в своих вопросах и ответах.]
Я искал в Google свою проблему, меня привели к этим вопросам и ответам, решения не помогли, мне в голову пришла идея, которая помогла мне сделать то, что я хочу делать, за часы, а не дни, и я подумал поделиться этой идеей с кем бы то ни было, может попасть в этот уголок виртуального мира. Если это поможет кому-то другому, это хорошо, но если это не получится, я не дам летающих фламинго!
Можете ли вы предоставить несколько примеров ввода, чтобы продемонстрировать, как это работает быстрее? Я удивлен (кроме особого случая, когда условие выполняется очень рано), что выражение генератора было бы эффективным. Ваша логика с выражением генератора
next
+, по сути, моя попытка №3.