1 Organisatorisches

1.1 Software

1.2 Kurs

  • 6 Termine wöchentlich donnerstags 13.00-16.15:
    • 24.10. vor Ort in Biel
    • 31.10. Remote
    • 7.11. Remote
    • 14.11. vor Ort in Biel
    • 21.11. Remote
    • 28.11. vor Ort in Biel
  • Erreichbarkeit ausserhalb des Kurses
    • Forum
    • rudolf.farys(at)unibe.ch
    • lukas.schlittler(at)bfh.ch
    • olivier.lehmann(at)bfh.ch

1.3 Kompetenznachweis

  • kleinere Übungen während der Kurstage (nicht bewertet aber sollen gelöst werden)
  • Note: Take-Home-Exam am Ende des Kurses (Einreichung bis 3. Januar 2025)

1.4 Kursinhalte

  • Grundlagen, Workflow
  • Datenmanagement
  • Datenmanagement, Überblick über Funktionen zur Datenanalyse

2 Nützliches

3 Warum R?

4 RStudio

5 Zwei Fallbeispiele

5.1 Beispiel 1: Visualisierung eines Bewegungsprofils (Google Location API / JSON)

Rohdaten

{
  "locations" : [ {
    "timestampMs" : "1417727161925",
    "latitudeE7" : 469345574,
    "longitudeE7" : 74339415,
    "accuracy" : 31
  }, {
    "timestampMs" : "1417727100712",
    "latitudeE7" : 469345603,
    "longitudeE7" : 74339025,
    "accuracy" : 32
  }, {
    "timestampMs" : "1417727030427",
    "latitudeE7" : 469345829,
    "longitudeE7" : 74339350,
    "accuracy" : 30
  }, {
    "timestampMs" : "1417726978911",
    ...

Einlesen in R

library(jsonlite)
library(plyr)

raw <- fromJSON('LocationHistory.json')

Datenansicht in R

> head(raw$locations)
    timestampMs latitudeE7 longitudeE7 accuracy velocity altitude activitys heading
1 1446466295025  469344810    74340787       20       NA       NA      NULL      NA
2 1446466154216  469344810    74340787       20       NA       NA      NULL      NA
3 1446466096080  469344859    74340783       20       NA       NA      NULL      NA
4 1446465976033  469344859    74340783       20       NA       NA      NULL      NA
5 1446465853219  469344859    74340783       20       NA       NA      NULL      NA

> lapply(locs,class)
$timestampMs
[1] "character"

$latitudeE7
[1] "integer"
...

# -> kein ordentliches Datenobjekt sondern eine riesige Liste
# -> Datentypen/Skalen noch nicht richtig

Aufbereitung

locs = raw$locations

# Einen data.frame bauen, der die Daten enthält:

ldf = data.frame(t=rep(0,nrow(locs)))

# Zeitformat ist als Character (POSIX) in ms gespeichert. 
# Umwandeln in eine Zahl (in Sekunden)
ldf$t = as.numeric(locs$timestampMs)/1000
class(ldf$t) = 'POSIXct'

# lat/lon sind gemessen in "10^7""
ldf$lat = as.numeric(locs$latitudeE7/1E7)
ldf$lon = as.numeric(locs$longitudeE7/1E7)

# zum Spass noch ein paar Variablen bauen:
ldf$Jahr = substring(as.Date(ldf$t),1,4)
ldf$Sommer = substring(as.Date(ldf$t),6,7)%in%c("06","07","08","09")

fertige Daten

> head(ldf)
                    t      lat      lon Jahr Sommer
1 2015-11-02 13:11:35 46.93448 7.434079 2015  FALSE
2 2015-11-02 13:09:14 46.93448 7.434079 2015  FALSE
3 2015-11-02 13:08:16 46.93449 7.434078 2015  FALSE
4 2015-11-02 13:06:16 46.93449 7.434078 2015  FALSE
5 2015-11-02 13:04:13 46.93449 7.434078 2015  FALSE
6 2015-11-02 13:03:23 46.93449 7.434096 2015  FALSE

Plotten

bern = get_map('Bern, CH',13,scale=2,source="google",maptype="hybrid")
ldf.bern = mapclip(ldf,bern)
png("bernmap.png", width=1200, height=1200)
ggmap(bern) + geom_point(data=ldf.bern,aes(x=lon,y=lat,colour=Sommer),size=2,alpha=0.1)
dev.off()

5.2 Beispiel 2: Modell für Fussballwetten (football-data.co.uk)

  • Ziele
    • Ein Ratingsystem für Fussballmannschaften entwickeln
    • Ein Prognosemodell für Fussballwetten trainieren
  • Daten: Historische Ergebnisse und Wettquoten europäischer Mannschaften: http://www.football-data.co.uk/data.php
  • Schritte:
    • Kommaseparierte Daten automatisiert herunterladen (Crawler)
    • alle Daten poolen
    • aufbereiten
    • Rating-Algorithmus anwenden
    • Modelle bauen / Resultate ausgeben lassen
# für jede Website die csv Links grabben
for(url in urls) {
doc <- read_html(url)
links <- html_nodes(doc, "a") |> html_attr("href")
csvlinks <- paste0("https://www.football-data.co.uk/", 
                   links[grep(pattern = ".csv", 
                              x = links)])

# Was bedeuten die Variablen?
# Div = League Division
# Date = Match Date (dd/mm/yy)
# HomeTeam = Home Team
# AwayTeam = Away Team
# FTHG = Full Time Home Team Goals
# FTAG = Full Time Away Team Goals
# FTR = Full Time Result (H=Home Win, D=Draw, A=Away Win)

# Alle Links parsen und zusammentackern
for(i in 1:length(csvlinks)) {
  tempdata <- as.data.table(read.csv(csvlinks[i]))
  try(dsocc <- rbind(dsocc,tempdata[, c("Div", "Date", "HomeTeam", "AwayTeam", "FTHG",
                                       "FTAG", "BbAvH", "BbAvD", "BbAvA"),
                                    with=FALSE], fill=TRUE, use.names=TRUE))
}
}

# Variablen definieren
dsocc[,win:=ifelse(FTHG>FTAG,1,ifelse(FTHG<FTAG,0,0.5))] # Sieg
dsocc[,numDate:=as.numeric(as.Date(Date,format="%d/%m/%y"))] # Datum numerich
dsocc[,HomeTeam:=as.character(HomeTeam)] # Datentyp fixen
dsocc[,AwayTeam:=as.character(AwayTeam)] # Datentyp fixen

Algorithmus (vereinfachter Ausschnitt)

  # Ähnlich wie die Elo-Zahl 
  # bekannt durch Schach, mittlerweile in Variationen aber sehr verbreitet:

  # 1. Startwerte setzen für alle Mannschaften
  # 2. Beim ersten Spiel im Datensatz anfangen -> Algorithmus anwenden, Ratings updaten
  # 3. Durch die Daten Zeile für Zeile gehen und Algorithmus anwenden, Ratings updaten
  # 4. Historie der Ratings (Mannschaftsstärken) speichern/mitprotokollieren

  # Kernstück:

  # Erwartungswerte für Tore und Gegentore
  E_a <- lambda * (off_a + h1) / def_b
  E_b <- lambda * (off_b - h2) / def_a
  
  # Tatsächlicher Spielstand (Tore/Gegentore):
  S_a <- dbl$FTHG[i]
  S_b <- dbl$FTAG[i]
  
  # neues Rating definieren auf Basis vom bisherigen Rating und den 
  # Abweichungen von der Erwartung
  neu_off_a <- off_a + K * (S_a - E_a)
  neu_def_a <- def_a - K * (S_b - E_b)
  neu_off_b <- off_b + K * (S_b - E_b)
  neu_def_b <- def_b - K * (S_a - E_a)
off def team mean
4146.210 3275.007 Bayern Munich 3710.608
3287.654 2557.058 Dortmund 2922.356
3205.189 2892.206 Leverkusen 3048.697
2567.968 1892.386 Hoffenheim 2230.177
2538.551 2369.014 Stuttgart 2453.782
2408.713 2374.429 Ein Frankfurt 2391.571
2375.921 2022.079 M’gladbach 2199.000
2189.751 2272.785 Wolfsburg 2231.268
2100.420 2118.469 Werder Bremen 2109.444
1996.193 2302.602 Mainz 2149.397
1905.965 1918.582 Augsburg 1912.273
1786.803 1840.454 Hertha 1813.628
1745.362 2117.526 Ingolstadt 1931.444
1712.447 2137.553 FC Koln 1925.000
1638.118 1807.842 Hannover 1722.980
1544.824 1758.929 Schalke 04 1651.876
1498.660 1622.374 Darmstadt 1560.517
1481.709 2092.286 Hamburg 1786.997

6 Arbeitsverzeichnis, Objekte und Workspace

6.1 Beispiel: Arbeitsverzeichnis, Objekte und Workspace

# Kommentare beginnen mit #
# Alles in der Zeile nach # wird von R ignoriert

# 1. R starten
# 2. Arbeitsverzeichnis (mit Schreibrecht) anlegen 
#    und R auf dieses Verzeichnis setzen: setwd()
# 3. Code ausführen mit STRG + Enter (Oder Icon "Run" oben rechts)

5 + 5

getwd() # Arbeitsverzeichnis anzeigen
# setwd() # Arbeitsverzeichnis definieren
setwd("meinpfad")

dir() # Arbeitsverzeichnis anzeigen

a <- 50 # Erzeugt Objekt a (Vektor der Länge 1) mit dem einzelnen Wert 50
a

# Objekt erzeugen, dass die Vornamen der Beatles enthält
die.beatles <- c("John", "Paul", "George", "Ringo")
die.beatles

# Mit c() - concatenate lässt sich auch ein Zahlenvektor bauen:
b <- c(1, 2, 3, 4)

# oder kürzer
b <- seq(1, 4)

# oder noch kürzer
b <- 1:4

# Objektnamen dürfen keine Leerzeichen haben. Ferner empfiehlt es sich - und _ zu meiden 
# siehe Google R Style Guide
# Namen sollten aussagekräftig sein. Namensgebung sollte im ganzen Code-File konsistent 
# sein (Punkte, Gross-/Kleinschreibung)

ls() # Workspace anzeigen

# Objekte a, b und die.beatles speichern in "beispiel1.RData"
save(a, b, die.beatles, file = "beispiel1.RData")

# Objekte löschen
rm(a, b, die.beatles) #oder:
rm(list=ls()) # löscht den gesamten Workspace
ls() # was ist jetzt noch im Workspace?
die.beatles # nicht mehr da

load("beispiel1.RData") # gespeichertes Objekt laden

die.beatles # wieder da

7 Packages

7.1 Beispiel: Packages

# Nehmen wir an wir möchten eine XLSX Datei einlesen.
# Ein Package, dass dies sehr gut macht ist "readxl" mit der Funktion read_excel()

install.packages("readxl") # Install package

?read_excel
# geht erst wenn das Package auch geladen ist

library(readxl)
?read_excel

8 Arithmetische- und logische Operatoren

8.1 Beispiel: Rechnen und Vergleichen

# Rechnen
ergebnis <- (23+24)*11/(18+15)*5
ergebnis

# Funktionen
log(2) 
cos(2)

# Vergleich
x <- -3:3
x

# sind die Elemente von x gleich 0?
x == 0

# grösser 0?
x > 0

# kleiner 0?
x < 0

# grösser gleich 0?
x >= 0

# kleiner gleich 0?
x <= 0

# ungleich 0?
x != 0

# grösser als -1 aber kleiner als 1
x > -1 & x < 1

# grösser als  1 und kleiner als -1
x > 1 & x < -1

# grösser als 1 oder kleiner als -1
x > 1 | x < -1

8.2 Übung: Rechnen und Vergleichen

  1. Arbeiten Sie in einem Scriptfile innerhalb von RStudio. Bewahren Sie den Code für diese Übung (und generell für die weiteren Übungen) in diesem Scriptfile auf.
  2. Berechnen Sie folgende Terme in R
    1. \((3 + 4)^{2}\)
    2. \(\frac{-99}{33} + 42\)
    3. \(log(1)\)
    4. \((\sqrt{2})^{2}\)
  3. Prüfen Sie folgende Vergleiche:
    1. \(5 = 7\)
    2. \(5 \times 5 \geq 6 \times 4\)
    3. \(\sqrt{3} \neq cos(17)\)
  4. Für die mean() Funktion kann ein zusätzlicher Parameter trim angegeben werden. Finden Sie heraus, was mit diesem Parameter getan werden kann indem Sie die Hilfe von mean() lesen und halten Sie diese Info in ihrem Scriptfile fest.

8.3 Lösung: Rechnen und Vergleichen

# 2.
(3 + 4)^2
-99/33 + 42
log(1)
sqrt(2)^2

# 3.
5 == 7
5 * 5 >= 6 * 4
sqrt(3) != cos(17)

# 4.
# help(mean)
# ?mean
# the fraction (0 to 0.5) of observations to be trimmed from each end of x 
# before the mean is computed.

9 Klassen/Datentypen

9.1 Übersicht Datentypen

# Homogen

# Integer Vektor
x <- 1:9
class(x)
x


# Numerischer Vektor
x <- c(1.3, 2.4, 3.5)
class(x)
typeof(x)
x


# Logischer Vektor
x <- -3:3
y <- x >= 0
class(y)
y

# String/Character Vektor
x <- c("a", "b", "c", "d", "f")
class(x)
x


# Matrix
matrix.2mal3 <- matrix(c(1,2,11,12,20,30), nrow = 2, ncol=3)
class(matrix.2mal3)
typeof(matrix.2mal3)
matrix.2mal3


# Array (z.B. 3 Matrizen)
arraybsp <- array(1:50, c(5,5,2)) # Zahlen von 1 bis 50 einem Array mit 2 5x5 Matrizen 
class(arraybsp)
arraybsp



# Heterogen

# Liste
liste <- list(a= c(4:8), b = c(1:3), c = c(2:10))
class(liste)
liste



# Data frame: Kann man anschauen durch Aufruf von swiss oder fix(swiss)
class(swiss)
swiss


# Faktoren
sex <- c(0, 0, 1, 1)
factor(sex, labels=c("Mann", "Frau"))

# warum ist das nuetzlich?
a <- rep(c("Haus", "Strasse"), 10^5)
object.size(a)
object.size(as.factor(a))
 
a<-rep(c("Haus", "Strasse"), 5)
object.size(a)
object.size(as.factor(a))

# Funktionen: z.B. cos(); mean()
class(mean)
mean

#  Datum
# https://stat.ethz.ch/R-manual/R-devel/library/base/html/strptime.html
daten.unformatiert<-c("19990123","20110324","20100412")
daten.als.daten<-as.Date(daten.unformatiert,format = "%Y%m%d")
tage.seit<-c(17000,14000,13000)
tage.als.daten<-as.Date(tage.seit,origin="1970-01-01")

9.2 Sonderwerte

  • Inf und -Inf: Positiv und negativ unendlich
  • NaN: “Not a number”, z.B. 0/0
  • NA: fehlender Wert (Missing)
# Wichtiger Hinweis zu fehlenden Werten:
x <- c(1, 2, NA, 4)

#falsch:
x == NA
x == "NA"

#richtig:
is.na(x)

# siehe hierzu auch:
is.infinite(x)

9.3 Vektoroperationen

1:100
1:100 * 3

# Vektor erzeugen und speichern
x <- 3 * 1:100
x

# oder über concatenate
x <- c(1, 2, 3, 4, 5)
x

# Vektor mit 15 Elementen. 5 Wiederholungen von 1,2,3
x <- rep(1:3, time=5) #default ist times: verkettet den Vektor 1 bis 3 5 mal
x
x <- rep(1:3, each=5) #each wiederholt jedes Element 5 mal
x
# Vektor mit Zahlen von 10 bis 100 in Zehnerschritten
x <- seq(10, 100, 10)
x

# Verknüpfen
x <- c(a = 10, b = 20, c = 30, d = 40)
x

9.4 Zugriff auf Datenbereiche

  • Zugriff auf Elemente:
    • einzelne Elemente
      • durch Angabe ihrer Position im Vektor
      • oder durch einen Namen falls vorhanden
    • mehrere Elemente
      • Zugriff über logische Operatoren (TRUE/FALSE), d.h. über Bedingungen
      • oder eine Sequenz von Positionen oder Namen
x <- 11:200
x
x[1]              # erstes Element von x
x[1:10]           # die ersten 10 Elemente von x
x[-(11:100)]      # Alle Elements von x ausser die Positionen 11 bis 100

# Allgemeiner gibt es drei Möglichkeiten:
x <- c(a = 10, b = 20, c = 30, d= 40) # vergibt direkt Namen, alternativ:
# names(x) <- c("a", "b", "c", "d")

# Möglichkeit 1
x[1:2] # über die Position

# Möglichkeit 2
x[c("a", "c")] # über den Namen

# Möglichkeit 3
x[x < 20 | x >= 30] # über eine Bedingung


# head / tail
x <- 1:100
head(x)
tail(x)

9.4.1 Übung: Vektoren

  1. Arbeiten Sie weiter in ihrem Übungs-Script-File
  2. Erzeugen Sie einen Vektor x der Länge 50, der die Zahlen von 1 bis 5 zehn mal wiederholt.
  3. Erzeugen Sie einen Vektor y (Länge 3), der die Elemente von x an den Positionen 12, 20 und 50 enthält.
  4. Erzeugen Sie einen Vektor freunde mit drei Namen Ihrer Wahl.

9.4.2 Lösung: Vektoren

# 2.
x <- rep(1:5, 10)
x
length(x)

# 3.
y <- x[c(12, 20, 50)]
y

# 4. 
freunde <- c("Bernd", "Frank", "Franz")

9.5 Faktoren und Listen

  • Faktoren
    • Class: "factor"
    • factor(): Erzeugt einen Faktor
    • sind ein Weg, kategoriale Daten (nominal/ordinal) sauber zu speichern
    • sind Vektoren mit Attributen
    • levels(): Zeigt die Kategorien eines Faktors
    • as.numeric(): Zwingt den Faktor in einen numerischen Vektor
  • Listen
    • Class: "list"
    • list(): Erzeugt eine Liste
    • Können beliebige Objekte/Objekttypen beinhalten
    • list$switzerland: Greift auf das Element switzerland der Liste list zu
    • list[2]: Zweites Element von list
    • Listen werden häufig als Rückgabewert von Funktionen verwendet, z.B. als Behältnis für Schätzergebnisse
  • Im Laufe des Kurses werden noch weitere Objektklassen auftauchen

9.5.1 Beispiel: Faktoren und Listen

# Faktor erzeugen

sex <- factor(c(rep(0, 50), rep(1, 50)), labels = c("Mann", "Frau"))

# Hinweis zu as.numeric(): manchmal hat man folgendes Problem:
jahrgang <- factor(c("2000", "2000", "2001", "2002")) # falscher Datentyp "character"
# umwandeln in numerische Werte liefert aber nicht, was wir wollen
as.numeric(jahrgang)

# besser:
as.numeric(as.character(jahrgang))

# Eine Liste erzeugen:
kursteilnehmer <- list(Maenner = c("Simon", "Peter", "usw."),
                    Frauen = c("Daniela", "Johanna"))
kursteilnehmer

# Zuriff auf Elemente der Liste
kursteilnehmer$Maenner
kursteilnehmer[1]
kursteilnehmer[[1]]
kursteilnehmer["Maenner"]
kursteilnehmer[["Maenner"]]

# was ist der Unterschied?
class(kursteilnehmer[["Maenner"]])
class(kursteilnehmer["Maenner"])
# d.h. wenn man mit den Elementen aus einem Listenelement arbeiten will,
# braucht es doppelte Klammern oder Zugriff über "$"

length(kursteilnehmer)
kursteilnehmer$Frauen[2]
kursteilnehmer[[2]][2]

9.6 Data Frames

  • Data frames sind das typische Format für Datensätze
  • …es handelt sich um eine Liste von Vektoren derselben Länge siehe Wickham
  • …ähnelt Matrizen aber die Spalten können unterschiedliche Datentypen beinhalten
  • data.frame(): erzeugt einen data frame
  • as.data.frame(): konvertiert in einen data frame
  • order(): sortiert Daten
  • summary() und str(): Überblick über data frames
  • head() and tail(): erste/letzte Zeilen inspizieren
  • names(): zeigt Spaltennamen
  • object$var1: Greift direkt auf die Spalte var1 im data frame object zu
  • na.omit(): Zeilenweise Ausschluss von fehlenden Werten, d.h. Zeilen die mindestens 1 Missing beinhalten

9.6.1 Beispiel: Data Frames

# fertige Daten sind oft data frames:
titanic <- read.dta("http://www.stata-press.com/data/kkd/titanic2.dta")
is.data.frame(titanic)

# man kann sich auch leicht selber einen bauen
obst <- c("Apfel", "Apfel", "Birne")
gemuese <- c("Tomate", "Karotte", "Karotte")
id <- 1:3
df <- data.frame(id, obst, gemuese)
df

# Ansteuern von Zeilen und Spaltenpositionen
df$obst
df[, "obst"]
df[3, "gemuese"]
df[3, 3]

# d.h. es gibt wie bei Vektoren diverse Möglichkeiten: Position, Name ($ oder "") oder Bedingung

9.6.2 Spalten hinzufügen und löschen

# Spalte hinzufügen
df$drinks <- c("Milch", "Cola", "Bier")
df

# Spalte löschen
df$gemuese <- NULL
df

9.6.3 Übung: Data Frames

  1. Verwenden Sie den R-eigenen Datensatz swiss. Verschaffen Sie sich ggf. einen Überblick mit ?swiss.
  2. Lassen Sie sich die zehnte bis zwölfte Zeile des Datensatz ausgeben.
  3. Lassen Sie sich die Spalten Education und Catholic ausgeben, jedoch nur für Fälle, deren Kindersterblichkeit zwischen 20 und 22 liegt.

9.6.4 Lösung: Data Frames

swiss[10:12, ]

swiss[swiss$Infant.Mortality > 20 & swiss$Infant.Mortality < 22, c("Education", "Catholic")]

10 Kontrollstrukturen und Schleifen

10.1 Beispiel: Kontrollstrukturen und Schleifen

# For Schleife
for (x in 1:10) {
  print(sqrt(x)) 
}

# aber besser:

sqrt(1:10) # da Funktionen in R i.d.R. sowieso vektorisiert arbeiten

# Schleifen machen aber Sinn, wenn es Abhängigkeiten zwischen den Durchläufen gibt:
x <- 0
for(i in 1:10) {
  x <- x+i
  print(x)
  }
# x wird immer weiter inkrementiert
# mit ein bisschen Überlegen gibt es aber auch hier eine Lösung die performant ist und vektorisiert arbeitet:
cumsum(1:10)

# es braucht also schon etwas kompliziertere Abhängigkeiten


# Sequenzen in For-Schleifen können auch Character sein:
namen <- c("Alfred", "Jakob", "Peter")
for (name in namen) {
  print(paste("Hallo", name))
}

# einfacher aber:
paste("Hallo", namen) # weil vektorisiert

#sinnvolleres Beispiel:

for (dataset in c("data1.csv", "data2.csv", "data3.csv")) {
  read.csv(pfad/dataset)
  # Anweisungen, 
  # z.B. Datenbereinigung, Appending (rbind), Modellschätzungen, etc.
}

# das Beispiel könnte aber auch mit Hilfer einer selbstgeschriebenen Funktion gut gelöst werden (siehe nächstes Kapitel)


# Durch Spalten loopen
for (column in 2:6) { # this loop runs through 2 to 6
        print(names(swiss)[column])
        print(mean(swiss[, column]))
}

# aber wieder geht es einfacher und schneller:

colMeans(swiss[, 2:6]) # oder
apply(swiss[, 2:6], 2, mean) 

# die 2 verweist auf "spaltenweise" (1 wäre zeilenweise). 
# D.h. für jede Spalte der Daten wird mean() angewendet



# While Schleife
x <- 0 # Startbedingung sollte gelten
while(x < 13) {
  x <- x+1 # inkrementieren, da sonst die Bedingung für immer gilt -> Endlosschleife
  print(x)
} 

# wird wiederholt solange x<13==TRUE
# sicherstellen, dass irgendwann das Kriterium FALSE wird!

# Beispiel für eine sinnvole while-Schleife: Abfrage einer Web-Ressource, die nicht immer erreichbar ist. while(keinen erfolg) try(ressource abfragen)


# if-Beispiel:

# Daten einlesen aus einer Liste von Files
setwd("C:/path/to/some/excel/files")
myfiles <- list.files()
# manche sind aber nun xls, und andere xlsx:

library(tools)
for(file in myfiles) {
  if(file_ext == "xls") {
    daten <- read.xls(file)
  }
  if(file_ext == "xlsx") {
    daten <- read.xlsx(file)
  }
}

# if prüft immer nur genau eine Bedinung. 


# es unterscheidet sich dadurch vom Kommando ifelse, das vektorisiert arbeitet
# ifelse Beispiel:

a<- sample(1:100, 10)
b<-ifelse(a < 50, "Nicht bestanden", "Bestanden")
b

# ifelse prüft einen Vektor von Bedingungen. Naheliegenderweise ist so
# ein Konstrukt also auch gut zur Datenaufbereitung geeignet.
# obiges Beispiel ist identisch mit aber einfacher als:

b[a < 50] <- "Nicht bestanden"
b[a >= 50] <- "Bestanden"
b
  • Achtung: Die meisten Schleifen sind vermeidbar und es gibt effizientere Lösungen, z.B. Funktionen aus dplyr, sapply()/lapply()/apply() oder spezielle Funktionen, z.B. rowMeans(), colMeans(), colSums().
  • Überlegen Sie, ob eine Schleife wirklich nötig ist für ihr Problem (sehr komplizierte und interdependente Probleme z.B. können oft nicht vektorisiert werden und Schleifen sind dann ggf. unumgänglich).
  • Fall Sie Schleifen verwenden: Ziehen Sie soviel Code wie möglich aus der Schleife raus (alles statische) um nicht unnötig Performance einzubüssen. Vermeiden Sie innerhalb der Schleifen komplexe Datentypen (data.frames) und Funktionen die damit zusammenhängen, z.B. cbind()/rbind(). Verwenden Sie besser Vektoren, Matrizen oder data.tables (kommt in den folgenden Kapiteln).

10.2 Übung: Kontrollstrukturen und Schleifen

  1. Schreiben Sie eine Schleife die 10 Durchläufe hat. In jedem Durchlauf sollen 100 standardnormalverteilte Zufallszahlen gezogen werden (rnorm(100)). Berechnen Sie innerhalb der Schleife Mittelwert und Standardabweichung dieser 100 Werte (mean() und sd()) und geben Sie diese aus (print).
  2. Schauen Sie sich die Hilfe zu tapply an. Versuchen Sie, die vorherige Aufgabe damit zu lösen. Erzeugen Sie hierfür zwei Vektoren. Der erste soll alle 10*100 = 1000 Zufallszahlen beinhalten, der zweite die Samplezugehörigkeit (erste hundert, zweite hundert, usw.), also z.B. ein Vektor der Länge 1000 mit 100 1ern, 100 2ern, … Diese zwei Vektoren können dann an tapply “gefüttert” werden.

10.3 Lösung: Kontrollstrukturen und Schleifen

# 1. 
for (i in 1:10) {
  x <- rnorm(100)
  mittel <- mean(x)
  std <- sd(x)
  print(paste("Mittelwert:", mittel, "Standardabweichung:", std))
}

# Alternative mit tapply:
x <- rnorm(1000)
sample <- rep(1:10, each=100)
tapply(x, sample, mean)
tapply(x, sample, sd)

11 Funktionen

11.1 Beispiel: Funktionen

print("Hallo")
print

lm

# Beispiel: Definieren einer Funktion
wurzel <- function(x) {
  x^0.5
}


wurzel(4)

# Namen eingeben um Inhalt zu sehen
wurzel

11.2 Übung: Funktionen

  1. Schreiben Sie eine Funktion, die Input gemäss folgender Formel verarbeitet: \((x + 2)^{2}\) und wenden Sie diese Funktion auf einen Vektor von Zahlen (1 bis 10) an.
  2. Schreiben Sie eine Funktion mit zwei Inputvariablen x und w, die einen gewichteten Mittelwert berechnet (x gewichtet mit w) und zurückgibt: \(\frac{\sum_{i=1}^{N}w_{i}x_{i}}{\sum_{i=1}^{N}w_{i}}\). Testen Sie ihre Funktion mit x <- 1:5 und w <- c(2,4,5,6,7).
  3. Bonus: Gehen Sie zurück zur tapply-Aufgabe. Wie könnte eine Lösung auf Basis einer selbstgeschriebenen Funktion aussehen (konzeptionell, lauffähiger Code nicht nötig)?

11.3 Lösung: Funktionen

# 1.
plus2hoch2 <- function(x) {
  (x+2)^2
}

plus2hoch2(1:10)

# 2. 
w <- c(2, 4, 5, 6, 7)
x <- 1:5
gew.mittelwert <- function(x, w) sum(w * x) / sum(w)
gew.mittelwert(x, w)

# 3.
drawSample <- function(i) {
  x <- rnorm(100)
  data.frame(mean.val = mean(x), std = sd(x))
}

do.call(rbind, lapply(1:10, drawSample))

# noch kompakter mit Package data.table (und schneller bei grossen Datenmengen!):
library(data.table)
rbindlist(lapply(1:10, drawSample))

12 Code lesbarer schreiben mit “Piping”

12.1 Beispiel: Code lesbarer schreiben mit “Piping”

daten <- rnorm(100)

# normale Schreibweise
mean(daten)

# mit Piping
daten |> mean()

# auf diese Art lassen sich aber mehrere Schritte verketten, z.B.:

daten <- c("1", "2", "3") # Strings statt Zahlen

daten |>
  as.numeric() |> # umwandeln
  plus2hoch2()

# später mehr zum Thema Piping!

13 Kontingenztabellen und einfache Tests

13.1 Beispiel: Kontingenztabellen und einfache Tests

# zwei fiktive Vektoren erstellen
auto <- factor(c(1, 0, 0, 0, 0, 
          1, 0, 1, 0, 0, 
          1, 1, 0, 0, 1, 
          1, 1, 0, 0, 0), 
          labels = c("kein Auto", "Auto"))
geschlecht <- c(rep("Frau", 10), 
                rep("Mann", 10))

# Tabelle
table(geschlecht)
table(geschlecht, auto)

# Chi2-Test
chisq.test(table(geschlecht, auto))

# Tabelle in Prozent
100*prop.table(table(geschlecht))
100*prop.table(table(geschlecht, auto))
round(100*prop.table(table(geschlecht, auto)), 2) # gerundete Werte
round(100*prop.table(table(geschlecht, auto), margin=1), 2) # margin=1 berechnet Zeilenprozente, margin=2 Spaltenprozente

# oder mit Piping
table(geschlecht, auto) |>
  prop.table(margin = 1) |>
  round(2) * 100

# Lagemasse

## Mean
mean(x)

## Median
median(x) 
sort(x)

## gibt es verkürzt über die generische Funktion summary
summary(x)

# Streuung, z.B.

sd(x)
var(x)

# Korrelation zwischen Vektoren
cor(x, y)
cor(x, y, method = "spearman") # Rangkorrelation
cov(x, y)

# Mittelwertvergleich
t.test(x, y)

13.2 Übung: Kontingenztabellen und einfache Tests

  1. Laden (ggf. vorher installieren) Sie das Paket foreign. Laden Sie die Daten über den Untergang der Titanic mit folgendem Befehl: titanic <- read.dta("http://www.stata-press.com/data/kkd/titanic2.dta") und machen Sie sich ein bisschen mit den Daten vertraut (z.B. head() oder summary()).
  2. Erzeugen Sie eine Kreuztabelle mit den Variablen class und survived.
  3. Berechnen Sie Median und Mittelwert des Alters für Überlebte und Gestorbene.
  4. Bonus: Unterscheiden sich die beiden Gruppen bzgl. ihres Alters? Was sagt uns hier ein t-Test?

13.3 Lösung: Kontingenztabellen und einfache Tests

### 1.
library(foreign)
titanic <- read.dta("http://www.stata-press.com/data/kkd/titanic2.dta")
head(titanic)
summary(titanic)

### 2. 
# survived aus Konvention auf die Y-Achse, class auf die X-Achse (Y ist die abhängige, X die erklärende Variable)
table(titanic$survived, titanic$class) # Faktor crew ist hier nicht sauber gelabelt

### 3.
mean(titanic$age2[titanic$survived == "yes"])
mean(titanic$age2[titanic$survived == "no"])

# alternativ mit tapply
tapply(titanic$age2, titanic$survived, mean)

# ja sie unterscheiden sich um ca. 5 Jahre. Die Verstorbenen sind älter. Man könnte an der Stelle auch noch einen Signifikanztest machen:
t.test(titanic$age2 ~ titanic$survived)
# dieser zeigt, dass die 5 Jahre nicht zufällig sondern systematisch sind.

14 Workflow

15 Mit Strings arbeiten

15.1 Beispiel: Mit Strings arbeiten

# Gross-/Kleinschreibung
"trump" == "trump"
"Trump" == "trump"
tolower("Trump") == tolower("trump") 


# Trimmen
s <- "  Hello!   "
trimws(s)
trimws(s, "right")
trimws(s, "left")


# Extrahieren
s <- "I will build a great, great wall on our southern border, and I will have Mexico pay for that wall. Mark my words."
substr(s, 3, 6)
substr(s, 74, 79)


# Splitten
s <- c("To be blunt, people would vote for me. They just would.")
strsplit(s, ".", fixed = TRUE)


# Joinen
paste("one", "two", "three")
paste("one", "two", "three", sep = "-")

# paste erwartet normalerweise eine Reihe von ein-elementigen Inputs, nicht Vektoren:
s <- c("one", "two", "three")
paste(s)

# wir können paste() aber sagen es soll diesen Vektor "collapsen"
s <- c("one", "two", "three")
paste(s, collapse = " + ")
paste(s, collapse = "")


# Suchen und ersetzen
sub('Trump', 'Donald', 'Trump became president. Trump makes everyone great again.')
gsub('Trump', 'Donald', 'Trump became president. Trump makes everyone great again.')

15.2 Übung: Mit Strings arbeiten

  1. Erzeugen Sie einen Vektor mit 100 Strings mit den Werten “objekt_1”, “objekt_2”,…

Verwenden Sie nun folgenden String:

s <- c("Kontostand: 100 EUR", "Kontostand: 150 EUR", "Kontostand: 185 EUR")
  1. Ersetzen Sie EUR durch CHF.
  2. Splitten Sie den String um nur den Geldbetrag zu erhalten

15.3 Lösung: Mit Strings arbeiten

# 1. 
paste("objekt", 1:100, sep = "_")

# 2. 
s <- c("Kontostand: 100 EUR", "Kontostand: 150 EUR", "Kontostand: 185 EUR")
sub("EUR", "CHF", s)

# 3. 
strsplit(s, ": ", fixed = TRUE)
# um die gesplitteten Vektoren einzeln wieder einzusammeln kann man bspw. sapply() verwenden:
liste <- strsplit(s, ": ", fixed = TRUE)
sapply(liste, "[", 2)

# Man könnte so auch direkt an den numerischen Wert kommen, z.B.
liste <- strsplit(s, " ", fixed = TRUE)
as.numeric(sapply(liste, "[", 2))

# andere Ansätze könnten natürlich auch sein:
sub("Kontostand: ", "", s) # ersetze den Teil den man nicht will mit "nichts"
substr(s, 13, 100) # nimm den Teil ab Character Nr. 13


# dafür gibt es aber effiztientere Lösungen, z.B. mit regulären Ausdrücken

15.4 Reguläre Ausdrücke / Pattern Matching

  • Einige R-Funktionen erlauben die Verwendung von sogenannten regulären Ausdrucken, beispielsweise sub/gsub

  • Reguläre Ausdrücke erlauben “wildcards” und andere Elemente um ein Suchmuster zu definieren

  • Eine Auswahl von Metacharacters:

    • Wildcards/Zeichenklassen
      • .: matcht einen beliebigen Character.
      • [a-z]: lowercase a bis z
      • [A-Z]: uppercase A bis Z
      • [a-zA-Z]: lower a bis z und uppercase A bis Z
      • [0-9]: Ziffern
      • |: Alternation / “oder”
    • Quantifizierer
      • *: matcht mindestens 0 mal.
      • +: matcht mindestens 1 mal.
      • ?: matcht höchstens 1 mal.
      • {n}: matcht genau n mal.
      • {n,}: matcht mindestens n mal.
      • {n,m}: matcht zwischen n und m mal.
    • Position
      • ^: matcht am Anfang des Strings.
      • $: matcht am Ende des Strings.
    • Auslesen bestimmter Teile
      • ()
  • Es gibt viele weitere Metacharacter, siehe z.B.: https://stat.ethz.ch/R-manual/R-devel/library/base/html/regex.html

  • Siehe auch https://emailregex.com/ für ein ausführliches Regex Beispiel

15.4.1 Beispiel: Reguläre Ausdrücke / Pattern Matching

# wildcards

s <- c("Trump", "Trrump", "Trrrump", "Tump")

grepl("T.ump", s) 
grepl("Tr+ump", s) 
grepl("Tr{1,2}ump", s) 
grepl("Tr{2,}ump", s) 
grepl("Tr*ump", s) 

grepl("T.*ump", s)

grepl("T[a-z]ump", c("Tump", "Trump", "Trrump", "Tdump"))

# ersetzen
gsub("T.*ump", "Donald", c("Tump", "Trump", "Trrump", "Tdump"))

# wir können Teile des Strings als Gruppe erfassen um diese zu verwenden

# z.B.:
gsub("(Donald) (Trump) (.*)", "\\2 \\1 \\3", "Donald Trump is president.")
# gsub erinnert sich hier an die drei Gruppen und gibt sie in der Reihenfolge 2., 1., 3. zurück

# Das Package stringr hat zudem einige convenience Funktionen die auf sub/gsub/grep/grepl aufbauen
library(stringr)
library(magrittr)

# CHF Beträge aus Text extrahieren ({1,5} -> min. 1 Ziffer, max. 5 Ziffern)
str_extract(c("Ein Brot kostet 4 CHF.", "Ein Auto kostet 50000 CHF."), "[0-9]{1,5} CHF")

# Beispiel für die Verwendung eines oder-Operators, bei unterschiedlichen Schreibweisen:
str_extract(c("Ein Brot kostet 4 Franken.", "Ein Auto kostet 50000 CHF."), "[0-9]{1,5} (CHF|Franken)")

# Strings matchen aber nur Teile davon zurückgeben:
str_match(c("Ein Brot kostet 4 CHF.", "Ein Auto kostet 50000 CHF."), "([0-9]{1,5}) CHF")
# die () definieren den Teil der zusätzlich zurückgegeben wird.

# Beispiel wie man an die zweite Spalte kommt:
str_match(c("Ein Brot kostet 4 CHF.", "Ein Auto kostet 50000 CHF."), "([0-9]{1,5}) CHF")[, 2] |> as.numeric()

15.4.2 Übung: Reguläre Ausdrücke / Pattern Matching

Aus folgendem String

s <- "George Bush, born July 6, 1946, is a former American president. His full name is George Walker Bush."
  1. Extrahieren Sie George Bush’s Geburtsjahr und
  2. Seinen zweiten Vornamen

15.4.3 Lösung: Reguläre Ausdrücke / Pattern Matching

# andere Lösungen ebenso möglich!
# 1.
gsub(".*([0-9]{4}).*", "\\1", s)

# oder mit stringr
library(stringr)
str_extract(s, "[0-9]{4}")

# 2.
gsub(".*George ([a-zA-Z]+) Bush.*", "\\1", s)

# oder mit str_match
str_match(s, "George ([a-zA-Z]+) Bush") # Buchstaben zwischen George und Bush
str_match(s, "George ([A-Z]{1}[a-z]+) Bush") # noch etwas expliziter mit grossen Anfangsbuchstaben

16 Datenmanagement

16.1 Package dplyr

  • Eines der populärsten Packages für data wrangling: dplyr von Hadley Wickham/Romain Francois bietet ein Toolset zur Datenaufbereitung
  • Siehe die dplyr Vignette und das Data Wrangling Cheat Sheet für einen sehr guten Überblick
    • filter(): wählt ein Subset von Zeilen (siehe auch slice())
    • arrange(): sortiert
    • select(): wählt Spalten
    • mutate(): erzeugt neue Spalten
    • summarise(): aggregiert (collapses) Daten zu einzelnen Datenpunkten
    • group_by(): Definiert Untergruppen in den Daten, damit o.g. Funktionen separat pro Gruppe angewandt werden können
    • uvvm.
    • dplyr kann sehr gut zusammen mit Piping verwendet werden, d.h. das Datenobjekt wird von Funktion zu Funktion weitergereicht durch |> wodurch der Code deutlich besser lesbar und kompakter wird. (Base-R Pipe |>)

16.1.1 Beispiel: Package dplyr

library(foreign)
titanic <- read.dta("http://www.stata-press.com/data/kkd/titanic2.dta")

# install.packages("dplyr")
library(dplyr)

filter(titanic, class == "1st class", age2 < 18)

# konventionell wäre das komplizierter:
titanic[titanic$class == "1st class" & titanic$age2 < 18, ]

# zusätzlich Spalten selektieren:
titanic |>
  filter(class == "1st class", age2 < 18) |>
  select(sex, age2, survived)

# neue Variable "child" bauen
titanic |>
  mutate(child = age2 < 18) |>
  head()

# Auszählen wer gestorben ist nach Geschlecht und Kind (ja/nein)
titanic |>
  mutate(child = ifelse(age2 < 18, "yes", "no")) |>
  group_by(sex, child, survived) |>
  summarise(n=n()) |>
  arrange(sex, child, survived)

# Für Datenaufbereitung ebenfalls oft hilfreich ist case_when
titanic |>
  mutate(age_group = case_when(
    age2 < 5 ~ "Kleinkind",
    age2 >= 5 & age2 < 12 ~ "Kind",
    age2 >= 12 & age2 < 18 ~ "Jugendlicher",
    age2 >= 18 & age2 < 30 ~ "Junger Erwachsener",
    age2 >= 30 & age2 < 50 ~ "Erwachsener mittleren Alters",
    age2 >= 50 & age2 < 70 ~ "Älterer Erwachsener",
    age2 >= 70 ~ "Senior",
    TRUE ~ "Unbekannt"  # Für den Fall, dass age2 NA ist oder nicht in die anderen Kategorien fällt
  )) |>
  group_by(age_group, survived) |>  summarise(count = n()) 

16.1.2 Übung: Package dplyr

  1. Laden Sie die Daten http://www.farys.org/daten/allbus2008.dta (hierfür brauchen Sie die Funktion read.dta() aus dem Package foreign. Es handelt sich um Daten aus einer Bevölkerungsumfrage. R Version 3.4.x hat offenbar ein bislang nicht gefixtes Problem im Zusammenspiel mit read.dta(). Bei einem “factor level duplicated” Error bitte die Option convert.factors=FALSE verwenden.
  2. Wir brauchen nur die Variablen v151 (Geschlecht), v154 (Alter) und v386 (Einkommen). Legen Sie einen verkleinerten Datensatz an, der nur diese drei Variablen beinhaltet (und ordentliche Spaltennamen hat). Sie können in select direkt neue Spaltennamen vergeben: select(neuername = altername).
  3. Die Daten enthalten falsche Werte, die eigentlich als fehlende Werte deklariert sein müssten (z.B. 99997, 99998 und 99999 beim Einkommen und 999 beim Alter). Filtern Sie diese Werte weg (filter()).
  4. Berechnen Sie mit summarise() das Durchschnittseinkommen für jede Kombination aus Alter und Geschlecht (group_by(), summarise(), mean()).

16.1.3 Lösung: Package dplyr

# Daten laden
allbus <- read.dta("http://www.farys.org/daten/allbus2008.dta")

allbus.agg <- allbus |>
  select(geschlecht = v151, # die drei Variablen wählen und direkt umbenennen
         alter = v154,
         einkommen = v386) |>
  filter(einkommen < 99997, alter < 999) |>  # fehlende Werte droppen
  group_by(geschlecht, alter) |> # gruppieren
  summarise(m_einkommen = mean(einkommen)) # aggregieren

# Zusatz: Das ganze könnte man jetzt grafisch anschauen (müsste man ggf. etwas gröber gruppieren)
library(ggplot2)  
ggplot(allbus.agg, aes(x=alter,y=m_einkommen,color=geschlecht)) +
  geom_line()

16.2 Das Package data.table

  • Versucht dasselbe wie dplyr, ist aber stärker auf Performance und Funktionalität ausgerichtet während dplyr mehr die Lesbarkeit des Codes für den Menschen im Fokus hat
  • data.table bringt einen eigenen Objekttyp data.table mit, der mit allen data.frame Operationen abwärtskompatibel ist aber wesentliche Neuerungen beinhaltet:
  • Performance (schnellstes Paket für Datenoperationen, z.B. Sortieren, Matchen, Mergen, Gruppieren, Reshapen (dcast und melt ohne dass reshape2 geladen werden muss), Umbenennen von Spalten, Doubletten droppen, u.a.)
  • Memory efficient (beim Umbenennen/Umsortieren von Spalten oder Zeilen werden intern keine Kopien der Daten angelegt, die Daten sind lediglich “changed by reference”)
  • Generelle Form: DT[i,j,by]: “Nimm das DT an Zeilen i und berechne j, gruppiert nach by
  • Kennt einige Konzepte zur eleganten Datenaufbereitung, etwa
    • .N: Anzahl Zeilen im Subset,
    • .BY: Platzhalter für den aktuellen Wert der Gruppierungsvariable vom Typ list. z.b. Nützlich, wenn man bei nach numerischen Werten gruppierten Variablenberechnungen auf den Gruppenwert zurückgreifen will. Oder wenn man bei nach kategorisch gruppierten Berechnungen auf den Namen zugreifen will (z.b. um zu ploten),
    • .SD: Enthält die Daten des Subsets das gerade verarbeitet wird (alle Spalten ausser by). Mit .SDcols spezifizieren sie, welche Spalten zu .SD gehören sollen. Nützlich bei der Anwendung einer Funktion auf mehrere, aber nicht alle Spalten eines DT.
    • .I: Index eines Falls innerhalb des Subsets,
    • .GRP: Group-Counter
  • Sehr sehr mächtig, aber Syntax ist etwas schwerer zugänglich und braucht mehr Eingewöhnung.
  • Beim Arbeiten mit grossen Datensätzen unerlässlich!

16.2.1 Beispiel: Package data.table

library(foreign)
titanic <- read.dta("http://www.stata-press.com/data/kkd/titanic2.dta")
 
library(data.table)
titanic <- as.data.table(titanic) # als data.table definieren
titanic  # man beachte die andere Darstellung. data.table zeigt
# sicherheitshalber niemals alle Zeilen auf der Konsole
 
#Filtern ####
titanic[class=="1st class" & age2 < 18]
 
#Selektieren ####
titanic[,list(sex,class)]#shortcut für list(sex,class): .(sex,class)
titanic[,c("sex","class")]#data.table versteht c("name") als Liste
 
#Neue Variable berechnen ####
titanic[,child := ifelse(age2 < 18, "yes", "no")]# := um Variable by reference hinzuzufügen
titanic[,":="(child = ifelse(age2 < 18, "yes", "no"),
              oldperson = ifelse(age2 > 65, "yes", "no"))]#mehrere Variablen gleichzeitig
 
#Gruppierte Berechnungen ####
titanic[,Durchschnittsalter.Klasse:=mean(age2),by=.(class)] #Neue Spalte
titanic[,.(Durchschnittsalter.Klasse=mean(age2)),by=.(class)] #Aggregation
 
#Anwendung (eigener) Funktion auf eine Auswahl von Spalten####
# Z.b. Modusfunktion
Mode <- function(x) {
  val<-max(table(x))
  names(table(x))[table(x)==val]
}
 
#.SDcols wird als viertes Argument spezifiziert: definiert die zu bearbeitenden Spalten
vars <- c("survived","sex")
titanic[,lapply(.SD, Mode),,.SDcols=vars]
 
#Wenn man das Resultat in einer Variable behalten möchte: '(namensvektor):=' davor
varsnew <- paste0(vars,".Modus")
titanic[,(varsnew):=lapply(.SD, Mode),,.SDcols=vars]
 
#Chaining#####
titanic[, child := ifelse(age2 < 18, "yes", "no")][ # chaining: Äquivalent zum piping
  ,child.died := ifelse(child == "yes" &
                         survived == "no",
                       "yes", "no")]
 
#Tipps und Kniffs######
 
# Vorsicht beim Selektieren######
a<-c("sex","class")
titanic[,a] # Charaktervektoren können nicht als Objekte übergeben werden:
# data.table sucht immer nach Namen innerhalb des data.table, wenn es sie nicht findet
 
titanic[,..a] # Wenn Sie '..' vor einen Charactervector setzen funktioniert es.
titanic[,a,with=F] # Besser with=F (nicht within evaluieren).
 
# Dies ist nützlich, wenn sie nach Variablen mit bestimmtem Mustern suchen
# (z.b. mit grepl("s",names(titanic))). So versteht data.table auch logische
# Vektoren (oder Spaltennummern) als Selektionskriterien
 
titanic[,grepl("s",names(titanic)),with=F]
 
# Vorsicht beim Kopieren von Objekten#####
#Wenn man einen data.table kopiert, copy() verwenden, sonst werden neue Variablen auf das ursprüngliche Objekt geschrieben
 
titanic.copy <- copy(titanic)
 
#Beispiel für .I#####
a <- data.table(a = c(0,2,3,1,6,7,8))
a[, b := a[.I+1]]#Alternative zu lead() in dplyr
a[, c := c(NA, a[.I-1])]#Alternative zu lag() in dplyr
 
#neu auch
a[, b := shift(a, n = 1, type = "lag")]
a[, c := shift(a, n = 1, type = "lead")]

16.2.2 Übung: Package data.table

  1. Laden Sie die Daten des European Social Survey (Auszug) mit load(url("http://www.farys.org/daten/ESS.RDATA")).
  2. Welches Land hat am meisten Befragte (Tipp: verwenden Sie .N und chainen sie die Bedingung [N==max(N)] um die Anzahl und den Namen des Landes auszugeben))?
  3. Welches ist das durchschnittlich glücklichste Land? Welches das unglücklichste?
  4. In welchem Land gibt es anteilsmässig am meisten “komplett glückliche Menschen” (d.h. der Anteil der Personen mit angegebener Zufriedenheitswert = 10 ist maximal; Tipp: berechnen Sie den mittleren Anteil pro Land und werfen sie den höchsten Wert aus)?
  5. Welches Land hatte von 2008 bis 2010 den grössten Rückgang im Durchschnittsglück (Tipp: über die eckigen Klammern können sie innerhalb des bearbeiteten Subsets Fälle auswählen, z.b. mean(happy[year==2008],na.rm=T))?
  6. Berechnen sie gleichzeitig den Modus bezüglich Glücklichkeit und Support für Umverteilung pro Land (Variable gincdiff 1=agree strongly,5=disagree strongly). Tipp: verwenden Sie dabei die Modusfunktion aus dem Beispiel oben.

16.2.3 Lösung: Package data.table

library(data.table)
library(dplyr)
library(ggplot2)
load(url("http://www.farys.org/daten/ESS.RDATA"))
#gincdif: The government should take measures to reduce differences in income levels. 1=agree strongly,5=disagree strongly
#happy: Taking all things together, how happy would you say you are? #10 extremely happy 1 =Extremely unhappy
#uempla: Unemployed, actively seeking

#um Gruppenzähler kennenzulernen
#__________________________________

#2. Welches Land hat am meisten Befragte?#######
ess[,.N,Land][N==max(N)]

#dplyr
ess |>
  group_by(Land) |>
  count() |>
  arrange(-n)

#Base
a<-table(ess$Land)
names(a)[a==max(a)] 
#Um Aggregatsfunktion kennenzulernen
#__________________________________

#3. Welches ist das durchschnittlich glücklichste Land? Welches das unglücklichste?#######
ess[,.(m.happy=mean(happy,na.rm=T)),Land][m.happy==max(m.happy)|m.happy==min(m.happy)]

#Alternativ: ohne order
hap <- ess[,.(m.happy=mean(happy,na.rm=TRUE)),Land]
setorder(hap,m.happy)
hap

#dplyr

ess |>
  group_by(Land) |>
  summarise(m.happy=mean(happy,na.rm=T)) |>
  arrange(-m.happy)


#Base R
ess<-ess[!is.na(ess$happy)&!is.na(ess$Land),]
agghapp <- aggregate(ess$happy,list(ess$Land),FUN=mean)
agghapp[order(agghapp$x),] 

#um Variablengenerator kennenzulernen (:=)
#__________________________________

#4. In welchem Land gibt es am meisten komplett glückliche Menschen?#######
ess[,":="(komplett=ifelse(happy==10,1,0))][
  ,.(`Anteil komplett glücklich`=mean(komplett,na.rm = T)),by=Land][order(`Anteil komplett glücklich`)]


#dplyr
ess |>
  mutate(komplett=happy==10 |> 
  group_by(Land) |> 
  summarise(`Anteil komplett glücklich`=mean(komplett,na.rm=T)) |> 
  arrange(-`Anteil komplett glücklich`)

#base R

ess$komplett <- ifelse(ess$happy==10,1,0)
ess <- ess[!is.na(ess$komplett),]
agghapp <- aggregate(ess$komplett,list(ess$Land),FUN=mean)
agghapp[order(agghapp$x),]

#Um die Verwendung von Subsetberechnungen kennenzulernen
#__________________________________
#5. Welches Land hat die grösste Einbusse im Durchschnittsglück gehabt#######
#von 2008 bis 2010?
ess[,.(
  Dif=mean(happy[year==2010],na.rm=T)-
    mean(happy[year==2008],na.rm=T)),
  by=Land][
    order(Dif)][!is.na(Dif)]

##alternativ: m.happy-m.happy[-.N] (eine Zeile vorher)
#bei sample mit nur 2008 und 2010 (geordnet)

#dplyr
rank <- ess |> 
  group_by(Land) |> 
  summarise(Dif=mean(happy[year==2010],na.rm=T)-
              mean(happy[year==2008],na.rm=T)) |> 
  arrange(Dif) |> 
  filter(!is.na(Dif))

#Base R
zwei8 <- ess[ess$year==2008&!is.na(ess$happy),]
zwei10 <- ess[ess$year==2010&!is.na(ess$happy),]

agg8<-aggregate(zwei8$happy,list(zwei8$Land),FUN=mean)
names(agg8)<-c("Land","Happy.08")
agg10<-aggregate(zwei10$happy,list(zwei10$Land),FUN=mean)
names(agg10)<-c("Land","Happy.10")

ess.08.10 <- merge(agg8,agg10,by="Land",all.x = F) 
ess.08.10$dif <- ess.08.10$Happy.10-ess.08.10$Happy.08
ess.08.10[order(ess.08.10$dif),c("Land","dif")]


library(ggplot2)
ggplot(rank,aes(reorder(Land, Dif),Dif))+#einfache Art Faktoren nach Grösse einer anderen Dimension zu sortieren
  geom_bar(stat = "identity")+
  theme(axis.text.x = element_text(angle = 90, hjust = 1))+
  labs(x="",y="Glücklichkeitsveränderung 2010-2008")


# 6. Berechnen sie gleichzeitig den Modus bezüglich Glücklichkeit und Support für Umverteilung#####
# Um Funktion kennenzulernen, mit der man mehrere Variablen gleichzeitig modifiziert:
Mode <- function(x) {
  names(which.max(table(x)))
}

ess[,lapply(.SD, Mode),Land, .SDcols=c("happy","gincdif")]

#dplyr
ess |> 
  group_by(Land) |> 
  summarise_at(.vars = vars(happy, gincdif),
            .funs = Mode)

#Base R
aggregate(cbind(happy, gincdif)~Land,FUN = Mode,data = ess)

16.3 Vergleich Base-R, dplyr und data.table

# Beispiel data.frame:
data <- data.frame(A=1:10, B=2:11, C=3:12, D=c("a","b"))

# Beispiel data.table:
library(data.table)
data <- data.table(A=1:10, B=2:11, C=3:12, D=c("a","b"))
Base-R dplyr data.table
Auswahl von Zeilen nach Kriterium B==2 data <- data[data$B==2, ] data <- filter(data, B==2) oder
data <- data |> filter(B==2)
data <- data[B==2] oder
data <- data[B==2, ]
Auswahl von Spalten B und C data <- data[c("B", "C")] oder
data <- data[, 2:3] oder
data <- data[, c("B", "C")] oder
vars <- c("B", "C") data <- data[, vars]
data <- select(data, B, C) oder
data <- data |> select(B,C)
data <- data[, .(B, C)] oder
data <- data[, c("B", "C")] oder
vars <- c("B", "C") data <- data[, ..vars]
Erstellen neuer Variablen E (A/B) data$E <- data$A/data$B data$E <- mutate(data, A/B) oder
data <- data |> mutate(E = A/B)
data[,E := A/B] oder
data[, E:=A/B, ] oder
data[, ":="(E=A/B)]
Erstellen neuer, gruppierter Variablen E (Mittelwert A gruppiert nach D) data$E <- ave(data$A,data$D,FUN=mean) data <- group_by(data,D)
data <- mutate(data,E = mean(A)) oder
data <- data |>
   group_by(D) |>
  mutate(E = mean(A))
data[,E := mean(A),D] oder
data[,E := mean(A),by=D] oder
data[, ":="(E=mean(A)),by=D]
Aggregieren Mittelwert von A und B sapply(data[c("A", "B")], mean) data |>
  summarise_at(vars=vars(A, B), .funs = mean)
data[, .(meanB = mean(B), meanA = mean(A))] oder
data[, lapply(.SD, mean), , .SDcols = c("A", "B")]
Nach B gruppiert Aggregieren durch Mittelwert von A und C aggregate(data$A,data$B,mean) aggregate(data$C,data$B, mean) data <- group_by(data,B)
data <- summarise_at(data,vars = vars(A,C), .funs = mean) oder
data |>
  group_by(B) |>
  summarise_at(vars = vars(A,C), .funs = mean)
data[, .(meanC = mean(C), meanD=mean(A)), B] oder
data[, lapply(.SD, mean),B, .SDcols=c("C", "A")]

16.4 Tidy vs Messy Data

  • Hadley Wickham hat versucht, einige Konventionen zur sauberen Darstellung/Ablage von Daten aufzuschreiben
  • siehe http://vita.had.co.nz/papers/tidy-data.pdf und http://tidyverse.org/
  • Quintesenzen:
    • sobald Daten einmal sauber (tidy) sind, können Analysetools (Plotting, Modellfitting) auch sauber und ohne Zusatzaufwand arbeiten (z.B. ggplot2, reshape, lm/glm)
    • Fälle in Zeilen, Variablen (Eigenschaften dieser Fälle) in Spalten
  • Daten sind Messy, wenn
    • Spalten nicht/inkonsistent beschriftet sind
    • Eine Spalte mehr als eine Variable beinhaltet
    • Variablen statt in Spalten auch in Zeilen auftauchen
    • unterschiedliche Beobachtungseinheiten in derselben Tabelle sind
    • aber auch wenn wir den falschen Datentyp (z.B. String statt Datum, Character statt Numeric, etc.) verwenden.
  • wir brauchen also Werkzeuge um
      1. Strings und Werte zu manipulieren und
      1. Daten richtig anzuordnen

16.4.1 Tidy Data

  • Tidy Data liegen vor, wenn Beobachtungen in Zeilen sind und Attribute dieser Beobachtungen in Spalten
  • Sind die Daten anders organisiert, können wir melt() und dcast() aus dplyr resp. data.table verwenden
  • Es gibt neuere “Vokabeln” wie gather und spread aus tidyr sowie pivot_longer und pivot_wider ebenfalls aus tidyr
  • Alles macht im wesentlichen dasselbe
  • Installation des tidyr Packages macht generell Sinn. Dieses enthält eine Vielzahl von weiteren Paketen, die wir später teilweise noch brauchen werden (z.B. dplyr)

16.4.2 Beispiel: Tidy vs Messy Data

  • Betrachten wir den Beispieldatensatz billboard. Dieser enthält Chartplatzierungen von Songs für 76 Wochen (jeweils in separaten Spalten).
  • Angenommen wir wollen die Wochen nicht nebeneinander (wide) sondern untereinander (long) haben?
  • Das aktuell beste Tool dafür ist pivot_longer (Gegenstück ist pivot_wider) aus Package tidyr.
library(tidyr)
billboard

# reshapen von wide nach long:
billboard |> 
  pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    values_to = "rank",
    values_drop_na = TRUE
  )

# diese Darstellung macht mehr Sinn, wenn wir Auswertungen der Chartplatzierungen machen wollen.

# Ein bisschen Finetuning:
billboard |> 
  pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    names_prefix = "wk",
    names_transform = as.integer,
    values_to = "rank",
    values_drop_na = TRUE,
  )
us_rent_income

# Daten sind nicht tidy weil estimate und moe Informationen zu verschiedenen Attributen (income und rent) mischen!

us_rent_income |> 
  pivot_wider(
    names_from = variable, 
    values_from = c(estimate, moe)
  )

# beide Variablen werden aufgesplittet in Attribute bzgl income und rent.

16.4.3 Übung: Tidy data (interaktiv)

  1. Laden Sie die Daten http://www.farys.org/daten/satisfaction.csv. Es handelt sich um erfundene Daten die das Alter (age) und die Zufriedenheit (c44) von 5 Personen über die Jahre 2012 bis 2014 enthalten. Sie sehen: es gibt für jedes Beobachtungsjahr und jede Beobachtungsdimension (Alter und Zufriedenheit) eine Variable. Die Information “Beobachtungsjahr” steckt implizit in den Variablennamen obwohl das Jahr eine eigene Variable sein sollte.
  2. Erstellen sie einen “tidy” Datensatz im long-Format. Dieser soll vier Spalten haben - ID, Jahr, Alter, Zufriedenheit - und eine Zeile für jede Messung einer Person in einem Jahr.

Das fertige Ergebnis soll so aussehen:

> data.wide
# A tibble: 15 x 4
   idpers  jahr zufriedenheit alter
    <int> <dbl>         <int> <int>
 1      1  2012             5    45
 2      1  2013             5    46
 3      1  2014             6    47
 4      2  2012             6    57
 5      2  2013             9    58
 6      2  2014             9    59

Tipps:

  • Erstellen sie zuerst einen “very long” Datensatz mit drei Variablen (idpers, name und value; z.b. mit pivot_longer).
  • Erstellen sie eine Variable “Dimension”, die “Alter” oder “Zufriedenheit” als Werte haben kann.
  • Hierfür können Sie z.B. ifelse plus grepl() verwenden oder auch andere Tools aus der Regex-Werkzeugkiste.
  • Erstellen Sie eine Variable “jahr”, z.B. durch Extrahieren der Jahresinformation mit substr() oder anderen Werkzeugen.
  • Überführen Sie zuletzt den Datensatz in ein tidy-Format (für jede Dimension eine Spalte, mit pivot_wider).

16.4.4 Lösung: Tidy data

data <- read.csv("http://www.farys.org/daten/satisfaction.csv")
library(tidyr)

# Daten von wide nach long
data.long <- pivot_longer(data, cols = p12c44:age14)

# Auf welche Messdimension bezieht sich der Messwert?
data.long$dimension <- ifelse( # weise der Spalte dimension einen von zwei Werten zu, je nachdem ob folgende Bedingung erfüllt ist:
  grepl("c44", data.long$name), # Wenn die Spalte mit den ursprünglichen Spaltennamen ein «c44» enthält, 
  "zufriedenheit", # dann handelt es sich um eine Messung der Lebenszufriedenheit, also weise "zufriedenheit" zu, 
  "alter") # wenn nicht, dann weise "alter" zu.

# Auf welches Beobachtungsjahr bezieht sich der Messwert?
data.long$jahr <- ifelse(data.long$dimension == "zufriedenheit", # Hat die Spalte den Wert Lebenszufriedenheit
                    2000 + as.numeric(substr(data.long$name, 2, 3)), # dann ist die Jahresinformation and 2. Bis 3. Stelle
                    2000 + as.numeric(substr(data.long$name, 4, 5))) # sonst (Alter) an 4. Bis 5. Stelle

# Reshape ins wide Format damit es tidy wird:
data.long$name <- NULL # diese Spalte sollte entfernt werden, da für pivot_wide nicht klar ist, was es damit tun soll!
data.wide <- pivot_wider(data.long, names_from = dimension)



# Alternativ (und viel besser): falls die Namen in einem konsistenteren Format wären könnte man sich etwas Arbeit sparen:

# Bauen wir die Namen ein bisschen um:
names(data) <- sub(
  "p([0-9]{2})c44", 
  "satisfaction\\2\\1", 
  names(data))

# danach können wir die Struktur der Daten gut durch ein pattern beschreiben.

pivot_longer(data, cols = satisfaction12:age14, 
             names_pattern = "(.*)([0-9]{2})",
             names_to = c(".value", "year"))

# .value erklärt pivot_longer, dass dieser Teil den gemessenen Wert beschreibt und jeweils die neu entstehende Spaltenbeschriftung sein soll.

16.4.5 Beispiel: Von Messy zu sauber mit diversen Tools

# Wetterdaten
weather <- read.table("https://raw.githubusercontent.com/justmarkham/tidy-data/master/data/weather.txt", header=TRUE)
head(weather) # hier sind Variablen in Zeilen und Spalten

# Daten reshapen von wide nach long
library(tidyr)
weather1 <- pivot_longer(weather, cols = d1:d31, values_drop_na = TRUE)
head(weather1)

# saubere Spalte für "day"
library(stringr)    # für str_replace(), str_sub()
weather1$day <- as.integer(str_replace(weather1$name, "d", ""))

# die krude Spalte "variable" brauchen wir nicht. Löschen durch Zuweisen von NULL.
weather1$name <- NULL

# die Spalte element beherbergt zwei unterschiedliche Variablen tmin und tmax. 
# Diese sollen in zwei Spalten:
weather1$element <- tolower(weather1$element) # Kleinbuchstaben
weather.tidy <- pivot_wider(weather1, names_from = element)
head(weather.tidy)

# das Datum laesst sich zudem in einer Spalte darstellen als echtes Datum:
weather.tidy$date <- as.Date(paste(weather.tidy$year, 
                                   weather.tidy$month, 
                                   weather.tidy$day, sep="-"))
weather.tidy[, c("year", "month", "day")] <- NULL
head(weather.tidy)


# ABER: was für ein Mess mit weather1 weather.tidy etc. Warum nicht Piping verwenden?

weather |>
  pivot_longer(cols = d1:d31, values_drop_na = TRUE) |>
  mutate(day = as.integer(str_replace(name, "d", "")),
         element = tolower(element)) |>
  select(-name) |>
  pivot_wider(names_from = element) |>
  mutate(date = as.Date(paste(year, month, day, sep = "-"))) |>
  select(-year, -month, -day)

16.5 Mergen von Daten

  • Wird in der Praxis sehr häufig gebraucht. Klassisch gibt es merge() (siehe Quick R)
  • Die Join Funktionen in dplyr sind allerdings schneller
  • inner_join(x, y, by = NULL, copy = FALSE, ...) # behält alle Fälle die zwischen den Daten x und y überlappen; häufigster Fall
  • left_join(x, y, by = NULL, copy = FALSE, ...) # behält alle Fälle von x. Missing falls in y nicht vorhanden
  • semi_join(x, y, by = NULL, copy = FALSE, ...) # behält alle Fälle von x und behält nur die Spalten von x
  • anti_join(x, y, by = NULL, copy = FALSE, ...) # alle Fälle, für die es keinen Match gibt
    • x = Datensatz 1, y = Datensatz 2
    • by = “Matchingvariable (z.B. ID, Name, Countrycodes, etc.)”
    • ?join: Das Hilfe File ist sehr zu empfehlen!
  • ebenfalls häufig gebraucht: rbind() um data frames mit gleichen Spalten zeilenweise zusammenzukleben

16.5.1 Beispiel: Mergen von Daten

name <- c("Rudi","Simon","Daniela","Viktor")
geschlecht <- c("Mann", "Mann", "Frau","Mann")
daten1 <- data.frame(name, geschlecht)

name <- c("Johanna","Rudi","Simon","Daniela")
alter <- c(33,32,38,45)
daten2 <- data.frame(name, alter)

daten1
daten2

# klassisch
merge(daten1, daten2, by="name") # entspricht inner_join)

# oder über match()
# daten1$alter <- daten2$alter[match(daten1$name,daten2$name)] # entspricht left_join

# mit dplyr
inner_join(daten1,daten2,by="name")
left_join(daten1,daten2,by="name")
right_join(daten1,daten2,by="name")
full_join(daten1,daten2,by="name")

# mit data.table
# mit data.table
name <- c("Rudi","Simon","Daniela","Viktor")
geschlecht <- c("Mann", "Mann", "Frau","Mann")
daten1 <- data.table(name, geschlecht)

name <- c("Johanna","Rudi","Simon","Daniela")
alter <- c(33,32,38,45)
daten2 <- data.table(name, alter)
setkey(daten1, name)
setkey(daten2, name)

#Inner join
daten1[daten2,nomatch=0]

#Right-join
daten1[daten2]

#Left-join
daten2[daten1]

#Full outer-join
unique_keys <- unique(c(daten1[, name], daten2[, name]))
daten1[daten2[.(unique_keys)]]
#data.table kennt auch eine verbesserte Version von merge
merge(daten1, daten2, all = T)

# Rekodieren mit left_join() plus look-up-Tabellen kann ebenfalls sehr nützlich sein, hier ein Beispiel:

# Mitarbeiterdaten mit Abteilungsnummern
employee_data <- data.frame(
  employee_id = c(101, 102, 103, 104),
  dept_id = c(1, 2, 1, 3)
)

# Look-Up-Tabelle mit Abteilungsnamen
department_lookup <- data.frame(
  dept_id = c(1, 2, 3),
  dept_name = c("Sales", "HR", "IT")
)

# Zuordnung der Abteilungsnummern zu Abteilungsnamen
employee_data_recode <- employee_data |> 
  left_join(department_lookup, by = "dept_id")

employee_data_recode



# rbind / zeilenweise verknüpfen von Daten
name <- c("Rudi","Simon","Daniela","Viktor")
geschlecht <- c("Mann", "Mann", "Frau","Mann")
daten1 <- data.frame(name, geschlecht)

name <- c("Johanna","Ralf")
geschlecht <- c("Frau","Mann")
daten2 <- data.frame(name, geschlecht)

rbind(daten1,daten2)

# rbind() ist sehr langsam. Besser ist rbindlist() aus data.table
rbindlist(list(daten1,daten2))

17 Datenimport und -export

17.1 Häufige Packages und Funktionen zum Datenimport und -export

  • read.table(): Liest Textformate (.txt, .csv)
    • read.table("meinedaten.csv", header = TRUE, sep = ",", dec = ".")
      • header = TRUE: wenn die erste Zeile Spaltennamen enthält
      • sep = ",": Textseparator
      • dec = ".": Dezimalzeichen (Default ist .)
      • Für mehr siehe die Hilfe ?read.table
  • Text (.csv oder .txt) ist der kleinste Gemeinsame Nenner zwischen Softwarepaketen und empfiehlt sich, wenn mit vielen verschiedenen Programmen gearbeitet wird
  • Für sehr grosse Dateien empfiehlt sich die Funktion fread() aus data.table (Geschwindigkeit ca. Faktor 10)
  • Oft liegen andere Formate vor weil primär eine bestimmte Software eingesetzt wird, z.B. STATA, SPSS, SAS
    • foreign: kann enbtsprechende Formate lesen, z.B. (.dta, .por)
    • haven: neueres Package, das versucht foreign abzulösen
  • Ebenfalls verbreitet ist xls/xlsx. Hier gibt es mehrere Möglichkeiten, die sinnigsten sind:
    • read_excel() aus Package readxl: baut auf C. Ebenfalls sehr unproblematisch und versteht exceltypische Sheet-/Zellennotation.
    • read.xlsx() aus Package openxlsx (keine Abhängigkeiten jenseits von R, liest aber keine alten .xls Files, nur xlsx)
    • read.xls() aus Package gdata, braucht allerdings eine Perl Installation
    • read.xlsx() und read.xlsx2() aus Package xlsx; kann auch bestimmte Zeilen-/Spaltenbereiche auslesen statt komplette Sheets, benötigt jedoch Java; ggf. Java installieren und sicherstellen, dass R und Java dieselbe Architektur (32 oder 64bit) haben. Für Probleme bei Mac-Usern: http://stackoverflow.com/questions/28796108/r-rstudio-yosemite-and-java
  • Daten exportieren
    • Idealerweise als Text
    • write.table(swiss, "swiss.txt", sep=",") speichert das Objekt swiss kommasepariert nach swiss.txt
    • Siehe ?write.table für weitere Optionen (z.B. Zeilennamen, Separator, Dezimalzeichen)
    • Für eine gute Übersicht über andere Formate siehe Quick R
  • Für grössere Datenmengen interessant:
    • read_fst() und write_fst() aus Package fst: ist ein Format, in dem sich grosse Datenmengen effizient abspeichern und daraus einlesen lassen.
    • metadata_fst() aus demselben Package: gibt Meta-Informationen (Anzahl Zeilen und Spalten, sowie Namen und Formate der Variablen) von FST-Dateien wieder, ohne dass diese zuvor in R geladen werden müssen.

17.2 Übung: Datenimport und -export / Mergen / Tidy Data

  1. Beim BfS gibt es Daten zur ständigen Wohnbevölkerung der Schweizer Kantone. Lesen Sie Daten über die Population (Spalte B) der 26 Schweizer Kantone (Spalte A) ein. Diese Befinden sich in Sheet “2023” des Excel-Sheets “Bilanz der ständigen Wohnbevölkerung nach Kantonen” (ggf. nur relevante Zeilen auswählen!). Hinweis: Einlesen direkt via URL ist nicht möglich, da xlsx kein Plaintext ist sondern ein “binary zip”. Laden Sie die Datei entweder manuell herunter oder (besser) mit Hilfe von download.file(..., mode="wb") (write binary mode). Hinweis 2: Achten Sie auf das File-Encoding der Daten.
  2. Gehen Sie wie in 1. vor und besorgen Sie von der Website des BfS zum Thema Forstwirtschaft das Excel Sheet “Waldflächen nach Kantonen” und lesen Sie Kantonsnamen und die Gesamtwaldfläche in Hektar ein. Hinweis: Bei den Daten wurde eine konfuse Anzahl Leerzeichen hinter die Kantonsnamen gehängt (read_excel trimmt diese automatisch weg, read.xlsx jedoch nicht). Säubern Sie die Daten, damit die Kantonsnamen mit denen aus 1. korrespondiert.
  3. Verknüpfen (mergen) Sie die beiden Datensätze über die Kantonsnamen. Können alle Kantone gematcht werden?
  4. Berechnen Sie für alle Kantone die Anzahl Bäume pro Person (1 Hektar = 400 Bäume) und geben Sie eine Rangliste aus (z.B. mit arrange() aus dplyr).
  5. Speichern Sie diese Rangliste als csv (z.B. mit write.table()).

Tipps: Schauen Sie in die Hilfe von read_excel(). Hilfreiche Argumente, die hier spezifiziert werden können sind z.B. range, sheet und col_names.

17.3 Lösung: Datenimport und -export / Mergen / Tidy Data

# 1.
library(readxl)

# Datei runterladen als binary
download.file("https://dam-api.bfs.admin.ch/hub/api/dam/assets/32229209/master", 
              "bevoelkerung.xlsx", 
              mode="wb")

# einlesen als UTF-8 kodiertes File. Sheet 2014, Zeile 8-33, 
# ohne Header (den setzen wir besser manuell). über colIndex 
# nehmen wir nur die Spalten 1 (A) und 2 (B)


bevoelkerung <- read_excel("bevoelkerung.xlsx", sheet = "2023", range = "A6:B31", col_names = c("Kanton", "Einwohner"))


# 2.
download.file("https://dam-api.bfs.admin.ch/hub/api/dam/assets/32088900/master",
              "wald.xlsx", 
              mode="wb")

wald <- read_excel("wald.xlsx", sheet = 1, range = "A6:C35", col_names = c("Kanton", "Eigentuemer", "Waldflaeche"))[, c(1, 3)]


# säubern (entfällt bei readxl im Vergleich zu read.xlsx)
library(stringr)
wald$Kanton <- str_trim(wald$Kanton, side="both")

# 3. / 4.
# z.B. über dplyr
library(dplyr)
left_join(bevoelkerung, wald, by = "Kanton")

# St.Gallen und die beiden Appenzells werden nicht gematcht 
# da die Schreibweise minimal anders ist, fix:

wald$Kanton <- str_replace(wald$Kanton, pattern = "\\. ", replacement = ".") 

# der Punkt wird hier escaped, da es sich normal um einen
# Platzhalter für reguläre Ausdrücke handelt
# besser ist normalerweise über das Kantonskürzel oder
# Nummer zu matchen, dann stört die unterschiedliche Schreibweise nicht.

waldranking <- left_join(bevoelkerung,wald,by="Kanton") |>
  mutate(bpp = Waldflaeche*400/Einwohner) |>
  arrange(-bpp)

waldranking

# 5. 
write.csv(waldranking, "waldranking.csv")

17.4 Daten aus Datenbanken lesen

  • relevante Packages: RODBC, RJDBC, RMySQL, RPostgreSQL, DBI
  • ein kleines Beispiel:
library(DBI)
library(RMySQL)

# Verbindung definieren
con <-  dbConnect(MySQL(), 
                  username = "dataviz", 
                  password = "CASdataviz2016",
                  host = "db4free.net", 
                  port = 3306,
                  dbname = "cas_dataviz"
)

# Get-Anfrage schicken
dbGetQuery(con, "show databases")

# neue Tabelle erstellen
dbSendQuery(con, "CREATE TABLE anmeldungen (id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY, 
  vorname VARCHAR(30) NOT NULL,
  nachname VARCHAR(30) NOT NULL,
  email VARCHAR(50),
  reg_date TIMESTAMP)"
) 

# schauen welche Tabellen es gibt
dbGetQuery(con, "show tables")

#   Tables_in_cas_dataviz
# 1           anmeldungen

# Alle Zeilen der Tabelle löschen
# dbSendQuery(con, "DELETE from anmeldungen")

dbSendQuery(con, "INSERT into anmeldungen (vorname, nachname) values ('Rudi', 'Farys')")
dbSendQuery(con, "INSERT into anmeldungen (vorname, nachname,email) values ('Test', 'Test','test@test.de')")

# einfache Abfrage
daten <- data.frame(dbGetQuery(con, "SELECT * FROM anmeldungen"))

daten

dbDisconnect(con)

18 Webscraping

18.1 HTML/XML data

18.1.1 Beispiel: HTML Tabelle einlesen

library(rvest)
raw <- read_html("https://en.wikipedia.org/wiki/Cantons_of_Switzerland")
table <- html_table(raw, header = TRUE)[[3]]
head(table)

18.2 CSS Selectors für komplexere HTML-Strukturen

  • CSS Selektoren sind eine Syntax zur Definition und Navigation durch HTML/XML-Dokumente.
  • Bezugnahme auf Elemente (z.B. einer Website oder eines XML-Dokuments) durch deren Pfad, Position oder Bedingungen.
  • Wenn Sie Informationen von einer Website extrahieren: Element markieren, mit der rechten Maustaste klicken und “Inspizieren” / “Element prüfen” auswählen oder ein entsprechendes Browser-Plugin verwenden.
  • Paket: am besten rvest, das eine Reihe von Funktionen zum komfortablen Extrahieren von Elementen enthält, z.B. html_table, html_node, html_attr und andere.
  • Das Lesen der Website in R erfolgt in der Regel durch read_html() von xml2, auf dem rvest aufbaut.

18.2.1 Beispiel: CSS Selectors

library(rvest)
# Betrachten Sie folgende Websites
# https://www.transfermarkt.de/fill/profil/spieler/28003 and
# https://www.transfermarkt.de/fill/profil/spieler/401173

# Beachten Sie, wie es möglich ist, durch die Spielerprofile zu navigieren, wenn wir eine Liste von Spieler-IDs verwenden, z.B.
listofplayers <- c(28003, 401173)

for(playerid in listofplayers) {
  url <- paste0("https://www.transfermarkt.de/fill/profil/spieler/", playerid)
  raw <- read_html(url)
  ...
}

# oder noch performanter mit einer Funktion.
# nehmen wir an, dass wir Name und Marktwert des Spielers extrahieren wollen.

# Die Untersuchung des Marktwertelements zeigt, dass wir einen Anker (<a>) in einem <div> Container haben, der die Klasse "dataMarktwert" hat, die wahrscheinlich verwendbar ist. Der Text innerhalb von <a> ist das, was wir wollen:

raw <- read_html("https://www.transfermarkt.de/fill/profil/spieler/28003")
rawelement <- html_node(raw, ".data-header__market-value-wrapper") |>
  html_text()
rawelement

# was wir dann noch säubern müssen. Z.B.:
library(stringr)
rawelement |> 
  str_extract("[0-9]+,[0-9]{2}") |> 
  str_replace(",", ".") |>
  as.numeric()

# Name des Spielers:
# Es gibt viele Möglichkeiten, aber z.B. Quellcode anschauen und nach dem Spielernamen suchen. Wählen Sie einen geeigneten Eintrag aus, den Sie mit XPath gut greifen können. Z.B. das Titel-Tag:

raw |>
  html_node("title") |>
  html_text() |> 
  str_extract("^[^-]*") |> # nur den Teil vor "-"
  trimws()

# ein anderer Weg über ein Attribut aus einem meta-Tag:
raw |>
  html_node("meta[name = 'description']") |> 
  html_attr("content") |>
  str_extract("^[^,]*")

# Das ganze können wir jetzt in eine Funktion stecken und damit zahlreiche Profile scrapen

getPlayerStats <- function(playerid) {
  
  print(paste("Scraping player:", playerid))
  
  url <- paste0("https://www.transfermarkt.de/fill/profil/spieler/", playerid)
  raw <- read_html(url)
  
  marketvalue <- raw |>
    html_node(".data-header__market-value-wrapper") |>
    html_text() |>
    str_extract("[0-9]+,[0-9]{2}") |>
    str_replace(",", ".") |>
    as.numeric()
  
  name <- raw |>
    html_node("title") |>
    html_text() |> 
    str_extract("^[^-]*") |> 
    trimws()
  
  # Sys.sleep(10) # falls wir viele Scrapen wollen, ist es gut, eine Pause einzubauen, damit der Server nicht automatisiert blockt
  
  data.frame("id" = playerid, "name" = name, "marketvalue" = marketvalue)
}

# Messi example
getPlayerStats(28003)

# now for a list of IDs
listofplayers <- c(28003, 401173)
do.call(rbind, lapply(listofplayers, getPlayerStats))

# How to get a list of players?
# e.g.:
swiss_team_page <- read_html("https://www.transfermarkt.com/schweiz/startseite/verein/3384")

# get all player IDs
swiss_players <- swiss_team_page |>
  html_nodes("td.hauptlink a") |> 
  html_attr("href") |> 
  str_extract("[0-9]{4,8}") |>
  unique()

# swiss_team <- do.call(rbind, lapply(swiss_players, getPlayerStats)) # -> machen wir besser nicht ohne vorher Pausen einzubauen

# Stand 10/2024
#        id                name marketvalue
# 1  257814        Gregor Kobel        40.0
# 2  251322        Philipp Köhn         5.0
# 3  147051          Yvon Mvogo         3.0
# 4  284730       Manuel Akanji        45.0
# 5  192635         Nico Elvedi        10.0
# 6  382478      Cédric Zesiger         5.0
# 7  298583         Eray Cömert         4.0
# 8  192616      Ulisses Garcia         4.0
# 9   86784   Ricardo Rodríguez         3.5
# 10 168989       Silvan Widmer         2.5
# 11 111455        Granit Xhaka        20.0
# 12 247555 Edimilson Fernandes         3.5
# 13 237658    Michel Aebischer        11.0
# 14 148252        Remo Freuler         6.5
# 15 280387      Vincent Sierro         4.0
# 16 410787       Joël Monteiro         5.0
# 17 394236       Filip Ugrinic         7.0
# 18 507490    Christian Witzig         2.0
# 19 507341       Fabian Rieder         8.0
# 20 365108           Dan Ndoye        22.0
# 21 237662        Breel Embolo        12.0
# 22 548729        Zeki Amdouni        12.0
# 23 345468         Andi Zeqiri         3.5

18.2.2 Übung: CSS Selectors

  • Werfen Sie einen Blick auf die Website https://www.ariva.de/DE0007100000
  • Wie Sie sehen können, können Sie als Teil der URL die ISIN (International Securities Identification Number) eingeben, um die Aktienkurse und andere Informationen über eine Aktie zu erhalten.
  • Schreiben Sie eine Funktion, die den aktuellen Kurs einer Aktie über ihre ISIN extrahiert.
  • Hinweise: Schauen Sie sich den HTML-Quellcode an, suchen Sie nach dem Preis und analysieren Sie die Struktur des Elements. Welche CSS-Selektoren können Sie anwenden, um den Preis zu extrahieren?
  • Bonus: Stellen Sie auch sicher, dass die Ausgabe Ihrer Funktion numerisch ist.

18.2.3 Lösung: CSS Selectors

library(dplyr)
library(rvest)

arivaQuote <- function(isin) {
  url <- paste0("https://ariva.de/", isin)
  qt <- read_html(url) |>
    html_node(".instrument-header-quote") |>
    html_text() |> 
    str_extract("[0-9]+,[0-9]{2}") |>
    (\(x) sub(",", ".", x))() |> # anonyme Funktion \(x)
    as.numeric()
  return(qt)
}

arivaQuote("DE0005190003") # BMW

# auch noch lustig: obwohl die Funktion nur 1 URL aufs mal verarbeiten kann, könnten wir trotzdem eine vektorisierte Version bauen:

arivaQuote <- Vectorize(arivaQuote)
isins <- c("DE0005190003", "US88160R1014") # BMW und Tesla

mydf <- data.frame(isins)
mydf$quote <- arivaQuote(mydf$isins)

18.3 Simulation eines Webbenutzers für sehr haarige Fälle

  • Webscraping wird von Tag zu Tag schwieriger, hier sind einige Gründe:
    • viele Websites verwenden AJAX, welches Daten erst dann in das Layout der Website lädt, nachdem der Quellcode geladen wurde. Das Lesen des Quellcodes ergibt daher nur eine leere Struktur einer Website ohne Daten.
    • Möglicherweise müssen Sie sich bei einer Website anmelden.
    • Datenbesitzer wollen ihre Daten ggf. monetarisieren und wollen daher nicht, dass gescrapt wird.
  • RSelenium ist eine Lösung für einige dieser Probleme, da man damit einen Browser-Benutzer simulieren kann. Auf diese Weise können Sie z.B:
    • eine Login-Seite überwinden und Cookies, Session-IDs, die benötigt werden, verwenden.
    • Daten laden, die nicht im reinen Quellcode vorhanden sind (z.B. wenn Daten beim Scrollen dynamisch geladen werden).
  • Bevor Sie Websites scrapen, die schwer zu scrapen sind, kontaktieren Sie am besten den Website-Besitzer und bitten diesen um Unterstützung. Oftmals bekommt man dieselben Daten dadurch bereits in maschinenlesbarer Form.
  • RSelenium zu zeigen, geht über den Rahmen dieses Kurses hinaus, aber wenn nötig, können wir Ihnen Beispielcode zur Verfügung stellen.

19 APIs - Application programming interface

19.1 Beispiel: World Bank Indicators

19.2 JSON-Daten

  • JSON (java script object notation) ist das populärste Datenformat für den Datenaustausch über APIs
  • In R gibt es mehrere Packages hierfür. Das einschlägigste Package ist jsonlite.
library(jsonlite)

# Mini Beispiel aus dem Package
json <-
'[
  {"Name" : "Mario", "Age" : 32, "Occupation" : "Plumber"}, 
  {"Name" : "Peach", "Age" : 21, "Occupation" : "Princess"},
  {},
  {"Name" : "Bowser", "Occupation" : "Koopa"}
]'
mydf <- fromJSON(json)
mydf


# editieren wir die Daten ein bisschen
mydf$Ranking <- c(3, 1, 2, 4)

# und wandeln es zurück nach JSON
toJSON(mydf, pretty=TRUE)
  • Wie könnte das ganze für das Weltbank Beispiel aussehen?
# Indicator: NY.GDP.PCAP.PP.CD (GDP per capita PPP)
url <- "http://api.worldbank.org/v2/country/all/indicator/NY.GDP.PCAP.PP.CD?date=2021&format=json&per_page=10000"

# was für Teile gibt es hier in der URL?

baseurl <- "http://api.worldbank.org"
endpoint <- "/v2/country/all/indicator/NY.GDP.PCAP.PP.CD"
parameters <- "?date=2021&format=json&per_page=10000"
url <- paste0(baseurl, endpoint, parameters)

library(jsonlite)
res <- fromJSON(url)

str(res)
class(res[[2]])
names(res[[2]])

library(dplyr)
res[[2]] |> 
  select(country, gdp = value) |> 
  arrange(-gdp) |> 
  head(10)

19.3 XML-Daten

  • Grundsätzlich das gleiche Konzept wie JSON, jedoch mit unterschiedlichem Markup.
# XML mini Beispiel
library(XML)
download.file("https://www.w3schools.com/xml/simple.xml", "simple.xml")
# Leider erfordern die meisten Seiten, dass Sie sie zuerst herunterladen, da xmlParse() https nicht unterstützt.
roh <- xmlParse("simple.xml")
liste <- xmlToList(roh)
df <- xmlToDataFrame(roh)

19.4 APIs, die bereits in einem R-Paket verpackt sind

  • Einige Pakete wurden bereits für R geschrieben, die es einfacher machen, mit einzelnen APIs zu arbeiten.

  • Für die eben genannten World development indicators (Weltbank) gibt es ein Package WDI, aber auch für Daten der OECD, IWF-Daten, Börsendaten (quantmod und andere), twitteR, rfacebook, quandl, tidycensus, gapminder, Rspotify uvvm.

# install.packages("WDI") 
library(WDI)
#WDIsearch(string="gdp", field="name", cache=NULL)
DF <- WDI(country="all", 
          indicator="NY.GDP.PCAP.PP.CD", # Pro-Kopf-BIP kaufkraftbereinigt
          start=2010, end=2024) 

# Welche sind die reichsten Länder im Jahr 2010?
library(dplyr)
filter(DF, year==2010) |>
arrange(-NY.GDP.PCAP.PP.CD) |>
  head()

# Und wie sieht es 2023 aus?
filter(DF, year==2023) |>
arrange(-NY.GDP.PCAP.PP.CD) |>
  head()

# Was passiert da im Hintergrund?

# ?WDI
# ?wdi.query

country = "all"
indicator = "NY.GDP.PCAP.PP.CD"
extra = FALSE
cache = NULL
latest = NULL
language = "en"
years <- NULL


url <- paste0("https://api.worldbank.org/v2/", language, 
       "/country/", country, "/indicator/", indicator, "?format=json", 
       years, "&per_page=32500", "&page=", 1:10, latest)

url

19.5 Übung APIs

  • Schauen Sie sich die Dokumentation zu SWAPI (The Star Wars API) an: https://swapi-deno.azurewebsites.net/
  • Machen Sie eine API-Abfrage (json-Format) um Daten für alle Figuren (Endpoint /api/people) herunter zu laden.
  • Bonus (freiwillig): Finden Sie heraus, welches Terrain der Heimatplanet jedes Charakters hat. Dafür müssen Sie eine weitere API-Abfrage machen (Endpoint /api/planets) und die Daten zusammenführen.

19.6 Lösung APIs

library(jsonlite)

starwars_people <- fromJSON("https://swapi-deno.azurewebsites.net/api/people")
starwars_planets <- fromJSON("https://swapi-deno.azurewebsites.net/api/planets")

starwars_people$homeworld_terrain <- starwars_planets$terrain[match(starwars_people$homeworld, starwars_planets$url)]

Was sonst noch wichtig wäre

  • API Keys
  • Rate limits
  • Pagination

Andere interessante Beispiele

  • Twitter API
  • Google Maps API
  • Openweathermap API
  • Openstreetmap API
  • OpenAI
  • uvvm.

19.6.1 Mit APIs Daten bearbeiten

  • APIs sind nicht nur zum Herunterladen von Daten da, sondern auch zum Hochladen und Bearbeiten von Daten.
  • Ein Beispiel: Die NamSor API, die Namen in ethnische Herkunft und Geschlecht umwandelt.
# Namsor Documentation
# https://namsor.app/api-documentation/

library(httr)
api_key <- "0173ee5ae9184eda7203f90ff9d528aa" # bitte nicht missbrauchen

df <- data.frame(
  "name" = c("Rudolf Farys", "Chandrika Kumaratunga", "Adam Smith"),
  "age" = c(40, 60, 75)
)


getGender <- function(name, api_key) {
  
  # URL zusammenbauen je nach gewuenschtem endpoint
  baseurl <- "https://v2.namsor.com/NamSorAPIv2/api2/json/"
  endpoint <- "gender/"
  name <- URLencode(sub(" ", "/", name))
  url <- paste0(baseurl, endpoint, name)
  
  # Headers definieren (z.B. API Key zur Authentifizierung)
  headers <- add_headers("X-API-KEY" = api_key)
  
  # API call ausfuehren, Rueckgabe speichern
  response <- GET(url, headers)
  
  # Pruefen ob es funktioniert hat (Code 200)
  if (status_code(response) == 200) {
    # json lesen und in data frame umwandeln
    content <- fromJSON(rawToChar(response$content))
    
    # den interessanten Teil der Antwort ausgeben
    return(content$likelyGender)
  } else {
    print(paste("Request failed with status", status_code(response)))
    return(NA)
  }
}

# Funktion auf die Liste der Namen anwenden
df$gender <- sapply(df$name, getGender, api_key = api_key)


# Dasselbe Spiel fuer die Herkunft (leider nicht ganz so straightforward, da fuer diesen endpoint gleich mehrere Namen verarbeitet werden)

library(httr)
library(jsonlite)
library(dplyr)
library(tidyr)

getOrigin <- function(df, name_col = "name", api_key) {
  
  # Prepare the JSON body
  df <- df |> mutate(id = row_number()) 
  json_data <- toJSON(list(personalNames = select(df, id, !!sym(name_col))))
  
  # NamSor API endpoint for batch processing
  url <- "https://v2.namsor.com/NamSorAPIv2/api2/json/originFullBatch"
  
  # Set up the HTTP header with your API Key and specify content type as JSON
  headers <- add_headers("X-API-KEY" = api_key, "Content-Type" = "application/json")
  
  # Make the POST request with the data
  response <- POST(url, headers, body = json_data, encode = "json")
  
  # Check if the request was successful
  if (status_code(response) == 200) {
    # Parse the JSON response
    content <- fromJSON(rawToChar(response$content))
    
    return(content$personalNames)
  } else {
    print(paste("Request failed with status", status_code(response)))
    return(NA)
  }
}

origins <- getOrigin(df, "name", api_key = api_key)
df$origin <- origins$countryOrigin
df

20 Export von Tabellen

20.1 Beispiel: Export von Tabellen

library(huxtable) # ggf. Installation von officer und flextable erforderlich
library(dplyr)
library(tidyr)

# Beispiel für Formatierung

mtcars |>
  group_by(Zylinder = cyl, Gaenge = gear) |>
  summarise(PS = round(mean(hp)),
            "Sek. 1/4 Meile" = round(mean(qsec))) |>
  arrange(Zylinder, Gaenge) |>
  as_hux(add_colnames = TRUE) |>
  set_bold(1, 1:4, TRUE) |>
  set_bottom_border(1, 1:4, TRUE) |>
  set_left_border(everywhere,3, TRUE) |>
  set_bold(1:9, 1:2 ,TRUE) |>
  merge_cells(2:4, 1) |>
  merge_cells(5:7, 1) |>
  merge_cells(8:9, 1) |>
  map_text_color(everywhere, 3:4, 
                 by_quantiles(c(0.2, 0.8), c("blue", "darkgreen", "red"))) |>
  quick_docx(file = "Test_format.docx")


# Beispiel für Summary Tabelle
tabelle <- read.csv("http://farys.org/daten/Prestige.csv") |>
  select(education, income) |>
  #Kann beliebig ergänzt werden
  summarise_all(list(Minimum = min,
                     Median = median,
                     Mittelwert = mean,
                     Maximum = max,
                     Fallzahl = ~sum(!is.na(.))), na.rm = TRUE) |>
  pivot_longer(everything()) |>
  mutate(Dimension = gsub("_.*", "", name),
         Mass = gsub(".*_", "", name),
         value = as.character(round(value))) |>
  select(-name) |>
  pivot_wider(names_from = Mass, values_from = value) |>
  as_hux(add_colnames = T) |>
  set_bold(1,everywhere,TRUE)

# Schöne Spaltennamen
tabelle[, 1] <- c("", "Bildung (in Jahren)", "Einkommen")
quick_docx(tabelle, file = "Test_summary.docx")



# Regressionstabellen

x1 <- rnorm(100)
x2 <- rnorm(100)
y <- rnorm(100) + 2*x1 + 1*x2

fit1 <- lm(y~x1)
fit2 <- lm(y~x1+x2)

huxreg(fit1, fit2)

#https://cran.r-project.org/web/packages/huxtable/vignettes/huxreg.html für Formatierungsmöglichkeiten

20.2 Übung: Export von Tabellen

  1. Öffnen Sie die Daten http://www.farys.org/daten/bmi.dta. Diese enthalten (unter anderem) Alter, Geschlecht und den BMI (Body-Mass-Index) aus einer Befragung.

  2. Erzeugen Sie eine Summary-Statistik für die Variablen BMI, Alter und Einkommen. Nutzen Sie am besten das Beispiel als Template und passen Sie dieses an.

  3. Hübschen Sie die Tabelle noch etwas auf, indem Sie die Zeilen besser beschriften.

  4. Exportieren Sie die Tabelle als Word/HTML, Latex oder Ascii

  5. Erzeugen/exportieren Sie eine Tabelle, die alle Variablen der fünf höchsten und der fünf niedrigsten BMI-Werte zeigt.

20.3 Lösung: Export von Tabellen

# 1.
library(foreign)
bmi <- read.dta("http://www.farys.org/daten/bmi.dta")

# 2./3./4.
library(dplyr)

tabelle <- bmi |>
  select(bmi, alter, einkommen) |>
  #Kann beliebig ergänzt werden
  summarise_all(list(Minimum = min,
                     Median = median,
                     Mittelwert = mean,
                     Maximum = max,
                     Fallzahl = ~sum(!is.na(.))), na.rm = TRUE) |>
  pivot_longer(everything()) |>
  mutate(Dimension = gsub("_.*", "", name),
         Mass = gsub(".*_", "", name),
         value = as.character(round(value))) |>
  select(-name) |>
  pivot_wider(names_from = Mass, values_from = value) |>
  as_hux(add_colnames = T) |>
  set_bold(1,everywhere,TRUE)

#Schöne Namensgebung der Spalten
tabelle[, 1] <- c("Dimension", "Body-Mass-Index", "Alter", "Einkommen")
tabelle

quick_docx(tabelle, file = "summarytab.docx")


# 5. Es gibt viele Wege, Vorschlag:
topbottom <- rbind(arrange(bmi, -bmi) |> head(n = 5), 
      arrange(bmi, bmi) |> head(n = 5))

tabelle <- topbottom |>
  as_hux(add_colnames = T ) |>
  set_bold(1, 1:ncol(topbottom), TRUE) |>
  set_bottom_border(1, 1:ncol(topbottom), TRUE)
tabelle[1, ] <- c("Body-Mass-Index","Alter","Geschlecht","Einkommen","Bildungsjahre","ID")
tabelle

# siehe ggf. auch https://hughjonesd.github.io/huxtable/reference/number_format.html für Number formatting

21 Exkurs: Workflow

22 Lineare Regression

22.1 Beispiel: Lineare Regression

  • Daten über das Prestige von Berufen (Untersuchungseinheiten sind Berufe, nicht Individuen)
i prestige \(= y_{i}\) education \(= X_{i2}\) income \(= X_{i3}\)
1 gov.administrators 68.8 13.11 12351
2 general.managers 69.1 12.26 25879
3 accountants 63.4 12.77 9271
4 purchasing.officers 56.8 11.42 8865
5 chemists 73.5 14.62 8403
6 physicists 77.6 15.64 11030
7 biologists 72.6 15.09 8258
8 architects 78.1 15.44 14163
9 civil.engineers 73.1 14.52 11377
10 computer.programers 53.8 13.83 8425
11 economists 62.2 14.44 8049
  • Abhängige Variable: Pineo-Porter-Prestige für den jeweiligen Beruf (Werte stammen aus Befragungen)
  • Forschungsfrage: Haben Einkommen und Bildung einen Einfluss auf das Berufsprestige?
  • Erklärende Variablen
    • Education: durchschnittliche Anzahl Bildungsjahre der Leute im jeweiligen Berufsfeld
    • Income: durchschnittlicher Lohn im Berufsfeld
  • Datensatz heisst Prestige und befindet sich im Package car (siehe ?Prestige) (“Companion to Applied Regression” - John Fox)
# Berufliches Prestige und Bildung bzw. Einkommen

# Paket "car" laden bzw. installieren, da dort der 
# Beispieldatensatz "Prestige" enthalten ist

install.packages("car")
library(car) 

# alternativ, falls car Probleme mit Abhaenigkeiten macht: 
# Prestige <- read.csv("http://farys.org/daten/Prestige.csv")

# Für folgendes Beispiel brauchen wir jedoch die Funktion scatter3d() aus car. 
# Das Dependency Problem lässt sich ggf. so lösen:
# install.packages("lme4") # dependency für altes pbkrtest
# packageurl <- "https://cran.r-project.org/src/contrib/Archive/pbkrtest/pbkrtest_0.4-4.tar.gz"
# install.packages(packageurl, repos=NULL, type="source") # von hand installieren
# install.packages("car") # jetzt car installieren mit den 
# manuell installierten dependencies
# library(car)

# Wie kann man sich eine Regression mit zwei erklärenden Variablen vorstellen?
# Als Ebene durch eine 3d Punktewolke!

scatter3d(Prestige$income,Prestige$prestige,Prestige$education, fit="linear") 
  
# Ein kleines Modell schätzen:
fit <- lm(prestige ~ education + income, data=Prestige)

# summary() ist eine generische Funktion. Für ein lm() Objekt wird 
# ein typischer Überblick über Koeffizienten und Modellgüte gegeben
  
summary(fit)

# Was steckt im fit Objekt?
names(fit)

# Zugriff auf Koeffizienten und andere Bestandteile
fit$fitted.values # Vorhersage (y-Dach)
fit$residuals     # Residuen = Fehler, d.h. die Abweichung der beobachteten Werte 
                    # von der Modellvorhersage
coef(fit)         # Koeffizienten
fit$coefficients

23 Andere wichtige Packages/Funktionen für statistische Modelle und mehr

23.1 OLS Diagnostik - Annahmen testen

  • Ausreisseranalyse: avPlots() aus Package car
  • Linearität: component plus residual plot: crPlots() aus car
  • Unabhängigkeit der Residuen (Autokorrelation):
    • (partielle) Autokorrelationsfunktion: acf(), pacf()
    • Q-Test/Ljung-Box-Test: Box.test()
  • Homoskedastizität
    • ncv.test(fit) und spread.level.plot(fit) aus car
    • robuste Standardfehler lassen sich mit Funktionen aus dem Package sandwich berechnen.

23.2 Zeitreihenanalyse

# install.packages("quantmod")
library(quantmod)
getSymbols("^SSMI")
head(SSMI)

# install.packages("forecast")
library(forecast)

fit <- auto.arima(to.monthly(SSMI)[,"SSMI.Adjusted"])

plot(forecast(fit,h=6))

23.3 Data Mining

23.5 Kurze Umfrage zum Wahltag

24 Take-Home-Exam:

24.1 Teil 1: Auswertungen von Daten der IMDb

IMDb gibt für nicht-kommerzielle Zwecke Daten über Filme und Schauspieler heraus:

https://datasets.imdbws.com/

Im Folgenden analysieren Sie diese Daten. Die Daten sind in mehreren Teildatensätzen organisiert, u.a.:

  • title.basics.tsv.gz: Titel, Genre, Title: Dataset on Titles, Genres und andere Stammdaten von Filmen
  • title.crew.tsv.gz: Daten zu Regisseuren und Drehbuchautoren
  • title.ratings.tsv.gz: User-Ratings
  • name.basics.tsv.gz: Namen von Schauspielern und anderen relevanten Personen

Im Detail sind die Daten in der Dokumentation beschrieben: https://developer.imdb.com/non-commercial-datasets/

  1. Beschaffen Sie diese Datensätze und lesen Sie sie in R ein (Tipp: mit fread() aus data.table geht das Einlesen verlässlicher und schneller). Falls das Einlesen nicht klappen sollte, können Sie mit folgendem Code die 4 Datensätze laden. Achtung: da es sich um grössere Datenmengen handelt kann das Einlesen einige Sekunden dauern und die Daten werden ca. 6-7 GB im Memory benötigen.
# Notfall-Code zum Einlesen
load(url("http://www.farys.org/daten/imdb.RData"))
  1. Reduzieren Sie den “Basics” Datensatz auf Filme mit einer Laufzeit von mindestens einer Stunde.

  2. Wie haben sich Filmgenres über die Dekaden entwickelt? Gruppieren Sie hierfür die Daten nach Dekade und Genre und zählen Sie die Häufigkeit von Filmen pro Gruppe. Betrachten Sie die aggregierten Daten (optional auch mit einer Grafik). Welche Trends und Auffälligkeiten lassen sich erkennen?

  3. Welche Genres haben im Durchschnitt die beste Bewertung?

  4. Wer war/ist der bedeutendste Filmregisseur? Verknüpfen Sie hierzu den Datensatz zusätzlich mit dem “Crew” Datensatz. Was könnten geeignete Kennzahlen dafür sein?

  5. Erdenken Sie eine weitere interessante Analyse (etwa im gleichen Umfang wie eine der vorherigen Aufgaben).

24.2 Teil 2: Wikipedia API

  1. Machen Sie sich mit der API von Wikipedia vertraut: https://www.mediawiki.org/wiki/API:Query und schreiben Sie Code, der via API den Wikipedia-Artikel über “Bern” bereitstellt.

  2. Sie wollen wissen, ob Bern oder Zürich populärer ist und überlegen folgendes Vorgehen:

  • Alle Wikipedia-Artikel von Orten finden, die im 1km-Radius um den Bahnhof von Bern/Zürich sind.
  • Für beide Städte für jeden der gefundenen Artikel die Anzahl Bilder ermitteln, die im Artikel enthalten sind. Die Stadt die in Summe mehr Bilder hat sei der Gewinner :)
  • Sie können folgende Koordinaten verwenden.
    • Bern: 46.949722, 7.439444
    • Zürich: 47.377455, 8.539688
  • Hints finden sich hier: https://www.mediawiki.org/wiki/API:Geosearch/de#Beispiel_2:_Nach_Seiten_in_der_N%C3%A4he_suchen

Achten Sie auf gut lesbaren Code! Der Einreichungstermin ist der 3. Januar 2025.

Viel Erfolg!

25 Troubleshooting

25.1 Allgemeines Vorgehen bei einem Error:

  • Die meisten Fehlermeldungen haben eigentlich damit zu tun, dass irgendetwas nicht da ist: Library, Funktion, Objekt, Pfad. D.h. etwas ist nicht installiert, nicht geladen, nicht erzeugt worden, oder falsch geschrieben. Prüfen Sie, ob es Tippfehler gibt und alles geladen/erzeugt ist.
  • Fragen Sie ihren Nachbarn, ob er einen augenscheinlichen Fehler sieht. Oft ist man blind für die eigenen Tippfehler.
  • Googlen Sie den allgemeinen Teil der Fehlermeldung. I.d.R. hatte jemand vor Ihnen dasselbe Problem. Google Treffer führen häufig zu stackoverflow.com oder anderen Programmier-Hilfeforen. Dort macht es meistens Sinn:
    • Die Frage/Situation die ins Forum gepostet wurde kurz mit der eigenen Frage/Situation abzugleichen und wenn das Problem ähnlich gelagert scheint,
    • die bestbewertete Antwort zu lesen
  • Immer noch keine Lösung: Mich fragen. Am besten den Code, mit dem ich den Fehler reproduzieren kann, beilegen, sowie den Wortlaut der Fehlermeldung und Verweis auf die Zeile, die den Fehler erzeugt.

25.2 Häufigste Fehlermeldungen

  1. could not find function

Hat meistens eine dieser zwei Ursachen:

  • das Paket, das die Funktion enthält wurde nicht geladen. mit ??funktionsname kann man suchen, in welchem Paket die Funktion steckt
  • Falsch geschrieben
  1. Error in library
  • entweder falsch geschrieben oder
  • nicht installiert: install.packages("paketname"). Anführungszeichen nicht vergessen!!
  1. Error: object ‘…’ not found
  • falsch geschrieben oder
  • objekt wurde noch nicht erzeugt (Codezeile vergessen auszuführen; R wurde neu gestartet und das Objekt nicht neu erstellt).
  • überlegen, ob das Objekt direkt im Workspace liegt, oder ob es ein Objekt innerhalb eines Datensatzes ist: income -> “not found”. daten$income -> funktioniert
  1. cannot open the connection
  • Pfad/Dateiname oder Ähnliches falsch geschrieben
  • Anführungszeichen um die Pfadangabe vergessen
  • R sucht im falschen Pfad. Prüfen mit getwd(). Beim Eingeben des Pfads hilft das Drücken von TAB bei der Autovervollständigung des Pfads, was Fehler vermeidet
  • Falls der Pfad nicht bekannt ist: Den Pfad einer Datei kann man i.d.R. heruasfinden indem man die Eigenschaften der Datei (Windows: Rechtsklick; Apple: Apfel+Click) betrachtet (schauen nach “Ort” oder “Pfad” oder Ähnliches).
  1. … is not a …

R erwartet einen anderen Datentyp/Klasse als man ihm gibt.

  • prüfen ob der Vektor vom richtigen Typ ist (werden Zahlen und Buchstaben zusammengerechnet? Ist ein Vektor z.B. ein Faktor und der andere ein String?
  1. items to replace is not a multiple of / replacement has …
  • I.d.R.: Die Länge von zwei Vektoren passt nicht zusammen.
  • Prüfen, ob das was man zuweist die erwartete Länge hat, z.B. mit length() oder nrow().
  1. Error in if
  • im if-Statement steht vermutlich etwas das nicht vom Typ “logical” (d.h. TRUE oder FALSE) ist.
  • Prüfen, was genau für Daten im if-Statement verwendet werden.
  • ggf. sind Missings (NA, NaN) innerhalb der Daten.
  1. Es kommt keine Meldung aber es passiert einfach nichts

R ist nicht bereit für neue Eingaben!

  • Wenn beispielsweise ein Anführungszeichen oder eine Klammer geöffnet wurde aber nie geschlossen, dann wartet R, bis das schliessende Symbol (Klammer, Anführungszeichen) kommt. Selbst wenn man Enter drückt wird der Befehl dann nicht abgebrochen. Das erkennt man daran, dass die Konsole ein “+” zeigt statt ein “>”, d.h. das vorherige Kommandos um weitere Eingaben ergänzen will, bis das fehlende Zeichen kommt.

  • Evtl “hängt” die Session. Eine komplizierte Rechnung kann auch mal eine Weile dauern. Erkennbar i.d.R. daran, dass in der Konsole das Zeichen “>” nicht da ist sondern nur ein blinkender Coursor. Zudem erscheint rechts oben in der Konsole ein rotes Stopschild.

  • überlegen, ob R etwas sinnvolles rechnet: fertig rechnen lassen oder abbrechen mit Klick aufs Stopschild oder Escape-Taste
  • evtl. haben Sie etwas programmiert, was niemals endet (z.B. eine Endlosschleife) oder anderweitig die Ressourcen des Arbeitsgeräts in die Knie zwingt. In dem Fall abbrechen mit Escape und den Code überprüfen.