Aplicar múltiples funciones a múltiples columnas grupales.

Los documentos muestran cómo aplicar múltiples funciones en un objeto groupby a la vez utilizando un dict con los nombres de las columnas de salida como claves:

In [563]: grouped['D'].agg({'result1' : np.sum, .....: 'result2' : np.mean}) .....: Out[563]: result2 result1 A bar -0.579846 -1.739537 foo -0.280588 -1.402938 

Sin embargo, esto solo funciona en un objeto Group by Series. Y cuando un dict se pasa de manera similar a un grupo por DataFrame, se espera que las claves sean los nombres de columna a los que se aplicará la función.

Lo que quiero hacer es aplicar varias funciones a varias columnas (pero ciertas columnas se operarán varias veces). Además, algunas funciones dependerán de otras columnas en el objeto groupby (como las funciones sumif). Mi solución actual es ir columna por columna y hacer algo como el código anterior, usando lambdas para funciones que dependen de otras filas. Pero esto lleva mucho tiempo (creo que toma mucho tiempo recorrer un objeto groupby). Tendré que cambiarlo para que recorra todo el grupo por objeto en una sola ejecución, pero me pregunto si hay una forma integrada en pandas para hacer esto de manera limpia.

Por ejemplo, he intentado algo como

 grouped.agg({'C_sum' : lambda x: x['C'].sum(), 'C_std': lambda x: x['C'].std(), 'D_sum' : lambda x: x['D'].sum()}, 'D_sumifC3': lambda x: x['D'][x['C'] == 3].sum(), ...) 

pero como se esperaba, obtengo un KeyError (ya que las claves deben ser una columna si se llama a agg desde un DataFrame).

¿Hay alguna forma integrada de hacer lo que me gustaría hacer, o la posibilidad de que se pueda agregar esta funcionalidad, o simplemente tendré que recorrer el grupo manualmente?

Gracias

La segunda mitad de la respuesta actualmente aceptada está desactualizada y tiene dos desaprobaciones. Lo primero y lo más importante es que ya no puede pasar un diccionario de diccionarios al método colectivo de grupo. En segundo lugar, nunca utilice .ix .

Si desea trabajar con dos columnas separadas al mismo tiempo, sugeriría usar el método de apply que implícitamente pasa un DataFrame a la función aplicada. Usemos un dataframe similar al de arriba

 df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd')) df['group'] = [0, 0, 1, 1] df abcd group 0 0.418500 0.030955 0.874869 0.145641 0 1 0.446069 0.901153 0.095052 0.487040 0 2 0.843026 0.936169 0.926090 0.041722 1 3 0.635846 0.439175 0.828787 0.714123 1 

Un diccionario asignado de nombres de columna a funciones de agregación sigue siendo una manera perfectamente buena de realizar una agregación.

 df.groupby('group').agg({'a':['sum', 'max'], 'b':'mean', 'c':'sum', 'd': lambda x: x.max() - x.min()}) abcd sum max mean sum  group 0 0.864569 0.446069 0.466054 0.969921 0.341399 1 1.478872 0.843026 0.687672 1.754877 0.672401 

Si no te gusta ese nombre de columna feo lambda, puedes usar una función normal y proporcionar un nombre personalizado al atributo __name__ especial como este:

 def max_min(x): return x.max() - x.min() max_min.__name__ = 'Max minus Min' df.groupby('group').agg({'a':['sum', 'max'], 'b':'mean', 'c':'sum', 'd': max_min}) abcd sum max mean sum Max minus Min group 0 0.864569 0.446069 0.466054 0.969921 0.341399 1 1.478872 0.843026 0.687672 1.754877 0.672401 

Usando apply y devolver una serie

Ahora, si tenía varias columnas que necesitaban interactuar juntas, entonces no puede usar agg , que implícitamente pasa una serie a la función de agregación. Cuando se usa, se apply todo el grupo, ya que un DataFrame se pasa a la función.

Recomiendo hacer una única función personalizada que devuelva una serie de todas las agregaciones. Utilice el índice de Series como tags para las nuevas columnas:

 def f(x): d = {} d['a_sum'] = x['a'].sum() d['a_max'] = x['a'].max() d['b_mean'] = x['b'].mean() d['c_d_prodsum'] = (x['c'] * x['d']).sum() return pd.Series(d, index=['a_sum', 'a_max', 'b_mean', 'c_d_prodsum']) df.groupby('group').apply(f) a_sum a_max b_mean c_d_prodsum group 0 0.864569 0.446069 0.466054 0.173711 1 1.478872 0.843026 0.687672 0.630494 

Si está enamorado de los MultiIndexes, aún puede devolver una Serie con una como esta:

  def f_mi(x): d = [] d.append(x['a'].sum()) d.append(x['a'].max()) d.append(x['b'].mean()) d.append((x['c'] * x['d']).sum()) return pd.Series(d, index=[['a', 'a', 'b', 'c_d'], ['sum', 'max', 'mean', 'prodsum']]) df.groupby('group').apply(f_mi) ab c_d sum max mean prodsum group 0 0.864569 0.446069 0.466054 0.173711 1 1.478872 0.843026 0.687672 0.630494 

Para la primera parte, puede pasar un dictado de nombres de columna para las claves y una lista de funciones para los valores:

 In [28]: df Out[28]: ABCDE GRP 0 0.395670 0.219560 0.600644 0.613445 0.242893 0 1 0.323911 0.464584 0.107215 0.204072 0.927325 0 2 0.321358 0.076037 0.166946 0.439661 0.914612 1 3 0.133466 0.447946 0.014815 0.130781 0.268290 1 In [26]: f = {'A':['sum','mean'], 'B':['prod']} In [27]: df.groupby('GRP').agg(f) Out[27]: AB sum mean prod GRP 0 0.719580 0.359790 0.102004 1 0.454824 0.227412 0.034060 

ACTUALIZACIÓN 1:

Debido a que la función agregada funciona en Series, las referencias a los otros nombres de columna se pierden. Para solucionar esto, puede hacer referencia al dataframe completo e indexarlo utilizando los índices de grupo dentro de la función lambda.

Aquí hay una solución hacky:

 In [67]: f = {'A':['sum','mean'], 'B':['prod'], 'D': lambda g: df.loc[g.index].E.sum()} In [69]: df.groupby('GRP').agg(f) Out[69]: ABD sum mean prod  GRP 0 0.719580 0.359790 0.102004 1.170219 1 0.454824 0.227412 0.034060 1.182901 

Aquí, la columna ‘D’ resultante se compone de los valores ‘E’ sumdos.

ACTUALIZACIÓN 2:

Aquí hay un método que creo que hará todo lo que pidas. Primero haz una función lambda personalizada. A continuación, g hace referencia al grupo. Al agregar, g será una serie. Al pasar g.index a df.ix[] selecciona el grupo actual de df. Entonces pruebo si la columna C es menor que 0.5. La serie booleana devuelta se pasa a g[] que selecciona solo aquellas filas que cumplen con los criterios.

 In [95]: cust = lambda g: g[df.loc[g.index]['C'] < 0.5].sum() In [96]: f = {'A':['sum','mean'], 'B':['prod'], 'D': {'my name': cust}} In [97]: df.groupby('GRP').agg(f) Out[97]: ABD sum mean prod my name GRP 0 0.719580 0.359790 0.102004 0.204072 1 0.454824 0.227412 0.034060 0.570441 

Como alternativa (sobre todo en estética) a la respuesta de Ted Petrou, descubrí que prefería una lista algo más compacta. Por favor, no consideres aceptarlo, es solo un comentario mucho más detallado sobre la respuesta de Ted, más el código / datos. Python / pandas no es mi primera / mejor, pero encontré esto para leer bien:

 df.groupby('group') \ .apply(lambda x: pd.Series({ 'a_sum' : x['a'].sum(), 'a_max' : x['a'].max(), 'b_mean' : x['b'].mean(), 'c_d_prodsum' : (x['c'] * x['d']).sum() }) ) a_sum a_max b_mean c_d_prodsum group 0 0.530559 0.374540 0.553354 0.488525 1 1.433558 0.832443 0.460206 0.053313 

Me parece más evocador de dplyr tuberías data.table y los comandos encadenados data.table . No quiere decir que sean mejores, solo que me son más familiares. (Ciertamente reconozco el poder y, para muchos, la preferencia de usar funciones de def más formalizadas para este tipo de operaciones. Esta es solo una alternativa, no necesariamente mejor).


Generé datos de la misma manera que Ted, agregaré una semilla para reproducibilidad.

 import numpy as np np.random.seed(42) df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd')) df['group'] = [0, 0, 1, 1] df abcd group 0 0.374540 0.950714 0.731994 0.598658 0 1 0.156019 0.155995 0.058084 0.866176 0 2 0.601115 0.708073 0.020584 0.969910 1 3 0.832443 0.212339 0.181825 0.183405 1 

La respuesta de Ted es asombrosa. Terminé usando una versión más pequeña de eso en caso de que alguien esté interesado. Es útil cuando busca una agregación que depende de los valores de varias columnas:

crear un dataframe

 df=pd.DataFrame({'a': [1,2,3,4,5,6], 'b': [1,1,0,1,1,0], 'c': ['x','x','y','y','z','z']}) abc 0 1 1 x 1 2 1 x 2 3 0 y 3 4 1 y 4 5 1 z 5 6 0 z 

agrupando y agregando con aplicar (usando columnas múltiples)

 df.groupby('c').apply(lambda x: x['a'][(x['a']>1) & (x['b']==1)].mean()) c x 2.0 y 4.0 z 5.0 

agrupación y agregación con agregados (utilizando varias columnas)

Me gusta este enfoque ya que todavía puedo usar el agregado. Quizás la gente me permita saber por qué es necesario aplicar para acceder a varias columnas al hacer agregaciones en grupos.

Parece obvio ahora, pero mientras no seleccione la columna de interés directamente después del grupo , tendrá acceso a todas las columnas del dataframe desde su función de agregación.

Solo acceso a la columna seleccionada.

 df.groupby('c')['a'].aggregate(lambda x: x[x>1].mean()) 

Acceso a todas las columnas ya que la selección es después de toda la magia.

 df.groupby('c').aggregate(lambda x: x[(x['a']>1) & (x['b']==1)].mean())['a'] 

o similarmente

 df.groupby('c').aggregate(lambda x: x['a'][(x['a']>1) & (x['b']==1)].mean()) 

Espero que esto ayude.