Aplicar vs transformar en un objeto de grupo

174

Considere el siguiente marco de datos:

     A      B         C         D
0  foo    one  0.162003  0.087469
1  bar    one -1.156319 -1.526272
2  foo    two  0.833892 -1.666304
3  bar  three -2.026673 -0.322057
4  foo    two  0.411452 -0.954371
5  bar    two  0.765878 -0.095968
6  foo    one -0.654890  0.678091
7  foo  three -1.789842 -1.130922

Los siguientes comandos funcionan:

> df.groupby('A').apply(lambda x: (x['C'] - x['D']))
> df.groupby('A').apply(lambda x: (x['C'] - x['D']).mean())

pero ninguno de los siguientes trabajos:

> df.groupby('A').transform(lambda x: (x['C'] - x['D']))
ValueError: could not broadcast input array from shape (5) into shape (5,3)

> df.groupby('A').transform(lambda x: (x['C'] - x['D']).mean())
 TypeError: cannot concatenate a non-NDFrame object

¿Por qué? El ejemplo en la documentación parece sugerir que llamar transforma un grupo le permite a uno realizar el procesamiento de operaciones en filas:

# Note that the following suggests row-wise operation (x.mean is the column mean)
zscore = lambda x: (x - x.mean()) / x.std()
transformed = ts.groupby(key).transform(zscore)

En otras palabras, pensé que transform es esencialmente un tipo específico de aplicación (el que no se agrega). Donde me equivoco

Como referencia, a continuación se muestra la construcción del marco de datos original anterior:

df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',
                          'foo', 'bar', 'foo', 'foo'],
                   'B' : ['one', 'one', 'two', 'three',
                         'two', 'two', 'one', 'three'],
                   'C' : randn(8), 'D' : randn(8)})
Amelio Vazquez-Reina
fuente
1
La función pasada a transformdebe devolver un número, una fila o la misma forma que el argumento. si es un número, el número se establecerá en todos los elementos del grupo, si es una fila, se transmitirá a todas las filas del grupo. En su código, la función lambda devuelve una columna que no se puede transmitir al grupo.
HYRY
1
Gracias @HYRY, pero estoy confundido. Si observa el ejemplo en la documentación que copié anteriormente (es decir, con zscore), transformrecibe una función lambda que supone que cada uno xes un elemento dentro del group, y también devuelve un valor por elemento en el grupo. ¿Qué me estoy perdiendo?
Amelio Vazquez-Reina
Para aquellos que buscan una solución extremadamente detallada, vea esta a continuación .
Ted Petrou
@TedPetrou: el tl; dr de eso es: 1) applypasa todo el df, pero transformpasa cada columna individualmente como una Serie. 2) applypuede devolver cualquier salida de forma (escalar / Serie / Marco de datos / matriz / lista ...), mientras que transformdebe devolver una secuencia (Serie 1D / matriz / lista) de la misma longitud que el grupo. Es por eso que el OP apply()no necesita transform(). Esta es una buena pregunta ya que el documento no explicó ambas diferencias claramente. (similar a la distinción apply/map/applymapu otras cosas ...)
smci

Respuestas:

146

Dos diferencias principales entre applyytransform

Hay dos diferencias principales entre los métodos groupby transformy apply.

  • Entrada:
    • applyimplícitamente pasa todas las columnas para cada grupo como un DataFrame a la función personalizada.
    • while transformpasa cada columna para cada grupo individualmente como una Serie a la función personalizada.
  • Salida:
    • La función personalizada pasada a applypuede devolver un escalar, o una Serie o un Marco de datos (o una matriz numpy o incluso una lista) .
    • La función personalizada pasada a transformdebe devolver una secuencia (una serie, matriz o lista unidimensional) de la misma longitud que el grupo .

Por lo tanto, transformfunciona solo en una serie a la vez yapply funciona en todo el DataFrame a la vez.

Inspeccionar la función personalizada

Puede ayudar bastante inspeccionar la entrada a su función personalizada que se pasa applyotransform .

Ejemplos

Creemos algunos datos de muestra e inspeccionemos los grupos para que puedan ver de qué estoy hablando:

import pandas as pd
import numpy as np
df = pd.DataFrame({'State':['Texas', 'Texas', 'Florida', 'Florida'], 
                   'a':[4,5,1,3], 'b':[6,10,3,11]})

     State  a   b
0    Texas  4   6
1    Texas  5  10
2  Florida  1   3
3  Florida  3  11

Creemos una función personalizada simple que imprima el tipo del objeto pasado implícitamente y luego genere un error para que se pueda detener la ejecución.

def inspect(x):
    print(type(x))
    raise

Ahora pasemos esta función tanto al groupby applycomo a los transformmétodos para ver qué objeto se le pasa:

df.groupby('State').apply(inspect)

<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.frame.DataFrame'>
RuntimeError

Como puede ver, un DataFrame se pasa a la inspectfunción. Quizás se pregunte por qué el tipo, DataFrame, se imprimió dos veces. Pandas dirige el primer grupo dos veces. Hace esto para determinar si hay una manera rápida de completar el cálculo o no. Este es un detalle menor del que no debe preocuparse.

Ahora, hagamos lo mismo con transform

df.groupby('State').transform(inspect)
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
RuntimeError

Se pasa una serie, un objeto Pandas totalmente diferente.

Por lo tanto, transformsolo se permite trabajar con una sola serie a la vez. Es no imposible para que actúe en dos columnas al mismo tiempo. Entonces, si intentamos restar la columna adesde el binterior de nuestra función personalizada, obtendríamos un error transform. Vea abajo:

def subtract_two(x):
    return x['a'] - x['b']

df.groupby('State').transform(subtract_two)
KeyError: ('a', 'occurred at index a')

Obtenemos un KeyError cuando los pandas intentan encontrar el índice de la Serie aque no existe. Puede completar esta operación applyya que tiene todo el DataFrame:

df.groupby('State').apply(subtract_two)

State     
Florida  2   -2
         3   -8
Texas    0   -2
         1   -5
dtype: int64

El resultado es una serie y un poco confuso ya que se mantiene el índice original, pero tenemos acceso a todas las columnas.


Mostrar el objeto pandas pasado

Puede ayudar aún más mostrar todo el objeto pandas dentro de la función personalizada, para que pueda ver exactamente con qué está operando. Puede usar printdeclaraciones de Me gusta usar la displayfunción del IPython.displaymódulo para que los marcos de datos se generen correctamente en HTML en un cuaderno jupyter:

from IPython.display import display
def subtract_two(x):
    display(x)
    return x['a'] - x['b']

Captura de pantalla: ingrese la descripción de la imagen aquí


La transformación debe devolver una secuencia dimensional única del mismo tamaño que el grupo

La otra diferencia es que transformdebe devolver una secuencia dimensional única del mismo tamaño que el grupo. En este caso particular, cada grupo tiene dos filas, por lo que transformdebe devolver una secuencia de dos filas. Si no es así, se genera un error:

def return_three(x):
    return np.array([1, 2, 3])

df.groupby('State').transform(return_three)
ValueError: transform must return a scalar value for each group

El mensaje de error no es realmente descriptivo del problema. Debe devolver una secuencia de la misma longitud que el grupo. Entonces, una función como esta funcionaría:

def rand_group_len(x):
    return np.random.rand(len(x))

df.groupby('State').transform(rand_group_len)

          a         b
0  0.962070  0.151440
1  0.440956  0.782176
2  0.642218  0.483257
3  0.056047  0.238208

Devolver un solo objeto escalar también funciona para transform

Si devuelve un solo escalar de su función personalizada, transformlo usará para cada una de las filas del grupo:

def group_sum(x):
    return x.sum()

df.groupby('State').transform(group_sum)

   a   b
0  9  16
1  9  16
2  4  14
3  4  14
Ted Petrou
fuente
3
npno está definido. Supongo que los principiantes agradecerían si incluyera import numpy as npen su respuesta.
Qaswed
187

Como me sentí igualmente confundido con la .transformoperación vs. .applyencontré algunas respuestas que arrojan algo de luz sobre el tema. Esta respuesta, por ejemplo, fue muy útil.

Mi conclusión hasta ahora es que .transformfuncionará (o tratará) con Series(columnas) aisladas unas de otras . Lo que esto significa es que en tus dos últimas llamadas:

df.groupby('A').transform(lambda x: (x['C'] - x['D']))
df.groupby('A').transform(lambda x: (x['C'] - x['D']).mean())

Solicitó .transformtomar valores de dos columnas y 'it' en realidad no los 've' a ambos al mismo tiempo (por así decirlo). transformmirará las columnas del marco de datos una por una y devolverá una serie (o grupo de series) 'hecha' de escalares que se repiten varias len(input_column)veces.

Entonces, este escalar, que debería usarse .transformpara hacer el Seriesresultado de alguna función de reducción aplicada en una entrada Series(y solo en UNA serie / columna a la vez).

Considere este ejemplo (en su marco de datos):

zscore = lambda x: (x - x.mean()) / x.std() # Note that it does not reference anything outside of 'x' and for transform 'x' is one column.
df.groupby('A').transform(zscore)

rendirá:

       C      D
0  0.989  0.128
1 -0.478  0.489
2  0.889 -0.589
3 -0.671 -1.150
4  0.034 -0.285
5  1.149  0.662
6 -1.404 -0.907
7 -0.509  1.653

Que es exactamente lo mismo que si lo usaras solo en una columna a la vez:

df.groupby('A')['C'].transform(zscore)

flexible:

0    0.989
1   -0.478
2    0.889
3   -0.671
4    0.034
5    1.149
6   -1.404
7   -0.509

Tenga .applyen cuenta que en el último ejemplo ( df.groupby('A')['C'].apply(zscore)) funcionaría exactamente de la misma manera, pero fallaría si intentara usarlo en un marco de datos:

df.groupby('A').apply(zscore)

da error:

ValueError: operands could not be broadcast together with shapes (6,) (2,)

Entonces, ¿dónde más es .transformútil? El caso más simple es tratar de asignar los resultados de la función de reducción al marco de datos original.

df['sum_C'] = df.groupby('A')['C'].transform(sum)
df.sort('A') # to clearly see the scalar ('sum') applies to the whole column of the group

flexible:

     A      B      C      D  sum_C
1  bar    one  1.998  0.593  3.973
3  bar  three  1.287 -0.639  3.973
5  bar    two  0.687 -1.027  3.973
4  foo    two  0.205  1.274  4.373
2  foo    two  0.128  0.924  4.373
6  foo    one  2.113 -0.516  4.373
7  foo  three  0.657 -1.179  4.373
0  foo    one  1.270  0.201  4.373

Tratando el mismo con .applydaría NaNsen sum_C. Porque .applydevolvería un reducido Series, que no sabe cómo transmitir de nuevo:

df.groupby('A')['C'].apply(sum)

dando:

A
bar    3.973
foo    4.373

También hay casos en los que .transformse usa para filtrar los datos:

df[df.groupby(['B'])['D'].transform(sum) < -1]

     A      B      C      D
3  bar  three  1.287 -0.639
7  foo  three  0.657 -1.179

Espero que esto agregue un poco más de claridad.

Cebador
fuente
44
DIOS MIO. La diferencia es muy sutil.
Dawei
3
.transform()también podría usarse para completar valores perdidos. Especialmente si desea transmitir la media grupal o la estadística grupal a los NaNvalores de ese grupo. Desafortunadamente, la documentación de los pandas tampoco me fue útil.
matemáticas cibernéticas el
Creo que en el último caso, .groupby().filter()hace lo mismo. Gracias por tu explicación, .apply()y también .transform()me confunde mucho.
Jiaxiang
eso explica por qué df.groupby().transform()no puede funcionar para un subgrupo df, siempre obtengo el error ValueError: transform must return a scalar value for each groupporque transformve las columnas una por una
jerrytim
Realmente me gustó el último ejemplo .transform utilizado para filtrar los datos. ¡Super bonito!
Rishi Jain
13

Voy a usar un fragmento muy simple para ilustrar la diferencia:

test = pd.DataFrame({'id':[1,2,3,1,2,3,1,2,3], 'price':[1,2,3,2,3,1,3,1,2]})
grouping = test.groupby('id')['price']

El DataFrame se ve así:

    id  price   
0   1   1   
1   2   2   
2   3   3   
3   1   2   
4   2   3   
5   3   1   
6   1   3   
7   2   1   
8   3   2   

Hay 3 ID de clientes en esta tabla, cada cliente realizó tres transacciones y pagó 1,2,3 dólares cada vez.

Ahora, quiero encontrar el pago mínimo realizado por cada cliente. Hay dos formas de hacerlo:

  1. Utilizando apply :

    grouping.min ()

El regreso se ve así:

id
1    1
2    1
3    1
Name: price, dtype: int64

pandas.core.series.Series # return type
Int64Index([1, 2, 3], dtype='int64', name='id') #The returned Series' index
# lenght is 3
  1. Utilizando transform :

    grouping.transform (min)

El regreso se ve así:

0    1
1    1
2    1
3    1
4    1
5    1
6    1
7    1
8    1
Name: price, dtype: int64

pandas.core.series.Series # return type
RangeIndex(start=0, stop=9, step=1) # The returned Series' index
# length is 9    

Ambos métodos devuelven un Seriesobjeto, pero el lengthdel primero es 3 y ellength del segundo es 9.

Si desea responder What is the minimum price paid by each customer, entonces el applymétodo es el más adecuado para elegir.

Si quieres responder What is the difference between the amount paid for each transaction vs the minimum payment, entonces quieres usar transform, porque:

test['minimum'] = grouping.transform(min) # ceates an extra column filled with minimum payment
test.price - test.minimum # returns the difference for each row

Apply no funciona aquí simplemente porque devuelve una Serie de tamaño 3, pero la longitud del df original es 9. No puede integrarlo fácilmente al df original.

Cheng
fuente
3
¡Creo que esta es una gran respuesta! ¡Gracias por tomarse el tiempo de responder más de cuatro años después de la pregunta!
Benjamin Dubreu
4
tmp = df.groupby(['A'])['c'].transform('mean')

es como

tmp1 = df.groupby(['A']).agg({'c':'mean'})
tmp = df['A'].map(tmp1['c'])

o

tmp1 = df.groupby(['A'])['c'].mean()
tmp = df['A'].map(tmp1)
shui
fuente