Numpy: premier élément non nul (Trac #1673)

Créé le 20 oct. 2012  ·  26Commentaires  ·  Source: numpy/numpy

_Ticket original http://projects.scipy.org/numpy/ticket/1673 le 13/11/2010 par l'utilisateur du trac tom3118, attribué à inconnu._

Le "numpy pour les utilisateurs de matlab" suggère d'utiliser
nonzero(A)[0][0]
pour trouver l'indice du premier élément non nul du tableau A.

Le problème avec ceci est que A peut être long d'un million d'éléments et que le premier élément peut être nul.

C'est une opération extrêmement courante. Une méthode efficace et intégrée pour cela serait très utile. Cela faciliterait également la transition des gens depuis Matlab dans lequel find est si courant.

01 - Enhancement Other

Commentaire le plus utile

Je sais que c'est 3 ans de retard, mais est-ce inclus dans numpy maintenant ? Venant d'un arrière-plan Matlab, cette fonction me semble vraiment importante. Un PR serait très apprécié (pas que je sois l'un des développeurs).

Tous les 26 commentaires

_utilisateur trac tom3118 a écrit le 13/11/2010_

Un cas d'utilisation connexe est :
filter(test,A)[0]
Dans lequel A est long ou test est cher.

_@rgommers a écrit le 2011-03-24_

Ne doit pas nécessairement être le premier non nul, d'abord n'importe quelle valeur serait utile.

_@rgommers a écrit le 2011-03-24_

Comme indiqué dans #2333, la signification est sans ambiguïté pour 1-D. Pour >1-D, la sémantique est sujette à discussion.

Peut-être qu'un mot clé qui détermine l'ordre d'itération sur les axes fonctionne. Ou il peut simplement être indéfini pour >1-D.

_trac utilisateur lcampagn a écrit le 2011-07-09_

J'ai vu de nombreuses demandes pour un find_first dans numpy, mais la plupart de ces demandes ont des exigences légèrement différentes (et incompatibles) telles que "trouver la première valeur inférieure à x" ou "trouver la première valeur non nulle". Je suggère la spécification de fonction suivante :

  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 le tableau a ndim > 1, alors les tests sont effectués en utilisant les règles de diffusion normales.
Ainsi, par exemple, si j'ai un tableau avec la forme (2,3), ce qui suit serait valide :

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

Comme j'avais besoin de cette fonctionnalité l'autre jour, j'ai bien regardé cela et j'étais convaincu qu'une solution C était nécessaire pour obtenir un résultat suffisamment rapide, mais une approche de segmentation écrite en python s'est avérée suffisamment rapide, et beaucoup plus flexible pour démarrer, pour mon cas.

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

Je vais le mettre sur la liste de diffusion des développeurs, mais s'il y a suffisamment d'intérêt, je serais assez heureux de le transformer en RP.

Acclamations,

Je sais que c'est 3 ans de retard, mais est-ce inclus dans numpy maintenant ? Venant d'un arrière-plan Matlab, cette fonction me semble vraiment importante. Un PR serait très apprécié (pas que je sois l'un des développeurs).

Cela m'intéresserait aussi.

C'est peut-être évident, mais comme cela n'a pas été mentionné : np.all() et np.any() seraient probablement encore plus faciles (et sans ambiguïté pour la dimension > 1) à rendre paresseux. Actuellement...

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

(désolé, j'avais raté la réf à #3446 )

Comme je cherchais une solution efficace à ce problème depuis un certain temps et qu'il ne semble pas y avoir de plans concrets pour prendre en charge cette fonctionnalité, j'ai essayé de trouver une solution qui n'est pas tout à fait complète et polyvalente comme le suggère l'API. ci-dessus (notamment en ne supportant pour l'instant que les tableaux 1D), mais qui a l'avantage d'être entièrement écrit en C et donc semble plutôt efficace.

Vous trouvez la source et les détails ici :

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

Je serais reconnaissant pour tout commentaire sur l'implémentation, notamment la question du problème de performances quelque peu étonnant lors du passage dans des tableaux booléens et de la recherche de la première valeur vraie, décrite sur cette page PyPi.

J'ai trouvé ceci à partir d'un post stackexchange qui recherche cette fonctionnalité, qui a été vue plus de 70 000 fois. @roebel avez-vous déjà reçu des commentaires à ce sujet ? Pouvez-vous simplement faire un PR pour la fonctionnalité, qui pourrait attirer plus d'attention ?

non, je n'ai jamais eu de retour, mais quelques personnes ont apparemment utilisé le package sans problème.
BTW, pour anaconda linux et macos, j'ai créé un programme d'installation anaconda

https://anaconda.org/roebel/py_find_1st

En ce qui concerne un PR, je devrai examiner l'effort que cela me demande d'adapter de manière à ce qu'il puisse être fusionné facilement dans numpy. Je n'aurai pas le temps de me battre à travers des discussions sur les modifications et les extensions de l'API.

La suppression de « priority:normal » signifie-t-elle que cette fonctionnalité importante recevra moins d'attention ?

La priorité est toujours "normale", juste sans étiquette. Le problème a besoin d'un champion pour faire un PR et le faire passer par le processus d'approbation, y compris la documentation et, espérons-le, une référence.

Probablement utile ici pour pointer vers # 8528, qui est nominalement d'environ all_equal mais peut être considéré comme implémentant des parties de cela. En effet, dans https://github.com/numpy/numpy/pull/8528#issuecomment -365358119, @ahaldane suggère explicitement d'implémenter une méthode de réduction first sur tous les opérateurs de comparaison au lieu d'un nouveau gufunc all_equal .

Cela signifie également qu'il y a pas mal d'implémentation qui attend d'être adaptée (bien que ce ne soit pas un changement trivial d'un gufunc à une nouvelle méthode de réduction, et il y a la question de savoir si nous voulons une nouvelle méthode sur tous les ufuncs, même ceux pour ce qui first n'a pas beaucoup de sens.

Ce problème est connu depuis (au moins) 2012. Toute mise à jour sur un moyen d'empêcher nonzero(A)[0][0] de rechercher tout A ?

Est-ce la méthode dite Pythonic de toujours rechercher tous les éléments ?

@yunyoulu : C'est la méthode ufunc. Prenons un peu de recul et regardons le processus général d'un calcul en plusieurs étapes dans numpy, et le nombre de passes nécessaires :

  1. np.argwhere(x)[0] - effectue 1 passage des données
  2. np.argwhere(f(x))[0] - effectue 2 passages des données
  3. np.argwhere(f(g(x)))[0] - effectue 3 passages des données

Une option serait d'introduire une fonction np.first ou similaire - qui ressemblerait alors à ce qui suit, où k <= 1 varie en fonction de l'emplacement du premier élément :

  1. np.first(x)[0] - effectue 0+k passes des données
  2. np.first(f(x))[0] - effectue 1+k passages des données
  3. np.first(f(g(x)))[0] - effectue 2+k passages des données

La question à se poser ici est la suivante : cette économie vaut-elle vraiment tant que ça ? Numpy n'est fondamentalement pas une plate-forme informatique paresseuse, et rendre la dernière étape d'un calcul paresseuse n'est pas particulièrement utile si toutes les étapes précédentes ne l'étaient pas.


Dépassé

@eric-wieser

Je ne pense pas que ce soit bien formulé. Si k = 10 pour un problème, ce n'est pas 1+10=11 passe des données pour np.first(f(x))[0]

(édité par @eric-wieser pour plus de brièveté, cette conversation est déjà trop longue)

Le cas d'utilisation où je vois le plus le besoin de cette fonctionnalité est lorsque A est un grand tenseur avec A.shape = (n_1, n_2, ..., n_m) . Dans un tel cas, np.first(A) nécessiterait de ne regarder que k éléments de A au lieu de n_1*n_2*...*n_m (une économie potentiellement importante).

Je vois le plus le besoin de cette fonctionnalité lorsque A est un grand tenseur

Dans ce cas, vous avez probablement déjà effectué au moins une passe complète des données - vous obtenez donc au mieux un code qui s'exécute deux fois plus vite.

La question à se poser ici est la suivante : cette économie vaut-elle vraiment tant que ça ? Numpy n'est fondamentalement pas une plate-forme informatique paresseuse, et rendre la dernière étape d'un calcul paresseuse n'est pas particulièrement utile si toutes les étapes précédentes ne l'étaient pas.

C'est un point de vue intéressant qui, s'il est établi, pourrait être utilisé pour justifier l'abandon de presque tous les efforts visant à améliorer les performances de calcul "parce que nous calculons également autre chose et que c'est encore lent". (C'est le même argument utilisé par les négationnistes de l'action contre le changement climatique - eh bien, jusqu'à ce que cet autre pays fasse quelque chose, faire quelque chose dans notre pays n'aidera personne.) Je ne suis pas convaincu du tout. S'il y a une chance d'accélérer une partie d'un calcul de 1/k, avec k potentiellement très très petit, cela en vaut la peine à mon avis.

De plus, lorsque vous travaillez de manière interactive (Jupyter, etc.), très souvent, vous effectuez les "passes" des données dans des cellules séparées, de sorte que vous pourriez également accélérer une cellule entière.

np.first(f(x))[0] - effectue 1+k passages des données

@eric-wieser en effet quand j'ai regardé ce numéro en 2017 j'espérais vraiment que ce serait le premier pas vers une sorte de np.firstwhere(x, array_or_value_to_compare) , qui est en effet un cas spécifique - mais important dans mon expérience - de f(x) .

@toobaz : Je suppose que vous avez f = lambda x: x == value_to_compare dans cet exemple.

C'est exactement la raison pour laquelle je me méfie du tout de m'engager dans cette voie (cc @bersbersbers). Si vous ne faites pas attention, nous nous retrouvons avec (orthographe spéculative):

  1. np.first(x) - enregistrez une passe contre une valeur non nulle
  2. np.first_equal(x, v) - enregistrer un laissez-passer contre first(np.equal(x, v))
  3. np.first_square_equal(x*x, v) - enregistrer un laissez-passer contre first_equal(np.square(x), v)

Il devrait être assez évident que cela ne s'adapte pas du tout, et nous devons tracer la ligne quelque part. Je suis légèrement favorable à ce que 1 soit autorisé, mais 2 étant autorisé, c'est déjà une explosion de la surface API, et 3 me semble très imprudent.

Un argument en faveur de np.first - si nous l'implémentons, numba pourrait le cas particulier tel que np.first(x*x == v) _dans un contexte numba_ en fait _fait_ une seule passe.

Quoi qu'il en soit, il est bon de savoir qu'il est impossible de faire les choses paresseuses dans numpy, ce qui clarifie l'état actuel du problème.

Cependant, je ne me sens pas à l'aise lorsque les ajustements de performances ne sont pris en compte que dans l'évolutivité.

Posons-nous une question simple : les ordinateurs personnels évoluent-ils aujourd'hui ? La réponse est définitivement NON . Il y a trois ans, lorsque vous achetiez un ordinateur portable standard, il était équipé de 8 Go de mémoire ; et maintenant vous trouverez toujours 8 Go sur le marché. Cependant, chaque logiciel utilise 2 ou 4 fois plus de mémoire qu'auparavant. Au moins, les postes de travail n'évoluent pas de la même manière que les clusters.

Rendre une fonction 10 fois plus lente sans changer du tout sa complexité suffit à rendre fou un data scientist. Pire encore, il ne peut rien faire d'élégant même si le goulot d'étranglement est découvert grâce au profilage.

Ce que j'essaie d'élaborer, c'est que la capacité d'effectuer un traitement paresseux est toujours souhaitable et peut être cruciale pour la réactivité du système ainsi que pour la productivité des personnes utilisant le langage. La difficulté ou la charge de travail dans le développement de la bibliothèque constitue une très bonne excuse pour ne pas implémenter ces fonctionnalités et est certainement compréhensible, mais ne dites pas qu'elles ne sont pas utiles.

@toobaz : Je suppose que vous avez f = lambda x: x == value_to_compare dans cet exemple.

Correct

C'est exactement la raison pour laquelle je me méfie du tout de m'engager dans cette voie (cc @bersbersbers). Si vous ne faites pas attention, nous nous retrouvons avec (orthographe spéculative):

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

Je comprends votre inquiétude, mais je ne demanderais jamais np.first_square_equal exactement comme je ne demanderais jamais (et personne, je l'espère, n'a demandé) np.square_where . Et oui, je vois que cela signifie faire une passe complète des données si vous faites 3. Mais v est créé une fois, et je pourrais avoir besoin de chercher de nombreuses valeurs différentes de x dessus . Par exemple (en revenant par simplicité à l'exemple 2.), je veux vérifier si toutes mes 30 catégories possibles apparaissent dans mon tableau 10^9 éléments - et je soupçonne fortement qu'elles apparaissent toutes parmi les 10^3 premiers éléments.

Alors permettez-moi d'abord de clarifier mon commentaire précédent : j'aimerais que np.firstwhere(x, array_or_value_to_compare) soit une fonction qui corresponde à mon intuition, mais les problèmes de calcul que j'avais en 2017 auraient été résolus même avec np.first .

Deuxièmement, le point n'est - je pense - pas seulement du temps d'exécution de l'appel unique. Il est vrai que j'ai besoin de faire une passe complète des données de toute façon pour faire 2. et 3... mais peut-être que j'ai déjà fait cette passe quand j'ai initialisé les données, et maintenant je cherche vraiment un moyen d'accélérer une opération fréquente.

Je vois votre argument selon lequel np.first s'écarte vraiment de l'approche numpy standard, je vois que cela pourrait être non trivial à bien implémenter ... ce que je ne vois pas, c'est comment cela "infecterait" le reste de l'API, ou développez votre propre grande API.

Cela dit, si vous pensez que cela dépasse vraiment la portée de numpy, il y a peut-être une portée à partir d'un petit paquet indépendant à la place.

Salut Paul,

J'ai fait un petit benchmark comparant votre solution à np.flatnonzero et mon extension py_find_1st.

Vous trouverez le repère en pièce jointe.

Ici les résultats

(base) m3088.roebel : (test) (g:master)514> ./benchmark.py
utf1st.find_1st(rr, limite, utf1st.cmp_equal): :
temps d'exécution 0.131s
np.flatnonzero(rr==limite)[0] ::
autonomie 2.121s
next((ii for ii, vv in enumerate(rr) if vv == limit)): :
autonomie 1.612s

donc alors que votre solution proposée est 25% plus rapide que flatnonzero car elle ne nécessite pas
en créant le tableau de résultats, il est toujours ~ 12 plus lent que py_find_1st.find_1st.

Meilleur
Axelle

ÉDITER:
Il semble que le message auquel j'ai répondu par courrier ait disparu, ainsi que le repère joint à mon courrier. La référence est ici

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

désolé pour le bruit.

Le 15/05/2020 17:33, PK a écrit :

Qu'en est-il de |next(i for i, v in enumerate(x) if v)| ?


Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub https://github.com/numpy/numpy/issues/2269#issuecomment-629314457 ou désabonnez-vous
https://github.com/notifications/unsubscribe-auth/ACAL2LS2YZALARHBHNABVILRRVOEPANCNFSM4ABV5HGA .

Cette page vous a été utile?
0 / 5 - 0 notes