Sprachporträt · Statistik & Datenanalyse

R von Grund
auf

Eine Sprache, die nicht aus der Informatik kommt, sondern aus der Statistik — und deren gesamte Bauweise sich erklärt, sobald man das ernst nimmt. Von der Zuweisung bis zum eigenen Paket.

R 4.x — Konsole
> werte <- c(12.4, 9.8, 15.1, 11.2)
> mean(werte)
[1] 12.125
> werte * 2          # keine Schleife nötig
[1] 24.8 19.6 30.2 22.4
> summary(werte)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  9.800  10.850  11.800  12.125  13.075  15.100
01

Was R ist — und woher es kommt

Warum eine Sprache aus dem Statistiklabor anders aussieht als eine aus der Systemprogrammierung.

R ist eine freie Programmiersprache und Laufzeitumgebung für statistische Berechnungen und Grafik. Sie ist interpretiert, dynamisch typisiert, im Kern funktional und — das ist ihr eigentliches Merkmal — durchgehend vektorisiert: Die natürliche Einheit ist nicht der einzelne Wert, sondern der Vektor. Es gibt in R keinen Skalar; die Zahl 5 ist ein numerischer Vektor der Länge 1.

Herkunft

R ist eine Neuimplementierung von S, das John Chambers ab 1976 in den Bell Labs entwickelte — als Sprache, in der Statistiker ihre Analysen selbst formulieren konnten, statt sie in Fortran zu bestellen. Ross Ihaka und Robert Gentleman begannen Anfang der 1990er an der Universität Auckland eine eigene Umsetzung, kombinierten die S-Syntax mit der Semantik von Scheme (lexikalisches Scoping, Funktionen als Werte) und veröffentlichten sie 1993. Der Name spielt auf die Anfangsbuchstaben der beiden an und auf S selbst. Seit 1995 steht R unter der GPL, seit 1997 gibt es das Paketarchiv CRAN und ein R Core Team, Version 1.0.0 erschien am 29. Februar 2000. Heute läuft die 4.x-Reihe.

Was daraus folgt

Fast jede Eigenheit von R, die Umsteigern zunächst seltsam vorkommt, ist eine Konsequenz dieser Herkunft:

  • Indizierung beginnt bei 1, nicht bei 0 — wie in der mathematischen Notation, in der Statistiker schreiben.
  • Fehlende Werte sind erstklassig. NA ist ein eigener Wert in jedem Datentyp, und praktisch jede eingebaute Funktion hat eine Meinung dazu (na.rm). Reale Datensätze haben Lücken; die Sprache tut nicht so, als gäbe es sie nicht.
  • Der data.frame ist eingebaut, nicht nachgerüstet: eine Tabelle mit benannten Spalten unterschiedlichen Typs, also genau die Struktur eines Erhebungsdatensatzes.
  • Formeln sind Sprachbestandteil. y ~ x + z ist ein gültiger R-Ausdruck — ein Modell lässt sich hinschreiben wie im Lehrbuch.
  • Grafik ist kein Zusatz. Ein Grafiksystem gehört zum Sprachumfang, weil eine Analyse ohne Abbildung unvollständig ist.
  • Ausdrücke sind Daten. R kann unausgewertete Ausdrücke herumreichen (Non-Standard Evaluation) — die Grundlage dafür, dass plot(hoehe, gewicht) die Achsen automatisch beschriften kann.

Wofür R heute eingesetzt wird

Statistik und Ökonometrie, klinische Forschung und Pharma (R ist bei Zulassungsverfahren akzeptiert), Bioinformatik (über das Bioconductor-Projekt), amtliche Statistik, Sozial- und Politikwissenschaft, Epidemiologie, Ökologie, Finanzanalyse, Marketing-Analytics sowie überall dort, wo aus Daten Berichte und Grafiken werden sollen. Weniger gebräuchlich ist R für allgemeine Anwendungsentwicklung, Deep Learning oder Hochlast-Backends — dort liegen die Stärken anderer Sprachen.

02

Installation und Werkzeuge

Was man installiert, womit man schreibt und wie man Skripte außerhalb der Konsole ausführt.

R selbst kommt von CRAN (cran.r-project.org) und bringt eine schlichte Konsole mit. Geschrieben wird meist in einer IDE:

RStudio
Die etablierte IDE (von Posit, kostenlos in der Desktop-Variante). Editor, Konsole, Umgebungsanzeige, Plot-Fenster, Paketverwaltung und Projektbegriff in einem Fenster. Für Einsteiger der Standardweg.
Positron
Der neuere IDE-Ansatz desselben Herstellers auf VS-Code-Basis, für R und Python in einer Oberfläche.
VS Code
Mit der Erweiterung REditorSupport.r plus dem R-Paket languageserver: Vervollständigung, Diagnosen, interaktive Konsole.
Emacs / ESS
Die klassische Variante, in der akademischen Welt weiterhin verbreitet.

Skripte ausführen

# Interaktive Sitzung starten
R

# Ein Skript im Batch ausführen (das übliche Vorgehen für Cron, CI, Pipelines)
Rscript analyse.R
Rscript -e 'sessionInfo()'
Rscript analyse.R --args eingabe.csv 2024   # Zugriff über commandArgs()

Pakete installieren

install.packages("dplyr")                # von CRAN
install.packages(c("ggplot2", "readr"))    # mehrere auf einmal
library(dplyr)                            # in die Sitzung laden

# Bioconductor (Bioinformatik) hat ein eigenes Installationsprogramm
install.packages("BiocManager")
BiocManager::install("DESeq2")

# Direkt von GitHub
pak::pak("tidyverse/dplyr")
Empfehlung

Arbeite von Anfang an mit Projekten (in RStudio: .Rproj) statt mit setwd(). Ein Projekt setzt das Arbeitsverzeichnis, hält relative Pfade stabil und macht den Code auf anderen Rechnern lauffähig. Für die Paketversionen ergänzt renv eine projektlokale Bibliothek plus renv.lock — das R-Gegenstück zu package-lock.json oder requirements.txt.

Hilfe im laufenden Betrieb

?mean              # Hilfeseite einer Funktion
??"linear model"    # Volltextsuche über alle installierten Hilfeseiten
example(lm)         # Beispiele der Hilfeseite ausführen
vignette("dplyr")   # ausführliche Paket-Einführung
args(round)        # Signatur ansehen
str(objekt)        # Struktur eines Objekts kompakt anzeigen — die meistgenutzte Funktion überhaupt
03

Sprachkern und Syntax

Zuweisung, Typen, fehlende Werte, Vektorisierung, Recycling.

Zuweisung

R kennt mehrere Zuweisungsoperatoren. Konvention ist <-; = funktioniert auf oberster Ebene ebenfalls, wird aber für Funktionsargumente reserviert.

x <- 10          # Standard
10 -> x          # auch erlaubt, selten benutzt
x = 10           # funktioniert, aber unüblich für Zuweisungen
x <<- 10         # in die übergeordnete Umgebung — nur bewusst einsetzen

Grundlegende Typen

Ein atomarer Vektor enthält Werte genau eines Typs. Es gibt sechs; im Alltag begegnen einem vier davon.

TypBeispielPrüfungBemerkung
logicalTRUE, FALSE, NAis.logical()In Rechnungen zählt TRUE als 1
integer5Lis.integer()Das L ist Pflicht, sonst wird es double
double5, 3.14, 1e-8is.double()Der Standardtyp für Zahlen
character"Text"is.character()Einfache und doppelte Anführungszeichen gleichwertig
complex1+2iis.complex()Selten
rawas.raw(255)is.raw()Bytes, sehr selten

Mischt man Typen in einem Vektor, wandelt R stillschweigend in den „größeren“ um — coercion in der Reihenfolge logical → integer → double → character:

c(1, 2, "drei")
#> [1] "1"    "2"    "drei"

c(TRUE, 5L, 2.5)
#> [1] 1.0 5.0 2.5

Die vier Sonderwerte

NA
Not Available — ein fehlender Wert. Es gibt ihn typisiert (NA_integer_, NA_character_). Rechnungen mit NA ergeben NA. Prüfung ausschließlich mit is.na(), niemals mit == NA.
NULL
Die Abwesenheit eines Objekts, nicht eines Werts. Länge 0. Ein Listenelement auf NULL zu setzen löscht es.
NaN
Not a Number, etwa aus 0/0. Ist auch is.na(), aber zusätzlich is.nan().
Inf / -Inf
Unendlich, etwa aus 1/0. Prüfung mit is.infinite(), is.finite().

Vektorisierung

Das zentrale Idiom. Operationen wirken elementweise auf ganze Vektoren — die Schleife läuft in kompiliertem C-Code, nicht im Interpreter.

preise  <- c(19.99, 4.50, 120.00, 7.25)
menge   <- c(2, 10, 1, 4)

summe   <- preise * menge           # elementweise Multiplikation
sum(summe)
#> [1] 214.98

teuer   <- preise > 10              # ergibt einen logischen Vektor
sum(teuer)                          # TRUE zählt als 1 — also: wie viele?
#> [1] 2

sqrt(preise)                        # auch mathematische Funktionen sind vektorisiert
round(preise * 1.19, 2)             # Bruttopreise, gerundet

Recycling

Sind zwei Vektoren unterschiedlich lang, wiederholt R den kürzeren. Das ist praktisch (preise * 1.19) und gefährlich zugleich:

c(1, 2, 3, 4) + c(10, 20)
#> [1] 11 22 13 24        # der kurze Vektor wird zweimal durchlaufen

c(1, 2, 3) + c(10, 20)
#> Warning: longer object length is not a multiple of shorter object length
Falle

Wenn die Länge des kürzeren Vektors ein Teiler der längeren ist, gibt es keine Warnung. Ein Tippfehler, der einen Vektor der Länge 1 statt 100 liefert, läuft dann still durch und liefert falsche Zahlen. Prüfe Längen explizit mit stopifnot(length(a) == length(b)), wenn es darauf ankommt.

Operatoren im Überblick

GruppeOperatorenAnmerkung
Arithmetik+ - * / ^ %% %/%%% Modulo, %/% ganzzahlige Division
Vergleich== != < > <= >=vektorisiert, Ergebnis ist ein logischer Vektor
Logik (vektorisiert)&, |, !elementweise — für Filterbedingungen
Logik (skalar)&&, ||nur Länge 1, mit Kurzschluss — für if
Enthalten%in%x %in% c("a","b")
Sequenz:1:10; Vorsicht bei 1:length(x)
Pipe|>seit R 4.1 in der Sprache selbst
Zugriff$ [ [[ @ ::siehe Kapitel 05 und 08

Zeichenketten

paste("Jahr", 2024)                # mit Leerzeichen verbinden
paste0("datei_", 1:3, ".csv")      # ohne Trenner, vektorisiert
#> [1] "datei_1.csv" "datei_2.csv" "datei_3.csv"

sprintf("%.1f %%", 12.345)          # formatierte Ausgabe
nchar("Straße"); toupper("abc"); trimws("  x  ")
substr("2024-05-17", 1, 4)
grepl("^AB", kennzeichen)             # regulärer Ausdruck, logischer Vektor
sub(",", ".", betrag)                # erstes Vorkommen ersetzen; gsub: alle

# Raw Strings seit R 4.0 — praktisch für Regex und Windows-Pfade
pfad <- r"(C:\Users\daten\roh.csv)"

Für ernsthafte Textarbeit lohnt das Paket stringr: einheitliche Namen (str_detect, str_replace, str_extract), Datenargument immer an erster Stelle, konsistentes Verhalten bei NA.

Datum und Zeit

Sys.Date(); Sys.time()
d <- as.Date("2024-05-17")              # Klasse Date = Tage seit 1970-01-01
d + 30                                # Datumsarithmetik in Tagen
format(d, "%d.%m.%Y")
#> [1] "17.05.2024"

t <- as.POSIXct("2024-05-17 14:30:00", tz = "Europe/Berlin")
difftime(Sys.time(), t, units = "days")

Das Paket lubridate nimmt der Zeitrechnung viel Schmerz: ymd(), dmy_hms(), floor_date(), months(1), sowie ein sauberes Modell für Zeitzonen und Sommerzeit.

04

Datenstrukturen

Sechs Bauformen, aus denen praktisch alles in R zusammengesetzt ist.

StrukturDimensionenInhaltTypisch für
vector1ein Typeine Messreihe, eine Spalte
factor1Kategorien mit festen StufenGruppen, Merkmalsausprägungen
matrix2ein Typnumerische Rechnung, Korrelationen
arraynein Typmehrdimensionale Tabellen
list1beliebig, auch verschachteltModellobjekte, JSON, alles Uneinheitliche
data.frame2Spalten unterschiedlichen Typsder Standard-Datensatz

Vektoren erzeugen

c(3, 1, 4, 1, 5)                 # combine
1:10                            # Ganzzahlfolge
seq(0, 1, by = 0.25)             # [1] 0.00 0.25 0.50 0.75 1.00
seq_len(n); seq_along(x)          # sichere Varianten für Schleifen
rep(c("a", "b"), times = 3)
rep(c("a", "b"), each = 3)
numeric(5); character(5)          # leere Vektoren vorbelegen

# Namen sind ein Attribut, kein eigener Typ
umsatz <- c(nord = 120, sued = 95, west = 143)
umsatz["west"]
#> west
#>  143

Faktoren

Ein Faktor ist eine kategoriale Variable: intern ganze Zahlen, nach außen beschriftete Levels. Modelle und Grafiken benutzen die Level-Reihenfolge — deshalb ist sie kein Detail.

g <- factor(c("mittel", "hoch", "niedrig", "hoch"),
            levels = c("niedrig", "mittel", "hoch"),
            ordered = TRUE)
levels(g)
#> [1] "niedrig" "mittel"  "hoch"
table(g)                    # Häufigkeitstabelle, zeigt auch leere Stufen
g > "niedrig"              # bei ordered = TRUE erlaubt
Falle

as.numeric(faktor) gibt die internen Codes zurück, nicht die Beschriftungen. Bei einem Faktor mit den Levels "10", "20", "30" liefert es 1 2 3. Der korrekte Weg ist as.numeric(as.character(faktor)).

Matrizen

m <- matrix(1:6, nrow = 2)      # füllt spaltenweise!
dim(m); t(m)                   # Dimensionen, Transponierte
m %*% t(m)                     # Matrixmultiplikation
rowSums(m); colMeans(m)
solve(a, b)                  # lineares Gleichungssystem lösen
rbind(v1, v2); cbind(v1, v2)  # zeilen-/spaltenweise zusammensetzen

Listen

Die flexible Struktur: jedes Element beliebigen Typs und beliebiger Länge, beliebig verschachtelt. Modellobjekte, eingelesenes JSON und praktisch jede zusammengesetzte Rückgabe sind Listen.

person <- list(
  name    = "Meier",
  alter   = 42L,
  noten   = c(1.3, 2.0, 1.7),
  aktiv   = TRUE
)
person$noten          # das Element selbst: numerischer Vektor
person["noten"]       # eine Liste der Länge 1, die es enthält
person[["noten"]]     # gleichbedeutend mit $noten
str(person)           # Struktur ansehen
person$alter <- NULL  # Element entfernen
Merksatz

[ schneidet eine Scheibe der gleichen Struktur heraus, [[ holt ein Element heraus. Bei Listen ist der Unterschied entscheidend: x[1] ist eine Liste, x[[1]] ihr Inhalt. Bildlich: [ nimmt einen Waggon aus dem Zug, [[ lädt dessen Fracht aus.

Data Frames

Ein data.frame ist intern eine Liste gleich langer Vektoren, dargestellt als Tabelle. Zeilen sind Beobachtungen, Spalten Variablen.

df <- data.frame(
  kunde  = c("A", "B", "C", "D"),
  umsatz = c(1200, 830, 2145, 640),
  region = factor(c("nord", "sued", "nord", "west"))
)

nrow(df); ncol(df); dim(df)
names(df)              # Spaltennamen
head(df, 3); tail(df)
str(df)                # Typen aller Spalten
summary(df)            # Kennzahlen je Spalte
df$marge <- df$umsatz * 0.18    # neue Spalte

tibble

Das Tidyverse benutzt tibble, eine data.frame-Variante mit denselben Grundeigenschaften, aber ohne einige Altlasten: sie druckt nur die ersten zehn Zeilen mit Typangabe, wandelt nichts stillschweigend um, macht kein partial matching von Spaltennamen und liefert bei tib[, "spalte"] immer ein Tibble zurück statt manchmal einen Vektor.

05

Indizierung und Auswahl

Vier Wege, einen Teil eines Objekts zu benennen — und was drop damit anstellt.

x <- c(a = 10, b = 20, c = 30, d = 40)

# 1. positiv: was behalten wird
x[c(1, 3)]            #>  a  c / 10 30

# 2. negativ: was wegfällt
x[-1]                  # alles außer dem ersten Element

# 3. logisch: die verbreitetste Form — Filtern
x[x > 15]

# 4. über Namen
x[c("b", "d")]

Zweidimensional: [zeilen, spalten]

df[1:3, ]                          # erste drei Zeilen, alle Spalten
df[, c("kunde", "umsatz")]           # alle Zeilen, zwei Spalten
df[df$umsatz > 1000, ]              # Zeilen filtern
df[df$region == "nord" & df$umsatz > 1000, ]
df[order(-df$umsatz), ]             # absteigend sortieren
subset(df, umsatz > 1000, select = kunde:region)
Falle: drop

df[, "umsatz"] gibt bei einem klassischen data.frame einen Vektor zurück, df[, c("kunde","umsatz")] dagegen einen data.frame. Diese Typinstabilität bricht Code, sobald eine Auswahl zufällig auf eine Spalte zusammenschrumpft. Abhilfe: df[, "umsatz", drop = FALSE] erzwingt den data.frame, df$umsatz oder df[["umsatz"]] erzwingt den Vektor. Tibbles haben das Problem nicht.

Zuweisen an eine Auswahl

x[x < 0] <- 0                          # negative Werte kappen
df$region[is.na(df$region)] <- "unbekannt"
df[df$umsatz > 2000, "kategorie"] <- "gross"

# Bedingte Werte — vektorisiert statt if/else
df$stufe <- ifelse(df$umsatz > 1000, "hoch", "niedrig")

# Mehrere Bedingungen (dplyr), gut lesbar
df$stufe <- dplyr::case_when(
  df$umsatz >= 2000 ~ "A",
  df$umsatz >= 1000 ~ "B",
  .default            = "C"
)
Falle: NA in Filtern

df[df$wert > 10, ] erzeugt für jedes NA in wert eine komplette NA-Zeile im Ergebnis — denn NA > 10 ist NA, und ein NA-Index liefert eine Leerzeile. subset() und dplyr::filter() verwerfen solche Zeilen stattdessen. Bei [ hilft which(): df[which(df$wert > 10), ].

06

Kontrollfluss und Funktionen

Bedingungen, Schleifen, Funktionsdefinition, Scoping, verzögerte Auswertung.

Bedingungen

if (x > 0) {
  "positiv"
} else if (x < 0) {
  "negativ"
} else {
  "null"
}

# if ist ein Ausdruck mit Wert:
vz <- if (x > 0) 1 else -1

switch(typ,
  csv  = read.csv(p),
  json = jsonlite::fromJSON(p),
  stop("unbekannt: ", typ))

Schleifen

for (i in seq_along(x)) {
  cat(i, ":", x[i], "\n")
}

while (fehler > 1e-6) {
  fehler <- schritt(fehler)
}

repeat {
  if (fertig) break
  if (ueberspringen) next
}
Falle

for (i in 1:length(x)) läuft bei leerem x über 1:0, also über c(1, 0) — die Schleife wird zweimal ausgeführt statt keinmal. Nimm seq_along(x) oder seq_len(n). Seit R 4.2 ist außerdem eine if-Bedingung der Länge > 1 ein Fehler, nicht mehr nur eine Warnung.

Funktionen

Funktionen sind gewöhnliche Objekte: sie können in Variablen liegen, an andere Funktionen übergeben und aus Funktionen zurückgegeben werden. Der Wert des letzten ausgewerteten Ausdrucks ist der Rückgabewert; return() ist nur für den vorzeitigen Ausstieg nötig.

kennzahl <- function(x, trim = 0, na.rm = TRUE) {
  if (!is.numeric(x)) stop("x muss numerisch sein")
  if (na.rm) x <- x[!is.na(x)]
  list(
    n      = length(x),
    mittel = mean(x, trim = trim),
    sd     = sd(x)
  )                              # letzter Ausdruck = Rückgabewert
}

kennzahl(c(4, 8, NA, 15, 16))$mittel
#> [1] 10.75

# Kurzschreibweise für anonyme Funktionen seit R 4.1
sapply(1:5, \(i) i^2)
#> [1]  1  4  9 16 25

Argumente

  • Zuordnung erfolgt in drei Stufen: exakter Name, dann Präfix (partial matching), dann Position. In Skripten benennen, in Paketen ausschreiben.
  • Standardwerte dürfen sich auf andere Argumente beziehen: function(x, n = length(x)).
  • ... nimmt beliebige weitere Argumente auf und reicht sie durch: function(x, ...) plot(x, ...).
  • missing(arg) prüft, ob ein Argument überhaupt übergeben wurde.

Verzögerte Auswertung (Lazy Evaluation)

Argumente werden erst ausgewertet, wenn sie zum ersten Mal gebraucht werden. Ein nie benutztes, fehlerhaftes Argument löst deshalb keinen Fehler aus — und ein Standardwert kann teuer sein, ohne zu kosten, solange er unbenutzt bleibt.

f <- function(a, b) a * 2
f(5, stop("nie ausgewertet"))
#> [1] 10

force(x)   # erzwingt die Auswertung — wichtig beim Bauen von Closures in Schleifen

Lexikalisches Scoping und Closures

R sucht einen Namen zuerst in der aktuellen Funktion, dann in der Umgebung, in der die Funktion definiert wurde (nicht in der, aus der sie aufgerufen wird), und so weiter bis zum globalen Environment und den geladenen Paketen. Daraus ergeben sich Closures: Funktionen, die Zustand mitführen.

zaehler <- function() {
  n <- 0
  function() {
    n <<- n + 1      # schreibt in die umschließende Umgebung
    n
  }
}
naechste <- zaehler()
naechste(); naechste()
#> [1] 1
#> [1] 2

Copy-on-modify

R übergibt Argumente dem Wert nach: eine Funktion kann ihr Argument nicht beim Aufrufer verändern. Kopiert wird aber erst, wenn tatsächlich geschrieben wird, und nur, wenn das Objekt an mehr als einem Namen hängt. Das macht die Semantik sicher und den Speicherverbrauch meist erträglich — aber es erklärt, warum das schrittweise Wachsenlassen eines Objekts in einer Schleife so langsam ist (Kapitel 15).

Fehlerbehandlung

stop("Abbruch mit Fehler")
warning("nur eine Warnung")
message("Hinweis an stderr")
stopifnot(is.numeric(x), length(x) > 0)      # kompakte Vorbedingungen

ergebnis <- tryCatch(
  riskant(datei),
  error   = function(e) { message("fehlgeschlagen: ", conditionMessage(e)); NULL },
  warning = function(w) NA,
  finally = close(verbindung)
)

on.exit(close(con), add = TRUE)   # Aufräumen garantieren, auch im Fehlerfall

Pipes

Die Pipe reicht das Ergebnis links als erstes Argument nach rechts weiter und macht aus verschachtelten Aufrufen eine Leserichtung von oben nach unten.

# ohne Pipe — von innen nach außen zu lesen
head(sort(table(daten$region), decreasing = TRUE), 3)

# nativ, seit R 4.1 — kein Paket nötig
daten$region |>
  table() |>
  sort(decreasing = TRUE) |>
  head(3)

# Platzhalter _, wenn das Argument nicht an erster Stelle steht (benannt nötig)
daten |> lm(umsatz ~ region, data = _)

Daneben existiert die ältere magrittr-Pipe %>% (aus dplyr/magrittr) mit dem Platzhalter .. Sie kann etwas mehr, ist aber ein Funktionsaufruf zur Laufzeit; |> wird beim Parsen umgeschrieben und ist damit schneller und abhängigkeitsfrei. Für neuen Code ist |> die Empfehlung.

07

Funktional statt Schleife

Die apply-Familie und ihr modernes Gegenstück purrr.

Statt eine Schleife zu schreiben, die ein Ergebnisobjekt füllt, übergibt man in R eine Funktion an eine Funktion. Der Code sagt dann was auf jedes Element angewandt wird, statt wie iteriert wird — und das Ergebnisobjekt entsteht in einem Stück.

FunktionEingabeErgebnisTypisch
lapply(x, f)Vektor/Listeimmer eine Listevorhersagbar, gute Voreinstellung
sapply(x, f)Vektor/Listevereinfacht: Vektor, Matrix oder Listeinteraktiv bequem, in Skripten riskant
vapply(x, f, FUN.VALUE)Vektor/Listeder zugesicherte Typdie sichere Wahl für Produktivcode
mapply() / Map()mehrere Vektorenparallel elementweisezwei Argumente gleichzeitig
apply(m, MARGIN, f)Matrixüber Zeilen (1) oder Spalten (2)Matrizen, nicht data.frames
tapply(x, g, f)Vektor + Gruppenje Gruppeschnelle Gruppenkennzahl
Reduce(f, x)Vektor/Listeein Wertkumulieren, zusammenführen
Filter(f, x)Vektor/ListeTeilmengenach Prädikat auswählen
dateien <- list.files("daten/", pattern = "\\.csv$", full.names = TRUE)

# alle CSVs einlesen und zu einer Tabelle stapeln
tabellen <- lapply(dateien, read.csv)
gesamt   <- do.call(rbind, tabellen)

# Typ zusichern: „genau ein double je Element"
mittel <- vapply(tabellen, \(d) mean(d$wert, na.rm = TRUE), numeric(1))

# Mittelwert je Spalte eines data.frame (der ist eine Liste von Spalten!)
vapply(Filter(is.numeric, df), mean, numeric(1))

# zwei Vektoren parallel
Map(\(datei, jahr) paste0(jahr, "_", datei), dateien, 2020:2023)

# kumulieren
Reduce(`+`, 1:5, accumulate = TRUE)
#> [1]  1  3  6 10 15

purrr

Das Tidyverse-Paket purrr bietet dieselben Ideen mit typstabiler Benennung: map() gibt immer eine Liste zurück, map_dbl() immer einen double-Vektor, map_chr() einen character-Vektor, map2() und pmap() arbeiten über mehrere Eingaben, list_rbind() stapelt Tabellen, und possibly()/safely() fangen Fehler pro Element ab, statt den ganzen Lauf abzubrechen.

library(purrr)
werte <- dateien |> map(readr::read_csv) |> list_rbind()
mittel <- map_dbl(tabellen, \(d) mean(d$wert, na.rm = TRUE))
robust <- map(urls, possibly(abrufen, otherwise = NA))
Wann doch eine Schleife?

for ist in R nicht verboten und nicht per se langsam. Sinnvoll ist sie, wenn jeder Durchlauf vom vorherigen abhängt (Simulationen, iterative Verfahren) oder wenn es um Seiteneffekte geht (Dateien schreiben, Fortschritt melden). Langsam wird es erst, wenn das Ergebnisobjekt in der Schleife wächst. Lege es vorher in voller Länge an.

08

Objektsysteme: S3, S4, R6, S7

R hat nicht ein Objektsystem, sondern vier — und man begegnet ihnen allen.

S3 — das allgegenwärtige

S3 ist bemerkenswert einfach: Eine Klasse ist ein Attribut, das man auf ein beliebiges Objekt schreibt. Eine generische Funktion ruft UseMethod() auf und R sucht dann nach einer Funktion namens generisch.klasse. Mehr Mechanik gibt es nicht. Deshalb funktioniert print(), summary() und plot() auf fast allem.

# Konstruktor
neue_messreihe <- function(werte, einheit) {
  structure(
    list(werte = werte, einheit = einheit),
    class = "messreihe"
  )
}

# Methode für eine bestehende Generische
print.messreihe <- function(x, ...) {
  cat("<messreihe>", length(x$werte), "Werte in", x$einheit, "\n")
  cat("Mittel:", round(mean(x$werte), 2), "\n")
  invisible(x)
}

# Eigene Generische
normieren <- function(x, ...) UseMethod("normieren")
normieren.messreihe <- function(x, ...) x$werte / max(x$werte)
normieren.default   <- function(x, ...) stop("kein Verfahren für Klasse ", class(x)[1])

m <- neue_messreihe(c(2.1, 3.4, 2.9), "mm")
m
#> <messreihe> 3 Werte in mm
#> Mittel: 2.8

Wichtige Generische, für die sich eigene Methoden lohnen: print, format, summary, plot, as.data.frame, [, length, c. Mit methods(print) sieht man alle registrierten Methoden, mit getS3method("print", "lm") deren Quelltext.

S4 — das formale

S4 verlangt eine deklarierte Klassendefinition mit typisierten Slots, prüft beim Erzeugen, kennt Vererbung und Methodenauswahl über mehrere Argumente. Verbreitet in Bioconductor und überall dort, wo Korrektheit vor Bequemlichkeit geht.

setClass("Sensor",
  representation(id = "character", werte = "numeric"),
  validity = function(object) {
    if (length(object@id) != 1) "id muss genau einen Wert haben" else TRUE
  })

setGeneric("spanne", function(x) standardGeneric("spanne"))
setMethod("spanne", "Sensor", function(x) diff(range(x@werte)))

s <- new("Sensor", id = "T-01", werte = c(19.5, 21.2, 20.1))
s@id            # Slot-Zugriff mit @, nicht mit $
spanne(s)
#> [1] 1.7

Reference Classes (R5) und R6

Beide bieten Referenzsemantik: Ein Objekt wird beim Übergeben nicht kopiert, Methoden verändern es an Ort und Stelle — das gewohnte Modell aus Java oder Python. setRefClass() ist eingebaut, das CRAN-Paket R6 ist leichtgewichtiger, schneller und in der Praxis die übliche Wahl (Shiny und viele Infrastrukturpakete bauen darauf).

library(R6)
Konto <- R6Class("Konto",
  public = list(
    stand = 0,
    initialize = function(start = 0) { self$stand <- start },
    einzahlen  = function(betrag) {
      self$stand <- self$stand + betrag
      invisible(self)          # erlaubt Verkettung
    }
  ))

k <- Konto$new(100)
k$einzahlen(50)$einzahlen(25)
k$stand
#> [1] 175

S7

S7 ist der neuere, vom R Consortium getragene Versuch, S3 und S4 zu vereinen: die Formalität von S4 (deklarierte Eigenschaften, Validierung) mit der Leichtigkeit von S3 und voller Kompatibilität zu beidem. Es steht auf CRAN und ist der aussichtsreichste Kandidat für neuen Code, der formale Klassen braucht — verbreitet ist bislang aber weiterhin S3.

Welches nehmen?

S3 für so gut wie alles — es ist die Sprache des Ökosystems. S4, wenn man in Bioconductor arbeitet oder Methodenauswahl über mehrere Argumente braucht. R6, wenn ein Objekt echten veränderlichen Zustand hat (Verbindungen, Puffer, Sitzungen). S7, wenn man ein neues Paket mit formalen Klassen beginnt.

09

Pakete und Ökosystem

CRAN, Namespaces, und die drei Dialekte der Datenarbeit.

CRAN (Comprehensive R Archive Network) ist Archiv und Qualitätsschranke zugleich: Jedes Paket muss R CMD check auf mehreren Plattformen bestehen, Dokumentation zu jeder exportierten Funktion mitbringen und bei Änderungen der Abhängigkeiten nachziehen. Die Zahl der Pakete liegt im fünfstelligen Bereich. Bioconductor ist ein zweites, halbjährlich veröffentlichtes Archiv für Biowissenschaften mit noch strengeren Regeln; r-universe und GitHub decken den Rest ab.

Namespaces

library(dplyr)          # hängt alle Exporte an den Suchpfad an
dplyr::filter(df, x > 1) # einzelne Funktion, ohne Anhängen — eindeutig
requireNamespace("sf", quietly = TRUE)   # optionale Abhängigkeit prüfen
search()                 # aktueller Suchpfad
conflicts()              # verdeckte Namen anzeigen
Falle

Mehrere Pakete exportieren gleiche Namen — dplyr::filter() verdeckt stats::filter(), MASS::select() verdeckt dplyr::select(). Wer zuletzt geladen wird, gewinnt. In Skripten und Paketen ist paket::funktion() deshalb kein Ballast, sondern Dokumentation. In Paketen gilt außerdem: niemals library() im Code — Abhängigkeiten gehören in DESCRIPTION und NAMESPACE.

Die drei Dialekte

Für dieselbe Tabellenaufgabe gibt es in R drei etablierte Schreibweisen. Sie sind mischbar — alle arbeiten auf data.frames.

 base Rtidyversedata.table
Abhängigkeitenkeinevieleeine
Lesbarkeit für Einsteigermittelhochgering (sehr kompakt)
Geschwindigkeitmittelgutsehr hoch
Speicherkopiertkopiertändert in-place
Stärkestabil, immer verfügbardurchgängige Grammatikgroße Datenmengen

base

aggregate(umsatz ~ region,
  data = df[df$umsatz > 500, ],
  FUN  = mean)

dplyr

df |>
  filter(umsatz > 500) |>
  summarise(m = mean(umsatz),
            .by = region)

data.table

dt[umsatz > 500,
   .(m = mean(umsatz)),
   by = region]

Kernpakete, die man kennen sollte

tidyverse
Sammelpaket für dplyr (Tabellen), ggplot2 (Grafik), tidyr (Umformen), readr (Einlesen), purrr (Iteration), stringr, forcats, tibble — eine gemeinsame Entwurfslinie.
data.table
Hochleistungstabellen, In-Place-Änderung, schnellstes CSV-Lesen (fread).
ggplot2
Die Grammatik der Grafik, praktisch der Standard für Visualisierung.
shiny
Interaktive Webanwendungen direkt aus R.
knitr / quarto
Berichte, in denen Text, Code und Ergebnis in einer Datei stehen.
DBI + dbplyr
Datenbankzugriff; dbplyr übersetzt dplyr-Code in SQL.
arrow / duckdb
Parquet, Out-of-Memory-Analysen, sehr schnelles SQL auf lokalen Dateien.
sf
Geodaten nach dem Simple-Features-Standard.
tidymodels / caret
Einheitliche Rahmen für maschinelles Lernen und Modellvergleich.
testthat / devtools / usethis
Das Werkzeugset der Paketentwicklung.

Wer sucht, findet Orientierung in den CRAN Task Views — kuratierte Übersichten je Fachgebiet (Ökonometrie, Zeitreihen, Bayes, räumliche Statistik, Machine Learning, Klinische Studien).

10

Daten einlesen

Text, Excel, Datenbanken, JSON, Web — und der deutsche Sonderfall.

# base R
df <- read.csv("daten.csv")
df <- read.csv2("daten.csv")     # Semikolon als Trenner, Komma als Dezimalzeichen
df <- read.table("daten.txt", header = TRUE, sep = "\t",
                 dec = ",", fileEncoding = "latin1",
                 na.strings = c("", "NA", "k.A."))

# readr — schneller, gibt ein Tibble zurück, meldet erkannte Spaltentypen
library(readr)
df <- read_csv("daten.csv")
df <- read_csv2("daten.csv")                # deutsche Konvention
df <- read_delim("daten.txt", delim = "|",
                locale = locale(decimal_mark = ",",
                                grouping_mark = ".",
                                encoding = "ISO-8859-1"),
                col_types = cols(kundennr = col_character()))

# data.table — die schnellste Variante für große Dateien
dt <- data.table::fread("gross.csv")
Deutscher Sonderfall

Deutsche Exporte kommen typischerweise mit Semikolon als Feldtrenner, Komma als Dezimalzeichen, Punkt als Tausendertrenner und oft in Latin-1/CP1252 statt UTF-8. Dafür gibt es read.csv2() beziehungsweise read_csv2(). Zwingt man ein solches Format durch read.csv(), werden alle Zahlspalten stillschweigend zu Text. Ebenso wichtig: Postleitzahlen, Kunden- und Artikelnummern gehören ausdrücklich als character eingelesen — sonst verschwindet die führende Null.

Excel

library(readxl)
excel_sheets("buch.xlsx")
d <- read_excel("buch.xlsx",
  sheet = "Umsatz",
  range = "A3:F120",
  na    = "-")

# Schreiben
writexl::write_xlsx(d, "aus.xlsx")

Datenbanken

library(DBI)
con <- dbConnect(RPostgres::Postgres(),
  host = "db", dbname = "prod",
  user = Sys.getenv("DB_USER"))

dbListTables(con)
dbGetQuery(con,
  "SELECT * FROM t WHERE jahr = $1",
  params = list(2024))
dbDisconnect(con)
# dbplyr: dplyr-Code schreiben, SQL ausführen lassen
library(dplyr)
tbl(con, "verkaeufe") |>
  filter(jahr == 2024) |>
  summarise(summe = sum(betrag), .by = region) |>
  show_query() |>      # erzeugtes SQL ansehen
  collect()            # erst hier werden Daten geholt

# Weitere Quellen
jsonlite::fromJSON("api.json")          # JSON, wandelt sinnvoll in data.frames
httr2::request(url) |> httr2::req_perform()   # HTTP-Abfragen
rvest::read_html(url) |> rvest::html_table()  # Tabellen aus HTML
arrow::read_parquet("daten.parquet")      # spaltenorientiert, sehr schnell
haven::read_sav("umfrage.sav")            # SPSS, Stata, SAS

# R-eigene Formate
saveRDS(modell, "modell.rds"); modell <- readRDS("modell.rds")   # ein Objekt
save(a, b, file = "sitzung.RData"); load("sitzung.RData")         # mehrere, mit Namen

saveRDS() ist save() vorzuziehen: Es speichert genau ein Objekt und man vergibt den Namen beim Laden selbst — load() überschreibt dagegen stillschweigend Variablen im Arbeitsbereich.

11

Datenaufbereitung

Die dplyr-Verben, Umformen zwischen breit und lang, Verknüpfen.

Der größte Teil realer Analysearbeit ist Aufbereitung. dplyr bildet sie auf eine Handvoll Verben ab, die sich alle gleich verhalten: erstes Argument ist die Tabelle, Ergebnis ist wieder eine Tabelle, Spalten werden unquotiert benannt.

VerbWirkungbase-Entsprechung
filter()Zeilen auswählendf[cond, ], subset()
select()Spalten auswählen/umbenennendf[, cols]
mutate()Spalten anlegen oder änderndf$neu <- ..., transform()
arrange()sortierendf[order(x), ]
summarise()zu einer Zeile je Gruppe verdichtenaggregate(), tapply()
group_by() / .by=Gruppierung festlegenFormel-Seite von aggregate()
distinct()Duplikate entfernenunique()
slice_max()Spitzenwerte je Gruppe
count()zählentable()
library(dplyr)

bericht <- verkaeufe |>
  filter(!is.na(betrag), jahr >= 2022) |>
  mutate(
    netto  = betrag / 1.19,
    quartal = paste0(jahr, "-Q", ceiling(monat / 3))
  ) |>
  summarise(
    umsatz  = sum(netto),
    auftraege = n(),
    schnitt = mean(netto),
    .by = c(region, quartal)
  ) |>
  arrange(region, quartal)

# Spaltenauswahl mit Hilfsfunktionen
df |> select(kunde, starts_with("umsatz_"), where(is.numeric), -id)

# dieselbe Umformung auf viele Spalten
df |> mutate(across(where(is.numeric), \(x) round(x, 2)))
df |> summarise(across(c(umsatz, marge), \(x) mean(x, na.rm = TRUE)), .by = region)

Breit und lang

Tidy data heißt: eine Zeile je Beobachtung, eine Spalte je Variable, eine Tabelle je Beobachtungseinheit. Reale Tabellen liegen oft breit vor (ein Jahr je Spalte) — für Auswertung und Grafik braucht man lang.

library(tidyr)

# breit -> lang
lang <- breit |>
  pivot_longer(
    cols      = "2020":"2024",
    names_to  = "jahr",
    values_to = "umsatz",
    names_transform = list(jahr = as.integer)
  )

# lang -> breit
breit <- lang |>
  pivot_wider(names_from = jahr, values_from = umsatz, values_fill = 0)

# Weitere nützliche Werkzeuge
separate_wider_delim(df, name, delim = ", ", names = c("nach", "vor"))
drop_na(df, umsatz)
replace_na(df, list(umsatz = 0, region = "unbekannt"))
complete(df, region, jahr)          # fehlende Kombinationen ergänzen

Tabellen verknüpfen

left_join(a, b, by = join_by(kunden_id))            # alle Zeilen aus a
inner_join(a, b, by = join_by(id == kunden_id))     # nur Treffer, Namen verschieden
full_join(a, b); right_join(a, b)
anti_join(a, b, by = join_by(id))    # Zeilen aus a OHNE Entsprechung — gut zur Prüfung
semi_join(a, b, by = join_by(id))    # Zeilen aus a MIT Entsprechung, ohne b-Spalten

bind_rows(a, b, .id = "quelle")     # untereinander stapeln
bind_cols(a, b)                     # nebeneinander (Reihenfolge muss stimmen!)
Vor jedem Join

Prüfe, ob der Schlüssel in der rechten Tabelle wirklich eindeutig ist: b |> count(id) |> filter(n > 1). Ist er es nicht, vervielfacht der Join Zeilen — der häufigste stille Fehler in Auswertungen. left_join(..., relationship = "many-to-one") lässt R selbst darauf achten und einen Fehler werfen.

12

Statistik und Modelle

Wo R nicht Bibliothek braucht, sondern schon ist.

Deskriptive Statistik

mean(x); median(x); sd(x); var(x); IQR(x)
quantile(x, c(0.05, 0.5, 0.95))
range(x); min(x); max(x)
mean(x, na.rm = TRUE)               # ohne na.rm ergibt ein einziges NA: NA
table(gruppe); table(gruppe, geschlecht)    # Kreuztabelle
prop.table(table(gruppe, geschlecht), margin = 1)  # Zeilenanteile
cor(df$a, df$b, method = "spearman")
cor(Filter(is.numeric, df), use = "pairwise.complete.obs")

Verteilungen

R folgt einem strengen Namensschema: das Präfix bestimmt, was gefragt wird, der Rest die Verteilung.

PräfixBedeutungBeispielErgebnis
dDichtednorm(0)Wert der Dichtefunktion
pVerteilungsfunktionpnorm(1.96)0.975 — Fläche links
qQuantil (Umkehrung von p)qnorm(0.975)1.96
rZufallszahlenrnorm(100, 10, 2)100 Ziehungen

Der Namensteil dahinter: norm, binom, pois, unif, t, chisq, f, exp, gamma, beta und weitere. Für Reproduzierbarkeit von Simulationen: set.seed(42).

Hypothesentests

t.test(gewicht ~ gruppe, data = df)          # Zweistichproben-t-Test
t.test(vorher, nachher, paired = TRUE)      # verbunden
wilcox.test(x ~ g, data = df)               # nichtparametrisch
chisq.test(table(a, b))                    # Unabhängigkeit
shapiro.test(x)                            # Normalverteilung
cor.test(x, y)
aov(ertrag ~ sorte + block, data = df) |> summary()

Die Formelsprache

Formeln sind eigene Objekte und werden vom Modell interpretiert — dieselbe Notation gilt für lm, glm, aov, lmer, viele Grafikfunktionen und aggregate.

NotationBedeutung
y ~ xy erklärt durch x
y ~ x + zzwei Haupteffekte
y ~ x:znur die Wechselwirkung
y ~ x * zHaupteffekte und Wechselwirkung
y ~ .alle übrigen Spalten des Datensatzes
y ~ x - 1ohne Achsenabschnitt
y ~ poly(x, 2)orthogonales Polynom 2. Grades
y ~ x + I(x^2)I() schützt arithmetische Bedeutung
y ~ x + (1 | schule)zufälliger Achsenabschnitt (Paket lme4)

Lineare Modelle

modell <- lm(umsatz ~ werbebudget + region + saison, data = df)

summary(modell)        # Koeffizienten, Standardfehler, p-Werte, R²
coef(modell)           # nur die Schätzer
confint(modell)        # Konfidenzintervalle
anova(modell)          # Varianzzerlegung
plot(modell)           # vier Diagnoseplots: Residuen, QQ, Leverage
predict(modell, newdata = neu, interval = "prediction")
residuals(modell); fitted(modell)
AIC(m1, m2); anova(m1, m2)   # Modellvergleich

# Verallgemeinerte lineare Modelle
logit <- glm(gekauft ~ alter + kanal, data = df, family = binomial)
exp(coef(logit))        # Odds Ratios
poisson <- glm(anzahl ~ zeit, data = df, family = poisson)
Modellausgabe weiterverarbeiten

summary() ist zum Lesen gedacht, nicht zum Rechnen. Das Paket broom übersetzt Modellobjekte in Tabellen: tidy(modell) liefert eine Zeile je Koeffizient, glance(modell) eine Zeile Gütemaße je Modell, augment(modell) die Originaldaten mit Vorhersage und Residuum. Damit lassen sich Modelle wie Daten behandeln — hundert Modelle schätzen, mit map() tidy machen, in eine Tabelle stapeln und plotten.

Weiterführend

lme4/nlme für gemischte Modelle, survival für Ereigniszeitanalyse, forecast/fable für Zeitreihen, mgcv für additive Modelle, brms/rstanarm für bayessche Modelle, tidymodels für maschinelles Lernen mit Resampling, Vorverarbeitung und Tuning in einem Rahmen.

13

Grafik

Zwei Systeme: schnelle Diagnose und komponierte Darstellung.

Base-Grafik

Direkt, ohne Abhängigkeit, ideal für den schnellen Blick während der Analyse. Man malt auf ein Gerät: erst ein Bild öffnen, dann Schichten hinzufügen.

plot(df$budget, df$umsatz,
     pch = 19, col = "grey30",
     xlab = "Werbebudget (EUR)", ylab = "Umsatz (EUR)",
     main = "Umsatz nach Budget")
abline(lm(umsatz ~ budget, data = df), col = "firebrick", lwd = 2)
legend("topleft", legend = "Regression", col = "firebrick", lty = 1)

hist(x, breaks = 30); boxplot(wert ~ gruppe, data = df)
barplot(table(g)); pairs(df[, 1:4])
par(mfrow = c(2, 2))     # vier Grafiken in einem Fenster

ggplot2 und die Grammatik der Grafik

Der Grundgedanke: Eine Grafik ist eine Abbildung von Datenspalten auf visuelle Eigenschaften. Man beschreibt diese Zuordnung (aes), wählt geometrische Objekte (geom_*), die sie darstellen, und verfeinert mit Skalen, Facetten und Themes. Schichten werden mit + verbunden.

library(ggplot2)

ggplot(df, aes(x = budget, y = umsatz, colour = region)) +
  geom_point(alpha = 0.6, size = 2) +
  geom_smooth(method = "lm", se = FALSE) +
  facet_wrap(~ jahr, ncol = 2) +
  scale_y_continuous(labels = scales::label_currency(suffix = " €", prefix = "")) +
  labs(
    title    = "Umsatz und Werbebudget",
    subtitle = "nach Region und Jahr",
    x = "Budget", y = "Umsatz", colour = "Region",
    caption  = "Quelle: interne Auswertung"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")
BausteinBeispieleZweck
aes()x, y, colour, fill, shape, size, alpha, groupSpalte → visuelle Eigenschaft
geom_*point, line, col, histogram, boxplot, violin, tile, text, sfDarstellungsform
stat_*summary, smooth, binTransformation vor dem Zeichnen
scale_*scale_x_log10, scale_fill_viridis_cAchsen, Farbskalen, Beschriftungsformate
facet_*facet_wrap, facet_gridkleine Vielfache je Gruppe
coord_*coord_flip, coord_cartesian, coord_sfKoordinatensystem, Zoom
theme_*theme_minimal, theme()alles Nicht-Datenhafte
Der klassische ggplot-Fehler

Eine konstante Eigenschaft gehört außerhalb von aes(): geom_point(colour = "blue") färbt blau. Innerhalb — aes(colour = "blue") — erzeugt R stattdessen eine erfundene Gruppierungsvariable mit dem einzigen Wert „blue", vergibt die erste Skalenfarbe (rot) und baut eine Legende. In aes() steht nur, was aus den Daten kommt.

# Speichern — Maße und Auflösung explizit angeben
ggsave("umsatz.png", plot = p, width = 20, height = 12, units = "cm", dpi = 300)
ggsave("umsatz.pdf", plot = p, width = 20, height = 12, units = "cm")  # vektoriell

# Base-Grafik in eine Datei umleiten
png("diagnose.png", width = 1200, height = 900, res = 150)
plot(modell)
dev.off()

Ergänzende Pakete: patchwork setzt mehrere ggplots zu einer Abbildung zusammen (p1 + p2 / p3), scales liefert Achsenformate, ggrepel platziert Beschriftungen überschneidungsfrei, plotly macht aus einem ggplot eine interaktive Grafik, gt und flextable erzeugen präsentationsfertige Tabellen.

14

Reproduzierbarkeit, Berichte, Apps

Der Bereich, in dem R konkurrenzlos stark ist.

Quarto und R Markdown

Eine Datei enthält Text, Code und Ergebnis. Beim Rendern läuft der Code frisch durch, die Ausgabe wird eingesetzt und alles zu HTML, PDF, Word oder Folien gesetzt. Zahlen im Fließtext werden mitgerechnet — ein Bericht kann nicht mehr veralten, ohne dass es auffällt. Quarto ist der Nachfolger von R Markdown und arbeitet zusätzlich mit Python, Julia und Observable.

---
title: "Quartalsbericht Q2"
format: html
execute:
  echo: false
  warning: false
---

## Umsatzentwicklung

```{r}
#| label: fig-umsatz
#| fig-cap: "Umsatz je Region"
ggplot(daten, aes(quartal, umsatz, colour = region)) + geom_line()
```

Der Gesamtumsatz lag bei `r format(sum(daten$umsatz), big.mark = ".")` EUR,
das sind `r round(wachstum * 100, 1)` % mehr als im Vorjahresquartal.
quarto render bericht.qmd
quarto render bericht.qmd -P jahr:2024      # parametrisiert
# aus R heraus:
quarto::quarto_render("bericht.qmd", execute_params = list(region = "Nord"))

Shiny

Interaktive Webanwendungen ohne JavaScript: Eine Oberfläche beschreibt die Eingaben, eine Serverfunktion die Reaktionen. Alles, was von einem Eingabewert abhängt, rechnet sich automatisch neu, wenn der sich ändert — reaktive Programmierung.

library(shiny)

ui <- fluidPage(
  titlePanel("Umsatz nach Region"),
  sidebarLayout(
    sidebarPanel(
      selectInput("region", "Region:", choices = unique(daten$region)),
      sliderInput("jahr", "Zeitraum:", 2018, 2024, value = c(2020, 2024), sep = "")
    ),
    mainPanel(plotOutput("verlauf"), tableOutput("tabelle"))
  )
)

server <- function(input, output, session) {
  gefiltert <- reactive({                  # einmal rechnen, mehrfach nutzen
    daten |> filter(region == input$region,
                     jahr >= input$jahr[1], jahr <= input$jahr[2])
  })
  output$verlauf <- renderPlot(ggplot(gefiltert(), aes(jahr, umsatz)) + geom_col())
  output$tabelle <- renderTable(gefiltert())
}

shinyApp(ui, server)

Weitere Bausteine

plumber
Macht aus R-Funktionen eine REST-API — der übliche Weg, ein R-Modell in eine andere Anwendung einzubinden.
targets
Pipeline-Werkzeug: definiert Arbeitsschritte und ihre Abhängigkeiten und rechnet nach einer Änderung nur das Nötige neu. Für Analysen, die Stunden laufen, ein großer Gewinn.
renv
Sperrt Paketversionen projektweise, damit derselbe Code in einem Jahr dasselbe Ergebnis liefert.
sessionInfo()
Gehört ans Ende jedes Berichts: R-Version, Plattform und alle geladenen Paketversionen.
Docker / Rocker
Fertige Images (rocker/r-ver, rocker/tidyverse) für vollständig festgeschriebene Umgebungen.
15

Performance und Speicher

R ist selten zu langsam — meist ist der Code zu schleifenlastig.

Die drei großen Hebel

  1. Speicher vorbelegen statt wachsen lassen. Der mit Abstand häufigste Grund für langsamen R-Code.
  2. Vektorisieren. Die Schleife in kompilierten Code verlegen, statt sie im Interpreter zu fahren.
  3. Erst messen, dann optimieren. Der Engpass sitzt fast nie dort, wo man ihn vermutet.
# langsam: das Ergebnis wächst — R kopiert bei jedem Schritt den ganzen Vektor
erg <- c()
for (i in 1:1e5) erg <- c(erg, i^2)

# besser: volle Länge vorbelegen
erg <- numeric(1e5)
for (i in seq_len(1e5)) erg[i] <- i^2

# am besten: gar keine Schleife
erg <- seq_len(1e5)^2

Messen

system.time(langsame_funktion(x))            # grob
bench::mark(variante_a(x), variante_b(x))    # sauberer Vergleich inkl. Speicher
Rprof("profil.out"); analyse(); Rprof(NULL); summaryRprof("profil.out")
profvis::profvis(analyse())                   # grafisch, Zeile für Zeile
object.size(df); lobstr::obj_size(df)          # Speicherbedarf

Wenn das nicht reicht

data.table
Ändert Spalten mit := ohne Kopie, hat Schlüssel und binäre Suche. Bei Millionen Zeilen oft um Größenordnungen schneller.
duckdb / arrow
Analysen auf Daten, die nicht in den Arbeitsspeicher passen; SQL beziehungsweise dplyr direkt auf Parquet-Dateien.
Rcpp
C++-Funktionen direkt aus R aufrufbar. Für echte Rekursion und Schleifen, die sich nicht vektorisieren lassen, sind Faktoren von 50–100 üblich.
future / furrr / parallel
Parallelisierung über CPU-Kerne. future::plan(multisession) plus furrr::future_map() genügt oft, um bestehenden purrr-Code zu parallelisieren.
matrixStats / collapse
Optimierte Ersatzfunktionen für Zeilen- und Spaltenstatistiken.
Speichergrenzen
R hält Daten im Arbeitsspeicher und braucht dabei oft ein Mehrfaches der Dateigröße. gc() gibt frei, rm(objekt) entfernt — die eigentliche Lösung ist meist, nur die benötigten Spalten einzulesen.
16

Typische Fallstricke

Die Fehler, die jeder einmal macht — gesammelt.

SituationWas passiertRichtig
x == NAergibt NA, nie TRUEis.na(x)
0.1 + 0.2 == 0.3FALSE (Fließkomma)isTRUE(all.equal(a, b))
1:length(x) bei leerem xläuft über c(1, 0)seq_along(x)
df[, "a"]gibt manchmal Vektor, manchmal Tabelledrop = FALSE oder df$a
sapply() in SkriptenErgebnistyp hängt von den Daten abvapply() oder purrr::map_*()
T und Fsind Variablen und überschreibbarimmer TRUE / FALSE
as.numeric(faktor)liefert die internen Codesas.numeric(as.character(f))
df[df$x > 5, ] mit NAerzeugt NA-Zeilenwhich(), subset(), filter()
mean(x) mit NAErgebnis ist NAmean(x, na.rm = TRUE) — bewusst
Recycling ohne Warnungstille FehlberechnungLängen prüfen mit stopifnot()
setwd("C:/Users/ich/...")läuft nur auf einem RechnerRStudio-Projekt + here::here()
attach(df)verdeckt Namen, unauffindbare Fehlerwith(df, ...) oder df$spalte
library() im Paketcodeverändert die Sitzung des NutzersImports: in DESCRIPTION
Partial matching von Listenl$na trifft l$namel[["name"]] (exakt)
Historisch entschärft

Bis R 3.6 wandelte data.frame() und read.csv() Textspalten automatisch in Faktoren um — die berüchtigte stringsAsFactors-Voreinstellung, Ursache unzähliger merkwürdiger Fehler. Seit R 4.0.0 ist die Voreinstellung FALSE. Ältere Anleitungen im Netz setzen das Gegenteil voraus; wer sie liest, sollte das Erscheinungsdatum beachten.

Guter Stil

  • <- für Zuweisungen, = für Argumente.
  • snake_case für eigene Namen; das Ökosystem ist uneinheitlich (read.csv, readRDS, read_csv), aber der eigene Code muss es nicht sein.
  • Keine Punkte in eigenen Funktionsnamen — der Punkt ist in S3 der Methodentrenner.
  • Skripte enden nicht mit rm(list = ls()); starte lieber eine frische Sitzung.
  • Der Arbeitsbereich wird nicht gespeichert. In RStudio: „Save workspace to .RData on exit" auf Never.
  • styler formatiert automatisch, lintr prüft auf Stil und typische Fehler.
17

Eigene Pakete schreiben

In R ist das Paket nicht die große, sondern die naheliegende Form — auch für interne Analysen.

Ein Paket ist in R kein schweres Geschütz. Sobald derselbe Code in mehreren Projekten gebraucht wird, ist es der bequemste Weg: Dokumentation, Tests, Abhängigkeiten und Versionierung sind vorgesehen und werkzeugunterstützt.

library(usethis)
create_package("~/code/meinpaket")
use_git()
use_mit_license("Vorname Nachname")
use_r("kennzahlen")          # legt R/kennzahlen.R an
use_test("kennzahlen")       # legt tests/testthat/test-kennzahlen.R an
use_package("dplyr")         # Abhängigkeit in DESCRIPTION eintragen
use_vignette("einfuehrung")
use_github_action("check-standard")
PfadInhalt
DESCRIPTIONName, Version, Autoren, Lizenz, Abhängigkeiten (Imports, Suggests)
NAMESPACEWas exportiert und was importiert wird — wird von roxygen2 erzeugt
R/Der Quellcode, ausschließlich Funktionsdefinitionen
man/Hilfeseiten (.Rd) — ebenfalls erzeugt, nicht von Hand geschrieben
tests/testthat/Automatische Tests
vignettes/Ausführliche Einführungen als Quarto/Rmd
data/Mitgelieferte Beispieldatensätze
inst/Beliebige weitere Dateien, landen bei Installation im Paketverzeichnis

Dokumentation mit roxygen2

Die Hilfeseite steht als Kommentar über der Funktion und wird daraus erzeugt — Code und Dokumentation bleiben zusammen.

#' Kennzahlen einer Messreihe berechnen
#'
#' Berechnet Umfang, Mittelwert und Standardabweichung.
#'
#' @param x Ein numerischer Vektor.
#' @param na.rm Sollen fehlende Werte entfernt werden? Voreinstellung `TRUE`.
#' @return Eine Liste mit den Elementen `n`, `mittel` und `sd`.
#' @examples
#' kennzahl(c(4, 8, NA, 15))
#' @export
kennzahl <- function(x, na.rm = TRUE) { ... }

Tests mit testthat

test_that("kennzahl behandelt NA korrekt", {
  expect_equal(kennzahl(c(2, NA, 4))$n, 2)
  expect_equal(kennzahl(c(2, NA, 4))$mittel, 3)
  expect_true(is.na(kennzahl(c(2, NA), na.rm = FALSE)$mittel))
  expect_error(kennzahl("text"), "numerisch")
})

Der Entwicklungszyklus

library(devtools)
load_all()      # Strg+Shift+L — Paket laden, ohne zu installieren
document()      # Strg+Shift+D — NAMESPACE und man/ aus roxygen erzeugen
test()          # Strg+Shift+T — Tests laufen lassen
check()         # Strg+Shift+E — die vollständige CRAN-Prüfung
install()       # in die Bibliothek installieren

check() ist der eigentliche Qualitätsanker: Es prüft Syntax, führt alle Beispiele und Tests aus, kontrolliert die Dokumentation auf Vollständigkeit und meldet undeklarierte Abhängigkeiten. Ein Paket, das R CMD check ohne Warnung besteht, ist auf einem anderen Rechner mit hoher Wahrscheinlichkeit lauffähig.

18

R im Vergleich zu Python

Nicht besser oder schlechter — verschieden gebaut, für verschiedene Schwerpunkte.

 RPython
HerkunftStatistikallgemeine Programmierung
Tabelleneingebaut (data.frame)Bibliothek (pandas, polars)
Indizierungab 1, Ende einschließendab 0, Ende ausschließend
Statistische Verfahrenim Sprachkern und in CRAN, sehr breitstatsmodels, scipy, schmaler
Grafikggplot2 gilt als Referenzmatplotlib, plotnine, altair
Deep Learningüber Schnittstellen möglichder Standard
ProduktivbetriebShiny, plumber, Berichtebreite Web- und Infrastruktur-Ökosysteme
BerichtswesenQuarto/R Markdown, sehr ausgereiftJupyter, Quarto
Objektorientierungvier Systeme, S3 dominiertein einheitliches Klassenmodell

Die Frage stellt sich in der Praxis seltener als gedacht, weil beides zusammen läuft: reticulate ruft Python aus R auf (und umgekehrt), Quarto mischt beide Sprachen im selben Dokument, arrow und Parquet übergeben Daten verlustfrei zwischen den Ökosystemen. Eine gängige Arbeitsteilung: Modelltraining in Python, Auswertung, Grafik und Bericht in R.

Als grobe Orientierung: Wo Analyse, statistische Modellierung, Visualisierung und Bericht das Ziel sind, ist R oft der kürzere Weg. Wo Software das Ziel ist, in die Analyse eingebettet wird, spricht mehr für Python.

19

Lernpfad und Quellen

In welcher Reihenfolge sich der Aufwand am schnellsten auszahlt.

Reihenfolge

  1. Vektoren und Indizierung wirklich verstehen — einschließlich [ gegen [[. Wer das hat, versteht den Rest der Sprache.
  2. data.frame und dplyr — damit ist man an realen Daten arbeitsfähig.
  3. ggplot2 — früh anfangen, es verändert, wie man Daten ansieht.
  4. Funktionen schreiben, sobald sich Code zum zweiten Mal wiederholt.
  5. Quarto — ab hier entstehen Ergebnisse, die man weitergeben kann.
  6. apply/purrr statt Schleifen — der Sprung zum idiomatischen R.
  7. Eigenes Paket mit Tests — der Schritt vom Skript zur Software.

Quellen

R for Data Science
Wickham, Çetinkaya-Rundel & Grolemund, 2. Auflage, frei unter r4ds.hadley.nz. Der Standardeinstieg für die Datenarbeit.
Advanced R
Wickham, frei unter adv-r.hadley.nz. Wie die Sprache funktioniert: Umgebungen, Auswertung, Objektsysteme, Metaprogrammierung. Das Buch, nach dem R aufhört, überraschend zu sein.
R Packages
Wickham & Bryan, frei unter r-pkgs.org. Paketentwicklung von Anfang bis CRAN.
An Introduction to R
Das offizielle Handbuch des R Core Teams, mitinstalliert und über cran.r-project.org/manuals.html abrufbar. Knapp, präzise, autoritativ.
CRAN Task Views
Kuratierte Paketübersichten je Fachgebiet — der schnellste Weg von einer Fragestellung zum passenden Paket.
Posit Cheatsheets
Zweiseitige Übersichten zu dplyr, ggplot2, tidyr, purrr, Shiny. Ausdrucken lohnt sich.
Posit Community & Stack Overflow
Beim Fragen ein Reprex mitschicken (Paket reprex): ein minimales, selbstlauffähiges Beispiel. Das ist in der R-Gemeinschaft feste Umgangsform.
The R Journal
Begutachtete Artikel zu neuen Paketen und Methoden.
Zum Schluss

R belohnt eine bestimmte Denkweise: Beschreibe, was mit den Daten geschehen soll, nicht in welchen Schritten der Rechner es tut. Wer aus einer imperativen Sprache kommt, schreibt zuerst Schleifen und ärgert sich über die Geschwindigkeit. Der Umstieg auf vektorisiertes Denken ist die eigentliche Lernkurve — und danach ist R eine ungewöhnlich direkte Sprache für die Arbeit mit Daten.