Dies ist der erste Teil der Serie Automatisierte Klassifizierung in Python in der wir beispielhaft zeigen, wie man einen gegebenen Datensatz mithilfe von Machine Learning Klassifizierungsverfahren in Python klassifizieren kann.

In folgendem Beitrag zeigen wir die Analyse und Aufbereitung des frei verfügbaren „Adult“ Datensatzes zur Klassifizierung. Ebenfalls haben wir unser Skript zusammen mit den Datensätzen und der Dokumentation auf GitHub veröffentlicht.

Der Datensatz entstammt dem Machine Learning Repository der University of California Irvine. Dieses beinhaltet derzeit 473 Datensätze (zuletzt aufgerufen: 10. Mai 2019), die für Machine Learning Anwendungen zur Verfügung stehen. Der “Adult” Datensatz’’ basiert auf US-Zensus Daten. Ziel ist es, anhand der gegebenen Daten zu bestimmen, ob eine Person mehr oder weniger als 50.000 USD im Jahr verdient.

Data-profiling

Der erste Schritt den wir vornehmen, bevor wir mit der eigentlichen Klassifizierung beginnen können, ist, dass wir uns die Struktur des Datensatzes anschauen. Dabei stellen wir fest, dass der Datensatz aus zusammengenommen ca. 45.000 Personendaten besteht und bereits in Trainings- und Testdaten aufgeteilt ist.

Ein Teil der Daten (ca. 7,5%) sind unvollständig da für einzelne Personen Datenpunkte (Features) nicht angegeben wurden. Aufgrund der relativ niedrigen Zahl der fehlerhafter Datensätze, werden wir diese zunächst einfach in der Analyse ignorieren.

Die Personendaten bestehen aus kontinuierlichen und kategorischen Features der Personen. Die kontinuierliche Daten sind: Alter, ‘final weight’, Bildungsjahre, Kapitalzuwachs, Kapitalverlust und Wochenstunden. Die kategorischen Daten sind: Beschäftigungsverhältnis, Abschluss, Familienstand, Beruf, Beziehung, Rasse, Geschlecht und das Geburtsland.

Unsere Zielvariable ist das Einkommen einer Person, genauer ob eine Person weniger oder mehr als 50.000 US-Dollar im Jahr verdient. Da unsere Zielvariable nur zwei verschiedene Werte annehmen kann, handelt es sich um eine binäre Klassifikation. Innerhalb des Datensatzes beträgt das Verhältnis zwischen Personen die weniger als 50.000 US-Dollar verdienen, zu jenen, die mehr verdienen ca. 3:1.

Analyse der Featureeigenschaften

Bei der Analyse der einzelnen Features ist das Feature ‘final weight besonders aufgefallen: Dieses gruppiert ähnliche Personen, basierend auf sozioökonomischen Faktoren und diese Wertung ist abhängig vom US-Bundesstaat in dem eine Person lebt.  Aufgrund des relativ kleinen Datensatzes und der ungenauen Dokumentation der zu Grunde liegenden Berechnung, haben wir uns entschieden, dieses Feature in der ersten Berechnung nicht zu berücksichtigen. Ein späterer direkter Vergleich zeigte, dass das Auslassen dieses Features in Einzelfällen zu einer Verbesserung der Klassifikationsergebnisse führte, aber nie zu einer Verschlechterung.

Um die Problemstellung zu lösen, anhand der genannten Features einer Person ihr Einkommen vorherzusagen, verwenden wir einen supervised Machine Learning Ansatz, da wir über viele gelabelte Daten verfügen. Anhand dieser kann der Algorithmus die Abhängigkeit der einzelnen Features zum Target abschätzen. Im zweiten Teil unseres Beitrags stellen wir dafür einige Verfahren vor, die wir bereits in unserem Blog behandelt haben. All diese Verfahren setzen jedoch zunächst eine sehr genaue Vorverarbeitung der Daten voraus, damit unser Modell in der Lage ist, diese zu bewerten und Werte wie etwa “Montag” oder “Dienstag” zu  interpretieren. Man spricht vom “cleaning” der Daten.

Vorverarbeitung der Daten

Wir müssen unsere Daten zunächst vorverarbeiten, um auf diese die verschiedenen Machine Learning Modelle anwenden zu können. Die verschiedenen Modelle vergleichen die einzelnen Features der Daten miteinander, um den Zusammenhang zum Target zu bestimmen. Dafür müssen die Daten in einer einheitlichen Form vorliegen, um dadurch eine Vergleichbarkeit zu ermöglichen. Deshalb sprechen wir vom säubern der Daten.

Mit der folgenden Funktion säubern wir unsere Daten. Die Funktionsweise erläutern wir in den folgenden Abschnitten:

def setup_data(self):
        """ set up the data for classification """
        traindata = self.remove_incomplete_data(self.traindata)
        testdata = self.remove_incomplete_data(self.testdata)
        
        self.y_train = self.set_target(traindata)
        self.y_test = self.set_target(testdata)

        # set dummies of combined train and test data with removed target variable
        fulldata = self.get_dummies(traindata.append(testdata, ignore_index=True).drop(self.target, axis=1).drop("fnlwgt", axis=1), self.categorical_features)
        self.x_train = fulldata[0:len(traindata)]
        self.x_test = fulldata[len(traindata):len(fulldata)]

Unser Datensatz ist zwar bereits in einen Trainings- und einen Testdatensatz im Verhältnis 2:1 aufgeteilt, für die Erzeugung von Dummyvariablen müssen wir ihn dennoch zwischenzeitlich zusammenführen, um ihn später wieder im gleichen Verhältnis aufteilen zu können. Dieses Vorgehen bietet den entscheidenden Vorteil, dass die entstehenden Datensätze unter allen Umständen die gleiche Form und Dimensionalität besitzen. Andernfalls kann es passieren, dass wenn ein Wert in entweder dem Trainings- oder Testdatensatz fehlt, der jeweils neue Datensatz weniger Spalten besitzt, bzw. die Spalten mit gleichem Index für verschiedene Feature-Werte stehen. Dadurch geht die Vergleichbarkeit der beiden Datensätze verloren.

Weiterhin kommen in dem Datensatz vereinzelt unbekannt Werte vor, die wir gezielt angehen müssen. Der Anteil an Daten mit unbekannten Werten des Datensatzes ist allerdings relativ klein (<10%). Deswegen ist es uns möglich, diese unvollständigen Daten außen vor zulassen und aus dem Datensatz zu entfernen. Dies erreichen wir in der Funktion „setup_data“ durch den Aufruf unserer Funktion „remove_incomplete_data“:

def remove_incomplete_data(self, data):
    """ Remove every row of the data that contains atleast 1 "?". """
    return data.replace("?", np.nan).dropna(0, "any")

Bei dieser werden alle Zeilen die mindestens ein „?“ enthalten aus dem Datensatz entfernt. Wir tun dies, um sicherzustellen, dass der Algorithmus stets relevante Daten erhält und keine Relationen zwischen unbekannten Werten erstellt. Diese würden bei der späteren Erzeugung der Dummy-Variablen als gleiche Werte angesehen und nicht als unbekannt interpretiert werden. Nachdem wir die Funktion ausgeführt haben, besteht unser Datensatz jetzt noch aus 45.222 Einträgen, im Gegensatz zu den Vorherigen 48.842.

Zuweisen der Zielvariable

Im zweiten Teil der „setup_data“ Funktion ordnen wir über den Funktionsaufruf „set_target“ der Zielvariable die Werte 0 oder 1 zu, je nachdem, ob jemand mehr oder weniger als 50.000 US-Dollar im Jahr verdient.

def set_target(self, data):
    """ Set the target values of the target variables (0,1 for either case). """
    for i in range(len(data[self.target].unique())):
        data[self.target] = np.where(data[self.target] == data[self.target].unique()[i], i, data[self.target])
    return data[self.target].astype("int")

Kategorische Werte mit Dummy-Variablen ersetzen

Bevor wir nun damit beginnen, eine Klassifizierung der Daten vorzunehmen, müssen wir zunächst dafür sorgen, dass unser Modell in der Lage ist, mit kategorischen Werten umzugehen. Dafür erzeugen wir aus allen kategorischen Variablen sog. Dummy-Variablen über das one-hot encoding Verfahren. Dabei erhält jede mögliche Belegung einer kategorischen Variable eine eigene Variable, sodass anstelle einer einzelnen Variable, die unterschiedliche Werte annehmen kann, viele Variablen existieren, die jeweils nur den Wert 0 oder 1 annehmen können und die jeweils eine kategorische Belegung der ersetzen Variable repräsentieren.

Motivation

Ein Beispiel: Wir haben ein Objekt vom Typ “datum” mit dem Feature ‘wochentag = {‚Montag‘, ‚Dienstag‘, ‚Mittwoch‘, …}’. Nach der Erzeugung der Dummy-Variablen gibt es das Feature ‘wochentag’ nicht mehr. Stattdessen stellt jede mögliche Belegung ein eigenes Feature dar. Diese sind in unserem Beispiel: wochentag_dienstag, …, wochentag_sonntag. Je nachdem, welchen Wochentag das Feature vor der Erzeugung besaß, wird diese Variable auf 1 und die Restlichen auf 0 gesetzt.

Der aufmerksame Leser fragt sich an dieser Stelle bestimmt, wieso das Feature wochentag_montag nicht existiert. Der einfache Grund für das Auslassen liegt darin, dass sich aus der negativen Belegung der anderen Features implizit schließen lässt, dass ein Objekt den Wert wochentag_montag besitzt. Ein weiterer Vorteil besteht darin, dass eine zu starke Abhängigkeit, Multikollinearität, der einzelnen Variablen vermieden wird. Diese könnte sich negativ auf das Ergebnis auswirken, da die starke Abhängigkeit es erschweren kann, den genauen Effekt einer bestimmten Variable in einem Modell zu bestimmen. Die Erzeugung der Dummy-Variablen ist deshalb notwendig, da wie bereits erwähnt, ein Modell kein Wissen über einen Wochentag hat und wie es diesen interpretieren soll. Nach der Erzeugung der Dummyvariablen spielt dies auch keine Rolle mehr, da der Algorithmus nur noch unterscheidet, ob das Feature eines Objekt über den Wert 0 oder 1 verfügt. Dadurch wird ein Vergleich der einzelnen Objekte mit den jeweiligen Features möglich.

Umsetzung

Im letzten Teil unserer Funktion „setup_data“ haben wir die Erzeugung der Dummys über den Aufruf der Funktion „get_dummies“ wie folgt realisiert:

def get_dummies(self, data, categorical_features):
    """ Get the dummies of the categorical features for the given data. """
    for feature in self.categorical_features:
        # create dummyvariable with pd.get_dummies and drop all categorical variables with dataframe.drop
        data = data.join(pd.get_dummies(data[feature], prefix=feature, drop_first=True)).drop(feature, axis=1)
    return data

Wir erzeugen eine Schleife, die alle kategorischen Variablen des Datensatzes durchläuft. Bei jedem Durchlauf hängen wir dem Datensatz alle Dummyvariablen der jeweiligen kategorischen Variable mit Hilfe der Pandas Funktion „get_dummies“ an. Anschließend entfernen wir diese kategorische Variable. Nach Abschluss der Schleifenanweisung enthält unser Datensatz keine kategorischen Variablen mehr. Stattdessen besitzt er die jeweiligen Dummyvariablen.

So erhalten wir aus den ursprünglichen Features:

          age   workclass
Person1   39    Local-gov
Person2   50    Federal-gov

Die Folgenden:

          age   workclass_Federal-gov  workclass_Local-gov  workclass_Never-worked
Person1   39    0                      1                    0
Person2   50    1                      0                    0

Hierbei wird der Grund für das zwischenzeitliche Zusammenfügen der beiden Datensätze nochmals deutlich: Falls z.B. der Wert “Local-gov” in nur einem der Datensätze vorhanden ist, verfügen bei der Erzeugung der Dummyvariablen die entstehenden Datensätze über verschiedene Dimensionalitäten, da in dem anderen Datensatz die gesamte Spalte fehlt.

Beispiel: Wenn das Modell beispielsweise einen großen Zusammenhang zwischen “Local-gov” und einem Einkommen von über 50.000 USD herstellt, verschiebt sich dieser Zusammenhang in dem anderen Datensatz zu dem Feature, dass den Platz von “Local-gov” belegt. Daraus resultiert wahrscheinlich ein falsches Ergebnis aber in jedem Fall ein falscher Zusammenhang.

Im letzten Teil der “setup_data” Funktion teilen wir die Datensätze wieder in einen Trainings- und Testdatensatz auf.

self.x_train = fulldata[0:len(traindata)]
self.x_test = fulldata[len(traindata):len(fulldata)]

Im zweiten Teil gehen wir darauf ein, wie wir die aufbereiteten Daten auf verschiedene Klassifikatoren anwenden und anschließend die Ergebnisse vergleichen und evaluieren können.