SKOPOS GROUP

Wir sind ein Teil der SKOPOS GROUP für zeitgemäße Marktforschung.

Eine breit aufgestellte Unternehmensgruppe, die alle zeitgemäßen Marktforschungs-Dienstleistungen unter einem Dach vereint. Digital und innovativ. Von national bis international. Von Kunden-Befragung über UX-Research bis Insight-Community. Von Mitarbeiter-Befragung über Mystery-Shopping bis Customer-Experience und Data Science decken wir alle relevanten Themen und Methoden ab.

Journal Nützliches Wissen aus der Welt der Data Science
Data Analytics

So schreibst Du Kommandozeilen-Tools mit R und optparse

Die meisten kommen mit R als erstes über das mächtige RStudio in Kontakt. Damit fällt das Schreiben von einfachen Analyse-Skripten leicht. Doch für die Nutzung von Analysen oder Modellen aus R in Produktivsystemen ist RStudio ungeeignet. Hier zeigen wir Dir, wie du mit wenig Aufwand aus deinem R-Skript ein Helferlein für die Kommandozeile machst.

Zusammenfassung:

  • Lerne den Unterschied zwischen RStudio und einem R-Skript kennen.
  • Mit optparse einfache Skripte für die Kommandozeile schreiben.
  • R-Modelle in Produktion nutzen.

Wo ist der Unterschied zwischen RStudio und einem R-Skript?

Wenn du Analysen mit R machst, wirst Du sehr wahrscheinlich regelmäßig RStudio verwenden. Zwar gibt es auch Alternativen, aber unter Data Scientists ist das eigentlich das Standard-Tool, um Analysen durchzuführen und Modelle zu entwickeln.

Abbildung 1: So sieht es aus, wenn wir Analysen in RStudio entwickeln.

Was Du in RStudio im Code-Editor (im Screenshot oben links) schreibst und abspeicherst, ist für sich bereits ein R-Skript. Also eine Datei, in der Deine gesamte Analyse geschrieben steht. In der Praxis wirst Du wahrscheinlich häufig immer wieder einzelne Abschnitte mittels „Run“ starten. In den meisten anderen Programmiersprachen hingegen werden solche Skripte einfach von oben nach unten „durchgearbeitet“ und Zeile für Zeile ausgeführt – in etwa so wie das, was R macht, wenn Du den „Source“-Button verwendest.

Nun kannst Du ein geschriebenes R-Skript aber auch ähnlich wie mit Python komplett durchlaufen lassen, so dass R jede Zeile „ausführt“, ohne dass Du das Skript in RStudio geöffnet hast. Genau dafür ist dann die Kommandozeile praktisch. Wenn Du dort im gleichen Ordner wie Dein Skript bist und

Ein R-Skript über die Kommandozeile ausführen:

Rscript my_script.R

in der Kommandozeile eingibst, wird Dein R-Skript von oben nach unten durchgearbeitet. Und auf diese Weise stehen Dir für die Implementierung deines Machine-Learning-Modells viele Einsatzmöglichkeiten zur Verfügung.

Was ist eigentlich eine Kommandozeile?

Abbildung 2: So kann das Terminal (oder die Kommandozeile) aussehen, wenn man ein R-Skript zur Vorhersage von Daten verwendet.

Vielleicht fragst Du Dich, was genau die Kommandozeile (auch Terminal genannt) ist. Je nachdem, welches Betriebssystem Du nutzt, kommst Du auf unterschiedliche Weisen mit der Kommandozeile in Berührung. Als Linux-Nutzer wirst Du es gewohnt sein, über die so genannte Shell zu arbeiten. Unter Windows („Kommandozeile“ oder „command line“) oder macOS („Terminal“ oder „zsh“) ist das seltener notwendig, aber eben auch möglich. So oder so: Über das Terminal kannst du mittels Texteingabe viele System-Befehle nutzen, Programme starten und Dich durch das Dateisystem bewegen. Sobald Du Dich von Deinem Desktop-Rechner in die Cloud bewegst, ist das Terminal oft die einzige Möglichkeit, Programme auf einem Server zu starten. Hier gibt es meistens keine grafische Oberfläche, wo Du RStudio nutzen könntest. Kurz gesagt: Sobald du R-Skripte unabhängig von Deinem eigenen Computer benutzen möchtest, solltest Du Dir die Frage stellen, wie Du Deine Analysen für die Arbeit mit der Kommandozeile vorbereitest.

So lassen sich Analysen mit Microservices in der Cloud automatisieren

Bei uns geht es häufig darum, Prozesse in denen Machine-Learning-Modelle zum Einsatz kommen, zu automatisieren. Dabei wollen wir nicht immer von Hand das dazugehörige R-Skript in RStudio durchlaufen lassen, sondern wie oben beschrieben das Skript als kleines Helferlein benutzen, das über die Kommandozeile ausgeführt werden kann. Wenn es darum geht, Analysen und Modelle im produktiven Einsatz zu haben, stellt sich meist die Frage nach Schnittstellen. Über solche Programmierschnittstellen, auch API (Application Programming Interface) genannt, können Daten analysiert und das Ergebnis anschließend abrufbar gemacht werden. Wenn es eine Vielzahl von solchen kleinen Tools gibt, die einen bestimmten Zweck erfüllen, spricht man auch von Microservices, die häufig auch auf virtuellen Computern in der Cloud laufen.

Um unsere eigenen Modelle für Vorhersagen und Textanalysen für uns und unsere Kunden nutzbar zu machen, haben wir eine Infrastruktur für solche Microservices entwickelt (getauft auf den Namen Carbon). Jedes unserer Modelle ist dann ein kleines Kommandozeilen-Tool, das über eine entsprechende API aufgerufen werden kann – egal, von welchem Rechner in unserem Netzwerk und weitestgehend unabhängig davon, ob die Ergebnisse dann in R, Python oder Alteryx weiterverwendet werden.

Anhand eines sehr einfachen Modells wollen wir Dir nun zeigen, wie man einfache Kommandozeilen-Tools mit R schreiben kann, die sich dann flexibel einsetzen lassen.

Ein Beispiel für ein Modell: Wie viel Sprit benötigt mein neues Auto?

Natürlich bedienen wir uns für dieses Beispiel eines recht einfachen Datensatzes, den wir in R als Beispieldatensatz direkt verfügbar haben: mtcars. In diesem Datensatz sind mehrere Fahrzeuge mit ihren Eigenschaften enthalten. In unserem kleinen Modell wollen wir den Verbrauch (Spalte mpg) auf Basis der Leistung (Spalte hp), der Anzahl der Zylinder (Spalte cyl) und der Anzahl der Gänge (Spalte gear) vorhersagen. Da uns die Unsicherheit in unserer Vorhersage interessiert, nutzen wir ein Bayes’sches Mixed Effects Modell, das wir mit brms berechnen werden. Am Ende haben wir ein trainiertes Vorhersagemodell (predictive model), mit dem wir auf Basis von Gewicht, Pferdestärken, Anzahl der Zylinder und Anzahl der Gänge den Spritverbrauch von Fahrzeugen vorhersagen können.

Das bisherige Skript, in dem wir unser Modell in RStudio entwickelt haben, sieht vielleicht so aus:

Das Modell-Training in R:

library(tidyverse)
library(brms)
library(performance)

bm_cars <- brm(
  # Die Modell-Formel, cyl und gear als random intercepts
  mpg ~ wt + hp + (1 | cyl) + (1 | gear),
  # Unsere verwendeten Daten
  data = mtcars,
  # Die Priors für unser Bayes-Modell
  prior = c(
    prior(normal(0, 50), class = Intercept),
    prior(normal(0, 10), class = b),
    prior(student_t(3, 0, 1), class = sd),
    prior(student_t(3, 0, 1), class = sigma)
  ),
  # Einstellungen für das MCMC-Sampling
  control = list(adapt_delta = .99),
  chains = 4,
  cores = 4
)

# Modell-Zusammenfassung ansehen
summary(bm_cars)
conditional_effects(bm_cars)
ranef(bm_cars)

# Posterior Predictive Checks
pp_check(bm_cars, type = "dens_overlay")

# Vorhersage-Fehler
model_performance(bm_cars)

# Modell für spätere Nutzung bei der Vorhersage
saveRDS(bm_cars, file = "./bm_cars.Rds")

Wenn Du bisher noch keine Erfahrung mit brms gemacht hast: Kein Problem. Im Wesentlichen bauen wir hier eine einfache lineare Regression. Zwei Dinge unterscheiden sich zum klassischen Ansatz mit lm: Wir haben Priors, eine Eigenschaft von Bayes-Modellen, und wir betrachten die Anzahl Zylinder und die Anzahl Gänge als Faktoren, die je Ausprägung eine eigene Konstante in der Modellformel haben. Wenn Du mehr darüber erfahren willst: Eine Einführung zu diesen Methoden findest Du zum Beispiel in diesem Vortrag. Mehrebenen-Modelle, oder auch Mixed-Effects-Modelle, werden wir in einem der nächsten Journal-Beiträge vertiefen, aber diese Seite von Michael Freeman stellt die Grundidee sehr anschaulich dar.

In der Praxis würden wir natürlich die Modellentwicklung systematischer angehen: Verschiedene Modelle mittels Fehlerstatistiken oder Information Criteria vergleichen, Kreuzvalidierung zur Absicherung unserer Vorhersagegenauigkeit nutzen und weitere Prädiktoren aufnehmen, um die Genauigkeit zu verbessern. In diesem Fall soll aber zunächst dieses einfache Beispiel genügen.

Abbildung 3: Die Vorhersagen unseres Modells verglichen mit den Daten. Für eine echte, praktische Anwendung würden wir das Modell noch weiter verbessern.

Am Ende unseres Skripts haben wir das Modell abgespeichert, das wir für neue Datenpunkte nutzen wollen, um den Spritverbrauch vorherzusagen. Für diese Vorhersage schreiben wir uns ein neues R-Skript, das wir flexibel einsetzen können.

Parameter: Möge die Macht mit der Kommandozeile sein

Die wahre Macht von Kommandozeilen-Tools („command line utilities“) wie unserem R-Skript kommt durch Parameter, d. h. Informationen, die wir beim Aufruf des Skripts mitgeben und auf die unser Skript flexibel reagieren kann. Diese Parameter werden einfach an den Aufruf angehängt, zum Beispiel:

Ein R-Skript mit Parametern in der Kommandozeile ausführen:

Rscript predict_mpg.R --wt=2.62 --hp=110 --cyl=6 --gear=4

In unserem Fall wollen wir alle Prädiktoren für unser Modell über Kommandozeilen-Parameter angeben, so dass wir die Informationen zu unserem neuen Auto – für das wir eine Vorhersage machen wollen – nicht in das R-Skript fest reinschreiben müssen.

Ein neues Skript für die Kommandozeile

In der letzten Zeile unseres bisherigen R-Skripts haben wir das Modell als .Rds-Datei in das aktuelle Arbeitsverzeichnis abgespeichert. Wenn Du unsicher bist, wo das ist: Mit getwd() wird Dir dies in R angezeigt. Das .Rds-Format ist ein Standard von R, um Objekte (wie unser Modell) zu speichern und später wieder laden zu können. Sehr praktisch, denn wir wollen nicht bei jeder Vorhersage das Modell aufs Neue .

Nun müssen wir in unserem neuen R-Skript also das Modell laden und die Informationen aus den Kommandozeilen-Parametern extrahieren. Es gibt verschiedene Pakete, um das zu tun. Aber das Paket optparse hat sich bei uns als das einfachste und flexibelste erwiesen. Am besten installierst Du es direkt in R oder RStudio mit:

Paket optparse installieren:

install.packages('optparse')

Das Paket liefert einige Funktionen, um die Kommandozeilen-Parameter auszuwerten. Das Herzstück ist dabei die Funktion make_option, die wie folgt verwendet werden kann:

Einen Kommandozeilenparameter hinzufügen:

make_option(c("--wt"),			# Wie sieht der Parameter in der Kommandozeile aus?
			type = "integer",	# Um was für ein Datentyp handelt es sich?
			dest = "wt",		# Wie heißt die Variable, in der der Parameter-Wert gespeichert werden soll?
			default = NA,		# Der Standardwert, wenn der Parameter nicht angegeben wird
			metavar = "WEIGHT",	# Ein Platzhalter-Wert für die Hilfe (siehe unten)
			action = "store",	# Was soll mit dem Parameter passieren? "store" bedeutet, dass der Wert in einer Variablen gespeichert wird
			help = "The car's weight (in 1000 lbs)."
								# Ein kurzer Text, der den Parameter beschreibt
			)

Diesen Block müssen wir für jeden Parameter, den wir nutzen möchten, wiederholen und dann über zwei weitere Funktionen zusammensetzen. Am Ende sieht der gesamte Block so aus:

Alle benötigten Parameter hinzufügen:

suppressPackageStartupMessages(library(optparse))

cli_options <- list(
  make_option(c("--wt"), type = "integer", dest = "wt",
              default = NA, metavar = "WEIGHT", action = "store",
              help = "The car's weight (in 1000 lbs)."),
  make_option(c("--hp"), type = "integer", dest = "hp",
              default = NA, metavar = "HORSEPOWER", action = "store",
              help = "The car's gross horsepower."),
  make_option(c("--cyl"), type = "integer", dest = "cyl",
              default = NA, metavar = "CYLINDERS", action = "store",
              help = "Number of cylinders."),
  make_option(c("--gear"), type = "integer", dest = "gear",
              default = NA, metavar = "GEARS", action = "store",
              help = "Number of forward gears.")
)
args <- parse_args(OptionParser(option_list = cli_options))

Wenn wir nun dieses R-Skript als predict_mpg.R abspeichern und mit Rscript über die Kommandozeile aufrufen, passiert erstmal nicht sehr viel – aber optparse legt automatisch den Parameter -h an, mit dem es eine kurze Hilfe gibt:

Automatische Hilfe in der Kommandozeile:

❯ Rscript predict_mpg.R -h
Usage: predict_mpg.R [options]


Options:
        --wt=WEIGHT
                The car's weight (in 1000 lbs).

        --hp=HORSEPOWER
                The car's gross horsepower.

        --cyl=CYLINDERS
                Number of cylinders.

        --gear=GEARS
                Number of forward gears.

        -h, --help
                Show this help message and exit

Das ist sehr praktisch, damit andere Nutzer wissen, was sie mit diesem Skript machen können und orientiert sich an den Konventionen für Kommandozeilen-Programme aus der Linux-Welt.

Bevor wir zur eigentlichen Logik kommen, sollten wir noch sicherstellen, dass der Nutzer unseres kleinen Skripts alle wichtigen Parameter angegeben hat. optparse speichert alle Werte in einer Liste, die wir über die oben zugewiesene Variable args abfragen können. So können wir sehr einfach alle Parameter überprüfen und bei Bedarf das Skript mit quit() abbrechen:

Parameter-Angaben überprüfen:

suppressPackageStartupMessages(library(cli))

# Angegebene Parameter überprüfen
if (sum(is.na(args)) > 0) {
  cli_alert_danger("Alle Angaben zum Fahrzeug müssen angegeben werden.")
  quit()
}

Das Paket cli, das bei R immer dabei ist, stellt einige praktische Funktionen bereit, mit denen man Warnhinweise schön aufbereitet darstellen kann. Das sieht dann zum Beispiel so aus:

Wenn die Prüfung fehlschlägt:

❯ Rscript predict_mpg.R
x Alle Angaben zum Fahrzeug müssen angegeben werden.

Die eigentliche Logik des Skripts besteht aus:

  • Modell laden
  • Ein data.frame anlegen, das die Daten enthält, die mittels Kommandozeilen-Parameter angegeben wurden
  • Modell für Vorhersagen nutzen
  • Vorhersagen und Unsicherheit ausgeben

Im Skript sieht das dann so aus:

Vorhersagen mittels unseres Modells generieren:

suppressPackageStartupMessages(library(tidyverse))
suppressPackageStartupMessages(library(brms))

new_df <- tibble(wt = args$wt,
                 hp = args$hp,
                 cyl = args$cyl,
                 gear = args$gear)
bm_cars <- readRDS("bm_cars.Rds")

df_predictions <- predictive_interval(bm_cars, newdata = new_df, prob = .8)
new_df$pred <- round(predict(bm_cars, newdata = new_df)[,1])
new_df$pred_lower <- round(df_predictions[,1])
new_df$pred_upper <- round(df_predictions[,2])

print(new_df)

Wenn wir nun alle Teile zusammenwerfen und im Skript speichern (das fertige Skript findest Du auch hier), können wir für unsere neueste Ergänzung im Fuhrpark, ein Mazda RX4 Wagon, den Spritverbrauch mit unserem Tool vorhersagen:

Das fertige Skript in der Kommandozeile mit Vorhersagen:

❯ Rscript predict_mpg.R --wt=2.875 --hp=110 --cyl=6 --gear=4
# A tibble: 1 x 7
     wt    hp   cyl  gear mpg_predict mpg_predict_lower mpg_predict_upper
  <int> <int> <int> <int>       <dbl>             <dbl>             <dbl>
1     2   110     6     4        24.8              21.3              28.4

Unser Modell macht also eine Vorhersage von etwa 24,8 Meilen pro Gallon mit einem 80 % Vorhersage-Intervall von 21,3 bis 28,4. Das ist nah am tatsächlichen Wert, der in mtcars mit 21,0 angegeben bist.

Yippieh! Das Kommandozeilen-Tool ist damit fertig für den Einsatz als KI-Lösung in unserem Kubernetes-Cluster! Oder einfach nur als Spielerei für zwischendurch …

5 Tipps für gute Kommandozeilen-Skripte

Worauf solltest Du achten, wenn Du ein Kommandozeilen-Skript entwickeln möchtest? Ein paar Tipps, die Dir dabei helfen können:

  1. Schreibe eine nützliche Hilfe zu Deinen Parametern!
    Wie Du gesehen hast, legt optparse automatisch eine kleine Hilfe an, die Deine Parameter erläutert. Das ist sehr hilfreich, wenn Deine Nutzer das Skript weiterverwenden wollen. Eine kurze, prägnante Erläuterung hilft enorm und erklärt jedem Nutzer, was Du mit deinen Parametern vorhast.
  2. Überprüfe alle Parameter, bevor Du sie verwendest!
    Wir haben in unserem obigen Beispiel nur überprüft, dass überhaupt etwas für die Parameter angegeben wurde – und wir für die Vorhersage keine Missings haben. Besser wäre noch gewesen, wenn wir auch bspw. den Wertebereich der Parameter überprüft hätten: Wenn man für die Parameter nun absurde Werte eingibt, dann bekommt man auch absurde Vorhersagen. Garbage in, garbage out. Mit ein paar weiteren if’s kann man mögliche Verwirrung (auch bei Tippfehlern) verhindern.
  3. Gib dem Nutzer Rückmeldungen!
    Wenn Nutzer Dein Kommandozeilen-Skript benutzen sollen, wollen sie wissen, was passiert. Vor allem bei Schritten, die etwas mehr Zeit in Anspruch nehmen (z. B. Vorhersagen für viele Daten oder das Training größerer Modelle). Wenn Du kurze Status-Infos ausgibst, wird transparenter, was im Hintergrund passiert. In unserem Beispiel oben haben wir die Funktion cli_alert_danger() dafür verwendet. Das Paket cli hat noch einige weitere nützliche Funktionen, um ansehnlichen Output anzuzeigen. Ansonsten hilft manchmal auch ein einfaches print(„Hallo!“).
  4. Achte auf die Pfad-Angaben!
    Eine Herausforderung in R ist ganz sicher die Arbeit mit relativen Pfad-Angaben, wie z. B. ./bm_cars.Rds in unserem obigen Beispiel. Ein solcher relativer Pfad wird von R immer ausgehend vom aktuellen Arbeitsverzeichnis interpretiert. Du kannst Dich in Deinem Skript aber nicht darauf verlassen, dass das immer das Verzeichnis ist, in dem Dein Kommandozeilen-Skript liegt. Besser wäre es daher, wenn auch der Pfad zum Modell über ein Kommandozeilen-Parameter angegeben wird. So liegt es in der Verantwortung des Nutzers, den richtigen Pfad zum Modell anzugeben. Alternativ ist das Paket here sehr nützlich, um relative Pfade etwas zuverlässiger nutzen zu können.
  5. Mach Dein Tool mit Docker reproduzierbar!
    Wenn Du ein R-Skript entwickelst, nutzt Du von allen Paketen bestimmte Versionen – nämlich die, die bei Dir zurzeit installiert sind. Bei Deinem Nutzer können aber andere Pakete und andere Versionen installiert sein – dann kann es schnell passieren, dass Dein Skript erstmal nicht funktioniert, bis die notwendigen Pakete in der richtigen Version installiert sind. Eine Möglichkeit, sicherzustellen, dass Dein Tool immer in der gleichen Umgebung läuft, ist Docker. Damit kannst Du einen kleinen virtuellen Computer anlegen, in dem Dein R-Skript lebt – damit ist sichergestellt, dass immer die gleiche R-Version, die gleichen Pakete und die richtigen Versionen bei der Ausführung verwendet werden. Für die Implementierung von Machine-Learning-Modellen in der Praxis, ist Docker in Verbindung mit Kubernetes der absolute Standard, um die Implementierung skalierbar zu machen.

Du willst mit R und Python business-relevante Fragestellungen beantworten?

Dann lass uns gemeinsam ein Analyse-Konzept erarbeiten!

Warum wir?
Deine Anfrage

Unser hochmotiviertes Team freut sich, von Dir und Deiner Fragestellung zu hören.

  • Hidden
  • Hidden

Mehr lesen?