Prodotto cartesiano di punti array xey in un array singolo di punti 2D


147

Ho due array intorpiditi che definiscono gli assi xey di una griglia. Per esempio:

x = numpy.array([1,2,3])
y = numpy.array([4,5])

Vorrei generare il prodotto cartesiano di questi array per generare:

array([[1,4],[2,4],[3,4],[1,5],[2,5],[3,5]])

In un modo che non è terribilmente inefficiente poiché devo farlo molte volte in un ciclo. itertools.productSuppongo che convertirli in un elenco Python e utilizzare e tornare a un array numpy non sia la forma più efficiente.


Ho notato che il passaggio più costoso nell'approccio itertools è la conversione finale da elenco a array. Senza quest'ultimo passaggio è due volte più veloce dell'esempio di Ken.
Alexey Lebedev,

Risposte:


88
>>> numpy.transpose([numpy.tile(x, len(y)), numpy.repeat(y, len(x))])
array([[1, 4],
       [2, 4],
       [3, 4],
       [1, 5],
       [2, 5],
       [3, 5]])

Vedere Utilizzo di numpy per creare un array di tutte le combinazioni di due array per una soluzione generale per il calcolo del prodotto cartesiano di N array.


1
Un vantaggio di questo approccio è che produce un output coerente per array della stessa dimensione. L' approccio meshgrid+ dstack, sebbene in alcuni casi più veloce, può portare a bug se si prevede che il prodotto cartesiano sia costruito nello stesso ordine per matrici della stessa dimensione.
tlnagy,

3
@tlnagy, non ho notato alcun caso in cui questo approccio produce risultati diversi da quelli prodotti da meshgrid+ dstack. Potresti pubblicare un esempio?
mittente

148

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 numpycui 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_transposeche 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 numpy1.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_productcome 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_recursivegenera 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_productrimane competitivo fino a quando il numero di array di input supera (approssimativamente) quattro. Dopodiché, cartesian_product_transposeha 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 numpy1.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 tilee repeatper trasmettere insieme due array. Ma la meshgridfunzione fa praticamente la stessa cosa. Ecco l'output di tilee repeatprima 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 meshgridper dstacke 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+ dstackvs. 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+ dstackera 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, tilee repeatnon 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 numpyper il confronto.

La cartesianfunzione definita in un'altra risposta era usata abbastanza bene per input più grandi. (È lo stesso della funzione chiamata cartesian_product_recursivesopra.) Per confrontare cartesiana 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_productbatte ancora cartesiansu 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_productdefinite all'inizio di questa risposta, che è di per sé un po 'più lenta delle implementazioni più veloci tra le risposte a questa domanda.


1
e l'aggiunta dtype=objectin arr = np.empty( )consentirebbe l'utilizzo di diversi tipi nel prodotto, ad es arrays = [np.array([1,2,3]), ['str1', 'str2']].
user3820991

Grazie mille per le tue soluzioni innovative. Ho pensato di voler sapere che alcuni utenti potrebbero trovare cartesian_product_tranposepiù velocemente che a cartesian_productseconda del sistema operativo del computer, della versione di Python o di Numpy. Ad esempio, su Ubuntu 14.04, python3.4.3, numpy 1.14.0.dev0 + b7050a9, %timeit cartesian_product_transpose(x500,y500)produce 1000 loops, best of 3: 682 µs per loopmentre %timeit cartesian_product(x500,y500)produce 1000 loops, best of 3: 1.55 ms per loop. Sto anche scoprendo che cartesian_product_transposepotrebbe essere più veloce quando len(arrays) > 2.
unutbu,

Inoltre, cartesian_productrestituisce un array di dtype a virgola mobile mentre cartesian_product_transposerestituisce un array dello stesso dtype del primo array (trasmesso). La capacità di preservare il dtype quando si lavora con array di numeri interi può essere una ragione per gli utenti cartesian_product_transpose.
unutbu,

@unutbu grazie ancora - come avrei dovuto sapere, la clonazione del dtype non solo aggiunge praticità; accelera il codice di un altro 20-30% in alcuni casi.
Senderle,

1
@senderle: Wow, è carino! Inoltre, mi è venuto in mente che qualcosa del genere dtype = np.find_common_type([arr.dtype for arr in arrays], [])potrebbe essere usato per trovare il dtype comune di tutti gli array, invece di forzare l'utente a posizionare l'array che controlla per primo il dtype.
unutbu, il

44

Puoi semplicemente fare la normale comprensione dell'elenco in Python

x = numpy.array([1,2,3])
y = numpy.array([4,5])
[[x0, y0] for x0 in x for y0 in y]

che dovrebbe darti

[[1, 4], [1, 5], [2, 4], [2, 5], [3, 4], [3, 5]]

28

Mi interessava anche questo e ho fatto un piccolo confronto delle prestazioni, forse un po 'più chiaro rispetto alla risposta di @ senderle.

Per due array (il caso classico):

inserisci qui la descrizione dell'immagine

Per quattro matrici:

inserisci qui la descrizione dell'immagine

(Notare che la lunghezza delle matrici è solo di poche decine di voci qui.)


Codice per riprodurre i grafici:

from functools import reduce
import itertools
import numpy
import perfplot


def dstack_product(arrays):
    return numpy.dstack(numpy.meshgrid(*arrays, indexing="ij")).reshape(-1, len(arrays))


# Generalized N-dimensional products
def cartesian_product(arrays):
    la = len(arrays)
    dtype = numpy.find_common_type([a.dtype for a in 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 = reduce(numpy.multiply, broadcasted[0].shape), len(broadcasted)
    dtype = numpy.find_common_type([a.dtype for a in 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)))


perfplot.show(
    setup=lambda n: 2 * (numpy.arange(n, dtype=float),),
    n_range=[2 ** k for k in range(13)],
    # setup=lambda n: 4 * (numpy.arange(n, dtype=float),),
    # n_range=[2 ** k for k in range(6)],
    kernels=[
        dstack_product,
        cartesian_product,
        cartesian_product_transpose,
        cartesian_product_recursive,
        cartesian_product_itertools,
    ],
    logx=True,
    logy=True,
    xlabel="len(a), len(b)",
    equality_check=None,
)

17

Basandomi sul lavoro esemplare di @ senderle, ho escogitato due versioni - una per C e una per layout Fortran - che sono spesso un po 'più veloci.

  • cartesian_product_transpose_ppè - a differenza di @ senderle cartesian_product_transposeche usa una strategia completamente diversa - una versione cartesion_productche utilizza il layout di memoria di trasposizione più favorevole + alcune ottimizzazioni molto minori.
  • cartesian_product_ppsi attacca con il layout di memoria originale. Ciò che lo rende veloce è l'utilizzo di copie contigue. Le copie contigue risultano essere molto più veloci che copiare un intero blocco di memoria anche se solo una parte di essa contiene dati validi è preferibile solo per copiare i bit validi.

Alcuni perfplots. Ne ho fatti uno separato per i layout C e Fortran, perché questi sono diversi compiti IMO.

I nomi che terminano in 'pp' sono i miei approcci.

1) molti piccoli fattori (2 elementi ciascuno)

inserisci qui la descrizione dell'immagineinserisci qui la descrizione dell'immagine

2) molti piccoli fattori (4 elementi ciascuno)

inserisci qui la descrizione dell'immagineinserisci qui la descrizione dell'immagine

3) tre fattori di uguale lunghezza

inserisci qui la descrizione dell'immagineinserisci qui la descrizione dell'immagine

4) due fattori di uguale lunghezza

inserisci qui la descrizione dell'immagineinserisci qui la descrizione dell'immagine

Codice (è necessario eseguire esecuzioni separate per ogni grafico in b / c non sono riuscito a capire come reimpostare; è inoltre necessario modificare / commentare / commentare in modo appropriato):

import numpy
import numpy as np
from functools import reduce
import itertools
import timeit
import perfplot

def dstack_product(arrays):
    return numpy.dstack(
        numpy.meshgrid(*arrays, indexing='ij')
        ).reshape(-1, len(arrays))

def cartesian_product_transpose_pp(arrays):
    la = len(arrays)
    dtype = numpy.result_type(*arrays)
    arr = numpy.empty((la, *map(len, arrays)), dtype=dtype)
    idx = slice(None), *itertools.repeat(None, la)
    for i, a in enumerate(arrays):
        arr[i, ...] = a[idx[:la-i]]
    return arr.reshape(la, -1).T

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 itertools import accumulate, repeat, chain

def cartesian_product_pp(arrays, out=None):
    la = len(arrays)
    L = *map(len, arrays), la
    dtype = numpy.result_type(*arrays)
    arr = numpy.empty(L, dtype=dtype)
    arrs = *accumulate(chain((arr,), repeat(0, la-1)), np.ndarray.__getitem__),
    idx = slice(None), *itertools.repeat(None, la-1)
    for i in range(la-1, 0, -1):
        arrs[i][..., i] = arrays[i][idx[:la-i]]
        arrs[i-1][1:] = arrs[i]
    arr[..., 0] = arrays[0][idx]
    return arr.reshape(-1, la)

def cartesian_product_itertools(arrays):
    return numpy.array(list(itertools.product(*arrays)))


# 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

### Test code ###
if False:
  perfplot.save('cp_4el_high.png',
    setup=lambda n: n*(numpy.arange(4, dtype=float),),
                n_range=list(range(6, 11)),
    kernels=[
        dstack_product,
        cartesian_product_recursive,
        cartesian_product,
#        cartesian_product_transpose,
        cartesian_product_pp,
#        cartesian_product_transpose_pp,
        ],
    logx=False,
    logy=True,
    xlabel='#factors',
    equality_check=None
    )
else:
  perfplot.save('cp_2f_T.png',
    setup=lambda n: 2*(numpy.arange(n, dtype=float),),
    n_range=[2**k for k in range(5, 11)],
    kernels=[
#        dstack_product,
#        cartesian_product_recursive,
#        cartesian_product,
        cartesian_product_transpose,
#        cartesian_product_pp,
        cartesian_product_transpose_pp,
        ],
    logx=True,
    logy=True,
    xlabel='length of each factor',
    equality_check=None
    )

Grazie per aver condiviso questa risposta eccellente. Quando la dimensione di arraysin cartesian_product_transpose_pp (array) supera una determinata dimensione, MemoryErrorsi verificherà. In questa situazione, vorrei che questa funzione producesse piccoli blocchi di risultati. Ho pubblicato una domanda su questo argomento. Puoi rispondere alla mia domanda? Grazie.
Sun Bear,

13

A partire da ottobre 2017, numpy ora ha una np.stackfunzione generica che accetta un parametro axis. Usandolo, possiamo avere un "prodotto cartesiano generalizzato" usando la tecnica "dstack e meshgrid":

import numpy as np
def cartesian_product(*arrays):
    ndim = len(arrays)
    return np.stack(np.meshgrid(*arrays), axis=-1).reshape(-1, ndim)

Nota sul axis=-1parametro Questo è l'ultimo (più interno) asse del risultato. È equivalente all'utilizzo axis=ndim.

Un altro commento, dal momento che i prodotti cartesiani esplodono molto rapidamente, a meno che non abbiamo bisogno di realizzare l'array in memoria per qualche motivo, se il prodotto è molto grande, potremmo voler utilizzare itertoolse utilizzare i valori al volo.


8

Ho usato @kennytm answer per un po ', ma quando ho provato a fare lo stesso in TensorFlow, ma ho scoperto che TensorFlow non ha equivalenti numpy.repeat(). Dopo un po 'di sperimentazione, penso di aver trovato una soluzione più generale per vettori di punti arbitrari.

Per intorpidimento:

import numpy as np

def cartesian_product(*args: np.ndarray) -> np.ndarray:
    """
    Produce the cartesian product of arbitrary length vectors.

    Parameters
    ----------
    np.ndarray args
        vector of points of interest in each dimension

    Returns
    -------
    np.ndarray
        the cartesian product of size [m x n] wherein:
            m = prod([len(a) for a in args])
            n = len(args)
    """
    for i, a in enumerate(args):
        assert a.ndim == 1, "arg {:d} is not rank 1".format(i)
    return np.concatenate([np.reshape(xi, [-1, 1]) for xi in np.meshgrid(*args)], axis=1)

e per TensorFlow:

import tensorflow as tf

def cartesian_product(*args: tf.Tensor) -> tf.Tensor:
    """
    Produce the cartesian product of arbitrary length vectors.

    Parameters
    ----------
    tf.Tensor args
        vector of points of interest in each dimension

    Returns
    -------
    tf.Tensor
        the cartesian product of size [m x n] wherein:
            m = prod([len(a) for a in args])
            n = len(args)
    """
    for i, a in enumerate(args):
        tf.assert_rank(a, 1, message="arg {:d} is not rank 1".format(i))
    return tf.concat([tf.reshape(xi, [-1, 1]) for xi in tf.meshgrid(*args)], axis=1)

6

Il pacchetto Scikit-learn ha un'implementazione rapida esattamente di questo:

from sklearn.utils.extmath import cartesian
product = cartesian((x,y))

Nota che la convenzione di questa implementazione è diversa da quella che desideri, se ti interessa l'ordine dell'output. Per il tuo esatto ordine, puoi farlo

product = cartesian((y,x))[:, ::-1]

È più veloce della funzione di @ senderle?
cs95,

@ cᴏʟᴅsᴘᴇᴇᴅ Non ho ancora testato. Speravo che questo fosse implementato in eg C o Fortran e quindi praticamente imbattibile, ma sembra essere stato scritto usando NumPy. Come tale, questa funzione è conveniente ma non dovrebbe essere significativamente più veloce di ciò che si può costruire usando NumPy costruisce se stessi.
jmd_dk,

4

Più in generale, se si hanno due matrici numpy 2d aeb, e si desidera concatenare ogni riga di a ogni riga di b (Un prodotto cartesiano di righe, un po 'come un join in un database), è possibile utilizzare questo metodo :

import numpy
def join_2d(a, b):
    assert a.dtype == b.dtype
    a_part = numpy.tile(a, (len(b), 1))
    b_part = numpy.repeat(b, len(a), axis=0)
    return numpy.hstack((a_part, b_part))

3

Il più veloce che puoi ottenere è sia combinando un'espressione del generatore con la funzione della mappa:

import numpy
import datetime
a = np.arange(1000)
b = np.arange(200)

start = datetime.datetime.now()

foo = (item for sublist in [list(map(lambda x: (x,i),a)) for i in b] for item in sublist)

print (list(foo))

print ('execution time: {} s'.format((datetime.datetime.now() - start).total_seconds()))

Output (in realtà viene stampato l'intero elenco risultante):

[(0, 0), (1, 0), ...,(998, 199), (999, 199)]
execution time: 1.253567 s

o usando un'espressione di doppio generatore:

a = np.arange(1000)
b = np.arange(200)

start = datetime.datetime.now()

foo = ((x,y) for x in a for y in b)

print (list(foo))

print ('execution time: {} s'.format((datetime.datetime.now() - start).total_seconds()))

Output (elenco intero stampato):

[(0, 0), (1, 0), ...,(998, 199), (999, 199)]
execution time: 1.187415 s

Tenere presente che la maggior parte del tempo di calcolo va nel comando di stampa. I calcoli del generatore sono altrimenti decentemente efficienti. Senza stampare i tempi di calcolo sono:

execution time: 0.079208 s

per espressione generatore + funzione mappa e:

execution time: 0.007093 s

per la doppia espressione del generatore.

Se quello che vuoi effettivamente è calcolare il prodotto reale di ciascuna delle coppie di coordinate, il più veloce è risolverlo come un prodotto a matrice intorpidita:

a = np.arange(1000)
b = np.arange(200)

start = datetime.datetime.now()

foo = np.dot(np.asmatrix([[i,0] for i in a]), np.asmatrix([[i,0] for i in b]).T)

print (foo)

print ('execution time: {} s'.format((datetime.datetime.now() - start).total_seconds()))

Uscite:

 [[     0      0      0 ...,      0      0      0]
 [     0      1      2 ...,    197    198    199]
 [     0      2      4 ...,    394    396    398]
 ..., 
 [     0    997   1994 ..., 196409 197406 198403]
 [     0    998   1996 ..., 196606 197604 198602]
 [     0    999   1998 ..., 196803 197802 198801]]
execution time: 0.003869 s

e senza stampa (in questo caso non risparmia molto poiché in realtà viene stampato solo un piccolo pezzo della matrice):

execution time: 0.003083 s

Per il calcolo del prodotto, la trasmissione del prodotto esterno foo = a[:,None]*bè più veloce. Utilizzando il tuo metodo di timing senza print(foo), è 0,001103 s contro 0,002225 s. Usando timeit, è 304 μs contro 1,6 ms. Matrix è noto per essere più lento di ndarray, quindi ho provato il tuo codice con np.array ma è ancora più lento (1,57 ms) della trasmissione.
Syockit

2

Questo può anche essere fatto facilmente usando il metodo itertools.product

from itertools import product
import numpy as np

x = np.array([1, 2, 3])
y = np.array([4, 5])
cart_prod = np.array(list(product(*[x, y])),dtype='int32')

Risultato: array ([
[1, 4],
[1, 5],
[2, 4],
[2, 5],
[3, 4],
[3, 5]], dtype = int32)

Tempo di esecuzione: 0,000155 s


1
non è necessario chiamare numpy. anche le vecchie matrici di pitone semplici funzionano con questo.
Coddy,

0

Nel caso specifico in cui è necessario eseguire operazioni semplici come l'aggiunta su ciascuna coppia, è possibile introdurre una dimensione aggiuntiva e consentire alla trasmissione di fare il lavoro:

>>> a, b = np.array([1,2,3]), np.array([10,20,30])
>>> a[None,:] + b[:,None]
array([[11, 12, 13],
       [21, 22, 23],
       [31, 32, 33]])

Non sono sicuro se esiste un modo simile per ottenere effettivamente le coppie stesse.


Se dtypeè floatpossibile, (a[:, None, None] + 1j * b[None, :, None]).view(float)è sorprendentemente veloce.
Paul Panzer,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.