web-dev-qa-db-fra.com

Appliquer plusieurs fonctions à plusieurs colonnes groupby

Les docs montrent comment appliquer plusieurs fonctions à la fois sur un objet groupby à l'aide d'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

Toutefois, cela ne fonctionne que sur un objet Series groupby. Et lorsqu'un dict est également transmis à un groupe par DataFrame, il s'attend à ce que les clés correspondent aux 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). En outre, 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 des 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 devrai le changer pour pouvoir parcourir l'ensemble de l'objet groupby en une seule fois, mais je me demande s'il existe une méthode intégrée dans pandas pour le faire de manière plus propre.

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 erreur KeyError (car les clés doivent être une colonne si agg est appelé à partir d'un DataFrame).

Existe-t-il une méthode intégrée permettant 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

164
beardc

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

Si vous souhaitez utiliser deux colonnes distinctes en même temps, je vous suggérerais d'utiliser la méthode apply qui transmet implicitement un DataFrame à la fonction appliquée. Utilisons un dataframe similaire à celui du dessus

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é à partir de noms de colonnes sur des fonctions d'agrégation reste un moyen parfaitement efficace pour 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, vous pouvez utiliser une fonction normale et attribuer un nom personnalisé à l'attribut spécial __name__, comme ceci:

def max_min(x):
    return x.max() - x.min()

max_min.__= '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

Utiliser apply et renvoyer une série

Maintenant, si vous avez plusieurs colonnes qui doivent interagir ensemble, vous ne pouvez pas utiliser agg, qui passe implicitement une série à la fonction d'agrégation. Lorsque vous utilisez apply, l'ensemble du groupe en tant que DataFrame est transmis à la fonction.

Je recommande de créer une fonction personnalisée unique qui renvoie une série de toutes les agrégations. Utilisez l'index de la série 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 renvoyer une série avec 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
177
Ted Petrou

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 l’ensemble de la structure de données et l’indexer à l’aide des index de groupe de la fonction lambda.

Voici une solution de contournement:

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 composée des valeurs "E" additionnées.

MISE À JOUR 2:

Voici une méthode qui, je pense, fera tout ce que vous demandez. Commencez par créer 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 de g.index à df.ix[] sélectionne le groupe actuel de df. Je teste ensuite si la colonne C est inférieure à 0,5. La série booléenne renvoyée est transmise à g[] qui sélectionne uniquement 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
160
Zelazny7

Comme alternative (principalement esthétique) à la réponse de Ted Petrou, j’ai préféré une liste légèrement plus compacte. N'envisagez pas de l'accepter, il s'agit simplement d'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é que cela lisait 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

Je le trouve plus proche de dplyr pipes et de data.table commandes chaînées. Pour ne pas dire qu'ils sont meilleurs, mais me sont plus familiers. (Je reconnais certainement le pouvoir et, pour beaucoup, la préférence d'utiliser des fonctions plus formalisées def pour ces types d'opérations. Ceci 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
12
r2evans

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

créer un cadre 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 (utilisation de 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 (utilisation de plusieurs colonnes)

J'aime cette approche car je peux toujours utiliser l'agrégat. Peut-être que les gens me diront pourquoi il est nécessaire d'appliquer pour obtenir 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 du bloc de données à partir de votre fonction d'agrégation.

seul accès à la colonne sélectionnée

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

l'accès à toutes les colonnes depuis 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.

1
campo

Pandas >= 0.25.0, agrégations nommées

Depuis pandas version 0.25.0 ou supérieur, nous nous éloignons de l'agrégation par le dictionnaire et du renommage, pour nous diriger vers agrégations nommées qui accepte une 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

Appliquez GroupBy.agg avec 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
1
Erfan