Fortgeschrittene Programmierung in R

Data Science 1 - Programmieren & Visualisieren

Saskia Otto

Universität Hamburg, IMF

Wintersemester 2023/2024

Lernziele

Am Ende dieser VL- und Übungseinheit werden Sie

  • ein besseres Verständnis für die 3 Grundelemente Bedingungen, Schleifen und Funktionen haben.
  • die Fähigkeit besitzen, komplexe Bedingungen mit logischen Operatoren zu erstellen, um verschiedene Szenarien zu berücksichtigen.
  • ein grundlegendes Verständnis von Schleifenkonzepten haben, insbesondere von der Zählschleife ‘for’ für iterative Prozesse und Simulationen.
  • die Fähigkeit besitzen, Funktionen in R selbst zu definieren und aufzurufen.
  • ein besseres Verständnis für die Verwendung von Parametern und Argumenten in Funktionen haben.
  • Bedingungen, Schleifen und Funktionen zur Lösung von realen Problemen anwenden können.

DSB Cheatsheet

Eine Übersicht der wichtigsten Funktionen gibt es hier:

Dieses wie alle anderen DSB Cheatsheets gibt es auf Moodle und der GitHub Repository uham-bio/Cheatsheets.

Zur Erinnerung: Die 4 Grundelemente

von Objekt-orientierten Sprachen wie R, Python und Javascript

  • Variablen
  • Bedingungen
    • Eine Bedingung ist eine Abfrage; trifft diese zu, so wird das, was innerhalb der Bedingung ist, ausgeführt.
  • Schleifen
    • Eine Schleife enthält Code, der so lange ausgeführt wird, wie die Bedingung, die zur Schleife gehört, wahr ist.
  • Funktionen
    • Sammlung von Code, die jederzeit über den jeweiligen Funktionsnamen aufgerufen werden kann.

Bedingungen im Detail

Bedingungen | 3 Typen in R

Es können neben Operatoren auch Funktionen in der Abfrage verwendet werden, solange diese ein TRUE oder FALSE zurückgeben.

if.. Anweisung

if (Abfrage) {
   mache folgendes
}

x <- 10L
if ( is.integer(x) ) {
  print(
    "Yes, x is an integer!")
}
[1] "Yes, x is an integer!"
if ( is.double(x) ) {
  x*2
}

if..else.. Anweisung

if (Abfrage) {
   mache dies
} else {
   mache das
}

y <- 12.5
if (y < 6) {
  y <- 0
} else {
  y <- 1
}
y
[1] 1

ifelse() Funktion

ifelse (Abfrage, mache dies, mache das)

z <- 1:3
ifelse(z == 2, z^2, 0)
[1] 0 4 0

Wenn bei der Abfrage TRUE rauskommt, wird die (erste) Anweisung ausgeführt.

Bedingungen | Nützliche Operatoren für Abfragen

Vergleichs- und Verknüpfungsoperatoren geben immer einen logischen Wert (TRUE oder FALSE) zurück.

Anwendungsbeispiel 1

Verschachtelte if-else-Anweisung

# Beispieldatensatz mit Altersangaben
ages <- c(8, 25, 42, 60, 75, 30, 18, 5)

# Erstellung einer neuen Variable auf  
# Grundlage des Alters
age_group <- character(length(ages))

# Bedingung innerhalb einer Schleife
for (i in 1:length(ages)) {
  if (ages[i] < 18) {
    age_group[i] <- "Child"
  } else if (ages[i] >= 18 & ages[i] < 65) {
    age_group[i] <- "Adult"
  } else {
    age_group[i] <- "Senior"
  }
}

# Kombinieren beider Variablen
result <- data.frame(ages, age_group)
result
  ages age_group
1    8     Child
2   25     Adult
3   42     Adult
4   60     Adult
5   75    Senior
6   30     Adult
7   18     Adult
8    5     Child



  • Wenn ein Alter unter 18 Jahren liegt, wird die Person als “Child” eingestuft.
  • Liegt das Alter zwischen 18 (einschließlich) und 65 (ausschließlich), wird die Person als “Adult” eingestuft.
  • Liegt das Alter bei 65 Jahren oder darüber, wird die Person als “Senior” eingestuft.
  • Dieser Code erstellt eine neue Variable age_group, auf der Grundlage der Altersinformationen im Altersvektor.
  • Die Bedingung wird in einer Schleife Element-weise geprüft.
  • Der resultierende data frame, result, enthält die ursprünglichen Altersangaben und die entsprechenden Altersgruppen.

Diese Art von if-else-Logik wird häufig in der Datenanalyse verwendet und hilft bei der Kategorisierung von Daten in verschiedene Gruppen auf der Grundlage bestimmter Bedingungen.

Anwendungsbeispiel 2

Farbeinstellung bei Basisplots mit ifelse()

plot(x = iris$Sepal.Length, y = iris$Petal.Length, 
  col = ifelse(iris$Species == "setosa", "blue", "red"),
  pch = 20,  cex = 1, xlab = "Länge Kelchblatt", ylab = "Länge Bütenblatt", 
  main = "setosa vs. versicolor/virginica")

Nützliche dplyr Funktion: case_when()

  • Mit dieser Funktion können mehrere if-else-Anweisungen vektorisiert werden.
  • Jeder Fall wird nacheinander ausgewertet, und die erste Übereinstimmung für jedes Element bestimmt den entsprechenden Wert im Ausgabevektor.
  • Wenn keine Fälle übereinstimmen, wird die Standardeinstellung als letzte ‘else’-Anweisung verwendet.
starwars %>%
  select(name:mass, gender, species) %>%
  mutate(
    type = case_when(
      height > 200 | mass > 200 ~ "large", # if..
      species == "Droid" ~ "robot",  # else if..
      .default = "other"  # else ..
    )
  )
# A tibble: 87 × 6
   name               height  mass gender    species type 
   <chr>               <int> <dbl> <chr>     <chr>   <chr>
 1 Luke Skywalker        172    77 masculine Human   other
 2 C-3PO                 167    75 masculine Droid   robot
 3 R2-D2                  96    32 masculine Droid   robot
 4 Darth Vader           202   136 masculine Human   large
 5 Leia Organa           150    49 feminine  Human   other
 6 Owen Lars             178   120 masculine Human   other
 7 Beru Whitesun Lars    165    75 feminine  Human   other
 8 R5-D4                  97    32 masculine Droid   robot
 9 Biggs Darklighter     183    84 masculine Human   other
10 Obi-Wan Kenobi        182    77 masculine Human   other
# ℹ 77 more rows

Your turn …

01:30

Quiz 1-2 | Bedingungen

Q1

Der Körper einer Bedingung (wie auch Schleife und Funktion) wird immer in geschweifte Klammern gesetzt, damit er über mehrere Zeilen gehen kann.

In der Abfrage muss ein Operator stehen, welcher die Abfrage wahr macht, denn der Codeblock in der Bedingung wird ja ausgeführt. Richtig ist daher:

x <- 25
if (x <= 25) {
  print('Yep, this is correct')
}
[1] "Yep, this is correct"

Q2

Anhand der zweimaligen geschweiften Klammern wird deutlich, dass es sich um eine ‘if..else..’ Bedingung handelt.

An der Ausgabe 0 ist zu erkennen, dass der Alternativbefehl (in ‘else’) ausgeführt wurde. In die zweite Lücke muss daher ein Operator, bei der die Abfrage FALSE ergibt:

i <- 1
if (i != 1) {
  i + 1
} else {
  i - 1
}
[1] 0

Schleifen & Simulationen

Schleifen

Zur Erinnerung

Eine Schleife enthält Code, der so lange ausgeführt wird, wie die Bedingung, die zur Schleife gehört, wahr ist. Der Code kann also z.B. zehn Mal ausgeführt werden, das hat den Vorteil, dass der Code nicht zehn Mal geschrieben werden muss.

  • Schleifen gehören neben Bedingungen zu den Kontrollstrukturen → sie kontrollieren den Programmablauf, indem Programmteile nur unter gewissen Bedingungen ausgeführt oder wiederholt werden.
  • Der Codeblock an Anweisungen innerhalb einer Schleife wird auch ‘Schleifenrumpf’ oder ‘Schleifenkörper’ (im Englischen ‘loop body’) genannt.
  • Schleifen, deren Schleifenbedingung immer zur Fortsetzung führt oder die keine Schleifenbedingung haben, sind Endlosschleifen. Sie sollten vermieden werden!
  • Schleifen können beliebig verschachtelt werden.

Schleifentypen in R

  • Zählschleifen (for): führen eine bestimmte Anzahl an Wiederholungen aus, die durch einen Zähler oder einen Index kontrolliert wird. Dieser erhöht sich mit jeder Iteration.
  • Kopfgesteuerte/vorprüfende (while) vs. fußgesteuerte/nachprüfende Schleifen (repeat): basieren auf ein Einsetzen und einer Verifikation durch eine logische Bedingung. Die Bedingung wird zu Beginn oder zum Ende des Schleifenkonstrukts getestet.

for Schleife


for Schleife


for Schleife

Ausführung in R
for (i in c(2,4,7) ) {
  print(i)
}
[1] 2
[1] 4
[1] 7
i
[1] 7

for Schleife - Stile

Bestes Vorgehen für den Zähler

x <- c(2, 4, 7); y <- numeric(0)
Eine sichere Version: 1:length(x)
1:length(x)
[1] 1 2 3
for (i in 1:length(x)) {
   print(x[i])
}
[1] 2
[1] 4
[1] 7
1:length(y)
[1] 1 0
for (i in 1:length(y)) {
   print(y[i])
}
[1] NA
numeric(0)

1:length() iteriert zumindest einmal!

Besser: seq_along(x)
seq_along(x)
[1] 1 2 3
for (i in seq_along(x)) {
   print(x[i])
}
[1] 2
[1] 4
[1] 7
seq_along(y)
integer(0)
for (i in seq_along(y)) {
   print(y[i])
}

→ Ist der Zähler NULL, führt seq_along() keine Iteration aus!

Bestes Vorgehen für die Ergebnisausgabe

  • Bevor eine Schleife gestartet wird, sollte immer genug Platz für das Ergebnis bereitgestellt werden.
  • Wenn die for Schleife mit jeder Wiederholung mit z.B. c() wächst, wird die for Schleife sehr langsam werden:
Objekt wachsen lassen
grow_obj <- function(x){
  y <- numeric()
  for (i in 1:x) { 
    y <- c(y, i)
  }
}
Indexierung
# Better: Indexing
index_obj <- function(x){
  y <- numeric(length(x))
  for (i in 1:x){ 
    y[i] <- i
  }
}

Hier wird ein leerer Vektor mit der Länge des Zählers erstellt, bevor die Schleife startet.

Bestes Vorgehen für die Ergebnisausgabe

Testen wir die Geschwindigkeit der beiden Funktionen
microbenchmark::microbenchmark(unit = "ms", times = 5,
    grow_obj(500), grow_obj(5000), index_obj(500), index_obj(5000) )
Unit: milliseconds
            expr       min        lq       mean    median        uq       max
   grow_obj(500)  0.202089  0.202253  0.9555378  0.211929  0.240834  3.920584
  grow_obj(5000) 21.503762 21.881044 23.6789350 23.831906 24.227679 26.950284
  index_obj(500)  0.054530  0.055719  0.3091482  0.055965  0.060762  1.318765
 index_obj(5000)  0.475559  0.491098  0.4956162  0.492205  0.493189  0.526030
 neval cld
     5  a 
     5   b
     5  a 
     5  a 

In der Spalte mean ist zu sehen, dass die Variante mit der ‘Indexierung’ viel schneller ist als die mit wachsenden Objekten, besonders wenn viele Wiederholungen gemacht werden!

Anwendungsbeispiel

Einlesen vieler Dateien

Speichern der Dateinamen im Arbeitsverzeichnis
# Wir gehen davon aus, dass nur die zu importierenden Dateien im 
# Arbeitsverzeichnis sind
files <- dir()
files
 [1] "file_001.csv" "file_002.csv" "file_003.csv" "file_004.csv" "file_005.csv"
 [6] "file_006.csv" "file_007.csv" "file_008.csv" "file_009.csv" "file_010.csv"
Import aller 10 Dateien und Zusammenführung zu EINEM data frame
# Leere Liste für die importierten Datensätze
data_list <- vector("list", length = length(files)) 

# Schleife, bei der in jeder Iteration eine Datei importiert wird
for (i in seq_along(files)) {
  data_list[[i]] <- read.csv(files[i]) 
}

# Zusammenfügen der einzelnen data frames in der Liste
data_df <- dplyr::bind_rows(data_list)
str(data_df)
'data.frame':   200 obs. of  3 variables:
 $ station: chr  "A" "A" "A" "A" ...
 $ x      : int  40 24 23 12 46 48 34 35 36 41 ...
 $ y      : num  28.93 14.49 1.67 9.51 20.64 ...
data_df |> 
  group_by(station) |> 
  count()
# A tibble: 10 × 2
# Groups:   station [10]
   station     n
   <chr>   <int>
 1 A          20
 2 B          20
 3 C          20
 4 D          20
 5 E          20
 6 F          20
 7 G          20
 8 H          20
 9 I          20
10 J          20
Visualisierung der einzelnen Datensätze
ggplot(data_df, aes(x, y)) +
  geom_point() + 
  geom_smooth() +
  facet_wrap(~station, nrow = 2)

Your turn …

01:30

Quiz 3-4 | Schleifen

Q3

Anhand der Abfrage ist erkennbar, dass es sich um eine ‘while..’ Schleife handeln muss. Auch hier gilt wieder: der Schleifenblock gehört in geschweifte Klammern!

Und da eine ‘while..’ Schleife nur mehrere Iterationen durchgeht, wenn der Input in der Abfrage sich ändert, muss sich hier a im Schleifenkörper um den Wert erhöhen, den der Output sich erhöht (weil dieser a darstellt.)

a <- 3
while (a < 12) {
  print(a)
  a <- a + 4
}
[1] 3
[1] 7
[1] 11

Q4

Hier ist keine Abfrage jeweils zu sehen, sondern es wird ein Zählerindex definiert. Also muss es sich um die Zählschleife (‘for..’) handeln. Und zwar 2 ineinander verschachtelte. Wichtig ist hierbei, dass der Zähler unterschiedlich ist. Die erste Schleife hat meist den Index i, die nächste j, etc.).

Der innerste Schleifenkörper macht folgendes: es sollen die beiden Indizes zusammengefügt werden (mittels Funktion paste()) unter Verwendung des Trennzeichen ‘/’ . Damit dies auch wirklich in der Konsole sichtbar ist, braucht es aber die Funktion print(), welche als Input paste() enhält.

for (i in c(1,3)) {
  for (j in c(5,7)) {
    print(paste(i, j, sep = '/'))
  }
}
[1] "1/5"
[1] "1/7"
[1] "3/5"
[1] "3/7"

So, es ist Zeit für unsere erste Simulation!

Es gibt drei Möglichkeiten, das Verhalten eines mathematischen Modells zu untersuchen, sobald wir uns für die Gleichung (oder den Satz von Gleichungen) entschieden haben, aus denen das Modell besteht.

  1. Die Gleichungen analytisch zu lösen.
  2. Das Verhalten des Modells grafisch untersuchen.
  3. Die Durchführung von Computersimulationen, bei denen das Modell schrittweise auf eine computergenerierte Population angewendet wird.

So, es ist Zeit für unsere erste Simulation!

Eine Demonstration des logistischen Populations- wachstums

Das diskrete logistische Wachstum mit Dichte-Abhängigkeit lässt sich mit folgender Gleichung beschreiben:

\[N_{t+1}=N_t+rN_t\left(1-\frac{N_t}{K}\right)\]

  • \(N_{t+1}\) = Anzahl der Individuen zum neuen Zeitpunkt (neue Populationsgröße)
  • \(N_t\) = Individuenzahl zum vorherigen Zeitpunkt (alte Populationsgröße)
  • \(r\) = Differenz zwischen der Pro-Kopf Geburts- und Sterberate
  • \(K\) = Kapazitätsgrenze
  • \(\left(1-\frac{N_t}{K}\right)\) ist der Dichte-Abhängigkeits-Parameter, ansonsten wäre es ein exponentielles Wachstum.

Simulation | 1 Zeitschritt

Nun brauchen wir nur einen Startwert für \(N_t\) und Werte für die beiden Parameter \(r\) und \(K\) um \(N_{t+1}\) zu berechnen:

Nt <- 1
r <- 0.5
K <- 100

Nt1 <- Nt + r*Nt * (1 - (Nt/K))
Nt1
[1] 1.495

Aber was ist mit den nächsten Zeitschritten bzw. Generationen?

Simulation | Viele Zeitschritte

Wir brauchen eine Schleife

Schauen wir uns erstmal die nächsten 5 Zeitschritte an:

for (t in 1:5) { # 1:5 steht für 1,2,3,4,5
  Nt1 <- Nt + r*Nt * (1 - (Nt/K))
  print(Nt1)
  # neue popsize als alte speichern für nächste Iteration 
  Nt <- Nt1
}
[1] 1.495
[1] 2.231325
[1] 3.322093
[1] 4.927958
[1] 7.270514

Simulation | Visualisierung

Zeitliche Entwicklung

Nt <- 1 # neuer Start

# Grafik erstellen mit Startwert 
plot(0, Nt, 
  xlim = c(0, 50), # t max = 50 
  ylim = c(0, 120), # weil K=100
  col="black", pch = 16,
  xlab = "Generation", 
  ylab = "Population size")

for (t in 1:50) { 
  Nt1 <- Nt + r*Nt * (1 - (Nt/K))
  # neuen Wert in die Grafik fügen
  points(t, Nt1, 
    col="red", pch = 16)
  Nt <- Nt1
  # Verzögerung der Simulation:
  Sys.sleep(0.2)
}

Hier mit 50 Zeiteinheiten:

Your turn …

02:30

Quiz 5-6 | Simulationen

Q5: Ergebnisspeicherung

\[N_{t+1}=N_t+rN_t\left(1-\frac{N_t}{K}\right)\]

N0 <- 1
N <- numeric(20)
N[1] <- N0
r <- 0.5
K <- 100

for (t in 2:20) { 
  N[t] <- N[t-1] + r*N[t-1] * (1 - (N[t-1]/K))
}
N
 [1]  1.000000  1.495000  2.231325  3.322093  4.927958  7.270514 10.641469
 [8] 15.395999 21.908814 30.463241 41.054816 53.154734 65.604972 76.887397
[15] 85.772736 91.874293 95.607011 97.707014 98.827218 99.406732
plot(N)

Q6: Langfristige Entwicklung

set.seed(123)
Gen <- 200
N0 <- 10
N <- numeric(Gen)
N[1] <- N0
r <- 3
K <- 500

for (t in 2:Gen) { 
  N[t] <- N[t-1] + r*N[t-1] * (1 - (N[t-1]/K))
}
plot(N, type = 'ls')

Funktionen im Detail

Funktionen

Zur Erinnerung

Eine Funktion ist eine Sammlung von Code, die jederzeit über den jeweiligen Funktionsnamen aufgerufen werden kann. Sie können auch Parameter enthalten. Dies sind Werte, wie z.B. eine Zahl oder ein String. Diese Zahl kann von der Funktion wie eine Variable verwendet werden.

  • Helfen die Arbeit zu strukturieren und Daten zu
    • lesen
    • prozessieren
    • visualisieren
  • Unterteilen komplexe Probleme in kleine “einfache” Einheiten.

Komponenten von Funktionen


Beispiel ohne Argumente | Hallo Welt

Output immer gleich
sag_hallo <- function() {
  # Funktionskörper
  print("Hallo Welt")
}

sag_hallo()
[1] "Hallo Welt"
sag_hallo()
[1] "Hallo Welt"

Beispiel mit Argument

Output anpassbar
sag_hi <- function(name) {
  # Funktionskörper
  print(paste0("Hi ", name, "!"))
}
sag_hi("Anne")
[1] "Hi Anne!"
sag_hi("Jan")
[1] "Hi Jan!"
sag_hi()
Error in sag_hi() : argument "name" is missing, with no default

Beispiel mit Argument und Standardwert

Output anpassbar und keine Fehlermeldung
sag_moin <- function(name = "Moin") {
  # Funktionskörper
  print(paste0("Moin ", name, "!"))
}
sag_moin("Jan")
[1] "Moin Jan!"
sag_moin()
[1] "Moin Moin!"

Beispiel mit Bedingung

Lasst uns nun eine Funktion bauen, die anhand einer Bedingung überprüft, ob die übergebene Zahl ein gerade oder ungerade Zahl ist. Dazu ist der sog. Restwert- oder Modulo-Operator %% für die Division mit Rest nützlich:

Funktion mit 1 Argument
check_number <- function(x) {
  if (x %% 2 == 0)  { # d.h. x ist gerade weil kein Rest überbleibt
    print("x ist gerade")
  } else {
    print("x ist ungerade")
  }
}
check_number(5.98)
[1] "x ist ungerade"
check_number(4528)
[1] "x ist gerade"

Zur Info: Eine Ganzzahldivision muss nicht unbedingt glatt aufgehen, wie z.B. bei 8/3. In diesem Fall gibt es den Rest 2 (6 lässt sich durch 3 teilen, bleiben 2 übrig). Diesen Rest liefert der Restwert-Operator.

Funktion zur Berechnung der Schiefe | 1

\[\gamma_1 = \frac{\frac{1}{n}\sum\limits_{i=1}^{n} \left(x_{i} - \bar{x}\right)^{3}} {(\frac{1}{n}\sum\limits_{i=1}^{n} \left(x_{i} - \bar{x}\right)^{2})^{3/2}}\]

Von der Gleichung zum R Code
x <- iris$Sepal.Length

den <- sum((x-mean(x))^3)/length(x)
num <- (sum((x-mean(x))^2)/length(x))^(3/2)
gamma_1 <- den/num
gamma_1
[1] 0.3117531
Vom R Code zur Funktion
skewness <- function(x) {
  # Funktionskörper
  den <- sum((x-mean(x))^3)/length(x)
  num <- (sum((x-mean(x))^2)/length(x))^(3/2)
  gamma_1 <- den/num
  return(gamma_1)
}
skewness(iris$Sepal.Length)
[1] 0.3117531

Funktion zur Berechnung der Schiefe | 2

\[\gamma_1 = \frac{\frac{1}{n}\sum\limits_{i=1}^{n} \left(x_{i} - \bar{x}\right)^{3}} {(\frac{1}{n}\sum\limits_{i=1}^{n} \left(x_{i} - \bar{x}\right)^{2})^{3/2}}\]

Was machen wir, wenn der Vektor NAs enthält?

Wir führen ein weiteres Argument ein und bauen eine Bedingung in die Funktion!
skewness <- function(x, na.rm = TRUE) {
  if (na.rm == TRUE) { # Bedingung einbauen
    x <- x[!is.na(x)] 
  }
  den <- sum( (x - mean(x))^3) / length(x)
  num <- (sum( (x - mean(x))^2) / length(x))^(3/2)
  gamma_1 <- den/num
  return(gamma_1)
}
skewness(c(NA,3,7,0,1,NA,12,8,5))
[1] 0.3237934

Ausgabe von nur einem Wert bzw. Objekten pro Funktion

complex_function <- function(x) {
   out_mean <- mean(x)
   out_median <- median(x)
   out_min <- min(x)
   out_max <- max(x)
   result <- list(
     mean = out_mean, 
     median = out_median, 
     min = out_min, 
     max = out_max
    )
   return(result) 
}
complex_function(1:10)
$mean
[1] 5.5

$median
[1] 5.5

$min
[1] 1

$max
[1] 10

list() wird hier verwendet um Zwischenergebnisse zu einem Listenobjekt zu kombinieren, welches anschließend ausgegeben wird.

Your turn …

01:30

Quiz 7-8 | Funktionen

Q7

Die Funktion mit der eine Funktion definiert wird nennt sich function(). Um den Funktionskörper in einer nächsten Zeile anzeigen zu können und ggf. auch über mehrere Zeilen, braucht es die schweiften Klammern vorher und nachher.

stell_dich_vor <- function(name) {
  print(paste0('Hallo, mein Name ist', name, '!'))
}
stell_dich_vor('Max')
[1] "Hallo, mein Name istMax!"

Q8

Eine Funktion sollte nicht auf Skalare und andere Objekte zugreifen, die nicht innerhalb der Funktion definiert werden bzw. als Argument übergeben werden. Hier wird auf x und y zugegriffen, daher müssen diese als Argument definiert werden (function(x,y)). Das berechnete Produkt z wird explizit mit return(z) ausgegeben.

calc_product <- function(x, y) {
  z <- x*y
  return(z)
}
calc_product(10, 4)
[1] 40

Funktionale Programmierung

Von der Schleife zur funktionalen Programmierung

  • for Schleifen sind nicht so wichtig in R wie sie es in anderen Programmiersprachen sind, weil R eine funktionale Programmiersprache ist.
  • Es ist möglich for Schleifen in einer Funktion zusammenzuraffen und stattdessen die Funktion anzuwenden.

Von der Schleife zur funktionalen Programmierung | 2

‘*apply’ Funktionen und das ‘purrr’ Paket

  • Wie wir bereits gesehen haben, ist es sehr handlich eine Funktion an eine andere Funktion anzupassen/abzugeben; es reduziert das Potential für Fehler (viel weniger Code und weniger ‘copy & paste’) und es wird einfacher zu generalisieren.
  • Die ‘*apply’ Familie von Funktionen in ‘base’ R (apply(), lapply(), sapply(), vapply(), tapply(), mapply()) macht genau das: Diese Funktionen wenden eine ausgewählte oder selbst definierte Funktion mit einer oder mehreren optionalen Argumenten auf Listen, Vektoren, ‘data frames’, Matrizen oder Arrays an.
  • Die ‘map’ Familie von Funktionen wird durch das ‘tidyverse’ Paket ‘purrr’ zur Verfügung gestellt. Es agiert ähnlich, kann aber schneller sein (alle Funktionen sind in C++ geschrieben), ist konsistenter, sehr gut im ‘tidyverse’ Konzept integriert und ist einfacher zu lernen.
  • ‘purrr’ bietet zusätzlich viele weitere hilfreiche Funktionen zum Handling von Listen an. Für einen Überblick an verfügbaren Funktionen schauen Sie in das cheatsheet rein.

base R | ‘apply()’ Funktion 1



base R | ‘apply()’ Funktion 2

Statistiken pro Spalte
apply(X = df, MARGIN = 2, FUN = mean)
           x            y            z 
 0.190523876 -0.006471519  0.138796773 
apply(X = df, MARGIN = 2, FUN = median)
          x           y           z 
 0.35967550 -0.05496689  0.11438674 

Summe der 3 Spalten pro Zeile (Output als Vektor)
apply(X = df, MARGIN = 1, FUN = sum, simplify = TRUE)
 [1]  0.12799996  0.71241794 -0.06410025  0.16259230  0.26057783 -1.58409228
 [7]  0.69621551  0.03610525 -0.01471492  0.99366090  3.26856660 -0.32497088
[13]  0.10755072 -3.39786802  1.18089506  1.52047173 -0.77770169 -0.15961181
[19]  2.49096619  1.22202247
Summe der 3 Spalten pro Zeile (Output als Liste)
apply(X = df, MARGIN = 1, FUN = sum, simplify = FALSE)
[[1]]
[1] 0.128

[[2]]
[1] 0.7124179

[[3]]
[1] -0.06410025

[[4]]
[1] 0.1625923

[[5]]
[1] 0.2605778

[[6]]
[1] -1.584092

[[7]]
[1] 0.6962155

[[8]]
[1] 0.03610525

[[9]]
[1] -0.01471492

[[10]]
[1] 0.9936609

[[11]]
[1] 3.268567

[[12]]
[1] -0.3249709

[[13]]
[1] 0.1075507

[[14]]
[1] -3.397868

[[15]]
[1] 1.180895

[[16]]
[1] 1.520472

[[17]]
[1] -0.7777017

[[18]]
[1] -0.1596118

[[19]]
[1] 2.490966

[[20]]
[1] 1.222022

base R | ‘apply’ Familie 1

Diese Funktionen wenden eine Funktion auf einen Vektor oder eine Liste an:

lapply(X, FUN, ...) 
  # Output -> Liste

sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE) 
  # Output -> Vektor, Matrix

vapply(X, FUN, FUN.VALUE, ..., USE.NAMES = TRUE) 
  # wie sapply() mit definiertem Typ von Rückgabewert

replicate(n, expr, simplify = "array")
  # wrapper für sapply(), wird bei Simulationen genutzt

base R | ‘apply’ Familie 2

Beispiel für eine nützliche Anwendung von lapply()

Anzeige der einzigartigen Werte jeder Spalte in einem (heterogenen) Datensatz
lapply(iris, unique)
$Sepal.Length
 [1] 5.1 4.9 4.7 4.6 5.0 5.4 4.4 4.8 4.3 5.8 5.7 5.2 5.5 4.5 5.3 7.0 6.4 6.9 6.5
[20] 6.3 6.6 5.9 6.0 6.1 5.6 6.7 6.2 6.8 7.1 7.6 7.3 7.2 7.7 7.4 7.9

$Sepal.Width
 [1] 3.5 3.0 3.2 3.1 3.6 3.9 3.4 2.9 3.7 4.0 4.4 3.8 3.3 4.1 4.2 2.3 2.8 2.4 2.7
[20] 2.0 2.2 2.5 2.6

$Petal.Length
 [1] 1.4 1.3 1.5 1.7 1.6 1.1 1.2 1.0 1.9 4.7 4.5 4.9 4.0 4.6 3.3 3.9 3.5 4.2 3.6
[20] 4.4 4.1 4.8 4.3 5.0 3.8 3.7 5.1 3.0 6.0 5.9 5.6 5.8 6.6 6.3 6.1 5.3 5.5 6.7
[39] 6.9 5.7 6.4 5.4 5.2

$Petal.Width
 [1] 0.2 0.4 0.3 0.1 0.5 0.6 1.4 1.5 1.3 1.6 1.0 1.1 1.8 1.2 1.7 2.5 1.9 2.1 2.2
[20] 2.0 2.4 2.3

$Species
[1] setosa     versicolor virginica 
Levels: setosa versicolor virginica

Übungsaufgabe

Swirl-Lektionen zur Vertiefung

Kurs DS1-06-Fortgeschrittene R Programmierung

  • L01-Dateihandling aus R heraus
  • L02-Bedingungen
  • L03-Eigene Funktionen schreiben
  • L04-Schleifen und erste Simulationen
  • L05-FizzBuzz
  • L06-Funktionales Programmieren mit apply Funktionen

Wie fühlen Sie sich jetzt…?

Total konfus?

Dann schauen Sie doch mal hier nach:

Total gelangweilt?

Dann testen Sie doch Ihr Wissen in folgendem Abschlussquiz…

Abschlussquiz

Bei weiteren Fragen: saskia.otto(at)uni-hamburg.de

Creative Commons License
Diese Arbeit is lizenziert unter einer Creative Commons Attribution-ShareAlike 4.0 International License mit Ausnahme der entliehenen und mit Quellenangabe versehenen Abbildungen.