Umgang mit NaN

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 nan nan nan
<class 'float'>

nan ist auch Teil des math-Moduls seit Python 3.5:

import math
n1 = math.nan
print(n1)
print(math.isnan(n1))
nan
True

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)
False
False
False
False

NaN in Pandas

Sechs Temperaturfühler

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())
          sensor1  sensor2  sensor3  sensor4  sensor5  sensor6
time                                                          
06:00:00     14.3     13.7     14.2     14.3     13.5     13.6
06:15:00     14.5     14.5     14.0     15.0     14.5     14.7
06:30:00     14.6     15.1     14.8     15.3     14.0     14.2
06:45:00     14.8     14.5     15.6     15.2     14.7     14.6
07:00:00     15.0     14.9     15.7     15.6     14.0     15.3

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()
Der obige Code führt zu folgendem Ergebnis:
sensor1    19.775926
sensor2    19.757407
sensor3    19.840741
sensor4    20.187037
sensor5    19.181481
sensor6    19.437037
dtype: float64

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
time
06:00:00    13.933333
06:15:00    14.533333
06:30:00    14.666667
06:45:00    14.900000
07:00:00    15.083333
07:15:00    15.116667
07:30:00    15.283333
07:45:00    15.116667
dtype: float64
sensors = df.columns.values
# all columns will be removed:
df = df.drop(sensors, axis=1)
print(df[:5])
Empty DataFrame
Columns: []
Index: [06:00:00, 06:15:00, 06:30:00, 06:45:00, 07:00:00]

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])
          temperature
time                 
06:00:00    13.933333
06:15:00    14.533333
06:30:00    14.666667
06:45:00    14.900000
07:00:00    15.083333

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)
Führt man obigen Code aus, erhält man folgende Ausgabe:
0    NaN
1    NaN
2    2.0
3    3.0
4    4.0
dtype: float64
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)
   Foo  Bar
0  -15    6
1  -25   24
2    4  -11
3   -1    2

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])
          sensor1  sensor2  sensor3  sensor4  sensor5  sensor6
time                                                          
06:00:00    False    False     True     True    False    False
06:15:00     True     True     True     True     True    False
06:30:00     True    False     True     True     True     True
06:45:00     True     True     True     True    False     True
07:00:00    False     True     True     True     True     True

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])
          sensor1  sensor2  sensor3  sensor4  sensor5  sensor6
time                                                          
06:00:00      NaN      NaN     14.2     14.3      NaN      NaN
06:15:00     14.5     14.5     14.0     15.0     14.5      NaN
06:30:00     14.6      NaN     14.8     15.3     14.0     14.2
06:45:00     14.8     14.5     15.6     15.2      NaN     14.6
07:00:00      NaN     14.9     15.7     15.6     14.0     15.3
07:15:00     15.2     15.2     14.6     15.3     15.5     14.9
07:30:00     15.4     15.3     15.6     15.6     14.7     15.1
07:45:00     15.5     14.8     15.4     15.5     14.6     14.9
08:00:00     15.7     15.6     15.9     16.2     15.4     15.4
08:15:00      NaN     15.8     15.9     16.9     16.0     16.2

dropna() verwenden

dropna ist eine DataFrame-Methode. Wenn wir diese Methode ohne Argumente verwenden, wird ein Objekt zurückgegeben, bei dem jede Zeile entfernt wurde, in der Daten gefehlt haben, also NaN-Werte waren:

df = disturbed_data.dropna()
print(df)
          sensor1  sensor2  sensor3  sensor4  sensor5  sensor6
time                                                          
07:15:00     15.2     15.2     14.6     15.3     15.5     14.9
07:30:00     15.4     15.3     15.6     15.6     14.7     15.1
07:45:00     15.5     14.8     15.4     15.5     14.6     14.9
08:00:00     15.7     15.6     15.9     16.2     15.4     15.4
08:30:00     16.1     15.7     16.1     15.9     14.9     15.2
09:45:00     18.4     19.0     19.0     19.4     18.4     18.3
10:30:00     20.4     19.4     20.0     21.0     20.2     19.8
12:00:00     24.0     23.1     23.1     24.8     22.5     22.7
12:15:00     23.8     23.7     24.8     25.1     22.2     22.4
12:30:00     23.6     24.2     23.6     24.1     22.1     22.5
13:30:00     22.9     21.9     22.9     24.3     22.9     23.0
14:30:00     22.1     21.9     22.3     22.2     21.2     22.1
15:15:00     21.6     21.3     21.7     21.7     21.9     21.1
15:30:00     21.4     21.3     21.7     21.9     21.0     21.7
16:30:00     20.8     20.7     20.7     20.4     20.2     19.6
17:15:00     20.3     20.7     19.6     21.3     19.8     19.0
17:30:00     20.1     20.5     19.7     19.7     18.7     19.7
18:15:00     19.6     19.9     19.2     19.9     20.0     18.6
19:15:00     19.0     19.7     18.9     19.2     18.5     19.4

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 erhalten die folgende Ergebnisse:
time
06:00:00
06:15:00
06:30:00
06:45:00
07:00:00

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])
          sensor1  sensor2  sensor3  sensor4  sensor5  sensor6
time                                                          
06:15:00     14.5     14.5     14.0     15.0     14.5      NaN
06:30:00     14.6      NaN     14.8     15.3     14.0     14.2
06:45:00     14.8     14.5     15.6     15.2      NaN     14.6
07:00:00      NaN     14.9     15.7     15.6     14.0     15.3
07:15:00     15.2     15.2     14.6     15.3     15.5     14.9
07:30:00     15.4     15.3     15.6     15.6     14.7     15.1
07:45:00     15.5     14.8     15.4     15.5     14.6     14.9

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])
          temperature
time                 
06:15:00    14.500000
06:30:00    14.580000
06:45:00    14.940000
07:00:00    15.100000
07:15:00    15.116667
07:30:00    15.283333