# invisible
import pandas as pd
pd.set_option('display.max_colwidth', 65)
pd.set_option('display.max_columns', 65)
Umgang mit NaN
\index{ NaN wurde offiziell eingeführt vom IEEE-Standard für Floating-Point Arithmetic (IEEE 754). Es ist ein technischer Standard für Fließkommaberechnungen, der 1985 durch das "Institute of Electrical and Electronics Engineers" (IEEE) eingeführt wurde -- Jahre bevor Python entstand, und noch mehr Jahre, bevor Pandas kreiert wurde. Der Standard wurde eingeführt, um Probleme zu lösen, die man in vielen Fließkommaßmplementierungen gefunden hatte, welche es schwierig gemacht haben, diese einfach und übergreifend zu verwenden.
Der Standard fügte NaN zu den arithmetischen Formaten -- Mengen aus binären und dezimalen Fließkommadaten -- hinzu.
'nan' in Python
Python ohne Pandas kennt auch NaN-Werte. Wir können solche mit float()
erstellen:
n1 = float("nan")
n2 = float("Nan")
n3 = float("NaN")
n4 = float("NAN")
print(n1, n2, n3, n4)
print(type(n1))
nan
ist auch Teil des math
-Moduls seit Python 3.5:
import math
n1 = math.nan
print(n1)
print(math.isnan(n1))
Achtung: Führen Sie keine Vergleiche durch zwischen "NaN"-Werten und regulären Zahlen-Werten durch. Darüber hinaus gibt es keine Möglichkeit, NaN-Werte zu vergleichen und zu sortieren:
print(n1 == n2)
print(n1 == 0)
print(n1 == 100)
print(n2 < 0)
NaN in Pandas
In diesem Abschnitt möchten wir zeigen, wie man sinnvoll mit NaN-Werten in Pandas umgehen kann. Wir werden eine Datei mit Messwerten auswerten, die vereinzelt NaN-Werte aufweist. Doch bevor wir mit NaN-Werten arbeiten, bearbeiten wir zunächst eine Datei ohne jegliche NaN-Werte. Die Datei temperatures.csv beinhaltet die Temperaturen von sechs Sensoren, die alle 15 Minuten zwischen 6:00 Uhr und 19:15 Uhr gemessen wurden.
Die Daten aus dieser Datei können mit der Funktion read_csv
eingelesen werden:
import pandas as pd
df = pd.read_csv("data1/temperatures.csv",
sep=";",
index_col=0,
decimal=",")
print(df.head())
Wir wollen pro Messzeitpunkt die Durchschnittstemperatur berechnen. Dazu können wir die DataFrame-Methode mean
verwenden. Bei Verwendung der Methode mean
ohne Parameter werden die Spalten aufsummiert. Auch wenn dies nicht das ist, was wir wollen, ist es aber trotzdem interessant, denn damit haben wir die Durchschnitt über den Messtag berechnet.
df.mean()
Was wir eigentlich bestimmen wollen, ist die Durchschnittstemperatur über alle sechs Sensoren. Dazu setzen wir den Parameter axis
auf den Wert 1
:
average_temp_series = df.mean(axis=1)
print(average_temp_series[:8]) # die ersten 8 Zeilen
sensors = df.columns.values
# all columns will be removed:
df = df.drop(sensors, axis=1)
print(df[:5])
Nun fügen wir die Werte der Durchschnittstemperaturen dem DataFrame als neue Spalte temperature
hinzu:
# best practice:
df = df.assign(temperature=average_temp_series) # inplace option not available
# alternatively:
#df.loc[:,"temperature"] = average_temp_series
print(df[:5])
Beispiel mit NaNs
Stellen wir uns vor, die Datei temperatures.csv
enthielte in den Sensorspalten NaN-Werte. Ein NaN-Wert bedeutet, dass das Messgerät zu diesem Zeitpunkt keine Messung liefern konnte.
Da wir keine solche Datei haben, werden wir eine solche Datei nun zu Übungszwecken künstlich erzeugen. Wir werden die Werte aus der Datei temperatures.csv
nutzen, um ein DataFrame zu erzeugen. Dann erzeugen wir zufallsgesteuert NaN-Werte in dieser Datenstruktur:
temp_df = pd.read_csv("data1/temperatures.csv",
sep=";",
index_col=0,
decimal=",")
Nun weisen wir dem DataFrame-Objekt per Zufall NaN-Werte zu. Dazu verwenden wir die where
-Methode des DataFrame. Wenn where
auf ein DataFrame-Objekt df
angewendet wird, d.h. df.where(cond, other_df)
, wird ein Objekt mit dem identischen Muster (Shape) wie df
zurückgeliefert, dessen Werte aus df
stammen und das korrespondierende Element aus cond = True
ist. Ansonsten stammt der Wert aus other_df
.
Bevor wir mit unserem Temperaturen-Beispiel weitermachen, möchten wir die Arbeitsweise der where
-Methode an einfachen Beispielen demonstrieren:
s = pd.Series(range(5))
s.where(s > 1)
import numpy as np
A = np.random.randint(1, 30, (4, 2))
df = pd.DataFrame(A, columns=['Foo', 'Bar'])
m = df % 2 == 0
df.where(m, -df, inplace=True)
print(df)
Für unser Temperaturen-Beispiel brauchen wir ein DataFrame nan_df
, welches nur NaN-Werte beinhaltet und dasselbe Muster (Shape) wie unser Temperaturen-DataFrame temp_df
aufweist. Dieses DataFrame verwenden wir dann in der where
-Methode. Zusätzlich brauchen wir ein DataFrame df_bool
mit den Bedingungen als True
-Werten. Dazu erstellen wir ein DataFrame-Objekt mit Zufallswerten zwischen 0 und 1 mit der Anweisung random_df < 0.8
. Damit erhalten wir das DataFrame-Objekt df_bool
, in dem ca. 80 % der Werte True
sind:
random_df = pd.DataFrame(np.random.random(size=(54, 6)),
columns=temp_df.columns.values,
index=temp_df.index)
nan_df = pd.DataFrame(np.nan,
columns=temp_df.columns.values,
index=temp_df.index)
df_bool = random_df<0.8
print(df_bool[:5])
Wir haben nun alles zusammen, um unser DataFrame mit unvollständigen Messungen mittels where
zu erstellen und dieses DataFrame dann mit der Methode to_csv
in der Datei temperatures_with_NaN.csv
abzuspeichern:
disturbed_data = temp_df.where(df_bool, nan_df)
disturbed_data.to_csv("data1/temperatures_with_NaN.csv")
print(disturbed_data[:10])
df = disturbed_data.dropna()
print(df)
dropna
kann auch verwendet werden, um alle Spalten zu entfernen, in denen einige Werte NaN sind. Dafür muss lediglich der Parameter axis = 1
gesetzt werden. Wie wir im vorherigen Beispiel gesehen haben, ist der Default-Wert dafür False
. Sollte jede Spalte der Sensoren NaN-Werte enthalten, so werden auch alle Spalten ausgeblendet:
df = disturbed_data.dropna(axis=1)
df[:5]
Wir ändern unsere Aufgabe: Wir sind nun nur an den Zeilen interessiert, welche mehr als einen NaN-Wert enthalten. Dafür ist der Parameter thresh
ideal. Dieser kann auf einen Minimal-Wert gesetzt werden. thresh
wird auf den Integer-Wert gesetzt, der die minimale Anzahl an Nicht-NaN-Werten angibt. Wir haben sechs Temperaturwerte in jeder Zeile. Mit thresh = 5
stellen wir sicher, dass mindestens 5 von NaN
verschiedene Werte in jeder Zeile enthalten sind:
cleansed_df = disturbed_data.dropna(thresh=5, axis=0)
print(cleansed_df[:7])
Jetzt berechnen wir erneut die Durchschnittswerte, aber diesmal aus cleansed_df
, d.h. das DataFrame, aus dem bereits alle Zeilen entfernt wurden, die mehr als einen NaN-Wert hatten:
average_temp_series = cleansed_df.mean(axis=1)
sensors = cleansed_df.columns.values
df = cleansed_df.drop(sensors, axis=1) # nicht unbedingt notwendig
df = df.assign(temperature=average_temp_series) # inplace option not available
print(df[:6])
#prog4book
cleansed_df = disturbed_data.dropna(thresh=4, axis=0)
print(cleansed_df[:7])
#prog4book
average_temp_series = cleansed_df.mean(axis=1)
sensors = cleansed_df.columns.values
df = cleansed_df.drop(sensors, axis=1)
df = df.assign(temperature=average_temp_series)
print(df[:6])