Datenvorverarbeitung#

Eine Person, die weiß, dass sie nichts weiß, weiß mehr als eine Person,
die nicht weiß, dass sie nichts weiß.

— Socrates

Dieser Abschnitt erklärt und demonstriert bestimmte Datenbereinigungs- und Vorbereitungsaufgaben unter Verwendung von Pandas. Die Aufgabe hier besteht hauptsächlich darin, Sie mit verschiedenen nützlichen Funktionen vertraut zu machen und zu zeigen, wie häufige Aufgaben gelöst werden. Wir sprechen nicht viel über grundlegende Datenverarbeitungsprobleme.

Folien#

Fehlende Beobachtungen#

In vielen Datensätzen aus der Praxis stoßen wir auf fehlende Werte. Sie enstehen weil (i) Werte nicht regelmäßig erfasst wurden, (ii) Werte historisch nicht gemessen wurden, (iii) Messgeräte ausfallen oder (iv) Werte entfernt worden sind, weil man dachte, dass sie nicht benötigt werden.

Solche fehlenden Werte, können sehr schnell Berechnungen verfälschen oder unmöglich machen. Deshalb ist es wichtig, fehlende Werte zu identifizieren und diese richtig zu behandeln.

Wie werden fehlende Daten codiert?#

Grundsätzlich ist es wichtig das man fehlende Werte in den Daten codiert und abspeichert, denn nur so kann man unterscheiden, ob sie bewust oder zufällig fehlen.

  • Numpy np.nan - steht eigentlich für “Not a Number”

  • Pandas pd.NA - steht für “Not Available” und ist eindeutiger als np.nan

Zum Beispiel schauen Sie sich eine Lieferliste in Excel an. Bei einigen Lieferungen ist die Zelle der gelieferten Teile leer. Jetzt ist die Beutung unklar: (i) wurde nichts geliefert; (ii) wurde etwas geliefert, aber nicht erfasst; (iii) wurde der Eintrag gelöscht oder (iv) ist der Eintrag selbst falsch und es gab keine Lieferung. Deshalb ist die Aussage das ein Wert nicht da ist stets zu unterscheiden von einer leeren Menge.

Zur Codierung dieser fehlenden Werte bieten einige Sprachen für Machine Learning spezielle Werte an. So gibt es in R den Wert NA (“not available”) und in Julia oder Matlab den Wert missing (fehlend). Zusätzlich gibt es den numerischen Wert NaN (not a number) für ungültige mathematische Ergebnisse (wie den Logarithmus einer negativen Zahl) zu kennzeichnen. Der entscheidende Unterschied ist, das für NaN mathematische Operationen definiert sind, die meist NaN als Ergebnis haben. Ein numerischer Algorithmus, kann also ohne Fehler weiter rechnen. Das ist bei NA / missing nicht der Fall, wo Berechnungen scheitern.

Numpy bietet für die Codierung nicht vorhandener nur den speziellen numerischen Datentyp np.nan an. Dadurch wird gewährleistet, das numerische Berechnungen ausführbar sind. Es ist zu beachten, dass np.nan numerisch ist, wodurch eine Serie mit fehlenden Werten entweder numerisch sein sollte oder bei der Verwendung mit nicht-numerischen Werten (str, bool) zum generischen Typ object wechselt. Wenn beispielsweise ein fehlender Wert np.nan zu einer logischen Serie hinzugefügt wird, ist das Ergebnis vom Typ _object. In manchen Fällen ist es dennoch wichtig zu unterscheiden, ob der Wert das Ergebnis einer ungültigen Berechnung ist (z.B. \(log(-1)\)) oder ein fehlender Wert. Deshalb hat Pandas 2.0.3 der spezielle Wert pd.NA eingeführt, bei deren Verwendung sich der Datentyp einer Serie nicht verändert. Alternativ kann auch der Python Datentyp None zur Codierung von fehlenden Werten verwendet werden, er wird automatisch in pd.NA konvertiert. Es ist empfehlenswert pd.NA anstatt von np.nan zu benutzen, da es eindeutig ist und die Datentypen nicht verändert.

Aber nicht alle fehlenden Werte werden mit np.nan/pd.NA codiert. Zum Beispiel ist es in den Sozialwissenschaften üblich, fehlende Werte als negative Zahlen zu kennzeichnen. Die World Value Survey codiert alle fehlenden Antworten als -1: weiß nicht; -2: keine Antwort; -3: nicht zutreffend; -4 nicht in der Umfrage gestellt; -5: fehlend, unbekannt. Fehlende Zeichenfolgen können als leere Zeichenfolge, als “N/A” oder auf unzählige andere Arten markiert werden. Andere Umfragen können positive Werte außerhalb des Bereichs verwenden, z.B. kann fehlendes Einkommen als 90000 oder 99999 oder ähnliches codiert sein. Auch Messgeräte nutzen oft numerische Werte, die außerhalb des Messbereiches liegen um fehlende Werte zu codieren, z.B. -9999 bei einer Temperatur. Der erste Schritt, den man hier unternehmen sollte, ist, die Datendokumentation zu konsultieren. Einige Datensätze verfügen über recht umfangreiche Dokumentationen, in denen die Codierung von Werten sehr gut erklärt wird. Aber viele Datensätze haben sehr knappe Dokumentationen, wenn überhaupt, und selbst gut dokumentierte hochwertige Datensätze können Fehler enthalten. Deshalb sollte man immer auf solche Werte achten, da sie bei großen Datensätzen schnell untergehen können und Ergebnisse stark verzerren.

Im Folgenden gehen wir die wichtigsten Tools in Pandas und Numpy durch, die dabei helfen, fehlende Werte zu identifizieren, zu entfernen oder zu ersetzen. Da die dedizierten Tools nur mit np.nan-Codes funktionieren, geben wir auch Beispiele dafür, wie man benutzerdefinierte Codes und Eingabefehler behandeln kann.

Umgang mit fehlenden Werten#

Um den Umgang mit fehlenden Werten zu lernen, schauen wir uns den Wetter Datensatz des DWD für die Wetterstation in Warnemünde vor 1960 an.

import numpy as np # Import von NumPy
import pandas as pd # Import von Pandas
np.random.seed(3) # Fixierung des Zufallszahlgenerators zur beseren Erklärung

wetter = pd.read_csv("../data/Wetter/warnemuende_1960.csv", sep=';')
wetter.shape
(7209, 19)

Die Originaldaten enthalten {glue}:wetter_size1 Zeilen und {glue}:wetter_size2 Spalten.

Als erstes schauen wir uns die Daten mal an. Hierfür bieten sich die *.head() und *.tail() Funktionen, da sie uns den Anfang und das Ende des Datensatzes zeigen.

wetter.head()
STATIONS_ID MESS_DATUM QN_3 FX FM QN_4 RSK RSKF SDK SHK_TAG NM VPM PM TMK UPM TXK TNK TGK eor
0 4271 19470101 -999 -999 -999.0 5 -999.0 -999 -999.0 -999 -999.0 4.1 1019.3 -2.1 78.0 -1.5 -3.3 -5.8 eor
1 4271 19470103 -999 -999 -999.0 3 -999.0 -999 -999.0 -999 -999.0 3.6 1032.4 -4.8 80.0 -2.4 -6.2 -4.5 eor
2 4271 19470105 -999 -999 -999.0 5 -999.0 -999 -999.0 -999 -999.0 1.9 1031.9 -12.9 79.0 -9.4 -14.0 -16.0 eor
3 4271 19470108 -999 -999 -999.0 5 -999.0 -999 -999.0 -999 -999.0 1.9 1023.1 -12.9 82.0 -9.8 -15.3 -19.0 eor
4 4271 19470109 -999 -999 -999.0 5 -999.0 -999 -999.0 -999 -999.0 2.4 1017.5 -9.9 84.0 -8.7 -13.7 -19.0 eor

Als erstes schauen wir uns die Daten mal an. Hierfür bieteten sich die head und tail Funktionen, da sie uns den Anfang und das Ende des Datensatzes zeigen.

wetter.tail()
STATIONS_ID MESS_DATUM QN_3 FX FM QN_4 RSK RSKF SDK SHK_TAG NM VPM PM TMK UPM TXK TNK TGK eor
7204 4271 19660926 5 -999 6.6 5 0.3 1 2.2 0 6.6 12.8 1014.0 13.1 86.0 15.1 9.5 7.9 eor
7205 4271 19660927 5 -999 10.0 5 0.0 1 6.1 0 5.8 10.1 1014.3 12.0 72.0 13.3 10.9 10.4 eor
7206 4271 19660928 5 -999 4.5 5 0.0 0 9.9 0 2.6 9.1 1017.5 9.7 74.0 12.5 7.7 8.7 eor
7207 4271 19660929 5 -999 3.3 5 0.0 0 7.8 0 4.8 9.2 1015.8 9.7 78.0 14.9 5.1 3.1 eor
7208 4271 19660930 5 -999 3.5 5 0.0 0 7.8 0 2.2 10.9 1007.9 11.1 83.0 16.1 5.8 4.8 eor

Es gibt also am Anfang des Datensatzes sehr viele Werte die -999 oder -999.0 sind. Am Ende allerdings nur noch in der Spalte FX. Wir können also davon ausgehen, dass in dem Datensatz fehlende Werte mit -999 codiert worden sind. Die fehlenden Werte sind auch erklärbar, da 1947 Messungen noch händisch durchgeführt wurden und nur einfache Größen wie Temperatur erfasst wurden.

Diese fehlenden Werte müssen wir mit np.nan oder pd.NA ersetzen. Hierfür können wir die Dataframe-Methode replace an.

wetter = wetter.replace(-999, pd.NA)
wetter.head()
STATIONS_ID MESS_DATUM QN_3 FX FM QN_4 RSK RSKF SDK SHK_TAG NM VPM PM TMK UPM TXK TNK TGK eor
0 4271 19470101 <NA> <NA> <NA> 5 <NA> <NA> <NA> <NA> <NA> 4.1 1019.3 -2.1 78.0 -1.5 -3.3 -5.8 eor
1 4271 19470103 <NA> <NA> <NA> 3 <NA> <NA> <NA> <NA> <NA> 3.6 1032.4 -4.8 80.0 -2.4 -6.2 -4.5 eor
2 4271 19470105 <NA> <NA> <NA> 5 <NA> <NA> <NA> <NA> <NA> 1.9 1031.9 -12.9 79.0 -9.4 -14.0 -16.0 eor
3 4271 19470108 <NA> <NA> <NA> 5 <NA> <NA> <NA> <NA> <NA> 1.9 1023.1 -12.9 82.0 -9.8 -15.3 -19.0 eor
4 4271 19470109 <NA> <NA> <NA> 5 <NA> <NA> <NA> <NA> <NA> 2.4 1017.5 -9.9 84.0 -8.7 -13.7 -19.0 eor

Alternativ können wir die Daten direkt aus der CSV mit NA Werten laden, indem wir den Parameter na_values angeben.

wetter = pd.read_csv("../data/Wetter/warnemuende_1960.csv", sep=';', na_values=['-999','NA'])

Jetzt haben wir einen Datensatz, in dem die fehlenden Werte richtig codiert sind. Jetzt ist es sinnvoll zu evaluieren, wie viele Daten fehlen. Wir können mit der Methode count beginnen: Diese Methode gibt die Anzahl der gültigen Beobachtungen für ein Dataframe oder eine einzelne Variable an. Und “gültig” bedeutet hier alles außer np.nan. Die Anzahl der gültigen Beobachtungen für jede Variable ist:

wetter.count()
STATIONS_ID    7209
MESS_DATUM     7209
QN_3           4656
FX                0
FM             4656
QN_4           7209
RSK            5752
RSKF           5752
SDK            5752
SHK_TAG        5752
NM             5752
VPM            7209
PM             7209
TMK            7209
UPM            7209
TXK            7209
TNK            7209
TGK            7209
eor            7209
dtype: int64

Wir können sehen, dass einige Variablen, z.B. TNK und TGK, gültige Werte für alle {glue}:wetter_size1 Fälle haben, während FX keinen einzigen gültigen Werte hat.

Alternativ können wir die Anzahl der fehlenden Werte direkt zählen. Hierfür können wir zwei Methoden kombinieren: isna (oder isnull) gibt für jede Beobachtung an, ob sie fehlt (wahr oder falsch), und sum addiert diese Wahrheitswerte und Falschwerte, wobei Wahrheitswerte in Einsen und Falschwerte in Nullen umgewandelt werden. Auf diese Weise kann dieser Ansatz wie count die Anzahl der fehlenden Werte für jede Variable in einem Dataframe oder nur für eine einzelne Variable in einer Serie produzieren. So können wir die Anzahl der fehlenden Werte wie folgt erhalten:

wetter.isna().sum()
STATIONS_ID       0
MESS_DATUM        0
QN_3           2553
FX             7209
FM             2553
QN_4              0
RSK            1457
RSKF           1457
SDK            1457
SHK_TAG        1457
NM             1457
VPM               0
PM                0
TMK               0
UPM               0
TXK               0
TNK               0
TGK               0
eor               0
dtype: int64

Offensichtlich können wir das gleiche Ergebnis erzielen, indem wir die Anzahl gültigen Werte von der Anzahl der Zeilen abziehen

wetter.shape[0] - wetter.count()

Diese beiden Beispiele zeigen count und isna, die auf Dataframes angewendet werden. Wenn wir nur die Anzahl der fehlenden Werte in einer einzelnen Variablen zählen wollen, können wir einfach diese Variable auswählen. Zum Beispiel auf die Spalte QN_3:

wetter.QN_3.count()
4656

Ein guter Weg, um fehlende Beobachtungen zu entfernen, ist die Verwendung der Methode dropna. Standardmäßig werden alle Zeilen entfernt, die einen fehlenden Wert enthalten.

t = wetter.dropna()
t.shape
(0, 19)

Wir haben keine Beobachtungen mehr! Es gibt keine einzige Zeile, die gültige Werte in allen Zellen hat. Während dieser Fehler hier leicht zu erkennen ist—da nachfolgende Analysen ohne Werte scheitern werden—können solche Probleme insbesondere bei großen Datensätzen und in automatisierten Datenanalyseverfahren zu Fehlern und falschen Ergebnissen führen, die nicht augenscheinlich falsch sind. Stellen Sie sich den Fall vor, dass das Entfernen von fehlenden Werten unseren Datensatz um 90 % verkleinert, weil eine irrelevante Variable größtenteils fehlt. Es sei denn, wir überprüfen die resultierende Anzahl von Zeilen, könnten wir unsere Analyse auf einem völlig falschen Subset abschließen.

Wie die obigen Tabellen nahelegen, ist der Hauptverursacher die Spalte FX die keine einzigen gültigen Wert enthält. In solchen Fällen ist es sinnvoll solche Spalten komplett zu entfernen. Dies können wir auch mit der Methode dropna machen, indem wir sie nur auf Spalten anwenden (axis=1) und diese entfernen, wenn alle Werte darin fehlen (how="all"):

t = wetter.dropna(axis=1, how="all")
t.shape
(7209, 18)

Wenn unsere Analyse nur bestimmte Variablen erfordert, sollten wir nicht alle solchen Zeilen entfernen, sondern nur diejenigen Zeilen entfernen, in denen unsere wichtigen Variablen fehlen. Wollen wir z.B. nur die Temperatur (TMK) und Luftfeuchtigkeit (UPM) analysieren, können wir die fehlenden Zeilen nur in diesen beiden Variablen entfernen, indem wir das Argument subset verwenden:

w = t.dropna(subset=["TMK", "UPM"])
w.shape
(7209, 18)
w.head()
STATIONS_ID MESS_DATUM QN_3 FM QN_4 RSK RSKF SDK SHK_TAG NM VPM PM TMK UPM TXK TNK TGK eor
0 4271 19470101 NaN NaN 5 NaN NaN NaN NaN NaN 4.1 1019.3 -2.1 78.0 -1.5 -3.3 -5.8 eor
1 4271 19470103 NaN NaN 3 NaN NaN NaN NaN NaN 3.6 1032.4 -4.8 80.0 -2.4 -6.2 -4.5 eor
2 4271 19470105 NaN NaN 5 NaN NaN NaN NaN NaN 1.9 1031.9 -12.9 79.0 -9.4 -14.0 -16.0 eor
3 4271 19470108 NaN NaN 5 NaN NaN NaN NaN NaN 1.9 1023.1 -12.9 82.0 -9.8 -15.3 -19.0 eor
4 4271 19470109 NaN NaN 5 NaN NaN NaN NaN NaN 2.4 1017.5 -9.9 84.0 -8.7 -13.7 -19.0 eor

Wenn unsere Analyse nur bestimmte Variablen erfordert, sollten wir nicht alle solchen Zeilen entfernen, sondern nur diejenigen Zeilen entfernen, in denen unsere wichtigen Variablen fehlen. Stellen Sie sich vor, wir möchten die Variablen age und home.dest analysieren. Wir können die fehlenden Zeilen nur in diesen beiden Variablen entfernen, indem wir das Argument subset verwenden:

Falsche Beobachtungen#

Bisher haben wir diskutiert, wie wir mit fehlenden Werten umgehen. Es gibt darüber hinaus weitere Gründe für fehlerhafte und ungewöhnliche Daten, wie Messfehler, Eingabefehler, Einheitenfehler, Skalierungsfehler, Berechnungsfehler, etc. Diese Werte könne Ergebnisse sehr stark verzerren. Deshalb ist es wichtig sie zu identifizieren und zu behandeln. Man unterscheidet die folgenden Arten:

  • Fehlende Werte: Sind Werte bei denen bekannt ist, dass sie fehlen.

  • Außerhalb des Wertebereichs (Out-of-range): Sind Werte die logisch ausschließbar sind. Zum Beispiel weil sie außerhalb des definierten Wertebereichs liegen (z.B. Temperatur < -273.15°C), außerhalb des Messbereichs liegen (z.B. Raumtemperatursensor > 60°C), oder offensichtlich Falsch sind (z.B. 200 Jahre alter Mensch).

  • Ausreißer: Sind Werte, die statistisch deutlich von den restlichen Daten abweichen. Sie können entweder viel größer oder viel kleiner sein als die meisten anderen Werte. Sie sind insbesondere dann ein Problem, wenn sie häufiger auftreten als statistisch erklärbare Werte.

  • Anomalien: Sind Werte, die Kausal erklärbar sind, aber vom erwarteten Verhalten abweichen, z.B. Wetteranomalien.

Diese fehlerhaften oder ungewöhnlichen Werte werden meist unterschiedlich behandelt:

  • Werte außerhalb des Wertebereichs werden meist als fehlende Werte codiert und genauso behandelt, weil sie ja nicht erklärbar auftreten können.

  • Die Behandlung von Ausreißer ist schwieriger, da sie statistisch vorkommen können und somit erklärbar wären. Man behandelt sie meist nur dann, wenn sie statistisch ungewöhnlich häufig auftreten (ein Indiz für Fehler) oder aber z.B. beim Berechnen von stabilen ML-Modellen stören, da sie sehr starke Varianzen erzeugen.

  • Anomalien sind ungewöhnlich, aber erklärbar und häufig ist es ein Ziel von ML-Modellen diese zu identifizieren (Anomalierkennung). Deshalb entfernt man sie nur selten, wenn man anomaliefreie Datensätze braucht, um ML-Modelle zu trainieren die das Normalverhalten vorhersagen (z.B. zur Anomalieerkennung).

Sie können die Ergebnisse statistischer Analysen verzerren und die Modellierung erschweren. Daher ist es wichtig, Ausreißer zu identifizieren und den richtigen Umgang mit ihnen zu wählen.

Der erste Ansatz ist die Wertebereiche der Daten zu prüfen. Hier kann man deskriptive Methoden benutzen, um eventuelle verdächtige Werte zu erkennen. Bei numerischen Werten ist der offensichtlichste Ausgangspunkt, den Wertebereich zu überprüfen. Zum Beispiel - was ist der kleinste und größte Wert in unserem Wetter Datensatz:

Prüfen wir zum Beispiel die Mittlere Tagestemperatur (TMK):

wetter.TMK.min(), wetter.TMK.max()
(-14.8, 27.8)

Die Werte liegen durchaus im realistischen Bereich und erfordern keine Änderung notwendig ist. Schauen wir auf die relative Luftfeuchtigkeit (UPM):

wetter.UPM.min(), wetter.UPM.max()
(42.0, 1000.0)

Dann stellen wir fest, dass diese zwischen 42% und 1000% liegt. Letzteres ist nicht möglich und könnte ein Tippfehler sein. Bei solchen fehlerhaften Werten wissen wir allerdings nicht, was der originale Wert war, deshalb ist es sinnvoll ihn mit pd.NA ersetzen. Hierfür können wir die Funktion mask nutzen:

wetter["UPM"]=wetter.UPM.mask(cond=wetter.UPM > 100, other=pd.NA)

Wenn wir jetzt die Min und Max bestimmen, sehen wir, dass die Werte sich jetzt im Definitionsbereich liegen:

wetter.UPM.min(), wetter.UPM.max()
(42.0, 100.0)

Wie wir mit Ausreißern und Anomalien umgehen, werden wir später behandeln.

Fehlende Werte Ersetzen#

Manchmal möchten man fehlenden Werte durch neuen Wert ersetzen. Hier unterscheidet man unterschiedliche Fälle:

  • Fehlende Werte mit sinnvollen Standartwerten: Es gibt Datensätze, wo man durchaus sinnvolle Standartwerte annehmen kann für fehlende Werte. Zum Beispiel, zählen wir die Unfälle auf Baustellen pro Tag. Da diese nur registriert werden, wenn welche auftreten, ist für die anderen Tage anzunehmen (aber nicht garantiert), dass keine aufgetreten sind.

    Man könnte zum Beispiel annehmen, dass die Schneetiefe in unserem Wetterdatensatz 0 ist (Das wäre allerdings falsch), wenn sie nicht angegeben ist. In diesem Fall können wir die fehlenden Werte pd.NA mit fillna ersetzen:

wetter.SHK_TAG.fillna(value=0)
0       0.0
1       0.0
2       0.0
3       0.0
4       0.0
       ... 
7204    0.0
7205    0.0
7206    0.0
7207    0.0
7208    0.0
Name: SHK_TAG, Length: 7209, dtype: float64

Wenige Fehlende Werte Ersetzen#

Bei Zeitreihen, wie den Wetterdaten, kann man fehlende Werte zwischendurch ersetzen, wenn man davon ausgehen kann, dass sie sich nicht schnell ändern. Dazu nutzt man Methoden der Interpolation. Pandas unterstützt

  • ffill - Ersetzt NA/NaN-Werte, indem Sie die letzte gültige Beobachtung verwenden, um die Lücke zu füllen.

  • bfill - Ersetzt NA/NaN-Werte, indem Sie die nächste gültige Beobachtung nutzt, um die Lücke zu füllen.

  • interpolate - Ersetzt NA/NaN-Werte mit einer Interpolationsmethode, die mit dem Parameter method angegeben wird:

    • linear - Interpoliert linear zwischen dem letzten und dem nächsten Wert.

    • nearest - Wählt entweder den letzten und dem nächsten Wert, je nachdem welcher zeitlich näher liegt.

    • polynomial - Interpoliert zwischen dem letzten und dem nächsten Wert mit einem Polynom der agebenen Ordnung (z.B. order=5).

    • cubic - Interpoliert zwischen dem letzten und dem nächsten Wert mit einem cubischen Polynom (das Überschwingen vermeidet).

Bei allen diesen Methoden ist empfohlen den Parameter limit zu nutzen. Er bestimmt wie viele nacheinander folgende Werte ersetzt werden dürfen (andere bleiben NA). So mag es akzeptabel sein, ein einzelner Tag zu interpolieren, allerdings nicht eine ganze Woche, da der anzunehmende Interpolationsfehler zu groß wird.

Schauen wir uns die mittlere Temperatur an:

wetter.TMK[:10]
0    -2.1
1    -4.8
2   -12.9
3   -12.9
4    -9.9
5    -9.1
6   -10.8
7    -9.3
8    -1.1
9     2.6
Name: TMK, dtype: float64

Hier fehlen mehrere Werte. Wir können diese interpolieren mit:

wetter["TMK"]=wetter.TMK.interpolate(method="cubic", limit=1)
wetter.TMK[:10]
0    -2.1
1    -4.8
2   -12.9
3   -12.9
4    -9.9
5    -9.1
6   -10.8
7    -9.3
8    -1.1
9     2.6
Name: TMK, dtype: float64

Fehlende Werte Am Anfang und Ende#

Fehlende Werte am Anfang oder Ende einer Zeitreihe kann diese nicht Interpolieren, da Information über Vorgängerwerte oder Nachfolgerwerte fehlen. In solchen Fällen spricht man von Extrapolation. Prinzipiell kann man hier die Funktionen ffill und bfill verwenden. Da halt allerdings Randwertinformationen fehlen ist der anzunehmende Extrapolationsfehler meist groß, weshalb unbedingt limit zu verwenden ist.

wetter["RSK"]=wetter.RSK.ffill(limit=1)

Fehlende Datumswerte hinzufügen#

Gerade bei der Analyse von Zeitreihen ist es oft wichtig das Messungen in festem Abstand (Equidistant) aufgenommen werden. Um das zu prüfen müssen wir als erstes die Datumsangabe in unserm Wetterdatensatz in einen nutzbaren Datentyp für Zeiten umwandeln. Dies können wir mit der Funktion pd.to_datetime umwandeln. Dafür müssen wir die originale Datumsspalte MESS_DATUM in einen String umwandeln, der von to_datetime verwendet werden kann und ein String der das format repräsentiert (siehe Umgang mit Zeitstempeln):

wetter["DATUM_DT"] = pd.to_datetime(wetter.MESS_DATUM.astype(str), format="%Y%m%d")

Als nächstes empfiehlt es sich den Datensatz nach der Zeit zu sortieren:

wetter.sort_values(by="DATUM_DT", inplace=True) # Sortier nach der Spalte "MESS_DATUM" und ersetzt den Dataframe direkt (inplace)

Dann können wir prüfen, mit welcher Häufigkeit, welche zeitlichen Differenzen auftreten. Wir können dafür die diff Funktion nutzen, um die Zeitdifferenz zu bestimmen. Diese verketten wir mit value_counts. um die Häufigkeit der Differenzen zu berechnen.

wetter.DATUM_DT.diff().value_counts()
DATUM_DT
1 days    7205
2 days       2
3 days       1
Name: count, dtype: int64

Wir sehen, dass in unserem Datensatz mehre Messwerte fehlen, da Differenz von mehr als 1 Tag vorkommen.

Um diese Daten wiederherzustellen können wir die Funktion resample benutzen. Dafür müssen wir zuerst die Datumsspalte zum Index des Dataframes machen, so dass eine richtige mehrdimensionale Zeitreihe draus wird.

wetter.set_index("DATUM_DT", inplace=True)

Jetzt können wir mit resample den Dataframe in eine regelmäßige tägliche Auflösung bringen. Dabei müssen wir eine Funktion zur Interpolation der fehlenden Werte mit angeben.

wetter1D=wetter.resample('1d').ffill(limit=1)

Nun können wir den Index wieder zu eine Spalte machen

wetter1D.reset_index(inplace=True)

Der neue Dataframe enthält jetzt keine fehlenden Werte

wetter1D.DATUM_DT.diff().value_counts()
DATUM_DT
1 days    7212
Name: count, dtype: int64

Fehlende Werte und mathematische Operationen#

Die Methoden .sum und .mean von Pandas ignorieren fehlende Werte. Berechnen wir den Mittelwert der täglich Sonnenscheindauer (SDK):

wetter1D.SDK.mean()
4.904363699582754

Sie liegt bei 4.9 Studen.

Wenn wir jedoch den Mittelwert entsprechend der Formel für den arithmetischen Mittelwert

\[ \bar{x} = \frac{1}{N} \sum_{i=1}^{N} x_i. \]

selbst berechnen

wetter1D.SDK.sum()/len(wetter1D.SDK)
3.9109801746845974

Dann ergibt sich ein komplett anderes Ergebnis. Dies liegt daran, dass len fehlende Werte nicht ignoriert und daher die Summe durch die Gesamtzahl der Fälle teilen, inklusive der fehlenden Fälle.

In dem Fall wäre die Berechnung mit count richtig, da dann NA Werte ignoriert werden.

wetter1D.SDK.sum()/wetter1D.SDK.count()
4.904363699582754

Wenn wir jedoch einen fehlenden Wert mit einer Zahl vergleichen, ergibt sich False und nicht ein fehlender Wert pd.NA, was formal richtig wäre, da für die fehlenden Werte ja keine Aussage getroffen werden kann. Das kann sehr schnell zu falschen Ergebnissen führen. Wollen wir zum Beispiel die Anzahl der Sonnentage mit mehr als 8 Stunden Sonnenschein bestimmen, so ist es intuitiv zu berechnen:

sonnentage = wetter1D.SDK < 8
sonnentage.value_counts(dropna=False)
SDK
True     4180
False    3033
Name: count, dtype: int64

Das ist allerdings Falsch. Wir haben die Informationen über die fehlenden Werte im Prozess verloren und Tage mit fehlenden Werten automatisch zu keinem Sonnentag gemacht. Die richtige Berechnung kann man mit der Funktion map bestimmen, da mit dem Parameter na_action='ignore' enthaltene NA Werte belassen werden:

sonnentage = wetter1D.SDK.map(lambda x: x < 8, na_action='ignore')
sonnentage.value_counts(dropna=False)
SDK
True     4180
False    1572
NaN      1461
Name: count, dtype: int64

Dies unterstreicht die Bedeutung der Behandlung von fehlenden Werten in Daten und das man sich mit dem Umgang der Standardfunktionalität von Pandas mit NA vertraut machen sollte.

Konvertierung von Variablen#

Konvertierung von kategorischen Variablen#

Manchmal stoßen wir auf Situationen, in denen die in den Daten bereitgestellten Kategorien nicht gut zu den Anforderungen unserer Analyse passen. Zwei häufige Fälle sind, wenn die Originaldaten zu granularen Informationen enthalten (zum Beispiel, wenn wir an ein paar breiten Wirtschaftssektoren interessiert sind, aber die Daten 27 Branchen auflisten) oder Kategorien zu ausführlich sind (zum Beispiel kann der Beruf als “Handwerker, Vorarbeiter und ähnliche” aufgeführt sein, während wir lieber den kürzeren Begriff “qualifiziert” verwenden würden). In diesen Fällen müssen wir die ursprünglichen Kategorien in neue umwandeln und möglicherweise mehrere ursprüngliche Kategorien in eine einzige neue Kategorie zusammenführen.

In diesem Fall gibt es mehrere Möglichkeiten, wie man vorgehen kann. Wir zeigen die Optionen anhand des Falls, in dem die Daten numerisch codiert sind, aber nicht alle Codes echte numerische Werte darstellen.

Verwenden von pd.cut#

pd.cut ist eine dedizierte Funktion von Pandas, um kontinuierliche numerische Daten in vordefinierte Intervalle zu schneiden. Es wird wie folgt aufgerufen:

pd.cut(x, bins, right=True, labels=None)

wo x die Variable ist, die geschnitten werden soll, bins sind die Intervallgrenzen, labels sind die Intervallnamen und right gibt an, ob der rechte Grenzwert im Intervall enthalten ist (right = True bedeutet, dass die Grenzen rechts offen sind). Es ist hauptsächlich für das Aufteilen von kontinuierlichen Werten gedacht, kann aber auch leicht für diskrete Werte verwendet werden. Lassen Sie uns diese Daten in gewünschte Intervalle aufteilen:

wetter1D["TemperaturKlasse"]=pd.cut(wetter1D.TMK,
        bins = [-np.inf, 13, 18, 24, 30, np.inf],
        labels = ["Cold", "Cool", "Mild", "Warm", "Hot"],
        right=False)
wetter1D["TemperaturKlasse"].value_counts(dropna=False)
TemperaturKlasse
Cold    4861
Cool    1836
Mild     503
Warm      12
NaN        1
Hot        0
Name: count, dtype: int64

Wir setzen die Grenzen bei 13, 18, 24 und 30 °C und halten die Ränder offen, indem wir die erste und letzte Grenze auf Unendlich setzen. Wir geben den Intervallen explizite Labels, aufgrund der begrenzten Funktionalität von np.nan (es gibt keine fehlenden Werte vom Typ String), fügen wir explizit den fehlenden Wert “NA” in den String ein. Schließlich sagen wir cut, dass der rechte Grenzwert aus dem Intervall ausgeschlossen werden soll. Andernfalls würde der Wert 0 in die Kategorie “NA”, der Wert 1 in “0” usw. fallen.

Verwendung von np.where#

Eine weitere Option ist die Verwendung von np.where, einer vektorisierten Version einer if-else-Anweisung. np.where nimmt drei Argumente entgegen: eine vektorisierte logische Bedingung, den Wert, wenn die Bedingung wahr ist, und den Wert, wenn sie falsch ist. Wir können die Funktion verschachteln, um alternative Bedingungen zu prüfen. Zum Beispiel wollen wir die Heizgradtage bestimmen (Mittlere Temperatur < 15°C) und die Kühlgradtage (Mittlere Temperatur > 22°C).

wetter1D["HeizKuehlTage"] = np.where(wetter1D.TMK > 22, "Kühlgradtag", np.where(wetter1D.TMK < 15, "Heizgradtag", "Normaltag"))
wetter1D["HeizKuehlTage"].value_counts(dropna=False)
HeizKuehlTage
Heizgradtag    5611
Normaltag      1559
Kühlgradtag      43
Name: count, dtype: int64

Es ist zu beachten, dass die logische Bedingung in np.where wieder NA Werte auf False mapt. Deshalb sollte man das wieder gezielt behandeln:

wetter1D["HeizKuehlTage"] = np.where(wetter1D.TMK.isna(), pd.NA, np.where(wetter1D.TMK > 22, "Kühlgradtag", np.where(wetter1D.TMK < 15, "Heizgradtag", "Normaltag")))
wetter1D["HeizKuehlTage"].value_counts(dropna=False)
HeizKuehlTage
Heizgradtag    5611
Normaltag      1558
Kühlgradtag      43
<NA>              1
Name: count, dtype: int64

Mit der map Funktion lässt sich das auch realisieren:

wetter1D["HeizKuehlTage"] = wetter1D.TMK.map(lambda x: "Heizgradtag" if x < 15 else "Kühlgradtag" if x > 22 else "Normaltag", na_action='ignore')

Ersetzen ausgewählter Werte im Dataframe#

Häufig ist es notwendig bestimmte Werte gezielt zu ersetzen, auf Basis einer logischen Bedingung zu modifizieren. Dies kann man indem man den .loc-Index eines Dataframes nutzt. Das kann man auch nutzen, um neue Spalten zu berechnen. Wir erstellen eine neue leere Spalte und passen sie anschließend nach unseren Bedürfnissen an. Beachten Sie, dass dies einen gemischten Indexierungsansatz erfordert. Wir werden die Zeilen anhand eines logischen Vektors (logische Bedingung) und die Spalten anhand ihres Namens indizieren. Die Spalte QN_3 im Datensatz zum Beispiel codiert die Datenqualität, mit den folgenden kategorischen Werten.

wetter1D["QNS_4"] = pd.NA
wetter1D.loc[wetter1D.QN_4 ==  1, "QNS_4"] = "nur formale Prüfung"
wetter1D.loc[wetter1D.QN_4 ==  2, "QNS_4"] = "nach individuellen Kriterien geprüft"
wetter1D.loc[wetter1D.QN_4 ==  3, "QNS_4"] = "automatische Prüfung und Korrektur"
wetter1D.loc[wetter1D.QN_4 ==  5, "QNS_4"] = "historische, subjektive Verfahren"
wetter1D.loc[wetter1D.QN_4 ==  7, "QNS_4"] = "geprüft, gepflegt, nicht korrigiert"
wetter1D.loc[wetter1D.QN_4 ==  8, "QNS_4"] = "Qualitätsicherung ausserhalb ROUTINE"
wetter1D.loc[wetter1D.QN_4 ==  9, "QNS_4"] = "nicht alle Parameter korrigiert"
wetter1D.loc[wetter1D.QN_4 == 10, "QNS_4"] = "Qualitätsprüfung und Korrektur beendet"

wetter1D["QNS_4"].value_counts(dropna=False)
QNS_4
historische, subjektive Verfahren       7202
automatische Prüfung und Korrektur         3
nur formale Prüfung                        2
nach individuellen Kriterien geprüft       2
Qualitätsicherung ausserhalb ROUTINE       2
<NA>                                       1
nicht alle Parameter korrigiert            1
Name: count, dtype: int64

Für solche komplexeren Mappings kann man auch die map Funktion verwenden und in dieser eine speziell definierte Mappingfunktion.

def QN_Mapping(x):
    if x == 1: return "nur formale Prüfung"
    if x == 2: return "nach individuellen Kriterien geprüft"
    if x == 3: return "automatische Prüfung und Korrektur"
    if x == 5: return "historische, subjektive Verfahren"
    if x == 7: return "geprüft, gepflegt, nicht korrigiert"
    if x == 8: return "Qualitätsicherung ausserhalb ROUTINE"
    if x == 9: return "nicht alle Parameter korrigiert"
    if x == 10: return "Qualitätsprüfung und Korrektur beendet"
    return pd.NA

wetter1D["QNS_4"] = wetter1D.QN_4.map(QN_Mapping, na_action='ignore')

Konvertierung kategorischer Variablen in Dummy-Variablen#

Im vorherigen Abschnitt haben wir uns damit beschäftigt, Variablen in Kategorien umzuwandeln. In vielen Fällen (z.B. beim Einfügen kategorischer Variablen in statistische Modelle) müssen wir jedoch das Gegenteil tun - Kategorien in Vektoren von Zahlen umwandeln.

Dies kann durch zwei Ansätze erreicht werden, abhängig davon ob die Modelle nur boolesche Variablen unterstützen (z.B. Logistische Modelle) oder numerische Variablen (z.B. Regressionsmodelle). Um Kategorien in boolesche Variablen umzuwandeln, sogenannte Dummies, kann man pd.get_dummies verwenden. Dabei werden die kategorische Werte in einen Dataframe von Dummies umgewandelt, bei dem die Spaltennamen den Namen der Kategorien entsprechen. Beachten Sie auch, dass es den originalen Index beibehält, die erste Spalte in der Beispieltabelle. Man kann bei Bedarf einen Namenspräfix für die Spalten angeben:

QN_Dummies = pd.get_dummies(wetter1D.QNS_4, prefix="QN")
QN_Dummies
QN_Qualitätsicherung ausserhalb ROUTINE QN_automatische Prüfung und Korrektur QN_historische, subjektive Verfahren QN_nach individuellen Kriterien geprüft QN_nicht alle Parameter korrigiert QN_nur formale Prüfung
0 False False True False False False
1 False False True False False False
2 False True False False False False
3 False True False False False False
4 False False True False False False
... ... ... ... ... ... ...
7208 False False True False False False
7209 False False True False False False
7210 False False True False False False
7211 False False True False False False
7212 False False True False False False

7213 rows × 6 columns

Um Kategorien in numerische Werte umzuwandeln gibt es die Funktion pd.factorize. Hier wird eine numerische Serie erstellt der die Kategorien als Index angehängt werden, ähnlich der originalen numerischen Kodierung.

QN_Factors_Values, QN_Factors_Mapping = pd.factorize(wetter1D.QNS_4)
QN_Factors_Values, QN_Factors_Mapping
(array([0, 0, 1, ..., 0, 0, 0]),
 Index(['historische, subjektive Verfahren',
        'automatische Prüfung und Korrektur', 'nur formale Prüfung',
        'nach individuellen Kriterien geprüft',
        'Qualitätsicherung ausserhalb ROUTINE',
        'nicht alle Parameter korrigiert'],
       dtype='object'))

Zu beachten ist, dass die Funktion ein Tupel mit den numerischen Werten und dem Mapping zurück gibt. Wir können die Werte als neue Spalte dem Dataframe hinzufügen

wetter1D["QNF_4"] = QN_Factors_Values

Kombinieren von Daten in Dataframes#

Eine häufige Aufgabe besteht darin, einen Dataframe aus Serien zu erstellen, neue Spalten zu einem Dataframe hinzuzufügen und Dataframes zu einem breiteren Dataframe zu kombinieren. Einige der Aufgaben können entweder durch Erstellen einer neuen Variablen oder alternativ durch Erstellen eines Dataframes aus vorhandenen Serien erreicht werden. Dieser Ansatz scheitert jedoch, wenn Sie Dataframes anstelle von einzelnen Serien kombinieren möchten. Im Folgenden diskutieren wir pd.concat, ein Tool, das explizit für diese Aufgabe entwickelt wurde.

Verknüpfen von Daten mit pd.concat#

pd.concat kann Serien und Dataframes auf verschiedene Weise kombinieren. Lassen Sie uns dies anhand einiger Beispiele demonstrieren.

So wollen wir zum Beispiel unseren originalen Wetter Dataframe, um die erstellten Spalten im QN_Dummies erweitern.

wetter_ext = pd.concat((wetter1D, QN_Dummies), axis=1)
wetter_ext
DATUM_DT STATIONS_ID MESS_DATUM QN_3 FX FM QN_4 RSK RSKF SDK ... TemperaturKlasse HeizKuehlTage QNS_4 QNF_4 QN_Qualitätsicherung ausserhalb ROUTINE QN_automatische Prüfung und Korrektur QN_historische, subjektive Verfahren QN_nach individuellen Kriterien geprüft QN_nicht alle Parameter korrigiert QN_nur formale Prüfung
0 1947-01-01 4271.0 19470101.0 NaN NaN NaN 5.0 NaN NaN NaN ... Cold Heizgradtag historische, subjektive Verfahren 0 False False True False False False
1 1947-01-02 4271.0 19470101.0 NaN NaN NaN 5.0 NaN NaN NaN ... Cold Heizgradtag historische, subjektive Verfahren 0 False False True False False False
2 1947-01-03 4271.0 19470103.0 NaN NaN NaN 3.0 NaN NaN NaN ... Cold Heizgradtag automatische Prüfung und Korrektur 1 False True False False False False
3 1947-01-04 4271.0 19470103.0 NaN NaN NaN 3.0 NaN NaN NaN ... Cold Heizgradtag automatische Prüfung und Korrektur 1 False True False False False False
4 1947-01-05 4271.0 19470105.0 NaN NaN NaN 5.0 NaN NaN NaN ... Cold Heizgradtag historische, subjektive Verfahren 0 False False True False False False
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
7208 1966-09-26 4271.0 19660926.0 5.0 NaN 6.6 5.0 0.3 1.0 2.2 ... Cool Heizgradtag historische, subjektive Verfahren 0 False False True False False False
7209 1966-09-27 4271.0 19660927.0 5.0 NaN 10.0 5.0 0.0 1.0 6.1 ... Cold Heizgradtag historische, subjektive Verfahren 0 False False True False False False
7210 1966-09-28 4271.0 19660928.0 5.0 NaN 4.5 5.0 0.0 0.0 9.9 ... Cold Heizgradtag historische, subjektive Verfahren 0 False False True False False False
7211 1966-09-29 4271.0 19660929.0 5.0 NaN 3.3 5.0 0.0 0.0 7.8 ... Cold Heizgradtag historische, subjektive Verfahren 0 False False True False False False
7212 1966-09-30 4271.0 19660930.0 5.0 NaN 3.5 5.0 0.0 0.0 7.8 ... Cold Heizgradtag historische, subjektive Verfahren 0 False False True False False False

7213 rows × 30 columns

Dieses Beispiel zeigt die wichtigsten Argumente von pd.concat. Das erste Argument ist ein beliebig langes Tuple von Daten (Serien oder Dataframes), die wir zusammenführen möchten (alternativ kann auch eine Liste verwendet werden). Wir setzen den Parameter axis=1, um anzugeben, dass wir die Serien spaltenweise zusammenführen möchten, nicht zeilenweise.

Wenn die Dataframes kompatibel sind, können wir sie auch zeilenweise kombinieren, indem wir axis=0 angeben.

Zum Beispiel wollen wir unseren Wetterdatensatz um die Werte nach 1960 erweitern.

wetter_ab_1960 = pd.read_csv("../data/Wetter/warnemuende_ab_1960.csv", sep=';', na_values=['-999','NA'])
wetter_ab_1960["DATUM_DT"] = pd.to_datetime(wetter_ab_1960.MESS_DATUM.astype(str), format="%Y%m%d")
wetter_ab_1960
STATIONS_ID MESS_DATUM QN_3 FX FM QN_4 RSK RSKF SDK SHK_TAG NM VPM PM TMK UPM TXK TNK TGK eor DATUM_DT
0 4271 19600101 5.0 NaN 4.1 5 1.4 1.0 0.000 0.0 7.8 8.9 1003.40 7.0 90.00 7.9 4.9 4.1 eor 1960-01-01
1 4271 19600102 5.0 NaN 2.4 5 1.4 1.0 0.000 0.0 8.0 9.7 1004.80 6.8 97.00 8.0 6.3 5.2 eor 1960-01-02
2 4271 19600103 5.0 NaN 1.7 5 0.1 1.0 0.000 0.0 7.8 8.8 1014.20 5.2 99.00 6.7 4.8 4.0 eor 1960-01-03
3 4271 19600104 5.0 NaN 2.5 5 0.0 1.0 0.000 0.0 8.0 8.3 1027.10 4.3 98.00 5.3 3.3 3.4 eor 1960-01-04
4 4271 19600105 5.0 NaN 7.1 5 1.6 1.0 0.000 0.0 7.8 9.1 1016.00 5.9 95.00 9.3 3.8 3.5 eor 1960-01-05
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
23439 4271 20240304 1.0 11.0 3.1 1 0.0 0.0 8.667 0.0 3.9 8.3 1010.80 7.1 81.54 12.0 3.1 1.8 eor 2024-03-04
23440 4271 20240305 1.0 10.2 3.5 1 0.0 6.0 1.183 0.0 5.7 6.6 1017.63 3.7 82.50 6.0 2.1 0.7 eor 2024-03-05
23441 4271 20240306 1.0 8.1 2.7 1 0.0 0.0 1.383 0.0 7.7 7.1 1024.36 4.2 85.96 6.2 2.8 2.4 eor 2024-03-06
23442 4271 20240307 1.0 7.4 2.7 1 0.0 0.0 7.783 0.0 3.1 6.1 1026.72 4.4 74.04 9.5 0.6 -1.1 eor 2024-03-07
23443 4271 20240308 1.0 8.2 2.7 1 0.0 0.0 5.667 0.0 2.8 6.1 1020.17 2.5 84.29 8.9 -0.4 -3.1 eor 2024-03-08

23444 rows × 20 columns

Es ist wichtig zu erkennen, dass beim horizontalen Verketten von Daten pd.concat die Beobachtungen nicht zeilenweise, sondern nach Index kombiniert. Wenn wir beispielsweise versuchen beide Dataframes direkt zu kombinieren so wird das nicht gut ausgehen:

wetter_all=pd.concat((wetter1D, wetter_ab_1960), axis=0)
wetter_all
DATUM_DT STATIONS_ID MESS_DATUM QN_3 FX FM QN_4 RSK RSKF SDK ... TMK UPM TXK TNK TGK eor TemperaturKlasse HeizKuehlTage QNS_4 QNF_4
0 1947-01-01 4271.0 19470101.0 NaN NaN NaN 5.0 NaN NaN NaN ... -2.1 78.00 -1.5 -3.3 -5.8 eor Cold Heizgradtag historische, subjektive Verfahren 0.0
1 1947-01-02 4271.0 19470101.0 NaN NaN NaN 5.0 NaN NaN NaN ... -2.1 78.00 -1.5 -3.3 -5.8 eor Cold Heizgradtag historische, subjektive Verfahren 0.0
2 1947-01-03 4271.0 19470103.0 NaN NaN NaN 3.0 NaN NaN NaN ... -4.8 80.00 -2.4 -6.2 -4.5 eor Cold Heizgradtag automatische Prüfung und Korrektur 1.0
3 1947-01-04 4271.0 19470103.0 NaN NaN NaN 3.0 NaN NaN NaN ... -4.8 80.00 -2.4 -6.2 -4.5 eor Cold Heizgradtag automatische Prüfung und Korrektur 1.0
4 1947-01-05 4271.0 19470105.0 NaN NaN NaN 5.0 NaN NaN NaN ... -12.9 79.00 -9.4 -14.0 -16.0 eor Cold Heizgradtag historische, subjektive Verfahren 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
23439 2024-03-04 4271.0 20240304.0 1.0 11.0 3.1 1.0 0.0 0.0 8.667 ... 7.1 81.54 12.0 3.1 1.8 eor NaN NaN NaN NaN
23440 2024-03-05 4271.0 20240305.0 1.0 10.2 3.5 1.0 0.0 6.0 1.183 ... 3.7 82.50 6.0 2.1 0.7 eor NaN NaN NaN NaN
23441 2024-03-06 4271.0 20240306.0 1.0 8.1 2.7 1.0 0.0 0.0 1.383 ... 4.2 85.96 6.2 2.8 2.4 eor NaN NaN NaN NaN
23442 2024-03-07 4271.0 20240307.0 1.0 7.4 2.7 1.0 0.0 0.0 7.783 ... 4.4 74.04 9.5 0.6 -1.1 eor NaN NaN NaN NaN
23443 2024-03-08 4271.0 20240308.0 1.0 8.2 2.7 1.0 0.0 0.0 5.667 ... 2.5 84.29 8.9 -0.4 -3.1 eor NaN NaN NaN NaN

30657 rows × 24 columns

Obwohl beide Dataframes eine unterschiedliche Anzahl an Spalten haben (Es fehlen bei wetter_ab_1960 die Spalten, die wir hinzugefügt haben), kann concat beide Dataframes kombinieren. Hierbei werden fehlende Spalten mit np.NaN aufgefüllt. Deshalb ist es sinnvoll erst zusammenzufügen, bevor man abgeleitete Werte berechnet. Dies können wir korrigieren, indem wir die fehlenden Werte neu berechnen.

wetter_all["TemperaturKlasse"]=pd.cut(wetter_all.TMK, bins = [-np.inf, 13, 18, 24, 30, np.inf], labels = ["Cold", "Cool", "Mild", "Warm", "Hot"], right=False)
wetter_all["HeizKuehlTage"] = wetter_all.TMK.map(lambda x: "Heizgradtag" if x < 15 else "Kühlgradtag" if x > 22 else "Normaltag", na_action='ignore')
wetter_all["QNS_4"]    = wetter_all.QN_4.map(QN_Mapping, na_action='ignore')
wetter_all["QNF_4"], _ = pd.factorize(wetter_all.QNS_4)

Dataframe kombinieren#

Meist will man nicht nur Daten kombinieren, die nicht gleich groß sind oder den gleichen Index haben. Zum Beispiel wollen wir die Wetterdaten mit den Energiedaten der Universität Rostock kombinieren, um diese später zu analysieren.

uros_egy=pd.read_csv("../data/UROS/Energy1D_kW.csv", parse_dates=["Date"])
uros_egy.head()
Date EV_HT_740 EV_NT_740 E_AV_Lab E_SV_Lab ES_Lab
0 2020-12-30 00:00:00+00:00 NaN NaN NaN NaN NaN
1 2020-12-31 00:00:00+00:00 NaN NaN 1256.0 291.0 5.0
2 2021-01-01 00:00:00+00:00 0.0 4080.0 1221.0 290.0 1.0
3 2021-01-02 00:00:00+00:00 1170.0 2630.0 1243.0 284.0 2.0
4 2021-01-03 00:00:00+00:00 0.0 3750.0 1222.0 283.0 2.0

Die Daten können wir mit der Methode pd.merge() kombinieren, die wie ein SQL Join funktioniert. Hierbei können wir die Spalten die gematcht werden sollen mit left_on und right_on spezifizieren (oder on wenn die Spalten gleich sind). Mit how="left" spezifizieren wir, dass nur die Zeilen genommen werden sollen, die im Energiedatensatz vorhanden sind.

uros_egy_weater=pd.merge(uros_egy, wetter_all, right_on="DATUM_DT", left_on="Date", how="left")
uros_egy_weater.head()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[50], line 1
----> 1 uros_egy_weater=pd.merge(uros_egy, wetter_all, right_on="DATUM_DT", left_on="Date", how="left")
      2 uros_egy_weater.head()

File ~/miniconda3/envs/lehre/lib/python3.11/site-packages/pandas/core/reshape/merge.py:170, in merge(left, right, how, on, left_on, right_on, left_index, right_index, sort, suffixes, copy, indicator, validate)
    155     return _cross_merge(
    156         left_df,
    157         right_df,
   (...)
    167         copy=copy,
    168     )
    169 else:
--> 170     op = _MergeOperation(
    171         left_df,
    172         right_df,
    173         how=how,
    174         on=on,
    175         left_on=left_on,
    176         right_on=right_on,
    177         left_index=left_index,
    178         right_index=right_index,
    179         sort=sort,
    180         suffixes=suffixes,
    181         indicator=indicator,
    182         validate=validate,
    183     )
    184     return op.get_result(copy=copy)

File ~/miniconda3/envs/lehre/lib/python3.11/site-packages/pandas/core/reshape/merge.py:807, in _MergeOperation.__init__(self, left, right, how, on, left_on, right_on, left_index, right_index, sort, suffixes, indicator, validate)
    803 self._validate_tolerance(self.left_join_keys)
    805 # validate the merge keys dtypes. We may need to coerce
    806 # to avoid incompatible dtypes
--> 807 self._maybe_coerce_merge_keys()
    809 # If argument passed to validate,
    810 # check if columns specified as unique
    811 # are in fact unique.
    812 if validate is not None:

File ~/miniconda3/envs/lehre/lib/python3.11/site-packages/pandas/core/reshape/merge.py:1518, in _MergeOperation._maybe_coerce_merge_keys(self)
   1514     raise ValueError(msg)
   1515 elif isinstance(lk.dtype, DatetimeTZDtype) and not isinstance(
   1516     rk.dtype, DatetimeTZDtype
   1517 ):
-> 1518     raise ValueError(msg)
   1519 elif not isinstance(lk.dtype, DatetimeTZDtype) and isinstance(
   1520     rk.dtype, DatetimeTZDtype
   1521 ):
   1522     raise ValueError(msg)

ValueError: You are trying to merge on datetime64[ns, UTC] and datetime64[ns] columns for key 'Date'. If you wish to proceed you should use pd.concat

Duplikate entfernen#

Nachdem wir beide Datensätze zusammengefügt haben, ergibt sich das neue Problem, duplizierter Daten. Das kann in Datensätzen vorkommen, so dass es immer sinnvoll ist, auf Duplikate zu prüfen. Hierfür gibt es die Funktion duplicated. Sie gibt einen boolesche Serie zurück, die Wahr ist für Zeilen, wo alle Werte doppelt vorkommen.

sum(wetter_all.duplicated())
2465

Das ist nur sinvoll, wenn sich Zeilen vollständig wiederholen. In unserm Fall ist das Datum, der eigentliche Index und es ist empfohlen duplicated auf diesen anzuwenden. Wenn wir unseren zusammengefügte Dataframe prüfen, so sehen wir, dass wir einige Duplikate haben, d.h. Daten für die mehr als eine Messung existiert.

sum(wetter_all.duplicated(subset=['DATUM_DT']))
2465

Um diese herauszufiltern können wir die Funktion drop_duplicates nutzen, die ähnliche Parameter verwendet.

wetter_all.drop_duplicates(subset=['DATUM_DT'], inplace=True)
wetter_all.shape
(28192, 24)

Damit haben wir jetzt einen gereinigten Dataframe, den wir uns für die weitere Verwendung abspeichern.

wetter_all.to_csv("../data/Wetter/warnemuende_clean.csv", sep=';', index=False)
uros_egy_weater.to_csv("../data/UROS/Energy1D_weather_clean.csv", index=False)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[54], line 2
      1 wetter_all.to_csv("../data/Wetter/warnemuende_clean.csv", sep=';', index=False)
----> 2 uros_egy_weater.to_csv("../data/UROS/Energy1D_weather_clean.csv", index=False)

NameError: name 'uros_egy_weater' is not defined

Dataframes Transformieren#

Bisher haben wir mit einem Datensatz in Spaltendarstellung gearbeitet. Das ist für uns Menschen eine gewohnte tabellarische Darstellung. Für viele Berechnungen und Visualisierungen, ist das allerdings nicht immer das beste Format. Da braucht man häufig die Daten in einer Zeilendarstellung.

Dataframe Format in Spalten und Zeilendarstellung

Nehmen wir als Beispiel die Temperaturwerte und Klassen aus unserem Datensatz. Wir wollen jetzt den Zusammenhang zwischen der Temperaturklasse und der unteren TNK, mittleren TMK und oberen Temperatur TXK bestimmen. Wir könnten das jetzt für jede Spalte einzeln machen, aber es wäre praktischer, wenn wir statt dessen nur eine Spalte mit Werten hätten. Das könnte man erreichen indem man jede Spalte raus löst und dann mit pd.concat in einem neuen Dataframe zusammenführt. Oder man spart sich den Aufwand und nutzt die Methode melt. Dieser übergeben wir zwei Parameter. Der Parameter id_vars enthält eine Liste der Indexwerte, die wir behalten wollen und value_vars spezifiziert alle Spalten die wir in eine Reihendarstellung überführen wollen.

wetter_melted = wetter_all.melt(id_vars=['DATUM_DT', "TemperaturKlasse"], value_vars=["TXK","TMK","TNK"])
wetter_melted
DATUM_DT TemperaturKlasse variable value
0 1947-01-01 Cold TXK -1.5
1 1947-01-02 Cold TXK -1.5
2 1947-01-03 Cold TXK -2.4
3 1947-01-04 Cold TXK -2.4
4 1947-01-05 Cold TXK -9.4
... ... ... ... ...
84571 2024-03-04 Cold TNK 3.1
84572 2024-03-05 Cold TNK 2.1
84573 2024-03-06 Cold TNK 2.8
84574 2024-03-07 Cold TNK 0.6
84575 2024-03-08 Cold TNK -0.4

84576 rows × 4 columns

Im neuen Dataframe sehen wir jetzt, dass die ursprünglichen Spalten TNK,TMK,TxK mit Werten fehlen und stattdessen es eine neue Spalte variable und value gibt. Wenn man sich die Werte in der Spalte variable ansieht versteht man, was passiert ist: Hier stehen jetzt die alten Spaltennamen TNK,TMK,TxK und in der neuen Spalte value die neuen Werte. Damit haben wir den Spaltennamen in eine beschreibende kategorische Variable umgewandelt. Der neue Dataframe ist dreimal so lang wie der ursprüngliche Dataframe, weshalb man auch von einer Zeilendarstellung redet.

Aus der Zeilendarstellung könnten wir jetzt ein einzelnes ML-Modell bauen, was den Zusammenhang zwischen der Temperaturklasse und den Werten in value lernt und variable nur als Unterscheidungskriterium nutzt. Alternativ können wir damit auch die Zeitreihen einfacher darstellen. So lässt sich damit ein sehr übersichtliches Diagramm zum Vergleich aller Zeitreihen erstellen. Im nächsten Abschnitt Visuelle Datenanalyse gehen wir detaillierte darauf ein.

import plotly.express as px

px.line(wetter_melted, x="DATUM_DT", y="value", facet_row ="variable")
/Users/jploennigs/miniconda3/envs/lehre/lib/python3.11/site-packages/plotly/express/_core.py:2065: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.
  sf: grouped.get_group(s if len(s) > 1 else s[0])
/Users/jploennigs/miniconda3/envs/lehre/lib/python3.11/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning: The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
  v = v.dt.to_pydatetime()

Diese Zeilendarstellung wird auch oft bei komplexen Dataframes mit vielen kategorischen Variablen benutzt. So gibt es viele Datensätze des Statistischen Bundesamtes auf dem GENESIS-Portal im CSV-Flat format, was deutlich einfacher zu verarbeiten ist. Allerdings muss dann der Datensatz aus der Reihendarstellung in eine Spaltendarstellung überführt werden.

Hierfür bietet Pandas die Methode pivot an. Jeder, der sich gewundert hat, wofür man die Pivot-Darstellung in Excel nutzt, hat jetzt eine Antwort. Ähnlich zur melt-Methode, müssen wir bei der pivot-Methode die Spalten identifizieren, welche die Werte values enthält, die Spalte(n) columns welche in neue Spaltennamen umzuwandeln sind und die beizubehaltenden Indexe index.

wetter_hardened = wetter_melted.pivot(columns='variable', index=['DATUM_DT', "TemperaturKlasse"], values='value')
wetter_hardened
variable TMK TNK TXK
DATUM_DT TemperaturKlasse
1947-01-01 Cold -2.1 -3.3 -1.5
1947-01-02 Cold -2.1 -3.3 -1.5
1947-01-03 Cold -4.8 -6.2 -2.4
1947-01-04 Cold -4.8 -6.2 -2.4
1947-01-05 Cold -12.9 -14.0 -9.4
... ... ... ... ...
2024-03-04 Cold 7.1 3.1 12.0
2024-03-05 Cold 3.7 2.1 6.0
2024-03-06 Cold 4.2 2.8 6.2
2024-03-07 Cold 4.4 0.6 9.5
2024-03-08 Cold 2.5 -0.4 8.9

28192 rows × 3 columns

Mit der bekannten Methode reset_index() wandeln wir den Index in Spalten um:

wetter_hardened.reset_index()
variable DATUM_DT TemperaturKlasse TMK TNK TXK
0 1947-01-01 Cold -2.1 -3.3 -1.5
1 1947-01-02 Cold -2.1 -3.3 -1.5
2 1947-01-03 Cold -4.8 -6.2 -2.4
3 1947-01-04 Cold -4.8 -6.2 -2.4
4 1947-01-05 Cold -12.9 -14.0 -9.4
... ... ... ... ... ...
28187 2024-03-04 Cold 7.1 3.1 12.0
28188 2024-03-05 Cold 3.7 2.1 6.0
28189 2024-03-06 Cold 4.2 2.8 6.2
28190 2024-03-07 Cold 4.4 0.6 9.5
28191 2024-03-08 Cold 2.5 -0.4 8.9

28192 rows × 5 columns