Umgang mit NaN (Not a number)

Einführung

Dealing with Nan

NaN wurde offiziell eingeführt vom IEEE-Standard für Floating-Point Arithmetic (IEEE 754). Es ist ein techischer Standard für Fließkomma-Berechnungen, der 1985 entstanden ist - Jahre bevor Python angekündigt wurde und noch früher als Pandas kreiert wurde - durch das "Institute of Electrical and Electronics Engineers" (IEEE). Es wurde eingeführt um Probleme zu lösen die man in vielen Fliesskomma-Implementierungen gefunden hat, welche es schwierig gemacht haben diese einfach und übergreifend zu verwenden.

Der Standard fügte NaN zu den arithmetischen Formaten hinzu."Arithmetische Formate: Mengen aus binären und dezimalen Fliesskomma-Daten, die aus endlichen Zahlen bestehen, unendlichen und speziellen 'not a number (NaN)' Werten."

'nan' in Python

Python kennt NaN-Werte natürlich. Wir können solche mit float() erstellen:

n1 = float("nan")n2 = float("Nan")n3 = float("NaN")n4 = float("NAN")print(n1, n2, n3, n4)
nan nan nan nan

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

import mathn1 = math.nanprint(n1)print(math.isnan(n1))
nanTrue

Achtung: Führen Sie keine Vergleiche durch zwischen "NaN"-Werten und regulären Zahlen-Werten. Ein einfaches oder vereinfachtes Beispiel ist folgendes:Zwei Dinge sind "keine Zahl", somit können sie alles sein und wahrscheinlich meistens nicht das gleiche. Darüber hinaus gibt es keine Möglichkeit NaN-Werte zu sortieren:

print(n1 == n2)print(n1 == 0)print(n1 == 100)print(n2 < 0)
FalseFalseFalseFalse

NaN in Pandas

Beispiel ohne NaN-Werte

Bevor wir mit NaN-Werten arbeiten, verarbeiten wir zunächst eine Datei ohne jegliche NaN-Werte. Die Datei temperatures.csv beinhaltet die Temperaturen von sechs Sensoren die jede 15 Minuten zwischen 6:00 Uhr und 19:15 Uhr gemessen wurden.

Die Daten can mit der Funktion read_csv() eingelesen werden:

import pandas as pddf = pd.read_csv("data1/temperatures.csv",                 sep=";",                 decimal=",")print(df.loc[:3])
       time  sensor1  sensor2  sensor3  sensor4  sensor5  sensor60  06:00:00     14.3     13.7     14.2     14.3     13.5     13.61  06:15:00     14.5     14.5     14.0     15.0     14.5     14.72  06:30:00     14.6     15.1     14.8     15.3     14.0     14.23  06:45:00     14.8     14.5     15.6     15.2     14.7     14.6

Wir wollen pro Messungszeitpunkt die Durchschnittstemperatur berechnen. Dazu können wir die DataFrame-Methode mean() verwenden. Bei Verwendung der Methode mean() ohne Parameter, werden die Spalten aufsummiert. Das ist nicht was wir wollen, ist aber trotzdem interessant:

df.mean()
Der obige Python-Code liefert Folgendes:
sensor1    19.775926sensor2    19.757407sensor3    19.840741sensor4    20.187037sensor5    19.181481sensor6    19.437037dtype: float64
average_temp_series = df.mean(axis=1)print(average_temp_series[:8])
0    13.9333331    14.5333332    14.6666673    14.9000004    15.0833335    15.1166676    15.2833337    15.116667dtype: float64
sensors = df.columns.values[1:]# all columns except the time column will be removed:df = df.drop(sensors, axis=1)print(df[:5])
       time0  06:00:001  06:15:002  06:30:003  06:45:004  07:00:00

Wir fügen die Werte der Durchschnittstemperaturen nun 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[:3])
       time  temperature0  06:00:00    13.9333331  06:15:00    14.5333332  06:30:00    14.666667

Beispiel mit NaNs

Wir werden jetzt eine ähnliche Daten-Datei verwenden wie im vorherigen Beispiel. Diesmal müssen wir jedoch mit NaN-Daten umgehen, wenn die Sensoren mal nicht funktioniert haben.

Wir erstellen ein Temperaturen-DataFrame, in dem einige Daten nicht definiert sind, als NaN sind.Dazu verwenden wir die Daten aus der Datei temperatures.csv und passen sie an:

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 where()-Methode an einfachen Beispielen demonstrieren:

s = pd.Series(range(5))s.where(s > 1)
Wir können die folgenden Ergebnisse erwarten, wenn wir den obigen Python-Code ausführen:
0    NaN1    NaN2    2.03    3.04    4.0dtype: float64
import numpy as npA = np.random.randint(1, 30, (4, 2))df = pd.DataFrame(A, columns=['Foo', 'Bar'])m = df % 2 == 0df.where(m, -df, inplace=True)print(df)
   Foo  Bar0  -29   -51   12  -252  -29  -273   -1    4

Für unser Temperaturen-Beispiel brauchen wir ein DataFrame nan_df, welches nur NaN-Werte beinhaltet und das selbe Muster (Shape) hat wie unser Temperaturen-DataFrame temp_df.Dieses DataFrame verwenden wir dann in der where()-Methode. Zusätzlich brauchen wir ein DataFrame df_bool mit den Bedingungen als True-Werte. 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 über 20% 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.8print(df_bool[:5])
          sensor1  sensor2  sensor3  sensor4  sensor5  sensor6time                                                          06:00:00     True     True     True     True     True     True06:15:00    False     True     True    False     True     True06:30:00     True     True     True     True     True     True06:45:00     True    False    False     True     True     True07:00:00     True     True     True     True    False    False

Wir haben nun alles zusammen um unser DataFrame mit unvollständigen Messungen zu erstellen:

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  sensor6time                                                          06:00:00     14.3     13.7     14.2     14.3     13.5     13.606:15:00      NaN     14.5     14.0      NaN     14.5     14.706:30:00     14.6     15.1     14.8     15.3     14.0     14.206:45:00     14.8      NaN      NaN     15.2     14.7     14.607:00:00     15.0     14.9     15.7     15.6      NaN      NaN07:15:00     15.2     15.2     14.6     15.3     15.5     14.907:30:00     15.4     15.3     15.6     15.6     14.7     15.107:45:00     15.5     14.8     15.4      NaN     14.6     14.908:00:00     15.7     15.6     15.9     16.2     15.4     15.408:15:00     15.9     15.8     15.9     16.9      NaN     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  sensor6time                                                          06:00:00     14.3     13.7     14.2     14.3     13.5     13.606:30:00     14.6     15.1     14.8     15.3     14.0     14.207:15:00     15.2     15.2     14.6     15.3     15.5     14.907:30:00     15.4     15.3     15.6     15.6     14.7     15.108:00:00     15.7     15.6     15.9     16.2     15.4     15.409:00:00     16.8     17.3     17.7     17.8     15.9     16.109:30:00     17.7     18.2     18.2     18.6     16.9     17.411:45:00     24.2     23.1     25.3     23.7     24.5     24.812:45:00     23.4     22.6     23.7     24.4     21.8     23.815:45:00     21.3     21.6     21.6     22.6     20.5     21.018:00:00     19.8     20.0     19.1     19.7     20.1     20.218:30:00     19.5     19.1     19.2     19.7     18.3     18.3

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]
Der obige Code führt zu folgendem Ergebnis:
time
06:00:00
06:15:00
06:30:00
06:45:00
07:00:00

Wir ändern unsere Aufgabe: Wir sind 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 Temperatur-Werte in jeder Zeile. Mit thresh = 5 stellen wir sicher, dass mindestens 5 Werte in jeder Zeile enthalten sind:

cleansed_df = disturbed_data.dropna(thresh=5, axis=0)print(cleansed_df[:7])
          sensor1  sensor2  sensor3  sensor4  sensor5  sensor6time                                                          06:00:00     14.3     13.7     14.2     14.3     13.5     13.606:30:00     14.6     15.1     14.8     15.3     14.0     14.207:15:00     15.2     15.2     14.6     15.3     15.5     14.907:30:00     15.4     15.3     15.6     15.6     14.7     15.107:45:00     15.5     14.8     15.4      NaN     14.6     14.908:00:00     15.7     15.6     15.9     16.2     15.4     15.408:15:00     15.9     15.8     15.9     16.9      NaN     16.2

Jetzt berechnen wir erneut die Durchschnittswerte. Dieses Mal mit den Werten aus cleansed_df, d.h. auf dem 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.valuesdf = cleansed_df.drop(sensors, axis=1)# best practice:df = df.assign(temperature=average_temp_series)  # inplace option not availableprint(df[:6])
          temperaturetime                 06:00:00    13.93333306:30:00    14.66666707:15:00    15.11666707:30:00    15.28333307:45:00    15.04000008:00:00    15.700000