Numpy: Ruft den Index der Elemente eines 1d-Arrays als 2d-Array ab


10

Ich habe ein numpy Array wie dieses: [1 2 2 0 0 1 3 5]

Ist es möglich, den Index der Elemente als 2D-Array abzurufen? Zum Beispiel wäre die Antwort für die obige Eingabe[[3 4], [0 5], [1 2], [6], [], [7]]

Momentan muss ich die verschiedenen Werte schleifen und numpy.where(input == i)für jeden Wert aufrufen , was eine schreckliche Leistung mit einer ausreichend großen Eingabe hat.


np.argsort([1, 2, 2, 0, 0, 1, 3, 5])gibt array([3, 4, 0, 5, 1, 2, 6, 7], dtype=int64). dann können Sie einfach die nächsten Elemente vergleichen.
vb_rises

Antworten:


11

Hier ist ein O (max (x) + len (x)) Ansatz unter Verwendung von scipy.sparse:

import numpy as np
from scipy import sparse

x = np.array("1 2 2 0 0 1 3 5".split(),int)
x
# array([1, 2, 2, 0, 0, 1, 3, 5])


M,N = x.max()+1,x.size
sparse.csc_matrix((x,x,np.arange(N+1)),(M,N)).tolil().rows.tolist()
# [[3, 4], [0, 5], [1, 2], [6], [], [7]]

Dies funktioniert durch Erstellen einer Sparse-Matrix mit Einträgen an den Positionen (x [0], 0), (x [1], 1), ... Mit dem CSCFormat (komprimierte Sparse-Spalte) ist dies ziemlich einfach. Die Matrix wird dann in das LILFormat (verknüpfte Liste) konvertiert. In diesem Format werden die Spaltenindizes für jede Zeile als Liste in ihrem rowsAttribut gespeichert. Wir müssen sie also nur nehmen und in eine Liste konvertieren.

Beachten Sie, dass argsortLösungen auf der Basis kleiner Arrays wahrscheinlich schneller sind, bei einigen jedoch nicht wahnsinnig großen Größen überkreuzen.

BEARBEITEN:

argsort-basierte numpy-nur Lösung:

np.split(x.argsort(kind="stable"),np.bincount(x)[:-1].cumsum())
# [array([3, 4]), array([0, 5]), array([1, 2]), array([6]), array([], dtype=int64), array([7])]

Wenn die Reihenfolge der Indizes innerhalb von Gruppen keine Rolle spielt, können Sie es auch versuchen argpartition(es macht in diesem kleinen Beispiel keinen Unterschied, aber dies ist im Allgemeinen nicht garantiert):

bb = np.bincount(x)[:-1].cumsum()
np.split(x.argpartition(bb),bb)
# [array([3, 4]), array([0, 5]), array([1, 2]), array([6]), array([], dtype=int64), array([7])]

BEARBEITEN:

@ Divakar rät von der Verwendung von ab np.split. Stattdessen ist eine Schleife wahrscheinlich schneller:

A = x.argsort(kind="stable")
B = np.bincount(x+1).cumsum()
[A[B[i-1]:B[i]] for i in range(1,len(B))]

Oder Sie können den brandneuen Walross-Operator (Python3.8 +) verwenden:

A = x.argsort(kind="stable")
B = np.bincount(x)
L = 0
[A[L:(L:=L+b)] for b in B.tolist()]

BEARBEITEN (BEARBEITET):

(Nicht reines Numpy): Alternativ zu Numba (siehe Beitrag von @ senderle) können wir auch Pythran verwenden.

Kompilieren mit pythran -O3 <filename.py>

import numpy as np

#pythran export sort_to_bins(int[:],int)

def sort_to_bins(idx, mx):
    if mx==-1: 
        mx = idx.max() + 1
    cnts = np.zeros(mx + 2, int)
    for i in range(idx.size):
        cnts[idx[i] + 2] += 1
    for i in range(3, cnts.size):
        cnts[i] += cnts[i-1]
    res = np.empty_like(idx)
    for i in range(idx.size):
        res[cnts[idx[i]+1]] = i
        cnts[idx[i]+1] += 1
    return [res[cnts[i]:cnts[i+1]] for i in range(mx)]

Hier numbagewinnt ein Whisker leistungsmäßig:

repeat(lambda:enum_bins_numba_buffer(x),number=10)
# [0.6235917090671137, 0.6071486569708213, 0.6096088469494134]
repeat(lambda:sort_to_bins(x,-1),number=10)
# [0.6235359431011602, 0.6264424560358748, 0.6217901279451326]

Ältere Sachen:

import numpy as np

#pythran export bincollect(int[:])

def bincollect(a):
    o = [[] for _ in range(a.max()+1)]
    for i,j in enumerate(a):
        o[j].append(i)
    return o

Timings vs. Numba (alt)

timeit(lambda:bincollect(x),number=10)
# 3.5732191529823467
timeit(lambda:enumerate_bins(x),number=10)
# 6.7462647299980745

Dies endete etwas schneller als @ Randys Antwort
Frederico Schardong

Eine schleifenbasierte sollte besser sein als np.split.
Divakar

@ Divakar guter Punkt, danke!
Paul Panzer

8

Eine mögliche Option, die von der Größe Ihrer Daten abhängt, besteht darin, sie einfach zu löschen numpyund zu verwenden collections.defaultdict:

In [248]: from collections import defaultdict

In [249]: d = defaultdict(list)

In [250]: l = np.random.randint(0, 100, 100000)

In [251]: %%timeit
     ...: for k, v in enumerate(l):
     ...:     d[v].append(k)
     ...:
10 loops, best of 3: 22.8 ms per loop

Dann erhalten Sie ein Wörterbuch von {value1: [index1, index2, ...], value2: [index3, index4, ...]}. Die Zeitskalierung ist nahezu linear mit der Größe des Arrays, sodass 10.000.000 auf meinem Computer ~ 2,7 Sekunden benötigen, was vernünftig genug erscheint.


7

Obwohl es sich um eine numpyLösung handelt, habe ich mich entschlossen zu prüfen, ob es eine interessante numbaLösung gibt. Und tatsächlich gibt es! Hier ist ein Ansatz, der die partitionierte Liste als zerlumptes Array darstellt, das in einem einzelnen vorab zugewiesenen Puffer gespeichert ist. Dies ist inspiriert von dem argsortvon Paul Panzer vorgeschlagenen Ansatz . (Eine ältere Version, die nicht so gut lief, aber einfacher war, siehe unten.)

@numba.jit(numba.void(numba.int64[:], 
                      numba.int64[:], 
                      numba.int64[:]), 
           nopython=True)
def enum_bins_numba_buffer_inner(ints, bins, starts):
    for x in range(len(ints)):
        i = ints[x]
        bins[starts[i]] = x
        starts[i] += 1

@numba.jit(nopython=False)  # Not 100% sure this does anything...
def enum_bins_numba_buffer(ints):
    ends = np.bincount(ints).cumsum()
    starts = np.empty(ends.shape, dtype=np.int64)
    starts[1:] = ends[:-1]
    starts[0] = 0

    bins = np.empty(ints.shape, dtype=np.int64)
    enum_bins_numba_buffer_inner(ints, bins, starts)

    starts[1:] = ends[:-1]
    starts[0] = 0
    return [bins[s:e] for s, e in zip(starts, ends)]

Dadurch wird eine Liste mit zehn Millionen Elementen in 75 ms verarbeitet. Dies entspricht einer fast 50-fachen Beschleunigung gegenüber einer in reinem Python geschriebenen listenbasierten Version.

Für eine langsamere, aber etwas besser lesbare Version hatte ich Folgendes zuvor, basierend auf der kürzlich hinzugefügten experimentellen Unterstützung für dynamisch dimensionierte "typisierte Listen", mit denen wir jeden Behälter viel schneller in einer nicht ordnungsgemäßen Weise füllen können.

Dies ringt numbaein bisschen mit der Typ-Inferenz-Engine, und ich bin sicher, dass es einen besseren Weg gibt, mit diesem Teil umzugehen. Dies stellt sich auch als fast 10x langsamer als oben heraus.

@numba.jit(nopython=True)
def enum_bins_numba(ints):
    bins = numba.typed.List()
    for i in range(ints.max() + 1):
        inner = numba.typed.List()
        inner.append(0)  # An awkward way of forcing type inference.
        inner.pop()
        bins.append(inner)

    for x, i in enumerate(ints):
        bins[i].append(x)

    return bins

Ich habe diese gegen Folgendes getestet:

def enum_bins_dict(ints):
    enum_bins = defaultdict(list)
    for k, v in enumerate(ints):
        enum_bins[v].append(k)
    return enum_bins

def enum_bins_list(ints):
    enum_bins = [[] for i in range(ints.max() + 1)]
    for x, i in enumerate(ints):
        enum_bins[i].append(x)
    return enum_bins

def enum_bins_sparse(ints):
    M, N = ints.max() + 1, ints.size
    return sparse.csc_matrix((ints, ints, np.arange(N + 1)),
                             (M, N)).tolil().rows.tolist()

Ich habe sie auch gegen eine vorkompilierte Cython-Version getestet, die der enum_bins_numba_buffer(unten ausführlich beschriebenen) ähnelt .

Auf einer Liste von zehn Millionen zufälligen Ints ( ints = np.random.randint(0, 100, 10000000)) erhalte ich die folgenden Ergebnisse:

enum_bins_dict(ints)
3.71 s ± 80.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_list(ints)
3.28 s ± 52.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_sparse(ints)
1.02 s ± 34.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_numba(ints)
693 ms ± 5.81 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_cython(ints)
82.3 ms ± 1.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

enum_bins_numba_buffer(ints)
77.4 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Beeindruckenderweise numbaübertrifft diese Art der Arbeit eine cythonVersion derselben Funktion, selbst wenn die Grenzwertprüfung deaktiviert ist. Ich bin noch nicht vertraut genug pythran, um diesen Ansatz damit zu testen, aber ich wäre an einem Vergleich interessiert. Aufgrund dieser Beschleunigung scheint es wahrscheinlich, dass die pythranVersion mit diesem Ansatz auch etwas schneller ist.

Hier ist die cythonVersion als Referenz mit einigen Build-Anweisungen. Nach der cythonInstallation benötigen Sie eine einfache setup.pyDatei wie die folgende:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import numpy

ext_modules = [
    Extension(
        'enum_bins_cython',
        ['enum_bins_cython.pyx'],
    )
]

setup(
    ext_modules=cythonize(ext_modules),
    include_dirs=[numpy.get_include()]
)

Und das Cython-Modul enum_bins_cython.pyx:

# cython: language_level=3

import cython
import numpy
cimport numpy

@cython.boundscheck(False)
@cython.cdivision(True)
@cython.wraparound(False)
cdef void enum_bins_inner(long[:] ints, long[:] bins, long[:] starts) nogil:
    cdef long i, x
    for x in range(len(ints)):
        i = ints[x]
        bins[starts[i]] = x
        starts[i] = starts[i] + 1

def enum_bins_cython(ints):
    assert (ints >= 0).all()
    # There might be a way to avoid storing two offset arrays and
    # save memory, but `enum_bins_inner` modifies the input, and
    # having separate lists of starts and ends is convenient for
    # the final partition stage.
    ends = numpy.bincount(ints).cumsum()
    starts = numpy.empty(ends.shape, dtype=numpy.int64)
    starts[1:] = ends[:-1]
    starts[0] = 0

    bins = numpy.empty(ints.shape, dtype=numpy.int64)
    enum_bins_inner(ints, bins, starts)

    starts[1:] = ends[:-1]
    starts[0] = 0
    return [bins[s:e] for s, e in zip(starts, ends)]

Führen Sie mit diesen beiden Dateien in Ihrem Arbeitsverzeichnis den folgenden Befehl aus:

python setup.py build_ext --inplace

Sie können die Funktion dann mit importieren from enum_bins_cython import enum_bins_cython.


Ich frage mich, ob Ihnen Pythran bekannt ist, das im weitesten Sinne Numba ähnelt. Ich habe meinem Beitrag eine Pythranlösung hinzugefügt. Bei dieser Gelegenheit scheint Pythran die Oberhand zu haben und liefert eine schnellere und viel pythonischere Lösung.
Paul Panzer

@PaulPanzer interessant! Ich hatte noch nichts davon gehört. Ich nehme an, dass die numba-Entwickler den erwarteten syntaktischen Zucker hinzufügen werden, sobald der Listencode stabil ist. Hier scheint es auch einen Kompromiss zwischen Komfort und Geschwindigkeit zu geben - der JIT-Dekorator lässt sich sehr einfach in eine gewöhnliche Python-Codebasis integrieren, verglichen mit einem Ansatz, der separate vorkompilierte Module erfordert. Aber eine 3-fache Beschleunigung gegenüber dem Scipy-Ansatz ist in der Tat beeindruckend, sogar überraschend!
Absender

Ich erinnere mich nur daran, dass ich dies im Grunde schon einmal getan hatte: stackoverflow.com/q/55226662/7207392 . Würde es Ihnen etwas ausmachen, Ihre Numba- und Cython-Versionen zu diesen Fragen und Antworten hinzuzufügen? Der einzige Unterschied ist: Wir haben keine Indizes 0,1,2, ... sondern ein anderes Array. Und wir machen uns nicht die Mühe, das resultierende Array tatsächlich zu zerlegen.
Paul Panzer

@ PaulPanzer ah sehr cool. Ich werde versuchen, es heute oder morgen irgendwann hinzuzufügen. Schlagen Sie eine separate Antwort vor oder bearbeiten Sie Ihre Antwort nur? So oder so glücklich!
senderle

Großartig! Ich denke, ein separater Beitrag wäre besser, aber keine starke Präferenz.
Paul Panzer

6

Hier ist eine wirklich sehr seltsame Art, dies zu tun, die schrecklich ist, aber ich fand es zu lustig, um sie nicht zu teilen - und alles numpy!

out = np.array([''] * (x.max() + 1), dtype = object)
np.add.at(out, x, ["{} ".format(i) for i in range(x.size)])
[[int(i) for i in o.split()] for o in out]

Out[]:
[[3, 4], [0, 5], [1, 2], [6], [], [7]]

EDIT: Dies ist die beste Methode, die ich auf diesem Weg finden konnte. Es ist immer noch 10x langsamer als die argsortLösung von @PaulPanzer :

out = np.empty((x.max() + 1), dtype = object)
out[:] = [[]] * (x.max() + 1)
coords = np.empty(x.size, dtype = object)
coords[:] = [[i] for i in range(x.size)]
np.add.at(out, x, coords)
list(out)

2

Sie können dies tun, indem Sie ein Wörterbuch mit Zahlen erstellen. Schlüssel sind die Zahlen und Werte sollten die Indizes sein, die die Zahl sieht. Dies ist eine der schnellsten Möglichkeiten. Sie können den folgenden Code sehen:

>>> import numpy as np
>>> a = np.array([1 ,2 ,2 ,0 ,0 ,1 ,3, 5])
>>> b = {}
# Creating an empty list for the numbers that exist in array a
>>> for i in range(np.min(a),np.max(a)+1):
    b[str(i)] = []

# Adding indices to the corresponding key
>>> for i in range(len(a)):
    b[str(a[i])].append(i)

# Resulting Dictionary
>>> b
{'0': [3, 4], '1': [0, 5], '2': [1, 2], '3': [6], '4': [], '5': [7]}

# Printing the result in the way you wanted.
>>> for i in sorted (b.keys()) :
     print(b[i], end = " ")

[3, 4] [0, 5] [1, 2] [6] [] [7] 

1

Pseudocode:

  1. Ermitteln Sie die "Anzahl der 1d-Arrays im 2d-Array", indem Sie den Minimalwert Ihres Numpy-Arrays vom Maximalwert subtrahieren und dann plus eins. In Ihrem Fall ist es 5-0 + 1 = 6

  2. Initialisieren Sie ein 2d-Array mit der Anzahl der darin enthaltenen 1d-Arrays. Initialisieren Sie in Ihrem Fall ein 2d-Array mit 6 1d-Arrays. Jedes 1d-Array entspricht einem eindeutigen Element in Ihrem Numpy-Array. Das erste 1d-Array entspricht beispielsweise '0', das zweite 1d-Array entspricht '1', ...

  3. Durchlaufen Sie Ihr Numpy-Array und setzen Sie den Index des Elements in das entsprechende 1d-Array. In Ihrem Fall wird der Index des ersten Elements in Ihrem Numpy-Array auf das zweite 1d-Array gesetzt, der Index des zweiten Elements in Ihrem Numpy-Array wird auf das dritte 1d-Array gesetzt, ....

Die Ausführung dieses Pseudocodes dauert linear, da dies von der Länge Ihres Numpy-Arrays abhängt.


1

Dies gibt Ihnen genau das, was Sie wollen und würde ungefähr 10.000 Sekunden für 10.000.000 auf meinem Computer dauern:

import numpy as np
import timeit

# x = np.array("1 2 2 0 0 1 3 5".split(),int)
x = np.random.randint(0, 100, 100000)

def create_index_list(x):
    d = {}
    max_value = -1
    for i,v in enumerate(x):
        if v > max_value:
            max_value = v
        try:
            d[v].append(i)
        except:
            d[v] = [i]
    result_list = []
    for i in range(max_value+1):
        if i in d:
            result_list.append(d[i])
        else:
            result_list.append([])
    return result_list

# print(create_index_list(x))
print(timeit.timeit(stmt='create_index_list(x)', number=1, globals=globals()))

0

Wenn Sie also eine Liste von Elementen haben, möchten Sie (Element-, Index-) Paare bilden. In linearer Zeit könnte dies wie folgt erfolgen:

hashtable = dict()
for idx, val in enumerate(mylist):
    if val not in hashtable.keys():
         hashtable[val] = list()
    hashtable[val].append(idx)
newlist = sorted(hashtable.values())

Dies sollte O (n) Zeit dauern. Ich kann mir derzeit keine schnellere Lösung vorstellen, werde sie aber hier aktualisieren, wenn ich dies tue.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.