Appliquer vs transformer sur un objet de groupe

174

Considérez le dataframe suivant:

     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

Les commandes suivantes fonctionnent:

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

mais aucun des travaux suivants:

> 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

Pourquoi? L'exemple de la documentation semble suggérer que l'appel transformà un groupe permet d'effectuer un traitement d'opération par ligne:

# 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 d'autres termes, je pensais que la transformation est essentiellement un type d'application spécifique (celui qui ne s'agrège pas). Où ai-je tort?

Pour référence, voici la construction de la base de données originale ci-dessus:

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
la source
1
La fonction passée à transformdoit renvoyer un nombre, une ligne ou la même forme que l'argument. s'il s'agit d'un nombre, le nombre sera défini sur tous les éléments du groupe, s'il s'agit d'une ligne, il sera diffusé sur toutes les lignes du groupe. Dans votre code, la fonction lambda renvoie une colonne qui ne peut pas être diffusée vers le groupe.
HYRY
1
Merci @HYRY, mais je suis confus. Si vous regardez l'exemple dans la documentation que j'ai copié ci-dessus (c'est-à-dire avec zscore), transformreçoit une fonction lambda qui suppose que chacun xest un élément dans le group, et renvoie également une valeur par élément dans le groupe. Qu'est-ce que je rate?
Amelio Vazquez-Reina
Pour ceux qui recherchent une solution extrêmement détaillée, voyez celle-ci ci-dessous .
Ted Petrou
@TedPetrou: le tl; dr de qui est: 1) apply passe dans le df entier, mais transformpasse chaque colonne individuellement comme une série. 2) applypeut renvoyer n'importe quelle sortie de forme (scalaire / Series / DataFrame / array / list ...), alors que transformdoit renvoyer une séquence (1D Series / array / list) de la même longueur que le groupe. C'est pourquoi le PO apply()n'en a pas besoin transform(). C'est une bonne question car le doc n'a pas expliqué clairement les deux différences. (semblable à la distinction entre apply/map/applymap, ou d'autres choses ...)
smci

Réponses:

146

Deux différences majeures entre applyettransform

Il existe deux différences majeures entre les méthodes transformet applygroupby.

  • Contribution:
    • applytransmet implicitement toutes les colonnes de chaque groupe en tant que DataFrame à la fonction personnalisée.
    • while transformtransmet chaque colonne de chaque groupe individuellement en tant que série à la fonction personnalisée.
  • Production:
    • La fonction personnalisée passée à applypeut renvoyer un scalaire, ou une Series ou DataFrame (ou un tableau numpy ou même une liste) .
    • La fonction personnalisée passée à transform doit renvoyer une séquence (une série, un tableau ou une liste à une dimension) de la même longueur que le groupe .

Ainsi, transformfonctionne sur une seule série à la fois et applyfonctionne sur l'ensemble du DataFrame à la fois.

Inspection de la fonction personnalisée

Cela peut aider beaucoup d'inspecter l'entrée de votre fonction personnalisée passée à applyou transform.

Exemples

Créons des exemples de données et inspectons les groupes afin que vous puissiez voir de quoi je parle:

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

Créons une fonction personnalisée simple qui imprime le type de l'objet passé implicitement, puis génère une erreur afin que l'exécution puisse être arrêtée.

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

Passons maintenant cette fonction à la fois au groupby applyet aux transformméthodes pour voir quel objet lui est passé:

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

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

Comme vous pouvez le voir, un DataFrame est passé dans le inspect fonction. Vous vous demandez peut-être pourquoi le type, DataFrame, a été imprimé deux fois. Pandas dirige le premier groupe deux fois. Il fait cela pour déterminer s'il existe un moyen rapide de terminer le calcul ou non. C'est un détail mineur dont vous ne devriez pas vous inquiéter.

Maintenant, faisons la même chose avec transform

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

Il est passé une série - un objet Pandas totalement différent.

Ainsi, il transformn'est autorisé à travailler qu'avec une seule série à la fois. Il n'est pas impossible qu'il agisse sur deux colonnes en même temps. Donc, si nous essayons de soustraire la colonne ade l' bintérieur de notre fonction personnalisée, nous obtiendrions une erreur avec transform. Voir ci-dessous:

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

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

Nous obtenons une KeyError lorsque pandas tente de trouver l'index Series aqui n'existe pas. Vous pouvez terminer cette opération avec applycar il a le DataFrame entier:

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

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

La sortie est une série et un peu déroutante car l'index d'origine est conservé, mais nous avons accès à toutes les colonnes.


Affichage de l'objet pandas passé

Il peut être encore plus utile d'afficher l'objet pandas entier dans la fonction personnalisée, afin que vous puissiez voir exactement avec quoi vous travaillez. Vous pouvez utiliserprint instructions de I like pour utiliser la displayfonction du IPython.displaymodule afin que les DataFrames soient bien générés en HTML dans un notebook jupyter:

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

Capture d'écran: entrez la description de l'image ici


La transformation doit renvoyer une séquence unidimensionnelle de la même taille que le groupe

L'autre différence est que transformdoit renvoyer une séquence unidimensionnelle de la même taille que le groupe. Dans ce cas particulier, chaque groupe a deux lignes et transformdoit donc renvoyer une séquence de deux lignes. Si ce n'est pas le cas, une erreur est générée:

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

Le message d'erreur n'est pas vraiment descriptif du problème. Vous devez renvoyer une séquence de la même longueur que le groupe. Donc, une fonction comme celle-ci fonctionnerait:

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

Le renvoi d'un seul objet scalaire fonctionne également pour transform

Si vous ne renvoyez qu'un seul scalaire de votre fonction personnalisée, vous transforml'utiliserez pour chacune des lignes du groupe:

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
la source
3
npn'est pas défini. Je suppose que les débutants apprécieraient si vous incluez import numpy as npdans votre réponse.
Qaswed le
187

Comme je me sentais de la même manière confus avec l' .transformopération par rapport à, .applyj'ai trouvé quelques réponses pour éclairer le problème. Cette réponse, par exemple, a été très utile.

Mon plat à emporter jusqu'à présent est que .transform cela fonctionnera (ou traitera) Series(colonnes) isolément les unes des autres . Cela signifie que lors de vos deux derniers appels:

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

Vous avez demandé .transformde prendre des valeurs de deux colonnes et «il» ne les «voit» pas tous les deux en même temps (pour ainsi dire). transformexaminera les colonnes du dataframe une par une et retournera une série (ou un groupe de séries) `` fait '' de scalaires qui se répètentlen(input_column) fois.

Donc ce scalaire, qui devrait être utilisé par .transform pour faire le, Seriesest le résultat d'une fonction de réduction appliquée sur une entréeSeries (et seulement sur UNE série / colonne à la fois).

Considérez cet exemple (sur votre dataframe):

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)

donnera:

       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

C'est exactement la même chose que si vous ne l'utilisiez que sur une seule colonne à la fois:

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

cédant:

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

Notez que .applydans le dernier exemple ( df.groupby('A')['C'].apply(zscore)) fonctionnerait exactement de la même manière, mais échouerait si vous essayiez de l'utiliser sur un dataframe:

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

donne une erreur:

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

Alors, où d'autre est .transformutile? Le cas le plus simple consiste à essayer d'attribuer les résultats de la fonction de réduction à la trame de données d'origine.

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

cédant:

     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

Essayer la même chose avec .applydonnerait NaNsà sum_C. Car .applyretournerait un réduit Series, qu'il ne sait pas rediffuser:

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

donnant:

A
bar    3.973
foo    4.373

Il existe également des cas où .transformest utilisé pour filtrer les données:

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

J'espère que cela ajoute un peu plus de clarté.

Apprêt
la source
4
OMG. La différence est si subtile.
Dawei
3
.transform()pourrait également être utilisé pour remplir les valeurs manquantes. Surtout si vous souhaitez diffuser une moyenne de groupe ou une statistique de groupe aux NaNvaleurs de ce groupe. Malheureusement, la documentation des pandas ne m'a pas non plus été utile.
cyber-math
Je pense que dans le dernier cas, .groupby().filter()fait la même chose. Merci pour votre explication .apply()et .transform()cela me rend également très confus.
Jiaxiang
cela explique pourquoi df.groupby().transform()ne peut pas travailler pour un sous-groupe df, j'obtiens toujours l'erreur ValueError: transform must return a scalar value for each groupcar transformvoit les colonnes une par une
jerrytim
J'ai beaucoup aimé le dernier exemple .transform utilisé pour filtrer les données. super sympa!
rishi jain le
13

Je vais utiliser un extrait très simple pour illustrer la différence:

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

Le DataFrame ressemble à ceci:

    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   

Il y a 3 identifiants clients dans ce tableau, chaque client a effectué trois transactions et payé 1,2,3 dollars à chaque fois.

Maintenant, je veux trouver le paiement minimum effectué par chaque client. Il existe deux façons de procéder:

  1. En utilisant apply :

    grouping.min ()

Le retour ressemble à ceci:

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. En utilisant transform :

    grouping.transform (min)

Le retour ressemble à ceci:

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    

Les deux méthodes renvoient un Seriesobjet, mais la lengthde la première est 3 et lalength de la seconde est 9.

Si vous voulez répondre What is the minimum price paid by each customer, alors la applyméthode est la plus appropriée.

Si vous souhaitez répondre What is the difference between the amount paid for each transaction vs the minimum payment, vous souhaitez utiliser transform, car:

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

Apply ne fonctionne pas ici simplement parce qu'il renvoie une série de taille 3, mais la longueur du df d'origine est de 9. Vous ne pouvez pas l'intégrer facilement au df d'origine.

Cheng
la source
3
Je pense que c'est une excellente réponse! Merci d'avoir pris le temps de répondre plus de quatre ans après la question!
Benjamin Dubreu
4
tmp = df.groupby(['A'])['c'].transform('mean')

est comme

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
la source