Pandas: groupby

In diesem Kapitel unseres Pandas- und Python-Kurses geht es um eine äußerst wichtige Funktionalität, nämlich groupby. Eigentlich ist dieses Thema nicht wirklich kompliziert, aber nicht immer auf den ersten Blick ersichtlich und es wird manchmal als schwierig empfunden. Völlig zu Unrecht, wie wir sehen werden. Es ist auch sehr wichtig, sich mit groupby vertraut zu machen, weil man damit wichtige Probleme lösen kann, die ohne groupby nicht oder nur sehr schwer lösbar wären. Die Pandas groupby-Operation geschieht in mehreren Schriten

  • Aufsplitten des Objekts,
  • Anwendung einer Funktion und
  • Kombination der Ergebnisse.

Wir können ein DataFrame-Objekt nach verschiedenen Kriterien und zeilen- und spaltenweise, d.h. mit Hilfe des Parameters axis, in Gruppen aufteilen.

splitted banana

Durch die Anwendung einer Funktion können wir die Daten

  • filtern,
  • umwandeln
  • aggregieren.

groupby kann auf Pandas Series-Objekte und DataFrame-Objekte angewendet werden! Wie das funktioniert, werden wir in diesem Tutorial anhand vieler kleiner praktischer Beispiele verstehen lernen.

groupby mit Series-Objekten

Wir erzeugen mit dem folgenden Python-Programm ein Series-Objekt mit einem Index der Größe nvalues. Der Index wird nicht eindeutig sein, da die Zeichenketten für den Index aus der Liste fruits entnommen werden, die weniger Elemente hat als `nvalues``:

In [28]:
import pandas as pd
import numpy as np
import random

nvalues = 30
# wir erstellen Zufallswerte für unser Series-Objekt:
values = np.random.randint(1, 20, (nvalues,))
fruits = ["bananas", "oranges", "apples", 
          "clementines", "cherries", "pears"]
fruits_index = np.random.choice(fruits, (nvalues,))

s = pd.Series(values, index=fruits_index)
print(s[:10])
cherries       5
pears          4
cherries       3
oranges        5
clementines    1
cherries       9
oranges        5
oranges        8
pears          5
pears          9
dtype: int64
In [29]:
grouped = s.groupby(s.index)
grouped
Out[29]:
<pandas.core.groupby.generic.SeriesGroupBy object at 0x7fa1c6700640>

Wir können sehen, dass wir ein SeriesGroupBy-Objekt erhalten, wenn wir groupby auf den Index unseres Serienobjekts s anwenden. Das Ergebnis dieser Operation grouped ist iterierbar. In jedem Schritt erhalten wir ein Tupel-Objekt zurück, das aus einem Index-Label und einem Series-Objekt besteht. Das Series-Objekt s ist auf dieses Label reduziert.

In [30]:
grouped = s.groupby(s.index)

for fruit, s_obj in grouped:
    print(f"===== {fruit} =====")
    print(s_obj)
===== apples =====
apples    18
apples     5
apples     3
dtype: int64
===== bananas =====
bananas    16
bananas     4
bananas     3
bananas     3
bananas     1
dtype: int64
===== cherries =====
cherries     5
cherries     3
cherries     9
cherries    12
cherries    17
cherries     2
cherries    10
dtype: int64
===== clementines =====
clementines     1
clementines     4
clementines    19
dtype: int64
===== oranges =====
oranges     5
oranges     5
oranges     8
oranges    18
oranges    18
oranges    18
dtype: int64
===== pears =====
pears     4
pears     5
pears     9
pears     2
pears    14
pears    13
dtype: int64

Mit folgendem Python-Code hätten wir das gleiche Ergebnis - abgesehen von der Reihenfolge - auch ohne die Verwendung von groupby erzielen können.

In [31]:
for fruit in set(s.index):
    print(f"===== {fruit} =====")
    print(s[fruit])
===== oranges =====
oranges     5
oranges     5
oranges     8
oranges    18
oranges    18
oranges    18
dtype: int64
===== bananas =====
bananas    16
bananas     4
bananas     3
bananas     3
bananas     1
dtype: int64
===== apples =====
apples    18
apples     5
apples     3
dtype: int64
===== pears =====
pears     4
pears     5
pears     9
pears     2
pears    14
pears    13
dtype: int64
===== cherries =====
cherries     5
cherries     3
cherries     9
cherries    12
cherries    17
cherries     2
cherries    10
dtype: int64
===== clementines =====
clementines     1
clementines     4
clementines    19
dtype: int64

groupby mit DataFrames

Wir werden mit einem sehr einfachen DataFrame beginnen. Der DataFrame hat zwei Spalten, von denen die eine den Namen Name enthält und die andere Coffee die Anzahl der Tassen Kaffee, die die Person getrunken hat, in ganzen Zahlen.

In [32]:
import pandas as pd
beverages = pd.DataFrame({'Name': ['Robert', 'Melinda', 'Brenda',
                                   'Samantha', 'Melinda', 'Robert',
                                   'Melinda', 'Brenda', 'Samantha'],
                          'Coffee': [3, 0, 2, 2, 0, 2, 0, 1, 3],
                          'Tea':    [0, 4, 2, 0, 3, 0, 3, 2, 0]})
    
beverages
Out[32]:
Name Coffee Tea
0 Robert 3 0
1 Melinda 0 4
2 Brenda 2 2
3 Samantha 2 0
4 Melinda 0 3
5 Robert 2 0
6 Melinda 0 3
7 Brenda 1 2
8 Samantha 3 0

Es ist einfach, und wir haben bereits in den vorherigen Kapiteln unseres Tutorials gesehen, wie man die Gesamtzahl der Kaffeetassen berechnet. Die Aufgabe besteht darin, eine Spalte eines DatFrame zu summieren, z.B. die Spalte Coffee:

In [33]:
beverages['Coffee'].sum()
Out[33]:
13

Berechnen wir nun die Gesamtzahl der Kaffees und Tees:

In [34]:
beverages[['Coffee', 'Tea']].sum()
Out[34]:
Coffee    13
Tea       14
dtype: int64

groupby war für die bisherigen Aufgaben nicht erforderlich. Werfen wir noch einmal einen Blick auf unseren DataFrame. Wir können sehen, dass einige der Namen mehrfach vorkommen. Es wäre also sehr interessant sein, zu sehen, wie viele Tassen Kaffee und Tee jede Person insgesamt getrunken hat. Das heißt, wir wenden 'groupby' auf die Spalte 'Name' an. Dadurch teilen wir den DatFrame auf. Dann wenden wir 'sum' auf die Ergebnisse von 'groupby' an:

In [35]:
res = beverages.groupby(['Name']).sum()
print(res)
          Coffee  Tea
Name                 
Brenda         3    4
Melinda        0   10
Robert         5    0
Samantha       5    0

Wie wir sehen können, sind die Namen jetzt der Index des resultierenden DataFrame:

In [36]:
print(res.index)
Index(['Brenda', 'Melinda', 'Robert', 'Samantha'], dtype='object', name='Name')

Wir können auch die durchschnittliche Anzahl der Kaffee- und Teetassen berechnen, die die Personen getrunken haben:

In [37]:
beverages.groupby(['Name']).mean()
Out[37]:
Coffee Tea
Name
Brenda 1.5 2.000000
Melinda 0.0 3.333333
Robert 2.5 0.000000
Samantha 2.5 0.000000

Weiteres groupby-Beispiel

Der folgende Python-Code wird verwendet, um die Daten zu erzeugen, die wir in unserem nächsten groupby-Beispiel verwenden werden. Es ist nicht notwendig, den folgenden Python-Code für den nachfolgenden Inhalt zu verstehen. Das Modul faker muss installiert sein. Im Falle einer Anaconda-Installation kann dies durch die Ausführung eines der folgenden Befehle in einer Shell geschehen:

conda install -c conda-forge faker
conda install -c conda-forge/label/gcc7 faker
conda install -c conda-forge/label/cf201901 faker
conda install -c conda-forge/label/cf202003 faker 
In [38]:
from faker import Faker
import numpy as np
from itertools import chain

fake = Faker('de_DE')

number_of_names = 10
names = []
for _ in range(number_of_names):
    names.append(fake.first_name())


data = {}
workweek = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")
weekend = ("Saturday", "Sunday")

for day in chain(workweek, weekend):
    data[day] = np.random.randint(0, 10, (number_of_names,))
    
data_df = pd.DataFrame(data, index=names)
data_df
Out[38]:
Monday Tuesday Wednesday Thursday Friday Saturday Sunday
Victor 1 2 4 4 0 1 1
Daria 5 2 2 6 4 7 9
Ignaz 2 3 1 2 2 5 7
Marco 6 4 5 9 1 5 7
Bertha 8 6 3 4 4 4 5
Nadeschda 1 4 4 5 1 4 7
Alberto 5 5 0 3 6 2 3
Sylke 4 1 5 1 3 6 0
Marion 0 0 1 3 1 1 7
Ayse 0 2 7 6 3 0 8
In [39]:
print(names)
['Victor', 'Daria', 'Ignaz', 'Marco', 'Bertha', 'Nadeschda', 'Alberto', 'Sylke', 'Marion', 'Ayse']
In [40]:
names = ('Ortwin', 'Mara', 'Siegrun', 'Sylvester', 'Metin', 'Adeline', 'Utz', 'Susan', 'Gisbert', 'Senol')
data = {'Monday': np.array([0, 9, 2, 3, 7, 3, 9, 2, 4, 9]),
        'Tuesday': np.array([2, 6, 3, 3, 5, 5, 7, 7, 1, 0]),
        'Wednesday': np.array([6, 1, 1, 9, 4, 0, 8, 6, 8, 8]),
        'Thursday': np.array([1, 8, 6, 9, 9, 4, 1, 7, 3, 2]),
        'Friday': np.array([3, 5, 6, 6, 5, 2, 2, 4, 6, 5]),
        'Saturday': np.array([8, 4, 8, 2, 3, 9, 3, 4, 9, 7]),
        'Sunday': np.array([0, 8, 7, 8, 9, 7, 2, 0, 5, 2])}

data_df = pd.DataFrame(data, index=names)
data_df
Out[40]:
Monday Tuesday Wednesday Thursday Friday Saturday Sunday
Ortwin 0 2 6 1 3 8 0
Mara 9 6 1 8 5 4 8
Siegrun 2 3 1 6 6 8 7
Sylvester 3 3 9 9 6 2 8
Metin 7 5 4 9 5 3 9
Adeline 3 5 0 4 2 9 7
Utz 9 7 8 1 2 3 2
Susan 2 7 6 7 4 4 0
Gisbert 4 1 8 3 6 9 5
Senol 9 0 8 2 5 7 2

Mit diesem DataFrame wollen wir demonstrieren, wie man Spalten mittels einer Funktion kombinieren kann. Es geht darum festzustellen wieviele Stunden jede Person während der Arbeits- und während des Wochenendes gearbeitet hatte:

In [41]:
def is_weekend(day):
    if day in {'Saturday', 'Sunday'}:
        return "Weekend"
    else:
        return "Workday"
        
In [42]:
for res_func, df in data_df.groupby(by=is_weekend, axis=1):
    print(df)
           Saturday  Sunday
Ortwin            8       0
Mara              4       8
Siegrun           8       7
Sylvester         2       8
Metin             3       9
Adeline           9       7
Utz               3       2
Susan             4       0
Gisbert           9       5
Senol             7       2
           Monday  Tuesday  Wednesday  Thursday  Friday
Ortwin          0        2          6         1       3
Mara            9        6          1         8       5
Siegrun         2        3          1         6       6
Sylvester       3        3          9         9       6
Metin           7        5          4         9       5
Adeline         3        5          0         4       2
Utz             9        7          8         1       2
Susan           2        7          6         7       4
Gisbert         4        1          8         3       6
Senol           9        0          8         2       5
In [43]:
data_df.groupby(by=is_weekend, axis=1).sum()
Out[43]:
Weekend Workday
Ortwin 8 12
Mara 12 29
Siegrun 15 18
Sylvester 10 30
Metin 12 30
Adeline 16 14
Utz 5 27
Susan 4 26
Gisbert 14 22
Senol 9 24

Aufgaben / Übungen

Aufgabe 1

Berechnen Sie die Durchschnittspreise für die Produkte des folgenden DataFrame:

In [44]:
import pandas as pd

d = {"products": ["Oppilume", "Dreaker", "Lotadilo", 
                  "Crosteron", "Wazzasoft", "Oppilume", 
                  "Dreaker", "Lotadilo", "Wazzasoft"],
     "colours": ["blue", "blue", "blue", 
                 "green", "blue", "green", 
                 "green", "green", "red"],
     "customer_price": [2345.89, 2390.50, 1820.00, 
                        3100.00, 1784.50, 2545.89,
                        2590.50, 2220.00, 2084.50],
     "non_customer_price": [2445.89, 2495.50, 1980.00, 
                            3400.00, 1921.00, 2645.89, 
                            2655.50, 2140.00, 2190.00]}

product_prices = pd.DataFrame(d)
product_prices
Out[44]:
products colours customer_price non_customer_price
0 Oppilume blue 2345.89 2445.89
1 Dreaker blue 2390.50 2495.50
2 Lotadilo blue 1820.00 1980.00
3 Crosteron green 3100.00 3400.00
4 Wazzasoft blue 1784.50 1921.00
5 Oppilume green 2545.89 2645.89
6 Dreaker green 2590.50 2655.50
7 Lotadilo green 2220.00 2140.00
8 Wazzasoft red 2084.50 2190.00

Aufgabe 2

Berechnen Sie die Summe der Preise nach den Farben.

Aufgabe 3

Lesen Sie die Datei project_times.txt ein. Die Zeilen dieser Datei enthalten kommagetrennt das Datum, den Namen des Programmierers, den Namen des Projekts und die Zeit, die die Personen für das Projekt aufgewendet haben.

Berechnen Sie die Zeit, die für alle Projekte pro Tag aufgewendet wurde

Aufgabe 4

Erstellen Sie einen DateFrame mit der Gesamtzeit, die alle Programmiererinnen und Programmierer pro Tag für ein Projekt aufwenden

Aufgabe 5

Berechnen Sie die Gesamtzeit, die für die Projekte über den ganzen Monat hinweg aufgewendet wurde.

Aufgabe 6

Berechnen Sie die monatlichen Zeiten der einzelnen Programmierer und Programmiererinnen unabhängig von den Projekten

Aufgabe 7

Ordnen Sie den DataFrame mit einem MultiIndex, der aus dem Datum und den Projektnamen besteht, neu an. Für jede Person soll es eine Spalte mit den aufgewendeten Zeiten geben. Also so:

                   time
programmer         Antonie  Elise  Fatima  Hella  Mariola
date     project
2020-01-01 BIRDY   NaN      NaN    NaN     1.50   1.75
           NSTAT   NaN      NaN    0.25    NaN    1.25
           XTOR    NaN      NaN    NaN     1.00   3.50
2020-01-02 BIRDY   NaN      NaN    NaN     1.75   2.00
           NSTAT   0.5      NaN    NaN     NaN    1.75

Zusatzaufgabe: ersetzte NaN durch 0.

Aufgabe 8:

Die Datei donations.txt enthält die folgenden Daten, also "Vorname" (firstname), "Nachname" (surname), "Stadt" (city), "Job", "Einkommen" (income), "Spenden" (donations):

firstname,surname,city,job,income,donations
Janett,Schwital,Karlsruhe,Politician,244400,2512
Daniele,Segebahn,Freiburg,Student,16800,336
Kirstin,Klapp,Hamburg,Engineer,116900,1479
Oswald,Segebahn,Köln,Musician,57700,1142

Gruppiere die Daten anhand des Jobs der Personen.

Lösungen

Solution to Exercise 1

In [48]:
x = product_prices.groupby("products").mean()
x
Out[48]:
customer_price non_customer_price
products
Crosteron 3100.00 3400.00
Dreaker 2490.50 2575.50
Lotadilo 2020.00 2060.00
Oppilume 2445.89 2545.89
Wazzasoft 1934.50 2055.50

Lösung für Aufgabe 2

In [49]:
x = product_prices.groupby("colours").sum()
x
Out[49]:
customer_price non_customer_price
colours
blue 8340.89 8842.39
green 10456.39 10841.39
red 2084.50 2190.00

Lösung für Aufgabe 3

In [51]:
import pandas as pd

df = pd.read_csv("data1/project_times.txt", index_col=0)
df
Out[51]:
programmer project time
date
2020-01-01 Hella XTOR 1.00
2020-01-01 Hella BIRDY 1.50
2020-01-01 Fatima NSTAT 0.25
2020-01-01 Mariola NSTAT 0.50
2020-01-01 Mariola BIRDY 1.75
... ... ... ...
2030-01-30 Antonie XTOR 0.50
2030-01-31 Hella BIRDY 1.25
2030-01-31 Hella BIRDY 1.75
2030-01-31 Mariola BIRDY 1.00
2030-01-31 Hella BIRDY 1.00

17492 rows × 3 columns

In [52]:
times_per_day = df.groupby(df.index).sum()
print(times_per_day[:10])
             time
date             
2020-01-01   9.25
2020-01-02   6.00
2020-01-03   2.50
2020-01-06   5.75
2020-01-07  15.00
2020-01-08  13.25
2020-01-09  10.25
2020-01-10  17.00
2020-01-13   4.75
2020-01-14  10.00

Lösung für Aufgabe 4

In [53]:
times_per_day_project = df.groupby([df.index, 'project']).sum()
print(times_per_day_project[:10])
                    time
date       project      
2020-01-01 BIRDY    3.25
           NSTAT    1.50
           XTOR     4.50
2020-01-02 BIRDY    3.75
           NSTAT    2.25
2020-01-03 BIRDY    1.00
           NSTAT    0.25
           XTOR     1.25
2020-01-06 BIRDY    2.75
           NSTAT    0.75

Lösung für Aufgabe 5

In [54]:
df.groupby(['project']).sum()
Out[54]:
time
project
BIRDY 9605.75
NSTAT 8707.75
XTOR 6427.50

Lösung für Aufgabe 6

In [55]:
df.groupby(['programmer']).sum()
Out[55]:
time
programmer
Antonie 1511.25
Elise 80.00
Fatima 593.00
Hella 10642.00
Mariola 11914.75

Lösung für Aufgabe 7

In [56]:
x = df.groupby([df.index, 'project', 'programmer']).sum()

x = x.unstack()
x
Out[56]:
time
programmer Antonie Elise Fatima Hella Mariola
date project
2020-01-01 BIRDY NaN NaN NaN 1.50 1.75
NSTAT NaN NaN 0.25 NaN 1.25
XTOR NaN NaN NaN 1.00 3.50
2020-01-02 BIRDY NaN NaN NaN 1.75 2.00
NSTAT 0.5 NaN NaN NaN 1.75
... ... ... ... ... ... ...
2030-01-29 XTOR NaN NaN NaN 1.00 5.50
2030-01-30 BIRDY NaN NaN NaN 0.75 4.75
NSTAT NaN NaN NaN 3.75 NaN
XTOR 0.5 NaN NaN 0.75 NaN
2030-01-31 BIRDY NaN NaN NaN 4.00 1.00

7037 rows × 5 columns

In [57]:
x = x.fillna(0)
print(x[:10])
                      time                           
programmer         Antonie Elise Fatima Hella Mariola
date       project                                   
2020-01-01 BIRDY      0.00   0.0   0.00  1.50    1.75
           NSTAT      0.00   0.0   0.25  0.00    1.25
           XTOR       0.00   0.0   0.00  1.00    3.50
2020-01-02 BIRDY      0.00   0.0   0.00  1.75    2.00
           NSTAT      0.50   0.0   0.00  0.00    1.75
2020-01-03 BIRDY      0.00   0.0   1.00  0.00    0.00
           NSTAT      0.25   0.0   0.00  0.00    0.00
           XTOR       0.00   0.0   0.00  0.50    0.75
2020-01-06 BIRDY      0.00   0.0   0.00  2.50    0.25
           NSTAT      0.00   0.0   0.00  0.00    0.75

Lösung für Aufgabe 8:

In [60]:
import pandas as pd

data = pd.read_csv('data1/donations.txt')
data_sum = data.groupby(['job']).sum()
data_sum.sort_values(by='donations')
Out[60]:
income donations
job
Student 372900 7458
Musician 1448700 24376
Engineer 2067200 25564
Politician 4118300 30758
Manager 12862600 87475
In [61]:
data_sum['relative'] = data_sum.donations * 100 / data_sum.income

data_sum.sort_values(by='relative')
Out[61]:
income donations relative
job
Manager 12862600 87475 0.680072
Politician 4118300 30758 0.746862
Engineer 2067200 25564 1.236649
Musician 1448700 24376 1.682612
Student 372900 7458 2.000000