Numpy: primeiro elemento diferente de zero (Trac #1673)

Criado em 20 out. 2012  ·  26Comentários  ·  Fonte: numpy/numpy

_Ticket original http://projects.scipy.org/numpy/ticket/1673 em 13/11/2010 pelo usuário trac tom3118, atribuído a desconhecido._

O "numpy para usuários do matlab" sugere o uso
nonzero(A)[0][0]
para encontrar o índice do primeiro elemento diferente de zero da matriz A.

O problema com isso é que A pode ter um milhão de elementos e o primeiro elemento pode ser zero.

Esta é uma operação extremamente comum. Um método eficiente e integrado para isso seria muito útil. Também facilitaria a transição das pessoas do Matlab em que find é tão comum.

01 - Enhancement Other

Comentários muito úteis

Eu sei que isso está atrasado 3 anos, mas isso está incluído no numpy agora? Vindo de um background em Matlab, essas funções parecem muito importantes para mim. Um PR seria muito apreciado (não que eu seja um dos desenvolvedores).

Todos 26 comentários

_trac usuário tom3118 escreveu em 2010-11-13_

Um caso de uso relacionado é:
filter(test,A)[0]
Em que A é longo ou test é caro.

_@rgommers escreveu em 24-03-2011_

Não precisa ser apenas o primeiro diferente de zero, primeiro qualquer valor seria útil.

_@rgommers escreveu em 24-03-2011_

Conforme observado em #2333, o significado é inequívoco para 1-D. Para >1-D a semântica está em discussão.

Talvez uma palavra-chave que determina a ordem de iteração sobre os eixos funcione. Ou pode simplesmente ser indefinido para >1-D.

_trac usuário lcampagn escreveu em 2011-07-09_

Eu vi muitas solicitações para um find_first em numpy, mas a maioria dessas solicitações tem requisitos sutilmente diferentes (e incompatíveis), como "encontrar o primeiro valor menor que x" ou "encontrar o primeiro valor diferente de zero". Sugiro a seguinte especificação de função:

  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

Se a matriz tiver ndim > 1, os testes serão executados usando regras de transmissão normais.
Então, por exemplo, se eu tiver um array com shape (2,3), o seguinte seria válido:

  ## 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

Como eu precisava dessa funcionalidade outro dia, dei uma boa olhada nisso e estava convencido de que uma solução em C era necessária para obter um resultado adequadamente rápido, no entanto, uma abordagem de agrupamento escrita em python provou ser adequadamente rápida e muito mais flexível para inicializar, para o meu caso.

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

Vou colocar isso na lista de dev-mailing, mas se houver interesse suficiente eu ficaria feliz o suficiente em transformá-lo em um PR.

Saúde,

Eu sei que isso está atrasado 3 anos, mas isso está incluído no numpy agora? Vindo de um background em Matlab, essas funções parecem muito importantes para mim. Um PR seria muito apreciado (não que eu seja um dos desenvolvedores).

Eu também estaria interessado nisso.

Talvez seja óbvio, mas como não foi mencionado: np.all() e np.any() provavelmente seriam ainda mais fáceis (e inequívocos para dimensão > 1) tornar-se preguiçoso. Atualmente...

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

(desculpe, eu tinha perdido a referência para #3446)

Como estou procurando uma solução eficiente para este problema há algum tempo e como não parece haver planos concretos de suporte a esse recurso, tentei encontrar uma solução que não seja tão completa e versátil como a API sugeriu acima (notavelmente suportando no momento apenas arrays 1D), mas isso tem a vantagem de ser completamente escrito em C e, portanto, parece bastante eficiente.

Você encontra a fonte e os detalhes aqui:

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

Eu ficaria grato por quaisquer comentários sobre a implementação, notadamente, a questão do problema de desempenho um tanto surpreendente ao passar matrizes booleanas e pesquisar o primeiro valor verdadeiro, descrito nessa página PyPi.

Encontrei isso em uma postagem do stackexchange que está procurando por esse recurso, que foi visto mais de 70 mil vezes. @roebel você já recebeu algum feedback sobre isso? Você pode simplesmente colocar um PR para o recurso, que pode receber mais atenção?

não, nunca recebi nenhum feedback, mas algumas pessoas aparentemente usaram o pacote sem problemas.
BTW, para o anaconda linux e macos eu fiz um instalador do anaconda

https://anaconda.org/roebel/py_find_1st

Com relação a um PR, terei que analisar o esforço necessário para adaptar isso de modo que possa ser mesclado facilmente em numpy. Não terei tempo para discutir discussões sobre mudanças e extensões da API.

A remoção de " priority:normal " significa que esse recurso importante receberá menos atenção?

A prioridade ainda é "normal", apenas sem rótulo. A questão precisa de um campeão para realmente fazer um PR e empurrá-lo através do processo de aprovação, incluindo documentação e, esperançosamente, um benchmark.

Provavelmente útil aqui para apontar para #8528, que é nominalmente cerca de all_equal mas pode ser visto como parte da implementação disso. De fato, em https://github.com/numpy/numpy/pull/8528#issuecomment -365358119, @ahaldane sugere explicitamente a implementação de um método de redução first em todos os operadores de comparação em vez de um novo gufunc all_equal .

Isso também significa que há uma implementação esperando para ser adaptada (embora não seja uma mudança trivial de um gufunc para um novo método de redução, e há a questão se queremos um novo método em todos os ufuncs, mesmo aqueles para que first faz pouco sentido.

Esse problema é conhecido desde (pelo menos) 2012. Alguma atualização sobre uma maneira de impedir que nonzero(A)[0][0] pesquise em todos os A ?

É a chamada maneira Pythonic de sempre procurar todos os elementos?

@yunyoulu : É o jeito ufunc. Vamos dar um passo para trás e ver o processo geral de uma computação de várias etapas em numpy e o número de passagens necessárias:

  1. np.argwhere(x)[0] - executa 1 passagem dos dados
  2. np.argwhere(f(x))[0] - executa 2 passagens dos dados
  3. np.argwhere(f(g(x)))[0] - executa 3 passagens dos dados

Uma opção seria introduzir uma função np.first ou similar - que se pareceria com o seguinte, onde k <= 1 varia dependendo de onde está o primeiro elemento:

  1. np.first(x)[0] - executa 0+k passagem dos dados
  2. np.first(f(x))[0] - executa 1+k passagens dos dados
  3. np.first(f(g(x)))[0] - executa 2+k passagens dos dados

A pergunta a ser feita aqui é - essa economia realmente vale tanto assim? O Numpy fundamentalmente não é uma plataforma de computação preguiçosa, e tornar a última etapa de uma computação preguiçosa não é particularmente valiosa se todas as etapas anteriores não forem.


Desatualizado

@eric-wieser

Acho que não está bem formulado. Se k = 10 por algum problema, não é 1+10=11 passagens dos dados para np.first(f(x))[0]

(editado por @eric-wieser para brevidade, esta conversa já é muito longa)

O caso de uso em que mais vejo a necessidade dessa funcionalidade é quando A é um tensor grande com A.shape = (n_1, n_2, ..., n_m) . Nesse caso, np.first(A) exigiria olhar apenas k elementos de A em vez de n_1*n_2*...*n_m (uma economia potencialmente significativa).

Eu mais vejo a necessidade dessa funcionalidade quando A é um tensor grande

Presumivelmente, neste caso, você já fez pelo menos uma passagem completa dos dados - então, na melhor das hipóteses, você está obtendo um código que é executado duas vezes mais rápido.

A pergunta a ser feita aqui é - essa economia realmente vale tanto assim? O Numpy fundamentalmente não é uma plataforma de computação preguiçosa, e tornar a última etapa de uma computação preguiçosa não é particularmente valiosa se todas as etapas anteriores não forem.

Esse é um ponto de vista interessante que, se estabelecido, poderia ser usado para justificar o descarte de quase todos os esforços para melhorar o desempenho computacional "porque estamos computando outra coisa também e isso ainda é lento". (É o mesmo argumento usado pelos negadores da ação contra as mudanças climáticas - bem, até que esse outro país faça algo, fazer algo em nosso país não ajudará ninguém.) Não estou nem um pouco convencido. Se houver alguma chance de acelerar alguma parte de um cálculo em 1/k, com k potencialmente muito, muito pequeno, vale a pena na minha opinião.

Além disso, ao trabalhar interativamente (Jupyter etc), muitas vezes você faz os "passes" dos dados em células separadas, então você pode acabar acelerando uma célula inteira também.

np.first(f(x))[0] - executa 1+k passagens dos dados

@eric-wieser, de fato, quando olhei para esse problema em 2017, eu realmente esperava que fosse o primeiro passo para uma espécie de np.firstwhere(x, array_or_value_to_compare) , que é de fato um caso específico - mas importante na minha experiência - de f(x) .

@toobaz : suponho que você tenha f = lambda x: x == value_to_compare nesse exemplo.

Esta é exatamente a razão pela qual estou cauteloso em seguir esse caminho (cc @bersbersbers). Se você não tomar cuidado, acabamos com (ortografia especulativa):

  1. np.first(x) - salvar um passe vs diferente de zero
  2. np.first_equal(x, v) - salvar um passe vs first(np.equal(x, v))
  3. np.first_square_equal(x*x, v) - salvar um passe vs first_equal(np.square(x), v)

Deve ser bastante óbvio que isso não é dimensionado, e temos que traçar a linha em algum lugar. Eu sou um pouco a favor de 1 ser permitido, mas 2 sendo permitido já é uma explosão de área de superfície da API, e 3 parece muito imprudente para mim.

Um argumento a favor de np.first - se o implementarmos, numba poderia especializá-lo de tal forma que np.first(x*x == v) _dentro de um contexto numba_ realmente _faz_ uma única passagem.

De qualquer forma, é bom saber que é impossível fazer as coisas preguiçosas no numpy, o que esclarece o status atual do problema.

No entanto, não me sinto confortável quando os ajustes de desempenho são considerados apenas na escalabilidade.

Vamos fazer uma pergunta simples: os computadores pessoais estão escalando hoje? A resposta é definitivamente NÃO . Três anos atrás, quando você compra um laptop padrão, eles são equipados com 8 GB de memória; e agora você ainda encontrará 8GB no mercado. No entanto, cada software está usando 2x ou 4x mais memória do que costumava. Pelo menos as estações de trabalho não estão sendo dimensionadas da mesma forma que os clusters.

Tornar uma função 10x mais lenta sem alterar sua complexidade é suficiente para enlouquecer um cientista de dados. O que é pior, não há nada elegante que ele possa fazer, mesmo que o gargalo seja descoberto por meio de perfis.

O que estou tentando elaborar é que ter a capacidade de fazer processamento lento é sempre desejável e pode ser crucial para a capacidade de resposta do sistema, bem como para a produtividade das pessoas que usam a linguagem. Dificuldade ou carga de trabalho no desenvolvimento de bibliotecas constitui uma boa desculpa para não implementar esses recursos e certamente é compreensível, mas não diga que não são úteis.

@toobaz : suponho que você tenha f = lambda x: x == value_to_compare nesse exemplo.

Correto

Esta é exatamente a razão pela qual estou cauteloso em seguir esse caminho (cc @bersbersbers). Se você não tomar cuidado, acabamos com (ortografia especulativa):

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)`

Entendo sua preocupação, mas eu nunca pediria np.first_square_equal exatamente como nunca pediria (e ninguém, espero, pediu) np.square_where . E sim, eu vejo que significa fazer uma passagem completa dos dados se você fizer 3. Mas v é criado uma vez, e talvez eu precise procurar muitos valores diferentes de x sobre ele . Por exemplo (voltando para simplificar o exemplo 2.), quero verificar se todas as minhas 30 categorias possíveis aparecem no meu array de 10^9 itens - e suspeito fortemente que todos apareçam entre os primeiros 10^3 elementos.

Então, primeiro deixe-me esclarecer meu comentário anterior: gostaria de np.firstwhere(x, array_or_value_to_compare) como uma função que atendesse à minha intuição, mas os problemas computacionais que tive em 2017 teriam sido resolvidos mesmo com np.first .

Em segundo lugar, o ponto é - eu acho - não apenas do tempo de execução da única chamada. É verdade que eu preciso fazer uma passagem completa dos dados de qualquer maneira para fazer 2. e 3... mas talvez eu já tenha feito essa passagem quando inicializei os dados, e agora estou realmente procurando uma maneira de acelerar uma operação frequente.

Eu vejo seu ponto de que np.first realmente se desvia da abordagem numpy padrão, vejo que poderia ser não trivial implementar bem ... o que não vejo é como isso "infectará" o resto da API, ou desenvolver uma grande API própria.

Dito isto, se você acha que realmente está além do escopo numpy, talvez haja escopo de um pequeno pacote independente.

Olá paul,

Fiz um pequeno benchmark comparando sua solução com np.flatnonzero e minha extensão py_find_1st.

Você encontra o benchmark anexado.

Aqui os resultados

(base) m3088.roebel: (teste) (g:master)514> ./benchmark.py
utf1st.find_1st(rr, limite, utf1st.cmp_equal)::
tempo de execução 0,131s
np.flatnonzero(rr==limit)[0]::
tempo de execução 2.121s
next((ii for ii, vv in enumerate(rr) if vv == limit))::
tempo de execução 1.612s

portanto, embora sua solução proposta seja 25% mais rápida que flatnonzero, pois não requer
criando o array de resultados, ele ainda é ~12 mais lento que py_find_1st.find_1st.

Melhor
Axel

EDITAR:
Parece que a mensagem que respondi por e-mail desapareceu e o benchmark anexado ao meu e-mail também. A referência está aqui

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

desculpe o barulho.

Em 15/05/2020 17:33, PK escreveu:

Que tal |next(i for i, v in enumerate(x) if v)|?


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub https://github.com/numpy/numpy/issues/2269#issuecomment-629314457 ou cancele a inscrição
https://github.com/notifications/unsubscribe-auth/ACAL2LS2YZALARHBHNABVILRRVOEPANCNFSM4ABV5HGA .

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

dimpase picture dimpase  ·  53Comentários

rkern picture rkern  ·  166Comentários

shoyer picture shoyer  ·  54Comentários

numpy-gitbot picture numpy-gitbot  ·  49Comentários

valentinstn picture valentinstn  ·  61Comentários