Gruppenoperationen#

Mit groupby ist ein Prozess gemeint, der einen oder mehrere der folgenden Schritte umfasst:

  • Split teilt die Daten in Gruppen nach bestimmten Kriterien auf

  • Apply wendet eine Funktion unabhängig auf jede Gruppe an

  • Combine kombiniert die Ergebnisse in einer Datenstruktur

In der ersten Phase des Prozesses werden die in einem pandas-Objekt enthaltenen Daten, sei es eine Series, ein DataFrame oder etwas anderes, in Gruppen aufgeteilt, die auf einem oder mehreren Schlüsseln basieren. Die Aufteilung wird auf einer bestimmten Achse eines Objekts durchgeführt. Ein DataFrame kann zum Beispiel nach seinen Zeilen (axis=0) oder seinen Spalten (axis=1) gruppiert werden. Danach wird auf jede Gruppe eine Funktion angewendet, die einen neuen Wert erzeugt. Schließlich werden die Ergebnisse all dieser Funktionsanwendungen in einem Ergebnisobjekt kombiniert. Die Form des Ergebnisobjekts hängt normalerweise davon ab, was mit den Daten gemacht wird.

Jeder Gruppierungsschlüssel kann viele Formen annehmen, und die Schlüssel müssen nicht alle vom gleichen Typ sein: * Eine Liste oder ein Array von Werten, die die gleiche Länge wie die zu gruppierende Achse haben * Ein Wert, der einen Spaltennamen in einem DataFrame angibt * Ein Dict oder eine Series, die eine Entsprechung zwischen den Werten auf der Achse, die gruppiert wird, und den Gruppennamen darstellt * Eine Funktion, die auf dem Achsenindex oder den einzelnen Beschriftungen im Index aufgerufen wird

Hinweis

Die drei letztgenannten Methoden sind Abkürzungen, um ein Array von Werten zu erzeugen, die für die Aufteilung des Objekts verwendet werden.

Macht euch keine Sorgen, wenn dies alles abstrakt erscheint. Im Laufe dieses Kapitels werde ich viele Beispiele für all diese Methoden geben. Für den Anfang hier ein kleiner Tabellendatensatz als DataFrame:

[1]:
import pandas as pd
import numpy as np
[2]:
df = pd.DataFrame(
    {
        "Title": [
            "Jupyter Tutorial",
            "Jupyter Tutorial",
            "PyViz Tutorial",
            None,
            "Python Basics",
            "Python Basics",
        ],
        "Language": ["de", "en", "de", None, "de", "en"],
        "2021-12": [19651, 4722, 2573, None, 525, 157],
        "2022-01": [30134, 3497, 4873, None, 427, 85],
        "2022-02": [33295, 4009, 3930, None, 276, 226],
    }
)

df
[2]:
Title Language 2021-12 2022-01 2022-02
0 Jupyter Tutorial de 19651.0 30134.0 33295.0
1 Jupyter Tutorial en 4722.0 3497.0 4009.0
2 PyViz Tutorial de 2573.0 4873.0 3930.0
3 None None NaN NaN NaN
4 Python Basics de 525.0 427.0 276.0
5 Python Basics en 157.0 85.0 226.0

Angenommen, ihr möchtet den Summe der Spalte 02/2022 anhand der Beschriftungen von Title berechnen. Es gibt mehrere Möglichkeiten, dies zu tun. Eine davon ist der Zugriff auf 02/2022 und der Aufruf von groupby mit der Spalte (einer Series) in Title:

[3]:
grouped = df["2022-02"].groupby(df["Title"])

grouped
[3]:
<pandas.core.groupby.generic.SeriesGroupBy object at 0x10f466a50>

Diese grouped-Variable ist nun ein spezielles SeriesGroupBy-Objekt. Es hat noch nichts berechnet, außer einigen Zwischendaten über den Gruppenschlüssel df['Title']. Die Idee ist, dass dieses Objekt über alle Informationen verfügt, die benötigt werden, um eine Operation auf jede der Gruppen anzuwenden. Zur Berechnung der Gruppenmittelwerte können wir beispielsweise die Methode sum des GroupBy-Objekts aufrufen:

[4]:
grouped.sum()
[4]:
Title
Jupyter Tutorial    37304.0
PyViz Tutorial       3930.0
Python Basics         502.0
Name: 2022-02, dtype: float64

Später werde ich mehr darüber erklären, was passiert, wenn ihr .sum() aufruft. Wichtig ist hier, dass die Daten (eine Reihe) durch Aufteilung der Daten auf den Gruppenschlüssel aggregiert wurden, wodurch eine neue Reihe entsteht, die nun durch die eindeutigen Werte in der Spalte Title indiziert ist. Der resultierende Index ist Title, weil groupby(df['Title'] dies tat.

Hätten wir stattdessen mehrere Arrays als Liste übergeben, würden wir etwas anderes erhalten:

[5]:
sums = df["2021-12"].groupby([df["Language"], df["Title"]]).sum()

sums
[5]:
Language  Title
de        Jupyter Tutorial    19651.0
          PyViz Tutorial       2573.0
          Python Basics         525.0
en        Jupyter Tutorial     4722.0
          Python Basics         157.0
Name: 2021-12, dtype: float64

Hier haben wir die Daten anhand von zwei Schlüsseln gruppiert, und die resultierende Reihe hat nun einen hierarchischen Index, der aus den beobachteten eindeutigen Schlüsselpaaren besteht:

[6]:
sums.unstack()
[6]:
Title Jupyter Tutorial PyViz Tutorial Python Basics
Language
de 19651.0 2573.0 525.0
en 4722.0 NaN 157.0

Häufig befinden sich die Gruppierungsinformationen in demselben DataFrame wie die Daten, die ihr bearbeiten möchtet. In diesem Fall könnt ihr Spaltennamen (egal ob es sich um Zeichenketten, Zahlen oder andere Python-Objekte handelt) als Gruppenschlüssel übergeben:

[7]:
df.groupby("Title").sum()
[7]:
Language 2021-12 2022-01 2022-02
Title
Jupyter Tutorial deen 24373.0 33631.0 37304.0
PyViz Tutorial de 2573.0 4873.0 3930.0
Python Basics deen 682.0 512.0 502.0

Hierbei fällt auf, dass das Ergebnis keine Spalte Language enthält. Da es sich bei df['Language'] nicht um numerische Daten handelt, stört sie im Tabellenlayout und wird daher automatisch aus dem Ergebnis ausgeschlossen. Standardmäßig werden alle numerischen Spalten aggregiert.

[8]:
df.groupby(["Title", "Language"]).sum()
[8]:
2021-12 2022-01 2022-02
Title Language
Jupyter Tutorial de 19651.0 30134.0 33295.0
en 4722.0 3497.0 4009.0
PyViz Tutorial de 2573.0 4873.0 3930.0
Python Basics de 525.0 427.0 276.0
en 157.0 85.0 226.0

Unabhängig vom Ziel der Verwendung von groupby ist eine allgemein nützliche groupby-Methode size, die eine Serie mit den Gruppengrößen zurückgibt:

[9]:
df.groupby(["Language"]).size()
[9]:
Language
de    3
en    2
dtype: int64

Hinweis

Alle fehlenden Werte in einem Gruppenschlüssel werden standardmäßig aus dem Ergebnis ausgeschlossen. Dieses Verhalten kann deaktiviert werden, indem dropna=False an groupby übergeben wird.

[10]:
df.groupby("Language", dropna=False).size()
[10]:
Language
de     3
en     2
NaN    1
dtype: int64
[11]:
df.groupby(["Title", "Language"], dropna=False).size()
[11]:
Title             Language
Jupyter Tutorial  de          1
                  en          1
PyViz Tutorial    de          1
Python Basics     de          1
                  en          1
NaN               NaN         1
dtype: int64

Iteration über Gruppen#

Das von groupby zurückgegebene Objekt unterstützt Iteration und erzeugt eine Folge von 2-Tupeln, die den Gruppennamen zusammen mit dem Datenpaket enthalten. Betrachten wir das Folgende:

[12]:
for name, group in df.groupby("Title"):
    print(name)
    print(group)
Jupyter Tutorial
              Title Language  2021-12  2022-01  2022-02
0  Jupyter Tutorial       de  19651.0  30134.0  33295.0
1  Jupyter Tutorial       en   4722.0   3497.0   4009.0
PyViz Tutorial
            Title Language  2021-12  2022-01  2022-02
2  PyViz Tutorial       de   2573.0   4873.0   3930.0
Python Basics
           Title Language  2021-12  2022-01  2022-02
4  Python Basics       de    525.0    427.0    276.0
5  Python Basics       en    157.0     85.0    226.0

Bei mehreren Schlüsseln ist das erste Element des Tupels ein Tupel von Schlüsselwerten:

[13]:
for (i1, i2), group in df.groupby(["Title", "Language"]):
    print((i1, i2))
    print(group)
('Jupyter Tutorial', 'de')
              Title Language  2021-12  2022-01  2022-02
0  Jupyter Tutorial       de  19651.0  30134.0  33295.0
('Jupyter Tutorial', 'en')
              Title Language  2021-12  2022-01  2022-02
1  Jupyter Tutorial       en   4722.0   3497.0   4009.0
('PyViz Tutorial', 'de')
            Title Language  2021-12  2022-01  2022-02
2  PyViz Tutorial       de   2573.0   4873.0   3930.0
('Python Basics', 'de')
           Title Language  2021-12  2022-01  2022-02
4  Python Basics       de    525.0    427.0    276.0
('Python Basics', 'en')
           Title Language  2021-12  2022-01  2022-02
5  Python Basics       en    157.0     85.0    226.0

Als nächstes wollen wir ein dict der Daten als Einzeiler ausgeben:

[14]:
books = dict(list(df.groupby("Title")))

books
[14]:
{'Jupyter Tutorial':               Title Language  2021-12  2022-01  2022-02
 0  Jupyter Tutorial       de  19651.0  30134.0  33295.0
 1  Jupyter Tutorial       en   4722.0   3497.0   4009.0,
 'PyViz Tutorial':             Title Language  2021-12  2022-01  2022-02
 2  PyViz Tutorial       de   2573.0   4873.0   3930.0,
 'Python Basics':            Title Language  2021-12  2022-01  2022-02
 4  Python Basics       de    525.0    427.0    276.0
 5  Python Basics       en    157.0     85.0    226.0}

Standardmäßig gruppiert groupby auf axis=0, aber ihr könnt auch auf jeder der anderen Achsen gruppieren. Zum Beispiel könnten wir die Spalten unseres Beispiels df hier nach dtype gruppieren wie folgt:

[15]:
df.dtypes
[15]:
Title        object
Language     object
2021-12     float64
2022-01     float64
2022-02     float64
dtype: object
[16]:
grouped = df.groupby(df.dtypes, axis=1)
[17]:
for dtype, group in grouped:
    print(dtype)
    print(group)
float64
   2021-12  2022-01  2022-02
0  19651.0  30134.0  33295.0
1   4722.0   3497.0   4009.0
2   2573.0   4873.0   3930.0
3      NaN      NaN      NaN
4    525.0    427.0    276.0
5    157.0     85.0    226.0
object
              Title Language
0  Jupyter Tutorial       de
1  Jupyter Tutorial       en
2    PyViz Tutorial       de
3              None     None
4     Python Basics       de
5     Python Basics       en

Auswählen einer Spalte oder Untergruppe von Spalten#

Die Indizierung eines GroupBy-Objekts, das aus einem DataFrame mit einem Spaltennamen oder einem Array von Spaltennamen erstellt wurde, hat den Effekt einer Spaltenunterteilung für die Aggregation. Dies bedeutet, dass:

[18]:
df.groupby("Title")["2021-12"]
df.groupby("Title")[["2022-01"]]
[18]:
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x13e402250>

sind vereinfachte Schreibweisen für:

[19]:
df["2021-12"].groupby(df["Title"])
df[["2022-01"]].groupby(df["Title"])
[19]:
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x11f043590>

Insbesondere bei großen Datensätzen kann es wünschenswert sein, nur einige Spalten zu aggregieren. Um zum Beispiel im vorhergehenden Datensatz die Summe nur für die Spalte 01/2022 zu berechnen und das Ergebnis als DataFrame zu erhalten, könnten wir schreiben:

[20]:
df.groupby(["Title", "Language"])[["2022-01"]].sum()
[20]:
2022-01
Title Language
Jupyter Tutorial de 30134.0
en 3497.0
PyViz Tutorial de 4873.0
Python Basics de 427.0
en 85.0

Das von dieser Indizierungsoperation zurückgegebene Objekt ist ein gruppierter DataFrame, wenn eine Liste oder ein Array übergeben wird, oder eine gruppierte Serie, wenn nur ein einzelner Spaltenname als Skalar übergeben wird:

[21]:
series_grouped = df.groupby(["Title", "Language"])["2022-01"]

series_grouped
[21]:
<pandas.core.groupby.generic.SeriesGroupBy object at 0x13e416710>
[22]:
series_grouped.sum()
[22]:
Title             Language
Jupyter Tutorial  de          30134.0
                  en           3497.0
PyViz Tutorial    de           4873.0
Python Basics     de            427.0
                  en             85.0
Name: 2022-01, dtype: float64

Gruppierung mit dicts und Series#

Gruppierungsinformationen können auch in einer anderen Form als einem Array vorliegen:

[23]:
df.iloc[2:3, [2, 3]] = np.nan

Angenommen, ich habe eine Gruppenkorrespondenz für die Spalten und möchte die Spalten nach Gruppen zusammenfassen:

[24]:
mapping = {"2021-12": "Dec 2021", "2022-01": "Jan 2022", "2022-02": "Feb 2022"}

Nun könnte aus diesem dict ein Array konstruiert werden, um es an groupby zu übergeben, aber stattdessen können wir auch einfach das dict übergeben:

[25]:
by_column = df.groupby(mapping, axis=1)

by_column.sum()
[25]:
Dec 2021 Feb 2022 Jan 2022
0 19651.0 33295.0 30134.0
1 4722.0 4009.0 3497.0
2 0.0 3930.0 0.0
3 0.0 0.0 0.0
4 525.0 276.0 427.0
5 157.0 226.0 85.0

Die gleiche Funktionalität gilt für Series, die als eine Abbildung mit fester Größe betrachtet werden können:

[26]:
map_series = pd.Series(mapping)

map_series
[26]:
2021-12    Dec 2021
2022-01    Jan 2022
2022-02    Feb 2022
dtype: object
[27]:
df.groupby(map_series, axis=1).count()
[27]:
Dec 2021 Feb 2022 Jan 2022
0 1 1 1
1 1 1 1
2 0 1 0
3 0 0 0
4 1 1 1
5 1 1 1

Gruppieren mit Funktionen#

Die Verwendung von Python-Funktionen ist im Vergleich zu einem Dict oder einer Series eine allgemeinere Methode zur Definition einer Gruppenzuordnung. Jede Funktion, die als Gruppenschlüssel übergeben wird, wird einmal pro Indexwert aufgerufen, wobei die Rückgabewerte als Gruppennamen verwendet werden. Betrachtet konkret den Beispiel-DataFrame aus dem vorherigen Abschnitt, das die Titel als Indexwerte enthält. Angenommen, Wenn ihr nach der Länge der Namen gruppieren wollt, könnt ihr zwar ein Array mit den Längen der Strings berechnen, aber es ist einfacher, die Funktion len zu übergeben:

[28]:
df = pd.DataFrame(
    [
        [19651, 30134, 33295],
        [4722, 3497, 4009],
        [2573, 4873, 3930],
        [525, 427, 276],
        [157, 85, 226],
    ],
    index=[
        "Jupyter Tutorial",
        "Jupyter Tutorial",
        "PyViz Tutorial",
        "Python Basics",
        "Python Basics",
    ],
    columns=["2021-12", "2022-01", "2022-02"],
)
[29]:
df.groupby(len).count()
[29]:
2021-12 2022-01 2022-02
13 2 2 2
14 1 1 1
16 2 2 2

Das Mischen von Funktionen mit Arrays, Dicts oder Series ist kein Problem, da alles intern in Arrays umgewandelt wird:

[30]:
languages = ["de", "en", "de", "de", "en"]
[31]:
df.groupby([len, languages]).count()
[31]:
2021-12 2022-01 2022-02
13 de 1 1 1
en 1 1 1
14 de 1 1 1
16 de 1 1 1
en 1 1 1

Gruppierung nach Indexebenen#

Eine letzte praktische Funktion für hierarchisch indizierte Datensätze ist die Möglichkeit der Aggregation anhand einer der Indexebenen einer Achse. Schauen wir uns ein Beispiel an:

[32]:
version_hits = [
    [19651, 0, 30134, 0, 33295, 0],
    [4722, 1825, 3497, 2576, 4009, 3707],
    [2573, 0, 4873, 0, 3930, 0],
    [None, None, None, None, None, None],
    [525, 0, 427, 0, 276, 0],
    [157, 0, 85, 0, 226, 0],
]
df = pd.DataFrame(
    version_hits,
    index=[
        [
            "Jupyter Tutorial",
            "Jupyter Tutorial",
            "PyViz Tutorial",
            None,
            "Python Basics",
            "Python Basics",
        ],
        ["de", "en", "de", None, "de", "en"],
    ],
    columns=[
        ["2021-12", "2021-12", "2022-01", "2022-01", "2022-02", "2022-02"],
        ["latest", "stable", "latest", "stable", "latest", "stable"],
    ],
)
df.columns.names = ["Month", "Version"]

df
[32]:
Month 2021-12 2022-01 2022-02
Version latest stable latest stable latest stable
Jupyter Tutorial de 19651.0 0.0 30134.0 0.0 33295.0 0.0
en 4722.0 1825.0 3497.0 2576.0 4009.0 3707.0
PyViz Tutorial de 2573.0 0.0 4873.0 0.0 3930.0 0.0
NaN NaN NaN NaN NaN NaN NaN NaN
Python Basics de 525.0 0.0 427.0 0.0 276.0 0.0
en 157.0 0.0 85.0 0.0 226.0 0.0
[33]:
df.groupby(level="Month", axis=1).sum()
[33]:
Month 2021-12 2022-01 2022-02
Jupyter Tutorial de 19651.0 30134.0 33295.0
en 6547.0 6073.0 7716.0
PyViz Tutorial de 2573.0 4873.0 3930.0
NaN NaN 0.0 0.0 0.0
Python Basics de 525.0 427.0 276.0
en 157.0 85.0 226.0