Un canonico cartesian_product
(quasi)
Esistono molti approcci a questo problema con proprietà diverse. Alcuni sono più veloci di altri e alcuni sono più generici. Dopo molti test e modifiche, ho scoperto che la seguente funzione, che calcola un n-dimensionale cartesian_product
, è più veloce della maggior parte degli altri per molti input. Per un paio di approcci leggermente più complessi, ma in alcuni casi anche più veloci, vedi la risposta di Paul Panzer .
Data questa risposta, questa non è più l' implementazione più rapida del prodotto cartesiano di numpy
cui sono a conoscenza. Tuttavia, penso che la sua semplicità continuerà a renderlo un utile punto di riferimento per il miglioramento futuro:
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)
Vale la pena ricordare che questa funzione utilizza ix_
in modo insolito; mentre l'uso documentato di ix_
è di generare indici in un array, è possibile che matrici con la stessa forma possano essere utilizzate per assegnazioni trasmesse. Mille grazie a mgilson , che mi ha ispirato a provare ad usare ix_
questo modo, e a unutbu , che ha fornito un feedback estremamente utile su questa risposta, incluso il suggerimento da usare numpy.result_type
.
Notevoli alternative
A volte è più veloce scrivere blocchi di memoria contigui in ordine Fortran. Questa è la base di questa alternativa, cartesian_product_transpose
che si è dimostrata più veloce su alcuni hardware rispetto a cartesian_product
(vedi sotto). Tuttavia, la risposta di Paul Panzer, che utilizza lo stesso principio, è ancora più veloce. Tuttavia, includo questo qui per i lettori interessati:
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
Dopo aver compreso l'approccio di Panzer, ho scritto una nuova versione che è quasi veloce come la sua ed è quasi semplice come 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
Questo sembra avere un sovraccarico a tempo costante che lo rende più lento di Panzer per piccoli input. Ma per input più grandi, in tutti i test che ho eseguito, si comporta proprio come la sua implementazione più veloce ( cartesian_product_transpose_pp
).
Nelle sezioni seguenti, includo alcuni test di altre alternative. Questi sono ora in qualche modo obsoleti, ma piuttosto che un doppio sforzo, ho deciso di lasciarli qui per interesse storico. Per i test aggiornati, vedere la risposta di Panzer e quella di Nico Schlömer .
Prove contro alternative
Ecco una serie di test che mostrano l'incremento delle prestazioni che alcune di queste funzioni offrono rispetto a una serie di alternative. Tutti i test mostrati qui sono stati eseguiti su una macchina quad-core, con Mac OS 10.12.5, Python 3.6.1 e numpy
1.12.1. È noto che le variazioni su hardware e software producono risultati diversi, quindi YMMV. Esegui questi test per essere sicuro!
definizioni:
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)]
Risultati del test:
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 tutti i casi, cartesian_product
come definito all'inizio di questa risposta è la più veloce.
Per quelle funzioni che accettano un numero arbitrario di array di input, vale la pena controllare anche le prestazioni len(arrays) > 2
. (Fino a quando non riesco a determinare il motivo per cui cartesian_product_recursive
genera un errore in questo caso, l'ho rimosso da questi test.)
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)
Come dimostrano questi test, cartesian_product
rimane competitivo fino a quando il numero di array di input supera (approssimativamente) quattro. Dopodiché, cartesian_product_transpose
ha un leggero vantaggio.
Vale la pena ribadire che gli utenti con altro hardware e sistemi operativi possono vedere risultati diversi. Ad esempio, unutbu riporta di aver visto i seguenti risultati per questi test usando Ubuntu 14.04, Python 3.4.3 e 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
Di seguito, vado alcuni dettagli sui test precedenti che ho eseguito su questa linea. Le prestazioni relative di questi approcci sono cambiate nel tempo, per hardware diverso e versioni diverse di Python e numpy
. Sebbene non sia immediatamente utile per le persone che utilizzano versioni aggiornate di numpy
, mostra come sono cambiate le cose dalla prima versione di questa risposta.
Una semplice alternativa: meshgrid
+dstack
La risposta attualmente accettata utilizza tile
e repeat
per trasmettere insieme due array. Ma la meshgrid
funzione fa praticamente la stessa cosa. Ecco l'output di tile
e repeat
prima di essere passato per trasporre:
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])]
Ed ecco l'output di meshgrid
:
In [4]: numpy.meshgrid(x, y)
Out[4]:
[array([[1, 2, 3],
[1, 2, 3]]), array([[4, 4, 4],
[5, 5, 5]])]
Come puoi vedere, è quasi identico. Dobbiamo solo rimodellare il risultato per ottenere esattamente lo stesso risultato.
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])]
Invece di rimodellare a questo punto, però, siamo riusciti a passare l'output di meshgrid
per dstack
e rimodellare in seguito, che consente di risparmiare un po 'di lavoro:
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]])
Contrariamente a quanto affermato in questo commento , non ho visto prove che input diversi producano output di forma diversa e, come dimostra quanto sopra, fanno cose molto simili, quindi sarebbe abbastanza strano se lo facessero. Per favore fatemi sapere se trovate un controesempio.
Test meshgrid
+ dstack
vs. repeat
+transpose
Le prestazioni relative di questi due approcci sono cambiate nel tempo. In una versione precedente di Python (2.7), il risultato usando meshgrid
+ dstack
era notevolmente più veloce per input piccoli. (Nota che questi test provengono da una vecchia versione di questa risposta.) Definizioni:
>>> 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)
...
Per input di dimensioni moderate, ho visto un aumento di velocità significativo. Ma ho ripetuto questi test con versioni più recenti di Python (3.6.1) e numpy
(1.12.1), su una macchina più recente. I due approcci sono quasi identici ora.
Vecchio 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
Nuovo 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)
Come sempre, YMMV, ma ciò suggerisce che nelle recenti versioni di Python e numpy, questi sono intercambiabili.
Funzioni di prodotto generalizzate
In generale, potremmo aspettarci che l'uso delle funzioni integrate sarà più veloce per input piccoli, mentre per input grandi, una funzione appositamente costruita potrebbe essere più veloce. Inoltre, per un prodotto n-dimensionale generalizzato, tile
e repeat
non aiuterà, perché non hanno analoghi di dimensione superiore chiari. Quindi vale la pena indagare anche sul comportamento delle funzioni appositamente costruite.
La maggior parte dei test pertinenti appare all'inizio di questa risposta, ma qui ci sono alcuni dei test eseguiti su versioni precedenti di Python e numpy
per il confronto.
La cartesian
funzione definita in un'altra risposta era usata abbastanza bene per input più grandi. (È lo stesso della funzione chiamata cartesian_product_recursive
sopra.) Per confrontare cartesian
a dstack_prodct
, usiamo solo due dimensioni.
Anche in questo caso, il vecchio test ha mostrato una differenza significativa, mentre il nuovo test ne mostra quasi nessuno.
Vecchio 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
Nuovo 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)
Come prima, dstack_product
batte ancora cartesian
su scale più piccole.
Nuovo test ( vecchio test ridondante non mostrato )
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)
Queste distinzioni sono, penso, interessanti e meritevoli di essere registrate; ma alla fine sono accademici. Come hanno mostrato i test all'inizio di questa risposta, tutte queste versioni sono quasi sempre più lente di quelle cartesian_product
definite all'inizio di questa risposta, che è di per sé un po 'più lenta delle implementazioni più veloci tra le risposte a questa domanda.