Ein kanonischer cartesian_product
(fast)
Es gibt viele Ansätze für dieses Problem mit unterschiedlichen Eigenschaften. Einige sind schneller als andere, andere sind allgemeiner. Nach vielen Tests und Optimierungen habe ich festgestellt, dass die folgende Funktion, die eine n-Dimension berechnet cartesian_product
, für viele Eingaben schneller ist als die meisten anderen. Für ein paar Ansätze, die etwas komplexer sind, aber in vielen Fällen sogar etwas schneller, siehe die Antwort von Paul Panzer .
Angesichts dieser Antwort ist dies nicht mehr die schnellste Implementierung des kartesischen Produkts numpy
, die mir bekannt ist. Ich denke jedoch, dass seine Einfachheit es weiterhin zu einem nützlichen Maßstab für zukünftige Verbesserungen machen wird:
def cartesian_product(*arrays):
la = len(arrays)
dtype = numpy.result_type(*arrays)
arr = numpy.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(numpy.ix_(*arrays)):
arr[...,i] = a
return arr.reshape(-1, la)
Es ist erwähnenswert, dass diese Funktion auf ix_
ungewöhnliche Weise verwendet wird. Während die dokumentierte Verwendung von darin ix_
besteht, Indizes in einem Array zu generieren , kommt es nur so vor, dass Arrays mit derselben Form für die Broadcast-Zuweisung verwendet werden können. Vielen Dank an mgilson , der mich dazu inspiriert hat, ix_
diesen Weg zu versuchen , und an unutbu , der einige äußerst hilfreiche Rückmeldungen zu dieser Antwort gegeben hat, einschließlich des Vorschlags zur Verwendung numpy.result_type
.
Bemerkenswerte Alternativen
Es ist manchmal schneller, zusammenhängende Speicherblöcke in Fortran-Reihenfolge zu schreiben. Das ist die Basis dieser Alternative, cartesian_product_transpose
die sich auf einigen Hardwarekomponenten als schneller erwiesen hat als cartesian_product
(siehe unten). Die Antwort von Paul Panzer, die dasselbe Prinzip verwendet, ist jedoch noch schneller. Trotzdem füge ich dies hier für interessierte Leser ein:
def cartesian_product_transpose(*arrays):
broadcastable = numpy.ix_(*arrays)
broadcasted = numpy.broadcast_arrays(*broadcastable)
rows, cols = numpy.prod(broadcasted[0].shape), len(broadcasted)
dtype = numpy.result_type(*arrays)
out = numpy.empty(rows * cols, dtype=dtype)
start, end = 0, rows
for a in broadcasted:
out[start:end] = a.reshape(-1)
start, end = end, end + rows
return out.reshape(cols, rows).T
Nachdem ich Panzers Ansatz verstanden hatte, schrieb ich eine neue Version, die fast so schnell ist wie seine und fast so einfach wie cartesian_product
:
def cartesian_product_simple_transpose(arrays):
la = len(arrays)
dtype = numpy.result_type(*arrays)
arr = numpy.empty([la] + [len(a) for a in arrays], dtype=dtype)
for i, a in enumerate(numpy.ix_(*arrays)):
arr[i, ...] = a
return arr.reshape(la, -1).T
Dies scheint einen zeitlich konstanten Overhead zu haben, der es bei kleinen Eingaben langsamer laufen lässt als bei Panzer. Bei größeren Eingaben funktioniert es in allen von mir durchgeführten Tests genauso gut wie seine schnellste Implementierung ( cartesian_product_transpose_pp
).
In den folgenden Abschnitten füge ich einige Tests anderer Alternativen hinzu. Diese sind jetzt etwas veraltet, aber anstatt doppelte Anstrengungen zu unternehmen, habe ich beschlossen, sie aus historischem Interesse hier zu lassen. Aktuelle Tests finden Sie in Panzers Antwort sowie in der von Nico Schlömer .
Tests gegen Alternativen
Hier finden Sie eine Reihe von Tests, die die Leistungssteigerung zeigen, die einige dieser Funktionen im Vergleich zu einer Reihe von Alternativen bieten. Alle hier gezeigten Tests wurden auf einem Quad-Core-Computer unter Mac OS 10.12.5, Python 3.6.1 und numpy
1.12.1 durchgeführt. Es ist bekannt, dass Variationen von Hardware und Software zu unterschiedlichen Ergebnissen führen, so YMMV. Führen Sie diese Tests selbst durch, um sicherzugehen!
Definitionen:
import numpy
import itertools
from functools import reduce
### Two-dimensional products ###
def repeat_product(x, y):
return numpy.transpose([numpy.tile(x, len(y)),
numpy.repeat(y, len(x))])
def dstack_product(x, y):
return numpy.dstack(numpy.meshgrid(x, y)).reshape(-1, 2)
### Generalized N-dimensional products ###
def cartesian_product(*arrays):
la = len(arrays)
dtype = numpy.result_type(*arrays)
arr = numpy.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(numpy.ix_(*arrays)):
arr[...,i] = a
return arr.reshape(-1, la)
def cartesian_product_transpose(*arrays):
broadcastable = numpy.ix_(*arrays)
broadcasted = numpy.broadcast_arrays(*broadcastable)
rows, cols = numpy.prod(broadcasted[0].shape), len(broadcasted)
dtype = numpy.result_type(*arrays)
out = numpy.empty(rows * cols, dtype=dtype)
start, end = 0, rows
for a in broadcasted:
out[start:end] = a.reshape(-1)
start, end = end, end + rows
return out.reshape(cols, rows).T
# from https://stackoverflow.com/a/1235363/577088
def cartesian_product_recursive(*arrays, out=None):
arrays = [numpy.asarray(x) for x in arrays]
dtype = arrays[0].dtype
n = numpy.prod([x.size for x in arrays])
if out is None:
out = numpy.zeros([n, len(arrays)], dtype=dtype)
m = n // arrays[0].size
out[:,0] = numpy.repeat(arrays[0], m)
if arrays[1:]:
cartesian_product_recursive(arrays[1:], out=out[0:m,1:])
for j in range(1, arrays[0].size):
out[j*m:(j+1)*m,1:] = out[0:m,1:]
return out
def cartesian_product_itertools(*arrays):
return numpy.array(list(itertools.product(*arrays)))
### Test code ###
name_func = [('repeat_product',
repeat_product),
('dstack_product',
dstack_product),
('cartesian_product',
cartesian_product),
('cartesian_product_transpose',
cartesian_product_transpose),
('cartesian_product_recursive',
cartesian_product_recursive),
('cartesian_product_itertools',
cartesian_product_itertools)]
def test(in_arrays, test_funcs):
global func
global arrays
arrays = in_arrays
for name, func in test_funcs:
print('{}:'.format(name))
%timeit func(*arrays)
def test_all(*in_arrays):
test(in_arrays, name_func)
# `cartesian_product_recursive` throws an
# unexpected error when used on more than
# two input arrays, so for now I've removed
# it from these tests.
def test_cartesian(*in_arrays):
test(in_arrays, name_func[2:4] + name_func[-1:])
x10 = [numpy.arange(10)]
x50 = [numpy.arange(50)]
x100 = [numpy.arange(100)]
x500 = [numpy.arange(500)]
x1000 = [numpy.arange(1000)]
Testergebnisse:
In [2]: test_all(*(x100 * 2))
repeat_product:
67.5 µs ± 633 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
dstack_product:
67.7 µs ± 1.09 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
cartesian_product:
33.4 µs ± 558 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
cartesian_product_transpose:
67.7 µs ± 932 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
cartesian_product_recursive:
215 µs ± 6.01 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product_itertools:
3.65 ms ± 38.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [3]: test_all(*(x500 * 2))
repeat_product:
1.31 ms ± 9.28 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
dstack_product:
1.27 ms ± 7.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product:
375 µs ± 4.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product_transpose:
488 µs ± 8.88 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product_recursive:
2.21 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
105 ms ± 1.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [4]: test_all(*(x1000 * 2))
repeat_product:
10.2 ms ± 132 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
dstack_product:
12 ms ± 120 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product:
4.75 ms ± 57.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_transpose:
7.76 ms ± 52.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_recursive:
13 ms ± 209 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
422 ms ± 7.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In allen Fällen ist cartesian_product
die am Anfang dieser Antwort definierte Antwort am schnellsten.
Für Funktionen, die eine beliebige Anzahl von Eingabearrays akzeptieren, lohnt es sich, die Leistung auch dann zu überprüfen len(arrays) > 2
. (Bis ich feststellen kann, warum cartesian_product_recursive
in diesem Fall ein Fehler auftritt, habe ich ihn aus diesen Tests entfernt.)
In [5]: test_cartesian(*(x100 * 3))
cartesian_product:
8.8 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_transpose:
7.87 ms ± 91.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
518 ms ± 5.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [6]: test_cartesian(*(x50 * 4))
cartesian_product:
169 ms ± 5.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
cartesian_product_transpose:
184 ms ± 4.32 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
cartesian_product_itertools:
3.69 s ± 73.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [7]: test_cartesian(*(x10 * 6))
cartesian_product:
26.5 ms ± 449 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
cartesian_product_transpose:
16 ms ± 133 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
728 ms ± 16 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [8]: test_cartesian(*(x10 * 7))
cartesian_product:
650 ms ± 8.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
cartesian_product_transpose:
518 ms ± 7.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
cartesian_product_itertools:
8.13 s ± 122 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Wie diese Tests zeigen, cartesian_product
bleibt es wettbewerbsfähig, bis die Anzahl der Eingabearrays über (ungefähr) vier steigt. Danach cartesian_product_transpose
hat eine leichte Kante.
Es sollte wiederholt werden, dass Benutzer mit anderer Hardware und Betriebssystemen möglicherweise andere Ergebnisse sehen. Unutbu-Berichte zeigen beispielsweise die folgenden Ergebnisse für diese Tests mit Ubuntu 14.04, Python 3.4.3 und numpy
1.14.0.dev0 + b7050a9:
>>> %timeit cartesian_product_transpose(x500, y500)
1000 loops, best of 3: 682 µs per loop
>>> %timeit cartesian_product(x500, y500)
1000 loops, best of 3: 1.55 ms per loop
Im Folgenden gehe ich auf einige Details zu früheren Tests ein, die ich in diesem Sinne durchgeführt habe. Die relative Leistung dieser Ansätze hat sich im Laufe der Zeit für verschiedene Hardware und verschiedene Versionen von Python und geändert numpy
. Es ist zwar nicht sofort nützlich für Benutzer, die aktuelle Versionen von verwenden numpy
, zeigt jedoch, wie sich die Dinge seit der ersten Version dieser Antwort geändert haben.
Eine einfache Alternative: meshgrid
+dstack
Die aktuell akzeptierte Antwort verwendet tile
und repeat
sendet zwei Arrays zusammen. Aber die meshgrid
Funktion macht praktisch das Gleiche. Hier ist die Ausgabe von tile
undrepeat
bevor sie zur Transponierung übergeben wird:
In [1]: import numpy
In [2]: x = numpy.array([1,2,3])
...: y = numpy.array([4,5])
...:
In [3]: [numpy.tile(x, len(y)), numpy.repeat(y, len(x))]
Out[3]: [array([1, 2, 3, 1, 2, 3]), array([4, 4, 4, 5, 5, 5])]
Und hier ist die Ausgabe von meshgrid
:
In [4]: numpy.meshgrid(x, y)
Out[4]:
[array([[1, 2, 3],
[1, 2, 3]]), array([[4, 4, 4],
[5, 5, 5]])]
Wie Sie sehen können, ist es fast identisch. Wir müssen nur das Ergebnis umformen, um genau das gleiche Ergebnis zu erzielen.
In [5]: xt, xr = numpy.meshgrid(x, y)
...: [xt.ravel(), xr.ravel()]
Out[5]: [array([1, 2, 3, 1, 2, 3]), array([4, 4, 4, 5, 5, 5])]
Anstatt an dieser Stelle eine Umformung vorzunehmen, könnten wir die Ausgabe von meshgrid
an übergeben dstack
und anschließend umformen, was einige Arbeit spart:
In [6]: numpy.dstack(numpy.meshgrid(x, y)).reshape(-1, 2)
Out[6]:
array([[1, 4],
[2, 4],
[3, 4],
[1, 5],
[2, 5],
[3, 5]])
Entgegen der Behauptung in diesem Kommentar habe ich keine Beweise dafür gesehen, dass unterschiedliche Eingaben unterschiedlich geformte Ausgaben erzeugen, und wie oben gezeigt, tun sie sehr ähnliche Dinge, so dass es ziemlich seltsam wäre, wenn sie dies tun würden. Bitte lassen Sie mich wissen, wenn Sie ein Gegenbeispiel finden.
Testen meshgrid
+dstack
gegen repeat
+transpose
Die relative Leistung dieser beiden Ansätze hat sich im Laufe der Zeit geändert. In einer früheren Version von Python (2.7) war das Ergebnis mit meshgrid
+ dstack
bei kleinen Eingaben deutlich schneller. (Beachten Sie, dass diese Tests aus einer alten Version dieser Antwort stammen.) Definitionen:
>>> def repeat_product(x, y):
... return numpy.transpose([numpy.tile(x, len(y)),
numpy.repeat(y, len(x))])
...
>>> def dstack_product(x, y):
... return numpy.dstack(numpy.meshgrid(x, y)).reshape(-1, 2)
...
Bei mäßig großen Eingaben sah ich eine deutliche Beschleunigung. Ich habe diese Tests jedoch mit neueren Versionen von Python (3.6.1) und numpy
(1.12.1) auf einem neueren Computer wiederholt. Die beiden Ansätze sind jetzt fast identisch.
Alter Test
>>> x, y = numpy.arange(500), numpy.arange(500)
>>> %timeit repeat_product(x, y)
10 loops, best of 3: 62 ms per loop
>>> %timeit dstack_product(x, y)
100 loops, best of 3: 12.2 ms per loop
Neuer Test
In [7]: x, y = numpy.arange(500), numpy.arange(500)
In [8]: %timeit repeat_product(x, y)
1.32 ms ± 24.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [9]: %timeit dstack_product(x, y)
1.26 ms ± 8.47 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Wie immer YMMV, aber dies deutet darauf hin, dass diese in neueren Versionen von Python und Numpy austauschbar sind.
Verallgemeinerte Produktfunktionen
Im Allgemeinen können wir erwarten, dass die Verwendung integrierter Funktionen für kleine Eingaben schneller ist, während für große Eingaben eine speziell entwickelte Funktion möglicherweise schneller ist. Weiterhin für ein verallgemeinertes n-dimensionales Produkt tile
undrepeat
wird nicht helfen, weil sie keine klaren höherdimensionalen Analoga haben. Es lohnt sich also, auch das Verhalten von speziell entwickelten Funktionen zu untersuchen.
Die meisten relevanten Tests erscheinen am Anfang dieser Antwort, aber hier sind einige der Tests, die mit früheren Versionen von Python und numpy
zum Vergleich durchgeführt wurden.
Die cartesian
in einer anderen Antwort definierte Funktion wurde für größere Eingaben verwendet. (Es ist das gleiche wie die Funktion aufgerufen cartesian_product_recursive
oben.) Um zu vergleichen , cartesian
zu dstack_prodct
verwenden wir nur zwei Dimensionen.
Auch hier zeigte der alte Test einen signifikanten Unterschied, während der neue Test fast keinen zeigt.
Alter Test
>>> x, y = numpy.arange(1000), numpy.arange(1000)
>>> %timeit cartesian([x, y])
10 loops, best of 3: 25.4 ms per loop
>>> %timeit dstack_product(x, y)
10 loops, best of 3: 66.6 ms per loop
Neuer Test
In [10]: x, y = numpy.arange(1000), numpy.arange(1000)
In [11]: %timeit cartesian([x, y])
12.1 ms ± 199 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [12]: %timeit dstack_product(x, y)
12.7 ms ± 334 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Nach wie vor dstack_product
schlägt cartesian
in kleineren Maßstäben.
Neuer Test ( redundanter alter Test nicht gezeigt )
In [13]: x, y = numpy.arange(100), numpy.arange(100)
In [14]: %timeit cartesian([x, y])
215 µs ± 4.75 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [15]: %timeit dstack_product(x, y)
65.7 µs ± 1.15 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Diese Unterscheidungen sind meiner Meinung nach interessant und es lohnt sich, sie aufzuzeichnen. aber sie sind am Ende akademisch. Wie die Tests am Anfang dieser Antwort gezeigt haben, sind alle diese Versionen fast immer langsamer als cartesian_product
am Anfang dieser Antwort definiert - was selbst etwas langsamer ist als die schnellsten Implementierungen unter den Antworten auf diese Frage.