Numpy: erstes Element ungleich Null (Trac #1673)

Erstellt am 20. Okt. 2012  ·  26Kommentare  ·  Quelle: numpy/numpy

_Originalticket http://projects.scipy.org/numpy/ticket/1673 vom 13.11.2010 von Trac-Benutzer tom3118, zugewiesen an unknown._

Das "Numpy für Matlab-Benutzer" schlägt die Verwendung vor
nonzero(A)[0][0]
um den Index des ersten Nicht-Null-Elements von Array A zu finden.

Das Problem dabei ist, dass A eine Million Elemente lang sein kann und das erste Element null sein kann.

Dies ist eine sehr häufige Operation. Eine effiziente, eingebaute Methode dafür wäre sehr nützlich. Es würde auch den Übergang von Matlab erleichtern, in dem find so üblich ist.

01 - Enhancement Other

Hilfreichster Kommentar

Ich weiß, das ist 3 Jahre zu spät, aber ist das jetzt in numpy enthalten? Da ich aus einem Matlab-Hintergrund komme, scheint mir diese Funktion sehr wichtig zu sein. Eine PR wäre sehr willkommen (nicht dass ich einer der Entwickler bin).

Alle 26 Kommentare

_trac-Benutzer tom3118 schrieb am 13.11.2010_

Ein verwandter Anwendungsfall ist:
filter(test,A)[0]
Dabei ist entweder A lang oder test teuer.

_@rgommers schrieb am 24.03.2011_

Muss nicht nur zuerst ungleich Null sein, zuerst wäre jeder Wert nützlich.

_@rgommers schrieb am 24.03.2011_

Wie in #2333 angemerkt, ist die Bedeutung für 1-D eindeutig. Für >1-D steht die Semantik zur Diskussion.

Vielleicht funktioniert ein Schlüsselwort, das die Reihenfolge der Iteration über Achsen bestimmt. Oder es kann für >1-D einfach undefiniert sein.

_trac-Benutzer lcampagn schrieb am 09.07.2011_

Ich habe viele Anfragen nach einem find_first in numpy gesehen, aber die meisten dieser Anfragen haben subtil unterschiedliche (und inkompatible) Anforderungen wie „ersten Wert kleiner als x finden“ oder „ersten Wert ungleich Null finden“. Ich schlage folgende Funktionsspezifikation vor:

  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

Wenn das Array ndim > 1 hat, werden Tests unter Verwendung normaler Broadcasting-Regeln durchgeführt.
Wenn ich beispielsweise ein Array mit der Form (2,3) habe, wäre Folgendes gültig:

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

Da ich diese Funktionalität neulich benötigte, habe ich mich damit eingehend befasst und war überzeugt, dass eine C-Lösung erforderlich war, um ein angemessen schnelles Ergebnis zu erzielen. Ein in Python geschriebener Chunking-Ansatz hat sich jedoch als angemessen schnell und viel erwiesen flexibler zu booten, für meinen Fall.

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

Ich werde dies auf die Entwickler-Mailingliste setzen, aber wenn es genügend Interesse gibt, würde ich es gerne in eine PR umwandeln.

Prost,

Ich weiß, das ist 3 Jahre zu spät, aber ist das jetzt in numpy enthalten? Da ich aus einem Matlab-Hintergrund komme, scheint mir diese Funktion sehr wichtig zu sein. Eine PR wäre sehr willkommen (nicht dass ich einer der Entwickler bin).

Das würde mich auch interessieren.

Vielleicht ist es offensichtlich, aber da es nicht erwähnt wurde: np.all() und np.any() wären wahrscheinlich noch einfacher (und eindeutig für Dimension > 1) faul zu machen. Zur Zeit...

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

(Entschuldigung, ich hatte den Verweis auf #3446 übersehen)

Da ich eine ganze Weile nach einer effizienten Lösung für dieses Problem gesucht habe und es anscheinend keine konkreten Pläne zur Unterstützung dieser Funktion gibt, habe ich versucht, eine Lösung zu finden, die nicht ganz vollständig und vielseitig ist, wie es die API vorgeschlagen hat oben (insbesondere unterstützt im Moment nur 1D-Arrays), aber das hat den Vorteil, dass es vollständig in C geschrieben ist und daher ziemlich effizient erscheint.

Quelle und Details finden Sie hier:

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

Ich wäre dankbar für Kommentare zur Implementierung, insbesondere zur Frage des etwas erstaunlichen Leistungsproblems bei der Übergabe von booleschen Arrays und der Suche nach dem ersten wahren Wert, das auf dieser PyPi-Seite beschrieben wird.

Ich habe dies in einem Stackexchange-Beitrag gefunden, der nach dieser Funktion sucht, die über 70.000 Mal angezeigt wurde. @roebel hast du jemals Feedback dazu bekommen? Können Sie einfach eine PR für das Feature einreichen, das möglicherweise mehr Aufmerksamkeit erregt?

nein, ich habe nie Feedback bekommen, aber ein paar Leute haben das Paket anscheinend ohne Probleme benutzt.
Übrigens, für Anaconda Linux und Macos habe ich ein Anaconda-Installationsprogramm erstellt

https://anaconda.org/roebel/py_find_1st

In Bezug auf eine PR muss ich den Aufwand prüfen, den es erfordert, diese so anzupassen, dass sie problemlos in numpy zusammengeführt werden kann. Ich werde nicht die Zeit haben, mich durch Diskussionen über API-Änderungen und -Erweiterungen zu kämpfen.

Bedeutet die Entfernung von „ priority:normal “, dass diesem wichtigen Feature irgendwie weniger Aufmerksamkeit geschenkt wird?

Die Priorität ist immer noch "normal", nur ohne Label. Das Thema braucht einen Verfechter, um tatsächlich eine PR zu machen und sie durch den Genehmigungsprozess zu bringen, einschließlich Dokumentation und hoffentlich eines Benchmarks.

Wahrscheinlich nützlich, um hier auf #8528 zu verweisen, das nominell etwa all_equal ist, aber als Implementierung von Teilen davon angesehen werden kann. Tatsächlich schlägt @ahaldane in https://github.com/numpy/numpy/pull/8528#issuecomment -365358119 ausdrücklich vor, anstelle eines neuen gufunc all_equal eine first -Reduktionsmethode für alle Vergleichsoperatoren zu implementieren all_equal .

Das bedeutet auch, dass einiges an Implementierung darauf wartet, angepasst zu werden (obwohl es keine triviale Änderung von einem gufunc zu einer neuen Reduktionsmethode ist, und es stellt sich die Frage, ob wir eine neue Methode für alle ufuncs wollen, sogar für die für was first wenig Sinn macht.

Dieses Problem ist (mindestens) seit 2012 bekannt. Gibt es ein Update, um zu verhindern, dass nonzero(A)[0][0] A durchsucht?

Ist es die sogenannte pythonische Art, immer nach allen Elementen zu suchen?

@yunyoulu : Es ist der ufunc-Weg. Lassen Sie uns einen Schritt zurücktreten und uns den allgemeinen Prozess einer mehrstufigen Berechnung in numpy und die Anzahl der erforderlichen Durchgänge ansehen:

  1. np.argwhere(x)[0] - führt 1 Durchgang der Daten durch
  2. np.argwhere(f(x))[0] - führt 2 Durchläufe der Daten durch
  3. np.argwhere(f(g(x)))[0] - führt 3 Durchgänge der Daten durch

Eine Möglichkeit wäre, eine np.first -Funktion oder ähnliches einzuführen – das würde dann so aussehen, wobei k <= 1 je nachdem, wo das erste Element steht, variiert:

  1. np.first(x)[0] - führt einen 0+k-Durchgang der Daten durch
  2. np.first(f(x))[0] - führt 1+k Durchgänge der Daten durch
  3. np.first(f(g(x)))[0] - führt 2+k Durchläufe der Daten durch

Hier stellt sich die Frage: Ist diese Einsparung wirklich so viel wert? Numpy ist im Grunde keine Lazy-Computing-Plattform, und es ist nicht besonders wertvoll, den letzten Schritt einer Berechnung faul zu machen, wenn dies nicht alle vorherigen Schritte wären.


Veraltet

@eric-wieser

Ich finde das nicht ganz richtig formuliert. Wenn k = 10 für irgendein Problem, es ist nicht 1+10=11 die Daten für np.first(f(x))[0] $ übergibt

(der Kürze halber von @eric-wieser herausgegeben, dieses Gespräch ist schon zu lang)

Der Anwendungsfall, in dem ich die Notwendigkeit für diese Funktionalität am meisten sehe, ist, wenn A ein großer Tensor mit A.shape = (n_1, n_2, ..., n_m) ist. In einem solchen Fall würde np.first(A) erfordern, nur k Elemente von A statt n_1*n_2*...*n_m zu betrachten (eine potenziell erhebliche Einsparung).

Ich sehe die Notwendigkeit für diese Funktionalität am meisten, wenn A ein großer Tensor ist

Vermutlich haben Sie in diesem Fall bereits mindestens einen vollständigen Durchlauf der Daten durchgeführt - Sie erhalten also bestenfalls Code, der doppelt so schnell ausgeführt wird.

Hier stellt sich die Frage: Ist diese Einsparung wirklich so viel wert? Numpy ist im Grunde keine Lazy-Computing-Plattform, und es ist nicht besonders wertvoll, den letzten Schritt einer Berechnung faul zu machen, wenn dies nicht alle vorherigen Schritte wären.

Das ist ein interessanter Standpunkt, der, wenn er etabliert ist, dazu verwendet werden könnte, fast alle Bemühungen zur Verbesserung der Rechenleistung zu verwerfen, "weil wir auch noch etwas anderes berechnen und das immer noch langsam ist". (Es ist das gleiche Argument, das von Leugnern von Klimaschutzmaßnahmen verwendet wird – nun, bis dieses andere Land etwas unternimmt, wird es niemandem helfen, etwas in unserem Land zu tun.) Ich bin überhaupt nicht überzeugt. Wenn es eine Möglichkeit gibt, einen Teil einer Berechnung um 1 / k zu beschleunigen, wobei k möglicherweise sehr, sehr klein ist, lohnt sich das meiner Meinung nach.

Wenn Sie interaktiv arbeiten (Jupyter usw.), führen Sie sehr oft die "Durchgänge" der Daten in separaten Zellen durch, sodass Sie am Ende auch eine ganze Zelle beschleunigen können.

np.first(f(x))[0] - führt 1+k Durchläufe der Daten durch

@eric-wieser Als ich mir dieses Problem 2017 ansah, hatte ich wirklich gehofft, dass es der erste Schritt in Richtung einer Art np.firstwhere(x, array_or_value_to_compare) sein würde, was in der Tat ein spezifischer Fall – aber meiner Erfahrung nach wichtig – von f(x) ist.

@toobaz : Ich nehme an, Sie haben in diesem Beispiel f = lambda x: x == value_to_compare .

Genau aus diesem Grund scheue ich mich davor, diesen Weg überhaupt zu gehen (cc @bersbersbers). Wenn Sie nicht aufpassen, landen wir bei (Schreibweise spekulativ):

  1. np.first(x) - Speichern Sie einen Pass gegen Nicht-Null
  2. np.first_equal(x, v) - Sparen Sie einen Pass gegen first(np.equal(x, v))
  3. np.first_square_equal(x*x, v) - sparen Sie einen Pass gegen first_equal(np.square(x), v)

Es sollte ziemlich offensichtlich sein, dass dies überhaupt nicht skaliert, und wir müssen irgendwo die Grenze ziehen. Ich bin leicht dafür, dass 1 erlaubt ist, aber 2 erlaubt zu sein, ist bereits eine Explosion der API-Oberfläche, und 3 erscheint mir sehr unklug.

Ein Argument zugunsten von np.first – wenn wir es implementieren, könnte numba es in einen Sonderfall versetzen, sodass np.first(x*x == v) _innerhalb eines Numba-Kontexts_ tatsächlich einen einzigen Durchlauf _ausführt_.

Wie auch immer, es ist gut zu wissen, dass es unmöglich ist, die faulen Dinge in numpy zu tun, was den aktuellen Status des Problems verdeutlicht.

Ich fühle mich jedoch nicht wohl, wenn Leistungsoptimierungen nur in der Skalierbarkeit berücksichtigt werden.

Lassen Sie uns eine einfache Frage stellen: Skalieren PCs heute? Die Antwort ist definitiv NEIN . Wenn Sie vor drei Jahren einen Standard-Laptop kauften, waren diese mit 8 GB Speicher ausgestattet; und jetzt finden Sie immer noch 8 GB auf dem Markt. Jede Software verwendet jedoch 2x oder 4x mehr Speicher als früher. Zumindest skalieren die Workstations nicht auf die gleiche Weise wie Cluster.

Eine Funktion 10-mal langsamer zu machen, ohne ihre Komplexität überhaupt zu ändern, reicht aus, um einen Datenwissenschaftler in den Wahnsinn zu treiben. Was noch schlimmer ist, es gibt nichts Elegantes, was er tun kann, selbst wenn der Engpass durch Profiling herausgefunden wird.

Was ich zu erläutern versuche, ist, dass die Fähigkeit zur faulen Verarbeitung immer wünschenswert ist und sowohl für die Reaktionsfähigkeit des Systems als auch für die Produktivität der Menschen, die die Sprache verwenden, entscheidend sein kann. Schwierigkeiten oder Arbeitsbelastung bei der Bibliotheksentwicklung stellen eine sehr gute Entschuldigung dafür dar, diese Funktionen nicht zu implementieren, und sind sicherlich verständlich, aber sagen Sie nicht, dass sie nicht nützlich sind.

@toobaz : Ich nehme an, Sie haben in diesem Beispiel f = lambda x: x == value_to_compare .

Richtig

Genau aus diesem Grund scheue ich mich davor, diesen Weg überhaupt zu gehen (cc @bersbersbers). Wenn Sie nicht aufpassen, landen wir bei (Schreibweise spekulativ):

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

Ich verstehe Ihre Bedenken, aber ich würde niemals um np.first_square_equal bitten, genauso wie ich niemals um np.square_where bitten würde (und niemand, hoffe ich, darum gebeten hat). Und ja, ich verstehe, dass es bedeutet, die Daten vollständig zu durchlaufen, wenn Sie 3 tun. Aber v wird einmal erstellt, und ich muss möglicherweise nach vielen verschiedenen Werten von x darüber suchen . Zum Beispiel (der Einfachheit halber auf Beispiel 2 zurückkommend) möchte ich überprüfen, ob alle meine 30 möglichen Kategorien in meinem Array mit 10 ^ 9 Elementen erscheinen - und ich vermute stark, dass sie alle unter den ersten 10 ^ 3 Elementen erscheinen.

Lassen Sie mich also zunächst meinen vorherigen Kommentar klarstellen: Ich hätte gerne np.firstwhere(x, array_or_value_to_compare) als eine Funktion, die meiner Intuition entspricht, aber die Rechenprobleme, die ich 2017 hatte, wären selbst mit np.first gelöst worden.

Zweitens geht es - glaube ich - nicht nur um die Laufzeit des einzelnen Anrufs. Es ist wahr, dass ich sowieso einen vollständigen Durchgang der Daten machen muss, um 2. und 3 zu machen ... aber vielleicht habe ich diesen Durchgang bereits gemacht, als ich die Daten initialisierte, und jetzt suche ich wirklich nach einer Möglichkeit, schneller zu werden eine häufige Operation.

Ich verstehe Ihren Punkt, dass np.first wirklich vom Standard-Numpy-Ansatz abweicht, ich sehe, dass es nicht trivial sein könnte, es gut zu implementieren ... was ich nicht sehe, ist, wie es den Rest der API "infizieren" würde, oder bauen Sie eine eigene große API auf.

Wenn Sie stattdessen der Meinung sind, dass es wirklich außerhalb des numpy-Bereichs liegt, gibt es stattdessen möglicherweise einen Bereich aus einem kleinen unabhängigen Paket.

Hallo Paul,

Ich habe einen kleinen Benchmark erstellt, in dem Ihre Lösung mit np.flatnonzero und meiner Erweiterung py_find_1st verglichen wird.

Den Benchmark finden Sie im Anhang.

Hier die Ergebnisse

(Basis) m3088.roebel: (Test) (g:master)514> ./benchmark.py
utf1st.find_1st(rr, Grenze, utf1st.cmp_equal)::
Laufzeit 0,131s
np.flatnonzero(rr==limit)[0]::
Laufzeit 2.121s
next((ii für ii, vv in enumerate(rr) if vv == limit))::
Laufzeit 1.612s

Ihre vorgeschlagene Lösung ist also 25% schneller als Flatnonzero, da dies nicht erforderlich ist
Beim Erstellen des Ergebnisarrays ist es immer noch ~ 12 langsamer als py_find_1st.find_1st.

Am besten
Axel

BEARBEITEN:
Es scheint, dass die Nachricht, auf die ich per E-Mail geantwortet habe, verschwunden ist, und auch der an meine E-Mail angehängte Benchmark. Der Maßstab ist da

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

Entschuldigung für den Lärm.

Am 15.05.2020 17:33 schrieb PK:

Wie wäre es mit |next(i for i, v in enumerate(x) if v)|?


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub https://github.com/numpy/numpy/issues/2269#issuecomment-629314457 an oder kündigen Sie sie
https://github.com/notifications/unsubscribe-auth/ACAL2LS2YZALARHBHNABVILRRVOEPANCNFSM4ABV5HGA .

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

inducer picture inducer  ·  3Kommentare

marcocaccin picture marcocaccin  ·  4Kommentare

Levstyle picture Levstyle  ·  3Kommentare

kevinzhai80 picture kevinzhai80  ·  4Kommentare

dmvianna picture dmvianna  ·  4Kommentare