Der DataFrame - ein Werkzeug für das Arbeiten mit Tabellen

Was ist ein DataFrame?

Am besten vergleichbar ist ein DataFrame mit der Tabellenstruktur, die ihr aus Programmen wie z.B. Excel oder OpenOffice Calc kennt. Dataframes sind zweidimensionale Tabellen, deren Spalten Überschriften haben (können) und immer gleich lang sind (also dieselbe Anzahl an Dateneinträgen haben). Dabei darf eine Spalte immer nur den gleichen Datentyp (z.B. numerische oder logische Werte) beinhalten; ein Dataframe kann aber spaltenweise unterschiedliche Datentypen darstellen (was ein Datentyp genau ist, wirst Du im Kurs noch erfahren).

In dieser Lektion wirst Du lernen (a) wie man Dataframes in erstellt oder einliest, (b) wie Du Elemente/Werte in Dataframes ansprichst, (c) wie Du diese Elemente oder ganze Dataframes modifizierst und (d) wie Du Dataframes als .txt-Dateien exportieren bzw. speichern kannst.

pandas - ein Pythonpaket für DataFrames

Diejenigen unter Euch, die sich bereits mit R beschäftigt haben, kennen den data.frame als typischen, in R standardmäßig eingebautes Element. In Python wird der Dataframe über das Paket pandas bereitgestellt.

In [1]:
import pandas as pd

Aufgabe

Schau Dir mal die Hilfe für den DataFrame an. Weißt Du noch, wie das geht? Schreibe in die folgende Zeile pd.DataFrame, dann, je nach Geschmack, 1-3 mal Shift + Tab drücken.

In [ ]:
 

Bevor es losgeht - ein paar Grundlagen

Anhand des folgenden Skriptbeispiels lernen wir kurz wie Objektzuweisungen funktionieren, treffen Listen wieder (list), und erfahren nochmal, dass eine list auch unterschiedliche Datentypen beinhalten kann.

In [2]:
# Liste, die Zeichenketten ('strings') enthält
Dichter = ["Goethe", "Schiller", "Brecht"]

# Liste, die numerische Werte enthält
Sterbealter = [83, 46, 57]

# Liste, die logische Werte (wahr oder falsch) beinhaltet
Brillentraeger = [False, False, True]

# In den vorigen Beispiele enthielt eine Liste immer nur einen Datentyp.
#    Dies ist generell empehlenswert, da das Mischen von Datentypen
#    zu Problemen führen kann, wenn wir daraus DataFrames erzeugen wollen.
NotrufNr = [110, 112]         # gut!         
NotrufNr = [110, 112, "Mama"] # nicht gut!

Erstellen eines DataFrames

Aus den drei Listen Dichter, Sterbealter und Brillentraeger erstellen wir nun mit der Function pd.DataFrame unseren ersten DataFrame.

Beachtet im folgenden Feld die geschwungenen Klammern ({...}). Sie erzeugen in Python einen dictionary. Diesen habt Ihr in der letzten Vertiefungsaufgabe kennengelernt.

In [3]:
df = pd.DataFrame({"Dichter": Dichter, "Sterbealter": Sterbealter, "Brillentraeger": Brillentraeger})
In [4]:
df
Out[4]:
Dichter Sterbealter Brillentraeger
0 Goethe 83 False
1 Schiller 46 False
2 Brecht 57 True

Erkunden des Dataframes

Verschaffen wir uns zunächst einen Überblick über den DataFrame. Das ist bei unserem Objekt df oben nicht so schwer, aber bei größeren Dataframes mit vielen Zeilen und Spalten sinnvoll.

In [5]:
# So erhalten wir die Zahl der Zeilen und Spalten
df.shape
Out[5]:
(3, 3)
In [6]:
# So erhalten wir einen raschen Überblick über Zeilen, Spalten und die enthaltenen Datentypen.
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 3 columns):
 #   Column          Non-Null Count  Dtype 
---  ------          --------------  ----- 
 0   Dichter         3 non-null      object
 1   Sterbealter     3 non-null      int64 
 2   Brillentraeger  3 non-null      bool  
dtypes: bool(1), int64(1), object(1)
memory usage: 183.0+ bytes
In [7]:
# Für numerische Spalten erhalten wir so einen schnellen Überblick deskriptiver Statistiken
df.describe()
Out[7]:
Sterbealter
count 3.0
mean 62.0
std 19.0
min 46.0
25% 51.5
50% 57.0
75% 70.0
max 83.0

Elemente des DataFrame auswählen

Es gibt wirklich viele Möglichkeiten, auf Elemente eines DataFrames zuzugreifen. Was meinen wir mit "Element"? Das können Zeilen, Spalten, einzelne Felder oder nach bestimmten Kriterien ausgewählte Teilmengen sein. Wir gehen mal ganz stumpf ein paar Optionen durch...

In [8]:
# Spalte mit Namen auswählen
df["Dichter"]
Out[8]:
0      Goethe
1    Schiller
2      Brecht
Name: Dichter, dtype: object
In [9]:
# Genau das gleiche, nur schicker...
df.Dichter
Out[9]:
0      Goethe
1    Schiller
2      Brecht
Name: Dichter, dtype: object
In [10]:
# Oder gleich mehrere Spalten mit einer Liste auswählen
df[["Dichter", "Sterbealter"]]
Out[10]:
Dichter Sterbealter
0 Goethe 83
1 Schiller 46
2 Brecht 57
In [11]:
# Zeilen mit "Slices" auswählen (Achtung: Python-Indizierung beginnt bei 0)
df[0:2]
Out[11]:
Dichter Sterbealter Brillentraeger
0 Goethe 83 False
1 Schiller 46 False

Bei dem Versuch, auf diese Weise eine einzelne Zeile auszuwählen, laufen wir aber erstmal vor die Wand...

In [12]:
df[1]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/miniconda3/envs/umweltdv24/lib/python3.11/site-packages/pandas/core/indexes/base.py:3805, in Index.get_loc(self, key)
   3804 try:
-> 3805     return self._engine.get_loc(casted_key)
   3806 except KeyError as err:

File index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7081, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7089, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 1

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[12], line 1
----> 1 df[1]

File ~/miniconda3/envs/umweltdv24/lib/python3.11/site-packages/pandas/core/frame.py:4102, in DataFrame.__getitem__(self, key)
   4100 if self.columns.nlevels > 1:
   4101     return self._getitem_multilevel(key)
-> 4102 indexer = self.columns.get_loc(key)
   4103 if is_integer(indexer):
   4104     indexer = [indexer]

File ~/miniconda3/envs/umweltdv24/lib/python3.11/site-packages/pandas/core/indexes/base.py:3812, in Index.get_loc(self, key)
   3807     if isinstance(casted_key, slice) or (
   3808         isinstance(casted_key, abc.Iterable)
   3809         and any(isinstance(x, slice) for x in casted_key)
   3810     ):
   3811         raise InvalidIndexError(key)
-> 3812     raise KeyError(key) from err
   3813 except TypeError:
   3814     # If we have a listlike key, _check_indexing_error will raise
   3815     #  InvalidIndexError. Otherwise we fall through and re-raise
   3816     #  the TypeError.
   3817     self._check_indexing_error(key)

KeyError: 1

Hier versucht pandas nämlich, eine Spalte mit Namen ("label") 1 zu finden und scheitert wortreich daran. Wir müssen uns also mit der sog. label-basierten Auswahl (label-based selection) auseinandersetzen. Das ist zunächst vielleicht etwas verwirrend, aber es wird schon gutgehen...Versuch und Irrtum!

Schauen wir uns nochmal unseren DataFrame an:

In [13]:
df
Out[13]:
Dichter Sterbealter Brillentraeger
0 Goethe 83 False
1 Schiller 46 False
2 Brecht 57 True

Links sehen wir eine Spalte, die eigentlich gar nicht zur Tabelle gehört - Zahlen von 0 bis 2. Wir können sie uns vorstellen wie die Zeilennummern in Excel. Diese Spalte heißt index. Über diesen Index können wir Zeilen auswählen. Das mag hier unnötig kompliziert wirken, wird aber ein richtig cooles Feature, wenn wir statt Zahlen von 0 bis n z.B. einen Index haben, der aus Datumangaben besteht. Dann können wir nämlich ruck zuck Zeilen mit einem bestimmten Datum auswählen. Anstatt der Spaltenbezeichner in Excel (von A, B usw.) nutzen wir in pandas übrigens die Spaltennamen - haben wir ja schon gemacht.

Um nun also die index-Label für die Auswahl von Zeilen zu nutzen, müssen wir das .loc Feature von pandas nutzen.

In [14]:
# Erste Zeile
df.loc[0]
Out[14]:
Dichter           Goethe
Sterbealter           83
Brillentraeger     False
Name: 0, dtype: object
In [15]:
# Zweite Zeile, Spalten Dichter und Sterbealter
df.loc[1, ["Dichter", "Sterbealter"]]
Out[15]:
Dichter        Schiller
Sterbealter          46
Name: 1, dtype: object
In [16]:
# Zweite und dritte Zeile, Spalten Dichter und Sterbealter
df.loc[[1,2], ["Dichter", "Sterbealter"]]
Out[16]:
Dichter Sterbealter
1 Schiller 46
2 Brecht 57
In [17]:
# Bedingungen für eine Auswahl nutzen: Nur Brillenträger
df.loc[df.Brillentraeger]
Out[17]:
Dichter Sterbealter Brillentraeger
2 Brecht 57 True
In [18]:
# Bedingungen für eine Auswahl nutzen: Nur Dichter, die "jung" gestorben sind
df.loc[df.Sterbealter < 60]
Out[18]:
Dichter Sterbealter Brillentraeger
1 Schiller 46 False
2 Brecht 57 True

Wenn man wirklich strikt nach Position auswählen möchte, nutzt man das .iloc Feature. Man wählt auf diese Weise streng nach Position aus, sowohl für die Zeile als auch für die Spalten - also genauso wie bei einer Matrix. Aber Achtung: Die erste Position ist in Python immer 0!

In [19]:
# 2. Zeile, 1. Spalte
df.iloc[1,0]
Out[19]:
'Schiller'

Einen DataFrame modifizieren

Wir wollen nun einen bereits erzeugten DataFrage modifizieren.

  • Werte einzeler Einträge bestehende Daten ändern
  • Einträge hinzufügen oder löschen,
  • den DataFrame sortieren
  • den DataFrame filtern.

Bestehende Daten ändern

Eine kurze Recherche hat ergeben, dass Brecht nicht 57 sondern 58 Jahre alt wurde. Das wollen wir korrigieren. Dafür müssen wir zuerst den Wert, den wir ändern wollen, auswählen und ihm dann einen neuen Wert zuweisen. Wir wissen, dass Brecht an dritter Stelle in der Tabelle steht, also könnten wir den Eintrag mit df.loc[2, "Sterbealter] ansprechen. Aber bei sehr großen Datensätzen möchte man nicht erst die Position raussuchen. Wir lassen also Python für uns suchen - wir haben das oben schonmal gemacht.

In [20]:
# Vorher
df
Out[20]:
Dichter Sterbealter Brillentraeger
0 Goethe 83 False
1 Schiller 46 False
2 Brecht 57 True
In [21]:
# Spalte "Sterbealter" dort ändern wo der Dichter "Brecht" heißt
df.loc[df.Dichter=="Brecht", "Sterbealter"] = 58
In [22]:
# Kontrolle
df
Out[22]:
Dichter Sterbealter Brillentraeger
0 Goethe 83 False
1 Schiller 46 False
2 Brecht 58 True

Yay.

Einträge hinzufügen

Das Land der digitalen Dichter:innen und Denker:innen hat ja wohl noch mehr vorzuweisen als nur Goethe, Schiller und Brecht. Wir wollen unseren Dataframe also mit weiteren Einträgen (Zeilen) ergänzen. Außerdem wollen wir für alle Dichter:innen noch weitere Informationen (Spalten) hinzufügen. z.B. das Geschlecht.

Wir können eine neue Zeile an einer bestimmten Stelle einfügen (z.B. am Ende). Das "Ende" ergibt sich aus der Länge des DataFrames, welches wir über len(df) ermitteln.

In [23]:
df.loc[len(df)] = ["von Droste-Hülshoff", 51, False]
In [24]:
# Kontrolle
df
Out[24]:
Dichter Sterbealter Brillentraeger
0 Goethe 83 False
1 Schiller 46 False
2 Brecht 58 True
3 von Droste-Hülshoff 51 False

Einfacher ist es aber, einen neuen Dataframe über concat anzuhängen.

In [25]:
df2 = pd.DataFrame({"Dichter": ["Bachmann", "Seghers"], "Sterbealter": [47, 82], "Brillentraeger": [True, False]})
df2
Out[25]:
Dichter Sterbealter Brillentraeger
0 Bachmann 47 True
1 Seghers 82 False
In [26]:
# Mit dem ignore_index Argument sorgen wir dafür, 
# dass der index für die neuen Einträge auch neu erzeugt wird.
df = pd.concat([df, df2], ignore_index=True)
In [27]:
# Kontrolle
df
Out[27]:
Dichter Sterbealter Brillentraeger
0 Goethe 83 False
1 Schiller 46 False
2 Brecht 58 True
3 von Droste-Hülshoff 51 False
4 Bachmann 47 True
5 Seghers 82 False

Eine neue Spalte anzuhängen ist etwas einfacher. Um es nochmal für alle auszubuchstabieren, spezifieren wir das Geschlecht.

In [28]:
df["Geschlecht"] = ["m", "m", "m", "w", "w", "w"]
# Kontrolle
df
Out[28]:
Dichter Sterbealter Brillentraeger Geschlecht
0 Goethe 83 False m
1 Schiller 46 False m
2 Brecht 58 True m
3 von Droste-Hülshoff 51 False w
4 Bachmann 47 True w
5 Seghers 82 False w

Es wird Zeit, endlich den Namen der ersten Spalte anzupassen.

In [29]:
df = df.rename(columns={"Dichter": "Dichter*in"})
df
Out[29]:
Dichter*in Sterbealter Brillentraeger Geschlecht
0 Goethe 83 False m
1 Schiller 46 False m
2 Brecht 58 True m
3 von Droste-Hülshoff 51 False w
4 Bachmann 47 True w
5 Seghers 82 False w

Für Dataframes mit vielen Einträgen kann es hilfreich sein, Dateneinträge nach Größe zu sortieren, z.B. signifikante Werte (kleine p-values) nach vorne. Wie das grundsätzlich funktioniert folgt hier am Beispiel unserer Dichter und ihres Effizenzindexes.

In [30]:
df.sort_values("Sterbealter")
Out[30]:
Dichter*in Sterbealter Brillentraeger Geschlecht
1 Schiller 46 False m
4 Bachmann 47 True w
3 von Droste-Hülshoff 51 False w
2 Brecht 58 True m
5 Seghers 82 False w
0 Goethe 83 False m

Einträge löschen

Die Spalte "Brillentraeger" ist einfach albern und unnötig. Gern löschen wir sie raus. Dafür benutzen wir die Funktion drop und teilen ihr über das Argument labels den Namen der zu löschenden Spalte mit und über das Argument axis die Dimension (0 für Zeile, 1 für Spalte).

In [31]:
df = df.drop(labels=["Brillentraeger"], axis=1)
# Kontrolle
df
Out[31]:
Dichter*in Sterbealter Geschlecht
0 Goethe 83 m
1 Schiller 46 m
2 Brecht 58 m
3 von Droste-Hülshoff 51 w
4 Bachmann 47 w
5 Seghers 82 w

Filtern

Mit filtern meinen wir im Zusammenhang mit Tabellen oft die Auswahl von Daten auf Grundlage bestimmter Kriterien. Typischerweise machen wir das bei DataFrame durch Erzeugung von Indexarrays, die genauso lang sind wie unser Datensatz und für jede Zeile ein True (auswählen) oder ein False enthalten - am Besten am Beispiel:

In [32]:
df.Sterbealter < 60
Out[32]:
0    False
1     True
2     True
3     True
4     True
5    False
Name: Sterbealter, dtype: bool
In [33]:
(df.Sterbealter < 60) & (df.Geschlecht == "w")
Out[33]:
0    False
1    False
2    False
3     True
4     True
5    False
dtype: bool

So könnnen wir direkt eine Auswahl treffen:

In [34]:
df.loc[(df.Sterbealter < 60) & (df.Geschlecht == "w")]
Out[34]:
Dichter*in Sterbealter Geschlecht
3 von Droste-Hülshoff 51 w
4 Bachmann 47 w

Wie geht es weiter?

Wir haben nun den DataFrame ein wenig kennengelernt. In der Praxis haben wir es mit weitaus umfangreichen Datensätzen zu tun, die wir nicht manuell in einen DataFrame schreiben. Stattdessen können wir den Inhalt aus Tabellendateien (z.B. .csv oder .xlsx-Dateien) mit pandas einlesen und dann als Dataframe behandeln. Damit wird es im nächsten Teil weitergehen...

Weiteres Selbststudium