Pandas: La dépréciation des dicts de réétiquetage dans groupby.agg pose de nombreux problèmes

Créé le 19 nov. 2017  ·  37Commentaires  ·  Source: pandas-dev/pandas

Ce problème est créé sur la base de la discussion de #15931 suite à la dépréciation des dicts de réétiquetage dans groupby.agg . Beaucoup de ce qui est résumé ci-dessous a déjà été discuté dans la discussion précédente. Je recommanderais en particulier https://github.com/pandas-dev/pandas/pull/15931#issuecomment -336139085 où les problèmes sont également clairement énoncés.

La motivation derrière la dépréciation de #15931 était principalement liée à l'apport d'une interface cohérente pour agg() entre Series et Dataframe (voir aussi #14668 pour le contexte).

La fonctionnalité de réétiquetage avec un dict imbriqué a été décrite par certains comme étant trop complexe et/ou incohérente et donc obsolète.

Cependant, cela a un prix : l'impossibilité d'agréger et de renommer en même temps entraîne des problèmes très ennuyeux et une certaine incompatibilité descendante où aucune solution de contournement raisonnable n'est disponible :

  • _[ennuyeux]_ plus de contrôle sur les noms des colonnes résultantes
  • _[ennuyeux]_ vous devez trouver un moyen de renommer le MultiIndex _après_ effectuer l'agrégation, nécessitant de garder une trace de l'ordre des colonnes à deux endroits dans le code... pas du tout pratique et parfois carrément impossible (cas ci-dessous ).
  • ⚠️ _ [breaking] _ ne peut pas appliquer plus d'un callable avec le même nom interne sur la même colonne d'entrée. Il en résulte deux sous-cas :

    • _ [rupture] _ vous ne pouvez plus appliquer deux ou plusieurs agrégateurs lambda sur la même colonne

    • _ [rupture] _ vous ne pouvez plus appliquer deux agrégateurs ou plus à partir de fonctions partielles à moins que vous ne modifiiez leur attribut caché __name__

Exemple

_(veuillez noter qu'il s'agit d'un exemple conçu dans le but de démontrer le problème dans un code aussi court que possible, mais tous les problèmes démontrés ici m'ont mordu dans la vraie vie depuis le changement, et dans des situations pas aussi simples qu'ici )_

Cadre de données d'entrée

mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)
  cat  distance  energy
0   A      1.20    1.80
1   A      1.50    1.95
2   A      1.74    2.04
3   B      0.82    1.25
4   B      1.01    1.60
5   C      0.60    1.01

Avant de:

facile à écrire et à lire, et fonctionne comme prévu

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# median absolute deviation as a partial function
# in order to demonstrate the issue with partial functions as aggregators
mad_c1 = partial(smrb.mad, c=1)

# renaming and specifying the aggregators at the same time
# note that I want to choose the resulting column names myself
# for example "total_xxxx" instead of just "sum"
mydf_agg = mydf.groupby('cat').agg({
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
})

résulte en

          energy                             distance
    total_energy energy_p98 energy_p17 total_distance average_distance distance_mad distance_mad_c1
cat
A           5.79     2.0364     1.8510           4.44            1.480     0.355825           0.240
B           2.85     1.5930     1.3095           1.83            0.915     0.140847           0.095
C           1.01     1.0100     1.0100           0.60            0.600     0.000000           0.000

et il ne reste plus que :

# get rid of the first MultiIndex level in a pretty straightforward way
mydf_agg.columns = mydf_agg.columns.droplevel(level=0)

Bonne danse louant les pandas 🕺 !

Après

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# median absolute deviation as a partial function
# in order to demonstrate the issue with partial functions as aggregators
mad_c1 = partial(smrb.mad, c=1)

# no way of choosing the destination's column names...
mydf_agg = mydf.groupby('cat').agg({
    'energy': [
        'sum',
        lambda x: np.percentile(x, 98), # lambda
        lambda x: np.percentile(x, 17), # lambda
    ],
    'distance': [
        'sum',
        'mean',
        smrb.mad, # original function
        mad_c1,   # partial function wrapping the original function
    ],
})

Ce qui précède s'interrompt car les fonctions lambda donneront toutes des colonnes nommées <lambda> ce qui donne

SpecificationError: Function names must be unique, found multiple named <lambda>

Régression rétrocompatible : on ne peut plus appliquer deux lambdas différents à la même colonne d'origine.

Si l'on supprime le lambda x: np.percentile(x, 98) d'en haut, nous obtenons le même problème avec la fonction partielle qui hérite du nom de la fonction de la fonction d'origine :

SpecificationError: Function names must be unique, found multiple named mad

Enfin, après avoir écrasé l'attribut __name__ du partiel (par exemple avec mad_c1.__name__ = 'mad_c1' ) on obtient :

    energy          distance
       sum <lambda>      sum   mean       mad mad_c1
cat
A     5.79   1.8510     4.44  1.480  0.355825  0.240
B     2.85   1.3095     1.83  0.915  0.140847  0.095
C     1.01   1.0100     0.60  0.600  0.000000  0.000

avec encore

  • une colonne manquante (98e centile)
  • la gestion des colonnes MultiIndex
  • et le renommage des colonnes

traiter dans une étape séparée.

Il n'y a aucun contrôle possible pour les noms de colonnes après l'agrégation, le mieux que nous puissions obtenir de manière automatisée est une combinaison du nom de colonne d'origine et du _nom de la fonction d'agrégation_ comme ceci :

mydf_agg.columns = ['_'.join(col) for col in mydf_agg.columns]

ce qui se traduit par :

     energy_sum  energy_<lambda>  distance_sum  distance_mean  distance_mad distance_mad_c1
cat
A          5.79           1.8510          4.44          1.480      0.355825           0.240
B          2.85           1.3095          1.83          0.915      0.140847           0.095
C          1.01           1.0100          0.60          0.600      0.000000           0.000

et si vous avez vraiment besoin d'avoir des noms différents, vous pouvez le faire comme ceci :

mydf_agg.rename({
    "energy_sum": "total_energy",
    "energy_<lambda>": "energy_p17",
    "distance_sum": "total_distance",
    "distance_mean": "average_distance"
    }, inplace=True)

mais cela signifie que vous devez faire attention à garder le code de renommage (qui doit maintenant être situé à un autre endroit dans le code) en synchronisation avec le code où l'agrégation est définie...

Triste utilisateur de pandas (qui aime toujours les pandas bien sûr)


Je suis tout à fait pour la cohérence, et en même temps je regrette profondément la dépréciation de la fonctionnalité _aggregate and rename_. J'espère que les exemples ci-dessus clarifient les points douloureux.


Solutions possibles

  • Annuler la dépréciation de la fonctionnalité de réétiquetage dict-of-dict
  • Fournir une autre API pour pouvoir le faire (mais pourquoi devrait-il y avoir deux méthodes pour le même objectif principal, à savoir l'agrégation ?)
  • ??? (ouvert aux suggestions)

_Lecture facultative :_

En ce qui concerne la discussion susmentionnée dans la demande d'extraction qui dure déjà depuis quelques mois, je n'ai réalisé que récemment l'une des raisons pour lesquelles je suis si dérangé par cette dépréciation : "Agréger et renommer" est une chose naturelle à faire avec GROUP BY en SQL, car dans SQL, vous fournissez généralement le nom de la colonne de destination directement à côté de l'expression d'agrégation, par exemple SELECT col1, avg(col2) AS col2_mean, stddev(col2) AS col2_var FROM mytable GROUP BY col1 .

Je ne dis _pas_ que Pandas devrait nécessairement fournir les mêmes fonctionnalités que SQL bien sûr. Mais les exemples fournis ci-dessus démontrent pourquoi l'API dict-of-dict était à mon avis une solution propre et simple à de nombreux cas d'utilisation.

(* Personnellement, je ne suis pas d'accord pour dire que l'approche dict-of-dict est complexe.)

API Design Groupby

Commentaire le plus utile

Pour ce que ça vaut, je suis aussi fortement en faveur de ne pas déprécier la fonctionnalité.

Une grande raison pour moi est qu'il y a quelque chose de profondément étrange dans le mélange de l'espace de noms de la fonction Python (quelque chose à voir avec l'implémentation particulière) avec les données des noms de colonnes (quelque chose qui ne devrait sûrement pas connaître l'implémentation). Le fait que nous voyions des colonnes (potentiellement plusieurs colonnes) nommées '<lambda>' me cause une grave dissonance cognitive.

L'approche du changement de nom est efficace, car il y a cette étape intermédiaire où les noms de colonnes inutiles (et exposés) sont transportés. De plus, ils sont difficiles à renommer de manière fiable et systématique car il existe potentiellement des dépendances vis-à-vis de l'implémentation.

En dehors de cela, la fonctionnalité dict imbriquée est certes complexe, mais c'est une opération complexe qui est effectuée.

TL; DR S'il vous plaît ne dépréciez pas. :)

Tous les 37 commentaires

@zertrin : Merci d'avoir

@jreback @jorisvandenbossche @TomAugspurger @chris-b1

Je suis d'accord que renommer avec l'implémentation actuelle de agg est très maladroit et cassé dans cet exemple. Les dicts imbriqués sont quelque peu complexes, mais les écrire comme vous l'avez fait montre très clairement ce qui se passe.

Je suppose qu'il pourrait y avoir un paramètre names ajouté à agg qui prendrait le dictionnaire mappant les colonnes d'agrégation à leurs nouveaux noms. Vous pouvez même ajouter un autre paramètre drop_index tant que booléen pour déterminer s'il faut conserver le niveau d'index supérieur.

Donc la syntaxe deviendrait :

agg_dict = {'energy': ['sum',
                       lambda x: np.percentile(x, 98), # lambda
                       lambda x: np.percentile(x, 17), # lambda
                      ],
            'distance': ['sum',
                         'mean',
                         smrb.mad, # original function
                         mad_c1,   # partial function wrapping the original function
                        ]
           }

name_dict = {'energy':['energy_sum', 'energy_p98', 'energy_p17'],
             'distance':['distance_sum', 'distance_mean', 'distance_mad', 'distance_mad_c1']}


mydf.groupby('cat').agg(agg_dict, names=name_dict, drop_index=True)

Ou peut-être qu'une toute nouvelle méthode agg_assign pourrait être créée, qui fonctionnerait de la même manière que DataFrame.assign :

mydf.groupby('cat').agg_assign(energy_sum=lambda x: x.energy.sum(),
                               energy_p98=lambda x: np.percentile(x.energy, 98),
                               energy_p17=lambda x: np.percentile(x.energy, 17),
                               distance_sum=lambda x: x.distance.sum(),
                               distance_mean=lambda x: x.distance.mean(),
                               distance_mad=lambda x: smrb.mad(x.distance),
                               distance_mad_c1=lambda x: mad_c1(x.distance))

En fait, j'aime beaucoup mieux cette option.

Pour ce que ça vaut, je suis aussi fortement en faveur de ne pas déprécier la fonctionnalité.

Une grande raison pour moi est qu'il y a quelque chose de profondément étrange dans le mélange de l'espace de noms de la fonction Python (quelque chose à voir avec l'implémentation particulière) avec les données des noms de colonnes (quelque chose qui ne devrait sûrement pas connaître l'implémentation). Le fait que nous voyions des colonnes (potentiellement plusieurs colonnes) nommées '<lambda>' me cause une grave dissonance cognitive.

L'approche du changement de nom est efficace, car il y a cette étape intermédiaire où les noms de colonnes inutiles (et exposés) sont transportés. De plus, ils sont difficiles à renommer de manière fiable et systématique car il existe potentiellement des dépendances vis-à-vis de l'implémentation.

En dehors de cela, la fonctionnalité dict imbriquée est certes complexe, mais c'est une opération complexe qui est effectuée.

TL; DR S'il vous plaît ne dépréciez pas. :)

Ma contribution est motivée par deux choses.

  1. Je suis conscient et d'accord avec la motivation pour réduire l'API gonflée de Pandas. Même si je me trompe en ce qui concerne la motivation perçue pour réduire les éléments d'API "gonflés", je suis toujours d'avis que l'API de Pandas pourrait être rationalisée.
  2. Je pense qu'il vaut mieux avoir un bon livre de cuisine avec de bonnes recettes que de fournir des API pour satisfaire les envies et les désirs de chacun. Je ne prétends

De plus, les objets Pandas Series et DataFrame ont des méthodes pipe pour faciliter le pipeline. Dans ce segment de documentation, il est expliqué que nous pourrions utiliser pipe pour remplacer les méthodes par proxy. Dans le même esprit, nous pourrions utiliser le nouveau GroupBy.pipe pour jouer un rôle similaire et nous permettre de construire des méthodes proxy pour les objets groupby.

Je vais utiliser l'exemple de @zertrin

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# The DataFrame offered up above
mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)

# Identical dictionary passed to `agg`
funcs = {
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
}

# Write a proxy method to be passed to `pipe`
def agg_assign(gb, fdict):
    data = {
        (cl, nm): gb[cl].agg(fn)
        for cl, d in fdict.items()
        for nm, fn in d.items()
    }
    return pd.DataFrame(data)

# All the API we need already exists with `pipe`
mydf.groupby('cat').pipe(agg_assign, fdict=funcs)

Ce qui se traduit par

            distance                                                 energy                        
    average_distance distance_mad distance_mad_c1 total_distance energy_p17 energy_p98 total_energy
cat                                                                                                
A              1.480     0.355825           0.240           4.44     1.8510     2.0364         5.79
B              0.915     0.140847           0.095           1.83     1.3095     1.5930         2.85
C              0.600     0.000000           0.000           0.60     1.0100     1.0100         1.01

La méthode pipe rend l'ajout d'une nouvelle API inutile dans de nombreux cas. Il fournit également les moyens de remplacer la fonctionnalité obsolète dont nous discutons. Par conséquent, je serais enclin à aller de l'avant avec la dépréciation.

J'aime beaucoup l'idée de tdpetrou - à utiliser : names=name_dict .

Cela peut rendre tout le monde heureux. Cela nous donne la possibilité de renommer les colonnes facilement comme nous le souhaitons.

Pas vraiment, comme mentionné dans mon message initial, cela ne résoudrait pas le problème du découplage de l'endroit où l'opération d'agrégation est définie du nom de la colonne résultante, nécessitant un effort supplémentaire pour s'assurer que les deux sont "synchronisés".

Je ne dis pas que c'est une mauvaise solution (après tout, cela résout les autres problèmes), mais ce ne serait pas aussi simple et clair que l'approche dict of dict. Je veux dire ici qu'au moment de l'écriture, vous devez garder les deux dicts de listes synchronisés, et lors de la lecture de la source, le lecteur doit faire un effort pour faire correspondre les noms dans le deuxième dict de listes avec la définition agrégée dans le premier dict de listes. C'est deux fois l'effort dans chaque cas.

Les dicts imbriqués sont quelque peu complexes, mais les écrire comme vous l'avez fait montre très clairement ce qui se passe.

Je ne comprends toujours pas pourquoi tout le monde semble dire que dict of dict est complexe. Pour moi, c'est la façon la plus claire de le faire.

Cela dit, si le mot-clé names est la seule solution avec laquelle l'équipe des pandas est à l'aise, ce serait quand même une amélioration par rapport à la situation actuelle.

@pirsquared solution intéressante avec l'API actuelle. Bien qu'à mon avis pas assez facile à saisir (je ne comprends pas vraiment comment cela fonctionne :confused: )

J'ai lancé un fil sur le subreddit datascience - Qu'est-ce que tu détestes chez les pandas ? . Quelqu'un a exprimé son mépris pour le MultiIndex renvoyé après un groupby et a souligné le verbe dplyr do qui est implémenté dans plydata . Il se trouve que cela fonctionne exactement comme agg_assign donc c'était assez intéressant.

@zertrin agg_assign serait supérieur à votre approche dict of dict et serait identique aux agrégations SQL et permettrait à plusieurs colonnes d'interagir les unes avec les autres au sein de l'agrégation. Cela fonctionnerait également de la même manière que DataFrame.assign .

Des idées @jreback @TomAugspurger ?

...
mydf.groupby('cat').agg(agg_dict, names=name_dict, drop_index=True)

Bien que cela résolve le problème, il faut aligner les clés et les valeurs à deux endroits. Je pense qu'une API (comme suggéré pour .agg_assign ) qui ne nécessite pas un tel code de comptabilité est moins sujette aux erreurs.

Il y a aussi le problème du code de nettoyage après l'utilisation de l'API. Lorsque les opérations groupby renvoient une trame MultiIndex données MultiIndex . La manière déclarative simple d'utiliser .agg_assign , suggère aucune hiérarchie, aucune sortie MultiIndex , aucun nettoyage par la suite.

Sur la base des modèles d'utilisation, je pense que les sorties multi-index devraient être strictement opt-in et non opt-out.

Au départ, j'étais sceptique quant à la proposition agg_assign , mais les deux derniers commentaires m'ont convaincu que cela pourrait être une bonne solution.

Notamment en pensant à la possibilité de l'utiliser sous la forme agg_assign(**relabeling_dict) et ainsi pouvoir définir mes relabeling_dict comme ceci :

relabeling_dict = {
    'energy_sum': lambda x: x.energy.sum(),
    'energy_p98': lambda x: np.percentile(x.energy, 98),
    'energy_p17': lambda x: np.percentile(x.energy, 17),
    'distance_sum': lambda x: x.distance.sum(),
    'distance_mean': lambda x: x.distance.mean(),
    'distance_mad': lambda x: smrb.mad(x.distance),
    'distance_mad_c1': lambda x: mad_c1(x.distance)
}

Ce serait assez flexible et résoudrait tous les problèmes mentionnés dans mon PO.

@zertrin @has2k1

J'y pensais un peu plus et cette fonctionnalité existe déjà avec apply . Vous renvoyez simplement une série avec index comme nouveaux noms de colonnes et valeurs comme agrégation. Cela permet des espaces dans le nom et vous donne la possibilité d'ordonner les colonnes exactement comme vous le souhaitez :

def my_agg(x):
    data = {'energy_sum': x.energy.sum(),
            'energy_p98': np.percentile(x.energy, 98),
            'energy_p17': np.percentile(x.energy, 17),
            'distance sum' : x.distance.sum(),
            'distance mean': x.distance.mean(),
            'distance MAD': smrb.mad(x.distance),
            'distance MAD C1': mad_c1(x.distance)}
    return pd.Series(data, index=list_of_column_order)

mydf.groupby('cat').apply(my_agg)

Ainsi, il n'est peut-être pas nécessaire d'avoir une nouvelle méthode mais plutôt un meilleur exemple dans la documentation.

@tdpetrou , vous avez raison. J'avais oublié comment fonctionne apply car j'utilise ma propre version en raison de la double exécution du processus de sélection de chemin rapide-lent.

Hum en effet, il n'y a aucune chance que j'aurais pensé à l'utiliser dans un contexte d'agrégation juste en lisant la doc cependant...
De plus, je trouve toujours la solution avec apply un peu trop alambiquée. L'approche agg_assign semblait plus simple et compréhensible.

Puisqu'il n'y a jamais eu vraiment de déclaration à ce sujet, l'approche dict-of-dict (qui, bien qu'actuellement obsolète, est déjà implémentée et résout également tous ces problèmes) est-elle vraiment hors de question ?

À l'exception de l'approche agg_assign , dict-of-dict semble toujours la plus simple et n'a besoin d'aucun codage, juste d'être désapprouvée.

L'avantage et l'inconvénient de l'approche agg_assign est qu'elle pousse la sélection de colonne dans la méthode d'agrégation . Dans tous les exemples, le x passé au lambda est quelque chose comme self.get_group(group) pour chaque groupe dans self , un objet DataFrameGroupBy . C'est bien car cela sépare proprement le nommage , qui est dans **kwargs , de la sélection , qui est dans la fonction.

L'inconvénient est que vos fonctions d'agrégation génériques doivent maintenant se préoccuper de la sélection de colonnes. Il n'y a pas de déjeuner gratuit ! Cela signifie que vous vous retrouverez avec de nombreuses aides comme lambda x: x[col].min . Vous devrez également faire attention aux choses comme np.min qui réduit sur toutes les dimensions, contre pd.DataFrame.min , qui réduit sur axis=0 . C'est pourquoi quelque chose comme agg_assign ne serait pas équivalent à apply . apply fonctionne toujours par colonnes pour certaines méthodes.

Je ne suis pas sûr de ces compromis par rapport à la méthode dict-of-dicts, mais je suis curieux d'entendre les pensées des autres. Voici un croquis approximatif de agg_assign , que j'ai appelé et que j'ai appelé agg_table pour souligner que les fonctions sont transmises aux tables, pas aux colonnes :

from collections import defaultdict

import pandas as pd
import numpy as np
from pandas.core.groupby import DataFrameGroupBy

mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)


def agg_table(self, **kwargs):
    output = defaultdict(dict)
    for group in self.groups:
        for k, v in kwargs.items():
            output[k][group] = v(self.get_group(group))

    return pd.concat([pd.Series(output[k]) for k in output],
                     keys=list(output),
                     axis=1)

DataFrameGroupBy.agg_table = agg_table

Usage

>>> gr = mydf.groupby("cat")
>>> gr.agg_table(n=len,
                 foo=lambda x: x.energy.min(),
                 bar=lambda y: y.distance.min())

   n   foo   bar
A  3  1.80  1.20
B  2  1.25  0.82
C  1  1.01  0.60

Je soupçonne que nous pourrions faire un peu pour rendre les performances de ce jeu moins horribles, mais pas autant que .agg ...

Quelqu'un de l'équipe de base de Pandas pourrait-il expliquer quelle est la principale raison de la dépréciation des dicts de réétiquetage dans groupby.agg ?

Je pourrais facilement comprendre si cela cause trop de problèmes pour maintenir le code, mais s'il s'agit de complexité pour l'utilisateur final - j'opterais également pour le ramener, car c'est assez clair par rapport aux solutions de contournement nécessaires...

Merci!

Quelqu'un de l'équipe principale de Pandas pourrait-il expliquer quelle est la principale raison de la dépréciation des dicts de réétiquetage dans groupby.agg ?

Avez-vous vu https://github.com/pandas-dev/pandas/pull/15931/files#diff -52364fb643114f3349390ad6bcf24d8fR461 ?

La raison principale était que les touches dict étaient surchargées pour faire deux choses. Pour Series / SeriesGroupBy, ils servent à nommer. Pour DataFrame/DataFrameGroupBy, ils servent à sélectionner une colonne.

In [32]: mydf.aggregate({"distance": "min"})
Out[32]:
distance    0.6
dtype: float64

In [33]: mydf.aggregate({"distance": {"foo": "min"}})
/Users/taugspurger/Envs/pandas-dev/bin/ipython:1: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  #!/Users/taugspurger/Envs/pandas-dev/bin/python3.6
Out[33]:
     distance
foo       0.6

In [34]: mydf.distance.agg({"foo": "min"})
Out[34]:
foo    0.6
Name: distance, dtype: float64

In [35]: mydf.groupby("cat").agg({"distance": {"foo": "min"}})
/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/groupby.py:4201: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  return super(DataFrameGroupBy, self).aggregate(arg, *args, **kwargs)
Out[35]:
    distance
         foo
cat
A       1.20
B       0.82
C       0.60

In [36]: mydf.groupby("cat").distance.agg({"foo": "min"})
/Users/taugspurger/Envs/pandas-dev/bin/ipython:1: FutureWarning: using a dict on a Series for aggregation
is deprecated and will be removed in a future version
  #!/Users/taugspurger/Envs/pandas-dev/bin/python3.6
Out[36]:
      foo
cat
A    1.20
B    0.82
C    0.60

Ce n'est probablement pas la chose la plus déroutante chez les pandas, alors peut-être pourrions-nous la revoir :) Il me manque probablement quelques cas extrêmes. Mais même si nous supprimons les agrégations dict-of-dicts, nous avons toujours l'incohérence entre la dénomination et la sélection de colonnes :

Pour Series / SeriesGroupBy, les clés du dictionnaire servent toujours à nommer la sortie.

Pour DataFrame / DataFrameGroupby, les clés dict sont toujours pour la sélection. Avec dict-of-dicts, nous sélectionnons une colonne, puis le dict interne sert à nommer la sortie, tout comme Series / SeriesGroupBy.

Nous en avons discuté brièvement auparavant (quelque part dans la longue discussion sur la dépréciation), et j'ai proposé quelque chose de similaire ici : https://github.com/pandas-dev/pandas/pull/14668#issuecomment -274508089. Mais à la fin, seule la dépréciation a été implémentée, et non les idées pour faciliter l'autre fonctionnalité d'utilisation de dicts (la fonction "renommer").

Le problème était que les dicts étaient à la fois utilisés pour la "sélection" (sur quelle colonne voulez-vous que cette fonction soit appliquée) et pour le "renommage" (quel devrait être le nom de colonne résultant lors de l'application de cette fonction). Une syntaxe alternative, en dehors des dicts, pourrait être des arguments de mot-clé, comme cela est discuté ici dans la proposition agg_assign .
Je suis toujours en faveur d'explorer cette possibilité, que ce soit dans agg lui-même ou dans une nouvelle méthode comme agg_assign .

Ce que j'ai proposé à l'époque était quelque chose de similaire à agg_assign mais en utilisant un dict par mot-clé au lieu d'une fonction lambda. Traduit dans l'exemple ici, cela donnerait quelque chose comme :

mydf.groupby('cat').agg(
    energy_sum={'energy': 'sum'},
    energy_p98={'energy': lambda x: np.percentile(x, 98)},
    energy_p17={'energy': lambda x: np.percentile(x, 17)},
    distance_sum={'distance': 'sum'},
    distance_mean={'distance': 'mean'},
    distance_mad={'distance': smrb.mad},
    distance_mad_c1={'distance': mad_c1})

Je ne suis pas sûr que ce soit nécessairement plus lisible ou plus facile à écrire que la version avec tous les lambdas, mais celle-ci pourrait potentiellement être plus performante, car les pandas peuvent toujours utiliser les implémentations optimisées pour la somme, la moyenne, etc. pas de fonction lambda ou spécifiée par l'utilisateur.

Une grande question avec cette approche serait ce que signifierait df.groupby('cat').agg(foo='mean') ? Cela s'appliquerait logiquement à toutes les colonnes puisque vous n'avez fait aucune sélection (similaire à {'col1' : {'foo': 'mean'}, 'col2': {'foo':'mean'}, 'col3': ...} auparavant). Mais, cela se traduirait par des colonnes multi-indexées, alors que dans l'exemple ci-dessus, je pense qu'il serait bien de ne pas se retrouver avec des colonnes MI.

Je pense que ce qui précède peut être rétrocompatible dans le agg existant, mais la question est de savoir si cela est nécessaire.
Je pense aussi que cela s'étendrait bien au cas series comme :

mydf.groupby('cat').distance.agg(
    distance_sum='sum',
    distance_mean='mean',
    distance_mad=smrb.mad,
    distance_mad_c1=mad_c1)

(et vous pouvez même envisager de faire ce qui précède une fois pour la "distance" et une fois pour "l'énergie" et concaténer le résultat si vous n'aimez pas tous les dicts / lambda)

@TomAugspurger Dans votre exemple d'implémentation simple de agg_table , ne vaudrait-il pas mieux itérer sur les différentes fonctions à appliquer, au lieu d'itérer les groupes, et au final concaténer les nouvelles colonnes par axe=1 au lieu de concaténer les lignes nouvellement formées par axis=0 ?

BTW, @zertrin @tdpetrou @smcateer @pirsquared et d'autres, merci beaucoup d'avoir soulevé ce problème et d'avoir donné des commentaires aussi détaillés. Ces commentaires et cette implication de la communauté sont très importants !

En fait, j'aime beaucoup le modèle suggéré par @tdpetrou (en utilisant apply avec une fonction qui renvoie une série) - probablement encore mieux que le dict de dicts.

Si la fonction renvoie pd.Series(data, index=data.keys()) sommes-nous assurés d'obtenir les indices dans le bon ordre ? (Je pense juste à la meilleure façon d'implémenter le modèle dans mon code - au risque de dériver hors sujet).

Edit: désolé, j'ai mal compris le point de l'argument index (il est facultatif ici, nécessaire uniquement si vous souhaitez spécifier l'ordre des colonnes - le retour de pd.Series(data) fait le travail pour moi).

L'exemple de @tdpetrou fonctionnerait-il avec les agrégations first & last ?

J'ai dû recourir à la tête/queue comme ça

def agg_funcs(x):
    data = {'start':x['DATE_TIME'].head(1).values[0],
           'finish':x['DATE_TIME'].tail(1).values[0],
           'events':len(x['DATE_TIME'])}
    return pd.Series(data, index = list(data.keys()))

results = df.groupby('col').apply(agg_funcs)

J'aimerais quand même aborder cela, mais je ne pense pas que ce sera fait pour 0.23.

L' approche de elle fonctionner sans définir une fonction que nous n'utiliserons plus jamais dans notre code ? Venant d'un monde Q/Kdb+ (similaire à SQL), je ne comprends pas pourquoi nous devons créer une variable/fonction temporelle pour une simple instruction de sélection.

OP ici.

Honnêtement, après tout ce temps et les nombreuses discussions dans #15931 et ici, je ne suis toujours pas convaincu que ce soit une bonne idée de déprécier les dicts de réétiquetage.

En fin de compte, aucune des alternatives proposées ici n'est plus intuitive pour les utilisateurs que l'approche actuelle de dict de réétiquetage à mon humble avis. Quand c'était dans la documentation, juste avec un exemple, il était clair comment cela fonctionnait, et c'est très flexible.

Bien sûr, les développeurs de pandas peuvent toujours penser le contraire, se contentant de partager le point de vue d'un utilisateur.

Même l'approche de relabelling dict n'est pas très intuitive. À mon avis, la syntaxe devrait être similaire à SQL - func(column_name) as new_column_name . En Python, nous pouvons le faire avec un tuple à trois éléments. (func, column_name, new_column_name) . C'est ainsi que dexplo fait l'agrégation groupby.

dexplo

@zertrin avez-vous des retours sur ma proposition ci-dessus : https://github.com/pandas-dev/pandas/issues/18366/#issuecomment -349089667
Au final, cela inverse en quelque sorte l'ordre du dict : au lieu de "{col: {name: func}}" ce serait en quelque sorte "**{name: {col: func}}"

@jorisvandenbossche J'ai pris en compte votre approche. Le truc, c'est que je ne vois pas vraiment les avantages supplémentaires que cela apporte par rapport à l'approche actuelle.

Pour le dire plus crûment, étant donné les choix suivants :

  1. Dépréciation du comportement actuel qui fonctionne bien (quelques lignes de code de dépréciation à supprimer, rajoutez le document qui a été supprimé)
  2. Mettre en œuvre votre proposition (modifications importantes à apporter au code, poursuivre avec dépréciation de l'approche actuelle, nécessité pour tous les utilisateurs d'adapter leur code)

Je ne vois pas pourquoi nous devrions choisir 2 à moins qu'il n'apporte des avantages significatifs et tangibles du point de vue du développeur et de l'utilisateur.

Pour aborder certains des points de votre proposition ci-dessus :

Le problème était que les dicts étaient à la fois utilisés pour la "sélection" (sur quelle colonne voulez-vous que cette fonction soit appliquée) et pour le "renommage" (quel devrait être le nom de colonne résultant lors de l'application de cette fonction).

Comme il a été bien documenté auparavant, je ne pense pas que ce soit un problème pour les utilisateurs . Personnellement, j'ai compris tout de suite en regardant les exemples dans la documentation. (EDIT : et j'ai pensé : _"yay ! construction très utile, elle correspond exactement à ce que je cherchais. Nice."_)

Une syntaxe alternative, en dehors des dicts, pourrait être des arguments de mot-clé

L'une des choses intéressantes à utiliser l'approche dict-of-dict est que les utilisateurs peuvent facilement la générer dynamiquement avec un autre code. Comme vous l'avez souligné dans le commentaire juste au-dessus de celui-ci, passer aux arguments de mots clés comme dans votre proposition permettrait toujours cela via la construction **{name: {col: func}} . Je ne suis donc pas contre votre proposition. Je ne vois tout simplement pas la valeur ajoutée et la nécessité de tels changements alors que nous atteignons déjà le même niveau de fonctionnalité avec le système actuellement mis en œuvre.

En fin de compte, votre proposition serait _d'accord_ si les développeurs principaux de pandas avaient un fort sentiment contre l'approche actuelle. Je ne vois tout simplement aucun avantage en tant qu'_utilisateur_. (en fait, je vois l'inconvénient de changer tout le code utilisateur existant pour le faire fonctionner à nouveau avec la nouvelle proposition).

@zertrin, nous en avons discuté hier avec certains développeurs principaux, mais nous n'avons pas écrit le résumé ici. Alors maintenant, je vais le faire, avant de répondre à votre commentaire, pour ne refléter que nos pensées d'hier.


Donc, tout d'abord, l'idée qu'une fonctionnalité de base comme SQL "SELECT avg(col2) as col2_avg" devrait fonctionner et être simple, est quelque chose sur laquelle nous sommes complètement d'accord, et nous voulons vraiment avoir une solution pour cela.

Mis à part les raisons originales pour lesquelles nous avons décidé de déprécier ceci (qui peut ou non être si fort), les dicts (obsolètes) actuels des dicts ne sont pas non plus si idéaux, car cela crée un MultiIndex que vous ne voulez jamais vraiment :

In [1]: df = pd.DataFrame({'A': ['a', 'b', 'a'], 'B': range(3), 'C': [.1, .2, .3]})

In [3]: gr = df.groupby('A')

In [4]: gr.agg({'B': {'b_sum': 'sum'}, 'C': {'c_mean': 'mean', 'c_count': 'count'}})
Out[4]: 
        C            B
  c_count c_mean b_sum
A                     
a       2    0.2     2
b       1    0.2     1

Dans ce qui précède, le premier niveau du MultiIndex est superflu, car vous avez déjà spécifiquement renommé les colonnes (dans l'exemple de l'OP, cela est également directement suivi de la suppression du premier niveau des colonnes).
Il est cependant difficile de changer cela car vous pouvez également faire des choses comme gr.agg(['sum', 'mean']) ou (mixte) gr.agg({'B': ['sum', 'mean'], 'C': {'c_mean': 'mean', 'c_count': 'count'}}) le MultiIndex est nécessaire et logique.

Ainsi, l'une des propositions qui a été mentionnée dans la discussion ci-dessus était d'avoir un moyen de spécifier les noms de colonne finaux séparément (par exemple https://github.com/pandas-dev/pandas/issues/18366#issuecomment-346683449).
Ajouter par exemple un mot-clé supplémentaire à aggregate pour spécifier les noms de colonnes, comme

gr.agg({'B': 'sum', 'C': ['mean', 'count']}, columns=['b_sum', 'c_mean', 'c_count'])

serait possible.
Cependant, si nous divisons la spécification de colonne/fonction et les nouveaux noms de colonne, nous pouvons également le rendre plus générique qu'un nouveau mot-clé, et faire quelque chose comme :

gr.agg({'B': 'sum', 'C': ['mean', 'count']}).rename(columns=['b_sum', 'c_mean', 'c_count'])

Cela nécessite https://github.com/pandas-dev/pandas/issues/14829 pour être résolu (quelque chose que nous voulons faire pour 0.24.0).
(Note importante: pour cela , nous devons résoudre le problème des noms en double des fonctions lambda, nous devons donc faire une sorte de dédoublonnage automatique des noms si nous voulons soutenir cette solution.)


Ensuite, nous aimons toujours la manière des arguments de mot-clé pour renommer. Les raisons à cela sont :

  • c'est similaire à la façon dont assign fonctionne dans les pandas, et également cohérent avec la façon dont groupby().aggregate() fonctionne dans ibis (et aussi similaire à quoi il ressemble dans, par exemple, dplyr dans R)
  • il vous donne directement les noms de colonnes non hiérarchiques que vous voudriez (pas de MultiIndex)
  • pour les cas simples (également par exemple pour le cas de la série), je pense que c'est plus simple que le dict de dict

Nous avons encore discuté un peu de ce à quoi cela pourrait ressembler. Ce que j'ai proposé ci-dessus était (pour utiliser la sélection de colonne/fonction équivalente comme dans mes premiers exemples):

gr.agg(b_sum={'B': 'sum'}, c_mean={'C': 'mean'}, c_count={'C': 'count'})

Vous pouvez toujours créer cette spécification en tant que dict of dicts, mais avec le niveau interne et externe permuté par rapport à la version actuelle (obsolète):

gr.agg(**{'b_sum': {'B': 'sum'}, 'c_mean': {'C': 'mean'}, 'c_count': {'C': 'count'})

(nous pourrions avoir un exemple de fonction d'assistance qui convertit les dicts de dicts existants vers cette version)

Cependant, le dict n'est toujours qu'un seul {col: func} , et ces multiples dicts à élément unique semblent un peu étranges. Une alternative à laquelle nous avons pensé consiste donc à utiliser des tuples :

gr.agg(b_sum=('B', 'sum'), c_mean=('C', 'mean'), c_count=('C', 'count'))

Cela semble un peu mieux, mais d'un autre côté, le dict {'B': 'sum'} est cohérent avec les autres API pour spécifier la colonne sur laquelle appliquer la fonction.


Les deux suggestions ci-dessus (le renommage plus facile par la suite et le nommage basé sur des mots-clés) sont en principe orthogonales, mais il pourrait être bien d'avoir les deux (ou encore quelque chose d'autre basé sur une discussion plus approfondie)

Merci d'avoir transmis ici les réflexions actuelles des développeurs 😃

Je reconnais le (à mon avis, le seul) inconvénient de l'approche dict-of-dict obsolète avec le MultiIndex résultant. Pourrait être aplati si l'utilisateur passe une option supplémentaire (ouais YAO :-/ ).

Comme mentionné, je ne suis pas contre la deuxième version, tant qu'il reste possible de :

  • générer des choses dynamiquement d'une manière ou d'une autre et les décompresser (grâce à la construction **{} , yay Python !)
  • garder le renommage et la spécification d'agrégation rapprochés (avoir à garder une trace de deux listes de sorte que leur ordre reste le même est tout simplement ennuyeux en tant qu'utilisateur à mon humble avis)
  • utilisez des fonctions lambda ou partielles sans avoir besoin de solutions de contournement en raison des noms de fonction (potentiellement absents ou en conflit avec).

En tant que tel, la dernière suggestion (avec des dicts ou des tuples pour le mappage col>func) est ok, je pense.

La première proposition du commentaire précédent peut être mise en œuvre si vous le souhaitez vraiment, mais mon retour à ce sujet est qu'en tant qu'utilisateur, je ne choisirais pas de l'utiliser plutôt que la deuxième alternative en raison de la difficulté de garder les choses synchronisées entre deux listes.

Discuté lors de la réunion des développeurs aujourd'hui.

Court résumé

  1. @jorisvandenbossche essaiera d'implémenter gr.agg(b_sum=("B", "sum), ...) , c'est-à-dire qu'en l'absence de arg transmis à *GroupBy.agg , interpréter kwargs comme <output_name>=(<selection>, <aggfunc>)
  2. Orthogonalement à ces problèmes, nous aimerions implémenter MutliIndex.flatten et fournir un mot-clé flatten=True à .agg

Peut-être que cela aide : ma solution de contournement pour la dépréciation sont ces fonctions d'assistance qui remplacent les cartes alias->aggr par une liste de fonctions correctement nommées :

def aliased_aggr(aggr, name):
    if isinstance(aggr,str):
        def f(data):
            return data.agg(aggr)
    else:
        def f(data):
            return aggr(data)
    f.__name__ = name
    return f

def convert_aggr_spec(aggr_spec):
    return {
        col : [ 
            aliased_aggr(aggr,alias) for alias, aggr in aggr_map.items() 
        ]  
        for col, aggr_map in aggr_spec.items() 
    }

ce qui donne l'ancien comportement avec :

mydf_agg = mydf.groupby('cat').agg(convert_aggr_spec{
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
}))

qui est le même que

mydf_agg = mydf.groupby('cat').agg({
    'energy': [ 
        aliased_aggr('sum', 'total_energy'),
        aliased_aggr(lambda x: np.percentile(x, 98), 'energy_p98'),
        aliased_aggr(lambda x: np.percentile(x, 17), 'energy_p17')
    ],
    'distance': [
         aliased_aggr('sum', 'total_distance'),
         aliased_aggr('mean', 'average_distance'),
         aliased_aggr(smrb.mad, 'distance_mad'),
         aliased_aggr(mad_c1, 'distance_mad_c1'),
    ]
})

Cela fonctionne pour moi, mais ne fonctionnera probablement pas dans certains cas difficiles ...

Mise à jour : a découvert que le renommage n'est pas nécessaire, car les tuples dans une spécification d'agrégation sont interprétés comme (alias, aggr). La fonction alias_aggr n'est donc pas nécessaire et la conversion devient :

def convert_aggr_spec(aggr_spec):
    return {
        col : [ 
           (alias,aggr) for alias, aggr in aggr_map.items() 
        ]  
        for col, aggr_map in aggr_spec.items() 
    }

Je veux juste sonner ici comme un autre utilisateur qui manque vraiment, vraiment la fonctionnalité d'agréger une colonne sur n'importe quelle fonction et de la renommer immédiatement dans la même ligne. Je ne me suis jamais retrouvé à utiliser le MultiIndex renvoyé par les pandas - soit je l'aplatis immédiatement, soit je veux en fait spécifier manuellement mes noms de colonnes car ils signifient en fait quelque chose de spécifique.

Je serais satisfait de l'une des approches proposées ici : syntaxe de type SQL (en fait, je me retrouve déjà à utiliser beaucoup .query() dans les pandas), en revenant au comportement déprécié, à l'une des autres suggestions. L'approche actuelle m'a déjà ridiculisé de la part de collègues qui utilisent R.

Je me suis même récemment retrouvé à utiliser PySpark au lieu de pandas même si ce n'était pas nécessaire, simplement parce que j'aime beaucoup plus la syntaxe :

df.groupby("whatever").agg(
    F.max("col1").alias("my_max_col"),
    F.avg("age_col").alias("average_age"),
    F.sum("col2").alias("total_yearly_payments")
)

De plus, PySpark est beaucoup plus compliqué à écrire que les pandas dans la plupart des cas, cela a l'air tellement plus propre! J'apprécie donc vraiment que le travail à ce sujet soit encore à faire :-)

Je pense que nous avons une syntaxe convenue pour cette fonctionnalité ; nous avons besoin de quelqu'un pour
Mettre en œuvre.

Le mer. 27 mars 2019 à 9h01 Thomas Kastl [email protected]
a écrit:

Je veux juste intervenir ici en tant qu'encore un autre utilisateur qui est vraiment, vraiment
manquant la fonctionnalité d'agrégation d'une colonne sur n'importe quelle fonction et
en le renommant immédiatement dans la même rangée. je ne me suis
en utilisant le MultiIndex retourné par les pandas - soit je l'aplatit immédiatement,
ou je veux en fait spécifier manuellement mes noms de colonnes car ils
signifie en fait quelque chose de spécifique.

Je serais satisfait de l'une des approches proposées ici : syntaxe de type SQL
(En fait, je me retrouve déjà à utiliser beaucoup .query() dans les pandas),
revenir au comportement déprécié, l'une des autres suggestions. le
l'approche actuelle m'a déjà ridiculisé de la part de collègues qui utilisent R.

Je me suis même récemment retrouvé à utiliser PySpark au lieu de pandas même si
ce n'était pas nécessaire, juste parce que j'aime beaucoup plus la syntaxe :

df.groupby("whatever").agg( F.max("col1").alias("my_max_col"),
F.avg("age_col").alias("average_age"),
F.sum("col2").alias("total_yearly_payments") )

De plus, PySpark est beaucoup plus compliqué à écrire que les pandas dans la plupart des cas,
ça a l'air tellement plus propre ! J'apprécie donc vraiment ce travail sur
c'est encore à faire :-)

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/pandas-dev/pandas/issues/18366#issuecomment-477168767 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABQHIkCYYsah5siYA4_z0oop_ufIB3h8ks5va3nJgaJpZM4QjSLL
.

Je vais essayer d'y arriver pour 0.25.0

J'ai mis en place un PR sur https://github.com/pandas-dev/pandas/pull/26399. L'idée de base est de permettre ce mélange de renommer et d'agrégation spécifique à une colonne en utilisant **kwargs entendu que les valeurs doivent être des tuples de (selection, aggfunc) .

In [2]: df = 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 [3]: df
Out[3]:
  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 [4]: df.groupby('kind').agg(min_height=('height', 'min'), max_weight=('weight', 'max'))
Out[4]:
      min_height  max_weight
kind
cat          9.1         9.9
dog          6.0       198.0

Cela a quelques limites

  • C'est un peu particulier au reste des pandas. Le sytanx (output_name=(selection, aggfunc)) n'apparaît vraiment nulle part ailleurs (bien que .assign utilise le modèle output_name=... )
  • L'orthographe des noms de sortie qui ne sont pas des identifiants python est moche : .agg(**{'output name': (col, func)})
  • C'est uniquement Python 3.6+, ou nous avons besoin de quelques hacks laids pour 3.5 et versions antérieures, car l'ordre de **kwargs n'était pas précédemment préservé
  • L'aggfunc doit être une fonction unaire. Si votre aggfunc personnalisé a besoin d'arguments supplémentaires, vous devrez d'abord l'appliquer partiellement

Et il y a un détail d'implémentation, plusieurs lambda aggfuncs pour la même colonne ne sont pas encore pris en charge, bien que cela puisse être corrigé plus tard.


Je soupçonne que la plupart des personnes inscrites ici soutiendraient une alternative au comportement déconseillé. Que pensent les gens de celui-ci en particulier ?

cc @WillAyd si j'ai raté l'une de vos préoccupations.

Salut @TomAugspurger ,

Merci d'avoir fait avancer ce chemin.

Cela a quelques limites

  • C'est un peu particulier au reste des pandas. Le sytanx (output_name=(selection, aggfunc)) n'apparaît vraiment nulle part ailleurs (bien que .assign utilise le modèle output_name=... )

Je ne peux pas m'empêcher de penser que ce genre d'argument semble assez similaire à celui qui a motivé la dépréciation de l'implémentation existante en premier lieu.

Pourriez-vous expliquer pourquoi nous bénéficions davantage de cette nouvelle méthode par rapport à l'ancienne _en ce qui concerne cet argument particulier_ ?

Un avantage auquel je pouvais déjà penser est que (pour py3.6+) nous pouvons sélectionner l'ordre de sortie des colonnes individuellement.

  • L'orthographe des noms de sortie qui ne sont pas des identifiants python est moche : .agg(**{'output name': (col, func)})

D'une certaine manière, l'ancienne méthode était meilleure à cet égard. Mais comme je l'ai dit plus tôt, tant qu'il est possible d'utiliser la construction **{...} pour construire l'agrégation dynamiquement, je serais assez heureux.

  • C'est uniquement Python 3.6+, ou nous avons besoin de quelques hacks laids pour 3.5 et versions antérieures, car l'ordre de **kwargs n'a pas été précédemment préservé

Comment cela fonctionnait-il avant (fonctionnalité dict-of-dict existante) ? la commande était-elle garantie d'une manière ou d'une autre ?

  • L'aggfunc doit être une fonction unaire. Si votre aggfunc personnalisé a besoin d'arguments supplémentaires, vous devrez d'abord l'appliquer partiellement

Juste pour confirmer ma compréhension : l'aggfunc peut être n'importe quel appelable qui renvoie une valeur valide, n'est-ce pas ? (en plus des aggfungs de chaîne "souvent utilisés" comme 'min' , 'max' , etc. ). Y a-t-il une différence par rapport à avant ? (c'est-à-dire que la limitation unaire n'était pas déjà présente ?)

Et il y a un détail d'implémentation, plusieurs lambda aggfuncs pour la même colonne ne sont pas encore pris en charge, bien que cela puisse être corrigé plus tard.

Oui, celui-là est un peu ennuyeux, mais tant qu'il ne s'agit que d'une limitation temporaire et qu'il est possible de résoudre ce problème, cela pourrait fonctionner.

Je soupçonne que la plupart des personnes inscrites ici soutiendraient une alternative au comportement déconseillé. Que pensent les gens de celui-ci en particulier ?

Eh bien, dans tous les cas, je pense qu'agréger et renommer en une seule étape est vraiment important à conserver. Si l'ancien comportement n'est vraiment pas une option, alors cette alternative pourrait faire l'affaire.

Pourriez-vous expliquer pourquoi nous bénéficions davantage de cette nouvelle méthode par rapport à l'ancienne en ce qui concerne cet argument particulier.

Je me souviens peut-être mal, mais je pense que SeriesGroupby.agg et DataFrameGroupby.agg ont des significations différentes entre la clé externe d'un dictionnaire (s'agit-il d'une sélection de colonne ou d'un nom de sortie ?). Avec cette syntaxe, nous pouvons systématiquement faire en sorte que le mot-clé signifie le nom de la sortie.

D'une certaine manière, l'ancienne méthode était meilleure à cet égard.

La différence est-elle juste le ** ? Sinon, je pense que les mêmes limitations sont partagées.

Comment cela fonctionnait-il avant (fonctionnalité dict-of-dict existante) ? la commande était-elle garantie d'une manière ou d'une autre ?

Trier les clés, c'est ce que je fais maintenant dans les relations publiques.

Juste pour confirmer ma compréhension : l'aggfunc peut être n'importe quel appelable qui renvoie une valeur valide, n'est-ce pas ?

Voici la différence

In [21]: df = pd.DataFrame({"A": ['a', 'a'], 'B': [1, 2], 'C': [3, 4]})

In [22]: def aggfunc(x, myarg=None):
    ...:     print(myarg)
    ...:     return sum(x)
    ...:

In [23]: df.groupby("A").agg({'B': {'foo': aggfunc}}, myarg='bar')
/Users/taugspurger/sandbox/pandas/pandas/core/groupby/generic.py:1308: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  return super().aggregate(arg, *args, **kwargs)
None
Out[23]:
    B
  foo
A
a   3

avec la proposition alternative, nous réservons **kwargs pour les noms de colonnes de sortie. Vous auriez donc besoin de functools.partitial(aggfunc, myarg='bar') .

Ok merci, je pense que l'approche proposée est 👍 pour une première itération (et sera vraiment ok en remplacement dès que la limitation d'implémentation lambda multiple sera supprimée)

Cette page vous a été utile?
0 / 5 - 0 notes