Zum Inhalt springen

Pandas

starlightViewModes.switchTo

Pandas ist eine leistungsstarke und weit verbreitete Bibliothek für Datenanalyse und -manipulation in der Programmiersprache Python. Entwickelt auf Basis von NumPy bietet Pandas Datenstrukturen, die ideal für die Verarbeitung und Analyse von strukturierten Daten wie Tabellen oder CSV-Dateien geeignet sind. Diese Bibliothek erleichtert das Arbeiten mit großen Datensätzen und ermöglicht es, komplexe Operationen auf Daten schnell und effizient durchzuführen. Pandas wird in den Bereichen Datenwissenschaft, Finanzanalyse, Machine Learning und anderen datenintensiven Anwendungen eingesetzt. In dieser Einführung werden wir einen Blick auf die Schlüsselfunktionen und -konzepte von Pandas werfen, um ein Verständnis für die Vielseitigkeit dieser Bibliothek zu entwickeln.

Installing pandas
pip install pandas
import pandas as pd

Series und DataFrame

Pandas führt zwei grundlegende Datenstrukturen ein: DataFrames und Series. Ein DataFrame kann als eine zweidimensionale Datenstruktur betrachtet werden, die in Form einer Tabelle organisiert ist. Diese Tabelle besteht aus Zeilen und Spalten, wobei jede Spalte einen bestimmten Datentyp repräsentiert. Einzelne Spalten in einem DataFrame werden als Series bezeichnet. Series hingegen kann als eindimensionales Array oder eine Liste betrachtet werden und enthält Daten eines bestimmten Datentyps.

Series

Eine Series ist eine grundlegende eindimensionale, beschriftete Datenstruktur, vergleichbar mit einer Liste oder einem Array, jedoch mit einem Index, der jedem Element zugeordnet ist. In Pandas ist die Series eine essentielle Datenstruktur und bildet die Grundlage für DataFrames. Sie kann als eindimensionales Array oder Liste betrachtet werden und enthält Daten unterschiedlicher Datentypen wie numerische Werte, Zeichenketten oder Zeitstempel. Die Series ermöglicht den Zugriff auf Daten durch einen Index und unterstützt eine Vielzahl von Funktionen zur Datenmanipulation. Sie erlaubt es, spezifische Datenpunkte in einem DataFrame zu isolieren und gezielte Operationen auf einzelnen Spalten durchzuführen, was sie zu einem unverzichtbaren Werkzeug für die Arbeit mit strukturierten Daten in Pandas macht.

Eine Series kann in Python so erstellt werden:

import pandas as pd
s = pd.Series([1, 3, 5, 7, 9], index=['a', 'b', 'c', 'd', 'e'])

Diese Series ist dann eine Spalte, welche so aussieht (die Buchstaben sind in diesem Fall die Indizes, sprich keine aussagekräftigen Daten, sondern nur Werkzeuge der Series):

output.txt
a 1
b 3
c 5
d 7
e 9
dtype: int64

Zugriff auf Daten

Um nun auf eine Zelle mithilfe des Indexes zuzugreifen, kann man die Syntax mit den eckigen Klammern ([]) oder die Methode loc() verwenden.

print(s['a']) # Zugriff auf Element mit Index 'a'
print(s.loc['a']) # Zugriff auf Element mit Index 'a'

Man kann Zellen auch ohne den vergebenen Index selektieren (iloc()-Methode).

print(s.iloc[0]) # Zugriff auf Element an erster Stelle (0)

DataFrame

Ein DataFrame ist eine zweidimensionale, tabellarische Datenstruktur mit beschrifteten Achsen (Zeilen und Spalten), die die Hauptdatenstruktur in Pandas darstellt. Diese Struktur organisiert Daten in Zeilen und Spalten, wobei jede Spalte eine Series darstellt. DataFrames sind äußerst vielseitig und ermöglichen die effiziente Verarbeitung sowie Analyse von strukturierten Daten unterschiedlicher Datentypen. Sie erleichtern den Zugriff, die Filterung und Transformation von Daten, und werden häufig zur Darstellung von Tabellen in verschiedenen Formaten wie CSV-Dateien, Excel-Tabellen oder SQL-Abfragen verwendet. Operationen auf DataFrames können sowohl auf Zeilen als auch auf Spalten angewendet werden, was eine umfassende Datenmanipulation ermöglicht.

Erstellen eines DataFrames

Ein DataFrame kann ähnlich wie eine Series erstellt werden. Dabei kann man eine Vielzahl an verschiedenen Objekttypen übergeben.

  • Dictionary
    In diesem Fall hat der Input für das DataFrame den Typen Dictionary, dessen Schlüssel ein String und dessen Wert eine Liste ist.

    data = {
    'A': [1, 2, 3, 4],
    'B': [5, 6, 7, 8],
    'C': [9, 10, 11, 12]
    }
    df = pd.DataFrame(data, index=["I","II","III","IV"])
  • Liste
    In diesem Fall hat der Input für das DataFrame den Typen einer Liste, in welcher sich mehrere Dictionaries befinden.

    data = [{'A': 1, 'B': 5, 'C': 9},
    {'A': 2, 'B': 6, 'C': 10},
    {'A': 3, 'B': 7, 'C': 11},
    {'A': 4, 'B': 8, 'C': 12}]
    df = pd.DataFrame(data, index=["I","II","III","IV"])
  • Tuple
    In diesem Fall hat der Input für das DataFrame den Typen einer Liste, mit mehreren Tuples darin. Man beachte, dass die Spaltennamen beim instantiieren des DataFrames angegeben werden müssen, da data selbst keine Informationen darüber enthält.

    data = [(1, 5, 9),
    (2, 6, 10),
    (3, 7, 11),
    (4, 8, 12)]
    df = pd.DataFrame(data, columns=['A', 'B', 'C'], index=["I","II","III","IV"])
  • NumPy
    Man kann ein DataFrame auch mithilfe von einem Numpy-Array erstellen.

    import numpy as np
    data = np.array([[1, 5, 9],
    [2, 6, 10],
    [3, 7, 11],
    [4, 8, 12]])
    df = pd.DataFrame(data, columns=['A', 'B', 'C'], index=["I","II","III","IV"])
  • CSV
    Man kann ein DataFrame auch mithilfe einer CSV-Datei erstellen. Dies kommt in der Praxis sehr häufig vor, da CSV-Dateien häufig als Eingabe für Analyseprogramme verwendet werden. Hierbei kann man auch angeben, dass eine Bestimmte Spalte Informationen bezüglich der Indizes enthält.

    file.csv
    ,A,B,C
    I,1,5,9
    II,2,6,10
    III,3,7,11
    IV,4,8,12
    df = pd.read_csv('file.csv', index_col=0) # Beispiel für das Lesen aus einer CSV-Datei
  • JSON
    Natürlich kann das Datenformat auch JSON sein:

    file.json
    {
    "A": { "I": 1, "II": 2, "III": 3, "IV": 4 },
    "B": { "I": 5, "II": 6, "III": 7, "IV": 8 },
    "C": { "I": 9, "II": 10, "III": 11, "IV": 12 }
    }
    df = pd.read_json('file.json') # Beispiel für das Lesen aus einer JSON-Datei
  • SQL-Datenbank
    Man kann auch direkt aus einer SQL-Datenbank lesen. In diesem Fall muss man zunächst eine Verbindung mit der Datenbank herstellen.

    Installing sqlite3
    pip install pysqlite3
    import sqlite3
    con = sqlite3.connect("database.db")
    df = pd.read_sql_query("SELECT * FROM examples", con)

In allen Fällen sieht das DataFrame in diesem Beispiel zum Schluss so aus:

output.txt
A B C
I 1 5 9
II 2 6 10
III 3 7 11
IV 4 8 12

Speichern eines DataFrames

Um ein DataFrame persistieren zu können, kann man die Daten in eine Datei oder Datenbank speichern. Dabei werden die Formate CSV und JSON am häufigsten verwendet, weswegen die Integration mit Pandas einfacher denn je ist.

  • CSV

    df.to_csv('example.csv')
  • JSON

    df.to_json('example.json')
  • SQL

    df.to_sql('example.sql', con)

Zugriff auf Daten

Bei einem DataFrame ist der Zugriff auf die Daten nun ein wenig anders, da ein DataFrame quasi eine Liste von Series ist.

  • Zugriff auf bestimmte Spalten
    Um auf eine Spalte zuzugreifen, können diese Möglichkeiten verwendet werden:

    print(df.A) # Property A
    print(df['A']) # Spalte A
    print(df.loc[:, 'A']) # Spalte A; alle Zeilen
    print(df.iloc[:, 0]) # Spalte Index 0; alle Zeilen

    Um auf mehrere Spalten zuzugreifen, können diese Möglichkeiten verwendet werden:

    print(df[['A', 'B']]) # Spalten A & B
    print(df.loc[:, ['A', 'B']]) # Spalten A & B; alle Zeilen
    print(df.iloc[:, [0, 1]]) # Spalten Indizes 0 & 1; alle Zeilen
    print(df.iloc[:, 0:2]) # Spalten Indizes 0 bis 1; alle Zeilen
    print(df.iloc[:, :2]) # Spalten Indizes bis 1; alle Zeilen
    print(df.iloc[:, :-1]) # Spalten Indizes bis vorletzte Spalte; alle Zeilen
  • Zugriff auf bestimmte Zeilen
    Um auf eine Zeile zuzugreifen, können diese Möglichkeiten verwendet werden:

    print(df.loc["I"]) # Zeile I
    print(df.iloc[0]) # Zeile Index 0

    Um auf mehrere Zeilen zuzugreifen, können diese Möglichkeiten verwendet werden:

    print(df.loc[["I", "II"]]) # Zeilen I & II
    print(df.iloc[[0, 1]]) # Zeilen Indizes 0 & 1
    print(df.iloc[0:2]) # Zeilen Indizes 0 bis 1
    print(df.iloc[:2]) # Zeilen Indizes bis 1
    print(df.iloc[:-2]) # Zeilen Indizes bis vorvorletzte Zeile
  • Zugriff auf bestimmte Zellen
    Um auf eine Zelle zuzugreifen, können diese Möglichkeiten verwendet werden:

    print(df.loc["I", 'A']) # Zeile I; Spalte A
    print(df.iloc[0, 0]) # Zeile Index 0; Spalte Index 0

    Um auf mehrere Zellen zuzugreifen, können diese Möglichkeiten verwendet werden (bei iloc() sind die Angaben jeweils inklusiv:exklusiv):

    print(df.loc[["I","II","III"], ['A','B']]) # Zeilen I, II & III; Spalten A & B
    print(df.loc["I":"III", 'A':'B']) # Zeilen I bis III; Spalten A bis B
    print(df.iloc[0:3, 0:2]) # Zeilen Indizes 0 bis 2; Spalten Indizes 0 bis 1
    print(df.iloc[:3, :2]) # Zeilen Indizes bis 2; Spalten Indizes bis 1
    print(df.iloc[:-1, :-1]) # Zeilen Indizes bis vorletzte Zeile; Spalten Indizes bis vorletzte Spalte
  • Zugriff mittels Bedingungen
    Die Daten eines DataFrames können mithilfe von Bedingungen auch gefiltert werden:

    # Alle Zeilen, dessen Wert in Spalte 'A' größer 2 ist
    print(df[df['A'] > 2])
    """
    A B C
    III 3 7 11
    IV 4 8 12
    """
    # Alle Series 'A', dessen Wert größer 2 ist
    print(df.loc[df['A'] > 2, 'A'])
    """
    III 3
    IV 4
    """

    Diese Bedingungen funktieren nur, weil die Bedingung eine Series selbst ist und somit die Berechnung pro Zeile durchgeführt werden.

    df['A'] > 2
    """
    I False
    II False
    III True
    IV True
    Name: A, dtype: bool
    """

Operationen und Methoden

Pandas bietet eine Vielzahl von Methoden und Operationen zur Datenanalyse und -manipulation. Alle Methoden, welche die Daten des DataFrames verändern, verfügen über zwei Möglichkeiten, die Änderungen zu speichern:

  • Überschreiben
    Einerseits kann man die alten Daten einfach überschreiben, indem man das DataFrame auf die neuen Daten setzt:

    df = df.dropna()

    Im Hintergrund wird der Parameter inplace der Methode auf False gesetzt und die neuen Daten werden einfach returniert.

    df.dropna(inplace=False) # dieser Aufruf gibt das DataFrame zurück
  • Inplace
    Die elegantere Variante ist jedoch die Verwendung der inplace-Option, welche die Änderungen direkt in dem DataFrame speichert:

    df.dropna(inplace=True)

    Wichtig zu beachten ist, dass beim positiven Setzen des inplace-Parameters kein DataFrame mehr zurückgegeben wird. Stattdessen returniert dieser Aufruf nun None.

Mit der Methode head kann man die ersten n Zeilen eines DataFrames anzeigen.

df.head(3) # Standardmäßig ersten 5 Zeilen
"""
A B C
I 1 5 9
II 2 6 10
III 3 7 11
"""

tail()

Mit der Methode tail kann man die letzten n Zeilen eines DataFrames anzeigen.

df.tail(3) # Standardmäßig letzten 5 Zeilen
"""
A B C
II 2 6 10
III 3 7 11
IV 4 8 12
"""

shape

Mit der Eigenschaft shape kann man die Dimensionen des DataFrames anzeigen.

df.shape
"""
(4, 3)
"""

Dabei ist die Reihenfolge der Anzahl an Dimensionen gleich der aufsteigenden Achsenzahl. In unserem Beispiel bedeutet das, wir haben vier Zeilen und drei Spalten, weil Achse 0 die Zeilen sind und Achse 1 die Spalten.

Die erste Zahl ist in den meisten Fällen der künstlichen Intelligenz die Anzahl an Beispielen. In unserem Fall gibt uns die zweite Zahl Auskunft über die Anzahl an Features (Eigenschaften) jedes Beispiels. Beim Supervised Learning ist die letzte Spalte meistens die Antwort des Beispiels, weshalb es auch nur zwei Features sein könnten.

columns

Mit der Eigenschaft columns kann man sich die Namen der Spalten ansehen.

df.columns
"""
Index(['A', 'B', 'C'], dtype='object')
"""

info()

Mit der Methode info kann man sich allgemeine Informationen über ein DataFrame oder eine Series ausgeben lassen.

df.info()
"""
<class 'pandas.core.frame.DataFrame'>
Index: 4 entries, I to IV
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 A 4 non-null int64
1 B 4 non-null int64
2 C 4 non-null int64
dtypes: int64(3)
memory usage: 128.0+ bytes
None
"""
df['A'].info()
"""
<class 'pandas.core.series.Series'>
Index: 4 entries, I to IV
Series name: A
Non-Null Count Dtype
-------------- -----
4 non-null int64
dtypes: int64(1)
memory usage: 64.0+ bytes
None
"""

describe()

Mit der Methode describe kann man sich Informationen der deskriptiven Statistik über ein DataFrame oder eine Series ausgeben lassen.

df.describe()
"""
A B C
count 4.000000 4.000000 4.000000
mean 2.500000 6.500000 10.500000
std 1.290994 1.290994 1.290994
min 1.000000 5.000000 9.000000
25% 1.750000 5.750000 9.750000
50% 2.500000 6.500000 10.500000
75% 3.250000 7.250000 11.250000
max 4.000000 8.000000 12.000000
"""
df['A'].describe()
"""
count 4.000000
mean 2.500000
std 1.290994
min 1.000000
25% 1.750000
50% 2.500000
75% 3.250000
max 4.000000
Name: A, dtype: float64
"""

corr()

Die Methode corr errechnet die Korrelationen zwischen den Spalten.

"""
A B C
I 2 3 4
II 4 9 3
III 8 27 2
IV 16 81 1
"""
df.corr()
"""
A B C
A 1.000000 0.991934 -0.959166
B 0.991934 1.000000 -0.916515
C -0.959166 -0.916515 1.000000
"""

isnull()

Die Methode isnull gibt ein DataFrame mit boolischen Werte zurück, welche Auskunft über die fehlenden Werte eines DataFrames geben. Mithilfe der sum-Methode kann man dadurch wichtige Informationen des DataFrames extrahieren.

"""
A B C
I ‎NaN 5 9.0
II 2.0 6 10.0
III 3.0 7 11.0
IV 4.0 8 ‎NaN
"""
df.isnull()
"""
A B C
I True False False
II False False False
III False False False
IV False False True
"""
df.isnull().sum()
"""
A 1
B 0
C 1
dtype: int64
"""
df.isnull().sum(axis=1)
"""
I 1
II 0
III 0
IV 1
dtype: int64
"""
df.isnull().sum().sum()
"""
2
"""

dropna()

Die Methode dropna entfernt Zeilen (oder Spalten) mit fehlenden Werten (NaN — Not a Number) aus dem DataFrame.

dropna zählt zu den manipulativen Methoden, weswegen der Paramter inplace zur Verfügung steht.

"""
A B C
I ‎NaN 5 9.0
II 2.0 6 10.0
III 3.0 7 11.0
IV 4.0 8 ‎NaN
"""
df.dropna() # automatically axis 0
"""
A B C
II 2.0 6 10.0
III 3.0 7 11.0
"""
df.dropna(axis=1)
"""
B
I 5
II 6
III 7
IV 8
"""

fillna()

Die Methode fillna ersetzt Zellen mit fehlenden Werten (NaN — Not a Number).

fillna zählt zu den manipulativen Methoden, weswegen der Paramter inplace zur Verfügung steht.

"""
A B C
I ‎NaN 5 9.0
II 2.0 6 10.0
III 3.0 7 11.0
IV 4.0 8 ‎NaN
"""
df.fillna(0)
"""
A B C
I 0.0 5 9.0
II 2.0 6 10.0
III 3.0 7 11.0
IV 4.0 8 0.0
"""

unique()

Die Methode unique gibt die eindeutigen Werte in einer Spalte zurück.

"""
A B C
I 1 5 9
II 2 6 10
III ‎1 7 11
IV 4 8 12
"""
df['A'].unique()
"""
[1 2 4]
"""

nunique()

Die Methode nunique gibt die Anzahl der eindeutigen Werte insgesamt zurück.

"""
A B C
I 1 5 9
II 2 6 10
III ‎1 7 11
IV 4 8 12
"""
df['A'].nunique()
"""
3
"""

value_counts()

Die Methode value_counts gibt die Anzahl der eindeutigen Werte pro Spalte zurück.

"""
A B C
I 1 5 9
II 2 6 10
III ‎1 7 11
IV 4 8 12
"""
df['A'].value_counts()
"""
A
1 2
2 1
4 1
Name: count, dtype: int64
"""

apply()

Die Methode apply wendet eine Funktion entlang einer Achse des DataFrame an. Sie kann auf Zeilen oder Spalten angewendet werden.

Standardmäßig werden Operationen auf eine Spalte angewandt (axis=0).

# Durchschnitt pro Spalte
df.apply(lambda x: x.mean())
"""
A 2.5
B 6.5
C 10.5
dtype: float64
"""

Dies kann man jedoch einfach ändern:

# Durchschnitt pro Zeile
df.apply(lambda x: x.mean(), axis=1)
"""
I 5.0
II 6.0
III 7.0
IV 8.0
dtype: float64
"""

map()

Die Methode map wendet eine Funktion auf jedes Element einer Series an. Die Methode ist demnach ähnlich wie apply pro Spalte mit dem Unterschied, dass map nicht auf das gesamte DataFrame (alle Spalten auf einmal) angewandt werden kann.

# Funktion zur Quadratbildung auf jedes Element anwenden
df['A'].map(lambda x: x ** 2)
"""
I 1
II 4
III 9
IV 16
Name: A, dtype: int64
"""

applymap()

Die Methode applymap wendet eine Funktion auf jedes Element (jede Zelle) eines DataFrame an.

# Funktion zur Quadratbildung auf jedes Element des gesamten DataFrame anwenden
df.applymap(lambda x: x ** 2)
"""
A B C
I 1 25 81
II 4 36 100
III 9 49 121
IV 16 64 144
"""

groupby()

Die Methode groupby gruppiert das DataFrame nach den Werten einer oder mehrerer Spalten und ermöglicht Aggregationen wie bei SQL-Abfragen.

"""
A B C
I 1 5 9
II ‎1 6 10
III 3 7 11
IV ‎3 8 12
"""
df.groupby('A').sum() # Summe der Gruppen 1 und 3 berechnen
"""
B C
A
1 11 19
3 15 23
"""

rename()

Mithilfe der manipulativen Methode rename kann man jegliche Eigenschaften eines DataFrames umbenennen. Meistens will man die Spaltennamen oder Indexes umbenennen.

rename zählt zu den manipulativen Methoden, weswegen der Paramter inplace zur Verfügung steht.

df.rename(columns={'A':'X', 'B':'Y', 'C':'Z'})
"""
X Y Z
I 1 5 9
II 2 6 10
III 3 7 11
IV 4 8 12
"""
df.rename(index=str.lower)
"""
A B C
i 1 5 9
ii 2 6 10
iii 3 7 11
iv 4 8 12
"""

replace()

Mithilfe der manipulativen Methode replace kann man beliebige alphanumerische Werte mit statischen oder dynamischen Werten ersetzen. Allerdings können diese Werte nur in den Daten, also nicht in den Spaltennamen und Indizes, vorkommen. Deswegen ist das replace quasi das rename für die Werte.

replace zählt zu den manipulativen Methoden, weswegen der Paramter inplace zur Verfügung steht.

df.replace(1, 2)
"""
A B C
I 2 5 9
II 2 6 10
III 3 7 11
IV 4 8 12
"""

Anwendungsbeispiele

Bereinigen von Nullwerten

Im Bereich der künstlichen Intelligenz sind Nullwerte oft unerwünscht, weshalb man diese meistens mit Werte ersetzen möchte, welche das Trainieren des Modells nicht behindern. Eine einfache Möglichkeit, Nullwerte nicht ganz so störend zu machen, ist das spaltenweise Ersetzen der Nullwerte durch den Durchschnitt der Werte in der Spalte.

for col in df.columns:
df[col].fillna(df[col].mean(), inplace=True)

Meistens ist es auch noch sinnvoll, die Nullwerte nicht einfach mit dem Median aller Datensätze zu ersetzen, sondern nur mit dem Median der Datensätze, die beim Supervised Learning auch die gleichen Antworten haben. Somit sind die fehlenden Daten am wenigsten störend für das Trainieren.

"""
A B C Outcome
I 31 34.0 32.0 0
II 17 ‎NaN 10.0 1
III 13 ‎NaN 11.0 1
IV 14 18.0 12.0 1
V 29 26.0 29.0 0
VI 31 35.0 ‎NaN 0
"""
# Replace all NaN with median + watch out for Outcome value
sensible_col = ['A', 'B', 'C']
masks = [ df['Outcome'] == 0, df['Outcome'] == 1 ]
for mask in masks:
df.loc[mask, sensible_col] = df.loc[mask, sensible_col].replace(np.nan, df[df[mask] != np.nan].mean())
"""
A B C Outcome
I 31 34.0 32.0 0
II 17 ‎18.0 10.0 1
III 13 ‎18.0 11.0 1
IV 14 18.0 12.0 1
V 29 26.0 29.0 0
VI 31 35.0 ‎30.5 0
"""

Standardisierung der Spaltennamen

In Python gilt meistens snake_case. Das bedeutet, dass alles klein geschrieben und Wörter mit Unterstrichen (_) getrennt werden. Da viele Datensätze oft nicht diesen Konventionen folgen, ist es sinnvoll, die Spaltennamen zu standardisieren.

In diesem Beispiel werden alle Spaltennamen automatisch in Kleinbuchstaben konvertiert, Klammern entfernt und Leerzeichen durch Unterstriche ersetzt.

df.columns = [col.lower().replace("(", "").replace(")", "").replace(" ", "_") for col in df]