Aplicar vs transformar em um objeto de grupo

174

Considere o seguinte quadro de dados:

     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

Os seguintes comandos funcionam:

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

mas nenhum dos seguintes trabalhos:

> 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ê? O exemplo na documentação parece sugerir que a chamada transforma um grupo permite executar o processamento de operações em linhas:

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

Em outras palavras, eu pensei que transformar é essencialmente um tipo específico de aplicação (aquele que não agrega). Onde eu estou errado?

Para referência, abaixo está a construção do quadro de dados original acima:

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
fonte
1
A função transmitida para transformdeve retornar um número, uma linha ou a mesma forma que o argumento. se for um número, o número será definido para todos os elementos do grupo; se for uma linha, será transmitido para todas as linhas do grupo. No seu código, a função lambda retorna uma coluna que não pode ser transmitida para o grupo.
HYRY
1
Obrigado @HYRY, mas estou confuso. Se você observar o exemplo na documentação que copiei acima (por exemplo, com zscore), transformrecebe uma função lambda que assume que cada xum é um item dentro do groupe também retorna um valor por item no grupo. o que estou perdendo?
Amelio Vazquez-Reina
Para quem procura uma solução extremamente detalhada, consulte esta abaixo .
Ted Petrou
@TedPetrou: o tl; dr disso é: 1) applypassa em todo o df, mas transformpassa cada coluna individualmente como uma série. 2) applypode retornar qualquer saída de forma (escalar / Série / DataFrame / matriz / lista ...), enquanto transformdeve retornar uma sequência (1D Série / matriz / lista) do mesmo comprimento que o grupo. É por isso que o OP apply()não precisa transform(). Essa é uma boa pergunta, pois o documento não explicou claramente as duas diferenças. (semelhante à distinção entre apply/map/applymap, ou outras coisas ...)
SMCI

Respostas:

146

Duas grandes diferenças entre applyetransform

Existem duas grandes diferenças entre os métodos groupby transforme apply.

  • Entrada:
    • applypassa implicitamente todas as colunas para cada grupo como um DataFrame para a função personalizada.
    • while transformpassa cada coluna para cada grupo individualmente como uma série para a função personalizada.
  • Resultado:
    • A função personalizada transmitida para applypode retornar um escalar ou um Series ou DataFrame (ou matriz numpy ou mesmo lista) .
    • A função personalizada transmitida para transformdeve retornar uma sequência (uma série, matriz ou lista unidimensional) do mesmo comprimento que o grupo .

Portanto, transformfunciona apenas em uma série por vez e applyfunciona em todo o DataFrame de uma só vez.

Inspecionando a Função Personalizada

Pode ajudar bastante inspecionar a entrada da sua função personalizada passada para applyou transform.

Exemplos

Vamos criar alguns dados de amostra e inspecionar os grupos para que você possa ver do que estou falando:

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

Vamos criar uma função personalizada simples que imprima o tipo do objeto transmitido implicitamente e, em seguida, gere um erro para que a execução possa ser interrompida.

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

Agora vamos passar essa função para o groupby applye transformmétodos para ver qual objeto é passado para ele:

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

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

Como você pode ver, um DataFrame é passado para a inspectfunção. Você pode estar se perguntando por que o tipo DataFrame foi impresso duas vezes. Pandas dirige o primeiro grupo duas vezes. Isso é feito para determinar se existe uma maneira rápida de concluir o cálculo ou não. Este é um detalhe menor com o qual você não deve se preocupar.

Agora, vamos fazer a mesma coisa com transform

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

É passada uma série - um objeto Pandas totalmente diferente.

Portanto, transformsó é permitido trabalhar com uma única série de cada vez. É não impossível para ele para agir em duas colunas ao mesmo tempo. Portanto, se tentarmos subtrair a coluna ade bdentro da nossa função personalizada, obteremos um erro transform. Ver abaixo:

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

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

Nós obtemos um KeyError porque os pandas estão tentando encontrar o índice da série aque não existe. Você pode concluir esta operação com applyela, pois possui todo o DataFrame:

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

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

A saída é uma série e um pouco confusa, pois o índice original é mantido, mas temos acesso a todas as colunas.


Exibindo o objeto pandas passado

Pode ajudar ainda mais a exibir todo o objeto pandas na função personalizada, para que você possa ver exatamente com o que está operando. Você pode usar as printinstruções de que eu gosto de usar a displayfunção do IPython.displaymódulo para que os DataFrames sejam gerados em HTML em um notebook jupyter:

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

Captura de tela: insira a descrição da imagem aqui


A transformação deve retornar uma sequência dimensional única do mesmo tamanho que o grupo

A outra diferença é que transformdeve retornar uma sequência dimensional única do mesmo tamanho que o grupo. Nesse caso específico, cada grupo tem duas linhas, portanto, transformdeve retornar uma sequência de duas linhas. Caso contrário, será gerado um erro:

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

A mensagem de erro não é realmente descritiva do problema. Você deve retornar uma sequência do mesmo tamanho que o grupo. Portanto, uma função como esta funcionaria:

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

Retornar um único objeto escalar também funciona para transform

Se você retornar apenas um único escalar de sua função personalizada, transformuse-o para cada uma das linhas do 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
fonte
3
npnão está definido. Presumo que os iniciantes apreciariam se você incluísse import numpy as npem sua resposta.
precisa saber é o seguinte
187

Como me senti igualmente confuso com a .transformoperação vs. .apply, encontrei algumas respostas que esclareciam o assunto. Esta resposta, por exemplo, foi muito útil.

Minha opinião até agora é que .transformfuncionará (ou lidará) com Series(colunas) isoladamente um do outro . O que isso significa é que nas duas últimas chamadas:

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

Você pediu .transformpara pegar valores de duas colunas e 'it' na verdade não 'vê' as duas ao mesmo tempo (por assim dizer). transformexaminará as colunas do quadro de dados uma a uma e retornará uma série (ou grupo de séries) 'feita' de escalares que são repetidas len(input_column)vezes.

Portanto, este escalar, que deve ser usado .transformpara fazer isso, Seriesé resultado de alguma função de redução aplicada em uma entrada Series(e somente em UMA série / coluna de cada vez).

Considere este exemplo (no seu quadro de dados):

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)

produzirá:

       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

O que é exatamente o mesmo que se você o usasse apenas em uma coluna por vez:

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

produzindo:

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

Observe que .applyno último exemplo ( df.groupby('A')['C'].apply(zscore)) funcionaria exatamente da mesma maneira, mas falharia se você tentasse usá-lo em um dataframe:

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

dá erro:

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

Então, onde mais é .transformútil? O caso mais simples é tentar atribuir resultados da função de redução ao quadro de dados 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

produzindo:

     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

Tentando o mesmo com .applydaria NaNsno sum_C. Porque .applyretornaria um reduzido Series, que não sabe como transmitir de volta:

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

dando:

A
bar    3.973
foo    4.373

Também há casos em que .transformé usado para filtrar os dados:

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 isso adicione um pouco mais de clareza.

Primer
fonte
4
AMD. A diferença é tão sutil.
Dawei
3
.transform()também pode ser usado para preencher valores ausentes. Especialmente se você deseja transmitir a média do grupo ou a estatística do grupo para os NaNvalores desse grupo. Infelizmente, a documentação dos pandas também não foi útil para mim.
cyber-math
Eu acho que no último caso, .groupby().filter()faz a mesma coisa. Obrigado pela sua explicação, .apply()e também .transform()me deixa muito confuso.
Jiaxiang
isso explica por df.groupby().transform()que não posso trabalhar para um subgrupo df, sempre recebo o erro ValueError: transform must return a scalar value for each groupporque transformvê as colunas uma a uma
jerrytim
Gostei muito do último exemplo .transform usado para filtrar os dados. muito legal!
rishi jain 14/06
13

Vou usar um trecho muito simples para ilustrar a diferença:

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

O DataFrame fica assim:

    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   

Existem 3 IDs de clientes nesta tabela, cada cliente fez três transações e pagou 1,2,3 dólares por vez.

Agora, quero encontrar o pagamento mínimo feito por cada cliente. Existem duas maneiras de fazer isso:

  1. Usando apply:

    grouping.min ()

O retorno é assim:

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. Usando transform:

    grouping.transform (min)

O retorno é assim:

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 os métodos retornam um Seriesobjeto, mas o lengthprimeiro é 3 e lengtho segundo é 9.

Se você deseja responder What is the minimum price paid by each customer, o applymétodo é o mais adequado para sua escolha.

Se você quer responder What is the difference between the amount paid for each transaction vs the minimum payment, então quer 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 não funciona aqui simplesmente porque retorna uma série de tamanho 3, mas o comprimento do df original é 9. Você não pode integrá-lo novamente ao df original com facilidade.

Cheng
fonte
3
Eu acho que essa é uma ótima resposta! Obrigado por reservar um tempo para responder mais de quatro anos após a pergunta!
Benjamin Dubreu
4
tmp = df.groupby(['A'])['c'].transform('mean')

é como

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

ou

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