2011-12-24 18 views
6

Ho due array 1D NumPy uguale lunghezza, e iddata, dove id è una sequenza di ripetizione, ordinato interi che definiscono sottofinestre su data. Ad esempio,gruppo da massima o minima in una matrice NumPy

id data 
1  2 
1  7 
1  3 
2  8 
2  9 
2 10 
3  1 
3 -10 

Vorrei aggregare data raggruppando in id e prendendo sia il massimo o minimo. In SQL, questa sarebbe una tipica query di aggregazione come SELECT MAX(data) FROM tablename GROUP BY id ORDER BY id. C'è un modo per evitare i loop di Python e farlo in modo vettorializzato, o devo scendere a C?

risposta

8

Ho visto alcune domande molto simili sullo stack traboccare negli ultimi giorni. Il seguente codice è molto simile all'implementazione di numpy.unique e poiché sfrutta il meccanismo di numpy sottostante, è molto probabilmente più veloce di qualsiasi cosa tu possa fare in un loop python.

import numpy as np 
def group_min(groups, data): 
    # sort with major key groups, minor key data 
    order = np.lexsort((data, groups)) 
    groups = groups[order] # this is only needed if groups is unsorted 
    data = data[order] 
    # construct an index which marks borders between groups 
    index = np.empty(len(groups), 'bool') 
    index[0] = True 
    index[1:] = groups[1:] != groups[:-1] 
    return data[index] 

#max is very similar 
def group_max(groups, data): 
    order = np.lexsort((data, groups)) 
    groups = groups[order] #this is only needed if groups is unsorted 
    data = data[order] 
    index = np.empty(len(groups), 'bool') 
    index[-1] = True 
    index[:-1] = groups[1:] != groups[:-1] 
    return data[index] 
+0

Grazie a @Bago, questo offre grandi prestazioni. Un'altra cosa che trovo utile qui è che sembra che lexsort metterà sempre i valori NaN alla fine delle sottofinestre. Quindi, se voglio trovare, per esempio, il massimo di ciascuna finestra escludendo NaN, posso capovolgere il segno dei dati, applicare la formula min, e quindi capovolgere il segno di nuovo sulla via d'uscita, con solo una piccola penalizzazione delle prestazioni. D'altra parte, se in realtà desidero che venga restituito un valore NaN se c'è un NaN in qualsiasi punto della sottofinestra, allora lo lascio così com'è. – Abiel

+0

Abiel, vedere np.nanmax - max ignorando NaNs – denis

+0

Soluzione piacevole. È fastidioso il tempo O (n log n) e la memoria O (n), quando sappiamo che può essere risolto in tempo O (n) e memoria O (k) per k bin. Forse numpy dovrebbe supportare 'binmax' e' bincount'. – joeln

0

penso che questo compie quello che stai cercando:

[max([val for idx,val in enumerate(data) if id[idx] == k]) for k in sorted(set(id))] 

Per l'elenco di comprensione esterno, da destra a, set(id) raggruppa i id s, sorted() li ordina, for k ... itera su di loro, e max sinistra prende il massimo di, in questo caso, un'altra comprensione di lista. Passando quindi a comprendere la lista interna: enumerate(data) restituisce sia l'indice che il valore da data, if id[val] == k preleva i membri data corrispondenti a idk.

Questo itera l'intero elenco data per ogni id. Con alcune pre-elaborazione in sottoliste, potrebbe essere possibile velocizzarlo, ma non sarà un one-liner.

6

in puro Python:

from itertools import groupby, imap, izip 
from operator import itemgetter as ig 

print [max(imap(ig(1), g)) for k, g in groupby(izip(id, data), key=ig(0))] 
# -> [7, 10, 1] 

Una variante:

print [data[id==i].max() for i, _ in groupby(id)] 
# -> [7, 10, 1] 

Sulla base @Bago's answer:

import numpy as np 

# sort by `id` then by `data` 
ndx = np.lexsort(keys=(data, id)) 
id, data = id[ndx], data[ndx] 

# get max() 
print data[np.r_[np.diff(id), True].astype(np.bool)] 
# -> [ 7 10 1] 

Se pandas è installato:

from pandas import DataFrame 

df = DataFrame(dict(id=id, data=data)) 
print df.groupby('id')['data'].max() 
# id 
# 1 7 
# 2 10 
# 3 1 
+0

Grazie @JF per tutti i diversi approcci. Ovviamente la soluzione numpy è più veloce del Python puro, ma sono rimasto sorpreso dalla velocità con cui è stata la prima soluzione Python pura. Sono curioso delle prestazioni relative della soluzione panda; sfortunatamente non ho potuto testarlo perché ottengo un NameError quando provo ad importare DataFrame usando l'ultima build. – Abiel

+0

@Abiel: 'pandas .__ versione __ == '0.6.1'' – jfs

+2

+1 per Panda. Penso che sia il più semplice nella sua leggibilità. –

0

La seguente soluzione richiede solo una specie sui dati (non un lexsort) e non richiede trovando i confini tra i gruppi. Essa si basa sul fatto che se o è un array di indici in r poi r[o] = x riempirà r con l'ultimo valore x per ogni valore di o, tale che r[[0, 0]] = [1, 2] tornerà r[0] = 2. Essa richiede che i gruppi sono interi da 0 a numero di gruppi - 1, come per numpy.bincount, e che ci sia un valore per ogni gruppo:

def group_min(groups, data): 
    n_groups = np.max(groups) + 1 
    result = np.empty(n_groups) 
    order = np.argsort(data)[::-1] 
    result[groups.take(order)] = data.take(order) 
    return result 

def group_max(groups, data): 
    n_groups = np.max(groups) + 1 
    result = np.empty(n_groups) 
    order = np.argsort(data) 
    result[groups.take(order)] = data.take(order) 
    return result 
0

Una risposta un po 'più veloce e più generale di già accettato uno; come la risposta di joeln, evita il lexsort più costoso, e funziona per gli ufunc arbitrari.Inoltre, richiede solo che le chiavi siano ordinabili, piuttosto che essere in un intervallo specifico. La risposta accettata potrebbe comunque essere più veloce, considerando che il massimo/minimo non è calcolato in modo esplicito. La capacità di ignorare i nans della soluzione accettata è pulita; ma si può anche semplicemente assegnare ai valori nan una chiave fittizia.

import numpy as np 

def group(key, value, operator=np.add): 
    """ 
    group the values by key 
    any ufunc operator can be supplied to perform the reduction (np.maximum, np.minimum, np.substract, and so on) 
    returns the unique keys, their corresponding per-key reduction over the operator, and the keycounts 
    """ 
    #upcast to numpy arrays 
    key = np.asarray(key) 
    value = np.asarray(value) 
    #first, sort by key 
    I = np.argsort(key) 
    key = key[I] 
    value = value[I] 
    #the slicing points of the bins to sum over 
    slices = np.concatenate(([0], np.where(key[:-1]!=key[1:])[0]+1)) 
    #first entry of each bin is a unique key 
    unique_keys = key[slices] 
    #reduce over the slices specified by index 
    per_key_sum = operator.reduceat(value, slices) 
    #number of counts per key is the difference of our slice points. cap off with number of keys for last bin 
    key_count = np.diff(np.append(slices, len(key))) 
    return unique_keys, per_key_sum, key_count 


names = ["a", "b", "b", "c", "d", "e", "e"] 
values = [1.2, 4.5, 4.3, 2.0, 5.67, 8.08, 9.01] 

unique_keys, reduced_values, key_count = group(names, values) 
print 'per group mean' 
print reduced_values/key_count 
unique_keys, reduced_values, key_count = group(names, values, np.minimum) 
print 'per group min' 
print reduced_values 
unique_keys, reduced_values, key_count = group(names, values, np.maximum) 
print 'per group max' 
print reduced_values 
3

Sono abbastanza nuovo per Python e Numpy ma, sembra che è possibile utilizzare il metodo di ufunc s piuttosto che reduceat.at:

import numpy as np 
data_id = np.array([0,0,0,1,1,1,1,2,2,2,3,3,3,4,5,5,5]) 
data_val = np.random.rand(len(data_id)) 
ans = np.empty(data_id[-1]+1) # might want to use max(data_id) and zeros instead 
np.maximum.at(ans,data_id,data_val) 

Per esempio:

data_val = array([ 0.65753453, 0.84279716, 0.88189818, 0.18987882, 0.49800668, 
    0.29656994, 0.39542769, 0.43155428, 0.77982853, 0.44955868, 
    0.22080219, 0.4807312 , 0.9288989 , 0.10956681, 0.73215416, 
    0.33184318, 0.10936647]) 
ans = array([ 0.98969952, 0.84044947, 0.63460516, 0.92042078, 0.75738113, 
    0.37976055]) 

Ovviamente questo ha senso solo se i tuoi valori data_id sono adatti per l'uso come indici (cioè numeri interi non negativi e non enormi ... presumibilmente se sono grandi/sparse, è possibile inizializzare ans utilizzando np.unique(data_id) o qualcosa del genere).

Devo sottolineare che lo data_id non ha bisogno di essere ordinato.

1

Ive ha confezionato una versione della mia risposta precedente nel pacchetto numpy_indexed; è bello averlo tutto racchiuso e testato in un'interfaccia pulita; in più ha molto di più funzionalità e:

import numpy_indexed as npi 
group_id, group_max_data = group_by(id).max(data) 

E così via

Problemi correlati