pandas kompakt: pandas ist eine Bibliothek für tabellarische und beschriftete Daten. Die beiden zentralen Strukturen sind Series und DataFrame; beide verbinden Werte mit einem Index.

Diese Einführung behandelt Series, Ausrichtung über Indizes, elementweise Operationen und fehlende Werte. Weitere Kapitel führen zu DataFrames, groupby, Datei-I/O und Zeitreihen.

# invisible
import numpy as np
import pandas as pd
np.set_printoptions(linewidth=60)


pd.set_option('display.max_colwidth', 65)
pd.set_option('display.max_columns', 5)

Pandas Einführung

Spielende Pandas

pandas ist eine Python-Bibliothek für Datenanalyse und Datenmanipulation. Sie stellt beschriftete Datenstrukturen bereit und eignet sich besonders für tabellarische Daten, Zeitreihen und heterogene Datensätze. Der Name geht auf den statistischen Begriff „panel data“ zurück.

Die beiden zentralen Datenstrukturen sind Series (eindimensional) und DataFrame (zweidimensional). pandas baut eng auf NumPy auf, ergänzt dessen Arrays aber um Labels, Indizes, komfortable Auswahloperationen, fehlende Werte, Gruppierungen, Zusammenführungen und Datei-I/O.

NumPy ist eine Kernabhängigkeit von pandas; Bibliotheken wie Matplotlib ergänzen pandas beispielsweise bei der Visualisierung.

Daten-Strukturen in Pandas

Wir beginnen mit beiden wichtigsten Daten-Strukturen von Pandas:

  • Series und
  • DataFrame

Series

Eine Series ist eine eindimensionale, beschriftete Datenstruktur. Sie besitzt einen Index und einen dtype. Abhängig vom Datentyp kann eine Series Zahlen, Strings, Datumswerte, boolesche Werte oder auch Python-Objekte enthalten; innerhalb einer Series beschreibt der dtype jedoch den gemeinsamen Speichertyp bzw. Extension-Datentyp der Werte.

Eine Series kann als eine Datenstruktur mit zwei Arrays angesehen werden: Ein Array fungiert als Index, d.h. als Bezeichner (Label), und ein Array beinhaltet die aktuellen Daten (Werte).

Wir definieren im folgenden Beispiel ein einfaches Series-Objekt, indem wir dieses Objekt mit einer Liste instanziieren. Wir werden später sehen, dass wir auch andere Daten-Objekte verwenden können, z.B. Numpy-Arrays und Dictionaries.

import pandas as pd
S = pd.Series([11, 28, 72, 3, 5, 8])
print(S)
0    11
1    28
2    72
3     3
4     5
5     8
dtype: int64

Wir haben in unserem Beispiel keinen Index definiert. Trotzdem sehen wir zwei Spalten in der Ausgabe: Die rechte Spalte zeigt unsere Daten, die linke Spalte stellt den Index dar. Pandas erstellt einen Default-Index, der bei 0 beginnt und bis 5 läuft.

Wir können direkt auf die Indizes und die Werte der Series S zugreifen:

print(S.index)
print(S.values)
RangeIndex(start=0, stop=6, step=1)
[11 28 72  3  5  8]

Wenn wir dies mit der Erstellung eines Arrays in NumPy vergleichen, stellen wir viele Gemeinsamkeiten fest:

import numpy as np
X = np.array([11, 28, 72, 3, 5, 8])
print(X)
print(S.values)
# both are the same type:
print(type(S.values), type(X))
[11 28 72  3  5  8]
[11 28 72  3  5  8]
<class 'numpy.ndarray'> <class 'numpy.ndarray'>

Bis hierhin unterscheiden sich die Series noch nicht wirklich von den ndarrays aus Numpy. Das ändert sich aber, sobald wir Series-Objekte mit individuellen Indizes definieren:

fruits = ['apples', 'oranges', 'cherries', 'pears']
quantities = [20, 33, 52, 10]
S = pd.Series(quantities, index=fruits)
print(S)
apples      20
oranges     33
cherries    52
pears       10
dtype: int64

Eine großer Vorteil gegenüber Numpy-Arrays ist hier ganz offensichtlich: Wir können beliebige Indizes verwenden.

Wenn wir zwei Series-Objekte mit denselben Indizes addieren, so erhalten wir ein neues Series-Objekt mit diesem Index, und die Werte entsprechen den Summen der entsprechenden Werte aus den beiden Series-Objekten.

fruits = ['apples', 'oranges', 'cherries', 'pears']

S = pd.Series([20, 33, 52, 10], index=fruits)
S2 = pd.Series([17, 13, 31, 32], index=fruits)
print(S + S2)
print("Summe aus S: ", sum(S))
apples      37
oranges     46
cherries    83
pears       42
dtype: int64
Summe aus S:  115

Die Indizes müssen nicht identisch sein für die Addition von Series-Typen. Der resultierende Index ist eine "Vereinigung" beider Indizes. Wenn ein Index nicht in beiden Series-Objekten vorkommt, so wird der entsprechende Wert auf NaN gesetzt:

fruits = ['peaches', 'oranges', 'cherries', 'pears']
fruits2 = ['raspberries', 'oranges', 'cherries', 'pears']

S = pd.Series([20, 33, 52, 10], index=fruits)
S2 = pd.Series([17, 13, 31, 32], index=fruits2)
print(S + S2)
cherries       83.0
oranges        46.0
peaches         NaN
pears          42.0
raspberries     NaN
dtype: float64

Prinzipiell können die Indizes auch komplett verschieden sein, wie im folgenden Beispiel. Der zweite Index besteht aus der türkischen Übersetzung der englischen Fruchtnamen:

fruits = ['apples', 'oranges', 'cherries', 'pears']

fruits_tr = ['elma', 'portakal', 'kiraz', 'armut']

S = pd.Series([20, 33, 52, 10], index=fruits)
S2 = pd.Series([17, 13, 31, 32], index=fruits_tr)
print(S + S2)
apples     NaN
armut      NaN
cherries   NaN
elma       NaN
kiraz      NaN
oranges    NaN
pears      NaN
portakal   NaN
dtype: float64

Indizierung

Es ist möglich, auf einzelne Werte eines Series-Objektes zuzugreifen:

print(S['apples'])
20

Man kann auch auf mehrere Indizes gleichzeitig zugreifen, wenn man die Indices als Liste übergibt:

print(S[['apples', 'oranges', 'cherries']])
apples      20
oranges     33
cherries    52
dtype: int64

Filterung mit einem Booleschen Array:

S[S>30]
Ausgabe:
oranges     33
cherries    52
dtype: int64

Wie bei Numpy sind auch Operationen mit Skalaren oder die Anwendung von mathematischen Funktionen auf ein Series-Objekt möglich:

import numpy as np
print((S + 3) * 4)
print("======================")
print(np.sin(S))
apples       92
oranges     144
cherries    220
pears        52
dtype: int64
======================
apples      0.912945
oranges     0.999912
cherries    0.986628
pears      -0.544021
dtype: float64

pandas.Series.apply

Ab pandas 3.0 lautet die Signatur:

Series.apply(func, args=(), *, by_row="compat", **kwargs)

apply() ruft eine Funktion für Werte einer Series auf. Für einfache elementweise Abbildungen ist auch Series.map() häufig eine gute Wahl; für Aggregationen stehen unter anderem agg() bzw. aggregate() zur Verfügung.

Parameter Bedeutung
func Aufzurufende Python-Funktion oder NumPy-Ufunc.
args Zusätzliche Positionsargumente, die an func weitergereicht werden.
by_row Steuert bei aktuellen pandas-Versionen die Art der Anwendung; der Standardwert ist "compat".
**kwargs Zusätzliche Schlüsselwortargumente für func.

Bei pandas 2.x kann die lokal angezeigte Signatur noch den inzwischen entfernten Parameter convert_dtype enthalten.

Beispiel:

S.apply(np.log)
Ausgabe:
apples      2.995732
oranges     3.496508
cherries    3.951244
pears       2.302585
dtype: float64

Wir können auch Python-Lambda-Funktionen benutzen. Wir werden nun die Anzahl der Früchte prüfen: Wenn weniger als 50 von einer Sorte vorhanden sind, so soll der Bestand um 10 erhöht werden. Ansonsten lassen wir den Betrag unverändert:

S.apply(lambda x: x if x > 50 else x+10 )
Ausgabe:
apples      30
oranges     43
cherries    52
pears       20
dtype: int64

Zusammenhang zu Dictionaries

Ein Series-Objekt kann angesehen werden wie ein geordnetes Python-Dictionary mit einer festen Länge.

Wir können bei der Erstellung eines Series-Objektes ein Dictionary übergeben. Wir erhalten ein Series-Objekt mit den Schlüsseln des Dictionarys als Indizes.

cities = {"London":    8615246, 
          "Berlin":    3562166, 
          "Madrid":    3165235, 
          "Rome":      2874038, 
          "Paris":     2273305, 
          "Vienna":    1805681, 
          "Bucharest": 1803425, 
          "Hamburg":   1760433,
          "Budapest":  1754000,
          "Warsaw":    1740119,
          "Barcelona": 1602386,
          "Munich":    1493900,
          "Milan":     1350680}
city_series = pd.Series(cities)
print(city_series)
London       8615246
Berlin       3562166
Madrid       3165235
Rome         2874038
Paris        2273305
Vienna       1805681
Bucharest    1803425
Hamburg      1760433
Budapest     1754000
Warsaw       1740119
Barcelona    1602386
Munich       1493900
Milan        1350680
dtype: int64

NaN - Fehlende Daten

Missing Data

Ein Problem bei Aufgaben in der Datenanalyse besteht in fehlenden Daten.

Schauen wir uns noch einmal das vorherige Beispiel an. Dabei erkennen wir, dass die Indizes der Series mit den Keys des Dictionaries übereinstimmt, aus dem das Series-Objekt cities_series erzeugt wurde. Nehmen wir nun an, dass wir einen Index haben wollen, der sich nicht mit den Keys des Dictionaries überschneidet. Dafür können wir eine Liste oder ein Tupel dem Keyword-Argument 'index' mitgeben, um die Indizes zu definieren. Im nächsten Beispiel übergeben wir eine Liste (oder ein Tupel) als Indizes, welches nicht mit den Keys übereinstimmt. Das bedeutet, dass einige Städte des Dictionaries fehlen und für Stuttgart und Zürich keine Daten vorhanden sind.

my_cities = ["London", "Paris", "Zurich", "Berlin", 
             "Stuttgart", "Hamburg"]
my_city_series = pd.Series(cities, index=my_cities)
print(my_city_series)
London       8615246.0
Paris        2273305.0
Zurich             NaN
Berlin       3562166.0
Stuttgart          NaN
Hamburg      1760433.0
dtype: float64

Abgesehen von den NaN-Werten werden bei den anderen Bevölkerungswerten die Werte in float-Werte gewandelt. Im folgenden Beispiel gibt es keine fehlenden Daten, und damit werden die Werte in Integer-Werte gewandelt:

my_cities = ["London", "Paris", "Berlin", "Hamburg"]
my_city_series = pd.Series(cities, index=my_cities)
my_city_series
Ausgabe:
London     8615246
Paris      2273305
Berlin     3562166
Hamburg    1760433
dtype: int64

Fehlende Werte mit isna() und notna()

Wir sehen, dass für nicht vorhandene Städte ein fehlender Wert entsteht. Je nach Datentyp verwendet pandas unterschiedliche Missing-Value-Repräsentationen, darunter NaN, NaT und pd.NA.

Mit isna() und notna() können wir fehlende Werte prüfen. Die älteren Namen isnull() und notnull() existieren weiterhin als Aliase:

my_cities = ["London", "Paris", "Zurich", "Berlin", 
             "Stuttgart", "Hamburg"]
my_city_series = pd.Series(cities, index=my_cities)
print(my_city_series.isna())
London       False
Paris        False
Zurich        True
Berlin       False
Stuttgart     True
Hamburg      False
dtype: bool
print(my_city_series.notna())
London        True
Paris         True
Zurich       False
Berlin        True
Stuttgart    False
Hamburg       True
dtype: bool

Zusammenhang zwischen NaN und None

Wir erhalten ebenfalls NaN, wenn ein Wert in dem Dictionary None ist:

d = {"a":23, "b":45, "c":None, "d":0}
S = pd.Series(d)
print(S)
a    23.0
b    45.0
c     NaN
d     0.0
dtype: float64
pd.isna(S)
Ausgabe:
a    False
b    False
c     True
d    False
dtype: bool
pd.notna(S)
Ausgabe:
a     True
b     True
c    False
d     True
dtype: bool

Fehlende Daten filtern

Es ist möglich, die fehlenden Daten mit der Methode dropna aus einem Series-Objekt herauszufiltern. Die Methode liefert ein neues Series-Objekt zurück, welches keine NaN-Werte enthält:

print("Vorher:\n")
print(my_city_series)
print("\nNachher:\n")
print(my_city_series.dropna())
Vorher:

London       8615246.0
Paris        2273305.0
Zurich             NaN
Berlin       3562166.0
Stuttgart          NaN
Hamburg      1760433.0
dtype: float64

Nachher:

London     8615246.0
Paris      2273305.0
Berlin     3562166.0
Hamburg    1760433.0
dtype: float64

Fehlende Daten auffüllen

In vielen Fällen will man die fehlenden Daten gar nicht filtern. Stattdessen möchten man diese mit passenden Werten auffüllen. Eine gute Methode ist fillna:

print(my_city_series.fillna(0))
London       8615246.0
Paris        2273305.0
Zurich             0.0
Berlin       3562166.0
Stuttgart          0.0
Hamburg      1760433.0
dtype: float64

Okay, das sind nicht wirklich passende Werte für die Bevölkerung von Zürich und Stuttgart. Wenn wir der Methode fillna() ein Dictionary mitgeben, können wir so die passenden Daten bereitstellen, z.B. die Bevölkerungswerte für Zürich und Stuttgart. Wir setzen den Parameter inplace auf True, damit die Änderungen auch in dem Objekt geändert werden. Bei True wird ein neues Objekt mit den Einsetzungen erzeugt und zurückgeliefert, und das alte bleibt dabei unverändert:

missing_cities = {"Stuttgart":597939, "Zurich":378884}
my_city_series.fillna(missing_cities, inplace=True)
my_city_series
Ausgabe:
London       8615246.0
Paris        2273305.0
Zurich        378884.0
Berlin       3562166.0
Stuttgart     597939.0
Hamburg      1760433.0
dtype: float64

Dabei haben wir aber immer noch das Problem mit Integer-Werten. Die Werte, die Integer sein sollten, wie die Anzahl der Menschen, werden nach wie vor in Float-Werte gewandelt. Mit der Methode astype können wir die Daten in Integer wandeln:

my_city_series = my_city_series.astype(int)
print(my_city_series)
London       8615246
Paris        2273305
Zurich        378884
Berlin       3562166
Stuttgart     597939
Hamburg      1760433
dtype: int64