Pandas Einführung

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 und
  • DataFrame

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 ein einfaches Series-Objekt im folgenden Beispiel 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.

In [27]:
import pandas as pd
S = pd.Series([11, 28, 72, 3, 5, 8])
S
Out[27]:
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:

In [28]:
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:

In [29]:
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:

In [30]:
fruits = ['apples', 'oranges', 'cherries', 'pears']
quantities = [20, 33, 52, 10]
S = pd.Series(quantities, index=fruits)
S
Out[30]:
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 hinzugefügt:

In [31]:
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)
sum(S)
apples      37
oranges     46
cherries    83
pears       42
dtype: int64

Out[31]:
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:

In [32]:
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)
sum(S)
cherries       83.0
oranges        46.0
peaches         NaN
pears          42.0
raspberries     NaN
dtype: float64

Out[32]:
115

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

In [34]:
print(S['apples'])
20

In [35]:
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:

In [36]:
import numpy as np
print((S + 3) * 4)
print("======================")
np.sin(S)
apples       92
oranges     144
cherries    220
pears        52
dtype: int64
======================

Out[36]:
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 liefern entweder ein Series-Objekt oder ein DataFrame-Objekt zurück, in Abhängigkeit von "func".

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:

In [37]:
S.apply(np.sin)
Out[37]:
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:

In [38]:
S.apply(lambda x: x if x > 50 else x+10 )
Out[38]:
apples      30
oranges     43
cherries    52
pears       20
dtype: int64
In [39]:
v = (4, 6)
S.apply(lambda x, y, z: (x + y) / z if x > 50 else x+10, (4, 6) )
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-39-f3467fbcdb42> in <module>()
      1 v = (4, 6)
----> 2 S.apply(lambda x, y, z: (x + y) / z if x > 50 else x+10, (4, 6) )

/home/bernd/anaconda3/lib/python3.5/site-packages/pandas/core/series.py in apply(self, func, convert_dtype, args, **kwds)
   2218         else:
   2219             values = self.asobject
-> 2220             mapped = lib.map_infer(values, f, convert=convert_dtype)
   2221 
   2222         if len(mapped) and isinstance(mapped[0], Series):

pandas/src/inference.pyx in pandas.lib.map_infer (pandas/lib.c:62658)()

TypeError: <lambda>() missing 2 required positional arguments: 'y' and 'z'

Filterung mit einem Booleschen-Array:

In [ ]:
S[S>30]

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

In [ ]:
"apples" in S

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.

In [ ]:
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)

Wir haben bereits gesehen, dass wir eine Liste oder ein Tupel als Schlüsselwort-Argument "index" übergeben können. In diesem Fall passt die Liste (oder das Tupel) nicht unbedingt zu den Schlüsseln, d.h. es können mehr oder weniger Einträge im Index sein:

In [ ]:
my_cities = ["London", "Paris", "Zurich", "Berlin", 
             "Stuttgart", "Hamburg"]
my_city_series = pd.Series(cities, index=my_cities)
print(my_city_series)

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:

In [ ]:
print(my_city_series.isnull())
In [ ]:
print(my_city_series.notnull())

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

In [ ]:
d = {"a":23, "b":45, "c":None, "d":0}
S = pd.Series(d)
print(S)
In [ ]:
pd.isnull(S)
In [ ]:
pd.notnull(S)

DataFrame

Die grundlegende Idee von DataFrame basiert auf Tabellen. Wir können die Daten-Struktur eine DataFrame als tabellarisch und tabellenähnlich sehen. Es beinhaltet eine geordnete Sammlung von Spalten. Jede Spalte besteht aus einem eindeutigen Daten-Typen, aber verschiedene Spalten haben verschiedene Typen, z.B. hat die erste Spalte den Typ Integer, während die zweite Spalte vom Typ Boolean ist, usw.

Ein DataFrame hat einen Zeilen- und ein Spalten-Index. Es ist wie ein Dictionary aus Series mit einem normalen Index.

In [ ]:
cities = {"name": ["London", "Berlin", "Madrid", "Rome", 
                   "Paris", "Vienna", "Bucharest", "Hamburg", 
                   "Budapest", "Warsaw", "Barcelona", 
                   "Munich", "Milan"],
          "population": [8615246, 3562166, 3165235, 2874038,
                         2273305, 1805681, 1803425, 1760433,
                         1754000, 1740119, 1602386, 1493900,
                         1350680],
          "country": ["England", "Germany", "Spain", "Italy",
                      "France", "Austria", "Romania", 
                      "Germany", "Hungary", "Poland", "Spain",
                      "Germany", "Italy"]}

city_frame = pd.DataFrame(cities)
print(city_frame)

Der Index (0,1,2,...) wird automatisch dem DataFrame zugewiesen. Wir können ebenfalls einen angepassten Index verwenden:

In [ ]:
ordinals = ["first", "second", "third", "fourth",
            "fifth", "sixth", "seventh", "eigth",
            "ninth", "tenth", "eleventh", "twelvth",
            "thirteenth"]
city_frame = pd.DataFrame(cities, index=ordinals)
print(city_frame)

Die Reihenfolge der Spalten kann neu angeordnet werden.

In [ ]:
city_frame = pd.DataFrame(cities,
                          columns=["name", 
                                   "country", 
                                   "population"],
                          index=ordinals)
print(city_frame)

Wir können die Summe aller Spalten berechnen oder die Summe bestimmter Spalten:

In [ ]:
city_frame.sum()
In [ ]:
city_frame["population"].sum()

Mit "cumsum" erhalten wir die kumulierte Summe:

In [ ]:
x = city_frame["population"].cumsum()
print(x)

x ist ein Pandas-Series-Objekt. Wir können dies zur "population"-Spalte zuordnen:

In [ ]:
city_frame["population"] = x
print(city_frame)

Wir können einen Spalte-Namen miteinbeziehen der nicht im Dictionary existiert. In diesem Fall, werden alle Werte dieser Spalte auf NaN gesetzt:

In [ ]:
city_frame = pd.DataFrame(cities,
                          columns=["name", 
                                   "country", 
                                   "area",
                                   "population"],
                          index=ordinals)
print(city_frame)

Es gibt zwei Wege auf eine Spalte eines DataFrame zuzugreifen. Das Ergebnis ist in beiden Fällen ein Series-Objekt:

In [ ]:
# in a dictionary-like way:
print(city_frame["population"])
In [ ]:
# as an attribute
print(city_frame.population)
In [ ]:
print(type(city_frame.population))
In [ ]:
p = city_frame.population
p["first"] = 9000000
print(city_frame)

Im vorigen Beispiel wurde die population-Spalte nicht kopiert wurde. "p" ist eine Sicht auf die Daten von city_frame.

Ebenfalls können die Zeilen direkt angesprochen werden. Im folgenden lesen wir die Info der fünften Stadt:

In [ ]:
city_frame.ix['fourth']

Die Spalte "area" ist nicht definiert. Wir können alle Elemente der Spalte auf den gleichen Wert setzen:

In [ ]:
city_frame["area"] = 1572
print(city_frame)

In diesem Fall ist es definitiv besser die exakten Werte den Städten zuzuweisen. Die Liste mit den area-Werten muss die gleiche Länge haben wie die Anzahl der Zeilen in unserem DataFrame.

In [ ]:
# area in square km:
area = [1572, 891.85, 605.77, 1285, 
        105.4, 414.6, 228, 755, 
        525.2, 517, 101.9, 310.4, 
        181.8]
city_frame["area"] = area
print(city_frame)

Wir sortieren das DataFrame entsprechend den area-Werten:

In [ ]:
city_frame = city_frame.sort_values(by="area", ascending=False)
print(city_frame)

Wir nehmen an, dass wir nur die Flächen von London, Hamburg und Milan haben. Die Flächen sind in einem Series-Objekt mit den korrekten Indizes. Wir können dies ebenfalls zuweisen:

In [ ]:
city_frame = pd.DataFrame(cities,
                          columns=["name", 
                                   "country", 
                                   "area",
                                   "population"],
                          index=ordinals)

some_areas = pd.Series([1572, 755, 181.8], 
                    index=['first', 'eigth', 'thirteenth'])

city_frame['area'] = some_areas
print(city_frame)

Ein verschachteltes Dictionary kann einem DataFrame übergeben werden. Die Indizes des äusseren Dictionary werden als Spalten verwendet und die Schlüssel des inneren Dictionary werden als Zeilen-Indizes benutzt:

In [ ]:
growth = {"Switzerland": {"2010": 3.0, "2011": 1.8, "2012": 1.1, "2013": 1.9},
          "Germany": {"2010": 4.1, "2011": 3.6, "2012":	0.4, "2013": 0.1},
          "France": {"2010":2.0,  "2011":2.1, "2012": 0.3, "2013": 0.3},
          "Greece": {"2010":-5.4, "2011":-8.9, "2012":-6.6, "2013":	-3.3},
          "Italy": {"2010":1.7, "2011":	0.6, "2012":-2.3, "2013":-1.9}
          } 
In [ ]:
growth_frame = pd.DataFrame(growth)
growth_frame

Sie haben lieber die Jahre in den Spalten und die Länder in den Zeilen? Keine Problem, Sie können die Daten austauschen:

In [ ]:
growth_frame.T
In [ ]:
growth_frame = pd.DataFrame(growth)
growth_frame.reindex(["2013", "2012", "2011", "2010"])

Das DataFrame mit Zufalls-Werten füllen:

In [ ]:
df = pd.DataFrame(np.random.randn(10, 5),
columns=['a', 'b', 'c', 'd', 'e'])
df

Wir möchten aus einer CSV-Datei die Bevölkerung aller Länder lesen (Juli 2014). Der Delimiter der Datei ist ein Leerzeichen und Kommas separieren die Tausender in den Zahlen:

In [ ]:
pop = pd.read_csv("countries_population.csv", 
                  quotechar="'", 
                  sep=" ", 
                  thousands=",")
pop
In [ ]: