Qual è il modo più veloce per mappare i nomi dei gruppi di array numpy agli indici?


9

Sto lavorando con la nuvola di punti 3D di Lidar. I punti sono dati dall'array numpy che assomiglia a questo:

points = np.array([[61651921, 416326074, 39805], [61605255, 416360555, 41124], [61664810, 416313743, 39900], [61664837, 416313749, 39910], [61674456, 416316663, 39503], [61651933, 416326074, 39802], [61679969, 416318049, 39500], [61674494, 416316677, 39508], [61651908, 416326079, 39800], [61651908, 416326087, 39802], [61664845, 416313738, 39913], [61674480, 416316668, 39503], [61679996, 416318047, 39510], [61605290, 416360572, 41118], [61605270, 416360565, 41122], [61683939, 416313004, 41052], [61683936, 416313033, 41060], [61679976, 416318044, 39509], [61605279, 416360555, 41109], [61664837, 416313739, 39915], [61674487, 416316666, 39505], [61679961, 416318035, 39503], [61683943, 416313004, 41054], [61683930, 416313042, 41059]])

Vorrei che i miei dati fossero raggruppati in cubi di dimensioni in 50*50*50modo che ogni cubo conservasse un indice hash e indici numpy dei miei pointscontenuti . Per ottenere la suddivisione, assegno cubes = points \\ 50quali output a:

cubes = np.array([[1233038, 8326521, 796], [1232105, 8327211, 822], [1233296, 8326274, 798], [1233296, 8326274, 798], [1233489, 8326333, 790], [1233038, 8326521, 796], [1233599, 8326360, 790], [1233489, 8326333, 790], [1233038, 8326521, 796], [1233038, 8326521, 796], [1233296, 8326274, 798], [1233489, 8326333, 790], [1233599, 8326360, 790], [1232105, 8327211, 822], [1232105, 8327211, 822], [1233678, 8326260, 821], [1233678, 8326260, 821], [1233599, 8326360, 790], [1232105, 8327211, 822], [1233296, 8326274, 798], [1233489, 8326333, 790], [1233599, 8326360, 790], [1233678, 8326260, 821], [1233678, 8326260, 821]])

L'output desiderato è simile al seguente:

{(1232105, 8327211, 822): [1, 13, 14, 18]), 
(1233038, 8326521, 796): [0, 5, 8, 9], 
(1233296, 8326274, 798): [2, 3, 10, 19], 
(1233489, 8326333, 790): [4, 7, 11, 20], 
(1233599, 8326360, 790): [6, 12, 17, 21], 
(1233678, 8326260, 821): [15, 16, 22, 23]}

La mia vera nuvola di punti contiene fino a poche centinaia di milioni di punti 3D. Qual è il modo più veloce per fare questo tipo di raggruppamento?

Ho provato la maggior parte delle varie soluzioni. Ecco un confronto tra il consumo di tempo ipotizzando che la dimensione dei punti sia di circa 20 milioni e la dimensione di cubi distinti sia di circa 1 milione:

Panda [tuple (elem) -> np.array (dtype = int64)]

import pandas as pd
print(pd.DataFrame(cubes).groupby([0,1,2]).indices)
#takes 9sec

Defauldict [elem.tobytes () o tuple -> list]

#thanks @abc:
result = defaultdict(list)
for idx, elem in enumerate(cubes):
    result[elem.tobytes()].append(idx) # takes 20.5sec
    # result[elem[0], elem[1], elem[2]].append(idx) #takes 27sec
    # result[tuple(elem)].append(idx) # takes 50sec

numpy_indexed [int -> np.array]

# thanks @Eelco Hoogendoorn for his library
values = npi.group_by(cubes).split(np.arange(len(cubes)))
result = dict(enumerate(values))
# takes 9.8sec

Panda + riduzione della dimensionalità [int -> np.array (dtype = int64)]

# thanks @Divakar for showing numexpr library:
import numexpr as ne
def dimensionality_reduction(cubes):
    #cubes = cubes - np.min(cubes, axis=0) #in case some coords are negative 
    cubes = cubes.astype(np.int64)
    s0, s1 = cubes[:,0].max()+1, cubes[:,1].max()+1
    d = {'s0':s0,'s1':s1,'c0':cubes[:,0],'c1':cubes[:,1],'c2':cubes[:,2]}
    c1D = ne.evaluate('c0+c1*s0+c2*s0*s1',d)
    return c1D
cubes = dimensionality_reduction(cubes)
result = pd.DataFrame(cubes).groupby([0]).indices
# takes 2.5 seconds

È possibile scaricare il cubes.npzfile qui e utilizzare un comando

cubes = np.load('cubes.npz')['array']

per controllare il tempo di esecuzione.


Hai sempre lo stesso numero di indici in ogni elenco nel tuo risultato?
Mykola Zotko,

Sì, è sempre lo stesso: 983234 cubi distinti per tutte le soluzioni sopra menzionate.
Mathfux,

1
È improbabile che una soluzione Pandas così semplice sia battuta da un approccio semplice, poiché è stato speso un grande sforzo per ottimizzarla. Un approccio basato su Cython potrebbe probabilmente affrontarlo, ma dubito che lo avrebbe superato.
norok2

1
@mathfux Devi avere l'output finale come dizionario o sarebbe giusto avere i gruppi e i loro indici come due output?
Divakar,

@ norok2 numpy_indexedsi avvicina solo a questo. Immagino sia giusto. pandasAttualmente uso i miei processi di classificazione.
Mathfux,

Risposte:


6

Numero costante di indici per gruppo

Approccio n. 1

Siamo in grado di eseguire dimensionality-reductionper ridurre cubesa un array 1D. Questo si basa su una mappatura dei dati dei cubi dati su una griglia n-dim per calcolare gli equivalenti dell'indice lineare, discussi in dettaglio here. Quindi, in base all'unicità di quegli indici lineari, possiamo separare gruppi unici e i loro indici corrispondenti. Quindi, seguendo queste strategie, avremmo una soluzione, in questo modo -

N = 4 # number of indices per group
c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)
sidx = c1D.argsort()
indices = sidx.reshape(-1,N)
unq_groups = cubes[indices[:,0]]

# If you need in a zipped dictionary format
out = dict(zip(map(tuple,unq_groups), indices))

Alternativa n. 1: se i valori interi in cubessono troppo grandi, potremmo voler fare in dimensionality-reductionmodo che le dimensioni con estensione minore siano scelte come assi primari. Quindi, per quei casi, possiamo modificare il passo di riduzione per ottenere c1D, in questo modo -

s1,s2 = cubes[:,:2].max(0)+1
s = np.r_[s2,1,s1*s2]
c1D = cubes.dot(s)

Approccio n. 2

Successivamente, possiamo utilizzare Cython-powered kd-treeper una rapida ricerca del vicino più vicino per ottenere gli indici dei vicini più vicini e quindi risolvere il nostro caso in questo modo -

from scipy.spatial import cKDTree

idx = cKDTree(cubes).query(cubes, k=N)[1] # N = 4 as discussed earlier
I = idx[:,0].argsort().reshape(-1,N)[:,0]
unq_groups,indices = cubes[I],idx[I]

Caso generico: numero variabile di indici per gruppo

Estenderemo il metodo basato su argsort con alcune suddivisioni per ottenere il risultato desiderato, in questo modo -

c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)

sidx = c1D.argsort()
c1Ds = c1D[sidx]
split_idx = np.flatnonzero(np.r_[True,c1Ds[:-1]!=c1Ds[1:],True])
grps = cubes[sidx[split_idx[:-1]]]

indices = [sidx[i:j] for (i,j) in zip(split_idx[:-1],split_idx[1:])]
# If needed as dict o/p
out = dict(zip(map(tuple,grps), indices))

Utilizzo di versioni 1D di gruppi cubescome chiavi

Estenderemo il precedente metodo elencato con i gruppi di cubescome chiavi per semplificare il processo di creazione del dizionario e renderlo anche efficiente con esso, in questo modo -

def numpy1(cubes):
    c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)        
    sidx = c1D.argsort()
    c1Ds = c1D[sidx]
    mask = np.r_[True,c1Ds[:-1]!=c1Ds[1:],True]
    split_idx = np.flatnonzero(mask)
    indices = [sidx[i:j] for (i,j) in zip(split_idx[:-1],split_idx[1:])]
    out = dict(zip(c1Ds[mask[:-1]],indices))
    return out

Successivamente, utilizzeremo il numbapacchetto per iterare e arrivare all'output del dizionario hash finale. Accanto a questo, ci sarebbero due soluzioni: una che ottiene le chiavi e i valori separatamente usando numbae la chiamata principale si comprimerà e convertirà in dict, mentre l'altra creerà un numba-supportedtipo di dict e quindi nessun lavoro extra richiesto dalla funzione di chiamata principale .

Pertanto, avremmo la prima numbasoluzione:

from numba import  njit

@njit
def _numba1(sidx, c1D):
    out = []
    n = len(sidx)
    start = 0
    grpID = []
    for i in range(1,n):
        if c1D[sidx[i]]!=c1D[sidx[i-1]]:
            out.append(sidx[start:i])
            grpID.append(c1D[sidx[start]])
            start = i
    out.append(sidx[start:])
    grpID.append(c1D[sidx[start]])
    return grpID,out

def numba1(cubes):
    c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)
    sidx = c1D.argsort()
    out = dict(zip(*_numba1(sidx, c1D)))
    return out

E seconda numbasoluzione come:

from numba import types
from numba.typed import Dict

int_array = types.int64[:]

@njit
def _numba2(sidx, c1D):
    n = len(sidx)
    start = 0
    outt = Dict.empty(
        key_type=types.int64,
        value_type=int_array,
    )
    for i in range(1,n):
        if c1D[sidx[i]]!=c1D[sidx[i-1]]:
            outt[c1D[sidx[start]]] = sidx[start:i]
            start = i
    outt[c1D[sidx[start]]] = sidx[start:]
    return outt

def numba2(cubes):
    c1D = np.ravel_multi_index(cubes.T, cubes.max(0)+1)    
    sidx = c1D.argsort()
    out = _numba2(sidx, c1D)
    return out

Tempi con cubes.npzdati -

In [4]: cubes = np.load('cubes.npz')['array']

In [5]: %timeit numpy1(cubes)
   ...: %timeit numba1(cubes)
   ...: %timeit numba2(cubes)
2.38 s ± 14.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2.13 s ± 25.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.8 s ± 5.95 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Alternativa n. 1: possiamo ottenere un'ulteriore accelerazione con numexprcalcoli per array di grandi dimensioni c1D, in questo modo -

import numexpr as ne

s0,s1 = cubes[:,0].max()+1,cubes[:,1].max()+1
d = {'s0':s0,'s1':s1,'c0':cubes[:,0],'c1':cubes[:,1],'c2':cubes[:,2]}
c1D = ne.evaluate('c0+c1*s0+c2*s0*s1',d)

Questo sarebbe applicabile in tutti i luoghi che richiedono c1D.


Grazie mille per la risposta! Non mi aspettavo che l'uso di cKDTree fosse possibile qui. Tuttavia, ci sono ancora alcuni problemi con il tuo # Approach1. La lunghezza dell'output è solo 915791. Immagino che si tratti di una sorta di conflitto tra dtypes int32eint64
mathfux il

@mathfux Suppongo di aver number of indices per group would be a constant numberraccolto i commenti. Sarebbe un presupposto sicuro? Inoltre, stai testando cubes.npzl'output di 915791?
Divakar,

Sì, certamente. Non ho testato il numero di indici per gruppo perché l'ordine dei nomi dei gruppi potrebbe essere diverso. Provo solo la lunghezza del dizionario di output cubes.npzed è stato 983234per gli altri approcci che ho suggerito.
Mathfux,

1
@mathfux Scopri il Approach #3 caso generico di un numero variabile di indici.
Divakar,

1
@mathfux Sì, in genere è necessario compensare se il minimo è inferiore a 0. Buona cattura della precisione!
Divakar

5

Potresti semplicemente iterare e aggiungere l'indice di ciascun elemento all'elenco corrispondente.

from collections import defaultdict

res = defaultdict(list)

for idx, elem in enumerate(cubes):
    #res[tuple(elem)].append(idx)
    res[elem.tobytes()].append(idx)

Il runtime può essere ulteriormente migliorato utilizzando tobytes () invece di convertire la chiave in una tupla.


Sto provando a fare una revisione del tempo di esecuzione al momento (per 20 milioni di punti). Sembra che la mia soluzione sia più efficiente in termini di tempo perché si evita l'iterazione. Sono d'accordo, il consumo di memoria è enorme.
Mathfux,

un'altra proposta res[tuple(elem)].append(idx)ha richiesto 50 secondi rispetto alla sua edizione res[elem[0], elem[1], elem[2]].append(idx)che ha richiesto 30 secondi.
Mathfux,

3

Puoi usare Cython:

%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True

import math
import cython as cy

cimport numpy as cnp


cpdef groupby_index_dict_cy(cnp.int32_t[:, :] arr):
    cdef cy.size_t size = len(arr)
    result = {}
    for i in range(size):
        key = arr[i, 0], arr[i, 1], arr[i, 2]
        if key in result:
            result[key].append(i)
        else:
            result[key] = [i]
    return result

ma non ti renderà più veloce di quello che fa Pandas, anche se dopo è il più veloce (e forse la numpy_indexsoluzione di base), e non ne risente. Una raccolta di ciò che è stato proposto finora è qui .

Nella macchina di OP che dovrebbe avvicinarsi a circa 12 secondi di tempo di esecuzione.


1
Grazie mille, lo proverò più tardi.
Mathfux,
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.