R-Funktionen sind für die R-Experten ein alter Hut, aber für die Newbies unter Euch ein wichtiges Werkzeug. Ja, Funktionen sind eigentlich elementar in der R-Programmierung, schließlich handelt es sich bei R um eine funktionale Programmiersprache.

Warum soll man überhaupt eigene R-Funktionen programmieren?

Es gibt direkt mehrere Vorteile, warum Funktionen so nützlich beim Programmieren in R sind. Funktionen werden eigentlich immer dann eingesetzt, wenn man gleichen oder ähnlichen Programmcode an mehreren Stellen im Skript benutzt. Statt viele Zeilen identischen Codes zu haben, wird dieser Code in eine Funktion ausgelagert, so dass dann nur noch die Funktion aufgerufen werden muss.

Damit verringern wir nicht nur die Zeilen Code und machen das Ganze übersichtlicher, sondern es sinkt auch die Fehlerwahrscheinlichkeit, denn es gibt nur noch eine Stelle, an der Änderungen oder Bugfixes gemacht werden müssen.

Funktionen erhöhen die Übersichtlichkeit des R-Codes, insbesondere, wenn sie verständlich benannt sind. Statt sich durch x Zeilen Code zu kämpfen, steht dann im Hauptteil einer Datenanalyse vielleicht nur noch folgende paar Zeilen.

datum_von <- "01.01.2020"
datum_bis <- "30.09.2020"
daten <- daten_einlesen(datum_von, datum_bis)
ergebnisse <- daten_analysieren(daten)
daten_visualieren(ergebnisse, speichern = TRUE)

Damit ist für jeden klar, was in diesem Skript gemacht wird. Natürlich ist der vermutlich komplexe Code in den Funktionen versteckt und muss bei Fehlern genauso analysiert werden, aber die Vorteile sind klar. Und da haben wir noch gar nicht von Tools wie automatischen Unit Tests gesprochen.

Und durch die Übergabe von Parametern sind Funktionen total flexibel. Besonders komfortabel wird es, wenn ihr Standard-Werte für die Parameter definiert, so dass die Parameter nicht unbedingt angegeben werden müssen.

Ihr könnt euch auch eure nützlichsten Funktionen in ein R-Skript packen oder sogar ein eigenes R-Package bauen. Das wird dann am Anfang eines Skripts eingelesen (siehe xyz) bzw. eingebunden und schon stehen sie euch zur Verfügung.

 

Wie definiere ich eine Funktion in R?

Eine eigene Funktion zu definieren ist ganz einfach

meine_funktion <- function() {
    #hier kommt der Code rein
    print("meine_funktion wurde aufgerufen")
}

Der Aufruf erfolgt dann mittels meine_funktion(). Das ist natürlich eine ziemlich langweilige Funktion, denn sie hat weder Parameter noch wird etwas zurückgegeben. Von dem auszuführenden Code ganz zu schweigen 😉

Die Rückgabe von einer Variable v erfolgt mittels return(v). Dabei kann die Variable ein beliebiges R-Objekt sein, also nicht nur ein Wert, sondern auch Listen, data.frames oder sogar Funktionen. Code nach einem return-Befehl wird nicht mehr ausgeführt, sondern es direkt wird zum Ende gesprungen. Das ermöglicht es zum Beispiel, am Anfang Fehler in den Parametern abzufangen.

Nehmen wir als Beispiel die Überprüfung, ob eine Zahl eine ganze Zahl ist. Das geht leider nicht mit is.integer, denn diese Funktion überprüft nicht den Wert, sondern nur, ob es sich schon um den Typ integer handelt. Daher machen wir es so, dass wir von der gegebenen Zahl die Nachkommastellen runden (mittels round) und uns dann die Differenz zu der ursprünglichen Zahl anschauen. Ist diese 0 bzw. sehr klein, dann ist durch das Runden nichts passiert. Der Ausdruck stimmt dann.

Für den Toleranzparameter tol geben wir einen Default-Wert an, so dass dieser nicht beim Aufruf übergeben werden muss. Warum brauchen wir so einen Toleranzparameter überhaupt? Das hängt mit Rundungsungenauigkeiten zusammen. Ist x als 2/49*49 definiert, dann stimmt der direkte Vergleich zum Beispiel nicht mehr.

is.integer(3)
is.integer(3L)
 
x = 2/49*49
round(x)==x
 
istGanzeZahl <- function(zahl, tol = 0.000000001) {
    return(round(zahl)-zahl < tol)
}
 
istGanzeZahl(3)
istGanzeZahl(3.5)

Wie oben geschrieben, beendet R beim Schlüsselwort return direkt die Funktion. Das ist ganz praktisch, um Abfragen bezüglich fehlerhafter Parameter am Anfang abzufragen. Hier ein Beispiel dazu, dass die obige Funktion istGanzeZahl nutzt.

wiederhole <- function(wort, anzahl = 1) {
  if (!is.character(wort)) {
    print("Bitte gib als ersten Parameter eine Zeichenkette ein")
    return()
  }
  if (anzahl<1) {
    print("Bitte gib eine Anzahl größer oder gleich 1 ein")
    return()
  }
  if (!istGanzeZahl(anzahl)) {
    print(paste("Die Anzahl",anzahl,"ist keine ganze Zahl"))
    return()
  }
  return(rep(wort, anzahl))
}
 
wiederhole("Hallo")
wiederhole("Hallo",3)
wiederhole("Hallo",0)
wiederhole("Hallo",-3)
wiederhole("Hallo",3.5)

Zwei Parameter in einer R-Funktion zurückgeben

Eine Funktion in R kann immer nur ein Objekt zurückgeben. Um mehrere Variablen zurückzugeben, müssen wir diese also zu einem Objekt verbinden. Und das geht am besten mit einer Liste.

Wenn ihr zum Beispiel schon mal eine lineare Regression in R gerechnet habt, kennt ihr das schon.

n <- 100
x <- rnorm(n, 3, 4)
y <- 3 * x + rnorm(n, 0, 0.5)
l <- lm(y ~ x)

Die Ausgabe von l (also der print-Befehl) sieht zwar nicht unbedingt nach einer Liste aus, davon sollte man sich aber nicht täuschen lassen.

> print(l)
 
Call:
lm(formula = y ~ x)
 
Coefficients:
(Intercept)            x
-0.05468      3.00677

Wenn wir uns das Objekt l aber im Environment-Fenster von RStudio ansehen, dann steht dort Liste mit 12 Einträgen.

RStudio Environment: Klasse lm ist eine Liste

Das können wir auch leicht mit is.list(l) überprüfen. Mit names(l) bekommen wir die Namen der Elemente, auf die wir dann mit der eckigen Doppelklammer oder dem Dollarzeichen zugreifen können.

is.list(l)
names(l)
l[["coefficients"]]
l$coefficients

 

Warum sieht die Ausgabe mittels print dann so gar nicht nach Liste aus? Das liegt aber daran, dass die Programmierer von lm der Ausgabe die Klasse „lm“ gegeben hat, welche eine eigene print-Funktion besitzt und die Standard-Print-Funktion der Liste überschreibt. Abfragen kann man den Typ eines Objekts übrigens mittels class.

Wie gibt man mehrere Parameter in einer Funktion zurück?

Dazu bauen wir uns einfach in der Funktion eine Liste und geben diese mittels return zurück. Die folgende Funktion erzeugt einen data.frame und berechnet dann dazu eine lineare Regression. Zurückgegeben wird sowohl der data.frame als auch das lm-Objekt, also eine Liste bestehend aus data.frame und wiederum einer Liste (bzw. ein Objekt der Klasse lm).

erzeuge_lm <- function(n = 100, seed = NA) {
  if (!is.na(seed)) set.seed(seed)
  df <- data.frame(x = rnorm(n, 3, 4))
  df$y <- 3 * x + rnorm(n, 0, 0.5)
  l <- lm(y ~ x, df)
  liste <- list(daten = df, lm = l)
  return(liste)
}
 
a <- erzeuge_lm()
a$daten
a$lm

Der Phantasie sind hierbei kaum Grenzen gesetzt. Wir könnten jetzt noch sämtliche verwendeten Parameter als Parameter übergeben. Oder per Parameter steuern, aus welcher Verteilung die Daten stammen. Das führt uns zum nächsten Kapitel, denn Verteilungen haben unterschiedlich viele Parameter. Die t-Verteilung hat nur einen Freiheitsgrad, die Normalverteilung hat zwei.

Beliebige Anzahl Parameter in R-Funktionen mittels …

Um die Parameter bei der Funktionsdefinition nicht explizit anzugeben, kann man auch … verwenden. Das ist praktisch, wenn man in der Funktion wieder eine Funktion aufruft und ihr diese Parameter übergeben will.

Greifen wir das letzte Beispiel nochmal auf und definieren uns eine Funktion, die als Parameter die Stichprobengröße und den Namen der Verteilung bekommt und anschließend folgen die Parameter für die Verteilung. Also bei der Normalverteilung Mittelwert und Standardabweichung, bei der t- oder χ²-Verteilung die Freiheitsgrade.

erzeuge_daten <- function(n = 100, verteilung = "normal", ...) {
  if (verteilung == "normal") x = rnorm(n, ...)
  else if (verteilung == "t") x = rt(n, ...)
  else if (verteilung == "chi2") x = rchisq(n, ...)
  else {
    print("Aktuell werden nur Normal-, t- und Chi²-Verteilung unterstützt")
    return()
  }
  return(x)
}
 
erzeuge_daten(n = 10, verteilung = "normal", 3, 1)
erzeuge_daten(n = 10, verteilung = "normal", 3)
erzeuge_daten(n = 10, verteilung = "t")

Wollen wir ein bisschen mehr Kontrolle haben, wann welcher Parameter eingesetzt wird, können wir das mit zwei Punkten und der Position angeben, also ..1 für den ersten Parameter, ..2 für den zweiten Parameter usw.

erzeuge_daten <- function(n = 100, verteilung = "normal", ...) {
  if (verteilung == "normal") x = rnorm(n, ..1, ..2)
  else if (verteilung == "t") x = rt(n, ..1)
  else if (verteilung == "chi2") x = rchisq(n, ..1)
  else {
    print("Aktuell werden nur Normal-, t- und Chi²-Verteilung unterstützt")
    return()
  }
  return(x)
}
erzeuge_daten(n = 10, verteilung = "normal", 3, 1)
erzeuge_daten(n = 10, verteilung = "normal", 3)
erzeuge_daten(n = 10, verteilung = "t")

Machen wir das so, sehen wir beim Aufruf, dass die Default-Werte kaputt gegangen sind, d.h. für die Normalverteilung muss man nun zwei Parameter angeben.

Achtung: Der Einsatz von … in R-Funktionen sollte mit Bedacht gewählt sein. Denn nicht oder falsch benannte Parameter erzeugen keine Fehlermeldung.

Wir können natürlich fordern, dass die zusätzlichen Parameter benannt sind. Man muss sich allerdings selber darum kümmern und das artet dann in einige Zeilen Code mit unschönen if-Bedingungen aus. Die Länge bzw. Namen der Parameter bekommt man , indem man … in eine Liste umwandelt, wie das im folgenden Beispiel in der ersten Zeile der Funktion passiert.

erzeuge_daten <- function(n = 100, verteilung = "normal", ...) {
  args = list(...)
  if (verteilung == "normal") {
    if (length(args)==0 | 
        (length(args) <= 2  & ("mean" %in% names(args) | "sd" %in% names(args)))) {
      x = rnorm(n, ...)
    }
    else {
      print("Für die Normalverteilung gibt es die optionalen Parameter mean und sd")
      return()
    }    
  }
  else if (verteilung == "t") {
    if (length(args) == 1  & "df" %in% names(args)) {
      x = rt(n, ...)
    }
    else {
      print("Für die t-Verteilung muss der Parameter df angegeben werden")
      return()
    }  
  }
  else if (verteilung == "chi2") {
    if (length(args) == 1  & "df" %in% names(args)) {
      x = rchisq(n, ...)
    }
    else {
      print("Für die Chi²-Verteilung muss der Parameter df angegeben werden")
      return()
    }  
  }
  else {
    print("Aktuell werden nur Normal-, t- und Chi²-Verteilung unterstützt")
    return()
  }
  return(x)
}
erzeuge_daten(n = 10, verteilung = "normal", 3, 1)
erzeuge_daten(n = 10, verteilung = "normal", mean = 3)
erzeuge_daten(n = 10, verteilung = "t", df = 4)

Noch da? Bei dem ganzen Aufwand ist schon die Frage, ob man nicht einfach die Parameter explizit angibt. Dabei Default-Werte für die Parameter vergeben, dann müssen nur die benötigten angegeben werden.

erzeuge_daten <- function(n = 100, verteilung = "normal", mean = 0, sd = 1, df = 3) {
  if (verteilung == "normal") x = rnorm(n, mean, sd)
  else if (verteilung == "t") x = rt(n, df)
  else if (verteilung == "chi2") x = rchisq(n, df)
  else {
    print("Aktuell werden nur Normal-, t- und Chi²-Verteilung unterstützt")
    return()
  }
  return(x)
}

Oder ihr macht es ganz anders und gestaltet die Funktion ganz allgemein, indem man ihr als ersten Parameter die Zufalls-Erzeugungsfunktion (ist das ein Wort?) übergibt. Dann ist es nämlich nur ein Einzeiler:

erzeuge_daten <- function(zufallsfunktion, ...) {
  return(do.call(zufallsfunktion, list(...)))
}
erzeuge_daten(rnorm, n=10, mean = 3)

Rekursive Funktionen in R

Als rekursiv bezeichnen wir eine Funktion, wenn sie sich selbst wieder aufruft. Dabei sollte natürlich irgendein Parameter gesetzt werden, so dass wir nicht in einer Endlos-Schleife gefangen sind.

Dieses Konstrukt ist ganz elegant (obwohl nicht immer mit der besten Performance), wenn man mathematische Folgen nachbauen möchte, die selber schon rekursiv sind.

Ein Beispiel ist die Fakultätsfunktion n!, die alle natürlichen Zahlen bis zu n miteinander multipliziert. Würde man (n-1)! kennen, müsste man es nur noch mit n multiplizieren und hätte das Ergebnis

n! = 1 * 2 * … * n = (n-1)! * n

Zusätzlich ist 0! = 1 definiert. Die Umsetzung in R ist kein großes Problem.

fac <- function(n) {
  if(n == 0 | n ==1) return(1)
  else return(fac(n-1))
}

Ein weiteres beliebtes Beispiel sind die Fibonacci-Zahlen. Die n-te Fibonacci-Zahl ist definiert als die Summe der beiden Vorgänger. Zudem sind die ersten beiden Fibonacci-Zahlen als 1 definiert.

fibo <- function(n) {
  if(n == 1 | n == 2) return(1)
  else return(fibo(n-1) + fibo(n-2))
}

Hier sehen wir schon, dass das zwar elegant, aber nicht sonderlich effizient ist, denn die beiden Vorgänger werden einzeln berechnet. In der Praxis sind rekursive Funktionen auch eher die Ausnahme.

Operatoren in R definieren

Ok, jetzt habt ihr es bis hierhin geschafft. Dann gibt es als Belohnung noch eine Möglichkeit, Funktionen zu definieren. Und zwar kennt ihr vermutlich Operatoren, die zwischen zwei Objekten stehen. Also zum Beispiel 5 %in% 1:5, wobei geprüft wird, ob der erste Wert in dem zweiten Vektor enthalten ist. Solche Operatoren können wir auch selber definieren. Vielleicht nervt euch ja auch das Verbinden von Zeichenketten mittels paste. Wir könnten ja einen Operator definieren, der das für uns erledigt. Das geht ganz einfach, wenn man weiß wie. Dafür muss nämlich der Operator zum einen mit % anfangen und enden, zum anderen muss er bei der Definition in schräge Hochkommata eingepackt werden. Unser String-Verkettungs-Operator sieht dann folgendermaßen aus:

`%+%` <- function(str1, str2) {
  return(paste0(str1, str2))
}
"Hans" %+% "wurst" %+% " ist da!"

Aber Achtung: Viele eigene Operatoren können das Nachvollziehen für jemand anderen erschweren, da man sich quasi eine ganz eigene Syntax bastelt.

Ein R-Skript mit nützlichen Funktionen einbinden

Habt ihr eine Sammlung eigener Funktionen angelegt, von denen ihr die eine oder andere in fast jedem Skript braucht. Dann habt ihr zwei Möglichkeiten, diese einzulesen. Am einfachsten geht es mit dem source-Befehl. Dazu einfach am Anfang eines Skripts source(„Meine_Funktionen.R“) schreiben und fertig!

Ihr könnt natürlich auch direkt ein Package daraus machen. Das ist auch nicht so schwer, wie es sich anhört, aber das heben wir uns für einen anderen Artikel auf.

Happy functioning,
Euer Holger