Numpy: primer elemento distinto de cero (Trac #1673)

Creado en 20 oct. 2012  ·  26Comentarios  ·  Fuente: numpy/numpy

_Ticket original http://projects.scipy.org/numpy/ticket/1673 el 13-11-2010 por el usuario de trac tom3118, asignado a unknown._

El "numpy para usuarios de matlab" sugiere usar
nonzero(A)[0][0]
para encontrar el índice del primer elemento distinto de cero de la matriz A.

El problema con esto es que A puede tener un millón de elementos y el primer elemento puede ser cero.

Esta es una operación extremadamente común. Un método incorporado eficiente para esto sería muy útil. También facilitaría la transición de las personas desde Matlab en el que find es tan común.

01 - Enhancement Other

Comentario más útil

Sé que esto tiene 3 años de retraso, pero ¿está incluido en numpy ahora? Viniendo de un fondo de Matlab, estas funciones me parecen muy importantes. Un PR sería muy apreciado (no es que yo sea uno de los desarrolladores).

Todos 26 comentarios

_trac usuario tom3118 escribió el 2010-11-13_

Un caso de uso relacionado es:
filter(test,A)[0]
En el que A es largo o test es caro.

__@rgommers escribió el 2011-03-24_

No tiene que ser solo primero distinto de cero, primero cualquier valor sería útil.

__@rgommers escribió el 2011-03-24_

Como se señaló en el n.º 2333, el significado es inequívoco para 1-D. Para >1-D, la semántica está sujeta a discusión.

Tal vez funcione una palabra clave que determine el orden de iteración sobre los ejes. O simplemente puede estar indefinido para >1-D.

_trac usuario lcampagn escribió el 2011-07-09_

He visto muchas solicitudes de find_first en numpy, pero la mayoría de estas solicitudes tienen requisitos sutilmente diferentes (e incompatibles) como "buscar el primer valor menor que x" o "encontrar el primer valor distinto de cero". Sugiero la siguiente especificación de función:

  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

Si la matriz tiene ndim > 1, las pruebas se realizan utilizando las reglas de transmisión normales.
Entonces, por ejemplo, si tengo una matriz con forma (2,3), lo siguiente sería 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 necesitaba esta funcionalidad el otro día, analicé bien esto y estaba convencido de que se necesitaba una solución C para obtener un resultado adecuadamente rápido, sin embargo, un enfoque de fragmentación escrito en python ha demostrado ser adecuadamente rápido y mucho más flexible para arrancar, para mi 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

Pondré esto en la lista de correo de desarrolladores, pero si hay suficiente interés, me complacería convertirlo en una RP.

Salud,

Sé que esto tiene 3 años de retraso, pero ¿está incluido en numpy ahora? Viniendo de un fondo de Matlab, estas funciones me parecen muy importantes. Un PR sería muy apreciado (no es que yo sea uno de los desarrolladores).

Yo también estaría interesado en esto.

Tal vez sea obvio, pero dado que no se mencionó: np.all() y np.any() probablemente serían aún más fáciles (y sin ambigüedades para la dimensión> 1) para hacer perezoso. Actualmente...

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

(lo siento, me había perdido la referencia a # 3446)

Como he estado buscando una solución eficiente para este problema durante bastante tiempo y como parece que no hay planes concretos para admitir esta función, he intentado encontrar una solución que no sea tan completa y versátil como sugirió la API. anterior (en particular, admite por el momento solo matrices 1D), pero tiene la ventaja de estar completamente escrito en C y, por lo tanto, parece bastante eficiente.

Encuentra la fuente y los detalles aquí:

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

Agradecería cualquier comentario sobre la implementación, en particular, la cuestión del problema de rendimiento un tanto sorprendente al pasar matrices booleanas y buscar el primer valor verdadero, que se describe en esa página de PyPi.

Encontré esto en una publicación de stackexchange que busca esta función, que se ha visto más de 70 mil veces. @roebel , ¿alguna vez recibiste algún comentario sobre esto? ¿Puedes poner un PR para la función, que podría llamar más la atención?

no, nunca recibí ningún comentario, pero aparentemente algunas personas usaron el paquete sin problemas.
Por cierto, para anaconda linux y macos he creado un instalador de anaconda

https://anaconda.org/roebel/py_find_1st

Con respecto a un PR, tendré que analizar el esfuerzo que requiere adaptarlo de tal manera que pueda fusionarse fácilmente en numpy. No tendré tiempo para pelear en discusiones sobre cambios y extensiones de API.

¿La eliminación de " priority:normal " significa que esta característica importante de alguna manera recibirá menos atención?

La prioridad sigue siendo "normal", solo que sin etiqueta. El problema necesita un líder que realmente haga una RP y lo impulse a través del proceso de aprobación, incluida la documentación y, con suerte, un punto de referencia.

Probablemente sea útil señalar aquí el #8528, que nominalmente es alrededor de all_equal pero puede verse como una implementación de partes de esto. De hecho, en https://github.com/numpy/numpy/pull/8528#issuecomment -365358119, @ahaldane sugiere explícitamente implementar un método de reducción first en todos los operadores de comparación en lugar de un nuevo gufunc all_equal .

Esto también significa que hay bastante implementación esperando ser adaptada (aunque no es un cambio trivial de un gufunc a un nuevo método de reducción, y está la pregunta de si queremos un nuevo método en todos los ufuncs, incluso aquellos para que first tiene poco sentido.

Este problema se conoce desde (al menos) 2012. ¿Alguna actualización sobre una forma de evitar que nonzero(A)[0][0] busque en todo A ?

¿Es la llamada forma Pythonic de buscar siempre todos los elementos?

@yunyoulu : Es la manera ufunc. Demos un paso atrás y veamos el proceso general de un cálculo de varios pasos en numpy, y la cantidad de pases que se necesitan:

  1. np.argwhere(x)[0] - realiza 1 paso de los datos
  2. np.argwhere(f(x))[0] - realiza 2 pasadas de los datos
  3. np.argwhere(f(g(x)))[0] - realiza 3 pasadas de los datos

Una opción sería introducir una función np.first o similar, que tendría el siguiente aspecto, donde k <= 1 varía dependiendo de dónde se encuentre el primer elemento:

  1. np.first(x)[0] - realiza 0+k pase de los datos
  2. np.first(f(x))[0] - realiza 1+k pases de los datos
  3. np.first(f(g(x)))[0] - realiza 2+k pases de los datos

La pregunta que hay que hacerse aquí es: ¿realmente vale tanto la pena este ahorro? Numpy no es fundamentalmente una plataforma informática perezosa, y hacer que el último paso de un cálculo sea perezoso no es particularmente valioso si todos los pasos anteriores no lo fueron.


Anticuado

@eric-wieser

No creo que esté redactado del todo bien. Si k = 10 por algún problema, no es 1+10=11 pasa los datos para np.first(f(x))[0]

(editado por @eric-wieser por brevedad, esta conversación ya es demasiado larga)

El caso de uso en el que más veo la necesidad de esta funcionalidad es cuando A es un tensor grande con A.shape = (n_1, n_2, ..., n_m) . En tal caso, np.first(A) requeriría mirar solo k elementos de A en lugar de n_1*n_2*...*n_m (un ahorro potencialmente significativo).

Lo que más veo es la necesidad de esta funcionalidad cuando A es un tensor grande

Presumiblemente, en este caso, ya ha realizado al menos un paso completo de los datos, por lo que, en el mejor de los casos, obtiene un código que se ejecuta el doble de rápido.

La pregunta que hay que hacerse aquí es: ¿realmente vale tanto la pena este ahorro? Numpy no es fundamentalmente una plataforma informática perezosa, y hacer que el último paso de un cálculo sea perezoso no es particularmente valioso si todos los pasos anteriores no lo fueron.

Ese es un punto de vista interesante que, si se establece, podría usarse para justificar la destrucción de casi todos los esfuerzos para mejorar el rendimiento computacional "porque también estamos computando algo más y eso todavía es lento". (Es el mismo argumento utilizado por los que niegan la acción contra el cambio climático: bueno, hasta que este otro país haga algo, hacer algo en nuestro país no ayudará a nadie). No estoy nada convencido. Si existe alguna posibilidad de acelerar alguna parte de un cálculo en 1/k, con k potencialmente muy, muy pequeño, en mi opinión vale la pena.

Además, cuando se trabaja de forma interactiva (Jupyter, etc.), muy a menudo se realizan los "pasos" de los datos en celdas separadas, por lo que también podría terminar acelerando una celda completa.

np.first(f(x))[0] - realiza 1+k pases de los datos

@eric-wieser, de hecho, cuando analicé este problema en 2017, realmente esperaba que fuera el primer paso hacia una especie de np.firstwhere(x, array_or_value_to_compare) , que de hecho es un caso específico, pero importante en mi experiencia, de f(x) .

@toobaz : Supongo que tiene f = lambda x: x == value_to_compare en ese ejemplo.

Esta es exactamente la razón por la que desconfío de seguir este camino (cc @bersbersbers). Si no tiene cuidado, terminamos con (ortografía especulativa):

  1. np.first(x) - guardar un pase frente a distinto de cero
  2. np.first_equal(x, v) - ahorra un pase frente a first(np.equal(x, v))
  3. np.first_square_equal(x*x, v) - ahorra un pase frente a first_equal(np.square(x), v)

Debería ser bastante obvio que esto no escala en absoluto, y tenemos que trazar la línea en alguna parte. Estoy ligeramente a favor de que se permita 1, pero que se permita 2 ya es una explosión del área de superficie API, y 3 me parece muy imprudente.

Un argumento a favor de np.first : si lo implementamos, numba podría convertirlo en un caso especial de modo que np.first(x*x == v) _dentro de un contexto numba_ en realidad _sí_ hace una sola pasada.

De todos modos, es bueno saber que es imposible hacer las cosas perezosas en numpy, lo que aclara el estado actual del problema.

Sin embargo, no me siento cómodo cuando los ajustes de rendimiento se consideran solo en la escalabilidad.

Hagamos una pregunta simple: ¿están escalando las computadoras personales hoy en día? La respuesta es definitivamente NO . Hace tres años, cuando compra una computadora portátil estándar, está equipada con 8 GB de memoria; y ahora todavía encontrarás 8GB en el mercado. Sin embargo, cada software usa 2 o 4 veces más memoria que antes. Al menos, las estaciones de trabajo no se escalan de la misma manera que los clústeres.

Hacer una función 10 veces más lenta sin cambiar su complejidad en absoluto es suficiente para volver loco a un científico de datos. Lo que es peor, no hay nada elegante que pueda hacer incluso si el cuello de botella se resuelve a través de perfiles.

Lo que estoy tratando de elaborar es que tener la capacidad de realizar un procesamiento diferido siempre es deseable y puede ser crucial para la capacidad de respuesta del sistema, así como para la productividad de las personas que usan el lenguaje. La dificultad o la carga de trabajo en el desarrollo de la biblioteca constituye una muy buena excusa para no implementar estas características y ciertamente es comprensible, pero no digas que no son útiles.

@toobaz : Supongo que tiene f = lambda x: x == value_to_compare en ese ejemplo.

Correcto

Esta es exactamente la razón por la que desconfío de seguir este camino (cc @bersbersbers). Si no tiene cuidado, terminamos con (ortografía 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)`

Entiendo su preocupación, pero nunca pediría np.first_square_equal precisamente como nunca pediría (y espero que nadie haya pedido) np.square_where . Y sí, veo que significa hacer un pase completo de los datos si hace 3. Pero v se crea una vez, y es posible que deba buscar muchos valores diferentes de x sobre él . Por ejemplo (volviendo por simplicidad al ejemplo 2), quiero verificar si todas mis 30 categorías posibles aparecen en mi matriz de 10 ^ 9 elementos, y sospecho firmemente que todos aparecen entre los primeros 10 ^ 3 elementos.

Entonces, primero permítanme aclarar mi comentario anterior: me gustaría np.firstwhere(x, array_or_value_to_compare) como una función que cumple con mi intuición, pero los problemas computacionales que tuve en 2017 se habrían resuelto incluso con np.first .

En segundo lugar, el punto es, creo, no solo el tiempo de ejecución de la llamada única. Es cierto que necesito hacer un pase completo de los datos de todos modos para hacer 2 y 3... pero tal vez ya hice este pase cuando inicialicé los datos, y ahora realmente estoy buscando una forma de acelerar una operación frecuente.

Veo su punto de que np.first realmente se desvía del enfoque numpy estándar, veo que podría no ser trivial implementarlo bien... lo que no veo es cómo "infectaría" el resto de la API, o desarrollar una API grande propia.

Dicho esto, si en cambio cree que realmente está más allá del alcance numpy, tal vez haya alcance de un pequeño paquete independiente.

Hola Pablo,

Hice un pequeño punto de referencia comparando su solución con np.flatnonzero y mi extensión py_find_1st.

Usted encuentra el punto de referencia adjunto.

Aquí los resultados

(base) m3088.roebel: (prueba) (g:maestro)514> ./benchmark.py
utf1st.find_1st(rr, límite, utf1st.cmp_equal)::
tiempo de ejecución 0.131s
np.flatnonzero(rr==límite)[0]::
tiempo de ejecución 2.121s
next((ii for ii, vv in enumerate(rr) if vv == limit))::
tiempo de ejecución 1.612s

entonces, si bien su solución propuesta es un 25% más rápida que flatnonzero, ya que no requiere
al crear la matriz de resultados, sigue siendo ~12 veces más lento que py_find_1st.find_1st.

Mejor
axel

EDITAR:
Parece que el mensaje al que respondí por correo ha desaparecido, y el punto de referencia adjunto a mi correo también. El punto de referencia está aquí

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

perdón por el ruido.

El 15/05/2020 17:33, PK escribió:

¿Qué tal |siguiente(i para i, v en enumerar(x) si v)|?


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub https://github.com/numpy/numpy/issues/2269#issuecomment-629314457 o cancele la suscripción
https://github.com/notifications/unsubscribe-auth/ACAL2LS2YZALARHBHNABVILRRVOEPANCNFSM4ABV5HGA .

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

ghost picture ghost  ·  4Comentarios

Kreol64 picture Kreol64  ·  3Comentarios

kevinzhai80 picture kevinzhai80  ·  4Comentarios

astrofrog picture astrofrog  ·  4Comentarios

dmvianna picture dmvianna  ·  4Comentarios