Pandas: Die Einstellung der Umbenennung von Diktaten in groupby.agg bringt viele Probleme mit sich

Erstellt am 19. Nov. 2017  ·  37Kommentare  ·  Quelle: pandas-dev/pandas

Dieses Problem wurde basierend auf der Diskussion von #15931 erstellt, nachdem die Umbenennung von Diktaten in groupby.agg . Vieles von dem, was im Folgenden zusammengefasst wird, wurde bereits in der vorherigen Diskussion besprochen. Empfehlen würde ich insbesondere https://github.com/pandas-dev/pandas/pull/15931#issuecomment -36139085 wo die Probleme auch klar genannt werden.

Die Motivation hinter der Einstellung von #15931 war hauptsächlich damit verbunden, eine konsistente Schnittstelle für agg() zwischen Series und Dataframe zu schaffen (siehe auch #14668 für den Kontext).

Die Umbenennungsfunktionalität mit einem verschachtelten Diktat wurde von einigen als zu komplex und/oder inkonsistent beschrieben und daher veraltet.

Dies hat jedoch seinen Preis: Die Unmöglichkeit, gleichzeitig zu aggregieren und umzubenennen, führt zu sehr ärgerlichen Problemen und einer gewissen Rückwärtsinkompatibilität, bei der kein vernünftiger Workaround verfügbar ist:

  • _[ärgerlich]_ keine Kontrolle mehr über die Namen der resultierenden Spalten
  • _[ärgerlich]_ Sie müssen einen Weg finden, den MultiIndex umzubenennen, _nach_ der Aggregation durchgeführt wurde, was es erfordert, die Reihenfolge der Spalten an zwei Stellen im Code zu verfolgen.... überhaupt nicht praktikabel und manchmal geradezu unmöglich (Fälle unten ).
  • ⚠️ _ [breaking] _ kann nicht mehr als ein Callable mit demselben internen Namen auf dieselbe Eingabespalte anwenden. Daraus ergeben sich zwei Unterfälle:

    • _ [breaking] _ Sie können nicht mehr zwei oder mehr Lambda-Aggregatoren auf dieselbe Spalte anwenden

    • _ [breaking] _ Sie können nicht mehr zwei oder mehr Aggregatoren von Teilfunktionen anwenden, es sei denn, Sie ändern ihr verstecktes __name__ Attribut

Beispiel

_(Bitte beachten Sie, dass dies ein ausgearbeitetes Beispiel ist, um das Problem in einem so kurzen Code wie möglich zu demonstrieren, aber alle hier gezeigten Probleme haben mich seit der Änderung im wirklichen Leben gebissen, und in Situationen, die nicht so einfach sind wie hier )_

Eingabedatenrahmen

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

Vor:

einfach zu schreiben und zu lesen und funktioniert wie erwartet

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
    },
})

ergibt sich

          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

und alles was übrig bleibt ist:

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

Fröhliches Tanzen, das Pandas lobt 💃 🕺 !

Nach

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
    ],
})

Das Obige bricht ab, weil die Lambda-Funktionen alle zu Spalten mit dem Namen <lambda> was zu führt

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

Rückwärts inkompatible Regression: Man kann nicht mehr zwei verschiedene Lambdas auf dieselbe ursprüngliche Spalte anwenden.

Wenn man das lambda x: np.percentile(x, 98) von oben entfernt, bekommen wir das gleiche Problem mit der partiellen Funktion, die den Funktionsnamen von der ursprünglichen Funktion erbt:

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

Nachdem wir schließlich das Attribut __name__ des Teils überschrieben haben (zum Beispiel mit mad_c1.__name__ = 'mad_c1' ) erhalten wir:

    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

mit still

  • eine Spalte fehlt (98. Perzentil)
  • die Handhabung der MultiIndex-Spalten
  • und die Umbenennung der Spalten

in einem separaten Schritt zu behandeln.

Es ist keine Kontrolle für die Spaltennamen nach der Aggregation möglich, das Beste, was wir auf automatisierte Weise erreichen können, ist eine Kombination aus dem ursprünglichen Spaltennamen und dem Namen der _Aggregatfunktion_ wie folgt:

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

was in ... resultiert:

     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

und wenn Sie wirklich andere Namen haben müssen, können Sie es so machen:

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

aber das bedeutet, dass Sie darauf achten müssen, den Umbenennungscode (der sich jetzt an einer anderen Stelle im Code befinden muss) mit dem Code synchron zu halten, in dem die Aggregation definiert ist...

Sad Panda User 😢 (der natürlich immer noch Pandas liebt)


Ich bin voll und ganz für Konsistenz und bedauere gleichzeitig zutiefst die Einstellung der _Aggregate- und Umbenennungsfunktion_. Ich hoffe, die obigen Beispiele verdeutlichen die Schmerzpunkte.


Mögliche Lösungen

  • Nicht mehr veraltete Funktionen zur Umbenennung von Diktaten
  • Stellen Sie eine andere API bereit, um dies zu tun (aber warum sollte es zwei Methoden für denselben Hauptzweck geben, nämlich Aggregation?)
  • ??? (offen für Vorschläge)

_Optional lesen:_

In Bezug auf die oben erwähnte Diskussion im Pull-Request, die bereits seit einigen Monaten läuft, ist mir erst vor kurzem ein Grund klar geworden, warum mich diese Abwertung so sehr stört: "aggregieren und umbenennen" ist eine Selbstverständlichkeit GROUP BY-Aggregationen in SQL, da Sie in SQL normalerweise den Zielspaltennamen direkt neben dem Aggregationsausdruck angeben, zB SELECT col1, avg(col2) AS col2_mean, stddev(col2) AS col2_var FROM mytable GROUP BY col1 .

Ich sage _nicht_ , dass Pandas natürlich unbedingt die gleichen Funktionalitäten wie SQL bieten sollten. Aber die oben aufgeführten Beispiele zeigen, warum die dict-of-dict-API meiner Meinung nach eine saubere und einfache Lösung für viele Anwendungsfälle war.

(* Ich persönlich stimme nicht zu, dass der Diktier-Ansatz komplex ist.)

API Design Groupby

Hilfreichster Kommentar

Für alles, was es wert ist, bin ich auch stark dafür, die Funktionalität nicht abzuwerten.

Ein wichtiger Grund für mich ist, dass es etwas zutiefst seltsam ist, den Funktionsnamensraum von Python (was mit der jeweiligen Implementierung zu tun hat) mit den Daten der Spaltennamen zu mischen (etwas, das von der Implementierung sicherlich nicht wissen sollte). Die Tatsache, dass wir Spalten (möglicherweise mehrere Spalten) mit dem Namen '<lambda>' verursacht bei mir schwere kognitive Dissonanz.

Der Umbenennungsansatz scheitert, denn es gibt diesen Zwischenschritt, bei dem unnötige (und offengelegte) Spaltennamen herumgeschleppt werden. Darüber hinaus sind sie schwer zuverlässig und systematisch umzubenennen, da potenzielle Abhängigkeiten von der Implementierung bestehen.

Abgesehen davon ist die verschachtelte dict-Funktionalität zwar komplex, aber es ist eine komplexe Operation, die durchgeführt wird.

TL;DR Bitte nicht abwerten. :)

Alle 37 Kommentare

@zertrin : Danke für die Zusammenstellung. Ich habe gesehen, dass es in #15931 viele Diskussionen darüber gab. Da ich dies noch nicht vollständig lesen konnte, kann ich dazu im Moment noch nichts dazu sagen. Lass mich trotzdem pingen:

@jreback @jorisvandenbossche @TomAugspurger @chris-b1

Ich stimme zu, dass das Umbenennen mit der aktuellen agg Implementierung in diesem Beispiel sehr umständlich und fehlerhaft ist. Die verschachtelten Diktate sind etwas komplex, aber wenn Sie sie so schreiben, wie Sie es getan haben, wird sehr deutlich, was passiert.

Ich nehme an, es könnte ein names Parameter zu agg hinzugefügt werden, der dazu führen würde, dass das Wörterbuch die aggregierenden Spalten ihren neuen Namen zuordnet. Sie könnten sogar einen weiteren Parameter drop_index als booleschen

Die Syntax würde sich also in folgendes verwandeln:

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)

Oder vielleicht könnte eine ganz neue Methode agg_assign erstellt werden, die ähnlich wie DataFrame.assign funktionieren würde:

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

Diese Option gefällt mir eigentlich viel besser.

Für alles, was es wert ist, bin ich auch stark dafür, die Funktionalität nicht abzuwerten.

Ein wichtiger Grund für mich ist, dass es etwas zutiefst seltsam ist, den Funktionsnamensraum von Python (was mit der jeweiligen Implementierung zu tun hat) mit den Daten der Spaltennamen zu mischen (etwas, das von der Implementierung sicherlich nicht wissen sollte). Die Tatsache, dass wir Spalten (möglicherweise mehrere Spalten) mit dem Namen '<lambda>' verursacht bei mir schwere kognitive Dissonanz.

Der Umbenennungsansatz scheitert, denn es gibt diesen Zwischenschritt, bei dem unnötige (und offengelegte) Spaltennamen herumgeschleppt werden. Darüber hinaus sind sie schwer zuverlässig und systematisch umzubenennen, da potenzielle Abhängigkeiten von der Implementierung bestehen.

Abgesehen davon ist die verschachtelte dict-Funktionalität zwar komplex, aber es ist eine komplexe Operation, die durchgeführt wird.

TL;DR Bitte nicht abwerten. :)

Mein Beitrag ist von zwei Dingen motiviert.

  1. Ich bin mir der Motivation bewusst und stimme ihr zu, die aufgeblähte API von Pandas zu reduzieren. Auch wenn ich in Bezug auf die wahrgenommene Motivation, "aufgeblähte" API-Elemente zu reduzieren, fehlgeleitet bin, bin ich immer noch der Meinung, dass die API von Pandas gestrafft werden könnte.
  2. Ich denke, es ist besser, ein gutes Kochbuch mit guten Rezepten zu haben, als APIs bereitzustellen, um alle Wünsche und Bedürfnisse zu befriedigen. Ich behaupte nicht , dass die Umbenennung über verschachtelte Wörterbücher als befriedigende Launen gilt, da sie bereits existierte, und wir diskutieren ihre Einstellung. Aber es liegt im Spektrum zwischen optimierter API und etwas ... anderem.

Außerdem verfügten die Pandas Series- und DataFrame-Objekte über pipe Methoden, um das Pipelining zu erleichtern. In diesem Dokumentabschnitt wird diskutiert, dass wir pipe anstelle von Unterklassen als Proxy für Methoden verwenden könnten. Im gleichen Sinne könnten wir das neue GroupBy.pipe , um eine ähnliche Rolle zu erfüllen und es uns zu ermöglichen, Proxy-Methoden für groupby-Objekte zu erstellen.

Ich werde das Beispiel von @zertrin verwenden

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)

Was in ... resultiert

            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

Die Methode pipe macht das Hinzufügen neuer APIs in vielen Fällen unnötig. Es bietet auch die Möglichkeit, die veraltete Funktionalität, die wir besprechen, zu ersetzen. Daher würde ich geneigt sein, mit der Einstellung fortzufahren.

Ich mag die Idee von tdpetrou wirklich - zu verwenden: names=name_dict .

Dies kann jeden glücklich machen. Es gibt uns die Möglichkeit, Spalten einfach nach Belieben umzubenennen.

Nicht wirklich, wie in meinem ersten Beitrag erwähnt, würde dies das Problem der Entkopplung des Ortes, an dem die Aggregatoperation definiert ist, vom Namen der resultierenden Spalte nicht lösen, was einen zusätzlichen Aufwand erfordert, um sicherzustellen, dass beide "synchronisiert" sind.

Ich sage nicht, dass das eine schlechte Lösung ist (schließlich löst es die anderen Probleme), aber es wäre nicht so einfach und klar wie der Ansatz des Diktats. Ich meine hier, dass Sie zum Zeitpunkt des Schreibens beide Listendikte synchronisiert halten müssen, und beim Lesen der Quelle muss sich der Leser bemühen, die Namen im zweiten Listendikt mit der aggregierten Definition im ersten Listendikt abzugleichen. Das ist jeweils der doppelte Aufwand.

Die verschachtelten Diktate sind etwas komplex, aber wenn Sie sie so schreiben, wie Sie es getan haben, wird sehr deutlich, was passiert.

Ich verstehe immer noch nicht, warum jeder zu sagen scheint, dass das Diktat komplex ist. Für mich ist das der klarste Weg.

Das heißt, wenn das Schlüsselwort names die einzige Lösung ist, mit der das Pandas-Team zufrieden ist, wäre dies immer noch eine Verbesserung gegenüber der aktuellen Situation.

@pirsquared interessante Lösung mit aktueller API. Wenn auch meiner Meinung nach nicht ganz einfach zu verstehen (ich verstehe nicht so recht wie das geht :verwirrt: )

Ich habe einen Thread im Datascience-Subreddit gestartet - Was hasst du an Pandas? . Jemand brachte seine Verachtung für den zurückgegebenen MultiIndex nach einem groupby zum Ausdruck und zeigte auf das dplyr do Verb, das in plydata implementiert agg_assign , das war also ziemlich interessant.

@zertrin agg_assign wäre Ihrem dict-of-dict-Ansatz überlegen und mit SQL-Aggregationen identisch und ermöglicht es, dass mehrere Spalten innerhalb der Aggregation miteinander interagieren. Es würde auch genauso funktionieren wie DataFrame.assign .

Irgendwelche Gedanken @jreback @TomAugspurger ?

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

Obwohl dies das Problem löst, müssen Schlüssel und Werte an zwei Stellen ausgerichtet werden. Ich denke, eine API (wie für .agg_assign ), die keinen solchen Buchhaltungscode erfordert, ist weniger fehleranfällig.

Es gibt auch das Problem des Bereinigungscodes nach der Verwendung der API. Wenn groupby Operationen einen MultiIndex Datenrahmen zurückgeben, macht der Benutzer in den meisten Fällen den MultiIndex rückgängig. Der einfache deklarative Weg, .agg_assign , schlägt keine Hierarchie vor, keine MultiIndex Ausgabe, kein anschließendes Aufräumen.

Basierend auf den Nutzungsmustern denke ich, dass Multi-Index-Ausgaben ausschließlich Opt-in und nicht Opt-out sein sollten.

Ich war anfangs skeptisch gegenüber dem Vorschlag von agg_assign , aber die letzten beiden Kommentare haben mich davon überzeugt, dass dies eine gute Lösung sein könnte.

Vor allem über die Möglichkeit nachzudenken, es in der Form agg_assign(**relabeling_dict) und somit meine relabeling_dict so definieren zu können:

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

Das wäre ziemlich flexibel und löst alle in meinem OP genannten Probleme.

@zertrin @has2k1

Ich habe etwas mehr darüber nachgedacht und diese Funktionalität existiert bereits mit apply . Sie geben einfach eine Reihe mit Index als neue Spaltennamen und Werte als Aggregation zurück. Dies ermöglicht Leerzeichen im Namen und gibt Ihnen die Möglichkeit, Spalten genau nach Ihren Wünschen anzuordnen:

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)

Es besteht also möglicherweise keine Notwendigkeit für eine neue Methode und stattdessen nur ein besseres Beispiel in den Dokumenten.

@tdpetrou , du hast apply funktioniert, da ich meine eigene Version wegen der doppelten Ausführung im Schnell-Langsam-Pfadauswahlprozess verwende.

Hum in der Tat, es besteht keine Chance, dass ich daran gedacht hätte, es in einem Aggregationskontext zu verwenden, nur indem ich das Dokument lese ...
Außerdem finde ich die Lösung mit apply noch etwas zu umständlich. Der Ansatz von agg_assign schien einfacher und verständlicher zu sein.

Da es nie wirklich eine Aussage dazu gab, kommt der dict-of-dict Ansatz (der zwar derzeit zwar veraltet, aber bereits implementiert ist und auch all diese Probleme löst) wirklich definitiv nicht in Frage?

Abgesehen vom agg_assign Ansatz scheint dict-of-dict immer noch der einfachste Ansatz zu sein und benötigt keine Codierung, nur nicht veraltet.

Der Vor- und Nachteil des agg_assign Ansatzes besteht darin, dass die Spaltenauswahl in die Aggregationsmethode verschoben wird . In allen Beispielen ist das an x übergebene lambda etwa self.get_group(group) für jede Gruppe in self , einem DataFrameGroupBy Objekt. Das ist schön, weil es die Benennung , die in **kwargs , sauber von der Auswahl trennt, die in der Funktion steht.

Der Nachteil ist, dass sich Ihre netten, generischen Aggregationsfunktionen jetzt mit der Spaltenauswahl befassen müssen. Es gibt kein kostenloses Mittagessen! Das bedeutet, dass Sie viele Helfer wie lambda x: x[col].min . Sie müssen auch vorsichtig sein mit Dingen wie np.min , die über alle Dimensionen reduziert werden, vs. pd.DataFrame.min , die über axis=0 reduziert werden. Deshalb wäre etwas wie agg_assign nicht gleichbedeutend mit apply . apply arbeitet für bestimmte Methoden immer noch spaltenweise.

Ich bin mir bei diesen Kompromissen gegenüber der Methode des Diktats nicht sicher, aber ich bin neugierig, die Gedanken anderer Leute zu hören. Hier ist eine grobe Skizze von agg_assign , die ich aufgerufen habe, die ich agg_table , um zu betonen, dass die Funktionen an die Tabellen und nicht an die Spalten übergeben werden:

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

Verwendung

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

Ich vermute, wir könnten ein wenig tun, um die Leistung davon weniger schrecklich zu machen, aber nicht annähernd so viel wie .agg tut...

Könnte jemand vom Pandas-Kernteam bitte erklären, was der Hauptgrund dafür ist, dass die Umbenennung von Diktaten in groupby.agg abgelehnt wird?

Ich könnte leicht verstehen, wenn die Wartung des Codes zu viele Probleme verursacht, aber wenn es um Komplexität für den Endbenutzer geht, würde ich mich auch dafür entscheiden, ihn zurückzubringen, da es im Vergleich zu den erforderlichen Problemumgehungen ziemlich klar ist ...

Danke schön!

Könnte jemand vom Pandas Core Team bitte erklären, was der Hauptgrund dafür ist, dass die Umbenennung von Diktaten in groupby.agg eingestellt wird?

Hast du https://github.com/pandas-dev/pandas/pull/15931/files#diff -52364fb643114f3349390ad6bcf24d8fR461 gesehen?

Der Hauptgrund war, dass Diktiertasten überladen waren, um zwei Dinge zu tun. Bei Series / SeriesGroupBy dienen sie der Benennung. Bei DataFrame/DataFrameGroupBy dienen sie zum Auswählen einer Spalte.

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

Dies ist nicht das verwirrend , was wahrscheinlich in Pandas, so vielleicht wir es überdenken könnten :) ich vermutlich habe einige Grenzfälle fehlen. Aber selbst wenn wir Aggregationen von Diktaten entfernen, besteht immer noch die Inkonsistenz zwischen der Benennung und der Spaltenauswahl:

Bei Series / SeriesGroupBy dienen die Wörterbuchschlüssel immer der Benennung der Ausgabe.

Bei DataFrame / DataFrameGroupby stehen die Diktiertasten immer zur Auswahl. Bei dict-of-dicts wählen wir eine Spalte aus, und dann dient das innere Diktat zur Benennung der Ausgabe, genau wie Series / SeriesGroupBy.

Wir haben dies zuvor kurz besprochen (irgendwo in der langen Diskussion über die Einstellung), und ich habe hier etwas Ähnliches vorgeschlagen: https://github.com/pandas-dev/pandas/pull/14668#issuecomment -274508089. Aber am Ende wurde nur die Deprecation implementiert und nicht die Ideen, die andere Funktionalität der Verwendung von dicts (die 'Umbenennung'-Funktion) zu vereinfachen.

Das Problem war, dass dicts sowohl für die 'Auswahl' (auf welche Spalte soll diese Funktion angewendet werden) als auch für die 'Umbenennung' (was der resultierende Spaltenname bei der Anwendung dieser Funktion sein sollte) verwendet wurden. Eine alternative Syntax, abgesehen von dicts, könnten Schlüsselwortargumente sein, wie hier im agg_assign Vorschlag diskutiert wird.
Ich bin immer noch dafür, diese Möglichkeit zu erkunden, sei es in agg selbst oder in einer neuen Methode wie agg_assign .

Was ich damals vorgeschlagen habe, war etwas Ähnliches wie agg_assign aber mit einem dict pro Schlüsselwort anstelle einer Lambda-Funktion. Übersetzt auf das Beispiel hier wäre dies in etwa so:

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

Ich bin mir nicht sicher, ob dies unbedingt lesbarer oder einfacher zu schreiben ist als die Version mit allen Lambdas, aber diese könnte möglicherweise performanter sein, da Pandas immer noch die optimierten Implementierungen für Summe, Mittelwert usw. in den Spalten verwenden können, in denen Sie dies tun keine Lambda- oder benutzerdefinierte Funktion haben.

Eine große Frage bei diesem Ansatz wäre, was df.groupby('cat').agg(foo='mean') bedeuten würde? Das würde logischerweise 'Mittelwert' auf alle Spalten anwenden, da Sie keine Auswahl getroffen haben (ähnlich wie zuvor {'col1' : {'foo': 'mean'}, 'col2': {'foo':'mean'}, 'col3': ...} ). Aber das würde zu mehrfach indizierten Spalten führen, während ich im obigen Beispiel denke, dass es schön wäre, nicht mit MI-Spalten zu enden.

Ich denke, das Obige kann innerhalb des vorhandenen agg abwärtskompatibel gemacht werden, aber die Frage ist, ob dies erforderlich ist.
Ich denke auch, dass sich dies gut auf den Fall series erstrecken würde:

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

(und Sie könnten sogar in Betracht ziehen, das obige einmal für "Entfernung" und einmal für "Energie" zu tun und das Ergebnis zu verketten, wenn Sie nicht alle Diktate / Lambdas mögen)

@TomAugspurger In Ihrer einfachen Implementierung von agg_table wäre es nicht besser, über die verschiedenen anzuwendenden Funktionen zu iterieren, anstatt die Gruppen zu iterieren und am Ende die neuen Spalten mit axis=1 zu verketten anstatt die neu gebildeten Zeilen mit axis=0 zu verketten?

Übrigens , @tdpetrou @smcateer @pirsquared und andere, vielen Dank, dass Sie dieses Thema sehr wichtig!

Ich mag das von @tdpetrou vorgeschlagene Muster wirklich

Wenn die Funktion pd.Series(data, index=data.keys()) zurückgibt, erhalten wir die Indizes garantiert in der richtigen Reihenfolge? (Ich denke nur darüber nach, wie ich das Muster am besten in meinen Code implementiere - auf die Gefahr hin, vom Thema abzudriften).

Bearbeiten: Entschuldigung, ich habe den Punkt des Indexarguments falsch verstanden (es ist hier optional, nur erforderlich, wenn Sie die Reihenfolge der Spalten angeben möchten - die Rückgabe von pd.Series(data) erledigt die Arbeit für mich).

Würde das Beispiel von @tdpetrou mit first & last Aggregationen funktionieren?

Ich musste so auf Kopf/Schwanz zurückgreifen

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)

Ich würde das immer noch gerne ansprechen, aber ich glaube nicht, dass es für 0,23 getan wird.

Könnte der Ansatz von @tdpetrou funktionieren, ohne eine Funktion zu definieren, die wir nie wieder in unserem Code verwenden werden? Aus einer Q/Kdb+-Welt (ähnlich wie SQL) kommend, bin ich verwirrt, warum wir für eine einfache select-Anweisung eine temporale Variable/Funktion erstellen müssen.

OP hier.

Ehrlich gesagt bin ich nach all der Zeit und den vielen Diskussionen in #15931 und hier immer noch nicht davon überzeugt, dass dies eine gute Idee ist, die Umbenennung von Diktaten abzulehnen.

Letztendlich ist keine der hier vorgeschlagenen Alternativen für die Benutzer intuitiver als der aktuelle Relabeling-Diktat-Ansatz IMHO. Als es in der Dokumentation stand, war nur an einem Beispiel klar, wie das funktioniert, und es ist sehr flexibel.

Natürlich können Panda-Entwickler immer noch anders denken, indem sie sich nur der Sicht eines Benutzers anschließen.

Auch der Relabeling-Diktat-Ansatz ist nicht sehr intuitiv. Meiner Meinung nach sollte die Syntax SQL ähnlich sein - func(column_name) as new_column_name . In Python können wir dies mit einem Tupel aus drei Elementen tun. (func, column_name, new_column_name) . So führt dexplo die Gruppierung nach Gruppen durch.

dexplo

@zertrin hast du Feedback zu meinem obigen Vorschlag: https://github.com/pandas-dev/pandas/issues/18366/#issuecomment -349089667
Am Ende kehrt es die Reihenfolge des Diktats irgendwie um: Anstelle von "{col: {name: func}}" wäre es eine Art "**{name: {col: func}}"

@jorisvandenbossche Ich habe deinen Ansatz berücksichtigt. Die Sache ist, ich sehe nicht wirklich, welche zusätzlichen Vorteile es gegenüber dem aktuellen Ansatz bringt.

Um es deutlicher auszudrücken, angesichts der folgenden Auswahlmöglichkeiten:

  1. Nicht veraltetes aktuelles Verhalten, das gut funktioniert (ein paar Zeilen veralteten Codes zum Entfernen, fügen Sie die entfernte Dokumentation erneut hinzu)
  2. Implementieren Sie Ihren Vorschlag (erhebliche Änderungen im Code, Verabschiedung des aktuellen Ansatzes, Notwendigkeit für alle Benutzer, ihren Code anzupassen)

Ich sehe nicht, warum wir 2 wählen sollten, es sei denn, es bringt aus Entwickler- und Benutzerperspektive sinnvolle und greifbare Vorteile.

Um einige der Punkte in Ihrem obigen Vorschlag anzusprechen:

Das Problem war, dass dicts sowohl für die 'Auswahl' (auf welche Spalte soll diese Funktion angewendet werden) als auch für die 'Umbenennung' (was der resultierende Spaltenname bei der Anwendung dieser Funktion sein sollte) verwendet wurden.

Da es vorher gut dokumentiert war, glaube ich nicht, dass es ein Problem für Benutzer war . Persönlich habe ich sofort den Punkt verstanden, als ich mir die Beispiele in der Dokumentation ansah. (EDIT: und ich dachte: _"yay! sehr nützliches Konstrukt, es entspricht genau dem, wonach ich gesucht habe. Schön."_)

Eine alternative Syntax, abgesehen von dicts, könnten Schlüsselwortargumente sein

Eine der attraktiven Eigenschaften des Diktier-von-Diktat-Ansatzes besteht darin, dass Benutzer ihn leicht dynamisch mit einem anderen Code generieren können. Wie Sie in dem Kommentar direkt über diesem erwähnt haben, würde der Wechsel zu Schlüsselwortargumenten wie in Ihrem Vorschlag dies immer noch über das Konstrukt **{name: {col: func}} . Ich bin also nicht gegen Ihren Vorschlag. Ich sehe den Mehrwert und die Notwendigkeit solcher Veränderungen einfach nicht, wenn wir mit dem aktuell implementierten System bereits den gleichen Funktionsumfang erreichen.

Am Ende wäre Ihr Vorschlag _okay_, wenn Pandas Core-Entwickler ein starkes Gefühl gegen den aktuellen Ansatz haben. Ich sehe als _user_ einfach keinen Vorteil. (Tatsächlich sehe ich den Nachteil darin, den gesamten vorhandenen Benutzercode zu ändern, damit er mit dem neuen Vorschlag wieder funktioniert).

@zertrin wir haben das gestern mit einigen Core-Entwicklern besprochen, sind aber nicht dazu gekommen, die Zusammenfassung hier zu schreiben. Das werde ich jetzt tun, bevor ich auf Ihren Kommentar antworte, um nur unsere Gedanken von gestern wiederzugeben.


Um zunächst festzustellen, dass eine grundlegende Funktionalität wie die SQL "SELECT avg(col2) as col2_avg" funktionieren und einfach sein sollte, sind wir uns völlig einig, und wir möchten wirklich eine Lösung dafür haben.

Abgesehen von den ursprünglichen Gründen, aus denen wir uns entschieden haben, dies abzulehnen (was möglicherweise so stark ist oder nicht), sind die aktuellen (abgelehnten) Diktate auch nicht so ideal, da dies einen MultiIndex erzeugt, den Sie eigentlich nie wollen:

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

Oben ist die erste Ebene des MultiIndex überflüssig, da Sie die Spalten bereits gezielt umbenannt haben (im Beispiel im OP folgt hier auch direkt das Ablegen der ersten Ebene der Spalten).
Es ist jedoch schwierig, dies zu ändern, da Sie auch Dinge wie gr.agg(['sum', 'mean']) oder (gemischt) gr.agg({'B': ['sum', 'mean'], 'C': {'c_mean': 'mean', 'c_count': 'count'}}) tun können, wo der MultiIndex benötigt wird und Sinn macht.

Einer der Vorschläge, der in der obigen Diskussion erwähnt wurde, bestand darin, die endgültigen Spaltennamen separat anzugeben (zB https://github.com/pandas-dev/pandas/issues/18366#issuecomment-346683449).
B. ein zusätzliches Schlüsselwort zu aggregate hinzufügen, um die Spaltennamen anzugeben, wie

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

wäre möglich.
Wenn wir jedoch die Spalten-/Funktionsspezifikation und die neuen Spaltennamen aufteilen, können wir dies auch allgemeiner als ein neues Schlüsselwort machen und etwa Folgendes tun:

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

Dies muss https://github.com/pandas-dev/pandas/issues/14829 gelöst werden (etwas, das wir für 0.24.0 tun möchten).
(Wichtiger Hinweis: hierfür wir brauchen die doppelte Namen Problem der Lambda - Funktionen zu beheben, so dass wir eine Art automatische Deduplizierung der Namen tun sollten , wenn wir diese Lösung unterstützen wollen.)


Dann gefällt uns immer noch die Art und Weise der Schlüsselwortargumente zum Umbenennen. Gründe hierfür sind:

  • es ist ähnlich wie assign in Pandas funktioniert und auch in Übereinstimmung mit groupby().aggregate() in ibis (und auch ähnlich wie es zB in dplyr in R aussieht)
  • es gibt Ihnen direkt die nicht-hierarchischen Spaltennamen, die Sie möchten (kein MultiIndex)
  • für die einfachen Fälle (auch zB für Serienfall) finde ich es einfacher als das Diktat

Wir hatten noch eine kleine Diskussion, wie das aussehen könnte. Was ich oben vorgeschlagen habe, war (um die äquivalente Spalten-/Funktionsauswahl wie in meinen ersten Beispielen zu verwenden):

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

Sie können diese Spezifikation immer noch als Diktat von Diktaten aufbauen, aber mit der inneren und äußeren Ebene, die im Vergleich zur aktuellen (veralteten) Version vertauscht ist:

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

(Wir könnten eine Beispiel-Hilfsfunktion haben, die die vorhandenen Diktate von Diktaten in diese Version umwandelt)

Das Diktat ist jedoch immer nur ein einzelnes {col: func} , und diese mehreren Einzelelement-Diktats sehen etwas seltsam aus. Eine Alternative, die wir uns überlegt haben, ist die Verwendung von Tupeln:

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

Das sieht etwas besser aus, aber andererseits stimmt das {'B': 'sum'} Diktat mit den anderen APIs überein, um die Spalte anzugeben, auf die die Funktion angewendet werden soll.


Beide obigen Vorschläge (die einfachere Umbenennung danach und die schlüsselwortbasierte Benennung) sind im Prinzip orthogonal, aber es könnte schön sein, beide zu haben (oder noch etwas anderes, basierend auf weiterer Diskussion).

Danke, dass ihr hier die aktuellen Gedanken der Entwickler weiterleitet 😃

Ich erkenne den (meiner Meinung nach einzigen) Nachteil des veralteten Diktat-Ansatzes mit dem resultierenden MultiIndex an. Könnte abgeflacht werden, wenn der Benutzer eine zusätzliche Option übergibt (yeah YAO :-/ ).

Wie gesagt, ich bin nicht gegen die zweite Version, solange es möglich bleibt:

  • Dinge irgendwie dynamisch generieren und entpacken (dank des **{} Konstrukts, yay Python!)
  • die Umbenennung und die Aggregationsspezifikation eng beieinander halten (zwei Listen verfolgen zu müssen, damit ihre Reihenfolge gleich bleibt, ist für einen Benutzer IMHO einfach nervig)
  • Verwenden Sie Lambda- oder Teilfunktionen, ohne dass Umgehungen aufgrund der (möglicherweise fehlenden oder Konflikte mit) Funktionsnamen erforderlich sind.

Daher ist der letzte Vorschlag (mit dicts oder Tupeln für das col>func-Mapping) meiner Meinung nach in Ordnung.

Der erste Vorschlag im vorherigen Kommentar kann umgesetzt werden, wenn Sie dies wirklich möchten, aber mein Feedback dazu ist, dass ich als Benutzer die zweite Alternative nicht vorziehen würde, da es schwierig ist, die Synchronisation zwischen den beiden zu halten zwei Listen.

Heute beim Entwicklertreffen besprochen.

Kurze Zusammenfassung

  1. @jorisvandenbossche wird versuchen, gr.agg(b_sum=("B", "sum), ...) zu implementieren, dh wenn kein arg an *GroupBy.agg , interpretiere kwargs als <output_name>=(<selection>, <aggfunc>)
  2. Orthogonal zu diesem Problem möchten wir flatten=True Schlüsselwort für .agg bereitstellen

Vielleicht hilft das: Mein Workaround für die Deprecation sind diese Hilfsfunktionen, die die Alias->aggr-Maps durch eine Liste der korrekt benannten Funktionen ersetzen:

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() 
    }

das gibt das alte Verhalten mit:

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
    },
}))

was ist das gleiche wie

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'),
    ]
})

Das funktioniert für mich, wird aber in einigen Eckfällen wahrscheinlich nicht funktionieren ...

Update : Es wurde festgestellt, dass eine Umbenennung nicht erforderlich ist, da Tupel in einer Aggregationsspezifikation als (Alias, Aggr) interpretiert werden. Die alias_aggr-Funktion wird also nicht benötigt und die Konvertierung lautet:

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() 
    }

Ich möchte mich hier nur als ein weiterer Benutzer einklinken, der wirklich, wirklich die Funktionalität vermisst, eine Spalte für eine beliebige Funktion zu aggregieren und sie sofort in derselben Zeile umzubenennen. Ich habe _noch nie_ den von Pandas zurückgegebenen MultiIndex verwendet - entweder glätte ich ihn sofort oder ich möchte meine Spaltennamen manuell angeben, weil sie tatsächlich etwas Bestimmtes bedeuten.

Ich würde mich über jeden der hier vorgeschlagenen Ansätze freuen: SQL-ähnliche Syntax (ich benutze .query() tatsächlich schon viel in Pandas), zurück zum abgewerteten Verhalten, alle anderen Vorschläge. Der aktuelle Ansatz hat mir bereits Spott von Kollegen eingebracht, die R verwenden.

Ich habe vor kurzem sogar PySpark anstelle von Pandas verwendet, obwohl es nicht notwendig war, nur weil mir die Syntax so viel mehr gefällt:

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

Außerdem ist PySpark in den meisten Fällen viel komplizierter zu schreiben als Pandas, das sieht einfach so viel sauberer aus! Daher schätze ich es auf jeden Fall, dass daran noch gearbeitet wird :-)

Ich denke, wir haben eine vereinbarte Syntax für diese Funktionalität; wir brauchen jemanden
füge es ein.

Am Mi, 27.03.2019 um 09:01 Uhr Thomas Kastl [email protected]
schrieb:

Ich möchte mich hier nur als ein weiterer Benutzer einklinken, der wirklich, wirklich ist
die Funktionalität zum Aggregieren einer Spalte für eine beliebige Funktion fehlt und
sofort in der gleichen Zeile umbenennen. Ich habe mich noch nie gefunden
Verwenden des von Pandas zurückgegebenen MultiIndex - entweder glätte ich ihn sofort,
oder ich möchte meine Spaltennamen tatsächlich manuell angeben, weil sie
eigentlich etwas Bestimmtes bedeuten.

Ich würde mich über jeden der hier vorgeschlagenen Ansätze freuen: SQL-ähnliche Syntax
(Ich verwende .query() bereits oft in Pandas),
Zurückkehren zum herabgesetzten Verhalten, alle anderen Vorschläge. Der
Der aktuelle Ansatz hat mir bereits Spott von Kollegen eingebracht, die R verwenden.

Ich habe vor kurzem sogar PySpark anstelle von Pandas verwendet, obwohl
es war nicht nötig, nur weil mir die Syntax so viel besser gefällt:

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

Außerdem ist PySpark in den meisten Fällen viel komplizierter zu schreiben als Pandas.
das sieht einfach viel sauberer aus! Also ich schätze diese Arbeit auf jeden Fall
das geht noch :-)


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/pandas-dev/pandas/issues/18366#issuecomment-477168767 ,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/ABQHIkCYYsah5siYA4_z0oop_ufIB3h8ks5va3nJgaJpZM4QjSLL
.

Ich werde versuchen, für 0.25.0 . dazu zu kommen

Ich habe eine PR unter https://github.com/pandas-dev/pandas/pull/26399 erstellt. Die Grundidee besteht darin, diese Mischung aus Umbenennung und spaltenspezifischer Aggregation zu ermöglichen, indem **kwargs wobei die Werte Tupel von (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

Dies hat einige Einschränkungen

  • Es ist etwas eigenartig für den Rest der Pandas. Die Sytanx (output_name=(selection, aggfunc)) erscheint nirgendwo anders (obwohl .assign das output_name=... Muster verwendet)
  • Die Schreibweise für Ausgabenamen, die keine Python-Bezeichner sind, ist hässlich: .agg(**{'output name': (col, func)})
  • Es ist nur Python 3.6+, oder wir brauchen einige hässliche Hacks für 3.5 und früher, da die Reihenfolge von **kwargs vorher nicht beibehalten wurde
  • Die aggfunc muss eine unäre Funktion sein. Wenn Ihre benutzerdefinierte aggfunc zusätzliche Argumente benötigt, müssen Sie sie zuerst teilweise anwenden

Und es gibt ein Implementierungsdetail, mehrere lambda Aggfuncs für dieselbe Spalte werden noch nicht unterstützt, obwohl dies später behoben werden kann.


Ich vermute, dass die meisten hier abonnierten Leute eine Alternative zu dem veralteten Verhalten unterstützen würden. Was halten die Leute konkret von diesem?

cc @WillAyd, wenn ich deine Bedenken übersehen habe.

Hallo @TomAugspurger ,

Danke, dass Sie diesen Schritt vorangebracht haben.

Dies hat einige Einschränkungen

  • Es ist etwas eigenartig für den Rest der Pandas. Die Sytanx (output_name=(selection, aggfunc)) erscheint nirgendwo anders (obwohl .assign das output_name=... Muster verwendet)

Ich kann nicht anders, als zu glauben, dass diese Art von Argument derjenigen ziemlich ähnlich zu sein scheint, die dazu führte, dass die bestehende Implementierung von vornherein abgelehnt wurde.

Könnten Sie mitteilen, warum wir von diesem neuen Weg gegenüber dem alten _in Bezug auf dieses spezielle Argument_ mehr profitieren?

Ein Vorteil, den ich mir schon vorstellen konnte, ist, dass wir (für py3.6+) die Ausgabereihenfolge der Spalten einzeln auswählen können.

  • Die Schreibweise für Ausgabenamen, die keine Python-Bezeichner sind, ist hässlich: .agg(**{'output name': (col, func)})

Irgendwie war der alte Weg in dieser Hinsicht besser. Aber wie ich bereits sagte, solange es möglich ist, das Konstrukt **{...} verwenden, um die Aggregation dynamisch aufzubauen, wäre ich glücklich genug.

  • Es ist nur Python 3.6+, oder wir brauchen einige hässliche Hacks für 3.5 und früher, da die Reihenfolge von **kwargs vorher nicht beibehalten wurde

Wie hat es vorher funktioniert (bestehende Diktat-of-Dikt-Funktion)? War die Bestellung irgendwie garantiert?

  • Die aggfunc muss eine unäre Funktion sein. Wenn Ihre benutzerdefinierte aggfunc zusätzliche Argumente benötigt, müssen Sie sie zuerst teilweise anwenden

Nur um mein Verständnis zu bestätigen: aggfunc kann ein beliebiger Callable sein, der einen gültigen Wert zurückgibt, oder? (zusätzlich zu den "oft verwendeten" String-Aggfungen wie 'min' , 'max' , etc. ). Gibt es einen Unterschied zu vorher? (dh war die unäre Begrenzung nicht schon vorhanden?)

Und es gibt ein Implementierungsdetail, mehrere lambda Aggfuncs für dieselbe Spalte werden noch nicht unterstützt, obwohl dies später behoben werden kann.

Ja, das ist irgendwie nervig, aber solange es nur eine vorübergehende Einschränkung ist und es offen ist, dies zu beheben, könnte das funktionieren.

Ich vermute, dass die meisten hier abonnierten Leute eine Alternative zu dem veralteten Verhalten unterstützen würden. Was halten die Leute konkret von diesem?

Nun, auf jeden Fall denke ich, dass es sehr wichtig ist, das Aggregat und das Umbenennen in einem Schritt beizubehalten. Wenn das alte Verhalten wirklich keine Option ist, dann könnte diese Alternative reichen.

Könnten Sie uns sagen, warum wir in Bezug auf dieses Argument mehr von diesem neuen Weg gegenüber dem alten profitieren?

Ich erinnere mich vielleicht falsch, aber ich glaube, dass SeriesGroupby.agg und DataFrameGroupby.agg unterschiedliche Bedeutungen zwischen dem äußeren Schlüssel in einem Wörterbuch haben (ist es eine Spaltenauswahl oder eine Ausgabebenennung?). Mit dieser Syntax können wir das Schlüsselwort konsequent als Ausgabenamen bezeichnen.

Irgendwie war der alte Weg in dieser Hinsicht besser.

Ist der Unterschied nur ** ? Ansonsten denke ich, dass die gleichen Einschränkungen geteilt werden.

Wie hat es vorher funktioniert (bestehende Diktat-of-Dikt-Funktion)? War die Bestellung irgendwie garantiert?

Das Sortieren der Schlüssel, was ich jetzt in der PR mache.

Nur um mein Verständnis zu bestätigen: aggfunc kann ein beliebiger Callable sein, der einen gültigen Wert zurückgibt, oder?

Hier ist der Unterschied

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

mit dem alternativen Vorschlag reservieren wir **kwargs für Ausgabespaltennamen. Sie müssten also functools.partitial(aggfunc, myarg='bar') .

Ok danke, ich denke, der vorgeschlagene Ansatz ist 👍 für eine erste Iteration (und wird als Ersatz wirklich in Ordnung sein, sobald die Implementierungsbeschränkung für mehrere Lambda entfernt wird).

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen