Numpy: первый ненулевой элемент (Trac #1673)

Созданный на 20 окт. 2012  ·  26Комментарии  ·  Источник: numpy/numpy

_Исходный тикет http://projects.scipy.org/numpy/ticket/1673 от 13 ноября 2010 г., созданный пользователем trac tom3118, назначен неизвестным._

«numpy для пользователей Matlab» предлагает использовать
nonzero(A)[0][0]
найти индекс первого ненулевого элемента массива A.

Проблема в том, что A может состоять из миллиона элементов, а первый элемент может быть нулевым.

Это чрезвычайно распространенная операция. Эффективный встроенный метод для этого был бы очень полезен. Это также облегчило бы переход людей с Matlab, в котором так часто встречается find .

01 - Enhancement Other

Самый полезный комментарий

Я знаю, что это на 3 года позже, но теперь это включено в numpy? Исходя из фона Matlab, эти функции кажутся мне действительно важными. PR будет высоко оценен (не то, чтобы я один из разработчиков).

Все 26 Комментарий

_trac пользователь tom3118 написал 2010-11-13_

Связанный вариант использования:
filter(test,A)[0]
В котором либо A является длинным, либо test является дорогим.

_@rgommers написал 24 марта 2011 г.

Не обязательно сначала быть ненулевым, сначала любое значение будет полезно.

_@rgommers написал 24 марта 2011 г.

Как отмечено в #2333, значение однозначно для 1-D. Для> 1-D семантика подлежит обсуждению.

Возможно, сработает ключевое слово, определяющее порядок итерации по осям. Или он может просто быть неопределенным для> 1-D.

_trac пользователь lcampagn написал 2011-07-09_

Я видел много запросов на find_first в numpy, но большинство из этих запросов имеют несколько разные (и несовместимые) требования, такие как «найти первое значение меньше x» или «найти первое ненулевое значение». Я предлагаю следующую спецификацию функции:

  ind = array.find(x, testOp='eq', arrayOp='all', axis=0, test=None)
  arguments:
    x       -> value to search for
    testOp  -> condition to test for ('eq', 'ne', 'gt', 'lt', 'ge', 'le')
    arrayOp -> method for joining multiple comparisons ('any' or 'all')
    axis    -> the axis over which to search
    test    -> for convenience, this may specify a function to call to perform
               the test. This is not expected to be efficient.
  returns: 
    first index where condition is true (or test returns true, if given)
    or None if the condition was never met

Если в массиве ndim > 1, то тесты выполняются по обычным правилам вещания.
Так, например, если у меня есть массив с формой (2,3), будет допустимо следующее:

  ## find first row with all values=0
  array.find(0, testOp='eq', arrayOp='all', axis=0)
  ## equivalent to:
  for i in range(array.shape[axis]):
    if (array[i] == 0).all():
      return i

  ## find first column with any element greater than its corresponding element in col
  col = array([1,2])
  array.find(col, testOp='gt', arrayOp='any', axis=1)
  ## equivalent to:
  for i in range(array.shape[axis]):
    if (array[:,i] == col.any():
      return i

Поскольку на днях мне понадобилась эта функциональность, я хорошенько изучил ее и убедился, что для получения достаточно быстрого результата необходимо решение на C, однако метод фрагментации, написанный на python, оказался достаточно быстрым и намного более гибкий для загрузки, для моего случая.

import numpy as np
from itertools import chain, izip


def find(a, predicate, chunk_size=1024):
    """
    Find the indices of array elements that match the predicate.

    Parameters
    ----------
    a : array_like
        Input data, must be 1D.

    predicate : function
        A function which operates on sections of the given array, returning
        element-wise True or False for each data value.

    chunk_size : integer
        The length of the chunks to use when searching for matching indices.
        For high probability predicates, a smaller number will make this
        function quicker, similarly choose a larger number for low
        probabilities.

    Returns
    -------
    index_generator : generator
        A generator of (indices, data value) tuples which make the predicate
        True.

    See Also
    --------
    where, nonzero

    Notes
    -----
    This function is best used for finding the first, or first few, data values
    which match the predicate.

    Examples
    --------
    >>> a = np.sin(np.linspace(0, np.pi, 200))
    >>> result = find(a, lambda arr: arr > 0.9)
    >>> next(result)
    ((71, ), 0.900479032457)
    >>> np.where(a > 0.9)[0][0]
    71


    """
    if a.ndim != 1:
        raise ValueError('The array must be 1D, not {}.'.format(a.ndim))

    i0 = 0
    chunk_inds = chain(xrange(chunk_size, a.size, chunk_size), 
                 [None])

    for i1 in chunk_inds:
        chunk = a[i0:i1]
        for inds in izip(*predicate(chunk).nonzero()):
            yield (inds[0] + i0, ), chunk[inds]
        i0 = i1
In [1]: from np_utils import find

In [2]: import numpy as np

In [3]: import numpy.random    

In [4]: np.random.seed(1)

In [5]: a = np.random.randn(1e8)

In [6]: a.min(), a.max()
Out[6]: (-6.1194900990552776, 5.9632246301166321)

In [7]: next(find(a, lambda a: np.abs(a) > 6))
Out[7]: ((33105441,), -6.1194900990552776)

In [8]: (np.abs(a) > 6).nonzero()
Out[8]: (array([33105441]),)

In [9]: %timeit (np.abs(a) > 6).nonzero()
1 loops, best of 3: 1.51 s per loop

In [10]: %timeit next(find(a, lambda a: np.abs(a) > 6))
1 loops, best of 3: 912 ms per loop

In [11]: %timeit next(find(a, lambda a: np.abs(a) > 6, chunk_size=100000))
1 loops, best of 3: 470 ms per loop

In [12]: %timeit next(find(a, lambda a: np.abs(a) > 6, chunk_size=1000000))
1 loops, best of 3: 483 ms per loop

Я добавлю это в список рассылки разработчиков, но если будет достаточный интерес, я буду рад превратить это в PR.

Ваше здоровье,

Я знаю, что это на 3 года позже, но теперь это включено в numpy? Исходя из фона Matlab, эти функции кажутся мне действительно важными. PR будет высоко оценен (не то, чтобы я один из разработчиков).

Меня бы это тоже интересовало.

Может быть, это и очевидно, но так как это не было упомянуто: np.all() и np.any() было бы, вероятно, даже проще (и однозначно для размерности > 1) сделать ленивым. В настоящее время...

In [2]: zz = np.zeros(shape=10000000)

In [3]: zz[0] = 1

In [4]: %timeit -r 1 -n 1 any(zz)
1 loop, best of 1: 3.52 µs per loop

In [5]: %timeit -r 1 -n 1 np.any(zz)
1 loop, best of 1: 16.7 ms per loop

(извините, я пропустил ссылку на #3446)

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

Вы найдете источник и подробности здесь:

https://pypi.python.org/pypi?name=py_find_1st& :action=display

Я был бы признателен за любые комментарии по реализации, в частности, вопрос о несколько удивительной проблеме с производительностью при передаче логических массивов и поиске первого истинного значения, описанного на этой странице PyPi.

Я нашел это из сообщения stackexchange , которое искало эту функцию, которая была просмотрена более 70 тысяч раз. @roebel , вы когда-нибудь получали какие-либо отзывы по этому поводу? Можете ли вы просто сделать PR для этой функции, которая может привлечь больше внимания?

нет, я никогда не получал никаких отзывов, но несколько человек, по-видимому, использовали пакет без проблем.
Кстати, для anaconda linux и macos я сделал установщик anaconda

https://anaconda.org/roebel/py_find_1st

Что касается PR, мне придется изучить усилия, которые он требует от меня, чтобы адаптировать его так, чтобы его можно было легко объединить с numpy. У меня не будет времени участвовать в дискуссиях об изменениях и расширениях API.

Означает ли удаление « priority:normal » что этой важной функции будет как-то уделяться меньше внимания?

Приоритет по-прежнему "обычный", только без метки. Проблема нуждается в чемпионе, который фактически сделает PR и протолкнет его через процесс утверждения, включая документацию и, надеюсь, контрольный показатель.

Вероятно, здесь полезно указать на #8528, который номинально равен примерно all_equal , но может рассматриваться как реализующая его часть. Действительно, в https://github.com/numpy/numpy/pull/8528#issuecomment -365358119 @ahaldane явно предлагает реализовать метод сокращения first для всех операторов сравнения вместо нового gufunc all_equal .

Это также означает, что существует немало реализаций, ожидающих адаптации (хотя это не тривиальное изменение от gufunc к новому методу редукции, и возникает вопрос, хотим ли мы новый метод для всех ufunc, даже для что first не имеет большого смысла.

Эта проблема известна (по крайней мере) с 2012 года. Любое обновление, позволяющее предотвратить поиск nonzero(A)[0][0] по всем A ?

Это так называемый Pythonic способ всегда сканировать все элементы?

@yunyoulu : Это способ ufunc. Давайте сделаем шаг назад и посмотрим на общий процесс многоэтапного вычисления в numpy и количество проходов, которые он выполняет:

  1. np.argwhere(x)[0] - выполняет 1 проход данных
  2. np.argwhere(f(x))[0] - выполняет 2 прохода данных
  3. np.argwhere(f(g(x)))[0] - выполняет 3 прохода данных

Один из вариантов — ввести функцию np.first или аналогичную — тогда это будет выглядеть следующим образом, где k <= 1 варьируется в зависимости от того, где находится первый элемент:

  1. np.first(x)[0] - выполняет 0+k проходов данных
  2. np.first(f(x))[0] - выполняет 1+k проходов данных
  3. np.first(f(g(x)))[0] - выполняет 2+k проходов данных

Вопрос, который нужно задать здесь, - действительно ли эта экономия стоит так много? Numpy принципиально не является ленивой вычислительной платформой, и ленивый последний шаг вычислений не особенно ценен, если все предыдущие шаги не были такими.


Устаревший

@eric-wieser

Я не думаю, что это сформулировано совсем правильно. Если k = 10 для какой-то проблемы, это не 1+10=11 передачи данных для np.first(f(x))[0]

(отредактировано @eric-wieser для краткости, этот разговор и так слишком длинный)

Вариант использования, в котором я больше всего вижу потребность в этой функциональности, — это когда A — это большой тензор с A.shape = (n_1, n_2, ..., n_m) . В таком случае np.first(A) потребует просмотра только k элементов A вместо n_1*n_2*...*n_m (потенциально значительная экономия).

Я больше всего вижу необходимость в этой функциональности, когда A является большим тензором.

Предположительно, в этом случае вы уже выполнили по крайней мере один полный проход данных, поэтому в лучшем случае вы получаете код, который работает вдвое быстрее.

Вопрос, который нужно задать здесь, - действительно ли эта экономия стоит так много? Numpy принципиально не является ленивой вычислительной платформой, и ленивый последний шаг вычислений не особенно ценен, если все предыдущие шаги не были такими.

Это интересная точка зрения, которая, если она будет установлена, может быть использована для оправдания отказа почти от всех усилий по улучшению вычислительной производительности, «потому что мы также вычисляем что-то еще, и это все еще медленно». (Это тот же аргумент, который используют противники действий по изменению климата: ну, пока эта другая страна ничего не предпримет, действия в нашей стране никому не помогут.) Меня это совсем не убеждает. Если есть какой-то шанс ускорить какую-то часть вычислений на 1/k, с потенциально очень малым k, то, на мой взгляд, это того стоит.

Кроме того, при интерактивной работе (Jupyter и т. д.) очень часто вы выполняете «проходы» данных в отдельных ячейках, так что в конечном итоге вы также можете ускорить всю ячейку.

np.first(f(x))[0] - выполняет 1+k проходов данных

@eric-wieser действительно, когда я смотрел на эту проблему в 2017 году, я действительно надеялся, что это будет первый шаг к своего рода np.firstwhere(x, array_or_value_to_compare) , что действительно является конкретным случаем, но важным, по моему опыту, f(x) .

@toobaz : я предполагаю, что в этом примере у вас есть f = lambda x: x == value_to_compare .

Именно по этой причине я вообще опасаюсь идти по этому пути (cc @bersbersbers). Если вы не будете осторожны, мы получим (спекулятивное написание):

  1. np.first(x) - сохранить проход против ненулевого
  2. np.first_equal(x, v) — сохранить пропуск против first(np.equal(x, v))
  3. np.first_square_equal(x*x, v) — сохранить пропуск против first_equal(np.square(x), v)

Должно быть совершенно очевидно, что это вообще не масштабируется, и мы должны где-то провести черту. Я немного за то, чтобы было разрешено 1, но разрешено 2 — это уже взрыв площади поверхности API, а 3 кажется мне очень неразумным.

Один аргумент в пользу np.first — если мы его реализуем, numba может использовать его в особом случае, так что np.first(x*x == v) _внутри нумба-контекста_ на самом деле _делает_ один проход.

В любом случае приятно знать, что в numpy невозможно делать ленивые вещи, что проясняет текущий статус проблемы.

Однако я не чувствую себя комфортно, когда настройки производительности учитываются только в плане масштабируемости.

Давайте зададим простые вопросы: масштабируются ли сегодня персональные компьютеры? Ответ однозначно НЕТ . Три года назад при покупке стандартного ноутбука они оснащались 8 ГБ памяти; и теперь вы все еще найдете 8 ГБ на рынке. Однако каждое программное обеспечение использует в 2 или 4 раза больше памяти, чем раньше. По крайней мере, рабочие станции не масштабируются так же, как кластеры.

Сделать функцию в 10 раз медленнее без изменения ее сложности — достаточно, чтобы свести с ума одного специалиста по данным. Что еще хуже, он не может сделать ничего элегантного, даже если узкое место будет найдено с помощью профилирования.

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

@toobaz : я предполагаю, что в этом примере у вас есть f = lambda x: x == value_to_compare .

Правильный

Именно по этой причине я вообще опасаюсь идти по этому пути (cc @bersbersbers). Если вы не будете осторожны, мы получим (спекулятивное написание):

1. `np.first(x)` - save a pass vs nonzero

2. `np.first_equal(x, v)` - save a pass vs `first(np.equal(x, v))`

3. `np.first_square_equal(x*x, v)` - save a pass vs `first_equal(np.square(x), v)`

Я понимаю ваше беспокойство, но я бы никогда не попросил np.first_square_equal точно так же, как я никогда не попросил бы (и, надеюсь, никто не просил) np.square_where . И да, я вижу, что это означает полный проход данных, если вы делаете 3. Но v создается один раз, и мне, возможно, придется искать много разных значений x поверх него. . Например (возвращаясь для простоты к примеру 2.), я хочу проверить, все ли мои 30 возможных категорий появляются в моем массиве из 10 ^ 9 элементов, и я сильно подозреваю, что все они появляются среди первых 10 ^ 3 элементов.

Итак, сначала позвольте мне пояснить мой предыдущий комментарий: я хотел бы, чтобы np.firstwhere(x, array_or_value_to_compare) соответствовало моей интуиции, но проблемы с вычислениями, которые у меня были еще в 2017 году, были бы решены даже с помощью np.first .

Во-вторых, дело, я думаю, не только во времени работы одного вызова. Это правда, что мне все равно нужно сделать полный проход данных, чтобы сделать 2. и 3... но, возможно, я уже сделал этот проход, когда инициализировал данные, и теперь я действительно ищу способ ускорить частая операция.

Я понимаю вашу точку зрения, что np.first действительно отклоняется от стандартного подхода numpy, я вижу, что это может быть нетривиально для хорошей реализации... чего я не вижу, так это того, как это могло бы «заразить» остальную часть API, или вырастить собственный большой API.

При этом, если вместо этого вы думаете, что это действительно выходит за рамки numpy, возможно, вместо этого есть область действия из небольшого независимого пакета.

Привет Пол,

Я сделал небольшой тест, сравнивая ваше решение с np.flatnonzero и моим расширением py_find_1st.

Вы найдете тест прилагается.

Вот результаты

(базовый) m3088.roebel: (тест) (g:master)514> ./benchmark.py
utf1st.find_1st(rr, лимит, utf1st.cmp_equal)::
время выполнения 0,131 с
np.flatnonzero(rr==limit)[0]::
время работы 2,121 с
next((ii для ii, vv в enumerate(rr) if vv == limit))::
время работы 1,612 с

поэтому, хотя предлагаемое вами решение на 25% быстрее, чем flatnonzero, поскольку оно не требует
при создании массива результатов он по-прежнему примерно на 12 медленнее, чем py_find_1st.find_1st.

Лучший
Аксель

РЕДАКТИРОВАТЬ:
Похоже, сообщение, на которое я ответил по почте, исчезло, как и тест, прикрепленный к моему письму. Эталон здесь

https://github.com/roebel/py_find_1st/blob/master/test/benchmark.py

извините за шум.

15.05.2020 17:33, ПК написал:

Как насчет |следующего(i для i, v в enumerate(x), если v)|?


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub https://github.com/numpy/numpy/issues/2269#issuecomment-629314457 или отмените подписку.
https://github.com/notifications/unsubscribe-auth/ACAL2LS2YZALARHBHNABVILRRVOEPANCNFSM4ABV5HGA .

Была ли эта страница полезной?
0 / 5 - 0 рейтинги