Pandas Einführung

Playing Pandas

Die Pandas, über die wir in diesem Kapitel schreiben, haben nichts mit den süßen Panda-Bären zu tun und sie sind auch nicht das, was unsere Besucher hier in einem Python-Tutorial erwarten. Pandas ist ein Python-Modul, dass die Möglichkeiten von Numpy, Scipy und Matplotlib abrundet. Das Wort Pandas ist ein Akronym und ist abgleitet aus "Python and data analysis" und "panal data".

Es gibt oft Verwirrung darüber, ob Pandas nicht eine Alternative zu Numpy, Scipy und Matplotlib sei. Die Wahrheit ist aber, dass Pandas auf Numpy aufbaut. Das bedeutet auch, dass Numpy für Pandas Voraussetzung ist. Scipy und Matplotlib werden von Pandas nicht benötigt, sind aber extrem nützlich. Deshalb listet das Pandas-Projekt diese auch als "optionale Abhängigkeiten".

Pandas ist eine Software-Bibliothek die für Python geschrieben wurde. Sie wird für Daten-Manipulation und -Analyse verwendet. Sie stellt spezielle Funktionen und Daten-Strukturen zur Verfügung für die Manipulation von numerischen Tabellen und Zeit-Serien. Pandas ist eine freie Software und wurde unter der Drei-Klausel-BSD-Lizenz veröffentlicht.

Daten-Strukturen

Wir beginnen mit folgenden zwei wichtigen Daten-Strukturen von Pandas:

Series

Eine Series ist ein eindimensionales Array-ähnliches Objekt. Es kann verschiedene Daten-Typen aufnehmen, z.B. Integers, Floats, Strings, Python-Objekte, usw. Es kann als eine Daten-Struktur mit zwei Arrays angesehen werden: Ein Array fungiert als Index, d.h. als Bezeichner (Label), und ein Array beinhaltet die aktuellen Daten.

Wir definieren im folgenden Beispiel ein einfaches Series-Objekt indem wir dieses Objekt mit einer Liste instanziieren. Wir werden später sehen, dass 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])
S
Führt man obigen Code aus, erhält man Folgendes:
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, dagegen zeigt die linke Spalte den Index. Pandas erstellt einen Default-Index der bei 0 beginnt und bis 5 läuft, was der Länge-1 entspricht.

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)
S
Wir können die folgenden Ergebnisse erwarten, wenn wir den obigen Python-Code ausführen:
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 den selben Indizes addieren, so erhalten wir ein neues Series-Objekt mit diesem Index und die entsprechenden Werte werden addiert:

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 Series-Addition. Der 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
fruits = ['apples', 'oranges', 'cherries', 'pears']
fruits_ro = ["mere", "portocale", "cireșe", "pere"]
S = pd.Series([20, 33, 52, 10], index=fruits)
S2 = pd.Series([17, 13, 31, 32], index=fruits_ro)
print(S+S2)
apples      NaN
cherries    NaN
cireșe      NaN
mere        NaN
oranges     NaN
pears       NaN
pere        NaN
portocale   NaN
dtype: float64

Es ist möglich auf einzelne Werte eines Series-Objektes zuzugreifen oder auch auf mehrere bei einer Liste von Indizes:

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

Wie bei Numpy können wir die Skalar-Operationen oder Mathematische Funktionen auf ein Series-Objekt anwenden:

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

Series.apply(func, convert_dtype=True, args=(), **kwds)

Die Funktion "func" wird auf das Series-Objekt angwendet und liefert, in Abhängigkeit von "func", entweder ein Series-Objekt oder ein DataFrame-Objekt zurück.

Parameter Bedeutung
func Eine Funktion, die auf das gesamte Series-Objekt (Numpy-Funktion) oder nur auf einzelne Werte des Series (Python-Funktion) angewendet wird.
convert_dtype Ein Boolescher Wert. Wenn dieser auf True gesetzt wird (Standard), so wird versucht bei der Anwendung einen besseren dtype für die elementweisen Funktions-Ergebnisse zu finden. Wenn der Parameter auf False gesetzt wird, so wird dtype=objekt verwendet.
args Positions-Argumente die an die Funktion "func" übergeben werden, zusätzlich zu den Werten des Series-Objektes.
**kwds Zusätzliche Schlüsselwort-Argumente die als Schlüsselworte an die Funktion übergeben werden.

Beispiel:

S.apply(np.sin)
Führt man obigen Code aus, erhält man folgendes Ergebnis:
apples      0.912945
oranges     0.999912
cherries    0.986628
pears      -0.544021
dtype: float64

Wir können auch Python-Lambda-Funktionen benutzen. Wir nehmen folgende Aufgabenstellung an. Die Prüfung der Menge der Früchte. Wenn weniger als 50 vorhanden sind, so wird der Bestand um 10 erhöht:

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

Filterung mit einem Booleschen-Array:

S[S>30]
Führt man obigen Code aus, erhält man folgendes Ergebnis:
oranges     33
cherries    52
dtype: int64

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

"apples" in S
Führt man obigen Code aus, erhält man Folgendes:
True

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. Die Indizes werden sortiert.

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

Playing Pandas

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. Wir möchten einen Index, 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 de 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ölkerungs-Werten 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
Führt man obigen Code aus, erhält man folgendes Ergebnis:
London     8615246
Paris      2273305
Berlin     3562166
Hamburg    1760433
dtype: int64

Die Methoden isnull() und notnull()

Wir sehen, dass die Städte, die nicht im Dictionary existieren, den Wert NaN zugewiesen bekommen. NaN steht für "not a number". Es kann in unserem Beispiel auch als "fehlt" verstanden werden.

Wir können fehlende Werte prüfen mit den Methoden isnull() und notnull():

my_cities = ["London", "Paris", "Zurich", "Berlin", 
             "Stuttgart", "Hamburg"]
my_city_series = pd.Series(cities, index=my_cities)
print(my_city_series.isnull())
London       False
Paris        False
Zurich        True
Berlin       False
Stuttgart     True
Hamburg      False
dtype: bool
print(my_city_series.notnull())
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.isnull(S)
Der obige Python-Code liefert folgendes Ergebnis:
a    False
b    False
c     True
d    False
dtype: bool
pd.notnull(S)
Führt man obigen Code aus, erhält man folgendes Ergebnis:
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 wollen Sie die fehlenden Daten gar nicht filtern. Stattdessen möchten Sie 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, dass entspricht nun nicht wirklich "mit passenden Werten füllen". Wenn wir der Methode fillna() ein Dictionary mitgeben, können wir so die passenden Daten bereitstellen, z.B. die Bevölkerungs-Werte für Zürich und Stuttgart:

missing_cities = {"Stuttgart":597939, "Zurich":378884}
my_city_series.fillna(missing_cities)
Wir erhalten die folgende Ergebnisse:
London       8615246.0
Paris        2273305.0
Zurich        378884.0
Berlin       3562166.0
Stuttgart     597939.0
Hamburg      1760433.0
dtype: float64

Dabei haben wir aber immernoch das Problem mit Integer-Werten. Die Werte, die Integer sein sollten, wie die Anzahl der Menschen, werden nach wie vor in Float-Werte gewandelt. Das können wir nun mit der Methode fillna() lösen:

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