Aplicar múltiples funciones a múltiples grupos por columnas

221

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

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 groupby de la Serie. Y cuando un dict se pasa de manera similar a un grupo por DataFrame, espera que las claves sean los nombres de columna a los que se aplicará la función.

Lo que quiero hacer es aplicar múltiples 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 iterar a través de un objeto groupby). Tendré que cambiarlo para que repita todo el objeto groupby en una sola ejecución, pero me pregunto si hay una forma integrada en los 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 aggse llama desde un DataFrame).

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

Gracias

beardc
fuente
2
Si viene a esta pregunta en 2017+, consulte la respuesta a continuación para ver la forma idiomática de agregar varias columnas juntas. La respuesta actualmente seleccionada tiene varias desvalorizaciones, a saber, que ya no puede usar un diccionario de diccionarios para cambiar el nombre de las columnas en el resultado de un grupo.
Ted Petrou

Respuestas:

282

La segunda mitad de la respuesta actualmente aceptada está desactualizada y tiene dos desvalorizaciones. Primero y más importante, ya no puede pasar un diccionario de diccionarios al aggmétodo groupby. Segundo, nunca lo uses .ix.

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

df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd'))
df['group'] = [0, 0, 1, 1]
df

          a         b         c         d  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 los nombres de columna a las funciones de agregación sigue siendo una forma 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()})

              a                   b         c         d
            sum       max      mean       sum  <lambda>
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 le gusta el nombre de la columna lambda fea, puede usar una función normal y proporcionar un nombre personalizado al __name__atributo 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})

              a                   b         c             d
            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

Usar applyy devolver una serie

Ahora, si tenía varias columnas que necesitaban interactuar juntas, entonces no puede usarlas agg, lo que implícitamente pasa una Serie a la función de agregación. Cuando se usa applytodo el grupo como un DataFrame se pasa a la función.

Recomiendo hacer una única función personalizada que devuelva una serie de todas las agregaciones. Use el índice de la serie como etiquetas 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 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)

              a                   b       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
Ted Petrou
fuente
3
Me encanta el patrón de usar una función que devuelve una serie. Muy aseado.
Stephen McAteer
2
esta es la única forma en que he encontrado agregar un marco de datos a través de múltiples entradas de columna de forma simultánea (el ejemplo c_d anterior)
Blake
2
Estoy confundido por los resultados, tomando la suma de adentro del grupo, 0¿no debería ser así 0.418500 + 0.446069 = 0.864569? Lo mismo es cierto para otras celdas, los números no parecen sumar. ¿Podría ser un marco de datos subyacente ligeramente diferente que se utilizó en los ejemplos posteriores?
slackline
Con frecuencia uso .size () con groupby para ver la cantidad de registros. ¿Hay alguna manera de hacer esto usando el método agg: dict? Entiendo que podría contar un campo en particular, pero mi preferencia sería que el conteo sea independiente del campo.
Chris Decker
1
@slackline sí. Acabo de probarlo y funciona bien. Ted debe haber creado el marco varias veces diferentes y, dado que se creó a través de la generación de números aleatorios, los datos de df para generar realmente los datos fueron diferentes de los que se usaron en última instancia en los cálculos
Lucas H
166

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

In [28]: df
Out[28]:
          A         B         C         D         E  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]:
            A                   B
          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 de agregado funciona en Series, se pierden las referencias a los otros nombres de columna. Para evitar esto, puede hacer referencia al marco de datos completo e indexarlo utilizando los índices de grupo dentro de la función lambda.

Aquí hay una solución alternativa:

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]:
            A                   B         D
          sum      mean      prod  <lambda>
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' sumados.

ACTUALIZACIÓN 2:

Aquí hay un método que creo que hará todo lo que pidas. Primero haga una función lambda personalizada. A continuación, g hace referencia al grupo. Al agregar, g será una Serie. Pasando g.indexa df.ix[]selecciona el grupo actual de df. Luego pruebo si la columna C es menor que 0.5. La serie booleana devuelta se pasa a la g[]que selecciona solo aquellas filas que cumplen 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]:
            A                   B         D
          sum      mean      prod   my name
GRP
0    0.719580  0.359790  0.102004  0.204072
1    0.454824  0.227412  0.034060  0.570441
Zelazny7
fuente
Interesante, también puedo pasar un dict de {funcname: func}como valores en lugar de listas para mantener mis nombres personalizados. Pero en cualquier caso, no puedo pasar un lambdaque usa otras columnas (como lambda x: x['D'][x['C'] < 3].sum()arriba: "KeyError: 'D'"). ¿Alguna idea de si eso es posible?
beardc
He estado tratando de hacer exactamente eso, y me sale el errorKeyError: 'D'
Zelazny7
Genial, lo tengo para trabajar df['A'].ix[g.index][df['C'] < 0].sum(). Sin embargo, esto está empezando a ser bastante complicado: creo que para facilitar la lectura, puede ser preferible el bucle manual, además, no estoy seguro de que haya una manera de darle mi nombre preferido en el aggargumento (en lugar de <lambda>). Voy a mantener la esperanza de que alguien pueda conocer de una manera más sencilla ...
beardc
3
Puede pasar un dict para el valor de la columna {'D': {'my name':lambda function}}y hará que la clave dict interna sea el nombre de la columna.
Zelazny7
1
Creo que pandas ahora admite múltiples funciones aplicadas a un marco de datos agrupado: pandas.pydata.org/pandas-docs/stable/…
IanS
22

Como alternativa (principalmente en estética) a la respuesta de Ted Petrou, descubrí que prefería una lista un poco más compacta. No considere 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 primer / 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 una reminiscencia de dplyrtuberías y data.tablecomandos encadenados. Sin decir que son mejores, solo que me son más familiares. (Ciertamente reconozco el poder y, para muchos, la preferencia de utilizar deffunciones 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 la 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

          a         b         c         d  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
r2evans
fuente
2
Me gusta más esta respuesta. Esto es similar a las tuberías dplyr en R.
Renhuai
18

Pandas >= 0.25.0, agregaciones nombradas

Desde la versión pandas 0.25.0o superior, nos estamos alejando de la agregación y el cambio de nombre basados ​​en el diccionario, y nos estamos moviendo hacia agregaciones con nombre que aceptan a tuple. Ahora podemos agregar simultáneamente + cambiar el nombre a un nombre de columna más informativo:

Ejemplo :

df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd'))
df['group'] = [0, 0, 1, 1]

          a         b         c         d  group
0  0.521279  0.914988  0.054057  0.125668      0
1  0.426058  0.828890  0.784093  0.446211      0
2  0.363136  0.843751  0.184967  0.467351      1
3  0.241012  0.470053  0.358018  0.525032      1

Aplicar GroupBy.aggcon agregación con nombre:

df.groupby('group').agg(
             a_sum=('a', 'sum'),
             a_mean=('a', 'mean'),
             b_mean=('b', 'mean'),
             c_sum=('c', 'sum'),
             d_range=('d', lambda x: x.max() - x.min())
)

          a_sum    a_mean    b_mean     c_sum   d_range
group                                                  
0      0.947337  0.473668  0.871939  0.838150  0.320543
1      0.604149  0.302074  0.656902  0.542985  0.057681
Erfan
fuente
¿Me gustan estas agregaciones con nombre pero no puedo ver cómo se supone que debemos usarlas con varias columnas?
Simon Woodhead
Buena pregunta, no podría resolver esto, dudo que esto sea posible (todavía). Abrí un boleto para esto. Mantendré mi pregunta y usted actualizado. Gracias por señalar @SimonWoodhead
Erfan
4

Nuevo en la versión 0.25.0.

Para admitir la agregación específica de columna con control sobre los nombres de columna de salida, pandas acepta la sintaxis especial en GroupBy.agg () , conocida como "agregación con nombre" , donde

  • Las palabras clave son los nombres de las columnas de salida.
  • Los valores son tuplas cuyo primer elemento es la columna para seleccionar y el segundo elemento es la agregación que se aplica a esa columna. Pandas proporciona los pandas.NamedAgg namedtuple con los campos ['column', 'aggfunc'] para aclarar cuáles son los argumentos. Como de costumbre, la agregación puede ser un alias invocable o de cadena.
    In [79]: animals = pd.DataFrame({'kind': ['cat', 'dog', 'cat', 'dog'],
       ....:                         'height': [9.1, 6.0, 9.5, 34.0],
       ....:                         'weight': [7.9, 7.5, 9.9, 198.0]})
       ....: 

    In [80]: animals
    Out[80]: 
      kind  height  weight
    0  cat     9.1     7.9
    1  dog     6.0     7.5
    2  cat     9.5     9.9
    3  dog    34.0   198.0

    In [81]: animals.groupby("kind").agg(
       ....:     min_height=pd.NamedAgg(column='height', aggfunc='min'),
       ....:     max_height=pd.NamedAgg(column='height', aggfunc='max'),
       ....:     average_weight=pd.NamedAgg(column='weight', aggfunc=np.mean),
       ....: )
       ....: 
    Out[81]: 
          min_height  max_height  average_weight
    kind                                        
    cat          9.1         9.5            8.90
    dog          6.0        34.0          102.75

pandas.NamedAgg es solo una tupla nombrada. También se permiten tuplas simples.

    In [82]: animals.groupby("kind").agg(
       ....:     min_height=('height', 'min'),
       ....:     max_height=('height', 'max'),
       ....:     average_weight=('weight', np.mean),
       ....: )
       ....: 
    Out[82]: 
          min_height  max_height  average_weight
    kind                                        
    cat          9.1         9.5            8.90
    dog          6.0        34.0          102.75

No se pasan argumentos de palabras clave adicionales a las funciones de agregación. Solo los pares de (columna, aggfunc) deben pasarse como ** kwargs. Si sus funciones de agregación requieren argumentos adicionales, aplíquelos parcialmente con functools.partial ().

La agregación con nombre también es válida para las agrupaciones de grupo por serie. En este caso no hay selección de columna, por lo que los valores son solo las funciones.

    In [84]: animals.groupby("kind").height.agg(
       ....:     min_height='min',
       ....:     max_height='max',
       ....: )
       ....: 
    Out[84]: 
          min_height  max_height
    kind                        
    cat          9.1         9.5
    dog          6.0        34.0
exan
fuente
3

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

crear un marco de datos

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


   a  b  c
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 apply (usando múltiples columnas)

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 agregado (usando múltiples columnas)

Me gusta este enfoque ya que todavía puedo usar el agregado. Tal vez la gente me diga por qué se necesita aplicar para llegar a varias columnas al hacer agregaciones en grupos.

Parece obvio ahora, pero siempre que no seleccione la columna de interés directamente después del grupo , tendrá acceso a todas las columnas del marco de datos 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 todo, la magia

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

o de manera similar

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

Espero que esto ayude.

campo
fuente