R und Python: Mit Reticulate das Beste aus beiden Welten nutzen


Es ist der 15. März und das bedeutet: Es ist World Sleep Day (WSD). Noch nicht wegnicken! Wir schauen uns gleich ein Package an, mit dem sich R und Python wie im Schlaf nutzen lassen. Es heißt reticulate und wir werden es verwenden, um eine Support Vector Machine für eine einfache Klassifikationsaufgabe zu trainieren.
Was hat WSD mit Data Science zu tun?
Ich habe vom WSD im Zug zum statworx-HQ in Frankfurt erfahren. Wenn du schon mal mit der Bahn gereist bist, weißt du: Das Einzige, was schlimmer ist als genervtes Personal, ist bewusstloses Personal. Mir ist das zwar noch nicht passiert, aber mal ehrlich: Der Personalmangel bei der Deutschen Bahn wird so schnell nicht besser.
Damit wären wir beim Thema des heutigen Blogposts: die Schlafgewohnheiten von Eisenbahnbeschäftigten. Dafür habe ich ein Datenset von der US Federal Railroad Administration (FRA) verwendet. Die FRA hat eine Umfrage zu Arbeits- und Schlafzeiten ihrer Fahrdienstleiter durchgeführt. Du findest sie hier.
Wie der Titel schon andeutet, werden wir sowohl R als auch Python verwenden, um vorherzusagen, ob ein Fahrdienstleiter eine Schlafstörung diagnostiziert bekommen hat. Bevor wir anfangen, eine Warnung an alle R- und Python-Puristen: Das folgende Beispiel ist ein bisschen konstruiert. Alles, was wir tun, lässt sich auch vollständig in nur einer der beiden Sprachen umsetzen.
Warum also überhaupt R und Python zusammen verwenden?
Als Data Scientists sollten wir idealerweise mit beiden Sprachen vertraut sein. Schließlich haben, wie meine Kollegin Fran in einem kommenden Beitrag erklären wird, beide ihre eigenen Stärken, die man für sich nutzen kann. Ich persönlich liebe R für den tidyverse und ggplot2, Python wegen seiner einheitlichen Machine-Learning-API scikit-learn.
Okay, Argument verstanden, sagst du vielleicht, aber das Hin- und Herwechseln ist nervig. Das lohnt sich nicht – und bis vor ein paar Jahren hättest du damit recht gehabt! Heutzutage gibt es jedoch ein paar coole Möglichkeiten, das Beste aus beiden Welten zu kombinieren. Wenn du aus der R-Community kommst, ist reticulate genau das Richtige für dich!
Das reticulate-Package bietet eine Sammlung von Tools, mit denen man R und Python interaktiv innerhalb einer R-Session verwenden kann. Du arbeitest in Python und brauchst ein spezielles statistisches Modell aus einem R-Package – oder umgekehrt? Voilà: Dieses Package ist ein Geschenk des Himmels für alle Aufgaben, bei denen es notwendig oder einfach praktischer ist, beide Sprachen im Workflow einzusetzen.
Es gibt verschiedene Möglichkeiten, R und Python interaktiv zu kombinieren. Ich empfehle dir, die GitHub-Seite von reticulate zu besuchen und zu schauen, welche Variante dir am besten passt. In diesem Beitrag machen wir ein einfaches Beispiel, wie man Python-Module in einem R Notebook (also einem Markdown-Dokument) verwendet.
Wie startet man?
Wenn du auf einem Mac arbeitest, ist Python schon vorinstalliert. Unabhängig davon, welches System du nutzt, empfehle ich dir die Anaconda-Distribution herunterzuladen – damit hast du alles, was du brauchst. Noch ein Hinweis: Du brauchst die neueste Preview-Version 1.2 von RStudio, damit das funktioniert. That’s it!
Öffne ein R Notebook, füge einen R-Chunk ein und (installiere und) lade die reticulate-Bibliothek. Direkt nach dem Laden von reticulate solltest du den Befehl use_python() mit dem entsprechenden Pfad verwenden. Wenn du das später im Skript machst, kann es bei manchen Leuten zu Problemen führen. Ich habe meinen Pfad wie folgt angegeben (Hinweis: Deiner kann abweichen):
library(tidyverse)
library(recipes)
library(reticulate)
use_python("/anaconda3/bin/python")
Los geht’s. Wir lesen die Daten ein und führen mit dplyr ein paar Transformationen durch. Dabei geht es vor allem um Rekodierungen. Wie du im select-Befehl siehst, wählen wir eine Handvoll Variablen wie Geschlecht, Alter, Koffeinkonsum, Gesundheitsstatus und Stresslevel aus, um vorherzusagen, ob ein Fahrdienstleiter eine Schlafstörung diagnostiziert bekommen hat.
1) Daten einlesen
data <- readxl::read_xls("sleep.xls")
sleep <- data %>%
select(
Diagnosed_Sleep_disorder, Age_Group, Sex, Total_years_dispatcher,
Total_years_present_job, Marital_Status, Childrendependents,
Children_under_2_yrs, Caff_Beverages, Sick_Days_in_last_year,
Health_status, Avg_Work_Hrs_Week, FRA_report, Phys_Drained,
Mentally_Drained, Alert_at_Work, Job_Security
) %>%
rename_all(tolower) %>%
mutate_if(is.character, as.numeric) %>%
mutate_at(vars(diagnosed_sleep_disorder, sex, caff_beverages, fra_report),
~ -(. - 2)) %>%
mutate_at(vars(marital_status), ~ (. - 1)) %>%
drop_na()
Jetzt, wo wir die gewünschten Variablen haben, bringen wir die Daten in die richtige Form. Hier kommt ein weiterer Grund, R zu lieben: das recipes-Package. Wenn du es noch nicht kennst – schau es dir an. Der Workflow wirkt vielleicht zunächst ungewohnt, aber sobald man ihn verstanden hat, wird die Datenvorbereitung zum Kinderspiel.
Was wir hier machen, ist recht einfach. Zuerst teilen wir die Daten in Trainings- und Testmenge. Danach definieren wir ein Rezept zur Datenvorbereitung mit drei Schritten: One-Hot-Encoding für kategoriale Prädiktoren, Standardisierung der numerischen Prädiktoren und Downsampling der Daten. One-Hot-Encoding und Standardisierung stellen sicher, dass der SVM-Algorithmus korrekt funktioniert. Downsampling gleicht das Ungleichgewicht in den Klassen unseres Datensatzes aus.
2) Daten vorbereiten
numeric_variables <- c(
"total_years_dispatcher", "total_years_present_job",
"childrendependents", "children_under_2_yrs",
"sick_days_in_last_year", "avg_work_hrs_week"
)
factor_variables <- setdiff(colnames(sleep), numeric_variables)
sleep <- mutate_at(sleep, vars(factor_variables), as.factor)
set.seed(2019)
index <- sample(1:nrow(sleep), floor(nrow(sleep) * .75))
sleep_train <- sleep[index, ]
sleep_test <- sleep[-index, ]
recipe_formula <- recipes::recipe(diagnosed_sleep_disorder ~ ., sleep_train)
recipe_steps <- recipe_formula %>%
recipes::step_dummy(factor_variables, -all_outcomes(), one_hot = TRUE) %>%
recipes::step_downsample(diagnosed_sleep_disorder) %>%
recipes::step_center(numeric_variables) %>%
recipes::step_scale(numeric_variables)
recipe_prep <- recipes::prep(recipe_steps, sleep_train, retain = TRUE)
training_data <- juice(recipe_prep)
testing_data <- bake(recipe_prep, sleep_test)
Jetzt kommt der Teil, in dem Python glänzt: seine einheitliche ML-Bibliothek scikit-learn. Wir importieren den Support-Vector-Machine-Klassifikator und ein paar weitere Module, um unser Modell zu tunen und zu evaluieren.
SVM ist ein überwacht lernender Algorithmus. Er sucht eine Hyperebene im N-dimensionalen Raum, die zwei (oder mehr) Klassen möglichst sauber trennt. Technisch ausgedrückt: SVM maximiert den Abstand (die „Margin“) zwischen der trennenden Hyperebene und den nächstgelegenen Datenpunkten. Daher nennt man SVM auch einen Maximum Margin Estimator.
SVM wird meist für Klassifikationsaufgaben genutzt, kann aber auch Regression. Der Vorteil liegt in seiner Fähigkeit, mit hochdimensionalen Daten und verschiedenen Kernel-Funktionen zu arbeiten – dadurch kann er sich flexibel an unterschiedliche Datentypen anpassen. Der Nachteil: Bei großen Datensätzen wird die Berechnung teuer, und SVM reagiert empfindlich auf Hyperparameter. Trotzdem liefert SVM in vielen Anwendungen konkurrenzfähige Ergebnisse.
Durch die Kombination von SVM mit Kernels können wir unsere Daten in einen höherdimensionalen Raum projizieren. Der Zweck dahinter: die Klassen besser trennbar zu machen. In unserem Beispiel nutzen wir einen einfachen linearen Kernel und einen RBF-Kernel (Radial Basis Function). Letzterer kann den Prädiktorraum in unendlich viele Dimensionen abbilden.
3) Modell trainieren
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
y_train = r.training_data['diagnosed_sleep_disorder']
X_train = r.training_data.drop('diagnosed_sleep_disorder', axis = 1)
y_test = r.testing_data['diagnosed_sleep_disorder']
X_test = r.testing_data.drop('diagnosed_sleep_disorder', axis = 1)
clf = svm.SVC(kernel = 'linear')
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))
clf = svm.SVC(kernel = 'rbf')
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))
Ein SVM lässt sich über zwei Parameter tunen: C und gamma. C ist ein Regularisierungsparameter, der angibt, wie viel Fehler wir tolerieren. Je höher C, desto strenger wird das Modell – es versucht, die Daten möglichst exakt zu trennen, also mit kleineren Margen. Das erhöht aber auch das Risiko des Overfittings. Beim RBF-Kernel lässt sich zusätzlich gamma einstellen, das die Größe des Kernels – und damit die Entscheidungsgrenze – beeinflusst. Je höher gamma, desto näher liegt die Entscheidungsgrenze an einzelnen Trainingsbeispielen. Auch das kann Overfitting begünstigen.
4) Modell tunen
param_grid = [{'C': [0.01, 0.1, 1, 10, 100],
'gamma': [0.001, 0.01, 0.1, 1, 10],
'kernel': ['rbf', 'linear']}]
grid = GridSearchCV(svm.SVC(), param_grid, cv = 5, scoring = 'balanced_accuracy')
grid.fit(X_train, y_train)
print(grid.best_params_)
Laut Kreuzvalidierung ergibt sich, dass ein gamma von 0.1 und ein C von 0.01 in Kombination mit dem RBF-Kernel optimal sind. Also trainieren wir unser Modell damit.
5) Modellgüte bewerten
clf = grid.best_estimator_
y_pred = clf.predict(X_test)
print('Confusion Matrix:nn', confusion_matrix(y_test, y_pred))
print('nClassification Report:nn', classification_report(y_test, y_pred))
print('nTraining Set Accuracy: {:.2f}%'.format(clf.score(X_train, y_train)))
print('nTest Set Accuracy: {:.2f}%'.format(clf.score(X_test, y_test)))
conf_mat = confusion_matrix(y_test, y_pred)
sns.heatmap(conf_mat, square = True, annot = True, fmt = 'g',
cbar = False, cmap = 'viridis')
plt.xlabel('predicted')
plt.ylabel('observed')
plt.show()
In diesem Fall erreichen wir eine Trainingsgenauigkeit von 93 Prozent und eine Testgenauigkeit von 74 Prozent. Das deutet auf ein gewisses Overfitting hin. Um eine höhere Genauigkeit (oder besser: Sensitivität/Recall) zu erzielen, könnten wir mit verschiedenen Kernels oder Hyperparametern experimentieren. Aber das überlasse ich dir. Mit reticulate hast du jetzt ein Werkzeug, mit dem du das Beste aus R und Python kombinieren kannst.