Appliquer plusieurs fonctions à plusieurs colonnes groupby

221

Les documents montrent comment appliquer plusieurs fonctions sur un objet groupby à la fois en utilisant un dict avec les noms des colonnes de sortie comme clés:

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

Cependant, cela ne fonctionne que sur un objet Groupby Series. Et lorsqu'un dict est également transmis à un groupe par DataFrame, il s'attend à ce que les clés soient les noms de colonne auxquels la fonction sera appliquée.

Ce que je veux faire, c'est appliquer plusieurs fonctions à plusieurs colonnes (mais certaines colonnes seront exploitées plusieurs fois). De plus, certaines fonctions dépendent d'autres colonnes de l'objet groupby (comme les fonctions sumif). Ma solution actuelle consiste à aller colonne par colonne et à faire quelque chose comme le code ci-dessus, en utilisant lambdas pour les fonctions qui dépendent d'autres lignes. Mais cela prend beaucoup de temps (je pense qu'il faut beaucoup de temps pour parcourir un objet groupby). Je vais devoir le changer pour que je répète tout l'objet groupby en une seule fois, mais je me demande s'il y a un moyen intégré dans les pandas pour le faire quelque peu proprement.

Par exemple, j'ai essayé quelque chose comme

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(), ...)

mais comme prévu, j'obtiens une KeyError (puisque les clés doivent être une colonne si elle aggest appelée à partir d'un DataFrame).

Y a-t-il une manière intégrée de faire ce que j'aimerais faire, ou une possibilité que cette fonctionnalité soit ajoutée, ou devrais-je simplement parcourir le groupe manuellement?

Merci

barbe
la source
2
Si vous venez à cette question en 2017+, veuillez voir la réponse ci-dessous pour voir la façon idiomatique d'agréger plusieurs colonnes ensemble. La réponse actuellement sélectionnée contient plusieurs dépréciations, à savoir que vous ne pouvez plus utiliser un dictionnaire de dictionnaires pour renommer des colonnes dans le résultat d'un groupby.
Ted Petrou

Réponses:

282

La seconde moitié de la réponse actuellement acceptée est dépassée et comporte deux dépréciations. D'abord et surtout, vous ne pouvez plus transmettre un dictionnaire de dictionnaires à la aggméthode groupby. Deuxièmement, n'utilisez jamais .ix.

Si vous souhaitez travailler avec deux colonnes distinctes en même temps, je suggère d'utiliser la applyméthode qui transmet implicitement un DataFrame à la fonction appliquée. Utilisons une trame de données similaire à celle d'en haut

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 dictionnaire mappé des noms de colonne aux fonctions d'agrégation est toujours un excellent moyen d'effectuer une agrégation.

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 vous n'aimez pas ce nom de colonne lambda laid, vous pouvez utiliser une fonction normale et fournir un nom personnalisé à l' __name__attribut spécial comme ceci:

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

Utilisation applyet retour d'une série

Maintenant, si vous aviez plusieurs colonnes qui devaient interagir ensemble, vous ne pouvez pas utiliser agg, ce qui transmet implicitement une série à la fonction d'agrégation. Lors de l'utilisation de applyl'ensemble du groupe en tant que DataFrame est passé dans la fonction.

Je recommande de créer une seule fonction personnalisée qui renvoie une série de toutes les agrégations. Utilisez l'index des séries comme étiquettes pour les nouvelles colonnes:

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 vous êtes amoureux de MultiIndexes, vous pouvez toujours retourner une série avec une comme celle-ci:

    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
la source
3
J'adore le modèle d'utilisation d'une fonction qui renvoie une série. Très propre.
Stephen McAteer
2
c'est le seul moyen que j'ai trouvé pour agréger une trame de données via plusieurs entrées de colonne simulatneosly (l'exemple c_d ci-dessus)
Blake
2
Je suis confus par les résultats, prenant la somme de l' aintérieur du groupe, 0ne devrait-ce pas être le cas 0.418500 + 0.446069 = 0.864569? Il en va de même pour les autres cellules, les chiffres ne semblent pas s'additionner. Serait-ce un cadre de données sous-jacent légèrement différent qui a été utilisé dans les exemples suivants?
slackline
J'utilise fréquemment .size () avec un groupby pour voir le nombre d'enregistrements. Existe-t-il un moyen de le faire en utilisant la méthode agg: dict? Je comprends que je pourrais compter un champ particulier, mais ma préférence serait que le compte soit indépendant du champ.
Chris Decker
1
@slackline yes. je viens de le tester et cela fonctionne bien. Ted doit juste avoir créé le cadre à plusieurs reprises et comme il a été créé via la génération de nombres aléatoires, les données df pour générer réellement les données étaient différentes de celles finalement utilisées dans les calculs
Lucas H
166

Pour la première partie, vous pouvez passer un dict de noms de colonnes pour les clés et une liste de fonctions pour les valeurs:

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

MISE À JOUR 1:

Étant donné que la fonction d'agrégation fonctionne sur Series, les références aux autres noms de colonne sont perdues. Pour contourner ce problème, vous pouvez référencer la trame de données complète et l'indexer à l'aide des indices de groupe dans la fonction lambda.

Voici une solution de contournement 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]:
            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

Ici, la colonne «D» résultante est constituée des valeurs «E» sommées.

MISE À JOUR 2:

Voici une méthode qui, je pense, fera tout ce que vous demandez. Créez d'abord une fonction lambda personnalisée. Ci-dessous, g fait référence au groupe. Lors de l'agrégation, g sera une série. Passer g.indexà df.ix[]sélectionne le groupe actuel dans df. Je teste ensuite si la colonne C est inférieure à 0,5. La série booléenne retournée est transmise à g[]laquelle ne sélectionne que les lignes répondant aux critères.

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
la source
Intéressant, je peux également passer un dict de {funcname: func}valeurs as au lieu de listes pour conserver mes noms personnalisés. Mais dans les deux cas, je ne peux pas passer un lambdaqui utilise d'autres colonnes (comme lambda x: x['D'][x['C'] < 3].sum()ci-dessus: "KeyError: 'D'"). Une idée si c'est possible?
beardc
J'ai essayé de faire exactement cela, et j'ai l'erreurKeyError: 'D'
Zelazny7
Cool, je l'ai fait fonctionner df['A'].ix[g.index][df['C'] < 0].sum(). Cela commence à devenir assez compliqué, cependant - je pense que pour la lisibilité, le bouclage manuel peut être préférable, et je ne suis pas sûr qu'il existe un moyen de lui donner mon nom préféré dans l' aggargument (au lieu de <lambda>). J'espère que quelqu'un saura d'une manière plus simple ...
beardc
3
Vous pouvez passer un dict pour la valeur de la colonne {'D': {'my name':lambda function}}et cela fera de la clé de dict interne le nom de la colonne.
Zelazny7
1
Je crois que pandas prend désormais en charge plusieurs fonctions appliquées à un cadre de données groupé: pandas.pydata.org/pandas-docs/stable/…
IanS
22

Comme alternative (principalement sur l'esthétique) à la réponse de Ted Petrou, j'ai trouvé que je préférais une liste légèrement plus compacte. S'il vous plaît ne pensez pas à l'accepter, c'est juste un commentaire beaucoup plus détaillé sur la réponse de Ted, plus le code / les données. Python / pandas n'est pas mon premier / meilleur, mais j'ai trouvé cela bien lu:

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

Je le trouve plus proche des dplyrpipes et data.tabledes commandes chaînées. Pour ne pas dire qu'ils sont meilleurs, juste plus familiers pour moi. (Je reconnais certainement le pouvoir et, pour beaucoup, la préférence d'utiliser des fonctions plus formelles defpour ces types d'opérations. C'est juste une alternative, pas nécessairement meilleure.)


J'ai généré des données de la même manière que Ted, je vais ajouter une graine pour la reproductibilité.

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
la source
2
J'aime le plus cette réponse. Ceci est similaire aux tuyaux dplyr dans R.
Renhuai
18

Pandas >= 0.25.0, agrégations nommées

Depuis la version pandas 0.25.0ou supérieure, nous nous éloignons de l'agrégation et du renommage basés sur le dictionnaire, et nous nous dirigeons vers des agrégations nommées qui acceptent a tuple. Maintenant, nous pouvons simultanément agréger + renommer en un nom de colonne plus informatif:

Exemple :

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

Appliquer GroupBy.aggavec l'agrégation nommée:

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
la source
J'aime ces agrégations nommées mais je ne pouvais pas voir comment nous sommes censés les utiliser avec plusieurs colonnes?
Simon Woodhead
Bonne question, n'a pas pu comprendre cela, doute que cela soit (encore) possible. J'ai ouvert un ticket pour ça. Gardera ma question et vous mis à jour. Merci d'avoir signalé @SimonWoodhead
Erfan
4

Nouveau dans la version 0.25.0.

Pour prendre en charge l'agrégation spécifique aux colonnes avec un contrôle sur les noms des colonnes de sortie, pandas accepte la syntaxe spéciale dans GroupBy.agg () , appelée «agrégation nommée» , où

  • Les mots clés sont les noms des colonnes de sortie
  • Les valeurs sont des tuples dont le premier élément est la colonne à sélectionner et le deuxième élément est l'agrégation à appliquer à cette colonne. Pandas fournit à pandas.NamedAgg namedtuple les champs ['column', 'aggfunc'] pour clarifier les arguments. Comme d'habitude, l'agrégation peut être un alias appelable ou une chaîne.
    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 est juste un tuple nommé. Les tuples simples sont également autorisés.

    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

Les arguments de mots clés supplémentaires ne sont pas transmis aux fonctions d'agrégation. Seules les paires de (colonne, aggfunc) doivent être passées en tant que ** kwargs. Si vos fonctions d'agrégation nécessitent des arguments supplémentaires, appliquez-les partiellement avec functools.partial ().

L'agrégation nommée est également valide pour les agrégations Groupby de séries. Dans ce cas, il n'y a pas de sélection de colonne, donc les valeurs ne sont que les fonctions.

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

La réponse de Ted est incroyable. J'ai fini par en utiliser une version plus petite au cas où quelqu'un serait intéressé. Utile lorsque vous recherchez une agrégation qui dépend des valeurs de plusieurs colonnes:

créer une trame de données

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

regroupement et agrégation avec Apply (en utilisant plusieurs colonnes)

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

regroupement et agrégation avec agrégat (en utilisant plusieurs colonnes)

J'aime cette approche car je peux toujours utiliser des agrégats. Peut-être que les gens me diront pourquoi appliquer est nécessaire pour accéder à plusieurs colonnes lors de l'agrégation de groupes.

Cela semble évident maintenant, mais tant que vous ne sélectionnez pas la colonne d'intérêt directement après le groupby , vous aurez accès à toutes les colonnes de la trame de données depuis votre fonction d'agrégation.

accéder uniquement à la colonne sélectionnée

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

l'accès à toutes les colonnes puisque la sélection est après tout la magie

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

ou similaire

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

J'espère que ça aide.

campo
la source