Vektoren & Grundlagen
Variablen, Typen & Zuweisung
R verwendet <- als bevorzugten Zuweisungsoperator (= funktioniert, kann aber in Funktionsargumenten mehrdeutig sein). Alles in R ist ein Vektor – selbst eine einzelne Zahl ist ein Vektor der Länge 1. Die Kerntypen sind character, numeric (double), integer, logical und complex. NA repräsentiert fehlende Daten und propagiert durch Operationen (na.rm=TRUE verwenden, um zu überspringen). NULL ist die Abwesenheit eines Wertes (ein leeres Objekt), verschieden von NA. Vor der Analyse immer auf NAs prüfen.
# assignment operators (all equivalent for simple values)
name <- "Alice" # preferred
age = 30L # = also works (avoid in functions)
30L -> age2 # rightward assignment
# basic types (called "modes" in R)
class("text") # "character"
class(42) # "numeric" (double)
class(42L) # "integer"
class(3.14) # "numeric"
class(TRUE) # "logical"
class(2 + 3i) # "complex"
# check and convert types
is.numeric(age) # TRUE
is.character(name) # TRUE
as.integer(3.9) # 3 (truncates, not rounds)
as.character(42) # "42"
as.numeric("3.14") # 3.14
# special values
NA # missing value (Not Available)
NULL # null object (no value)
NaN # Not a Number (0/0)
Inf # infinity (1/0)
is.na(NA) # TRUE
is.null(NULL) # TRUEVektoren erstellen & indizieren
c() kombiniert Werte zu einem Vektor – die fundamentalste R-Funktion. R ist 1-indiziert (erstes Element ist [1], nicht [0]). Negative Indizes SCHLIESSEN Elemente aus: nums[-1] verwirft das erste. Logische Indizierung (nums[nums > 3]) filtert nach Bedingung – extrem mächtig. Vektoren können Namen haben, was Zugriff nach Label ermöglicht. Alle Elemente eines Vektors müssen denselben Typ haben; beim Mischen von Typen koerziert R (z.B. c(1, 'a') wird c('1', 'a')).
# create vectors with c() (combine)
nums <- c(1, 2, 3, 4, 5)
chars <- c("a", "b", "c")
logs <- c(TRUE, FALSE, TRUE)
# sequences
1:5 # 1 2 3 4 5
seq(1, 10, by = 2) # 1 3 5 7 9
seq(0, 1, length.out = 5) # 0 0.25 0.5 0.75 1
rep(1:3, times = 2) # 1 2 3 1 2 3
rep(1:3, each = 2) # 1 1 2 2 3 3
# indexing (1-indexed!)
nums[1] # 1 (first element)
nums[length(nums)] # 5 (last element)
nums[c(1, 3, 5)] # 1 3 5 (multiple indices)
nums[-1] # 2 3 4 5 (all EXCEPT first)
nums[-c(1, 2)] # 3 4 5 (exclude first two)
nums[2:4] # 2 3 4 (range)
# logical indexing
nums[nums > 2] # 3 4 5
nums[nums %% 2 == 0] # 2 4 (even numbers)
nums[nums > 2] <- 0 # modify in place
# named vectors
ages <- c(alice = 30, bob = 25, carol = 28)
ages["alice"] # 30
ages[c("alice", "bob")] # 30 25Vektor-Operationen & Funktionen
Rs Vektorisierung ist ihr Signature-Feature – Operationen gelten automatisch elementweise, ohne Schleifen. Recycling verwendet den kürzeren Vektor erneut, um zum längeren zu passen (c(1,2,3,4) + c(10,20) ergibt 11,22,13,24). order() gibt die Indizes zurück, die den Vektor sortieren würden – unerlässlich zum Sortieren eines Vektors nach einem anderen. unique() entfernt Duplikate. All diese Funktionen sind optimierter C-Code unter der Haube, was R für Vektor-Operationen schnell macht.
# arithmetic is element-wise (vectorized)
a <- c(1, 2, 3)
b <- c(4, 5, 6)
a + b # 5 7 9
a * b # 4 10 18
a ^ 2 # 1 4 9
a / b # 0.25 0.4 0.5
# recycling: shorter vector repeats
c(1, 2, 3, 4) + c(10, 20) # 11 22 13 24
# summary functions
sum(a) # 6
mean(a) # 2
median(a) # 2
sd(a) # 1
var(a) # 1
min(a); max(a) # 1; 3
range(a) # 1 3
cumsum(a) # 1 3 6
cumprod(a) # 1 2 6
# sorting and ordering
sort(c(3, 1, 2)) # 1 2 3
sort(c(3, 1, 2), decreasing = TRUE) # 3 2 1
order(c(3, 1, 2)) # 2 3 1 (indices that would sort)
v[order(v)] # sort using order
# useful functions
length(a) # 3
unique(c(1, 1, 2, 2, 3)) # 1 2 3
rev(a) # 3 2 1
head(a, 2) # 1 2 (first n)
tail(a, 2) # 2 3 (last n)Character- & String-Manipulation
paste/paste0 sind Rs String-Konkatenationsfunktionen – paste0 hat keinen Separator (wie Python's +). substr extrahiert Teilstrings (1-indiziert). gsub ersetzt alle Treffer; sub ersetzt nur den ersten. grep gibt Indizes übereinstimmender Elemente zurück; grepl gibt einen logischen Vektor zurück (nützlicher zum Filtern). R verwendet standardmäßig POSIX-extended-Regex. sprintf bietet C-Stil-Formatierung. Das stringr-Paket (tidyverse) bietet eine sauberere, konsistentere API.
# paste and paste0 (concatenation)
paste("Hello", "World") # "Hello World"
paste("Hello", "World", sep = "_") # "Hello_World"
paste0("a", "b", "c") # "abc" (no separator)
paste(c("a", "b", "c"), collapse = "-") # "a-b-c"
paste("file", 1:3, ".csv", sep = "") # "file1.csv" "file2.csv" "file3.csv"
# case conversion
toupper("hello") # "HELLO"
tolower("WORLD") # "world"
# substring
substr("Hello World", 1, 5) # "Hello"
nchar("Hello") # 5 (character count)
# split and replace
strsplit("a,b,c", ",")[[1]] # "a" "b" "c"
gsub("o", "0", "Hello World") # "Hell0 W0rld" (all matches)
sub("o", "0", "Hello World") # "Hell0 World" (first match only)
gsub("[0-9]+", "N", "a1b22c333") # "aNbNcN" (regex)
# grep and grepl (pattern matching)
grep("^A", c("Alice", "Bob", "Anna")) # 1 3 (indices)
grepl("^A", c("Alice", "Bob")) # TRUE FALSE
# sprintf (C-style formatting)
sprintf("Pi = %.2f", pi) # "Pi = 3.14"
sprintf("%s is %d", "Alice", 30) # "Alice is 30"Fehlende Werte & Koersion
NA (Not Available) repräsentiert fehlende Daten und propagiert durch die meisten Operationen – immer na.rm=TRUE verwenden oder herausfiltern. NULL ist anders: es ist die Abwesenheit eines Wertes und wird aus Vektoren gedroppt. R koerziert zum allgemeinsten Typ beim Kombinieren (logical < integer < numeric < character). as.numeric auf nicht-numerische Strings produziert NA mit einer Warnung. ifelse ist der vektorisierte ternäre Operator – extrem nützlich zum Erstellen kategorialer Variablen aus kontinuierlichen.
# NA propagation and handling
x <- c(1, 2, NA, 4, 5)
mean(x) # NA (NA propagates!)
mean(x, na.rm = TRUE) # 3 (skip NA)
sum(x, na.rm = TRUE) # 12
is.na(x) # FALSE FALSE TRUE FALSE FALSE
sum(is.na(x)) # 1 (count of NAs)
x[!is.na(x)] # 1 2 4 5 (remove NAs)
# NULL vs NA
length(c(1, NA, 3)) # 3 (NA is an element)
length(c(1, NULL, 3)) # 2 (NULL is dropped)
# type coercion hierarchy: logical < integer < double < character
c(TRUE, 1) # 1 1 (logical -> numeric)
c(1L, 2.5) # 1.0 2.5 (integer -> double)
c(1, "a") # "1" "a" (numeric -> character)
# explicit coercion
as.numeric(c("1", "2", "abc")) # 1 2 NA (with warning)
as.logical(c(0, 1, 2)) # FALSE TRUE TRUE
factor(c("low", "high", "low")) # factor with 2 levels
# ifelse vectorized conditional
ifelse(x > 2, "big", "small") # "small" "small" NA "big" "big"Datenstrukturen
Listen (heterogene Container)
Listen sind Rs flexibelste Datenstruktur – sie können Elemente beliebigen Typs und Größe halten (wie Python-Dicts oder JavaScript-Objekte). Der $-Operator ist ein praktischer Shortcut für benannten Zugriff. Die kritische Unterscheidung: [ ] gibt eine Sub-Liste zurück (immer noch eine Liste), während [[ ]] das tatsächliche Element extrahiert. Dies ist die #1 Verwirrungsquelle für R-Anfänger. [[ ]] verwenden, wenn der Wert selbst gewünscht wird, [ ] zum Subsetting. lapply/sapply iterieren über List-Elemente und wenden eine Funktion an.
# lists can hold different types and sizes
user <- list(
name = "Alice",
age = 30,
scores = c(90, 85, 88),
active = TRUE
)
# access by name ($ or [[]])
user$name # "Alice"
user[["age"]] # 30
user[["scores"]][2] # 85
# [] returns a sublist (list); [[]] returns the element
user["name"] # list with one element
user[["name"]] # "Alice" (the string itself)
user[1:2] # sublist with first 2 elements
# modify and add
user$age <- 31
user$email <- "[email protected]" # add new element
user[["scores"]] <- NULL # remove element
# iterate over a list
for (key in names(user)) {
cat(key, ":", user[[key]], "\n")
}
# lapply and sapply on lists
lapply(user$scores, sqrt) # list of square roots
sapply(user$scores, sqrt) # vector of square rootsData Frames
Data Frames sind Rs primäre tabellarische Datenstruktur – wie eine Tabelle oder SQL-Tabelle, wo jede Spalte ein anderer Typ sein kann. Auf Spalten mit $ oder [[ ]] zugreifen; Zeilen mit logischer Indizierung filtern (df[df$age > 25, ]). subset() ist eine sauberere Alternative. cbind fügt Spalten hinzu; rbind fügt Zeilen hinzu. str() zeigt die Struktur (Typen und Vorschau). Immer stringsAsFactors=FALSE setzen (oder R 4.0+ verwenden, wo dies der Standard ist), um Strings als Characters zu behalten, nicht als Factors.
# create a data frame (like a table/spreadsheet)
df <- data.frame(
name = c("Alice", "Bob", "Carol"),
age = c(30, 25, 28),
score = c(90, 85, 88),
stringsAsFactors = FALSE
)
# dimensions
nrow(df) # 3
ncol(df) # 3
dim(df) # 3 3
names(df) # "name" "age" "score"
str(df) # structure summary
head(df, 2) # first 2 rows
# access columns
df$name # vector (by name)
df[["age"]] # vector (by name, alternative)
df[, "age"] # vector (column)
df[, 2] # vector (by index)
df[2] # data frame with one column
# access rows
df[1, ] # first row (as data frame)
df[1:2, ] # first 2 rows
df[c(1, 3), ] # rows 1 and 3
# filter rows (logical indexing)
df[df$age > 26, ] # rows where age > 26
df[df$score >= 88, c("name", "score")]
subset(df, age > 26, select = c(name, score))
# add and modify columns
df$grade <- c("A", "B", "A") # add column
df$age <- df$age + 1 # modify column
df <- cbind(df, pass = df$score > 60) # column bind
# add rows
new_row <- data.frame(name = "Dan", age = 22, score = 75, grade = "C")
df <- rbind(df, new_row)Matrizen & Arrays
Matrizen sind 2D-Arrays, bei denen ALLE Elemente denselben Typ haben müssen (anders als Data Frames). %*% ist Matrixmultiplikation; * ist elementweise. solve() berechnet die Matrix-Inverse; det() die Determinante. rowSums/colSums/rowMeans/colMeans sind schnelle eingebaute Shortcuts. apply(m, MARGIN, FUN) ist der allgemeine Weg, eine Funktion über Zeilen (MARGIN=1) oder Spalten (MARGIN=2) anzuwenden. Arrays erweitern Matrizen auf n Dimensionen. Für Datenanalyse Data Frames bevorzugen; Matrizen für Lineare Algebra verwenden.
# create a matrix (2D, all same type)
m <- matrix(1:12, nrow = 3, ncol = 4)
# [,1] [,2] [,3] [,4]
# [1,] 1 4 7 10
# [2,] 2 5 8 11
# [3,] 3 6 9 12
# fill by row instead of column
m2 <- matrix(1:6, nrow = 2, byrow = TRUE)
# dimensions and attributes
dim(m) # 3 4
nrow(m) # 3
ncol(m) # 4
rownames(m) <- c("r1", "r2", "r3")
colnames(m) <- c("c1", "c2", "c3", "c4")
# indexing
m[2, 3] # 8 (single element)
m[1, ] # 1 4 7 10 (first row)
m[, 2] # 4 5 6 (second column)
m[1:2, 2:3] # 2x2 submatrix
m["r1", "c2"] # 4 (by name)
# matrix operations
t(m) # transpose
m * m # element-wise multiply
m %*% t(m) # matrix multiplication
solve(m[, 1:3]) # inverse (square matrix)
det(matrix(1:4, 2)) # determinant
rowSums(m) # sum of each row
colMeans(m) # mean of each column
apply(m, 1, sum) # row sums (general)
apply(m, 2, max) # column maxima
# arrays (n-dimensional)
arr <- array(1:24, dim = c(2, 3, 4)) # 2x3x4 array
arr[1, 2, 3] # single elementFactors & kategoriale Daten
Factors speichern kategoriale Daten effizient als Integer-Codes mit Label-Mappings – unerlässlich für statistische Modellierung (lm, glm verwenden Factors für Gruppierung). Ein häufiger Fallstrick: as.numeric(factor) gibt die Integer-CODES, nicht die ursprünglichen Werte – immer erst via as.character konvertieren. cut() teilt kontinuierliche Daten in Factor-Level ein. ordered=TRUE erstellt ordinale Factors, die Vergleichsoperatoren unterstützen. relevel ändert die Referenzkategorie (wichtig für Regression-Interpretation). table() produziert Häufigkeitszählungen.
# factors represent categorical data (stored as integers with labels)
gender <- factor(c("male", "female", "male", "female"))
print(gender)
# [1] male female male female
# Levels: female male
levels(gender) # "female" "male" (sorted alphabetically)
table(gender) # frequency table
nlevels(gender) # 2
# ordered factor (for ordinal data)
size <- factor(c("M", "S", "L", "XL"),
levels = c("S", "M", "L", "XL"),
ordered = TRUE)
size[1] > size[2] # TRUE (M > S)
# convert to factor and back
nums <- factor(c(1, 2, 3, 2, 1))
as.numeric(nums) # 1 2 3 2 1 (level codes, NOT original values!)
as.numeric(as.character(nums)) # 1 2 3 2 1 (correct way)
# relevel (change reference level)
gender <- relevel(gender, ref = "male")
# cut: convert numeric to factor (binning)
ages <- c(15, 25, 35, 45, 55, 65)
age_groups <- cut(ages, breaks = c(0, 18, 35, 50, 100),
labels = c("child", "young", "adult", "senior"))
table(age_groups) # frequency per group
# interaction of factors
f1 <- factor(c("A", "A", "B", "B"))
f2 <- factor(c("X", "Y", "X", "Y"))
interaction(f1, f2) # A.X A.Y B.X B.YListen zu Data Frames & Reshaping
Reshaping zwischen Wide- und Long-Formaten ist eine häufige Data-Wrangling-Aufgabe. pivot_longer/pivot_wider (tidyr, tidyverse) sind die modernen, intuitiven Funktionen. Wide-Format hat eine Zeile pro Subjekt mit Spalten für jeden Zeitpunkt; Long-Format hat eine Zeile pro Beobachtung. Long-Format wird für ggplot2 und die meisten Analysen bevorzugt. split() teilt einen Data Frame in eine Liste nach einem Factor; do.call(rbind, ...) kombiniert neu. Die base-R-reshape()-Funktion ist mächtig, hat aber eine verwirrende Oberfläche – tidyr bevorzugen.
# convert list to data frame
my_list <- list(
a = 1:3,
b = c("x", "y", "z")
)
df <- as.data.frame(my_list)
# stack multiple vectors
do.call(rbind, list(
data.frame(name = "A", val = 1),
data.frame(name = "B", val = 2)
))
# reshape: wide to long
library(tidyr)
wide_df <- data.frame(
id = 1:2,
q1 = c(10, 20),
q2 = c(15, 25)
)
long_df <- pivot_longer(wide_df, cols = starts_with("q"),
names_to = "quarter", values_to = "value")
# long to wide
pivot_wider(long_df, names_from = quarter, values_from = value)
# base R reshape (without tidyr)
reshape(wide_df, direction = "long",
varying = c("q1", "q2"), v.names = "value",
timevar = "quarter", idvar = "id")
# split and combine
split_results <- split(df, df$group) # list of data frames by group
combined <- do.call(rbind, split_results) # recombine
# melt and cast (reshape2 package, legacy)
# library(reshape2)
# melt(wide_df, id.vars = "id")
# dcast(long_df, id ~ quarter, value.var = "value")Kontrollfluss & Funktionen
If / Else & Switch
Rs if/else erfordert geschweifte Klammern für mehrzeilige Bodies und das else muss auf derselben Zeile wie die schließende Klammer stehen (sonst denkt R, das if sei vollständig). ifelse(test, yes, no) ist vektorisiert – es gilt für ganze Vektoren gleichzeitig und gibt einen Vektor von Ergebnissen zurück. Für mehrere Bedingungen ist dplyrs case_when viel sauberer als verschachteltes ifelse. switch dispatcht nach einem String (oder numerischer Position) – eine saubere Alternative zu langen if-else-Ketten.
# if-else if-else
score <- 85
if (score >= 90) {
grade <- "A"
} else if (score >= 80) {
grade <- "B"
} else {
grade <- "C"
}
# ifelse (vectorized ternary)
ages <- c(15, 25, 35, 45)
ifelse(ages >= 18, "adult", "minor")
# "minor" "adult" "adult" "adult"
# nested ifelse (avoid deep nesting!)
ifelse(ages < 18, "minor",
ifelse(ages < 65, "adult", "senior"))
# dplyr::case_when is cleaner for multiple conditions
# case_when(
# ages < 18 ~ "minor",
# ages < 65 ~ "adult",
# TRUE ~ "senior"
# )
# switch (dispatch on a value)
day_type <- function(day) {
switch(day,
"Mon" = "weekday",
"Tue" = "weekday",
"Wed" = "weekday",
"Thu" = "weekday",
"Fri" = "weekday",
"Sat" = "weekend",
"Sun" = "weekend",
"unknown"
)
}Schleifen: For, While, Repeat
for-Schleifen in R iterieren über Elemente (oder eine Sequenz). seq_along(x) ist sicherer als 1:length(x), wenn x leer sein könnte (es gibt integer(0) statt c(1,0) zurück). next springt zur nächsten Iteration (wie continue); break verlässt. repeat ist eine Endlosschleife, die explizit abgebrochen werden muss. IMMER Ergebnis-Vektoren vorinitialisieren (result <- numeric(N)) – das Wachsen eines Vektors in einer Schleife mit c() ist O(n²) und extrem langsam. Jedoch vektorisierte Operationen oder die apply-Familie gegenüber Schleifen bevorzugen, wenn möglich.
# for loop
for (i in 1:5) {
print(i)
}
# iterate over a vector
fruits <- c("apple", "banana", "cherry")
for (fruit in fruits) {
print(paste("Fruit:", fruit))
}
# iterate with index
for (i in seq_along(fruits)) {
print(paste(i, fruits[i]))
}
# while loop
count <- 0
while (count < 5) {
count <- count + 1
if (count == 3) next # skip (like continue)
print(count)
}
# repeat loop (infinite, must break)
i <- 0
repeat {
i <- i + 1
if (i >= 3) break # exit loop
print(i)
}
# preallocate for speed (IMPORTANT!)
result <- numeric(1000)
for (i in 1:1000) {
result[i] <- i^2
}
# nested loops
mat <- matrix(0, 3, 3)
for (i in 1:3) {
for (j in 1:3) {
mat[i, j] <- i * j
}
}Funktionsdefinition & Argumente
R-Funktionen geben automatisch den zuletzt ausgewerteten Ausdruck zurück (kein explizites return nötig, obwohl return() für frühe Exits klarer ist). Default-Argumente machen Funktionen flexibel. Die ... (Ellipse) erfasst zusätzliche Argumente zum Weiterreichen – unerlässlich für Wrapper-Funktionen. Benannte Argumente können in beliebiger Reihenfolge sein. R verwendet Lazy Evaluation: Argumente werden nur beim ersten Gebrauch ausgewertet, sodass ungenutzte Argumente keine Fehler verursachen. Mehrere Werte durch Paketieren in einer Liste zurückgeben.
# basic function (last expression is returned)
add <- function(a, b) {
a + b
}
add(3, 4) # 7
# explicit return
is_positive <- function(x) {
if (x > 0) return(TRUE)
FALSE
}
# default arguments
greet <- function(name, greeting = "Hello", punctuation = "!") {
paste0(greeting, ", ", name, punctuation)
}
greet("Alice") # "Hello, Alice!"
greet("Bob", greeting = "Hi") # "Hi, Bob!"
greet(name = "Carol", punctuation = "?") # named args
# ... (ellipsis: pass arguments through)
my_plot <- function(x, y, ...) {
plot(x, y, col = "blue", ...)
}
my_plot(1:10, 1:10, main = "My Plot", type = "l")
# return multiple values via list
stats <- function(x) {
list(mean = mean(x), sd = sd(x), n = length(x))
}
result <- stats(1:10)
result$mean # 5.5
# lazy evaluation (args evaluated only when used)
f <- function(a, b) {
a * 2 # b is never used, so not evaluated
}
f(5) # 10 (no error despite missing b)Die apply-Familie
Die apply-Familie ersetzt Schleifen durch funktionale Iteration – idiomatischer und oft schneller. lapply gibt immer eine Liste zurück; sapply versucht, zu einem Vektor/einer Matrix zu vereinfachen (praktisch, aber unberechenbar); vapply ist die sichere Version mit garantiertem Rückgabetyp. apply arbeitet auf Matrizen (MARGIN=1 für Zeilen, 2 für Spalten). tapply gruppiert Daten nach einem Factor und wendet eine Funktion an – wie ein mini GROUP BY. replicate wiederholt zufällige Simulationen. Für Data Frames bietet das purrr-Paket (tidyverse) eine sauberere, konsistentere map()-Familie.
# lapply: apply function to each element of a list, returns list
my_list <- list(a = 1:3, b = 4:6, c = 7:9)
lapply(my_list, mean) # list: 2, 5, 8
lapply(my_list, sum) # list: 6, 15, 24
# sapply: like lapply but simplifies to vector/matrix
sapply(my_list, mean) # 2 5 8 (named vector)
sapply(my_list, range) # matrix with 2 rows
# vapply: like sapply but with guaranteed output type (safer)
vapply(my_list, mean, numeric(1)) # always numeric vector
# apply: apply function over matrix/array margins
m <- matrix(1:12, nrow = 3)
apply(m, 1, sum) # row sums: 22 26 30
apply(m, 2, mean) # column means: 2 5 8 11
apply(m, c(1, 2), sqrt) # element-wise sqrt
# mapply: multivariate apply (vectorized over multiple args)
mapply(rep, 1:3, 3:1) # rep(1,3), rep(2,2), rep(3,1)
# tapply: apply function by groups
df <- data.frame(
group = c("A", "A", "B", "B", "B"),
value = c(10, 20, 30, 40, 50)
)
tapply(df$value, df$group, mean) # A: 15, B: 40
# replicate: repeat an expression n times
replicate(3, mean(rnorm(10))) # 3 random meansScoping & Environments
R verwendet lexikalisches Scoping: Funktionen suchen freie Variablen in der Environment, wo sie definiert wurden (nicht wo sie aufgerufen werden). Dies ermöglicht Closures – Funktionen, die ihre umschließende Environment erfassen. Der <<- Operator weist einer Variablen in der Parent-Environment zu (Super-Zuweisung), was wie Closures Zustand erhalten (wie das Counter-Beispiel). Jeder Funktionsaufruf erstellt eine neue Environment. Der Suchpfad (search()) bestimmt, wo R nach Objekten sucht – globalenv ist der Workspace, gefolgt von angehängten Paketen.
# lexical scoping: functions look up variables in defining environment
y <- 10
f <- function(x) {
x + y # y found in global env
}
f(5) # 15
# local variables shadow globals
g <- function(x) {
y <- 100 # local y
x + y
}
g(5) # 105
y # 10 (global unchanged)
# <<- assigns to parent environment (super-assignment)
counter <- function() {
count <- 0
function() {
count <<- count + 1 # modifies count in enclosing scope
count
}
}
c1 <- counter()
c1() # 1
c1() # 2
# search path for variables
search() # shows environments: globalenv, package namespaces
ls() # list objects in current environment
ls(envir = .GlobalEnv) # explicitly global
exists("y") # TRUE if variable exists
# new environment (isolated scope)
e <- new.env()
e$x <- 42
e$x # 42 (separate from global x)Datenmanipulation (dplyr & tidyr)
dplyr: Filter, Select & Arrange
dplyr (Teil von tidyverse) bietet intuitive Verben für Datenmanipulation, die SQL-Operationen spiegeln. filter selektiert Zeilen nach Bedingung; select wählt Spalten; arrange sortiert. Der %in%-Operator testet Mitgliedschaft. Helfer-Funktionen wie starts_with, ends_with, contains machen Spaltenauswahl flexibel. rename() ändert Spaltennamen ohne Kopieren. Diese Verben komponieren mit der Pipe (%>%) für lesbare Datenpipelines. dplyr ist viel schneller als base R für große Daten, weil es intern C++ verwendet.
library(dplyr)
df <- data.frame(
name = c("Alice", "Bob", "Carol", "Dan"),
age = c(30, 25, 28, 35),
dept = c("Eng", "Sales", "Eng", "Sales"),
salary = c(80000, 50000, 75000, 90000)
)
# filter rows (like WHERE in SQL)
filter(df, age > 26)
filter(df, dept == "Eng" & salary > 70000)
filter(df, dept %in% c("Eng", "Sales"))
filter(df, age > 26 | salary > 85000)
# select columns (like SELECT in SQL)
select(df, name, age)
select(df, name:dept) # range of columns
select(df, -salary) # exclude column
select(df, starts_with("s")) # columns starting with 's'
select(df, ends_with("e")) # columns ending with 'e'
select(df, contains("am")) # columns containing 'am'
select(df, everything()) # all columns (useful for reordering)
# arrange (sort, like ORDER BY)
arrange(df, age) # ascending
arrange(df, desc(age)) # descending
arrange(df, dept, desc(salary)) # sort by dept, then salary desc
# rename columns
rename(df, years = age, department = dept)
# distinct rows
distinct(df, dept) # unique departments
distinct(df, dept, .keep_all = TRUE) # keep all columnsdplyr: Mutate, Summarize & Group By
mutate fügt Spalten hinzu oder modifiziert sie (vektorisiert). summarize reduziert jede Gruppe auf eine einzelne Summary-Zeile. Die Macht kommt von group_by + summarize – Rs Äquivalent von SQL GROUP BY. n() zählt Zeilen; across() wendet eine Funktion auf mehrere Spalten an (neu in dplyr 1.0). Window-Funktionen (rank, cumsum, lag, lead) operieren innerhalb von Gruppen und ermöglichen Berechnungen wie 'Rang innerhalb Abteilung'. Die Pipe %>% verkettet Operationen von links nach rechts und macht komplexe Pipelines lesbar.
library(dplyr)
# mutate: add/modify columns
df %>%
mutate(
bonus = salary * 0.1,
total = salary + bonus,
category = ifelse(age > 30, "senior", "junior")
)
# transmute: like mutate but only keeps new columns
transmute(df, name, annual = salary, monthly = salary / 12)
# summarize (reduces to single row per group)
summarize(df,
avg_salary = mean(salary),
max_age = max(age),
n = n()
)
# group_by + summarize (like GROUP BY in SQL)
df %>%
group_by(dept) %>%
summarize(
count = n(),
avg_salary = mean(salary),
avg_age = mean(age)
) %>%
arrange(desc(avg_salary))
# multiple summaries
df %>%
group_by(dept) %>%
summarize(across(everything(), list(mean, sd)))
# count and tally
count(df, dept) # count per dept
df %>% group_by(dept) %>% tally()
# window functions (within groups)
df %>%
group_by(dept) %>%
mutate(
rank = rank(desc(salary)),
cumsum = cumsum(salary)
) %>%
arrange(dept, rank)Der Pipe-Operator (%>%)
Die Pipe (%>% von magrittr/dplyr) übergibt die linke Seite als erstes Argument an die rechte Seite und transformiert verschachtelte Funktionsaufrufe in lesbare Links-nach-rechts-Pipelines. Dies ist das definierende Feature des tidyverse-Stils. Der Punkt (.) repräsentiert die gepipten Daten, wenn man sie explizit benötigt. %$% exponiert Spaltennamen; %<>% weist zurück; %T>% setzt die Pipe nach einem Seiteneffekt (wie Plotten) fort. R 4.1+ hat eine native Pipe |>, aber sie ist weniger flexibel (kein Dot-Platzhalter). Pipes machen Data-Wrangling-Code dramatisch lesbarer.
library(magrittr) # or library(dplyr)
# without pipe: nested, hard to read
result <- arrange(
filter(
select(df, name, age, salary),
age > 25
),
desc(salary)
)
# with pipe: linear, readable
result <- df %>%
select(name, age, salary) %>%
filter(age > 25) %>%
arrange(desc(salary))
# the dot (.) refers to the piped data
df %>% plot(.$age, .$salary) # . = df
# %$% exposes column names (magrittr)
df %$% cor(age, salary) # correlation
# %<>% assigns result back (compound assignment)
df %<>% filter(age > 20)
# T pipe: returns first argument (for side effects)
rnorm(100) %T>%
hist() %>% # plot histogram (returns input)
mean() # then compute mean
# native pipe (R 4.1+): |>
df |>
subset(age > 25) |>
colMeans()Data Frames joinen
dplyrs join-Funktionen spiegeln SQL-Joins: inner_join (Schnittmenge), left_join (alle linken Zeilen), right_join, full_join (Vereinigung). semi_join filtert auf Zeilen mit Treffern (ohne Spalten hinzuzufügen); anti_join findet Zeilen ohne Treffer – beide nützlich für Datenvalidierung. Das by-Argument spezifiziert den Join-Schlüssel; einen benannten Vektor verwenden (c('id' = 'emp_id')), wenn Spaltennamen abweichen. Joins sind viel schneller als base Rs merge(). Zeilenanzahlen vor und nach dem Joinen immer prüfen, um unerwartete Duplikate zu erwischen.
library(dplyr)
employees <- data.frame(
id = c(1, 2, 3, 4),
name = c("Alice", "Bob", "Carol", "Dan")
)
salaries <- data.frame(
id = c(1, 2, 3, 5),
salary = c(80000, 50000, 75000, 60000)
)
# inner join: only matching rows
inner_join(employees, salaries, by = "id")
# id name salary
# 1 Alice 80000
# 2 Bob 50000
# 3 Carol 75000
# left join: all rows from left, NA for non-matches
left_join(employees, salaries, by = "id")
# Dan has NA salary
# right join: all rows from right
right_join(employees, salaries, by = "id")
# id 5 has NA name
# full join: all rows from both
full_join(employees, salaries, by = "id")
# different column names
left_join(employees, salaries, by = c("id" = "id"))
# semi join: rows in left that have a match (no right columns)
semi_join(employees, salaries, by = "id")
# anti join: rows in left with NO match
anti_join(employees, salaries, by = "id")
# Dan (id 4 has no salary)
# multiple keys
left_join(df1, df2, by = c("dept" = "department", "year" = "yr"))tidyr: Daten reshaping
tidyr (tidyverse) behandelt Daten-Reshaping. pivot_longer/wider ersetzen die Legacy gather/spread – sie sind intuitiver und flexibler. Tidy Data hat eine Zeile pro Beobachtung und eine Spalte pro Variable; pivot_longer konvertiert Wide-Daten in dieses Format (für ggplot2 benötigt). separate/unite teilen und mergen Spalten. separate_rows explodiert delimite Werte in mehrere Zeilen. drop_na/replace_na/fill behandeln fehlende Daten sauber. Diese Verben komponieren mit dplyr via der Pipe für mächtige Datenpipelines.
library(tidyr)
# wide to long
wide_df <- data.frame(
id = 1:2,
q1_sales = c(100, 200),
q2_sales = c(150, 250),
q3_sales = c(120, 220)
)
long_df <- wide_df %>%
pivot_longer(
cols = starts_with("q"),
names_to = "quarter",
values_to = "sales"
)
# long to wide
long_df %>%
pivot_wider(
names_from = quarter,
values_from = sales
)
# separate one column into multiple
df <- data.frame(id = 1, name = "Alice_Smith")
df %>% separate(name, into = c("first", "last"), sep = "_")
# unite multiple columns into one
df %>% unite("full_name", first, last, sep = " ")
# separate_rows: split delimited values into rows
df <- data.frame(id = 1:2, tags = c("a,b,c", "x,y"))
df %>% separate_rows(tags, sep = ",")
# drop NA values
df %>% drop_na() # drop rows with any NA
df %>% drop_na(salary) # drop rows where salary is NA
# replace NA with a value
df %>% replace_na(list(salary = 0, name = "Unknown"))
# fill missing values (carry forward)
df %>% fill(salary, .direction = "down")Statistik & Modellierung
Deskriptive Statistik
summary() ist der schnellste Weg, einen Überblick über beliebige Daten zu erhalten – zeigt Min, Quartile, Median, Mean und Max. sd() und var() berechnen die SAMPLE-Statistiken (n-1). cor() misst lineare Assoziation (Pearson) oder Rang-Assoziation (Spearman). Bei echten Daten mit NAs immer na.rm=TRUE verwenden. Für Data Frames gibt summary(df) per-Spalten-Statistiken. Die psych- und Hmisc-Pakete bieten erweiterte deskriptive Statistiken.
data <- c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
# central tendency
mean(data) # 5.5
median(data) # 5.5
# R has no built-in mode; use:
as.numeric(names(sort(table(data), decreasing = TRUE)[1]))
# spread
sd(data) # 3.028 (sample standard deviation)
var(data) # 9.167 (sample variance)
IQR(data) # 5 (interquartile range)
range(data) # 1 10
diff(range(data)) # 9
# quantiles
quantile(data) # 0% 25% 50% 75% 100%
quantile(data, c(0.1, 0.9)) # 10th and 90th percentiles
# summary (all at once)
summary(data)
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 1.00 3.25 5.50 5.50 7.75 10.00
# correlation and covariance
x <- c(1, 2, 3, 4, 5)
y <- c(2, 4, 5, 4, 5)
cor(x, y) # 0.77 (Pearson correlation)
cor(x, y, method = "spearman") # rank correlation
cov(x, y) # covariance
# handling NA
data_na <- c(1, 2, NA, 4, 5)
mean(data_na, na.rm = TRUE) # 3
sd(data_na, na.rm = TRUE) # 1.826Wahrscheinlichkeitsverteilungen
R hat jede gängige Verteilung mit einer konsistenten Namenskonvention: d/p/q/r-Präfix + Verteilungsname. d gibt Dichte (für kontinuierlich) oder Wahrscheinlichkeitsmasse (für diskret); p gibt kumulative Wahrscheinlichkeit; q gibt Quantile (Perzentile); r generiert zufällige Stichproben. Gängige Verteilungen: norm (normal), binom (binomial), pois (Poisson), unif (gleichmäßig), exp (exponentiell), t, chisq, f. Vor zufälligen Operationen immer set.seed() für reproduzierbare Ergebnisse aufrufen. sample() zieht zufällige Elemente aus einem Vektor.
# R uses d/p/q/r prefix for distributions:
# d = density (PDF/PMF)
# p = cumulative (CDF: P(X <= x))
# q = quantile (inverse CDF)
# r = random samples
# Normal distribution
dnorm(0) # 0.399 (density at 0)
pnorm(1.96) # 0.975 (P(Z <= 1.96))
qnorm(0.975) # 1.96 (97.5th percentile)
rnorm(5, mean = 100, sd = 15) # 5 random samples
# Binomial distribution
dbinom(3, size = 10, prob = 0.5) # P(X=3)
pbinom(3, size = 10, prob = 0.5) # P(X<=3)
rbinom(10, size = 1, prob = 0.5) # 10 coin flips
# Poisson distribution
dpois(2, lambda = 3) # P(X=2) with rate 3
rpois(100, lambda = 5) # 100 random samples
# Uniform distribution
runif(5, min = 0, max = 1) # 5 uniform random numbers
# Exponential
rexp(10, rate = 0.5) # 10 exponential samples
# set random seed for reproducibility
set.seed(42)
rnorm(3) # same results every time with same seed
# sample from a vector
sample(1:10, 5) # 5 random numbers without replacement
sample(1:10, 5, replace = TRUE) # with replacement
sample(c("A", "B", "C"), 2) # sample from categoriesHypothesentests
R macht Hypothesentests einfach. t.test vergleicht Mittelwerte (One-Sample, Two-Sample oder gepaart). Der p-Wert < 0.05 zeigt typischerweise statistische Signifikanz. var.equal=TRUE nimmt gleiche Varianzen an (Student's t); der Standard ist Welch's (robuster). chisq.test prüft Unabhängigkeit kategorialer Variablen. wilcox.test ist die nicht-parametrische Alternative (keine Normalitätsannahme). aov führt ANOVA für den Vergleich von 3+ Gruppen durch. Alle Test-Funktionen geben eine Liste mit $p.value, $statistic, $conf.int zurück, die man programmatisch extrahieren kann.
# one-sample t-test (is mean different from hypothesized?)
data <- c(5.1, 4.9, 5.0, 5.2, 4.8, 5.1, 5.0)
t.test(data, mu = 5.0)
# t = 0.527, df = 6, p-value = 0.616
# (fail to reject H0: mean = 5)
# two-sample t-test (are two means different?)
group1 <- c(5.1, 4.9, 5.0, 5.2)
group2 <- c(4.5, 4.7, 4.6, 4.4)
t.test(group1, group2)
t.test(group1, group2, var.equal = TRUE) # assume equal variance
# paired t-test (before/after)
before <- c(70, 80, 65, 90, 75)
after <- c(75, 85, 70, 92, 80)
t.test(before, after, paired = TRUE)
# chi-squared test (independence of categorical vars)
tbl <- table(c("A","A","B","B"), c("X","Y","X","Y"))
chisq.test(tbl)
# Wilcoxon (non-parametric alternative to t-test)
wilcox.test(group1, group2)
# ANOVA (compare means across multiple groups)
df <- data.frame(
value = c(5, 6, 7, 8, 9, 10),
group = factor(c("A","A","B","B","C","C"))
)
result <- aov(value ~ group, data = df)
summary(result) # F-test p-value
# extract p-value from any test
test_result <- t.test(data, mu = 5)
test_result$p.value # 0.616Lineare Regression (lm)
lm() passt lineare Modelle mit Formel-Syntax: y ~ x (einfach), y ~ x1 + x2 (multipel), y ~ . (alle Spalten), y ~ x1*x2 (mit Interaktion), y ~ I(x^2) (Transformationen). summary() zeigt Koeffizienten, Standardfehler, t-Werte, p-Werte, R² und den F-Test. Factors werden automatisch zu Dummy-Variablen konvertiert. Die Formel-Mini-Sprache ist mächtig: - entfernt Terme, : ist Interaktion, * ist Haupteffekte + Interaktion. Immer Diagnostik-Plots untersuchen (Residuen, Q-Q-Plot), um Modellannahmen zu prüfen.
# simple linear regression: y ~ x
x <- c(1, 2, 3, 4, 5)
y <- c(2, 4, 5, 4, 5)
model <- lm(y ~ x)
# view results
print(model) # coefficients
summary(model) # full summary with R², t-tests, F-test
coef(model) # (Intercept) 2.2, x 0.6
fitted(model) # predicted values
residuals(model) # y - predicted
confint(model) # 95% confidence intervals
# make predictions
predict(model, newdata = data.frame(x = c(6, 7)))
# multiple regression
df <- data.frame(
y = c(10, 12, 15, 18, 20),
x1 = c(1, 2, 3, 4, 5),
x2 = c(5, 4, 3, 2, 1)
)
model2 <- lm(y ~ x1 + x2, data = df)
model3 <- lm(y ~ ., data = df) # all predictors
model4 <- lm(y ~ x1 + I(x1^2), data = df) # polynomial
# interactions
model5 <- lm(y ~ x1 * x2, data = df) # x1 + x2 + x1:x2
# diagnostic plots
par(mfrow = c(2, 2))
plot(model) # 4 diagnostic plots
# with factors (automatic dummy variables)
model6 <- lm(y ~ x1 + factor(group), data = df)GLM & andere Modelle
glm() generalisiert lm() auf nicht-normale Outcomes via des family-Arguments: binomial (logistische Regression für binär), poisson (Zähldaten), gaussian (wie lm). predict() mit type='response' gibt Wahrscheinlichkeiten (nicht Log-Odds) für logistische Modelle. step() führt automatische Variablenauswahl durch (AIC-basiert). anova(model1, model2) testet, ob das größere Modell signifikant besser ist. Für fortgeschrittene Methoden hat R Pakete für alles: lme4 (Mixed Models), survival (Kaplan-Meier, Cox), randomForest, caret (ML-Pipeline), glmnet (Regularisierung).
# logistic regression (binary outcome)
df <- data.frame(
admit = c(0, 1, 0, 1, 1, 0),
gre = c(380, 660, 800, 640, 520, 760),
gpa = c(3.6, 3.7, 3.8, 3.9, 3.5, 3.6)
)
logit <- glm(admit ~ gre + gpa, data = df, family = binomial)
summary(logit)
# predict probabilities (type = "response")
predict(logit, newdata = df, type = "response")
# Poisson regression (count data)
counts <- c(2, 3, 1, 4, 2, 5)
pois <- glm(counts ~ x, family = poisson)
# stepwise model selection
full_model <- lm(y ~ x1 + x2 + x3, data = df)
step_model <- step(full_model) # AIC-based selection
# anova to compare models
model1 <- lm(y ~ x1, data = df)
model2 <- lm(y ~ x1 + x2, data = df)
anova(model1, model2) # is x2 significant?
# other models
# nls() - nonlinear least squares
# glm.nb() - negative binomial (MASS package)
# lme()/lmer() - mixed effects (nlme/lme4)
# rpart()/randomForest() - tree-based methods
# coxph() - survival analysis (survival package)
# cross-validation
library(boot)
cv_result <- cv.glm(df, logit, K = 10) # 10-fold CV
cv_result$delta # cross-validation errorPlotting (Base R & ggplot2)
Base R: plot, hist & boxplot
Base-R-Grafiken sind schnell und ausreichend für explorative Analyse. plot() ist generisch – es dispatcht nach Eingabetyp (Scatter für zwei Vektoren, Boxplot für eine Formel). type steuert Punkt-/Linienstil; pch setzt das Punktsymbol; lty setzt den Linientyp. par(mfrow=c(r,c)) arrangiert mehrere Plots in einem Raster. hist() mit freq=FALSE zeigt Dichte (sodass man eine Dichtekurve überlagern kann). Für publikationsqualitative Grafiken ggplot2 verwenden. Base-R-Plots sind imperativ – man baut sie Schritt für Schritt auf.
# scatter plot
x <- 1:10
y <- x^2
plot(x, y, type = "p", # p=points, l=lines, b=both, o=overplotted
main = "Quadratic", # title
xlab = "x", ylab = "y", # axis labels
col = "blue", pch = 16, # color and point character
xlim = c(0, 10), ylim = c(0, 100))
# add lines and points to existing plot
lines(x, x*10, col = "red", lty = 2) # dashed line
points(x, y + 5, col = "green")
legend("topleft", legend = c("x^2", "10x"),
col = c("blue", "red"), lty = c(NA, 2), pch = c(16, NA))
# histogram
data <- rnorm(1000)
hist(data, breaks = 30, col = "lightblue",
main = "Normal Distribution", xlab = "Value", freq = FALSE)
lines(density(data), col = "red", lwd = 2) # add density curve
# boxplot (compare groups)
boxplot(count ~ spray, data = InsectSprays,
col = "lightgreen", main = "Insect Count by Spray")
# barplot
counts <- table(mtcars$cyl)
barplot(counts, col = c("red","green","blue"),
main = "Cars by Cylinders", xlab = "Cylinders")
# multiple plots in one window
par(mfrow = c(2, 2)) # 2x2 grid
plot(x, y); hist(data); boxplot(data); plot(density(data))
par(mfrow = c(1, 1)) # resetggplot2: Grammar of Graphics
ggplot2 (tidyverse) implementiert die Grammar of Graphics: Plots werden aus Layern (Daten, Ästhetik, Geometrie, Skalen, Facets, Themes) aufgebaut, kombiniert mit +. aes() mappt Datenspalten auf visuelle Properties (x, y, color, size). Jedes geom_* fügt einen Layer hinzu: geom_point (Scatter), geom_line, geom_bar, geom_histogram, geom_boxplot, geom_smooth (Trendlinie). Dieser Layer-Ansatz bedeutet, dass man komplexe Plots inkrementell aufbauen kann. Anders als base R ist ggplot2 deklarativ – man beschreibt, was man will, nicht wie man es zeichnet.
library(ggplot2)
# basic structure: data + aesthetic mapping + geometry
ggplot(data = mtcars, aes(x = wt, y = mpg)) +
geom_point()
# add aesthetics (color, size, shape mapped to data)
ggplot(mtcars, aes(x = wt, y = mpg, color = cyl, size = hp)) +
geom_point()
# add layers
ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point(color = "blue", size = 3) +
geom_smooth(method = "lm", se = TRUE) + # regression line
labs(title = "MPG vs Weight",
x = "Weight (1000 lbs)", y = "MPG",
color = "Cylinders") +
theme_minimal()
# the + operator adds layers (like %>% but for ggplot)
p <- ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point()
p + geom_smooth() # add to saved plot
p + facet_wrap(~ cyl) # facet by cylinders
# key components:
# data - the data frame
# aes() - aesthetic mappings (x, y, color, size, shape)
# geom_*() - geometry (point, line, bar, histogram, etc.)
# scale_*() - control axes, colors, legends
# facet_*() - small multiples (subplots)
# theme_*() - overall appearance
# labs() - titles and labelsggplot2: Geoms & Ästhetik
ggplot2 hat Dutzende Geoms für jeden Chart-Typ. geom_bar/geom_col für Balken, geom_histogram/geom_density für Verteilungen, geom_boxplot für Vergleiche, geom_line/geom_area für Zeitreihen. stat_summary berechnet und plottet Summaries (Mittelwert, Fehlerbalken). alpha steuert Transparenz (0-1) – unerlässlich für überlappende Punkte. coord_flip rotiert den Plot. geom_jitter fügt Rauschen hinzu, um Overplotting zu verhindern. Jedes geom hat spezifische Ästhetiken, die es versteht (z.B. geom_point braucht x und y; geom_bar braucht nur x).
library(ggplot2)
# bar chart (counts)
ggplot(mpg, aes(class)) +
geom_bar(fill = "steelblue") +
coord_flip() # horizontal bars
# histogram
ggplot(mpg, aes(hwy)) +
geom_histogram(binwidth = 2, fill = "orange", color = "black")
# density plot
ggplot(mpg, aes(hwy, fill = class)) +
geom_density(alpha = 0.5) # semi-transparent
# boxplot by group
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
geom_jitter(width = 0.2, alpha = 0.5) # overlay points
# line plot (time series)
ggplot(economics, aes(date, unemploy)) +
geom_line(color = "red") +
geom_area(fill = "pink", alpha = 0.3)
# scatter with smoothing
ggplot(mpg, aes(displ, hwy, color = drv)) +
geom_point(size = 2) +
geom_smooth(method = "loess", se = FALSE)
# error bars
ggplot(df, aes(group, value)) +
stat_summary(fun = mean, geom = "bar") +
stat_summary(fun.data = mean_se, geom = "errorbar")
# text labels
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_text(size = 3)ggplot2: Facets, Scales & Themes
Facets erstellen Small Multiples – ein Subplot pro Kategorie – der beste Weg, Gruppen zu vergleichen. facet_wrap(~var) erstellt ein 1D-Band; facet_grid(row~col) erstellt ein 2D-Raster. Scales steuern, wie Daten auf visuelle Properties mappen: scale_x_log10 für Log-Achsen, scale_color_brewer für farbenblindenfreundliche Paletten, scale_fill_manual für benutzerdefinierte Farben. Themes steuern Non-Data-Elemente (Schriftarten, Gitterlinien, Legende). theme_minimal/bw/classic sind Presets; theme() passt einzelne Elemente an. ggsave exportiert zu PNG/PDF/SVG mit Kontrolle über Größe und DPI.
library(ggplot2)
# facets: small multiples (subplots by a variable)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class) # one panel per class
# facet_grid: 2D grid of panels
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl) # rows ~ cols
# scales: control axes and colors
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point() +
scale_x_log10() + # log scale
scale_color_brewer(palette = "Set1") + # color palette
scale_size_continuous(range = c(2, 8))
# manual colors
ggplot(mpg, aes(class, fill = drv)) +
geom_bar() +
scale_fill_manual(values = c("red", "green", "blue"))
# themes: overall appearance
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_minimal() + # or theme_classic, theme_bw
theme(
text = element_text(size = 14, family = "serif"),
plot.title = element_text(face = "bold", hjust = 0.5),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "bottom",
panel.grid.major = element_line(color = "gray90")
)
# save plot
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = "pdf")Daten speichern & exportieren
RDS ist am besten zum Speichern eines einzelnen R-Objekts (erhält Typen, schnell, kompakt). RData speichert mehrere Objekte. CSV ist am portabelsten (von Excel, Python usw. lesbar), verliert aber Typinformation – immer stringsAsFactors=FALSE setzen. Das readr-Paket (tidyverse) bietet schnellere, konsistentere CSV-I/O, die Tibbles (verbesserte Data Frames) zurückgibt. readxl/writexl behandeln Excel. Für große Datensätze data.table::fread (sehr schnell) oder parquet (arrow-Paket) für spaltenorientierten Speicher in Betracht ziehen. Beim Schreiben von CSVs immer row.names=FALSE verwenden.
# save single object to RDS (binary, preserves type)
saveRDS(my_data, "data.rds")
restored <- readRDS("data.rds")
# save multiple objects to RData
save(df1, df2, model, file = "workspace.RData")
load("workspace.RData") # loads all objects into workspace
# CSV (most portable)
write.csv(df, "data.csv", row.names = FALSE)
df <- read.csv("data.csv")
df <- read.csv("data.csv", stringsAsFactors = FALSE)
df <- read.csv("data.csv", na.strings = c("", "NA", "N/A"))
# readr (tidyverse): faster, smarter CSV I/O
library(readr)
write_csv(df, "data.csv")
df <- read_csv("data.csv") # returns a tibble
df <- read_csv("data.csv", col_types = cols(
age = col_integer(),
name = col_character()
))
# Excel (requires readxl/writexl packages)
library(readxl)
df <- read_excel("data.xlsx", sheet = 1, range = "A1:D100")
library(writexl)
write_xlsx(df, "output.xlsx")
# RDS vs RData vs CSV
# RDS: one object, binary, fast, preserves types
# RData: multiple objects, binary, fast
# CSV: text, portable to other tools, loses type info
# save and load workspace
save.image("project.RData") # save everything
load("project.RData") # restore everything
# serialize to JSON (jsonlite package)
library(jsonlite)
write_json(df, "data.json")
df <- fromJSON("data.json")OOP & funktionale Programmierung
S3-Klassen & Methoden (einfaches OOP)
S3 ist Rs häufigstes OOP-System – leichtgewichtig und informell. Eine 'Klasse' ist nur eine Liste mit einem class-Attribut; Methoden sind Funktionen namens generic.classname. UseMethod() in einem Generic dispatcht zur entsprechenden Methode basierend auf der Klasse des Objekts. Die meisten R-Objekte (data.frame, lm, ggplot) sind S3. print(), summary(), plot() sind Generics, die man erweitern kann. S3 ist informell (keine Validierung), was es flexibel, aber fehleranfällig macht. methods(generic) verwenden, um alle Methoden zu sehen, methods(class='x') für Methoden einer Klasse.
# S3 is R's simplest OOP system: a class is just an attribute
# create an object: list + class attribute
account <- list(owner = "Alice", balance = 1000)
class(account) <- "BankAccount"
# generic function dispatches by class
print.BankAccount <- function(x, ...) {
cat("Account owner:", x$owner, "\n")
cat("Balance: $", x$balance, "\n", sep = "")
}
print(account)
# Account owner: Alice
# Balance: $1000
# define a custom method for an existing generic
deposit <- function(obj, amount) {
UseMethod("deposit") # dispatch based on class of obj
}
deposit.BankAccount <- function(obj, amount) {
obj$balance <- obj$balance + amount
obj
}
account <- deposit(account, 500)
account$balance # 1500
# check available methods for a generic
methods(print) # all print methods
methods(class = "lm") # methods for lm objects
# default method (fallback)
deposit.default <- function(obj, amount) {
stop("No deposit method for this class")
}S4-Klassen (formales OOP)
S4 ist Rs formales OOP-System: Klassen haben definierte Slots (Felder) mit Typen, Validierung und Vererbung. Definiert mit setClass(); instanziiert mit new(). Auf Slots mit @ zugreifen (nicht $). setMethod() definiert Methoden für Generics. setValidity() erzwingt Constraints. S4 wird von Bioconductor und Paketen verwendet, die strenge Verträge benötigen (z.B. Matrix, sp). S4 ist robuster, aber geschwätziger als S3. Die meiste tägliche R-Arbeit verwendet S3; S4 verwenden, wenn Typsicherheit und formale Vererbung benötigt werden.
# S4 is formal: classes are defined with setClass and validated
setClass("Person",
slots = list(
name = "character",
age = "numeric"
),
prototype = list(name = NA_character_, age = NA_real_)
)
# constructor: new(ClassName, ...)
alice <- new("Person", name = "Alice", age = 30)
# access slots with @ (not $)
alice@name # "Alice"
slot(alice, "age") # 30
# define a method
setMethod("show", "Person", function(object) {
cat("Person:", object@name, "(", object@age, "y)\n")
})
show(alice) # Person: Alice ( 30 y)
# validity check
setValidity("Person", function(object) {
if (object@age < 0) return("age must be non-negative")
TRUE
})
# new("Person", name="Bob", age=-5) # error
# inheritance
setClass("Student", contains = "Person",
slots = list(gpa = "numeric"))
stu <- new("Student", name="Bob", age=20, gpa=3.8)Closures & Funktionsfabriken
Eine Closure ist eine Funktion, die Zugriff auf Variablen in ihrer definierenden Environment behält – Rs primärer Weg, zustandsbehaftete Funktionen zu erstellen und private Daten zu kapseln. Der <<- Operator weist in der umschließenden (Parent-)Environment zu, nicht in der lokalen. Funktionsfabriken (Funktionen, die Funktionen zurückgeben) sind mächtig zum Erstellen spezialisierter Funktionen. Häufige Verwendungen: Zähler, Memoisierung (Caching von Ergebnissen) und das Modul-Muster (Rückgabe einer Liste von Funktionen, die privaten Zustand teilen). Dies ist Rs nächstes Analogon zu Klassen mit privaten Feldern.
# a closure is a function that remembers its enclosing environment
# function factory: creates functions with private state
make_counter <- function() {
count <- 0
function() {
count <<- count + 1 # <<- modifies in enclosing env
count
}
}
c1 <- make_counter()
c1() # 1
c1() # 2
c1() # 3
c2 <- make_counter()
c2() # 1 (independent counter)
# memoization (cache expensive results)
memoize <- function(f) {
cache <- new.env(parent = emptyenv())
function(x) {
key <- as.character(x)
if (!exists(key, envir = cache)) {
assign(key, f(x), envir = cache)
}
get(key, envir = cache)
}
}
slow_sqrt <- function(x) { Sys.sleep(0.1); sqrt(x) }
fast_sqrt <- memoize(slow_sqrt)
fast_sqrt(16) # slow first time
fast_sqrt(16) # instant second time
# capturing state in a list (module pattern)
bank_account <- function(initial) {
balance <- initial
list(
deposit = function(amt) { balance <<- balance + amt },
withdraw = function(amt) { balance <<- balance - amt },
get_balance = function() balance
)
}
acc <- bank_account(100)
acc$deposit(50)
acc$get_balance() # 150Funktionale Programmierung: Map/Reduce/Filter
R hat eingebaute funktionale Programmier-Primitiven: Map (Funktion auf jedes Element anwenden), Reduce (falten/akkumulieren), Filter (übereinstimmende Elemente behalten), Find/Position (suchen), Negate (Prädikat invertieren). Diese geben Listen oder Vektoren zurück und vermeiden explizite Schleifen. Das purrr-Paket (tidyverse) bietet eine konsistentere API: map_dbl/map_chr geben typisierte Vektoren zurück, keep/discard filtern, reduce akkumuliert. Die ~ .x-Formel-Syntax erstellt anonyme Funktionen prägnant. Funktionale Programmierung macht Code deklarativer und leichter zu verstehen, besonders für Daten-Transformationspipelines.
# Map: apply a function to each element (returns list)
Map(function(x) x^2, 1:5)
# [[1]] 1 [[2]] 4 [[3]] 9 [[4]] 16 [[5]] 25
# Reduce: combine elements with a binary function
Reduce("+", 1:5) # 15 (1+2+3+4+5)
Reduce("*", 1:5) # 120 (factorial 5!)
Reduce(c, list(1:2, 3:4, 5:6)) # 1 2 3 4 5 6
# Filter: keep elements satisfying a predicate
Filter(function(x) x > 2, 1:5) # 3 4 5
Filter(is.numeric, list(1, "a", 2, "b")) # 1 2
# Negate: invert a predicate
Negate(is.null)
is_not_null <- Negate(is.null)
# Position: find first index satisfying a predicate
Position(function(x) x > 3, 1:10) # 4
# Find: first element satisfying a predicate
Find(function(x) x > 3, 1:10) # 4
# purrr (tidyverse): cleaner functional programming
library(purrr)
map_dbl(1:5, ~ .x^2) # 1 4 9 16 25 (vector output)
map_chr(c("a","b"), toupper) # "A" "B"
keep(1:5, ~ .x > 2) # 3 4 5
reduce(1:5, `+`) # 15
walk(1:3, print) # side effects onlyDebugging & Fehlerbehandlung
browser() ist Rs interaktiver Debugger – in Code einfügen, um zu pausieren und Variablen zu inspizieren (n=next, c=continue, Q=quit). debug(fn) durchschreitet eine Funktion Zeile für Zeile. traceback() zeigt den Call-Stack nach einem Absturz. tryCatch() ist Rs try/catch: es fängt Fehler und Warnungen via Handler-Funktionen ab und gibt einen Fallback-Wert zurück. withCallingHandlers() behandelt Warnungen ohne die Ausführung zu unterbrechen. options(warn=2) setzen wandelt Warnungen in Fehler um (nützlich zum Finden der Quelle). options(error=browser) betritt automatisch den Debugger bei jedem nicht abgefangenen Fehler. Diese Tools zu beherrschen ist unerlässlich für die Diagnose von Problemen in komplexem R-Code.
# browser(): pause execution and inspect
f <- function(x) {
browser() # pauses here; use n, c, Q, ls, print
y <- x * 2
if (y > 10) stop("y too big")
y
}
# f(6) # enters browser at the browser() line
# debug() / undebug(): debug a function
debug(lm)
# lm(y ~ x) # steps through lm line by line
undebug(lm)
# traceback(): see call stack after an error
# log("abc") # error
# traceback() # shows the call stack
# tryCatch(): handle errors gracefully
safe_log <- function(x) {
tryCatch(
log(x),
error = function(e) {
message("Error: ", conditionMessage(e))
NA_real_
},
warning = function(w) {
message("Warning: ", conditionMessage(w))
log(x) # retry or handle
}
)
}
safe_log("abc") # NA with message
safe_log(-1) # NA with warning (NaN)
# withCallingHandlers(): handle warnings without stopping
withCallingHandlers(
{ warn("oops"); 42 },
warning = function(w) { message("caught: ", w$message); invokeRestart("muffleWarning") }
)
# options for debugging
options(warn = 2) # warnings become errors (for debugging)
options(error = browser) # enter browser on error
options(warn = 0) # resetZeitreihen & Prognose
Zeitreihen-Objekte erstellen (ts)
ts() ist base Rs Zeitreihen-Klasse – ein Vektor mit Zeit-Attributen (start, frequency). frequency codiert die Periode: 12 für monatlich, 4 für quartalsweise, 52 für wöchentlich. window() subsettet nach Zeitbereich. diff() berechnet Differenzen (nützlich, um eine Reihe stationär zu machen). lag() verschiebt Werte. aggregate() konvertiert zu niedrigerer Frequenz (z.B. monatlich zu quartalsweise). ts funktioniert gut für regelmäßige, feste Frequenz-Daten. Für irreguläre Zeitstempel (z.B. Aktienkurse mit Lücken) die xts/zoo-Pakete verwenden.
# ts(): create a regular time series
# frequency: 12=monthly, 4=quarterly, 52=weekly, 365.25=daily
monthly_sales <- ts(c(30, 35, 40, 38, 42, 45, 50, 48, 52, 55, 60, 58),
start = c(2020, 1), frequency = 12)
print(monthly_sales)
# Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
# 2020 30 35 40 38 42 45 50 48 52 55 60 58
# attributes
start(monthly_sales) # 2020 1
end(monthly_sales) # 2020 12
frequency(monthly_sales) # 12
time(monthly_sales) # time points
# window(): subset a time range
q1 <- window(monthly_sales, start = c(2020, 3), end = c(2020, 5))
# multiple series (multivariate)
ts1 <- ts(rnorm(12), start = 2020, frequency = 12)
ts2 <- ts(rnorm(12), start = 2020, frequency = 12)
mts <- cbind(ts1, ts2) # multivariate ts
# lag and diff
lag(monthly_sales, k = 1) # shift by 1 period
diff(monthly_sales) # first differences (month-over-month change)
diff(monthly_sales, lag = 12) # year-over-year change
# aggregate to lower frequency
aggregate(monthly_sales, nfrequency = 4, FUN = mean) # monthly -> quarterlyZerlegung & Glättung
Zeitreihen-Zerlegung separiert eine Reihe in Trend-, Saison- und Residual-Komponenten. decompose() verwendet klassische Moving-Average-Zerlegung (additiv oder multiplikativ). stl() verwendet LOESS-Glättung und ist robuster gegenüber Ausreißern und behandelt changing seasonality. HoltWinters() passt exponentielle Glättung an (Level + Trend + Saison). Das forecast-Paket (jetzt fable in tidyverse) bietet forecast() für Vorhersage mit Konfidenzintervallen. Immer die Zerlegung plotten, um die Struktur vor der Modellierung zu verstehen. Multiplikative Zerlegung ist angemessen, wenn die saisonale Amplitude mit dem Trend wächst.
# decompose(): split a series into trend/seasonal/random
decomp <- decompose(monthly_sales, type = "additive")
# type = "additive" (T+S+R) or "multiplicative" (T*S*R)
plot(decomp) # shows observed, trend, seasonal, random panels
decomp$trend # trend component
decomp$seasonal # seasonal component
decomp$random # remainder
# stl(): Seasonal-Trend-Loess decomposition (more flexible)
stl_fit <- stl(monthly_sales, s.window = "periodic")
plot(stl_fit)
stl_fit$time.series[, "trend"] # extract trend
# moving average smoothing
ma3 <- filter(monthly_sales, filter = rep(1/3, 3), sides = 2)
ma12 <- filter(monthly_sales, filter = rep(1/12, 12), sides = 1)
# HoltWinters(): exponential smoothing
hw <- HoltWinters(monthly_sales, seasonal = "additive")
plot(hw)
hw$fitted # fitted values
hw$coefficients # alpha, beta, gamma
# forecast with HoltWinters
library(forecast)
fc <- forecast(hw, h = 12) # 12-step ahead forecast
plot(fc) # with prediction intervals
autoplot(fc) # ggplot2 versionACF, PACF & ARIMA-Modellierung
ACF/PACF-Plots diagnostizieren die Autokorrelationsstruktur: ACF zeigt totale Korrelation bei jedem Lag, PACF zeigt direkte (partielle) Korrelation. Ihre Muster suggerieren ARIMA-Ordnungen: ACF-Cutoff suggeriert MA, PACF-Cutoff suggeriert AR. adf.test() prüft Stationarität (p<0.05 bedeutet stationär). auto.arima() (forecast-Paket) wählt automatisch das beste ARIMA(p,d,q)(P,D,Q)-Modell via AICc. d ist die Differenzierungs-Ordnung (um Stationarität zu erreichen); (P,D,Q) sind saisonale Komponenten. checkresiduals() verifiziert, dass das Modell gut passt (Residuen sollten White Noise sein). ARIMA ist das Arbeitspferd der Zeitreihen-Prognose.
# ACF (autocorrelation) and PACF (partial autocorrelation)
acf(monthly_sales, main = "ACF") # correlation with lagged self
pacf(monthly_sales, main = "PACF") # direct correlation at each lag
# interpret:
# ACF tails off slowly -> need differencing (non-stationary)
# ACF cuts off at lag p -> AR(p) process
# PACF cuts off at lag q -> MA(q) process
# stationarity test
library(tseries)
adf.test(monthly_sales) # Augmented Dickey-Fuller
# p < 0.05 -> stationary
# auto.arima(): automatically select ARIMA order
library(forecast)
fit <- auto.arima(monthly_sales)
summary(fit) # shows ARIMA(p,d,q)(P,D,Q)[m]
# ARIMA(0,1,1)(1,0,0)[12] example output
# forecast
fc <- forecast(fit, h = 12)
plot(fc)
accuracy(fc) # ME, RMSE, MAE, MAPE
# manual ARIMA
fit2 <- arima(monthly_sales, order = c(1, 1, 1),
seasonal = list(order = c(1, 0, 0), period = 12))
# residuals should look like white noise
checkresiduals(fit) # Ljung-Box test
tsdisplay(residuals(fit)) # ACF + PACF of residualsxts & zoo (irreguläre Zeitreihen)
xts/zoo erweitern ts für irreguläre Zeitreihen (z.B. Finanzdaten mit fehlenden Wochenenden/Feiertagen). xts-Objekte sind nach tatslichen Daten/Zeiten indiziert, was intuitives Subsetting wie prices['2024-01'] für den ganzen Januar ermöglicht. merge() richtet mehrere Reihen nach Datum aus und füllt Lücken mit NA (fill=na.locf verwenden, um vorwärts zu tragen). rollmean/rollapply berechnen rollierende Statistiken. endpoints/period.apply aggregieren zu gröberen Perioden (Wochen, Monate). xts ist die Grundlage der meisten R-Finanzpakete (quantmod, TTR, PerformanceAnalytics). Für tidy Zeitreihen bieten die tsibble/fable-Pakete eine moderne Alternative.
library(xts)
library(zoo)
# create xts from a matrix and time index
dates <- as.Date(c("2024-01-01", "2024-01-03", "2024-01-10"))
prices <- xts(c(100, 102, 98), order.by = dates)
colnames(prices) <- "AAPL"
# subset by date range (very intuitive)
prices["2024-01-03"] # specific date
prices["2024-01-01/2024-01-05"] # date range
prices["2024-01"] # entire January
prices["/2024-01-05"] # up to a date
# lag and diff (returns xts)
lag(prices, k = 1) # previous day's price
diff(prices) # daily change
daily_returns <- diff(prices) / lag(prices, 1)
# rolling operations (zoo)
rollmean(prices, k = 3) # 3-day rolling mean
rollapply(prices, 3, sd) # 3-day rolling std dev
rollmax(prices, 3) # 3-day rolling max
# period.apply: aggregate by period
ep <- endpoints(prices, on = "weeks") # week endpoints
period.apply(prices, ep, mean) # weekly averages
# merge multiple series (aligns by date)
aapl <- xts(c(100, 102, 98), as.Date(c("2024-01-01","2024-01-02","2024-01-03")))
msft <- xts(c(200, 201), as.Date(c("2024-01-01","2024-01-03")))
merged <- merge(aapl, msft) # NA fills gaps
merged <- merge(aapl, msft, fill = na.locf) # carry forward
# to.ts <- as.ts(prices) # convert to base ts (loses dates)Prognose-Evaluation & Visualisierung
Prognosen immer auf einem zurückgehaltenen Test-Set evaluieren, nicht in-sample. accuracy(fit, test) berechnet Fehlermetriken: MAE und RMSE (absolute Skala), MAPE (Prozent, skalenfrei, aber instabil nahe Null), MASE (skaliert nach naiver Prognose-Fehler; <1 bedeutet besser als naiv). tsCV() führt Zeitreihen-Kreuzvalidierung durch (rolling origin). Mehrere Modelle vergleichen (naive Baseline, ETS, ARIMA) und das fehlerminimale auswählen. autoplot() + autolayer() visualisieren Prognosen mit Vorhersageintervallen. Die naive Prognose (letzter Wert) ist eine kritische Baseline – das Modell muss sie schlagen, um nützlich zu sein. Niemals zufällige Train/Test-Splits für Zeitreihen verwenden (sie lecken zukünftige Info); immer chronologisch splitten.
library(forecast)
library(ggplot2)
# split into train/test
train <- window(monthly_sales, end = c(2020, 9))
test <- window(monthly_sales, start = c(2020, 10))
# fit multiple models
fit_naive <- naive(train, h = 3) # naive: last value
fit_snaive <- snaive(train, h = 3) # seasonal naive
fit_ets <- ets(train) %>% forecast(h = 3) # exponential smoothing
fit_arima <- auto.arima(train) %>% forecast(h = 3)
# compare accuracy on test set
accuracy(fit_naive, test)
accuracy(fit_arima, test)
# metrics: ME, RMSE, MAE, MPE, MAPE, MASE
# time series cross-validation
ts_cv <- tsCV(train, forecastfunction = naive, h = 3)
sqrt(mean(ts_cv^2, na.rm = TRUE)) # CV RMSE
# visualize forecasts with ggplot
autoplot(monthly_sales, series = "Actual") +
autolayer(fit_arima, series = "ARIMA", PI = FALSE) +
autolayer(fit_ets, series = "ETS", PI = FALSE) +
labs(title = "Forecast Comparison", x = "Time", y = "Sales") +
theme_minimal()
# prediction intervals
fc <- forecast(auto.arima(train), h = 3, level = c(80, 95))
autoplot(fc) +
autolayer(test, series = "Actual")
# accuracy measures explained:
# MAE = Mean Absolute Error (same units as data)
# RMSE = Root Mean Squared Error (penalizes large errors)
# MAPE = Mean Absolute Percentage Error (scale-free, %)
# MASE = Mean Absolute Scaled Error (< 1 beats naive)dplyr Deep Dive
Kern-Verben: filter, select, mutate, summarize
dplyrs fünf Kern-Verben decken die meisten Datenmanipulationen: filter (Zeilen nach Bedingung), select (Spalten nach Name), mutate (Spalten hinzufügen/transformieren), summarize (auf Summary-Stats reduzieren) und arrange (sortieren). Die Pipe %>% (oder nativ |>) verkettet Operationen von links nach rechts und macht Code lesbar. Helfer-Funktionen wie starts_with, ends_with, contains und everything() machen Spaltenauswahl prägnant. Vor summarize immer group_by für per-Gruppen-Statistiken; n() zählt Zeilen pro Gruppe.
library(dplyr)
df <- tibble(
name = c("Alice","Bob","Carol","Dave"),
dept = c("Eng","Sales","Eng","Sales"),
salary = c(90000, 70000, 95000, 72000),
years = c(5, 3, 8, 4)
)
# filter rows
eng <- df %>% filter(dept == "Eng", salary > 85000)
# select columns (with helpers)
df %>% select(name, salary)
df %>% select(starts_with("s"))
df %>% select(-years)
df %>% select(name:dept) # range
df %>% select(dept, everything())# reorder
# mutate: add/modify columns
df %>% mutate(
bonus = salary * 0.1,
total = salary + bonus,
level = if_else(years >= 5, "Senior", "Junior")
)
# transmute: keep only new columns
df %>% transmute(name, annual_k = salary / 1000)
# summarize (with group_by)
df %>%
group_by(dept) %>%
summarize(
avg_salary = mean(salary),
max_years = max(years),
n = n()
)Joins & Set-Operationen
dplyr-Joins spiegeln SQL: inner, left, right, full zum Kombinieren; semi und anti zum Filtern nach einer anderen Tabelle. Immer by spezifizieren, um stille Treffer auf unbeabsichtigten Spalten zu vermeiden. Für unterschiedliche Schlüsselnamen by = c('left_col' = 'right_col') verwenden. Set-Operationen (union, intersect, setdiff) erfordern identische Spaltensets. bind_rows stapelt (füllt fehlende Spalten mit NA); bind_cols klebt Seite an Seite ohne Schlüssel zu prüfen – normalerweise will man stattdessen einen Join. Joins sind die häufigste Quelle subtiler Daten-Bugs, also Zeilenanzahlen vor und nach prüfen.
employees <- tibble(id = 1:4, name = c("Al","Bo","Cy","Di"))
salaries <- tibble(id = c(1,2,4,5), salary = c(50,60,80,90))
# mutating joins (combine columns)
inner_join(employees, salaries, by = "id") # only matching ids
left_join(employees, salaries, by = "id") # all from left
right_join(employees, salaries, by = "id") # all from right
full_join(employees, salaries, by = "id") # all rows
# filtering joins (filter rows, no new columns)
semi_join(employees, salaries, by = "id") # rows in left with match
anti_join(employees, salaries, by = "id") # rows in left WITHOUT match
# different column names
left_join(x, y, by = c("emp_id" = "id"))
# set operations (require identical columns)
union(a, b) # unique rows in either
union_all(a, b) # all rows (with dups)
intersect(a, b) # rows in both
setdiff(a, b) # rows in a but not b
# bind rows/columns
bind_rows(list(df1, df2)) # stack vertically
bind_cols(df1, df2) # side by side (no key check!)Window-Funktionen & gruppiertes Mutate
Window-Funktionen operieren innerhalb von Gruppen, die durch group_by definiert sind. row_number, min_rank und dense_rank unterscheiden sich in der Behandlung von Gleichständen. lead/lag greifen auf benachbarte Zeilen zu – unerlässlich für Zeitreihen und Änderungserkennung. Kumulative Funktionen (cumsum, cummax, cummean) berechnen laufende Aggregate. slice_max/min/head/sample extrahieren spezifische Zeilen pro Gruppe. rowwise() + c_across() ermöglicht Zeile-für-Zeile-Operationen über Spalten (langsamer als vektorisiert, aber manchmal nötig). Diese Funktionen machen dplyr so mächtig wie SQL-Window-Funktionen.
df <- tibble(
dept = c("A","A","A","B","B","B"),
name = c("x","y","z","p","q","r"),
salary = c(50, 70, 60, 80, 90, 75)
)
# ranking within groups
df %>% group_by(dept) %>%
mutate(
rank = row_number(), # 1, 2, 3 (ties get distinct)
min_rank = min_rank(salary), # ties get same rank, gaps
dense_rank = dense_rank(salary),
pct_rank = percent_rank(salary)
)
# offsets (lead / lag)
df %>% group_by(dept) %>%
mutate(
prev = lag(salary),
next = lead(salary),
change = salary - lag(salary)
)
# cumulative aggregates
df %>% group_by(dept) %>%
mutate(
cum_total = cumsum(salary),
cum_max = cummax(salary),
running_avg = cummean(salary)
)
# top N per group
df %>% group_by(dept) %>%
slice_max(salary, n = 2) # or slice_min, slice_head, slice_sample
# row-wise operations
df %>% rowwise() %>%
mutate(total = sum(c_across(where(is.numeric))))across, where & Multi-Spalten-Operationen
across() (dplyr 1.0+) ist der moderne Weg, eine Funktion auf mehrere Spalten anzuwenden – ersetzt die alten _at, _if, _all-Suffixe. where(is.numeric) selektiert Spalten nach Prädikat. Das .names-Argument steuert Output-Namen mit {col}- und {fn}-Templates. if_all/if_any filtern Zeilen, bei denen alle/irgendeine ausgewählte Spalte eine Bedingung erfüllt. Das .data-Pronomen ermöglicht programmatischen Spaltenzugriff (nützlich in Funktionen und Shiny-Apps). Diese Tools machen dplyr hochgradig ausdrucksstark für Batch-Operationen auf vielen Spalten.
df <- tibble(id = 1:3, a = c(1,2,3), b = c(4,5,6), c = c("x","y","z"))
# apply function to multiple columns
df %>% mutate(across(a:b, ~ .x * 10))
df %>% mutate(across(where(is.numeric), log))
df %>% mutate(across(everything(), as.character))
# summarize multiple columns
df %>% summarize(across(where(is.numeric), list(
mean = ~mean(.x),
sd = ~sd(.x)
), .names = "{.col}_{.fn}"))
# rename multiple columns
df %>% rename_with(toupper, starts_with("a"))
# conditional transformation
df %>% mutate(across(where(is.numeric), ~ if_else(.x > 3, "high", "low")))
# use .data pronoun for programmatic access
col <- "a"
df %>% mutate(new = .data[[col]] * 2)
# pick columns by type in any verb
df %>% filter(if_all(a:b, ~ .x > 1)) # all must satisfy
df %>% filter(if_any(a:b, ~ .x > 4)) # any must satisfyDatenbank-Backends & dbplyr
dbplyr übersetzt dplyr-Verben in SQL und ermöglicht das Manipulieren von Datenbanktabellen mit derselben Syntax wie lokale Data Frames. Dies ist riesig für Big Data: schwere Berechnung passiert in der Datenbank (oft spaltenorientiert/parallel), und nur Ergebnisse werden via collect() nach R gezogen. show_query() offenbart das generierte SQL zum Debuggen. Die meisten dplyr-Verben übersetzen direkt; Window-Funktionen und einige String-Operationen benötigen möglicherweise SQL-spezifische Funktionen. Immer dbDisconnect, wenn fertig. Für Produktion einen Connection-Pool und parametrisierte Queries verwenden, um SQL-Injection zu verhindern.
library(DBI)
library(dbplyr)
# connect to a database
con <- dbConnect(RSQLite::SQLite(), "my.db")
# or: con <- dbConnect(odbc::odbc(), "PostgreSQL")
# copy data to database
copy_to(con, mtcars, "mtcars_db", temporary = FALSE)
# reference a remote table
mtcars_remote <- tbl(con, "mtcars_db")
# dplyr verbs translate to SQL!
mtcars_remote %>%
group_by(cyl) %>%
summarize(avg_mpg = mean(mpg), n = n()) %>%
arrange(desc(avg_mpg))
# see the generated SQL
mtcars_remote %>%
filter(mpg > 20) %>%
select(mpg, cyl, hp) %>%
show_query()
# collect: pull results into local tibble
result <- mtcars_remote %>%
filter(cyl == 4) %>%
collect()
# write back to database
copy_to(con, result, "efficient_cars")
dbDisconnect(con)ggplot2 Deep Dive
Grammar of Graphics & geschichtete Plots
ggplot2 basiert auf der Grammar of Graphics: jeder Plot ist eine Kombination aus Daten, ästhetischen Mappings (aes), geometrischen Objekten (geom_*), Statistiken (stat_*), Skalen, Koordinatensystemen und Facets. Layer werden mit + hinzugefügt. Ästhetik mappt Datenspalten auf visuelle Properties (x, y, color, size, shape); feste Werte gehen außerhalb von aes(). facet_wrap und facet_grid erstellen Small Multiples – eines der mächtigsten explorativen Tools. Die meisten Geoms haben einen Standard-stat (z.B. geom_bar verwendet stat_count), aber man kann mit stat_summary für benutzerdefinierte Aggregationen überschreiben.
library(ggplot2)
df <- data.frame(x = 1:10, y = (1:10)^2 + rnorm(10))
# basic structure: data + aesthetic + geom
ggplot(df, aes(x = x, y = y)) +
geom_point()
# multiple layers
ggplot(df, aes(x, y)) +
geom_point(color = "blue", size = 3) +
geom_smooth(method = "lm", se = TRUE)
# aesthetics map data to visual properties
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point() +
geom_smooth(method = "loess", se = FALSE)
# facets: small multiples
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, ncol = 4)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl) # rows ~ cols
# statistics (compute then plot)
ggplot(diamonds, aes(price)) +
geom_histogram(bins = 50) # stat_bin
ggplot(diamonds, aes(cut, price)) +
stat_summary(fun = "mean", geom = "bar")Scales, Themes & Annotationen
Scales steuern das Mapping von Daten auf visuelle Properties – jede Ästhetik hat eine entsprechende Scale (scale_x_*, scale_color_* usw.). scale_*_log10, scale_*_sqrt transformieren Achsen; scale_*_continuous/discrete/manual passen Werte an. Viridis-Paletten sind farbenblinden-sicher und drucken gut in Graustufen. labs() setzt alle Labels in einem Aufruf. theme() steuert Non-Data-Elemente (Schriftarten, Gitterlinien, Legenden-Position); von theme_minimal oder theme_classic starten und anpassen. annotate() fügt feste Elemente (Text, Rechtecke, Segmente) unabhängig von Daten hinzu.
p <- ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point(size = 3)
# scales control how data maps to visuals
p + scale_x_log10() +
scale_y_continuous(limits = c(0, 50), breaks = seq(0, 50, 10))
# manual colors
p + scale_color_manual(values = c("red","blue","green"))
# color brewer / viridis (colorblind-safe)
p + scale_color_brewer(palette = "Set1")
p + scale_color_viridis_d()
# labels
p + labs(
title = "Fuel Efficiency",
subtitle = "By vehicle class",
x = "Engine Displacement (L)",
y = "Highway MPG",
color = "Class",
caption = "Source: EPA"
)
# themes
p + theme_minimal()
p + theme_classic()
p + theme(
plot.title = element_text(face = "bold", size = 16),
panel.grid.minor = element_blank(),
legend.position = "bottom"
)
# annotations
p + annotate("text", x = 5, y = 40, label = "Outlier", color = "red") +
annotate("rect", xmin = 4, xmax = 6, ymin = 30, ymax = 45,
alpha = 0.2, fill = "blue")Statistische Geoms & Verteilungen
Statistische Geoms visualisieren Verteilungen und Summaries. Boxplots zeigen Quartile und Ausreißer; Violins fügen die Dichte-Form hinzu. geom_density glättet Histogramme; geom_bin2d/hex zeigen 2D-Verteilungen für große Datensätze. geom_qq prüft Normalität (Punkte sollten der Linie folgen). geom_errorbar/geom_pointrange zeigen Unsicherheit. Für gepaarte Vergleiche fügt ggsignif Signifikanz-Klammern hinzu. Das ggridges-Paket erstellt Ridgeline-Plots – exzellent zum Vergleichen von Verteilungen über viele Gruppen. Immer Geoms wählen, die die zugrunde liegenden Daten ehrlich repräsentieren.
# boxplot by group
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# violin + boxplot overlay
ggplot(mpg, aes(class, hwy, fill = class)) +
geom_violin() +
geom_boxplot(width = 0.1)
# density plot
ggplot(mpg, aes(hwy, fill = drv)) +
geom_density(alpha = 0.5)
# 2D density / heatmap
ggplot(diamonds, aes(carat, price)) +
geom_bin2d(bins = 50) # or geom_hex()
# quantile-quantile plot
ggplot(mtcars, aes(sample = mpg)) +
geom_qq() + geom_qq_line()
# error bars
df <- data.frame(group = c("A","B","C"),
mean = c(5, 7, 4), se = c(0.5, 0.8, 0.3))
ggplot(df, aes(group, mean)) +
geom_col() +
geom_errorbar(aes(ymin = mean - se, ymax = mean + se), width = 0.2)
# ridgeline plot (ggridges)
# library(ggridges)
# ggplot(diamonds, aes(price, cut, fill = cut)) + geom_density_ridges()Facets, Koordinatensysteme & Erweiterungen
Facets mit scales = 'free' lassen jedes Panel seinen eigenen Achsenbereich haben – nützlich, wenn Gruppen sehr unterschiedliche Skalen haben. coord_polar macht Balkendiagramme zu Kuchen-/Radar-Charts; coord_flip tauscht Achsen (praktisch für horizontale Balkendiagramme). Das sf-Paket integriert räumliche Daten mit geom_sf für Karten. patchwork kombiniert mehrere Plots mit +, / und | Operatoren – viel flexibler als gridExtra. ggsave exportiert zu PNG/PDF/SVG; cairo_pdf behandelt benutzerdefinierte Schriftarten. ggplotly konvertiert ggplots zu interaktiven HTML-Widgets für Web-Deployment.
# free scales per facet
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, scales = "free")
# polar coordinates (pie chart from bar)
ggplot(mtcars, aes(x = factor(1), fill = factor(cyl))) +
geom_bar(width = 1) +
coord_polar(theta = "y")
# flipped coordinates
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# map coordinates (sf)
# library(sf)
# ggplot(nc_sf) + geom_sf(aes(fill = AREA))
# patchwork: combine multiple plots
# library(patchwork)
# p1 <- ggplot(...)
# p2 <- ggplot(...)
# p1 + p2
# p1 / (p2 + p3)
# save plots
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = cairo_pdf) # vector, supports fonts
# interactive (plotly)
# library(plotly)
# ggplotly(p)Reproduzierbares Plotten & Funktionen
Das Einpacken von ggplot-Aufrufen in Funktionen macht sie wiederverwendbar. Der {{ }}-Operator (tidy evaluation) ermöglicht das Übergeben von unquoted Spaltennamen; .data[[string]] behandelt programmatischen Zugriff. Gespeicherte Plot-Objekte (.rds) können neu geladen und modifiziert werden – nützlich für Berichte, die leichte Variationen benötigen. Benutzerdefinierte Themes können einmal definiert und überall angewendet werden und stellen visuelle Konsistenz über ein Projekt sicher. Für Batch-Generierung über Spaltennamen mit .data[[]] und ggsave loopen. Dieser funktionale Ansatz ist unerlässlich für Shiny-Apps und automatisierte Reporting-Pipelines.
# wrap plots in functions for reuse
make_scatter <- function(data, x, y, color = NULL) {
ggplot(data, aes({{ x }}, {{ y }}, color = {{ color }})) +
geom_point() +
theme_minimal()
}
make_scatter(mpg, displ, hwy, class)
# using .data pronoun (string column names)
plot_col <- function(data, col) {
ggplot(data, aes(.data[[col]])) +
geom_bar() +
theme_minimal()
}
plot_col(mpg, "class")
# save and load plot objects
p <- ggplot(mpg, aes(displ, hwy)) + geom_point()
saveRDS(p, "plot.rds")
p_loaded <- readRDS("plot.rds")
# modify saved plot
p_loaded + geom_smooth()
# themes as reusable objects
my_theme <- theme_minimal() +
theme(text = element_text(family = "Helvetica"),
plot.title = element_text(face = "bold"))
ggplot(mpg, aes(displ, hwy)) + geom_point() + my_theme
# programmatic plot generation
for (col in c("hwy","cty","cyl")) {
p <- ggplot(mpg, aes(.data[[col]])) + geom_histogram()
ggsave(paste0("hist_", col, ".png"), p)
}tidyr Data Tidying
Pivot Longer & Widerder
Tidy Data hat eine Zeile pro Beobachtung und eine Spalte pro Variable – die meisten Analysefunktionen erwarten dieses Format. pivot_longer konvertiert Wide zu Long (sammelt Spalten in Key-Value-Paare); pivot_wider macht das Umgekehrte. Der .value-Sentinel in names_to behält Teile von Spaltennamen als separate Spalten (z.B. 'a_1' wird a=1, b=1). Immer vor dem Plotten oder Modellieren reshaping: ggplot2 will Long-Format für gruppierte Ästhetik; einige Modellierungsfunktionen wollen Wide-Format. Das names_pattern-Argument behandelt komplexere Spaltennamen-Strukturen mit Regex.
library(tidyr)
# wide format (one row per observation, columns for each variable)
wide <- tibble(
country = c("A","B","C"),
`2020` = c(100, 150, 200),
`2021` = c(110, 160, 210),
`2022` = c(120, 170, 220)
)
# pivot_longer: wide -> long
long <- wide %>%
pivot_longer(
cols = -country, # all columns except country
names_to = "year",
values_to = "gdp"
)
# A tibble: 9 x 3
# pivot_wider: long -> wide
wide2 <- long %>%
pivot_wider(names_from = year, values_from = gdp)
# multiple value columns
df <- tibble(id = 1:2, a_1 = c(1,2), a_2 = c(3,4), b_1 = c(5,6), b_2 = c(7,8))
df %>%
pivot_longer(
-id,
names_to = c(".value", "time"), # .value keeps a, b as columns
names_sep = "_"
)
# id time a b
# 1 1 1 5
# 1 2 3 7Separate, Unite & Extract
separate teilt eine Spalte an einem Delimiter in mehrere Spalten; unite kombiniert mehrere Spalten in eine. extract verwendet Regex-Capture-Gruppen für flexibleres Splitten. separate_rows explodiert delimite Strings in mehrere Zeilen – unerlässlich, wenn eine Zelle eine Liste enthält (z.B. Tags, Kategorien). Die convert = TRUE-Option auto-konvertiert Typen (Zahlen, Daten). Diese Funktionen bereinigen unordentliche echte Daten: Vollnamen splitten, Daten parsen, delimite Felder normalisieren. Kombiniert mit pivot_* behandeln sie fast jede Reshaping-Aufgabe.
df <- tibble(
name = c("John_Smith", "Jane_Doe", "Bob_Jones"),
date_range = c("2020-01-01_2020-12-31", "2021-01-01_2021-12-31", "2022-01-01_2022-12-31")
)
# separate one column into many
df %>% separate(name, into = c("first", "last"), sep = "_")
# separate with conversion
df %>% separate(name, into = c("first","last"), sep = "_", convert = TRUE)
# extract with regex groups
df %>%
extract(date_range,
into = c("start", "end"),
regex = "(.+)_(.+)")
# unite multiple columns into one
df2 <- tibble(first = c("John","Jane"), last = c("Smith","Doe"))
df2 %>% unite("full_name", first:last, sep = " ")
# separate_rows: split delimited values into rows
df3 <- tibble(id = 1:2, tags = c("a,b,c", "x,y"))
df3 %>% separate_rows(tags, sep = ",")
# id tags
# 1 a
# 1 b
# 1 c
# 2 x
# 2 yUmgang mit fehlenden Werten
Fehlende Werte sind in echten Daten allgegenwärtig. drop_na entfernt Zeilen mit NAs; replace_na füllt sie mit Konstanten. fill trägt die letzte Beobachtung vorwärts (LOCF) – häufig in Zeitreihen. coalesce wählt den ersten Non-NA über Spalten (nützlich zum Mergen überlappender Quellen). na_if konvertiert einen Sentinel-Wert (wie -99 oder 'N/A') zu echtem NA. Immer untersuchen, WARUM Werte fehlen, vor dem Imputieren; MCAR (missing completely at random), MAR und MNAR haben unterschiedliche Implikationen. Für anspruchsvolle Imputation die mice- oder Amelia-Pakete verwenden.
df <- tibble(
x = c(1, 2, NA, 4, 5),
y = c(NA, 2, 3, NA, 5),
z = c("a", NA, "c", "d", NA)
)
# drop rows with any NA
df %>% drop_na()
df %>% drop_na(x) # only column x
# replace NA with a value
df %>% replace_na(list(x = 0, y = -1, z = "unknown"))
# fill NA with previous/next value
df %>% fill(x, y, .direction = "down") # carry forward
df %>% fill(x, y, .direction = "up") # carry backward
# coalesce: first non-missing value
df %>% mutate(x_filled = coalesce(x, y, 0))
# na_if: replace specific value with NA
df %>% mutate(z = na_if(z, "a"))
# detect missing values
is.na(df$x)
sum(is.na(df)) # total NAs
df %>% map_df(~ sum(is.na(.))) # NAs per column
# imputation (simple)
df %>% mutate(x = if_else(is.na(x), mean(x, na.rm = TRUE), x))Nesting & List-Spalten
List-Spalten speichern mehrere Werte (oder sogar ganze Tibbles, Modelle, Plots) pro Zeile – mächtig für Split-Apply-Combine-Workflows. nest() gruppiert Zeilen in verschachtelte Tibbles; map() wendet eine Funktion auf jedes an; unnest() expandiert die Ergebnisse zurück. Dieses Muster (nest → map → unnest) ersetzt viele for-Schleifen und ist der idiomatische tidyverse-Weg für per-Gruppen-Analyse. broom::tidy/glance/augment konvertieren Modell-Objekte in Tibbles und machen sie nest-freundlich. List-Spalten sind auch die Grundlage purrr-basierter funktionaler Programmierung in R.
# nest: group rows into list-columns
nested <- mtcars %>%
group_by(cyl) %>%
nest()
# cyl data
# 4 <tibble 11x10>
# 6 <tibble 7x10>
# 8 <tibble 14x10>
# access nested data
nested$data[[1]] # first group's tibble
# fit models to each group
models <- nested %>%
mutate(
model = map(data, ~ lm(mpg ~ wt, data = .x)),
glance = map(model, broom::glance),
tidy = map(model, broom::tidy)
)
# unnest results
models %>% unnest(glance)
models %>% unnest(tidy)
# unnest a list-column back to rows
df <- tibble(id = 1:2, vals = list(c(1,2,3), c(4,5)))
df %>% unnest(vals)
# id vals
# 1 1
# 1 2
# 1 3
# 2 4
# 2 5
# chop/unchop (similar but keeps other columns as lists)
df %>% chop(vals)
df %>% unchop(vals)Rectangling & JSON
Rectangling konvertiert verschachtelte/hierarchische Daten (JSON, API-Antworten) in tidy Tibbles. hoist() zieht spezifische Elemente aus einer List-Spalte; unnest_wider() spreadet eine Liste in Spalten; unnest_longer() expandiert jedes Element in eine Zeile. Für tief verschachtelte Strukturen purrrs map-Funktionen mit Tibble-Konstruktion kombinieren. jsonlite::fromJSON mit simplifyDataFrame = TRUE auto-flacht einfaches JSON. Dieser Workflow ist unerlässlich für das Arbeiten mit REST-APIs, NoSQL-Datenbanken und Konfigurationsdateien – das tägliche Brot des modernen Dateningenieurs.
library(jsonlite)
library(tidyr)
# parse JSON
json <- '[
{"name":"Alice","age":30,"skills":["R","Python"]},
{"name":"Bob","age":25,"skills":["SQL"]}
]'
parsed <- fromJSON(json, simplifyDataFrame = FALSE)
# convert list to tibble
tibble(person = parsed) %>%
hoist(person,
name = "name",
age = "age",
skills = "skills"
)
# name age skills
# Alice 30 <chr [2]>
# Bob 25 <chr [1]>
# unnest longer for nested lists
tibble(person = parsed) %>%
unnest_wider(person) # spread list to columns
tibble(skills = list(c("R","Python"), c("SQL"))) %>%
unnest_longer(skills)
# deeply nested: use purrr + tibble
flatten_df <- function(lst) {
tibble(
name = lst$name,
age = lst$age,
n_skills = length(lst$skills)
)
}
map_dfr(parsed, flatten_df)
# rectangularize arrays
jsonlite::fromJSON(json, simplifyDataFrame = TRUE) # auto-flattenpurrr Funktionale Programmierung
map-Familie & typsichere Varianten
purrrs map-Familie ersetzt lapply/sapply durch konsistente, typsichere Varianten. map_dbl/chr/int/lgl geben typisierte Vektoren zurück (mit Fehler bei Mismatch) – viel sicherer als sapply, das still koerziert. map2 und pmap iterieren über mehrere Vektoren parallel. imap liefert sowohl Wert als auch Index. walk ist für Seiteneffekte (Drucken, Dateien schreiben), wo man den Rückgabewert nicht benötigt. Die ~ .x-Shortcut erstellt anonyme Funktionen; .x ist das erste Argument, .y das zweite. In Produktionscode immer map_* gegenüber sapply bevorzugen, um Typinstabilität zu vermeiden.
library(purrr)
# map: apply function to each element, return list
map(1:3, ~ .x ^ 2) # list(1, 4, 9)
# type-specific variants (safer, faster)
map_dbl(1:3, ~ .x ^ 2) # numeric vector: 1 4 9
map_chr(c(1,2,3), ~ paste("n=", .x))
map_int(c(1.5, 2.7), floor)
map_lgl(c(1, NA, 3), ~ !is.na(.x))
# map over two vectors in parallel
map2_dbl(c(1,2,3), c(10,20,30), ~ .x + .y) # 11 22 33
# map over multiple vectors (pmap)
pmap_dbl(list(a = 1:3, b = 4:6, c = 7:9), sum) # 12 15 18
# map over indices (imap)
imap_chr(c("a","b","c"), ~ paste0(.y, ":", .x))
# "1:a" "2:b" "3:c"
# walk: side effects (no return value)
walk(c("file1.csv","file2.csv"), ~ print(read.csv(.x)))
# map over columns of a data frame
map_dbl(mtcars, mean) # mean of each column
map(mtcars, class) # class of each columnAnonyme Funktionen & Formel-Syntax
purrr bietet mehrere Wege, Funktionen zu spezifizieren: die Formel-Shortcut (~ .x + 1) für einfache Fälle, volle function(x)-Syntax für komplexe Bodies und benannte Funktionen für Wiederverwendung. Die map-Funktionen akzeptieren auch Strings/Zahlen, um nach Name/Position zu extrahieren – keine Wrapper-Funktion nötig. pluck() navigiert sicher tief verschachtelte Strukturen mit einem .default-Fallback; chuck() ist die strenge Version, die bei fehlenden Pfaden fehlerhaft ist. Dies macht purrr ideal für das Arbeiten mit JSON, API-Antworten und anderen hierarchischen Daten, wo base Rs [[-Extraktion unhandlich wird.
# formula syntax (~ creates a function)
map(1:3, ~ .x + 10) # .x is the argument
map2(1:3, 10:12, ~ .x + .y) # .x, .y for two args
pmap(list(1:3, 4:6, 7:9), ~ ..1 + ..2 + ..3) # ..1, ..2 for many
# full function syntax (for complex bodies)
map(1:3, function(x) {
if (x > 2) return(x * 10)
x
})
# named function
square <- function(x) x^2
map(1:3, square)
# extract by name/position (no function needed)
map(list(a = list(x = 1), b = list(x = 2)), "x")
map(list(list(1,2), list(3,4)), 1) # first element of each
# pluck: safely extract deeply nested
deep <- list(a = list(b = list(c = 42)))
pluck(deep, "a", "b", "c") # 42
pluck(deep, "a", "x", "y", .default = NA) # NA (no error)
# chuck: same but errors if missing
# chuck(deep, "a", "x") # errorReduce, Accumulate & Prädikat-Funktionen
reduce() kombiniert Elemente paarweise (Left Fold) – perfekt zum Mergen vieler Data Frames oder Berechnen von Produkten. accumulate() behält Zwischenergebnisse, nützlich für laufende Summen. keep/discard filtern Elemente nach einem Prädikat (wie dplyr::filter, aber für Vektoren/Listen). some/every testen, ob irgendein/alle Elemente eine Bedingung erfüllen. detect findet das erste übereinstimmende Element. Diese Higher-Order-Funktionen ersetzen viele Schleifen durch prägnanten, deklarativen Code. negate() invertiert eine Prädikat-Funktion – praktisch zum Komponieren von Bedingungen. Zusammen machen sie purrr zu einem vollständigen funktionalen Programmier-Toolkit.
# reduce: combine elements pairwise
reduce(c(1,2,3,4), `+`) # 10 (sum)
reduce(c(1,2,3,4), `*`) # 24 (product)
reduce(list(df1, df2, df3), full_join, by = "id") # merge many
# accumulate: keep intermediate results
accumulate(c(1,2,3,4), `+`) # 1 3 6 10 (running sum)
accumulate(c(2,3,4), `*`) # 2 6 24 (running product)
# keep/discard: filter by predicate
keep(1:10, ~ .x %% 2 == 0) # 2 4 6 8 10
discard(1:10, ~ .x %% 2 == 0) # 1 3 5 7 9
keep(mtcars, is.numeric) # keep numeric columns
# some/every/detect: test predicates
some(1:10, ~ .x > 5) # TRUE
every(1:10, ~ is.numeric(.x)) # TRUE
detect(1:10, ~ .x > 5) # 6 (first match)
detect_index(1:10, ~ .x > 5) # 6 (position)
# head_while/tail_while
head_while(1:10, ~ .x < 5) # 1 2 3 4
tail_while(10:1, ~ .x > 5) # 10 9 8 7 6
# negate a predicate
negate(is.na)(5) # TRUE
keep(c(1, NA, 3), negate(is.na)) # 1 3Safely, Possibly & Fehlerbehandlung
safely() wickelt eine Funktion ein, um immer eine Liste mit 'result' und 'error' zurückzugeben – wirft nie. Dies ist unerlässlich für Batch-Operationen, bei denen ein Fehlschlag nicht den ganzen Lauf stoppen sollte. possibly() gibt einen Default-Wert bei Fehler zurück (sauberer, wenn man die Fehlerdetails nicht benötigt). quietly() erfasst Warnungen und Nachrichten. transpose() konvertiert eine Liste von {result, error}-Paaren in separate Listen – praktisch zum Separieren von Erfolgen und Fehlern. Verwenden, wenn viele Items verarbeitet werden, die individuell fehlschlagen könnten (API-Aufrufe, Datei-Lesungen, Modell-Fits).
# safely: capture errors without stopping
safe_log <- safely(log)
safe_log(10) # list(result = 2.3, error = NULL)
safe_log(-1) # list(result = NULL, error = <error>)
# process many, capturing failures
results <- map(c(10, -1, 0, "x"), safely(log))
successes <- map(results, "result") %>% discard(is.null)
failures <- map(results, "error") %>% discard(is.null)
# possibly: return default on error
safe_log2 <- possibly(log, otherwise = NA_real_)
map_dbl(c(10, -1, 0, "x"), safe_log2) # 2.3 NA NA NA
# quietly: capture warnings/messages
quiet_log <- quietly(log)
quiet_log(10) # list(result, warnings, messages)
# transpose: restructure list-of-lists
transposed <- transpose(results)
transposed$result # all results
transposed$error # all errors
# rate-limited / retried operations
# library(purrr)
# safely_slow <- slowly(safely(f), rate = rate_backoff())
# walk + safely for batch file processing
walk(files, ~ safely(read.csv)(.x))Vektorisiertes & paralleles purrr
modify() ist wie map(), behält aber den Eingabetyp – perfekt zum Transformieren von Data-Frame-Spalten in Place. modify_if und modify_at zielen auf spezifische Spalten. list_modify/list_merge aktualisieren Listen nicht-destruktiv. Für Parallelismus bietet furrr future_map (Drop-in-Ersatz für map) mit dem future-Backend – von sequenziell zu parallel durch Ändern von plan(). Die .progress = TRUE-Option zeigt eine Fortschrittsleiste, unbezahlbar für lang laufende maps. Diese Tools machen purrr geeignet für sowohl interaktive Exploration als auch Produktionspipelines.
# vectorized map (faster for simple operations)
# map_vec returns a vector, auto-detecting type
map_vec(1:3, ~ .x * 2) # numeric vector
map_vec(c("a","b"), ~ toupper(.x)) # character vector
# modify: like map but preserves type
modify(mtcars, ~ .x * 2) # still a data frame
modify_if(mtcars, is.numeric, ~ .x * 2)
modify_at(mtcars, c("mpg","cyl"), ~ .x * 2)
# list_modify / list_merge
l1 <- list(a = 1, b = 2)
list_modify(l1, b = 20, c = 30) # a=1, b=20, c=30
list_merge(l1, list(b = 20, c = 30))
# parallel mapping with future + furrr
# library(furrr)
# plan(multisession) # parallel backend
# future_map(1:100, slow_function, .options = furrr_options(seed = TRUE))
# progress bar
# walk(1:100, ~ Sys.sleep(0.1), .progress = TRUE)
# conditional execution in map
map(1:5, ~ if (.x > 3) .x * 10 else .x)
# 1 2 3 40 50stringr & lubridate
stringr Pattern-Matching & Extraktion
stringr bietet eine konsistente, pipe-freundliche API, die die ICU-Regex-Engine wrappt. Alle Funktionen beginnen mit str_ für einfaches Autovervollständigen. str_detect/which/subset filtern nach Pattern. str_extract/match ziehen Treffer heraus (match erfasst Gruppen). str_replace/remove modifizieren Text. str_split bricht Strings in Stücke. Die zugrunde liegende Regex-Syntax ist Standard (PCRE-ähnlich), mit Helfern wie \\w, \\d, \\s. Für feste Strings (kein Regex) str_detect(text, fixed('a.b')) verwenden, um literal zu matchen. stringr ist viel konsistenter als base Rs grep/sub-Familie.
library(stringr)
text <- c("apple pie", "banana bread", "cherry tart")
# detect pattern
str_detect(text, "pie") # TRUE FALSE FALSE
str_which(text, "pie") # 1
str_count(text, "a") # 1 3 1
# subset strings
str_subset(text, "pie") # "apple pie"
str_extract(text, "[aeiou]") # first vowel
str_extract_all(text, "[aeiou]") # list of all vowels
# match groups
str_match(text, "(\\w+) (\\w+)") # matrix with groups
str_match_all(text, "(\\w+)")
# locate pattern positions
str_locate(text, "a") # start/end matrix
str_locate_all(text, "a")
# replace
str_replace(text, "a", "A") # first match
str_replace_all(text, "a", "A") # all matches
str_remove(text, "a") # str_replace(text, "a", "")
# split
str_split("a,b,c", ",") # list of vectors
str_split_fixed("a,b,c", ",", 3) # matrixString-Manipulation & Transformation
stringr deckt alle häufigen String-Operationen mit einer konsistenten Oberfläche ab. Case-Konvertierung (str_to_upper/lower/title/sentence) respektiert Locale. str_trim entfernt Whitespace; str_squish kollabiert auch internen Whitespace. str_pad richtet Strings auf eine feste Breite aus (nützlich zum Formatieren von Tabellen). str_sub extrahiert oder ersetzt Teilstrings mit Rs 1-basierter Indizierung (negative Indizes zählen vom Ende). str_c ist das pipe-freundliche Äquivalent von paste0. Diese Funktionen machen String-Manipulation vorhersehbar und lesbar im Vergleich zu base Rs verstreuten String-Funktionen.
library(stringr)
# case conversion
str_to_upper("hello") # "HELLO"
str_to_lower("WORLD") # "world"
str_to_title("the wind in the willows") # "The Wind In The Willows"
str_to_sentence("hello world") # "Hello world"
# trimming and padding
str_trim(" hello ") # "hello" (both sides)
str_trim(" hello ", side = "left")
str_squish(" hello world ") # "hello world" (collapse spaces)
str_pad("5", width = 3, pad = "0") # "005"
str_pad("5", width = 3, side = "left", pad = "0") # "005"
# subsetting
str_sub("hello", 1, 3) # "hel"
str_sub("hello", -3, -1) # "llo" (negative from end)
str_sub("hello", 2) # "ello" (to end)
str_sub("hello", 1, 1) <- "H" # assignment: "Hello"
# length
str_length("hello") # 5
str_length(c("a","bb","ccc")) # 1 2 3
# combine and duplicate
str_c("a", "b", "c", sep = "-") # "a-b-c"
str_c(c("x","y"), collapse = ",") # "x,y"
str_dup("ab", 3) # "ababab"
# truncate
str_trunc("Hello World", 8) # "Hello..."Reguläre Ausdrücke im Detail
stringr verwendet die ICU-Regex-Engine mit Standard-Syntax. ^ und $ ankern an Start/Ende. Character-Classes [a-z] matchen Bereiche; \w, \d, \s sind Shortcuts. Quantoren {n,m}, ?, +, * steuern Wiederholung. Klammern erstellen Capture-Gruppen; | ist Alternation. Lookahead (?=) und Lookbehind (?<=) assertieren ohne zu konsumieren. Benannte Gruppen (?<name>...) machen Extraktion selbstdokumentierend. fixed() immer verwenden, wenn man literale Strings matcht, die Regex-Metazeichen enthalten – es ist auch schneller. Für komplexes Parsing das rebus-Paket in Betracht ziehen, um Regex lesbar zu bauen.
library(stringr)
# anchors
str_detect(c("cat","scat","catch"), "^cat") # starts with: T F T
str_detect(c("cat","scat","catch"), "cat$") # ends with: T F F
# character classes
str_extract_all("a1 b2 c3", "[a-z]") # letters
str_extract_all("a1 b2 c3", "[0-9]") # digits
str_extract_all("a1 b2 c3", "[^0-9]") # non-digits
str_extract_all("a1 b2 c3", "\\w") # word chars
str_extract_all("a1 b2 c3", "\\s") # whitespace
# quantifiers
str_detect("aaa", "a{2,3}") # 2-3 a's
str_detect("color", "colou?r") # u optional
str_detect("abbbbc", "ab+c") # one or more
str_detect("ac", "ab*c") # zero or more
# groups and alternation
str_match("2024-01-15", "(\\d{4})-(\\d{2})-(\\d{2})")
str_detect("cat", "cat|dog|bird") # alternation
# lookahead/lookbehind
str_extract("abc123", "(?<=abc)\\d+") # 123 (lookbehind)
str_extract("abc123", "\\d+(?=end)") # nothing (no 'end')
# named capture (stringr 1.5+)
str_match("John 30", "(?<name>[A-Za-z]+) (?<age>\\d+)")
# use fixed() for literal matching
str_detect("a.b.c", fixed(".")) # TRUE (no regex)lubridate Datum- & Zeit-Parsing
lubridates Parsing-Funktionen (ymd, mdy, dmy) auto-erkennen Separatoren und Formate – viel nachsichtiger als base Rs strptime. Der Funktionsname indiziert die Reihenfolge: ymd = Jahr-Monat-Tag. make_date/assemble baut aus Komponenten. today() und now() geben das aktuelle Datum/die Zeit zurück. Komponenten-Funktionen (year, month, day, wday, yday) bekommen und setzen Werte; wday(label=TRUE) gibt Wochentagsnamen zurück. update() modifiziert mehrere Komponenten auf einmal. Für datetime immer tz (Zeitzone) explizit spezifizieren, um stille UTC-Annahmen zu vermeiden.
library(lubridate)
# parse dates (auto-detect format)
ymd("2024-01-15") # 2024-01-15
ymd("2024/01/15")
ymd("24/1/15")
mdy("01-15-2024") # month-day-year
dmy("15/01/2024") # day-month-year
ymd_hms("2024-01-15 14:30:00")
ymd_hm("2024-01-15 14:30")
hms("14:30:45")
# from components
make_date(2024, 1, 15)
make_datetime(2024, 1, 15, 14, 30, 0, tz = "UTC")
# current date/time
today() # 2024-01-15
now() # 2024-01-15 14:30:00 UTC
# extract components
d <- ymd("2024-01-15")
year(d) # 2024
month(d) # 1
month(d, label = TRUE) # "Jan"
day(d) # 15
wday(d, label = TRUE) # "Mon"
yday(d) # 15 (day of year)
quarter(d) # 1
semester(d) # 1
# set components
year(d) <- 2025
month(d) <- 6
d <- update(d, year = 2025, month = 6, day = 1)Zeitzonen, Dauern & Intervalle
Zeitzonen sind der trickreichste Teil der datetime-Behandlung. with_tz zeigt denselben Zeitpunkt in einer anderen Zone; force_tz ändert die Zone, ohne die Uhrzeit zu ändern (nützlich zum Korrigieren falsch gelabelter Daten). Dauern (dseconds, dhours) sind exakte Sekunden – gut für Physik. Perioden (minutes, hours, days) sind kalenderbewusst: das Hinzufügen von months(1) zum 31. Jan. gibt den 28. Feb., und days(1) behandelt DST-Übergänge. Intervalle (start %--% end) repräsentieren Spannen mit festen Endpunkten. Perioden für menschliche Arithmetik (Scheduling) und Dauern für verstrichene Zeitmessung verwenden.
library(lubridate)
# timezones
t <- ymd_hms("2024-01-15 14:30:00", tz = "UTC")
with_tz(t, "America/New_York") # 09:30 EST
with_tz(t, "Asia/Shanghai") # 22:30 CST
force_tz(t, "America/New_York") # keeps clock, changes zone
# list timezones
OlsonNames()[1:5]
# durations (exact seconds)
d <- dseconds(60) + dminutes(2) # 180 seconds
as.numeric(d, "seconds")
dhours(1) + ddays(1)
# periods (calendar-aware, variable length)
p <- minutes(5) + hours(2)
ymd("2024-01-15") + p # 2024-01-15 02:05:00
ymd("2024-03-10") + days(1) # handles DST
months(1) # variable length!
# intervals (start to end)
int <- interval(ymd("2024-01-01"), ymd("2024-02-01"))
int_length(int) # seconds
int %within% int2 # test overlap
as.period(int) # "1M 0S"
# arithmetic
ymd("2024-01-15") %--% ymd("2024-02-15") # interval
ymd("2024-01-15") + weeks(2) # period
ymd("2024-01-15") + dweeks(2) # duration (exact)
# rounding dates
floor_date(t, "hour") # round down
ceiling_date(t, "day")
round_date(t, "hour")R Markdown & Reporting
R Markdown Grundlagen & Chunks
R Markdown kombiniert Prosa (Markdown), Code (R/Python/SQL) und Output (Tabellen, Plots) zu reproduzierbaren Berichten. Der YAML-Header setzt Metadaten und Output-Format. Code-Chunks, die durch dreifache Backticks abgegrenzt sind, führen beim Knitten aus; Chunk-Optionen steuern Verhalten (echo=FALSE verbirgt Code, include=FALSE führt aus, zeigt aber nichts, fig.width setzt Plot-Größe). Inline-Code mit einfachen Backticks fügt Werte in Text ein. kable() formatiert Tabellen; für schickere Tabellen kableExtra oder gt verwenden. Der Knit-Button rendert zu HTML/PDF/Word.
---
title: "Analysis Report"
author: "Data Team"
date: "`r format(Sys.Date(), '%B %d, %Y')`"
output: html_document
---
## Introduction
This is **Markdown** with embedded R.
Inline code: the mean is `r mean(mtcars$mpg)`.
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(dplyr)
library(ggplot2)
```
```{r load-data}
df <- mtcars
head(df)
```
```{r plot, fig.width=8, fig.height=6, fig.cap="MPG by weight"}
ggplot(df, aes(wt, mpg)) + geom_point() + theme_minimal()
```
```{r table}
library(knitr)
kable(summary(df), caption = "Summary statistics")
```Output-Formate & Parameter
Eine R-Markdown-Datei kann mehrere Output-Formate (HTML, PDF, Word, Slides) aus derselben Quelle produzieren – einfach unter output listen. HTML-spezifische Optionen (toc_float, code_folding, theme) erstellen interaktive Dokumente. Parameter (params) ermöglichen das Rendern desselben Berichts mit unterschiedlichen Eingaben – unerlässlich für Batch-Reporting (einer pro Region, Kunde oder Zeitraum). Programmatisch mit rmarkdown::render() rendern, um Berichtsgenerierung in cron-Jobs oder Shiny-Apps zu automatisieren. Das params-Objekt ist innerhalb von Chunks zum Filtern von Daten verfügbar.
---
title: "Quarterly Report"
output:
html_document:
toc: true
toc_float: true
code_folding: hide
theme: flatly
df_print: paged
pdf_document:
toc: true
number_sections: true
word_document: default
params:
quarter: "Q1"
region: "North"
---
## Report for `r params$quarter` - `r params$region`
```{r}
data <- filter(all_data, quarter == params$quarter, region == params$region)
```
# Render with parameters:
# rmarkdown::render("report.Rmd", params = list(quarter = "Q2"))
# parameterized batch rendering
quarters <- c("Q1","Q2","Q3","Q4")
for (q in quarters) {
rmarkdown::render("report.Rmd",
params = list(quarter = q),
output_file = paste0("report_", q, ".html")
)
}Tabellen mit kable, gt & DT
kable + kableExtra produziert publikationsqualitative Tabellen mit Styling, Gruppierung und bedingter Formatierung. gt ist eine moderne Alternative mit einer ausdrucksstärkeren Grammatik (wie ggplot für Tabellen). DT erstellt interaktive HTML-Tabellen mit Suche, Sortierung und Paginierung – perfekt für explorative Berichte. reactable bietet noch mehr Interaktivität. Wahl basierend auf Output: kable/gt für PDF/Word, DT/reactable für HTML. Zahlen immer konsistent formatieren (fmt_number, formatRound) und Captions für Kontext hinzufügen. Gute Tabellen sind genauso wichtig wie gute Plots für die Kommunikation von Ergebnissen.
library(knitr); library(kableExtra)
# basic kable
kable(head(mtcars), caption = "First 6 rows")
# styled kable
kable(head(mtcars), "html") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
row_spec(0, bold = TRUE, background = "lightblue") %>%
column_spec(1, bold = TRUE)
# gt package (modern, flexible)
library(gt)
mtcars %>%
head() %>%
gt() %>%
tab_header(title = "Car Specifications", subtitle = "Top 6") %>%
fmt_number(columns = mpg, decimals = 1) %>%
data_color(columns = mpg, colors = scales::col_numeric("Reds", NULL))
# DT: interactive tables
library(DT)
datatable(mtcars,
filter = "top",
options = list(pageLength = 10),
caption = "Searchable table"
) %>%
formatRound(columns = c("mpg","disp"), digits = 2)
# reactable for even more interactivity
# library(reactable)Quarto & fortgeschrittene Features
Quarto ist der Nachfolger von R Markdown und unterstützt R, Python, Julia und Observable in einem Dokument. Querverweise (@fig-label, @tbl-label) auto-nummerieren Abbildungen und Tabellen. Die #|-Syntax für Chunk-Optionen ist sauberer als die alten knitr-opts. Code-Folding erstellt einklappbare Code-Blöcke für interaktives HTML. Quartos Multi-Language-Support macht es ideal für Teams, die sowohl R als auch Python verwenden. Bestehende .Rmd-Dateien können konvertiert werden; die Syntax ist ähnlich, aber konsistenter. Quarto produziert auch Präsentationen (revealjs), Websites und Bücher aus derselben Quelle.
---
title: "Modern Report"
format:
html:
toc: true
code-fold: true
pdf:
documentclass: article
execute:
echo: false
warning: false
filters:
- lightbox
---
## Cross-references
See Figure @fig-scatter and Table @tbl-summary.
```{r}
#| label: fig-scatter
#| fig-cap: "MPG vs Weight"
ggplot(mtcars, aes(wt, mpg)) + geom_point()
```
```{r}
#| label: tbl-summary
#| tbl-cap: "Summary by cylinder"
mtcars %>%
group_by(cyl) %>%
summarize(mean_mpg = mean(mpg)) %>%
gt()
```
## Multiple languages
```{python}
import pandas as pd
df = pd.DataFrame({'a': [1,2,3]})
print(df)
```
```{sql}
SELECT * FROM mtcars WHERE mpg > 20
```
# Quarto supports Python, Julia, Observable, and more
# in the same document, sharing data via ojs.Automatisiertes Reporting & Cron
Automatisiertes Reporting verwandelt einmalige Analysen in wiederkehrende Deliverables. rmarkdown::render() generiert Berichte programmatisch; mit params für Anpassung kombinieren. Ergebnisse per E-Mail mit blastula oder emayili versenden. Mit cronR (Linux/Mac) oder taskscheduleR (Windows) für tägliche/wöchentliche Läufe schedulen. Für Batch-Generierung über Parameter mit purrr::walk loopen. Chunk-Caching (cache=TRUE) aktivieren, um Iteration auf teuren Berechnungen zu beschleunigen – nur geänderte Chunks laufen neu. Diese Pipeline ist das Rückgrat von Business Intelligence und automatisierten Datenprodukten in R.
# render a report programmatically
rmarkdown::render("daily_report.Rmd",
output_dir = "reports",
output_file = paste0("report_", Sys.Date(), ".html"),
params = list(date = Sys.Date() - 1),
quiet = TRUE
)
# email the report
# library(blastula)
# render_email("email_template.Rmd") %>%
# smtp_send(
# to = "[email protected]",
# from = "[email protected]",
# subject = paste("Daily Report -", Sys.Date())
# )
# schedule with cron (Linux/Mac) or Task Scheduler (Windows)
# crontab -e:
# 0 8 * * * cd /path/to/project && Rscript -e 'rmarkdown::render("daily.Rmd")'
# or use the cronR package
# library(cronR)
# cmd <- cron_rscript("render_report.R")
# cron_add(cmd, frequency = "daily", at = "08:00")
# batch render multiple reports
purrr::walk(regions, function(r) {
rmarkdown::render("regional.Rmd",
params = list(region = r),
output_file = paste0("report_", r, ".html")
)
})
# version control: cache expensive computations
# knitr::opts_chunk$set(cache = TRUE)Shiny Web Apps
App-Struktur: UI & Server
Jede Shiny-App hat zwei Teile: ui (das HTML-Layout) und server (die R-Logik). Die UI verwendet fluidPage und Layout-Funktionen (sidebarLayout, tabsetPanel, navbarPage). Inputs (sliderInput, selectInput usw.) sammeln Benutzerdaten; Outputs (plotOutput, textOutput) zeigen Ergebnisse. Die Server-Funktion verbindet sie via render*-Funktionen. Reactive Expressions (reactive({...})) cachen Berechnungen und laufen nur neu, wenn Inputs sich ändern. Als app.R speichern und mit runApp() ausführen oder auf shinyapps.io / RStudio Connect / Shiny Server hosten.
library(shiny)
ui <- fluidPage(
titlePanel("My First Shiny App"),
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points:", min = 1, max = 100, value = 50),
selectInput("color", "Color:", choices = c("red","blue","green"))
),
mainPanel(
plotOutput("scatter"),
verbatimTextOutput("summary")
)
)
)
server <- function(input, output, session) {
data <- reactive({
data.frame(x = rnorm(input$n), y = rnorm(input$n))
})
output$scatter <- renderPlot({
plot(data()$x, data()$y, col = input$color, pch = 19,
xlab = "X", ylab = "Y", main = paste("n =", input$n))
})
output$summary <- renderPrint({
summary(data())
})
}
shinyApp(ui, server)
# save as app.R in a folder, then runApp("folder")
# or run with shiny::runApp()Reaktive Programmierung
Reaktivität ist Shinys Kernkonzept. reactive() erstellt lazy, gecachte Expressions, die nur neu laufen, wenn sich Dependencies ändern. observe() läuft eagerly für Seiteneffekte (Inputs aktualisieren, Logging). observeEvent/eventReactive triggern bei spezifischen Events (Button-Klicks). reactiveVal und reactiveValues halten mutablen Zustand. isolate() liest einen Wert, ohne eine Dependency zu erstellen. Die key insight: Outputs rendern automatisch neu, wenn sich ihre reaktiven Dependencies ändern. Missverstehen von Reaktivität ist die #1 Quelle von Shiny-Bugs – reactiveLogViewer() verwenden, um Dependency-Graphen zu debuggen.
server <- function(input, output, session) {
# reactive expression: lazy, cached
filtered <- reactive({
mtcars %>% filter(cyl == input$cyl)
})
# observe: eager, for side effects
observe({
updateSelectInput(session, "model",
choices = unique(filtered()$model))
})
# observeEvent: triggered by specific input
observeEvent(input$reset, {
updateSliderInput(session, "cyl", value = 4)
})
# eventReactive: lazy, triggered by event
result <- eventReactive(input$go, {
run_analysis(input$param)
})
# reactiveVal: mutable value
counter <- reactiveVal(0)
observeEvent(input$add, counter(counter() + 1))
# reactiveValues: multiple mutable values
rv <- reactiveValues(data = NULL, status = "ready")
observeEvent(input$load, {
rv$data <- read.csv(input$file$datapath)
rv$status <- "loaded"
})
# isolate: read without dependency
output$plot <- renderPlot({
plot(isolate(rv$data))
})
}Dynamische UI & Module
Dynamische UI (renderUI + uiOutput) generiert Controls basierend auf Daten oder Benutzerwahlen. insertUI/removeUI fügen Elemente hinzu/entfernen, ohne die ganze Seite neu zu rendern. Module (NS + moduleServer) kapseln UI+Server-Logik für Wiederverwendung – unerlässlich für komplexe Apps mit wiederholten Komponenten. Jede Modul-Instanz bekommt einen eindeutigen Namespace (ns), sodass Input-IDs nicht kollidieren. Module sind der Schlüssel zu wartbaren Shiny-Apps: die App in kleine, testbare Module aufteilen (ein Chart-Modul, ein Filter-Modul, ein Daten-Upload-Modul) und komponieren.
# dynamic UI
ui <- fluidPage(
uiOutput("dynamic_controls"),
plotOutput("plot")
)
server <- function(input, output, session) {
output$dynamic_controls <- renderUI({
cols <- names(input$data)
selectInput("xvar", "X variable:", choices = cols)
})
# insert/remove UI
observeEvent(input$add, {
insertUI("#placeholder", "beforeEnd",
sliderInput(paste0("s", input$add), "Slider", 0, 100, 50))
})
# modules: reusable UI+server
histogramUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(ns("var"), "Variable:", names(mtcars)),
plotOutput(ns("hist"))
)
}
histogramServer <- function(id) {
moduleServer(id, function(input, output, session) {
output$hist <- renderPlot(hist(mtcars[[input$var]]))
})
}
# use module
ui <- fluidPage(histogramUI("hist1"), histogramUI("hist2"))
server <- function(input, output, session) {
histogramServer("hist1")
histogramServer("hist2")
}
}Inputs, Outputs & Rendering
Shiny unterstützt viele Input-Typen (file, date, slider, selectize, checkbox) und Output-Typen (plot, table, text, image, UI). DT::renderDataTable erstellt interaktive Tabellen mit Suche/Sortierung. downloadHandler ermöglicht Benutzern, Daten zu exportieren. .data[[]] ermöglicht dynamische Spaltenauswahl in ggplot. Für große Daten DTs serverseitige Verarbeitung oder plotly für interaktive Plots verwenden. renderImage zeigt vor-generierte Dateien (schneller als renderPlot für komplexe Visuals). Inputs mit reaktiven Expressions kombinieren, um anspruchsvolle, responsive Dashboards zu bauen.
# file upload
fileInput("data", "Upload CSV:", accept = ".csv")
# in server: input$data$datapath (temp file path)
# date range
dateRangeInput("dates", "Period:", start = Sys.Date() - 30, end = Sys.Date())
# tabset with conditional panels
tabsetPanel(
tabPanel("Plot", plotOutput("p")),
tabPanel("Table", DT::dataTableOutput("t")),
tabPanel("Summary", verbatimTextOutput("s"))
)
# render DataTable (interactive)
output$t <- DT::renderDataTable({
datatable(filtered(), filter = "top", options = list(pageLength = 25))
})
# render UI from ggplot
output$p <- renderPlot({
ggplot(filtered(), aes(.data[[input$x]], .data[[input$y]])) +
geom_point() + theme_minimal()
})
# download handlers
output$download <- downloadHandler(
filename = function() paste0("data_", Sys.Date(), ".csv"),
content = function(file) write.csv(filtered(), file)
)
# render image (pre-generated)
output$img <- renderImage({
list(src = "plot.png", contentType = "image/png", width = 400)
}, deleteFile = FALSE)Performance, Deployment & Skalierung
Shiny-Performance: debounce/throttle schnelle Inputs (Suchboxen), um exzessive Neuberechnung zu vermeiden. future + promises für asynchrone Operationen verwenden (den Event-Loop nicht blockieren). bindCache cacht Render-Ergebnisse nach Schlüssel – riesige Gewinne für teure Plots, auf die viele Benutzer zugreifen. Auf shinyapps.io (managed Cloud), RStudio Connect (kommerziell) oder Shiny Server (Open Source) hinter Docker deployen. Für hohen Traffic mehrere Worker-Prozesse laufen lassen und Load Balancing betreiben. Nutzung mit shinylogs überwachen, um Benutzerverhalten zu verstehen und Fehler zu erwischen. Langsame Apps mit profvis::profvis() profilen, um Bottlenecks zu finden.
# performance: debounce/throttle rapid inputs
input_debounced <- debounce(reactive({ input$text }), 500)
# async with future
library(future)
plan(multisession)
result <- reactive({
future_promise({ expensive_computation(input$params) })
})
# caching expensive computations
# in UI: add resourcePath or use bindCache
output$plot <- renderPlot({ ... }) %>% bindCache(input$dataset)
# deploy to shinyapps.io
# library(rsconnect)
# deployApp("myapp/")
# run multiple Shiny apps behind a load balancer
# (shinyapps.io / RStudio Connect handle this automatically)
# dockerize for self-hosting
# Dockerfile:
# FROM rocker/shiny
# COPY app.R /srv/shiny-server/
# EXPOSE 3838
# shiny server config (/etc/shiny-server/shiny-server.conf)
# run_as shiny;
# server { listen 3838; location / { site_dir /srv/shiny-server; } }
# monitor with shinylogs or shiny.telemetryStatistische Tests & Inferenz
Hypothesentest-Framework
Hypothesentesten evaluiert, ob beobachtete Daten mit einer Nullhypothese konsistent sind. Der t-Test vergleicht Mittelwerte (parametrisch, nimmt Normalität an); Wilcoxon ist die nicht-parametrische Alternative. Immer Effektgrößen und Konfidenzintervalle berichten, nicht nur p-Werte – p hängt von der Stichprobengröße ab, während CIs praktische Signifikanz zeigen. Annahmen vor der Interpretation prüfen: Normalität (Shapiro-Wilk), gleiche Varianz (Levene). Power-Analyse (pwr-Paket) bestimmt die benötigte Stichprobengröße vor der Datensammlung. Die 0.05-Schwelle ist konventionell, nicht magisch – Effektgröße und Kontext berücksichtigen.
# one-sample t-test: is mean different from hypothesized value?
t.test(mtcars$mpg, mu = 20)
# t = 0.085, df = 31, p-value = 0.933
# 95% CI: [17.92, 22.26]
# mean of x: 20.09
# two-sample t-test
t.test(mpg ~ cyl, data = subset(mtcars, cyl %in% c(4, 6)))
# paired t-test
t.test(pre, post, paired = TRUE)
# Wilcoxon (non-parametric alternative)
wilcox.test(mpg ~ cyl, data = subset(mtcars, cyl %in% c(4,6)))
# interpret results:
# p < 0.05: reject null hypothesis (significant)
# p > 0.05: fail to reject (not enough evidence)
# CI shows plausible range of effect
# check assumptions: normality (shapiro.test), equal variance
# power analysis
library(pwr)
pwr.t.test(d = 0.5, sig.level = 0.05, power = 0.8)
# n = 64 per group needed to detect medium effectANOVA & multiple Vergleiche
ANOVA testet, ob sich Mittelwerte über 3+ Gruppen unterscheiden. Der F-Test sagt, ob IRGENDEIN Unterschied existiert; Post-hoc-Tests (Tukey HSD, pairwise.t.test mit Korrektur) identifizieren, WELCHE Gruppen sich unterscheiden. Immer Annahmen prüfen (Normalität, Homoskedastizität) und nicht-parametrische Alternativen (Kruskal-Wallis) verwenden, wenn verletzt. Für repeated measures oder hierarchische Daten Mixed-Effects-Modelle (lme4::lmer) verwenden, die within-subject-Korrelation richtig behandeln. Multiple-Comparison-Korrekturen (Bonferroni, FDR) verhindern False Positives beim Ausführen vieler Tests. Das afex-Paket vereinfacht repeated-measures-ANOVA.
# one-way ANOVA: compare means across 3+ groups
fit <- aov(mpg ~ factor(cyl), data = mtcars)
summary(fit)
# Df Sum Sq Mean Sq F value Pr(>F)
# factor(cyl) 2 824.8 412.4 39.7 4.98e-09
# check assumptions
plot(fit) # residuals vs fitted, QQ
shapiro.test(resid(fit)) # normality of residuals
library(car); leveneTest(fit) # equal variance
# post-hoc tests (which groups differ?)
TukeyHSD(fit) # Tukey HSD
pairwise.t.test(mtcars$mpg, mtcars$cyl, p.adjust = "bonferroni")
# two-way ANOVA with interaction
fit2 <- aov(mpg ~ cyl * am, data = mtcars)
summary(fit2)
# Kruskal-Wallis (non-parametric ANOVA)
kruskal.test(mpg ~ factor(cyl), data = mtcars)
# repeated measures ANOVA
# library(afex)
# aov_ez(id = "subject", dv = "score", data = df, within = "condition")
# mixed-effects models (lme4)
library(lme4)
lmer(score ~ treatment + (1|subject), data = df)Korrelation & Regression-Diagnostik
Korrelation misst lineare Assoziation (-1 bis 1); cor.test fügt Inferenz hinzu. Lineare Regression (lm) passt y = β0 + β1*x + ε. Die vier Diagnostik-Plots offenbaren Annahmeverletzungen: Nicht-Linearität, nicht-normale Residuen, Heteroskedastizität und einflussreiche Punkte. VIF > 5-10 indiziert Multikollinearität (Prädiktoren zu korreliert). Cook's Distance identifiziert einflussreiche Beobachtungen. Konfidenzintervalle (Mittelwert-Antwort) vs. Vorhersageintervalle (neue Beobachtung) angemessen verwenden. Modelle mit anova (verschachtelt) oder AIC/BIC (nicht-verschachtelt, niedriger ist besser) vergleichen. Immer visualisieren, bevor man p-Werten vertraut.
# correlation
cor(mtcars$mpg, mtcars$wt) # -0.867
cor.test(mtcars$mpg, mtcars$wt) # with p-value
cor(mtcars[, c("mpg","wt","hp","disp")]) # correlation matrix
library(corrplot); corrplot(cor(mtcars[,1:4]))# visualize
# linear regression
fit <- lm(mpg ~ wt + hp, data = mtcars)
summary(fit)
confint(fit) # CIs for coefficients
# diagnostics
par(mfrow = c(2,2)); plot(fit)
# 1. Residuals vs Fitted: check linearity
# 2. Normal Q-Q: check normality
# 3. Scale-Location: check homoscedasticity
# 4. Residuals vs Leverage: check influential points
# check for multicollinearity
library(car); vif(fit) # variance inflation factor
# influential observations
cooks.distance(fit) |> plot()
influence.measures(fit)
# predictions with intervals
predict(fit, newdata, interval = "confidence") # CI for mean
predict(fit, newdata, interval = "prediction") # PI for new obs
# compare nested models
anova(fit1, fit2) # F-test
AIC(fit1, fit2); BIC(fit1, fit2) # information criteriaKategoriale Daten: Chi-Quadrat & Fisher
Chi-Quadrat testet Unabhängigkeit zwischen kategorialen Variablen; Fisher's exakter Test ist genauer für kleine Stichproben (erwartete Zählungen < 5). Erwartete Zählungen prüfen, bevor man Chi-Quadrat-Ergebnissen vertraut. Goodness-of-Fit vergleicht beobachtete mit theoretischen Proportionen. McNemar testet gepaarte nominale Daten (vor/nach). Effektgrößen (Cramer's V, phi) quantifizieren Assoziationsstärke jenseits von p-Werten. Mosaic-Plots visualisieren Kontingenztafeln intuitiv. Für ordinale Daten Spearman-Korrelation oder Trend-Tests in Betracht ziehen. Das vcd-Paket (Visualizing Categorical Data) bietet umfassende Tools.
# contingency table
tbl <- table(mtcars$cyl, mtcars$am)
# 0 1
# 4 3 8
# 6 4 3
# 8 12 2
# chi-square test of independence
chisq.test(tbl)
# X-squared = 8.74, df = 2, p-value = 0.0126
# expected counts (should be > 5 for valid chi-square)
chisq.test(tbl)$expected
# Fisher's exact test (small samples)
fisher.test(tbl)
# goodness of fit (one variable vs expected proportions)
chisq.test(c(10, 20, 30), p = c(1/3, 1/3, 1/3))
# McNemar's test (paired nominal data)
mcnemar.test(table(pre, post))
# Cochran-Mantel-Haenszel (stratified)
mantelhaen.test(tbl3d)
# visualize
library(ggplot2); library(ggmosaic)
ggplot(as.data.frame(tbl)) +
geom_mosaic(aes(weight = Freq, x = product(Var1), fill = Var2))
# effect size
library(vcd); assocstats(tbl) # Cramer's V, phiBayesianische Inferenz mit brms
Bayesianische Inferenz (via brms, das Stan wrappt) bietet volle Posterior-Verteilungen statt Punktschätzungen. Priors spezifizieren, um Domain-Wissen zu enkodieren; schwach informative Priors (normal(0, 10)) regularisieren ohne zu verzerren. Posterior-Summaries geben Credible Intervals (direkte Wahrscheinlichkeitsaussagen, anders als frequentistische CIs). pp_check validiert Modell-Fit durch Vergleichen von simulierten mit beobachteten Daten. LOO-CV und WAIC vergleichen Modelle via kreuzvalidierter prädiktiver Genauigkeit. Hierarchische Modelle behandeln gruppierte Daten natürlich. Bayesianische Methoden glänzen bei kleinen Stichproben, komplexen Modellen und wenn man Unsicherheitsquantifizierung benötigt.
library(brms)
# Bayesian linear regression
fit <- brm(
mpg ~ wt + hp,
data = mtcars,
family = gaussian(),
prior = c(
prior(normal(0, 10), class = "b"), # weakly informative
prior(cauchy(0, 2), class = "sigma")
),
chains = 4, cores = 4, iter = 2000
)
# summary
summary(fit)
plot(fit) # posterior distributions
pp_check(fit) # posterior predictive check
# extract posterior samples
posterior <- as_draws_df(fit)
mean(posterior$b_wt > 0) # P(coefficient > 0)
# predictions
posterior_predict(fit, newdata) |> as.data.frame() -> preds
# model comparison
fit_null <- brm(mpg ~ 1, data = mtcars, ...)
loo(fit, fit_null) # leave-one-out CV
waic(fit, fit_null)
# hierarchical model
fit_h <- brm(
score ~ treatment + (1 + treatment|subject),
data = df, family = gaussian()
)
# Stan code behind the model
stancode(fit)Machine Learning mit caret
Daten-Splitting & Preprocessing
Ordnungsgemäßes Daten-Splitting und Preprocessing sind 80% des ML-Erfolgs. createDataPartition macht stratifiziertes Sampling (erhält Klassenbalance). trainControl konfiguriert Resampling (CV, Bootstrap, wiederholtes CV). preProcess behandelt Standardisierung, Transformation, PCA und Imputation – immer nur auf Trainingsdaten fitten und auf Test anwenden, um Leakage zu vermeiden. nzv entfernt uninformative Spalten. Für Zeitreihen createTimeSlices statt zufälligem CV verwenden. Carets einheitliche Oberfläche bedeutet, dasselbe Preprocessing über alle Modelltypen hinweg funktioniert.
library(caret)
# split data
set.seed(42)
idx <- createDataPartition(iris$Species, p = 0.8, list = FALSE)
train <- iris[idx, ]; test <- iris[-idx, ]
# stratified split for classification
# createDataPartition preserves class proportions
# k-fold cross-validation
ctrl <- trainControl(
method = "cv", number = 10,
savePredictions = "final",
classProbs = TRUE, # for AUC
summaryFunction = multiClassSummary
)
# preprocessing pipeline
preproc <- preProcess(train[, -5],
method = c("center","scale","YeoJohnson","nzv"))
train_processed <- predict(preproc, train[, -5])
test_processed <- predict(preproc, test[, -5])
# common preprocessing methods:
# center, scale: standardize
# BoxCox, YeoJohnson: transform to normality
# pca: principal components
# nzv: remove near-zero variance
# knnImpute, bagImpute: impute missing values
# dummy variables for categorical
dummies <- dummyVars(Species ~ ., data = train)
predict(dummies, train)Modelle trainieren & Tuning
carets train()-Funktion bietet eine einheitliche Oberfläche zu 200+ Modellen – einfach den method-String ändern. tuneLength auto-generiert ein Tuning-Grid; tuneGrid gibt volle Kontrolle. resamples() vergleicht mehrere Modelle via resampled Performance (ehrlicher als Single-Test-Set-Evaluation). Immer dasselbe trControl über Modelle verwenden für fairen Vergleich. Der Dotplot von resamples zeigt Überlappung in Performance – wenn CIs überlappen, sind Modelle nicht signifikant unterschiedlich. Das einfachste Modell innerhalb eines Standardfehlers des besten wählen (die 'One-SE-Regel').
library(caret)
# train a random forest
ctrl <- trainControl(method = "cv", number = 5)
rf_fit <- train(
Species ~ ., data = train,
method = "rf",
trControl = ctrl,
tuneGrid = expand.grid(mtry = 1:4),
tuneLength = 5 # auto-tune grid
)
print(rf_fit) # shows accuracy by tuning param
plot(rf_fit) # tuning curve
# try multiple models
models <- c("rf","gbm","svmRadial","knn","rpart")
fits <- lapply(models, function(m) {
train(Species ~ ., data = train, method = m, trControl = ctrl)
})
names(fits) <- models
# compare models
resamps <- resamples(fits)
summary(resamps)
dotplot(resamps, metric = "Accuracy")
# custom tuning grid
grid <- expand.grid(
mtry = c(2, 4, 8),
splitrule = c("gini","extratrees"),
min.node.size = c(1, 5, 10)
)
fit <- train(Species ~ ., data = train, method = "ranger",
trControl = ctrl, tuneGrid = grid)Klassifikationsmetriken & Confusion Matrix
Accuracy allein ist irreführend für unausgewogene Daten. confusionMatrix bietet per-Klassen-Sensitivity (Recall), Specificity, Precision und F1. Für binäre Probleme misst ROC-AUC Diskriminierung; PR-Kurven sind besser, wenn die positive Klasse selten ist. Für Multi-Klassen macro/micro-averaged Metriken oder Log-Loss verwenden. Immer auf einem zurückgehaltenen Test-Set evaluieren (oder via nested CV für unbeeinflusste Schätzungen). In caret summaryFunction in trainControl setzen, um die richtige Metrik zu optimieren (z.B. mnLogLoss für probabilistische Vorhersagen). Konfidenzintervalle auf Performance berichten, nicht nur Punktschätzungen.
# predictions
pred <- predict(rf_fit, test)
prob <- predict(rf_fit, test, type = "prob")
# confusion matrix
cm <- confusionMatrix(pred, test$Species)
print(cm)
# Reference
# Prediction setosa versicolor virginica
# setosa 10 0 0
# versicolor 0 10 1
# virginica 0 0 9
# Overall Accuracy: 0.9667
# byClass: Sensitivity, Specificity, etc.
# two-class metrics
cm$byClass[, c("Sensitivity","Specificity","Precision","Recall","F1")]
# ROC and AUC
library(pROC)
roc_curve <- roc(test$Species == "versicolor", prob$versicolor)
auc(roc_curve)
plot(roc_curve)
# multi-class AUC
library(pRoc)
multiclass.roc(test$Species, prob)
# precision-recall curve (better for imbalanced data)
library(PRROC)
pr <- pr.curve(scores.class0 = prob$versicolor,
weights.class0 = test$Species == "versicolor",
curve = TRUE)
plot(pr)
# custom metrics in trainControl
twoClassSummary # built-in for 2-class
mnLogLoss # multi-class log lossFeature-Auswahl & Interpretation
Feature-Auswahl verbessert Modell-Performance und Interpretierbarkeit. RFE (Recursive Feature Elimination) wickelt ein Modell ein und entfernt iterativ die am wenigsten wichtigen Features. varImp extrahiert Wichtigkeit aus jedem caret-trainierten Modell (Random Forest, gbm usw.). Filter-Methoden (findCorrelation, findLinearCombos) entfernen redundante Features vor dem Training. Für Black-Box-Interpretation attribuieren SHAP-Werte (fastshap, shapviz) Vorhersagen zu Features. Feature-Auswahl immer innerhalb von Kreuzvalidierung durchführen, um Selection-Bias zu vermeiden. Einfachere Modelle mit weniger Features generalisieren oft besser.
library(caret)
# recursive feature elimination (RFE)
ctrl <- rfeControl(functions = rfFuncs, method = "cv", number = 10)
rfe_fit <- rfe(train[,-5], train$Species,
sizes = c(1:4), rfeControl = ctrl)
print(rfe_fit) # optimal subset
predictors(rfe_fit) # selected features
plot(rfe_fit)
# variable importance from trained model
varImp(rf_fit) # caret extracts importance
plot(varImp(rf_fit))
# filter methods (before training)
# remove highly correlated features
cor_matrix <- cor(train[,-5])
high_cor <- findCorrelation(cor_matrix, cutoff = 0.9)
train_filtered <- train[,-high_cor]
# remove linear dependencies
findLinearCombos(train[,-5])
# genetic algorithm / simulated annealing selection
# gafsFit <- gafs(x, y, iters = 20)
# safsFit <- safs(x, y, iters = 20)
# SHAP values (model-agnostic interpretation)
# library(fastshap)
# explain(rf_fit, X = test, nsim = 100)Ensembles & Stacking
Ensembles kombinieren mehrere Modelle für bessere Performance als jedes einzelne Modell. caretEnsemble trainiert Modelle mit identischem Resampling (erforderlich für faires Stacking). caretStack trainiert ein Meta-Modell auf Basis-Vorhersagen – das Meta-Modell lernt, wann jedem Basis-Modell zu vertrauen ist. Einfaches Mitteln funktioniert überraschend gut für ähnlich genaue Modelle. Gewichtete Ensembles ermöglichen das Betonen besserer Modelle. Bagging (treebag) reduziert Varianz durch Mitteln gebootstrappter Modelle. Die Diversität der Basis-Modelle ist wichtiger als ihre individuelle Genauigkeit – Modelle kombinieren, die unterschiedliche Fehler machen.
library(caretEnsemble)
# train multiple models with same resampling
ctrl <- trainControl(method = "cv", number = 5,
savePredictions = "final",
classProbs = TRUE)
models <- caretList(
Species ~ ., data = train,
trControl = ctrl,
methodList = c("rf","gbm","svmRadial","knn")
)
# simple ensemble (average predictions)
ens <- caretEnsemble(models)
summary(ens)
plot(ens)
# stack: train a meta-model on base predictions
stack <- caretStack(models, method = "glm",
metric = "Accuracy", trControl = ctrl)
print(stack)
# predict with ensemble
pred_ens <- predict(ens, test)
pred_stack <- predict(stack, test)
# weighted ensemble (custom weights)
weights <- c(0.4, 0.3, 0.2, 0.1)
probs <- lapply(models, function(m) predict(m, test, type = "prob"))
final_prob <- Reduce('+', Map(function(p, w) p * w, probs, weights))
# bagging (bootstrap aggregating)
bag_fit <- train(Species ~ ., data = train, method = "treebag",
trControl = ctrl)Apply-Familie & Performance
apply, lapply, sapply, vapply
Die apply-Familie ist base Rs funktionales Programmier-Toolkit. apply arbeitet auf Arrays (Margin 1 für Zeilen, 2 für Spalten verwenden), aber eingebaute rowSums/colSums sind schneller. lapply gibt immer eine Liste zurück; sapply versucht, zu einem Vektor zu vereinfachen (praktisch, aber typ-instabil). vapply ist die sichere Version – man spezifiziert das Output-Template, sodass es bei Mismatch fehlerhaft ist statt still zu koerzieren. vapply in Produktionscode verwenden, sapply interaktiv. replicate ist praktisch für Simulationen. mapply (oder Map) iteriert über mehrere Argumente parallel. Für modernen Code purrrs map-Familie für Konsistenz bevorzugen.
# apply: over array margins (rows or columns)
m <- matrix(1:12, 3, 4)
apply(m, 1, sum) # row sums: 22 26 30
apply(m, 2, mean) # column means
apply(m, c(1,2), sqrt) # element-wise (silly but valid)
# built-in row/col functions are faster
rowSums(m); rowMeans(m)
colSums(m); colMeans(m)
# lapply: list in, list out
lapply(list(a=1:3, b=4:6), mean) # list(a=2, b=5)
lapply(mtcars, class) # class of each column
# sapply: list in, vector out (simplifies)
sapply(mtcars, mean) # named numeric vector
sapply(mtcars, is.numeric) # logical vector
# vapply: type-safe sapply (specify output template)
vapply(mtcars, mean, numeric(1)) # always numeric(1)
vapply(mtcars, class, character(1)) # always character(1)
# replicate: repeat expression n times
replicate(5, rnorm(3)) # 3x5 matrix
replicate(100, mean(rexp(30))) # 100 sample means
# mapply: multivariate map
mapply(rep, 1:3, 3:1) # list(1,1,1, 2,2, 3)
mapply(function(a,b) a+b, 1:3, 4:6) # 5 7 9Vektorisierung & Geschwindigkeit
Vektorisierung ist die #1 R-Performance-Optimierung. Rs Arithmetik, Vergleichs- und Mathe-Funktionen operieren auf ganzen Vektoren via optimiertem C-Code – Schleifen in R sind interpretiert und langsam. ifelse ist vektorisiert, hat aber Overhead; direkte logische Indizierung (x * (x > 5)) ist am schnellsten. apply auf Data Frames vermeiden (es koerziert zu Matrix); stattdessen vektorisierte Spalten-Operationen verwenden. Ergebnis-Vektoren immer vorinitialisieren – das Wachsen mit c() ist O(n^2). Für echte Hot-Schleifen ermöglicht Rcpp, C++ inline zu schreiben. Mit microbenchmark microbenchmarden, um Verbesserungen zu verifizieren; system.time ist zu grob.
# BAD: element-wise loop
x <- 1:1e6
y <- numeric(length(x))
for (i in seq_along(x)) y[i] <- x[i]^2
# GOOD: vectorized
y <- x^2 # ~100x faster
# ifelse vs vectorized
# BAD
y <- numeric(length(x))
for (i in seq_along(x)) {
y[i] <- if (x[i] > 5) x[i] else 0
}
# GOOD
y <- ifelse(x > 5, x, 0)
# BEST (fastest)
y <- x * (x > 5)
# row-wise operations (avoid if possible)
df <- data.frame(a = 1:1000, b = 1001:2000)
# BAD
df$sum <- apply(df, 1, sum)
# GOOD
df$sum <- df$a + df$b
# preallocate!
n <- 1000
result <- numeric(n) # not result <- c()
for (i in 1:n) result[i] <- i^2
# use Rcpp for hot loops
# library(Rcpp)
# cppFunction('double sumc(NumericVector x) { return sum(x); }')
# benchmark
library(microbenchmark)
microbenchmark(
loop = {y <- numeric(length(x)); for(i in seq_along(x)) y[i] <- x[i]^2},
vectorized = x^2,
times = 10
)Memory & data.table
data.table ist dramatisch schneller und memory-effizienter als data.frame/dplyr für große Daten (1M+ Zeilen). Die dt[i, j, by]-Syntax kombiniert Filterung, Auswahl und Gruppierung in einem Ausdruck. Referenz-Semantik (:=) modifiziert in Place ohne Kopieren – entscheidend für Memory. setkey erstellt einen Index, der Binary-Search-Lookups und schnelle Merges ermöglicht. fread/fwrite sind 5-10x schneller als read.csv/write.csv. Für Daten, die in den Speicher passen, schlägt data.table oft sogar Spark. Der Trade-off ist eine steilere Lernkurve und weniger lesbare Syntax im Vergleich zu dplyr.
# data.table: faster, memory-efficient alternative to data.frame
library(data.table)
dt <- data.table(mtcars)
# syntax: dt[i, j, by]
dt[mpg > 20, .(mean_hp = mean(hp)), by = .(cyl, gear)]
# cyl gear mean_hp
# 1: 4 4 76.0
# 2: 4 5 102.0
# reference semantics (modify in place, no copy)
dt[, mpg_dbl := mpg * 2] # add column in place
dt[1:5, mpg := 0] # modify subset in place
dt[, c("a","b") := .(mpg*2, hp/10)] # multiple columns
# setkey for fast lookups and joins
setkey(dt, cyl)
dt[.(4)] # all rows where cyl == 4 (binary search)
dt[.(4), mean(mpg)] # fast aggregation
# joins
dt1 <- data.table(id = 1:3, x = letters[1:3])
dt2 <- data.table(id = 2:4, y = LETTERS[2:4])
setkey(dt1, id); setkey(dt2, id)
dt1[dt2] # right join
dt2[dt1] # left join
merge(dt1, dt2, by = "id") # inner join
# fread/fwrite: fast I/O
big <- fread("huge.csv") # ~10x faster than read.csv
fwrite(big, "out.csv")Paralleles Rechnen
R ist standardmäßig Single-Threaded, aber Parallelismus ist einfach. Das parallel-Paket (eingebaut) bietet mclapply (Fork, nur Linux/Mac) und parLapply (Cluster, alle Plattformen). foreach + doParallel ist eine beliebte Alternative. Das future-Ökosystem (mit furrr) ist modern und vereinheitlicht – Backends durch Ändern von plan() wechseln. Für caret allowParallel = TRUE setzen und ein Backend registrieren. Benötigte Variablen immer exportieren und Pakete auf Workern laden. Parallelismus hat Overhead – hilft nur, wenn jede Aufgabe substanziell ist (>100ms). Benchmarden, um Speedup zu verifizieren; Amdahls Gesetz limitiert Gewinne.
# parallel package (built-in)
library(parallel)
ncores <- detectCores() - 1
# parallel lapply
cl <- makeCluster(ncores)
results <- parLapply(cl, 1:100, function(i) slow_function(i))
stopCluster(cl)
# foreach with doParallel
library(foreach); library(doParallel)
registerDoParallel(ncores)
results <- foreach(i = 1:100, .combine = "c") %dopar% {
slow_function(i)
}
stopImplicitCluster()
# future ecosystem (modern, flexible)
library(future); library(furrr)
plan(multisession, workers = ncores) # or multicore (Linux/Mac)
results <- future_map(1:100, slow_function)
# parallel caret
library(caret)
ctrl <- trainControl(method = "cv", number = 10, allowParallel = TRUE)
fit <- train(y ~ ., data = train, method = "rf", trControl = ctrl)
# benchmark
library(microbenchmark)
microbenchmark(
serial = lapply(1:100, slow_function),
parallel = parLapply(makeCluster(4), 1:100, slow_function),
times = 5
)
# export variables to workers
clusterExport(cl, c("my_data", "my_function"))
clusterEvalQ(cl, library(dplyr))Profiling & Optimierungs-Workflow
Vor dem Optimieren profilen – Intuition über Bottlenecks ist meistens falsch. profvis bietet einen interaktiven Flame-Graphen, der Zeit pro Zeile und Call zeigt. Rprof ist das base-R-Äquivalent. Rprofmem verfolgt Allokationen. object.size misst Memory; gc() erzwingt Garbage Collection und berichtet Nutzung. Die Optimierungs-Hierarchie: (1) vektorisieren, (2) vorinitialisieren, (3) zu data.table wechseln, (4) Rcpp für irreduzible Schleifen, (5) parallelisieren. memoise cacht Funktionsergebnisse – großartig für teure reine Funktionen, die wiederholt mit denselben Argumenten aufgerufen werden. Immer vorher und nachher messen, um Verbesserungen zu bestätigen.
# profile to find bottlenecks
library(profvis)
profvis({
result <- my_analysis(big_data)
})
# opens interactive flame graph
# Rprof (base R)
Rprof("profile.out")
my_analysis(big_data)
Rprof(NULL)
summaryRprof("profile.out")
# memory profiling
Rprofmem("mem.out")
my_analysis(big_data)
Rprofmem(NULL)
# object sizes
object.size(my_data) # bytes
format(object.size(my_data), "MB")
# find memory hogs
sort(sapply(ls(), function(x) object.size(get(x))))
# garbage collection
gc() # force collection, show memory
gcinfo(TRUE) # report on auto GC
# common optimizations:
# 1. Vectorize (avoid loops)
# 2. Preallocate
# 3. Use data.table instead of data.frame
# 4. Use Rcpp for hot loops
# 5. Avoid growing objects (c(), rbind())
# 6. Use appropriate data types (integer vs double)
# 7. Cache expensive computations (memoise)
library(memoise)
slow_cached <- memoise(slow_function)dplyr Deep Dive
Kern-Verben
dplyrs fünf Kern-Verben: filter (Zeilen nach Bedingung), select (Spalten), mutate (neue Spalten), arrange (sortieren), summarize (aggregieren). Mit %>% verketten. group_by + summarize ist das Arbeitspferd für Aggregation. na.rm = TRUE ist unerlässlich – sonst macht jedes NA in den Daten die Summary NA.
library(dplyr)
starwars %>%
filter(species == "Human", height > 170) %>%
select(name, height, mass, homeworld) %>%
mutate(bmi = mass / (height/100)^2) %>%
arrange(desc(height)) %>%
slice_head(n = 5)
# group_by + summarize
starwars %>%
group_by(species) %>%
summarize(
n = n(),
avg_height = mean(height, na.rm = TRUE),
avg_mass = mean(mass, na.rm = TRUE)
) %>%
arrange(desc(n)) %>%
filter(n >= 2)Joins
Mutating Joins kombinieren Spalten; Filtering Joins subsetten Zeilen. left_join ist der häufigste – behält alle Zeilen von x, füllt NA für Non-Matches. Immer auf duplizierte Schlüssel in der rechten Tabelle prüfen (verursacht Zeilenvervielfachung). semi_join/anti_join sind großartig zum Filtern basierend auf einer anderen Tabelle, ohne deren Spalten einzubringen.
library(dplyr)
# mutating joins (add columns from y to x)
left_join(x, y, by = "id") # all rows in x
right_join(x, y, by = "id") # all rows in y
inner_join(x, y, by = "id") # only matching rows
full_join(x, y, by = "id") # all rows from both
# different column names
left_join(x, y, by = c("id" = "user_id"))
# multiple keys
left_join(x, y, by = c("first", "last"))
# filtering joins (filter x, no columns added)
semi_join(x, y, by = "id") # rows in x that match y
anti_join(x, y, by = "id") # rows in x that DON'T match y
# check for duplicates
x %>% count(id) %>% filter(n > 1)Window-Funktionen
Window-Funktionen berechnen Werte über Zeilen, die sich auf die aktuelle Zeile beziehen. lag/lead greifen auf vorherige/nächste Zeilen zu – unerlässlich für Zeitreihen. cumsum/cummean sind kumulative Aggregate. slice_max/slice_min sind Shortcuts für Top-n pro Gruppe. Immer zuerst group_by, um innerhalb von Gruppen zu berechnen.
library(dplyr)
# row numbering and ranking
df %>% group_by(group) %>%
mutate(
row_num = row_number(),
rank = rank(value),
dense_rank = dense_rank(value),
min_rank = min_rank(value)
)
# offsets
df %>% group_by(group) %>%
mutate(
prev = lag(value),
next = lead(value, 2),
diff = value - lag(value)
)
# cumulative
df %>% group_by(group) %>%
mutate(
cum_sum = cumsum(value),
cum_mean = cummean(value),
cum_max = cummax(value),
cum_min = cummin(value)
)
# top n per group
df %>% group_by(group) %>%
slice_max(value, n = 3) # top 3
df %>% group_by(group) %>%
slice_min(value, n = 2)across und mehrere Spalten
across (dplyr 1.0+) ersetzt die alten _at, _if, _all-Suffixe. where(is.numeric) verwenden, um Spalten nach Prädikat zu wählen. Das .x-Pronomen referenziert die aktuelle Spalte innerhalb von Lambdas (~). rename_with benennt Spalten mit einer Funktion um. across ist der moderne, konsistente Weg, um auf mehreren Spalten zu operieren.
library(dplyr)
# apply function to multiple columns
starwars %>%
mutate(across(where(is.numeric), ~ replace_na(.x, 0)))
# summarize multiple columns
starwars %>%
summarize(across(where(is.numeric), mean, na.rm = TRUE))
# rename multiple columns
starwars %>%
rename_with(tolower, everything())
# transform specific columns
df %>%
mutate(across(c(height, mass), ~ .x * 0.453592)) # lb to kg
# multiple functions
df %>%
summarize(across(value, list(mean = mean, sd = sd, n = ~ n()), na.rm = TRUE))
# conditional transformation
df %>%
mutate(across(where(is.character), tolower))Summarize-Muster
summarize reduziert Gruppen auf einzelne Werte. n() zählt Zeilen; n_distinct() zählt eindeutige Werte. Statistik-Funktionen immer na.rm = TRUE übergeben, sonst propagieren NAs. across + list ermöglicht das Berechnen mehrerer Statistiken auf einmal. count() ist Shortcut für group_by + summarize(n = n()) + ungroup.
library(dplyr)
# basic
df %>%
group_by(category) %>%
summarize(
count = n(),
distinct = n_distinct(id),
total = sum(value, na.rm = TRUE),
avg = mean(value, na.rm = TRUE),
median = median(value, na.rm = TRUE),
sd = sd(value, na.rm = TRUE),
min = min(value, na.rm = TRUE),
max = max(value, na.rm = TRUE),
first = first(value),
last = last(value),
q25 = quantile(value, 0.25, na.rm = TRUE)
)
# multiple summary stats at once
df %>%
group_by(category) %>%
summarize(
across(value, list(mean = mean, sd = sd, n = ~ n()), na.rm = TRUE)
)
# count and proportions
df %>%
count(category) %>%
mutate(pct = n / sum(n))ggplot2 Fortgeschritten
Layer und Ästhetik
ggplot baut Plots in Layern auf, verbunden durch +. aes() mappt Datenspalten auf visuelle Properties. geom_* definiert die Geometrie; scale_* steuert Achsen/Farben; labs beschriftet alles; theme_* styled Non-Data-Elemente. facet_wrap splittet nach einer Variable; facet_grid macht ein 2D-Raster von zwei Variablen.
library(ggplot2)
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
geom_smooth(method = "lm", se = TRUE) +
scale_color_brewer(palette = "Set2") +
labs(
title = "Fuel efficiency by engine size",
subtitle = "Source: EPA mpg dataset",
x = "Engine displacement (L)",
y = "Highway MPG",
color = "Vehicle class"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")
# facets
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, ncol = 4)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl)Scales und Koordinaten
scale_*-Funktionen steuern, wie Daten auf visuelle Properties mappen. scale_x_log10 log-transformiert; scale_color_viridis_c gibt wahrnehmungs-gleichmäßige Colormaps. coord_cartesian zoomt, ohne Daten zu droppen (anders als xlim). coord_flip tauscht Achsen. coord_polar macht Balken zu Kuchen-Slices. scale_x_date formatiert Datum-Achsen.
ggplot(mpg, aes(displ, hwy, color = cty)) +
geom_point() +
scale_x_log10() +
scale_y_continuous(limits = c(10, 50), breaks = seq(10, 50, 5)) +
scale_color_viridis_c(option = "plasma") +
coord_cartesian(xlim = c(1, 7)) # zoom without removing data
# coord flip
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# coord polar (pie chart from bar)
ggplot(mtcars, aes(factor(1), fill = factor(cyl))) +
geom_bar(width = 1) +
coord_polar(theta = "y")
# date axis
ggplot(economics, aes(date, unemploy)) +
geom_line() +
scale_x_date(date_labels = "%Y", date_breaks = "5 years")Themes und Anpassung
theme() steuert jedes Non-Data-Element. element_text/rect/line/blank sind die Bausteine. Häufige Tweaks: x-Achsen-Labels rotieren (angle, hjust), fette Titel, minor grid verbergen (panel.grid.minor = element_blank()). ggsave exportiert zu PNG/PDF/SVG – Dimensionen in Zoll und dpi für Raster-Formate angeben.
library(ggplot2)
p <- ggplot(mpg, aes(class, hwy)) + geom_boxplot()
# built-in themes
p + theme_minimal()
p + theme_classic()
p + theme_bw()
# custom theme
p + theme(
panel.background = element_rect(fill = "white"),
panel.grid.major = element_line(color = "gray90"),
panel.grid.minor = element_blank(),
axis.text.x = element_text(angle = 45, hjust = 1, size = 10),
axis.title = element_text(face = "bold"),
plot.title = element_text(size = 16, hjust = 0.5),
plot.subtitle = element_text(color = "gray40"),
legend.position = "right",
legend.key = element_blank(),
strip.background = element_rect(fill = "lightblue"),
strip.text = element_text(face = "bold")
)
# save
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = cairo_pdf)Statistiken und Glättung
stat_summary berechnet benutzerdefinierte Summaries (mean, median, mean_se, mean_cl_normal). geom_smooth fügt Trendlinien hinzu – method='lm' für linear, 'loess' für lokale Regression, 'gam' für verallgemeinerte additive. geom_density/geom_density_2d zeigen Verteilungen. geom_violin zeigt die volle Verteilungsform neben Boxplots.
library(ggplot2)
# stat_summary for custom summaries
ggplot(mtcars, aes(factor(cyl), mpg)) +
stat_summary(fun = "mean", geom = "point", size = 3) +
stat_summary(fun.data = "mean_se", geom = "errorbar")
# smooth
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x, se = TRUE) +
geom_smooth(method = "loess", se = FALSE, color = "red")
# density
ggplot(iris, aes(Sepal.Length, fill = Species)) +
geom_density(alpha = 0.5)
# 2d density
ggplot(diamonds, aes(carat, price)) +
geom_density_2d() +
scale_x_log10() + scale_y_log10()
# histograms with binning
ggplot(diamonds, aes(price)) +
geom_histogram(bins = 50, fill = "steelblue") +
scale_x_log10()
# boxplot and violin
ggplot(iris, aes(Species, Sepal.Length)) +
geom_boxplot() +
geom_violin(alpha = 0.3, fill = "orange")Erweiterungen und patchwork
patchwork kombiniert mehrere Plots mit + (Seite an Seite), / (gestapelt) und plot_layout für feine Kontrolle. plot_annotation fügt Gesamt-Titel und Tags (A, B, C...) hinzu. Das ggplot2-Erweiterungs-Ökosystem ist riesig: ggrepel für Labels, ggridges für Ridge-Plots, gganimate für Animationen, ggiraph für Interaktivität, geom_sf für Karten.
library(ggplot2)
library(patchwork)
p1 <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
p2 <- ggplot(mtcars, aes(factor(cyl))) + geom_bar()
p3 <- ggplot(mtcars, aes(mpg)) + geom_histogram(bins = 10)
p4 <- ggplot(mtcars, aes(factor(cyl), mpg)) + geom_boxplot()
# combine plots
p1 + p2 # side by side
p1 / p2 # stacked
(p1 + p2) / p3 # grouped
p1 + p2 + p3 + plot_layout(nrow = 2, byrow = FALSE)
# annotations
p1 + p2 + plot_annotation(
title = "MT cars analysis",
tag_levels = "A"
)
# other extensions:
# ggrepel: non-overlapping labels
# ggridges: joy plots
# gganimate: animated plots
# ggiraph: interactive
# ggplot2::geom_sf: maps
library(ggrepel)
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_point() +
geom_text_repel()tidyr Data Wrangling
Pivot longer und wider
pivot_longer/pivot_wider (ersetzen gather/spread) reshaping Daten. Long-Format ist am besten für ggplot und dplyr-Aggregation; Wide ist besser für menschliches Lesen. names_pattern mit .value ermöglicht das Splitten in mehrere Spalten basierend auf der Spaltennamen-Struktur. Immer cols, names_to und values_to explizit angeben.
library(tidyr)
# wide to long
# id q1 q2 q3
# A 10 20 30
wide_data %>%
pivot_longer(
cols = starts_with("q"),
names_to = "quarter",
values_to = "score"
)
# id quarter score
# A q1 10
# A q2 20
# A q3 30
# long to wide
long_data %>%
pivot_wider(
names_from = quarter,
values_from = score
)
# multiple value columns
df %>%
pivot_longer(
cols = c(starts_with("q"), starts_with("r")),
names_to = c(".value", "quarter"),
names_pattern = "([a-z])([0-9])"
)Separate und unite
separate teilt eine Spalte in viele; unite kombiniert viele in eine. separate_rows splittet in mehrere Zeilen (nützlich für Tag-Listen). extract verwendet Regex-Capture-Gruppen. Alle nehmen ein sep-Argument (Standard ist nicht-alphanumerisch). Typen automatisch mit convert = TRUE in separate konvertieren.
library(tidyr)
# split a column
df <- tibble(x = c("a_1", "b_2", "c_3"))
df %>% separate(x, c("letter", "number"), sep = "_")
# letter number
# a 1
# b 2
# split into rows
df %>% separate_rows(x, sep = "_")
# x
# a
# 1
# b
# 2
# unite columns
df %>%
separate(x, c("letter", "number")) %>%
unite("combined", letter, number, sep = "-")
# combined
# a-1
# b-2
# extract with regex
df %>% extract(x, c("letter", "number"), "([a-z])_([0-9])")Fehlende Werte
replace_na füllt NAs mit Konstanten; fill propagiert Werte (großartig für Zeitreihen); drop_na entfernt unvollständige Zeilen; coalesce wählt den ersten Non-NA über Spalten. complete expandiert zu allen Kombinationen spezifizierter Spalten (wie ein kartesisches Produkt) – nützlich, um sicherzustellen, dass fehlende Gruppen in Summaries erscheinen.
library(tidyr)
# replace NA
df %>% replace_na(list(value = 0, name = "unknown"))
# fill NA with previous/next value
df %>% fill(value, .direction = "down") # forward fill
df %>% fill(value, .direction = "up") # backward fill
df %>% fill(value, .direction = "downup") # down then up
# drop rows with NA
df %>% drop_na()
df %>% drop_na(value) # only specific columns
# detect NA
df %>% filter(!is.na(value))
sum(is.na(df$value))
df %>% mutate_all(~ sum(is.na(.))) # NA count per column
# coalesce: first non-NA
df %>% mutate(value = coalesce(value, fallback, 0))
# complete: expand combinations
df %>% complete(group, year) # all group-year combos, NA filled
df %>% complete(group, year, fill = list(value = 0))Nesting und List-Spalten
nest packt gruppierte Zeilen in List-Spalten – die Grundlage von Split-Apply-Combine mit purrr. unnest kehrt es um. unnest_wider spreadet List-Elemente in Spalten; unnest_longer spreadet sie in Zeilen. List-Spalten ermöglichen das Halten beliebiger Objekte (Modelle, Data Frames, Vektoren) innerhalb eines Tibbles.
library(tidyr)
library(dplyr)
# nest: pack rows into list columns
nested <- mtcars %>%
nest(data = -cyl)
# cyl data
# 4 <tibble 11x10>
# 6 <tibble 7x10>
# 8 <tibble 14x10>
# operate on each group
nested %>%
mutate(model = map(data, ~ lm(mpg ~ wt, data = .x)))
# unnest
df %>%
unnest(data)
# unnest specific column
df %>%
unnest_wider(data) # list-col of named lists -> cols
df %>%
unnest_longer(data) # list-col of vectors -> rows
# chop (similar to nest)
df %>% chop(value)
# pack (opposite of unnest)
df %>% pack(x = c(a, b))Tibbles und Daten lesen
Tibbles sind verbesserte Data Frames: keine String-zu-Factor-Konvertierung, kein Row-Name-Munging, bessere Ausgabe. read_csv ist viel schneller als read.csv und gibt ein Tibble zurück. col_types spezifizieren, um Überraschungen zu vermeiden (z.B. IDs als numerisch gelesen). na-Argument ermöglicht das Behandeln mehrerer Strings als NA. Für tabellarische Daten immer readr über base verwenden.
library(tibble)
library(readr)
# create tibble
tibble(
x = 1:5,
y = c("a", "b", "c", "d", "e"),
z = runif(5)
)
# tribble: row-wise construction
tribble(
~name, ~age, ~score,
"Alice", 30, 90,
"Bob", 25, 80
)
# read csv
df <- read_csv("data.csv", col_names = TRUE, col_types = "cdd")
df <- read_csv("data.csv", na = c("", "NA", "N/A"))
df <- read_tsv("data.tsv")
df <- read_delim("data.txt", delim = "|")
# write
write_csv(df, "out.csv")
write_tsv(df, "out.tsv")
# read from clipboard (Excel)
df <- read_clipboard()
# column types
# c = character, d = double, i = integer, l = logical, f = factor, D = dateStatistische Tests
t-Tests
t-Tests vergleichen Mittelwerte. One-Sample (vs. einem Wert), Two-Sample (zwischen Gruppen), gepaart (within subjects). Standard ist Welch's (ungleiche Varianzen) – meistens was man will. Immer Annahmen prüfen: Normalität (Shapiro) und gleiche Varianz (var.test). Effektgröße berichten (Cohen's d), nicht nur p-Werte.
# one-sample t-test
t.test(x, mu = 5)
# H0: mean of x equals 5
# two-sample t-test
t.test(x, y)
t.test(x, y, var.equal = TRUE) # Student's (assume equal var)
t.test(x, y, alternative = "greater")
# paired t-test
t.test(before, after, paired = TRUE)
# output interpretation
result <- t.test(x, y)
result$p.value # < 0.05 -> reject H0
result$conf.int # 95% CI of difference
result$statistic # t value
# check assumptions
shapiro.test(x) # normality
var.test(x, y) # equal variances
# effect size (effsize package)
library(effsize)
cohen.d(x, y)ANOVA
ANOVA testet auf Unterschiede über 3+ Gruppen. * für faktorielle Designs mit Interaktionen verwenden. Immer Post-hoc-Tests (TukeyHSD) nach einer signifikanten ANOVA durchführen, um zu finden, welche Paare sich unterscheiden. Homogenität der Varianz prüfen (Levene). Für nicht-normale Daten Kruskal-Wallis verwenden. Für repeated measures lmer aus lme4 verwenden.
# one-way ANOVA
result <- aov(yield ~ fertilizer, data = df)
summary(result)
# two-way ANOVA with interaction
result <- aov(yield ~ fertilizer + density + fertilizer:density, data = df)
result <- aov(yield ~ fertilizer * density, data = df) # shorthand
# Tukey post-hoc
TukeyHSD(result)
# check assumptions
plot(result) # diagnostic plots
library(car)
leveneTest(yield ~ fertilizer, data = df) # equal variances
# Kruskal-Wallis (non-parametric alternative)
kruskal.test(yield ~ fertilizer, data = df)
# repeated measures
result <- aov(score ~ time + Error(subject/time), data = df)
# mixed effects (lme4)
library(lme4)
model <- lmer(score ~ time + (1 | subject), data = df)
anova(model)Chi-Quadrat und kategorial
Chi-Quadrat testet, ob zwei kategoriale Variablen unabhängig sind. Erwartete Häufigkeiten sollten >= 5 in jeder Zelle sein; wenn nicht, Fisher's exakter Test verwenden. McNemar ist für gepaarte binäre Daten (vor/nach). Pearson-Residuen zeigen, welche Zellen am meisten von erwartet abweichen. assocstats gibt Cramer's V für Effektgröße.
# chi-square test of independence
tbl <- table(df$gender, df$vote)
chisq.test(tbl)
# goodness of fit
chisq.test(table(df$color), p = c(0.25, 0.25, 0.25, 0.25))
# Fisher's exact (small samples)
fisher.test(tbl)
# McNemar (paired binary)
mcnemar.test(tbl)
# expected frequencies
ct <- chisq.test(tbl)
ct$expected
ct$observed
ct$residuals # Pearson residuals
# association measures
library(vcd)
assocstats(tbl)
# visualize
library(ggplot2)
ggplot(df, aes(gender, fill = vote)) +
geom_bar(position = "fill")Korrelation
Pearson misst lineare Korrelation; Spearman/Kendall messen monotone (rang-basierte) – robust gegenüber Ausreißern und nicht-linear. cor.test gibt einen p-Wert und CI. corrplot visualisiert Matrizen; ggpairs zeigt Scatterplots und Korrelationen. use='pairwise.complete.obs' verwenden, um fehlende Daten zu behandeln, ohne ganze Zeilen zu droppen.
# Pearson (linear, normal)
cor(x, y, method = "pearson")
cor.test(x, y, method = "pearson")
# Spearman (rank, non-parametric)
cor(x, y, method = "spearman")
# Kendall (rank, smaller samples)
cor(x, y, method = "kendall")
# correlation matrix
cor(mtcars[, c("mpg", "wt", "hp", "disp")])
# with p-values
library(psych)
corr.test(mtcars[, 1:6])
# visualize
library(corrplot)
M <- cor(mtcars)
corrplot(M, method = "circle", type = "lower", order = "hclust")
# GGally for scatterplot matrix
library(GGally)
ggpairs(mtcars[, c("mpg", "wt", "hp", "cyl")])
# handle missing
cor(df, use = "complete.obs") # listwise deletion
cor(df, use = "pairwise.complete.obs")Nicht-parametrische Tests
Nicht-parametrische Tests nehmen keine Normalität an – verwenden, wenn Stichproben klein, Daten schief oder ordinale Daten vorliegen. Mann-Whitney/Wilcoxon sind die rang-basierten Gegenstücke zu t-Tests. Permutationstests und Bootstrap sind rechenintensiv, treffen aber minimale Annahmen. Immer Effektgrößen neben p-Werten berichten.
# Mann-Whitney U (alternative to two-sample t)
wilcox.test(x, y)
wilcox.test(x, y, paired = FALSE)
# Wilcoxon signed-rank (alternative to paired t)
wilcox.test(before, after, paired = TRUE)
# Kruskal-Wallis (alternative to one-way ANOVA)
kruskal.test(y ~ group, data = df)
# Friedman (alternative to repeated measures ANOVA)
friedman.test(y ~ group | subject, data = df)
# permutation test
library(coin)
oneway_test(y ~ group, data = df, distribution = approximate(nresample = 9999))
# bootstrap CI
library(boot)
boot_mean <- function(data, idx) mean(data[idx])
b <- boot(x, boot_mean, R = 10000)
boot.ci(b, type = "perc")
# sign test
library(BSDA)
SIGN.test(x, md = 5) # median different from 5?Regressionsanalyse
Lineare Regression
lm passt lineare Modelle. summary zeigt Koeffizienten, std-Errors, t-Werte, p-Werte, R² und F-Statistik. Immer Diagnostik prüfen: Residual-Plots für Linearität/Homoskedastizität, VIF für Multikollinearität (>5 ist bedenklich). I() schützt Arithmetik in Formeln; poly(x, 2) gibt orthogonale Polynome.
# simple linear
model <- lm(mpg ~ wt, data = mtcars)
summary(model)
coef(model)
confint(model)
fitted(model)
residuals(model)
predict(model, newdata = new_df, interval = "confidence")
# multiple regression
model <- lm(mpg ~ wt + hp + cyl, data = mtcars)
# interactions
model <- lm(mpg ~ wt * hp, data = mtcars)
model <- lm(mpg ~ wt + hp + wt:hp, data = mtcars)
# transformations
model <- lm(log(mpg) ~ wt, data = mtcars)
model <- lm(mpg ~ poly(wt, 2), data = mtcars) # quadratic
model <- lm(mpg ~ I(wt^2) + wt, data = mtcars)
# categorical predictors
model <- lm(mpg ~ factor(cyl), data = mtcars)
# diagnostics
plot(model) # 4 diagnostic plots
library(car)
vif(model) # variance inflation factorVerallgemeinerte lineare Modelle
glm erweitert lm auf nicht-normale Responses. family=binomial für logistisch (binäre Outcomes); family=poisson für Zählungen. Für logistisch gibt exp(coef) Odds Ratios. Verschachtelte Modelle mit anova(..., test='Chisq') vergleichen. Für überdispersierte Zählungen (Varianz > Mittelwert) glm.nb (Negative Binomial) statt Poisson verwenden.
# logistic regression (binary)
model <- glm(am ~ wt + hp, data = mtcars, family = binomial)
summary(model)
# predicted probabilities
predict(model, type = "response")
# odds ratios
exp(coef(model))
# Poisson regression (counts)
model <- glm(count ~ group, data = df, family = poisson)
# negative binomial (overdispersed counts)
library(MASS)
model <- glm.nb(count ~ group, data = df)
# compare models
model1 <- glm(y ~ x1, family = binomial, data = df)
model2 <- glm(y ~ x1 + x2, family = binomial, data = df)
anova(model1, model2, test = "Chisq")
# goodness of fit
1 - pchisq(model$deviance, model$df.residual)
# McFadden pseudo R²
1 - model$deviance / model$null.devianceModellauswahl
Stepwise-Auswahl ist einfach, aber verzerrt – All-Subsets (regsubsets) oder Regularisierung (glmnet) bevorzugen. AIC/BIC balancieren Fit und Komplexität (niedriger ist besser). Kreuzvalidierung gibt ehrliche Out-of-Sample-Schätzungen. glmnet mit alpha=0 ist Ridge, alpha=1 ist Lasso (welches Koeffizienten nullen kann – Feature-Auswahl).
# stepwise selection
full <- lm(mpg ~ ., data = mtcars)
step(full, direction = "both")
step(full, direction = "backward")
step(full, direction = "forward", scope = list(lower = ~1, upper = full))
# AIC and BIC
AIC(model1, model2)
BIC(model1, model2)
# all subsets
library(leaps)
leaps <- regsubsets(mpg ~ ., data = mtcars, nvmax = 10)
plot(leaps, scale = "adjr2")
plot(leaps, scale = "Cp")
# cross-validation
library(caret)
train(mpg ~ ., data = mtcars, method = "lm",
trControl = trainControl(method = "cv", number = 10))
# regularized (glmnet)
library(glmnet)
X <- model.matrix(mpg ~ ., mtcars)[, -1]
y <- mtcars$mpg
cv_model <- cv.glmnet(X, y, alpha = 0) # ridge
cv_model <- cv.glmnet(X, y, alpha = 1) # lassoMixed-Effects-Modelle
Mixed-Effects-Modelle behandeln korrelierte Daten (repeated measures, geclusterte Stichproben). (1 | subject) ist ein Random Intercept pro Subjekt; (time | subject) fügt eine Random Slope hinzu. Verschachtelte Gruppen verwenden /. Crossed Groups verwenden +. lmer für kontinuierlich, glmer für nicht-kontinuierlich. lmerTest für p-Werte verwenden (lme4 berechnet sie standardmäßig nicht).
library(lme4)
library(lmerTest)
# random intercept
model <- lmer(score ~ treatment + (1 | subject), data = df)
# random intercept and slope
model <- lmer(score ~ time + (time | subject), data = df)
# nested random effects
model <- lmer(score ~ 1 + (1 | school/class), data = df)
# crossed random effects
model <- lmer(score ~ 1 + (1 | subject) + (1 | item), data = df)
# generalized mixed model
glmer(outcome ~ treatment + (1 | subject), data = df, family = binomial)
# summary and CIs
summary(model)
confint(model, method = "Wald")
confint(model, method = "boot")
# anova
anova(model)
# random effects
ranef(model)
VarCorr(model)
# predict
predict(model, newdata = df, allow.new.levels = TRUE)Diagnostik und Vorhersage
Immer Diagnostik prüfen: Residual-Plots offenbaren Nicht-Linearität und Heteroskedastizität; Cook's Distance (>4/n) flaggt einflussreiche Punkte; hatvalues (>2p/n) flaggt High-Leverage-Punkte. predict mit interval='confidence' für den Mittelwert, 'prediction' für individuelle Werte (breiter). ggeffects berechnet marginale Effekte für Visualisierung.
model <- lm(mpg ~ wt + hp, data = mtcars)
# diagnostic plots
par(mfrow = c(2, 2))
plot(model)
par(mfrow = c(1, 1))
# residuals vs leverage
plot(model, which = 5)
# influence measures
influence.measures(model)
hatvalues(model) # leverage
cooks.distance(model) # Cook's distance
dfbeta(model) # change in coef per obs
# identify outliers
which(cooks.distance(model) > 4 / nrow(mtcars))
# predictions
new_data <- data.frame(wt = c(2, 3, 4), hp = c(100, 150, 200))
predict(model, newdata = new_data, interval = "confidence")
predict(model, newdata = new_data, interval = "prediction")
# marginal effects
library(ggeffects)
ggeffect(model, terms = "wt")
plot(ggeffect(model, terms = c("wt", "hp")))
# R squared variants
library(rsq)
rsq(model, type = "v") # variance-based
rsq(model, type = "kl") # Kullback-LeiblerR Markdown & Berichte
R Markdown Grundlagen
R Markdown kombiniert Narrativ (Markdown), Code (R-Chunks) und Output (Tabellen/Plots). YAML-Header setzt Metadaten und Output-Format. Chunk-Optionen: echo=FALSE verbirgt Code, include=FALSE führt aus, verbirgt aber alles, fig.cap fügt Captions hinzu. Inline r-Code mit Backticks fügt Werte in Prosa ein. Knit (Ctrl+Shift+K) rendert.
---
title: "Quarterly Report"
author: "Data Team"
date: "2024-12-31"
output: html_document
---
## Section
Inline code: the mean is `r mean(x)`.
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(dplyr)
library(ggplot2)
data <- read_csv("data.csv")
```
```{r plot, fig.cap="Sales over time"}
ggplot(data, aes(date, sales)) + geom_line()
```
```{r table}
library(knitr)
kable(head(data), caption = "First 6 rows")
```Chunk-Optionen
Chunk-Optionen steuern Code-Ausführung und Output. cache=TRUE beschleunigt Re-Knits, kann aber Änderungen verbergen – dependson für Cache-Invalidierung setzen. R Markdown unterstützt viele Sprachen via knitr-Engines: python, bash, sql, javascript usw. Für Python das reticulate-Paket verwenden, um Objekte zwischen R und Python zu teilen.
```{r chunk-name, options}
# code here
```
# Common options:
# echo=FALSE hide code, show output
# eval=FALSE show code, don't run
# include=FALSE run, hide code and output
# results="hide" show code, hide text output
# warning=FALSE hide warnings
# message=FALSE hide messages
# fig.width=6 figure width (inches)
# fig.height=4 figure height
# fig.cap="..." figure caption
# fig.path="figs/" where to save figures
# cache=TRUE cache results (faster re-knits)
# dependson="..." cache dependencies
# dev="svg" output device
# global options
knitr::opts_chunk$set(
echo = TRUE,
fig.width = 6,
fig.height = 4,
fig.path = "figures/",
cache = TRUE
)
# language engines
```{python}
# Python code
```
```{bash}
# Shell commands
```
```{sql, connection=db}
# SQL queries
```Output-Formate
html_document ist am flexibelsten (interaktiv, code_folding, paged tables). pdf_document erfordert LaTeX (TinyTeX via tinytex::install_tinytex() installieren). word_document generiert Word-Dateien mit einem referenz-docx für Styling. Für Präsentationen ioslides (eingebaut) oder revealjs (polierter) verwenden. Mehrere Outputs können zusammen spezifiziert werden.
---
output:
html_document:
toc: true
toc_float: true
toc_depth: 3
number_sections: true
theme: flatly
highlight: tango
code_folding: hide
df_print: paged
---
---
output:
pdf_document:
toc: true
number_sections: true
fig_caption: true
latex_engine: xelatex
---
---
output:
word_document:
toc: true
reference_docx: template.docx
---
---
output:
ioslides_presentation:
widescreen: true
---
---
output:
revealjs::revealjs_presentation:
theme: solarized
transition: slide
---
# Multiple formats
---
output:
html_document: default
pdf_document: default
---Tabellen mit kable und gt
kable + kableExtra produziert publikationsqualitative Tabellen mit Gruppierung, bedingter Formatierung und Styling. gt ist eine neuere, mehr Grammar-of-Graphics-Stil-Alternative. DT erstellt interaktive HTML-Tabellen mit Sortierung, Filterung und Paginierung – großartig für HTML-Berichte. Wahl basierend auf Output-Format (kable funktioniert überall; DT nur in HTML).
library(knitr)
library(kableExtra)
library(gt)
# basic kable
kable(head(mtcars))
# styled kable
kable(head(mtcars), format = "html", caption = "Top cars") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE) %>%
add_header_above(c(" " = 1, "Specs" = 3, "Performance" = 7))
# conditional formatting
kable(df) %>%
cell_spec(value, color = ifelse(value > 0, "green", "red")) %>%
row_spec(1, bold = TRUE, background = "yellow")
# gt (modern alternative)
mtcars %>%
head() %>%
gt() %>%
tab_header(title = "MT Cars", subtitle = "First 6 rows") %>%
cols_label(mpg = "MPG", cyl = "Cylinders") %>%
tab_options(table.width = pct(80))
# DT for interactive tables
library(DT)
datatable(mtcars, filter = "top", options = list(pageLength = 5))Parameter und Automatisierung
params machen Berichte wiederverwendbar – in YAML definieren, via params$<name> zugreifen. Mit benutzerdefinierten params via rmarkdown::render() rendern. Über Parameter-Werte loopen, um mehrere Berichte zu generieren (einer pro Region, pro Quartal usw.). Dies ist die Grundlage automatisierter Reporting-Pipelines. Mit cron/scheduled R für periodische Berichte kombinieren.
---
title: "Regional Sales Report"
output: html_document
params:
region: "West"
year: 2024
data_file: "sales.csv"
---
## Report for `r params$region` in `r params$year`
```{r}
data <- read_csv(params$data_file) %>%
filter(region == params$region, year == params$year)
```
# Render from R
rmarkdown::render("report.Rmd",
params = list(region = "East", year = 2024),
output_file = "east_2024.html")
# Render from command line
# Rscript -e 'rmarkdown::render("report.Rmd", params = list(region = "East"))'
# Loop over parameters
for (r in c("West", "East", "North")) {
rmarkdown::render("report.Rmd",
params = list(region = r),
output_file = paste0(r, "_report.html"))
}purrr Funktionale Programmierung
map-Familie
map ist das tidyverse-lapply – gibt immer eine Liste zurück. map_dbl/chr/int/lgl geben typisierte Vektoren zurück (sicherer als map). map2 und pmap iterieren über mehrere Argumente parallel. walk ist für Seiteneffekte (Drucken, Speichern). Das .x-Pronomen referenziert das aktuelle Element; in pmap ..1, ..2 usw. verwenden. map_dfr bindet Data Frames nach Zeile.
library(purrr)
# map: list output
map(1:5, ~ .x^2) # list(1, 4, 9, 16, 25)
map_dbl(1:5, ~ .x^2) # numeric vector
map_chr(1:5, ~ paste0("n", .x)) # character vector
map_int(1:5, ~ .x * 2L) # integer vector
map_lgl(1:5, ~ .x > 3) # logical vector
map_dfr(1:3, ~ data.frame(id = .x, val = .x^2)) # row-bound df
map_dfc(1:3, ~ data.frame(x = .x)) # col-bound df
# multiple arguments
map2(1:3, 4:6, ~ .x + .y)
pmap(list(1:3, 4:6, 7:9), ~ ..1 + ..2 + ..3)
# walk (for side effects)
walk(1:5, ~ print(.x))
walk(files, ~ process(.x))
walk2(plots, filenames, ~ ggsave(.y, .x))
# in pipelines
mtcars %>%
split(.$cyl) %>%
map(~ lm(mpg ~ wt, data = .x)) %>%
map(summary) %>%
map_dbl(~ .x$r.squared)Safely und quietly
safely wickelt eine Funktion ein, um (result, error) zurückzugeben statt zu werfen – unerlässlich für Batch-Verarbeitung, wo ein Fehlschlag nicht alles stoppen sollte. possibly gibt einen Default zurück. quietly erfasst Messages/Warnungen. transpose dreht eine Liste von Paaren in ein Paar von Listen um. insistently retryt mit Backoff – großartig für flaky APIs.
library(purrr)
# safely: capture errors without stopping
safe_log <- safely(log)
safe_log(10) # list(result = 2.3, error = NULL)
safe_log("a") # list(result = NULL, error = <error>)
# process many, keep going on error
results <- map(values, safely(my_function))
successes <- map(results, "result") %>% compact()
errors <- map(results, "error") %>% compact()
# quietly: capture messages/warnings
quiet_log <- quietly(log)
quiet_log(10) # list(result, output, warnings, messages)
# possibly: return default on error
safe_log <- possibly(log, otherwise = NA_real_)
map(values, safe_log) # never errors
# transpose for cleaner output
results <- map(values, safely(fun)) %>% transpose()
results$result # all results
results$error # all errors
# insistently: retry on failure
slow_fn <- insistently(api_call, rate = rate_backoff())Reduce und accumulate
reduce kombiniert Elemente paarweise (wie foldl); accumulate behält Zwischenergebnisse. Nützlich zum Joinen vieler Data Frames oder Aufbauen kumulativer Berechnungen. detect/find erster Treffer; keep/discard filtern. every/some testen Prädikate. Diese ersetzen Schleifen durch prägnante, komponierbare Operationen.
library(purrr)
# reduce: combine pairwise
reduce(c(1, 2, 3, 4), ) # 10
reduce(c(1, 2, 3, 4), ~ .x * .y) # 24
reduce(list(df1, df2, df3), full_join, by = "id")
# accumulate: keep intermediate results
accumulate(c(1, 2, 3, 4), ) # 1 3 6 10
accumulate(c(2, 3, 4), ~ .x * .y) # 2 6 24
# right reduce
reduce(c(1, 2, 3, 4), , .dir = "backward")
# with init
reduce(c(1, 2, 3), ~ c(.x, .y), .init = numeric(0))
# detect
detect(1:10, ~ .x > 5) # 6 (first match)
detect_index(1:10, ~ .x > 5) # 6 (index)
detect(1:10, ~ .x > 5, .right = TRUE) # 10 (last match)
# keep/discard
keep(1:10, ~ .x %% 2 == 0) # 2 4 6 8 10
discard(1:10, ~ .x %% 2 == 0) # 1 3 5 7 9
every(1:10, ~ .x > 0) # TRUE
some(1:10, ~ .x > 5) # TRUEPartielle Anwendung und Komposition
partial pre-füllt Argumente – nützlich zum Erstellen spezialisierter Funktionen aus allgemeinen. compose verkettet Funktionen (rechts nach links). negate invertiert ein Prädikat. lift konvertiert eine Funktion, um eine Liste von Argumenten zu nehmen. imap ist map2 mit dem Index als zweitem Argument. Diese Tools machen funktionale Komposition sauber und lesbar.
library(purrr)
# partial: pre-fill arguments
add_one <- partial(, 1)
add_one(5) # 6
round2 <- partial(round, digits = 2)
round2(3.14159) # 3.14
# compose: chain functions
clean_string <- compose(
str_trim,
str_to_lower,
~ str_replace_all(.x, "[^a-z]", " ")
)
clean_string(" Hello, World! ") # "hello world "
# %>% (magrittr pipe) vs compose
# pipe: data %>% f %>% g %>% h
# compose: g %>% f creates a new function
# negate
is_missing <- negate(is.na)
is_missing(5) # TRUE
# lift (vectorize)
sum2 <- lift() # takes a list of 2 args
sum2(list(1, 2)) # 3
# walk with index
imap(letters[1:3], ~ paste0(.y, ": ", .x))
# list("1: a", "2: b", "3: c")List-Spalten und verschachtelte Daten
Das nest + map + unnest-Muster ist das tidyverse Split-Apply-Combine. List-Spalten halten beliebige Objekte (Modelle, Vorhersagen, Sub-Daten). map auf einem Data Frame iteriert über Spalten. pluck extrahiert sicher verschachtelte Elemente. modify_if ändert Elemente, die einem Prädikat entsprechen, und behält die Originalstruktur.
library(purrr)
library(dplyr)
library(tidyr)
# nest data, fit models, predict
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(
model = map(data, ~ lm(mpg ~ wt, data = .x)),
rsq = map_dbl(model, ~ summary(.x)$r.squared),
pred = map2(model, data, predict)
)
# unnest predictions
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(model = map(data, ~ lm(mpg ~ wt, .x))) %>%
mutate(pred = map2(model, data, predict)) %>%
select(cyl, data, pred) %>%
unnest(c(data, pred))
# apply function to each column
mtcars %>% map_dbl(mean)
mtcars %>% map_dbl(sd)
# apply to columns of specific type
iris %>%
map_if(is.numeric, mean) %>%
map_dbl(round, 2)
# pluck
list(a = list(b = list(c = 42))) %>% pluck("a", "b", "c")
# 42
# modify (returns same type)
modify_if(iris, is.numeric, ~ .x * 2)
modify_depth(nested_list, 2, ~ .x + 1)Shiny Apps
Grundlegende App-Struktur
Eine Shiny-App hat ui (Layout) und server (Logik). Inputs kommen von input$<id>; Outputs gehen zu output$<id> via render*-Funktionen. fluidPage ist das Basis-Layout; sidebarLayout splittet in Sidebar (Controls) und Main (Output). Als app.R im eigenen Ordner speichern; der Ordnername wird der App-Name.
library(shiny)
ui <- fluidPage(
titlePanel("Hello Shiny"),
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points", 1, 100, 50)
),
mainPanel(
plotOutput("scatter")
)
)
)
server <- function(input, output, session) {
output$scatter <- renderPlot({
plot(1:input$n, rnorm(input$n))
})
}
shinyApp(ui, server)
# Save as app.R in a folder, run with:
# shiny::runApp("path/to/folder")
# Or click "Run App" in RStudioInputs und Reaktivität
Shiny hat viele Input-Controls. observeEvent führt Code aus, wenn sich ein Input ändert; eventReactive erstellt einen reaktiven Wert aus einem Event. reactive() cacht sein Ergebnis, bis sich Inputs ändern. reactiveVal/reactiveValues halten mutablen Zustand. actionButton + observeEvent für explizite Trigger verwenden (nicht auf jeden Tastenanschlag reagieren).
library(shiny)
ui <- fluidPage(
numericInput("n", "N", value = 10, min = 1, max = 100),
textInput("label", "Label", value = "Data"),
selectInput("color", "Color", choices = c("red", "blue", "green")),
dateInput("date", "Date"),
dateRangeInput("range", "Range"),
checkboxInput("show", "Show?", value = TRUE),
checkboxGroupInput("vars", "Variables", choices = names(mtcars)),
radioButtons("dist", "Distribution",
choices = c("Normal", "Uniform", "Exponential")),
fileInput("file", "Upload CSV", accept = ".csv"),
actionButton("go", "Go!")
)
server <- function(input, output, session) {
# event-triggered
observeEvent(input$go, {
showNotification(paste("Clicked", input$go))
})
# reactive expression (cached)
data <- reactive({
rnorm(input$n)
})
# reactive value
val <- reactiveVal(0)
observeEvent(input$go, val(val() + 1))
}Outputs und Rendering
Jeder Output-Typ hat eine passende render*-Funktion: renderPlot für Plots, renderTable für Tabellen, renderPrint für Konsolen-Output, renderText für Strings, renderUI für dynamische UI. Outputs sind reaktiv – sie führen neu aus, wenn sich ihre Input-Dependencies ändern. DT::dataTableOutput ist der Standard für interaktive Tabellen.
library(shiny)
library(ggplot2)
library(DT)
ui <- fluidPage(
plotOutput("plot", click = "plot_click"),
tableOutput("table"),
DT::dataTableOutput("dt"),
verbatimTextOutput("summary"),
textOutput("text"),
uiOutput("dynamic")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
ggplot(mtcars, aes(wt, mpg)) + geom_point()
})
output$table <- renderTable({
head(mtcars)
})
output$dt <- DT::renderDataTable({
datatable(mtcars, filter = "top")
})
output$summary <- renderPrint({
summary(mtcars)
})
output$text <- renderText({
paste("You clicked:", input$plot_click$x)
})
output$dynamic <- renderUI({
if (input$n > 5) strong("Big!") else em("Small")
})
}
# observe vs reactive
# observe: side effects (output$ assignments, updates)
# reactive: returns a value, used by other reactivesReaktive Programmierung
reactive() ist das Arbeitspferd – gecacht und lazy (berechnet nur neu, wenn gelesen). observe() ist eager (läuft sofort bei Dependency-Änderung) – für Seiteneffekte. eventReactive wartet auf ein Event. reactiveValues hält mehrere mutable Werte (wie ein kleines reaktives Objekt). isolate liest einen Wert, ohne eine Dependency zu erstellen. debounce drosselt schnelle Input-Änderungen.
library(shiny)
server <- function(input, output, session) {
# reactive: cached, lazy
filtered <- reactive({
mtcars %>% filter(cyl == input$cyl)
})
# observe: eager, side effects
observe({
updateSelectInput(session, "model",
choices = unique(filtered()$model))
})
# eventReactive: triggered by event
result <- eventReactive(input$go, {
run_analysis(input$params)
})
# reactiveVal: single mutable value
counter <- reactiveVal(0)
observeEvent(input$add, counter(counter() + 1))
# reactiveValues: multiple values
rv <- reactiveValues(data = NULL, status = "idle")
observeEvent(input$load, {
rv$data <- read.csv(input$file$datapath)
rv$status <- "loaded"
})
# debounce (throttle rapid changes)
search <- reactive({ input$search }) %>% debounce(500)
# isolate (use value without dependency)
output$plot <- renderPlot({
plot(isolate(rv$data)) # not reactive on rv$data
})
}Deployment
shinyapps.io ist das einfachste Hosting (kostenlose Stufe verfügbar). Für Self-Hosting Shiny Server auf Linux installieren oder Docker verwenden (rocker/shiny-Image). Für Performance: Plots cachen, async (future/promises) für lange Aufgaben verwenden und Datei-Re-Lesen in render-Funktionen vermeiden. reactlog aktivieren (Ctrl+F3), um den reaktiven Graphen zum Debuggen zu visualisieren.
# Deploy to shinyapps.io
# 1. Create account at shinyapps.io
# 2. Install rsconnect
install.packages("rsconnect")
# 3. Authorize (paste token from shinyapps.io)
rsconnect::setAccountInfo(name = "<name>", token = "<token>", secret = "<secret>")
# 4. Deploy
rsconnect::deployApp("path/to/app")
# Run on local network
shiny::runApp("app.R", host = "0.0.0.0", port = 3838)
# Shiny Server (self-hosted on Linux)
# Install shiny-server, put apps in /srv/shiny-server/
# Config: /etc/shiny-server/shiny-server.conf
# Access at http://server:3838/appname
# Docker
# FROM rocker/shiny
# COPY app.R /srv/shiny-server/myapp/
# EXPOSE 3838
# Performance tips:
# - Use renderCachedPlot for expensive plots
# - Use future + promises for async work
# - Move heavy compute to reactive({...})
# - Avoid re-reading files in render functions
# Debugging
options(shiny.fullstacktrace = TRUE)
options(shiny.reactlog = TRUE) # press Ctrl+F3 in appPerformance & Profiling
Profiling mit profvis
profvis ist der moderne Profiler – produziert einen interaktiven Flame-Graphen, der zeigt, wo Zeit verbracht wird. Nach breiten Balken (langsame Operationen) und hohen Stacks (tiefe Call-Ketten) suchen. Mit realistischen Daten-Größen profilen; winzige Inputs verbergen O(n²)-Probleme. summaryRprof ist die base-R-Alternative. Immer vor dem Optimieren profilen – Intuition ist oft falsch.
library(profvis)
# profile a block of code
profvis({
data <- read.csv("large.csv")
result <- lapply(data, function(x) {
x[x > 0]
})
plot(result)
})
# profile a function call
profvis(my_function(arg1, arg2))
# save profile
p <- profvis({ ... })
htmlwidgets::saveWidget(p, "profile.html")
# Rprof (base R)
Rprof("profile.out")
# ... code to profile ...
Rprof(NULL)
summaryRprof("profile.out")
# tips:
# - profile realistic inputs (not too small)
# - run multiple times for stable results
# - look for the widest bars in the flame graph
# - focus on hotspots, not micro-optimizationsVektorisierung
Vektorisierung ist Rs größter Performance-Hebel – Operationen auf ganzen Vektoren sind 10-100x schneller als Schleifen, weil sie in C abtauchen. Preallocation (numeric(n)) ist der zweitgrößte Gewinn – niemals einen Vektor innerhalb einer Schleife wachsen lassen. vapply ist sicherer und schneller als sapply (typisierter Output). rowMeans/colSums sind hoch optimiert – über apply verwenden.
# BAD: loop with growing result
slow <- function(n) {
result <- numeric(0)
for (i in 1:n) {
result <- c(result, i^2)
}
result
}
# BETTER: preallocate
better <- function(n) {
result <- numeric(n)
for (i in 1:n) {
result[i] <- i^2
}
result
}
# BEST: vectorize
fast <- function(n) {
(1:n)^2
}
# benchmarks
microbenchmark::microbenchmark(
slow = slow(1000),
better = better(1000),
fast = fast(1000),
times = 10
)
# apply family
# sapply/lapply: list apply
# vapply: typed sapply (safer, faster)
# map_dbl: tidyverse, typed
# rowMeans/colSums: faster than apply(x, 1, mean)Rcpp für Hotspots
Rcpp ermöglicht das Schreiben von C++-Funktionen, die von R aufrufbar sind – typischerweise 10-100x schneller für Schleifen, die nicht vektorisiert werden können. cppFunction für One-Liner; sourceCpp für Dateien. Rcpp 'sugar' bietet vektorisierte C++-Operatoren (sodass x*2+1 auf Vektoren funktioniert). Für Hotspots verwenden, die durch Profiling identifiziert wurden, nicht für alles – Rs vektorisierte Operationen sind bereits C.
library(Rcpp)
# inline C++ in R
cppFunction('
double sumC(NumericVector x) {
double s = 0;
for (int i = 0; i < x.size(); i++) {
s += x[i];
}
return s;
}
')
sumC(1:1e6)
# source from file
# file: code.cpp
# #include <Rcpp.h>
# using namespace Rcpp;
# // [[Rcpp::export]]
# double meanC(NumericVector x) {
# return std::accumulate(x.begin(), x.end(), 0.0) / x.size();
# }
sourceCpp("code.cpp")
# use Rcpp sugar (vectorized C++)
cppFunction('
NumericVector funC(NumericVector x) {
return x * 2 + 1; // vectorized via Rcpp sugar
}
')
# data frames
cppFunction('
DataFrame subsetC(DataFrame df, LogicalVector mask) {
return df[mask];
}
')Memory und data.table
data.table ist dramatisch schneller als dplyr für große Daten (>1M Zeilen) – oft 5-50x. Die dt[i, j, by]-Syntax ist prägnant, einmal gelernt. := modifiziert in Place (keine Kopie) – riesige Memory-Einsparungen. setkey ermöglicht schnelle Lookups und Joins. fread/fwrite sind die schnellsten CSV-Reader/Writer in R. data.table verwenden, wenn Geschwindigkeit zählt; dplyr für Lesbarkeit.
library(data.table)
# data.table is much faster than data.frame for large data
dt <- fread("huge.csv") # fast CSV reader
fwrite(dt, "out.csv") # fast CSV writer
# syntax: dt[i, j, by]
dt[, mean(value), by = group] # group by + summarize
dt[order(-value)] # sort
dt[value > 100, .N, by = group] # filter + count by group
dt[, .(avg = mean(value), n = .N), by = group]
# reference semantics (no copy)
dt[, new_col := value * 2] # add column in place
dt[value < 0, value := 0] # modify in place
# keys for fast lookups
setkey(dt, id)
dt["abc"] # fast lookup
dt["abc", mult = "first"]
# joins
dt1[dt2, on = "id"] # left join
dt1[dt2, on = .(id), nomatch = 0] # inner join
# memory tips
# - gc() to force garbage collection
# - object.size(x) to check size
# - rm() large objects when done
# - read data in chunks for huge filesParalleles Rechnen
parallel (base R) ist portabel, aber geschwätzig. future + furrr ist der moderne, tidyverse-freundliche Ansatz – Backends mit plan() wechseln. multicore verwendet Fork (schnell, nur Linux/Mac); multisession verwendet separate R-Sessions (portabel, langsamer). Parallelismus hat Overhead – nur lohnt es sich für Aufgaben, die >100ms pro Stück dauern. Immer vorher und nachher benchmarden.
library(parallel)
library(future)
library(furrr)
# parallel lapply
cl <- makeCluster(detectCores() - 1)
result <- parLapply(cl, items, function(x) {
# work on x
})
stopCluster(cl)
# future: modern, simpler
library(future)
plan(multisession) # parallel backend
result <- future_lapply(items, fun)
# or
plan(multicore) # fork (Linux/Mac, faster)
plan(cluster, workers = 4)
# furrr: parallel purrr
library(furrr)
plan(multisession, workers = 4)
result <- future_map(items, fun)
result <- future_map_dbl(items, ~ .x^2)
# foreach
library(foreach)
library(doParallel)
registerDoParallel(4)
result <- foreach(i = 1:10, .combine = c) %dopar% {
i^2
}
# always benchmark — parallel overhead can outweigh gains
# for small tasks
microbenchmark::microbenchmark(
serial = lapply(1:100, slow_fn),
parallel = future_lapply(1:100, slow_fn),
times = 5
)Verwandte R-Snippets
Copy-paste ready code for common tasks.
Data Frames
Create, inspect, filter, mutate, sort, aggregate, and merge data frames.
Vectors
Build atomic vectors, apply vectorized ops, index, and recycle.
ggplot2
Build layered plots with geoms, facets, and themes using the grammar of graphics.
dplyr
Chain mutate, filter, group_by, summarise, and joins with the native pipe.
Statistics
Compute summaries, run t-tests, linear models, ANOVA, and use distributions.
Apply Family
Apply functions over arrays, lists, and groups with apply, lapply, sapply, tapply.
Data Import/Export
Read and write CSV, TSV, RDS, RData, and text files with base R.
Functions
Define functions with defaults, variadic args, closures, and higher-order use.
Was this helpful?