2011-12-24 21 views
6

Tengo dos matrices 1D numpy de igual longitud, id y data, donde id es una secuencia de repetición, ordenado números enteros que definen sub-ventanas en data. Por ejemplo,Grupo por max o min en una matriz numpy

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

me gustaría a agregarse data al agrupar en el id y teniendo o bien el máximo o el min. En SQL, esta sería una consulta de agregación típica como SELECT MAX(data) FROM tablename GROUP BY id ORDER BY id. ¿Hay alguna forma de evitar los bucles de Python y hacer esto de forma vectorializada, o tengo que bajar a C?

Respuesta

8

He estado viendo algunas preguntas muy similares sobre el desbordamiento de pila en los últimos días. El siguiente código es muy similar a la implementación de numpy.unique y porque se aprovecha de la maquinaria Numpy subyacente, es probable que sea más rápido que cualquier cosa que se pueda hacer en un ciclo de 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

Gracias @Bago, esto ofrece un gran rendimiento. Otra cosa que encuentro útil aquí es que parece que lexsort siempre colocará valores NaN al final de las subventanas. Por lo tanto, si quiero encontrar, por ejemplo, el máximo de cada ventana excluyendo NaN, puedo voltear el signo de los datos, aplicar la fórmula mínima y luego voltear el signo nuevamente en el camino de salida, con solo una pequeña penalización de rendimiento. Por otro lado, si de hecho quiero que se devuelva un valor NaN si hay un NaN en alguna parte en la ventana secundaria, entonces lo dejo tal como está. – Abiel

+0

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

+0

Buena solución. Es molesto que el tiempo O (n log n) y la memoria O (n), cuando sabemos que se puede resolver en el tiempo O (n) y la memoria O (k) para k bandejas. Quizás numpy debería ser compatible con 'binmax' y' bincount'. – joeln

0

creo que esto logra lo que estás buscando:

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

Para la lista por comprensión externa, de derecha a set(id), agrupa los id s, sorted() ordena ellos, for k ... itera sobre ellos, y max izquierda toma el máximo de, en este caso, otra lista de comprensión. Así que pasar a la comprensión de la lista interna: enumerate(data) devuelve tanto el índice como el valor de data, if id[val] == k selecciona los miembros data correspondientes a idk.

Esto itera sobre la lista completa data para cada id. Con algún preprocesamiento en las sublistas, es posible que se acelere, pero no será de una sola línea.

6

En Python puro:

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 variación:

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

Basado en @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] 

Si pandas está instalado:

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

Gracias @JF para todos los diferentes enfoques. Por supuesto, la solución numpy es más rápida que la Python pura, pero me sorprendió lo rápido que fue tu primera solución pura de Python. Tengo curiosidad sobre el rendimiento relativo de la solución pandas; desafortunadamente no pude probarlo porque recibo un NameError cuando trato de importar DataFrame usando la última compilación. – Abiel

+0

@Abiel: 'pandas .__ versión __ == '0.6.1'' – jfs

+2

+1 para pandas. Creo que el más simple en su legibilidad. –

0

la siguiente solución sólo se requiere una especie en los datos (no un lexsort) y no requiere la búsqueda de los límites entre los grupos. Se basa en el hecho de que si o es un conjunto de índices en r continuación r[o] = x llenará r con el último valor x para cada valor de o, de tal manera que r[[0, 0]] = [1, 2] volverá r[0] = 2. Se requiere que sus grupos son números enteros de 0 a número de grupos - 1, como para numpy.bincount, y que no es un valor para cada grupo:

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 respuesta un poco más rápido y más general que la ya aceptada uno; al igual que la respuesta de joeln, evita el lexsort más caro y funciona para ufuncs arbitrarios.Además, solo exige que las claves sean ordenables, en lugar de ser entradas en un rango específico. Sin embargo, la respuesta aceptada puede ser aún más rápida, ya que el máximo/mínimo no se calcula explícitamente. La capacidad de ignorar nans de la solución aceptada es clara; pero también se puede simplemente asignar a los valores de nan una clave ficticia.

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

Soy bastante nuevo en Python y Numpy pero, parece que se puede utilizar el método de ufunc s en lugar de 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) 

Por ejemplo:

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]) 

Por supuesto, esto solo tiene sentido si sus valores data_id son adecuados para usar como índices (es decir, enteros no negativos y no enormes ... presumiblemente si son grandes/dispersos, puede inicializar ans usando np.unique(data_id) o algo similar).

Debo señalar que el data_id en realidad no necesita ser ordenado.

1

He incluido una versión de mi respuesta anterior en el paquete numpy_indexed; es bueno tener todo esto envuelto y probado en una interfaz ordenada; además de que tiene una funcionalidad mucho más así:

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

Y así sucesivamente

Cuestiones relacionadas