Zeilen in einer Matrix verdoppeln.

Hier ein kleiner Tipp aus dem R-Praxisleben. Beispiel: In einer Matrix (respektive data.frame) jede Zeile verdoppeln.

1
2
3
4
5
6
daten <- mtcars 
 
#Doppelte Zeilen direkt untereinander
daten_doppelt <- daten[rep(1:nrow(daten),each=2),]
#Doppelte untereinander
daten_doppelt2 <- daten[rep(1:nrow(daten),times=2),]

Ja das ist wirklich nicht schwer. Die Grundidee: Die Selektionsvektoren, welche immer in den eckigen Klammern stehen, kann man natürlich auch nutzen um bestimmte Zeilen mehrfach zu wählen.

Damit es nicht nur bei diesem simplen Beispiel bleibt, was vielleicht jedem eingefallen wäre, noch ein paar Ideen, wie man mit dieser simplen Methode auch wesentlich komplexere Verdopplungen durchführen kann.
Beispiel: In einer Matrix sollen Zeilen mit bestimmten eigenschaft verdreifacht werden.

8
9
10
merkmalerfuellt <- daten$cyl>=6 & daten$gear>=4
wiederholen <- ifelse(merkmalerfuellt,3,1)
daten_dreifach <- daten[rep(1:nrow(daten),times=wiederholen),]

Anmerkungen

rep(x,each/times) hat ein paar unterschiedliche Funktionsweisen. Für each=n kann nur eine Zahl angegeben werden, wie oft ein Item direkt untereinander wiederholt wird. Mit times=n wird angegeben wie oft ein Vektor untereinander wiederholt werden soll. Daneben gibt es noch eine weitere tolle, im zweiten Beispiel verwendete, Funktionsweise: Übergibt man rep(x,times=y) einen Vektor y der gleichen Länge wie x (length(x)), so wiederholt rep() den Eintrag x[i] genau y[i]-mal. Es kann also für jedes Item angegeben werden, wie oft es wiederholt werden soll. Die Duplikate stehen dann sinnvollerweise direkt untereinander.
Ist y zu kurz oder zu lang findet kein Recycling oder eine Kürzung statt!

lapply() und sapply() – Der Einstieg in die apply-Funktionen.

Warum gerade lapply() und keine der gebräuchlicheren Funktionen apply Funktionen aus R? Ganz einfach: lapply() ist die Grundfunktion und die anderen Funktionen bauen auf dieser Funktionsweise auf. Da man lapply() nicht nur auf Vektoren, sondern auch auf Listen anwenden kann bietet es ungeahnte (vllt. auch unausdenkbare) Möglichkeiten der Verwendung. Aus diesem Grund ist das Beispiel auch äußerst praxisunrelevant.

Funktionsweise lapply()

Versuch eines Schemas: So ungefähr arbeitet lapply().


Beschäftigen wir uns zunächst mit Listen. Listen sind das vermutlich flexibelste Werkzeug um Daten zu “speichern”, denn als einzelnes Listenelement lässt sich wohl alles in R definieren. u.a. sinnvoll können sein:

  • Vektoren aller Art
  • Funktionen
  • data.frames
  • lineare Modelle
  • Listen
  • usw.

Ebenso kann die Ausgabe der Funktion die in lapply() arbeitet jeglichen Datentyp annehmen.
Um dies zu demonstrieren definieren wir für unser Beispiel diese krude Liste:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
a <- list()
a[[1]] <- 8
a[[2]] <- 5:10
a[[3]] <- c(Frucht="Banane")
a[[4]] <- function(x) x+1
a[["Banane"]] <- "test"
a[[5]] <- list(a=1:10,b=10:1)
a[["Ende"]] <- "fin"
 
#Wie sieht das fertig aus?
a
# [[1]]
# [1] 8
# 
# [[2]]
# [1]  5  6  7  8  9 10
# 
# [[3]]
# Frucht 
# "Banane" 
# 
# [[4]]
# function (x) 
#   x + 1
# 
# $Banane
# $Banane$a
# [1]  1  2  3  4  5  6  7  8  9 10
# 
# $Banane$b
# [1] 10  9  8  7  6  5  4  3  2  1
# 
# 
# $Ende
# [1] "fin"

Wie man schön sehen kann, können tatsächlich in jedes einzelne Listenelement unterschiedliche Datentypen gespeichert werden. Auch wird deutlich, dass wir nicht unbedingt Zahlen zur Indizierung benötigen, sondern den einzelnen Listenelementen auch Namen geben können.
Doch Vorsicht! Auch ein benanntes Listenelement hat intern eine Nummer. So z.B. wird a[["banane"]] das 5te Listenelement und mit a[[5]] kann ebenfalls darauf zugegriffen werden.
Auch wichtig: Wenn ein Listenelement zu einer Liste hinzugefügt werden soll, so kann dies nur am direkten Ende der Liste geschehen. Unsere Liste hat die Länge 6. Ein Element mit der Nummer 8 (a[[8]]) ließe sich nicht erstellen.
Schauen wir uns nun eine ebenfalls untypische lapply()-Funktion an:

37
38
39
40
41
42
43
44
45
46
47
48
ergebnis <- lapply(a,function(x){
  if(is.numeric(x)){
    res <- sum(x)^2
  }else if(is.character(x)){
    res <- paste(names(x),x) #Fügt zwei character zu einem zusammen.
  }else if(is.function(x)){
    res <- x(9)
  }else{ #Wenn kein Datentyp bisher passt.
    res <- "irgendwas komisches"
  }
  return(res)
})

Da verschiedenste Datentypen behandelt werden sollen wurden die Fallunterscheidungen eingebaut. Was hier geschieht sollte klar werden, wenn wir uns das Ergebnis ansehen:

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
ergebnis
[[1]]
[1] 64
 
[[2]]
[1]  25  36  49  64  81 100
 
[[3]]
[1] "Frucht Banane"
 
[[4]]
[1] 10
 
$Banane
[1] "irgendwas komisches"
 
$Ende
[1] " fin"

Und was ist mit sapply()?

Das ist sehr einfach zu erklären. Und im prinzip steht auch alles in der R-Hilfe: ?sapply liefert:

sapply is a user-friendly version and wrapper of lapply by default returning a vector, matrix or, if simplify=”array”, an array if appropriate, by applying simplify2array(). sapply(x, f, simplify=FALSE, USE.NAMES=FALSE) is the same as lapply(x,f).

sapply() arbeitet also genau so wie lapply(). Es wird nur versucht das Ergebnis nicht als Liste sondern als Vektor oder Matrix auszugeben. Das funktioniert jedoch nur, wenn das Ergebnis der Funktion immer die gleiche Länge hat. Also:

  • Funktionsergebnis ist eine Zahl oder ein Character
  • Funktionsergebnis ist ein numerischer Vektor immer gleicher Länge

Im ersten Fall ist das Gesamtergebnis ein Vektor, im zweiten Fall eine Matrix in der die Funktionsergebnisse spaltenweise stehen. Dafür noch zwei kurze Beispiele:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
### Beispiel 1
 
b <- c(2,4,1)
e2 <- sapply(b,function(x){
  res <- numeric(max(b))
  res[x] <- 1
  return(res)
})
e2
#      [,1] [,2] [,3]
# [1,]    0    0    1
# [2,]    1    0    0
# [3,]    0    0    0
# [4,]    0    1    0
 
### Beispiel 2
 
c <- list(a=1:3,b=4:6,c=c(1,3,5))
sapply(c,function(x){
  res <- numeric(6)
  res[x] <- 1
  return(res)
})
#      a b c
# [1,] 1 0 1
# [2,] 1 0 0
# [3,] 1 0 1
# [4,] 0 1 0
# [5,] 0 1 1
# [6,] 0 1 0

Viel Spaß beim selber programmieren!

Schnelleres R dank Kompilierung (also ohne apply())

Inspiriert durch diesen Blogbeitrag wollte ich die Vorkompilierung selbst einmal ausprobieren und auch mit dem so hoch gelobten apply() vergleichen um mich von der Effektivität zu überzeugen.

1
2
3
4
5
6
7
8
9
eingabe <- rnorm(1000000,mean=8,sd=9) #das geht noch sehr schnell
 
meineFunktion <- function(){
	ausgabe <- numeric(length=length(eingabe))
	for(i in seq_along(eingabe)){
		ausgabe[i] <- (eingabe[i] - 8)/9
	}
	return(ausgabe)
}

Warum stecken wir das in eine Funktion? Nur Funktionen können kompiliert werden, darum.
Wie messen wir überhaupt die Zeit? Das geht mit der Funktion system.time(). Alles was dort in den Klammern steht wird wie gewöhnt ausgeführt mit dem Unterschied, dass am Ende in der Konsole die Zeit ausgegeben wird, die benötigt wurde.
Probieren wir nun mal alles aus.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
system.time(erg1 <- meineFunktion())
#User      System verstrichen 
#3.45        0.00        3.47 
 
#Nun mal vorkompeliert:
library(compiler) #sollte schon in R enthalten sein.
meineFunktionKompiliert <- cmpfun(meineFunktion)
 
system.time(erg2 <- meineFunktionKompiliert())
#User      System verstrichen 
#0.73        0.02        0.75 
 
#Und sapply()?
system.time(erg3 <- sapply(eingabe,function(x) (x-8)/9))
#User      System verstrichen 
#4.80        0.00        4.80 
 
#und richtig?
system.time(erg4 <- (eingabe-8)/9)
#User      System verstrichen 
#0.02        0.00        0.01

Schön! Die kompilierte Version ist wirklich um einiges schneller, aber warum zum teufel ist apply() so lahm? Das liegt vor allem daran, dass meineFunktion() noch halbwegs klug programmiert ist.
Zeit geht bei R immer für zwei Ressourcen drauf. Einmal die Rechenzeit (CPU) und einmal für Speicheroperationen (bei R nur RAM). Sonderlich kompliziert ist die Rechnung (x-8)/9 ja nicht. Viel der Zeit geht also für die Speicheroperationen drauf.
Durch die Zeile ausgabe <- numeric(length=length(eingabe)) in der Funktion wurde ein Ausgabevektor erzeugt, der schon Plätze für die 1000000 Ergebnisse der Rechnung bereithält. Die Rechnung geht dann flott durch und schiebt die Ergebnisse in die bereits vorhandenen Speicherplätze.
Würde man folgende beliebte Initialisierung wählen: ausgabe <- NULL bräuchte der Code erheblich länger. Das liegt daran, dass dann für jedes neue Ergebnis intern ein neuer Vektor erzeugt wird, welcher um 1 länger ist, damit das neue Resultat hineinpasst.
Viel Zeit kann also gespart werden, wenn der Ergebnisvektor schon vorher in seiner kompletten länge definiert wird. Außerdem wirkt cmpfun() besonders dann effektiv, wenn viele arithmetische Berechnungen durchgeführt werden. Greift man in der zu kompilierenden Funktion ausschließlich auf R-Funktionen zurück wird der Speedup nicht besonders groß ausfallen.

Hervorhebungen in Listings

Eine kleine Ergänzung zum Beitrag Quellcode und Latex in der Beamer-Class: Aus didaktischen Gründen kann es sehr hilfreich sein bestimmte Begriffe oder Abschnitte hervorzuheben.

Begriffe Hervorheben

Einzelne Begriffe können recht leicht umformatiert werden. Leider nur (soweit ich es herausfinden konnte) für Wörter, die nicht bereits eingefärbt wurden. Also keine keywords und keine comments. Dafür sind folgende Zeilen in dem unteren kompletten Quelltext zuständig:

28
29
emph={x,test}, %Woerter die hervorgehoben werden sollen, koennen keine keywords oder comments sein.
emphstyle=\color{red}\underbar %rot und unterstrichen

Abschnitte Hervorheben

Es können auch ganze Abschnitte hervorgehoben werden, indem wir eine colorbox um den Code legen. Damit das geht, müssen wir listings klar machen, dass der Latex-Code nicht zu dem anderen Code in listings gehört. Dazu nutzten wir escapechar=@. Das @ kann man theoretisch auch ändern. Das Zeichen darf nur nicht im restlichen Quelltext vorkommen.
So kann mit @\colorbox{highlightcolor}{quelltext hier}@ ein Teil des Quelltextes hervorgehoben werden. Leider wird eine Zeile dadurch etwas höher, sodass eine Linie am Rand zerstückelt wird.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
\documentclass{beamer}
 
\usepackage[utf8x]{inputenc} %An eigene Codierung anpassen! z.B. latin1 anstelle von utf8x
\usepackage[ngerman]{babel}
 
%Definieren uns farbigen Quellcode
\usepackage{color}
\definecolor{dkgreen}{rgb}{0,0.6,0}
\definecolor{gray}{rgb}{0.5,0.5,0.5}
\definecolor{mauve}{rgb}{0.58,0,0.82}
\definecolor{highlightcolor}{rgb}{1,0.96,0.8}
 
%Damit wir Quellcode nutzen können.
\usepackage{listings}
\lstset{numbers=left,
	numberstyle=\tiny,
	numbersep=5pt,
	breaklines=true,
	showstringspaces=false,
	frame=l ,
	xleftmargin=15pt,
	xrightmargin=15pt,
	basicstyle=\ttfamily\scriptsize, %\scriptsize weg, wenns groesser werden soll
	stepnumber=1,
	keywordstyle=\color{blue},          % keyword style
  	commentstyle=\color{dkgreen},       % comment style
  	stringstyle=\color{mauve},         % string literal style
  	emph={x,test}, %Woerter die hervorgehoben werden sollen, koennen keine keywords oder comments sein.
  	emphstyle=\color{red}\underbar %rot und unterstrichen
}
%Sprache Festelegen
\lstset{language=R}
 
\begin{document}
 
\begin{frame}[fragile] %fragile ist sehr wichtig!
\frametitle{R-Code Minimalbeispiel}
 \begin{lstlisting}[escapechar=@]
library(gridExtra)
test <- tapply(mtcars$hp, mtcars$cyl, function(x) @\colorbox{highlightcolor}{c(mean=round(mean(x)), min=min(x), max=max(x))}@, simplify=T)
test <- simplify2array(test)
grid.table(test)
  \end{lstlisting}
\end{frame}
 
\end{document}

Ergebnis:

Leider gibt es auch keine Einfärbung innerhalb der colorbox. Kann man aber verschmerzen, denke ich.

Daten im R-Quelltext hinterlegen.

Ok, dieses Beispiel wird man wohl selten gebrauchen, aber vielleicht mag es mal nützlich sein, wenn man jemandem ein wenig R-Code schicken will, der mit einem Datensatz arbeitet, ihr ihm aber nicht den kompletten Datensatz schicken wollt/könnt/dürft. Gleichzeitig sollte alles in einer Datei sein, damit der Empfänger nicht die Mühe hat alles noch mal einlesen zu müssen.

Funktioniert auch gut, wenn man sich nur selbst ein paar Daten ausdenken will, mit denen man etwas testen möchte. So erspart man sich lästiges c("a","b", usw.).

So geht’s:

1
2
3
4
5
6
datentxt <- "	lcavol	lweight	age	lbph	svi	lcp	gleason	pgg45	lpsa	train
1	-0.579818495	2.769459	50	-1.38629436	0	-1.38629436	6	  0	-0.4307829	T
2	-0.994252273	3.319626	58	-1.38629436	0	-1.38629436	6	  0	-0.1625189	T
3	-0.510825624	2.691243	74	-1.38629436	0	-1.38629436	7	 20	-0.1625189	T"
 
daten <- read.table(textConnection(datentxt))

Die ersten Textzeilen aus dem Prostata-Datensatz markieren, kopieren und dann datentxt <- "HIER" einfügen. Dabei darauf achten keine neuen Zeilenumbrüche einzubauen.

Hübscher Quellcode mit Latex und Beamer

Aus gegebenem Anlass mal ein kurzer Beitrag, wie man Quelltext hübsch in eine LaTeX-Präsentation mit der Beamer-Class einbaut.
Anstelle der bekannten Verbatim-Umgebung wird hier auf das (auch für Berichte empfehlenswerte) Paket listings gesetzt.
Einge recht gute und knappe Hilfe findet man bei wikibooks.org

Vorteile:

  • farbiger Code
  • kein Problem mit langen Zeilen

Nachteile:

  • keine ;)

Hier ein “Minimalbeispiel”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
\documentclass{beamer}
 
\usepackage[utf8x]{inputenc} %An eigene Codierung anpassen! z.B. latin1 anstelle von utf8
\usepackage[ngerman]{babel}
 
%Definieren uns farbigen Quellcode
\usepackage{color}
\definecolor{dkgreen}{rgb}{0,0.6,0}
\definecolor{gray}{rgb}{0.5,0.5,0.5}
\definecolor{mauve}{rgb}{0.58,0,0.82}
 
%Damit wir Quellcode nutzen können.
\usepackage{listings}
\lstset{numbers=left,
	numberstyle=\tiny,
	numbersep=5pt,
	breaklines=true,
	showstringspaces=false,
	frame=l ,
	xleftmargin=15pt,
	xrightmargin=15pt,
	basicstyle=\ttfamily\scriptsize,
	stepnumber=1,
	keywordstyle=\color{blue},          % keyword style
  	commentstyle=\color{dkgreen},       % comment style
  	stringstyle=\color{mauve}         % string literal style
}
%Sprache Festelegen
\lstset{language=R}
 
\begin{document}
 
\begin{frame}[fragile] %fragile ist sehr wichtig!
\frametitle{R-Code Minimalbeispiel}
\begin{lstlisting}
#Zeitintervall und Feinheit
T <- 1; N <- 100
 
#Zeitschritte
Delta <- T/N
 
#Zeit-Achse
t <- seq(0,T,length.out=N+1)
 
#Simulation und Aufsummierung der Zuwaechse
S <- cumsum(sample(x=c(-1,1),size=n,replace=T) * sqrt(Delta)) 
 
W <- c(0,S)                  #0 als ersten Wert.
 
plot(t,W, type="l")          #Linienplot
\end{lstlisting}
\end{frame}
 
\end{document}

Und so toll kann das dann aussehen:
Listings in LaTex und die Beamer-class

Hier gibt’s jetzt auch eine kleine Anleitung zum Hervorheben von Quelltext.

Workflow für Simulationsstudien

Eine häufige Aufgabe ist es, für verschiedene Parameter ein und die selbe Simulation mit R durchzuführen und dann die Ergebnisse auszuwerten. Persönlich bevorzuge ich es, erst alles zu simulieren und dann auszuwerten. Dieses Vorgehen hat mehrere Vorteile: Bei langandauernden Simulationen müssen diese nur einmalig ausgeführt werden. Außerdem: Wenn ich mit meiner Auswertung unzufrieden bin, kann ich einfach die Auswertungsprozedur modifizieren ohne noch einmal alles durchlaufen lassen zu müssen und der Quelltext ist auch nachvollziehbarer. Insbesondere ist das Vorgehen aber auch angepasst auf die grafische Darstellung und Auswertung mit ggplot2.

Darum ist es immer mein Ziel alle Ergebnisse der Simulation in einem data.frame zu speichern, wobei in jeder Zeile des data.frames der simulierte Wert und seine Parameter mit denen er erzeugt wurde stehen sollen.

Beispiel

1
2
3
4
parameterA <- c(10,50,100)
parameterB <- c(0.3,0.9)
parameterC <- round(c(0.1,0.2)*pi,3)
parameter <- expand.grid(A=parameterA,B=parameterB,C=parameterC)

Hier treffen wir schon auf die erste tolle Funktion expand.grid(). Sie erzeugt einen data.frame der alle möglichen Kombinationen von den Werten in den Eingabevektoren erstellt. Natürlich geht das auch mit mehr als dreien und auch mit Zeichenketten anstelle von Zahlen.

Kommen wir nun zur Simulation:

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
set.seed(234)
simerg <- apply(parameter,1,function(pars){
	A <- pars[1]
	B <- pars[2]
	C <- pars[3]
	xycord <- cbind.data.frame(x=0,y=0)
	richtung <- 0
	for(i in 2:A){
		neuerichtung <- richtung + C * rnorm(1,1,i/A*2)
		richtung <- neuerichtung
		schrittweite <- rnorm(1,mean=B,sd=B)
		xneu <- xycord[i-1,"x"] + schrittweite*sin(richtung)
		yneu <- xycord[i-1,"y"] + schrittweite*cos(richtung)
		xycord[i,] <- cbind.data.frame(x=xneu,y=yneu)
	}
	#Ausgabe als data.frame sehr nützlich
	#Netterweise werden A und B über alle Zeilen wiederholt
	cbind.data.frame(Steps=A,Weite=B,Drift=C,xycord) #letzte Zeiel wird automatisch zurückgegeben
})
#Warnings können ignoriert werden

Die nächste Funktion, die kein Fremdbegriff für R-Nutzer sein darf: apply(). Angewendet auf einen data.frame kann sie über die Zeilen apply(...,MARGIN=1,...) oder über die Spalten apply(...,MARGIN=2,...) laufen. Wenn bspw. MARGIN=1 wird jeweils jede Zeile als Vektor (also in dem Fall transponiert) an die in apply() angegebene Funktion übermittelt. Die Funktion können wir unter Verwendung von {...} übersichtlich über mehrere Zeilen schreiben, wie man es gewohnt ist.
Damit wir am Ende alle Simulationsergebnisse gut zusammenfassen können ist es von Vorteil diese gleich in einem data.frame zusammenzupacken. Mit cbind.data.frame() können wir die Faktoren und die Ergebnisse zusammenfassen und den Spalten gleichzeitig schon jetzt bedeutsame Namen geben (hier A,B und Z).
Schauen wir uns das Ergebnis mal an:

28
29
30
31
32
33
34
35
36
37
38
simerg
# [[1]]
#    Steps Weite Drift          x           y
# 1     10   0.3 0.314  0.0000000  0.00000000
# 2     10   0.3 0.314 -0.1221397 -0.29132710
# ...
# [[2]]
#    Steps Weite Drift          x           y
# 1     50   0.3 0.314 0.00000000  0.00000000
# 2     50   0.3 0.314 0.09795731  0.28858860
# ...

Da die apply() hier einen data.frame zurückgegeben hat, war sich R unsicher und hat jedes Ergebnis in eine Liste gesteckt (Das macht R nicht standardmäßig, aber wenn es “denkt”, dass es damit den sicheren Weg geht, denn in ein einzelnes List-Item kann alles rein – unabhängig davon, was in den anderen Listeneinträgen enthalten ist.). Wir sehen natürlich, dass das blöd ist, denn alle Listeneinträge haben die gleiche Struktur (gleiche Spaltenanzahl und gleiche Spaltennamen) und passen gut untereinander in den heiß geliebten data.frame.
Dafür gibt es einen kurzen und genialen Trick:

39
simulationsergebnis <- do.call("rbind",simerg) #Funktioniert natürlich nur, wenn alle data.frames in der Liste die gleichen Spaltennamen haben. Die Anzahl der Zeilen kann glücklicherweise unterschiedlich sein!

Jetzt ist simulationsergebnis ein data.frame und kinderleicht auszuwerten.

41
42
43
44
45
46
47
48
49
50
51
52
#folgendes ist für ggplot sinnvoll:
simulationsergebnis$Steps <- as.factor(simulationsergebnis$Steps)
simulationsergebnis$Weite <- as.factor(simulationsergebnis$Weite)
simulationsergebnis$Drift <- as.factor(simulationsergebnis$Drift)
 
#Auswertung der Simulation
library("ggplot2")
g <- ggplot(data=simulationsergebnis, aes(x=x,y=y,color=Steps))
g + geom_path() + facet_wrap(Weite~Drift,scales="free")
 
#Zahlenauswertung
aggregate(x~Steps+Weite+Drift,data=simulationsergebnis,FUN=range)

Simulationsergebnis mit ggplot
Zum Verstehen kann ich nur raten alles einmal auszuführen und sich den Inhalt der Variablen nach jedem Schritt anzuschauen und alles mal mit anderen Werten und eigenen Ideen anzureichern.

merge() – eine kleine Einleitung

Es kann sein, dass man in die Situation kommt verschiedene Datensätze miteinander kombinieren zu wollen. In einer Tabelle steht vielleicht für verschiedene Lebensmittel die Energiewerte und Vitamingehalte und in einer anderen Tabelle steht wie viel von welchem Lebensmittel eine Person gegessen hat. Will man jetzt schnell berechnen, wie viele kcal jede Person zu sich genommen hat kann man das schnell und elegant durch mergen der Datensätze, gefolgt von aggregate() um die kcal für jeden Patienten zusammenzurechnen.
Das ist natürlich nur ein dämliches Beispiel. Der geneigte R-Nutzer wird schon seine eigenen Anwendungen finden. Oft ist es auch viel einfacher Berechnungen an einem Datensatz durchzuführen als Funktionen zu basteln, die sich Daten aus mehreren Datensätzen zusammensuchen.

Das einfachste Beispiel

wie man es auch in jeder Datenbanken/Informationssysteme-Vorlesung vorgesetzt bekommt:
Wir haben zwei Tabellen, welche sich in einer Spalte ein Merkmal teilen.
Erzeugen wir so ein Beispiel in R:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kartoffeln <- cbind.data.frame(sorte=c("Adretta","Amflora","Markies"), kgpreis=c(1.44,0.76,1.10))
einkauf <- cbind.data.frame(sorte=c("Adretta","Adretta","Amflora","Markies","Amflora"), kunde=c("Huber","Meyer","Meyer","Schmidt","Huber"),kg=c(10,23,5,8,6))
kartoffeln
#     sorte kgpreis
# 1 Adretta    1.44
# 2 Amflora    0.76
# 3 Markies    1.10
einkauf
#     sorte   kunde kg
# 1 Adretta   Huber 10
# 2 Adretta   Meyer 23
# 3 Amflora   Meyer  5
# 4 Markies Schmidt  8
# 5 Amflora   Huber  6

Jetzt wird schnell klar, was wir haben möchten; Eine Tabelle die für jeden Kunden zeigt, was er bezahlen soll.

18
19
20
21
22
23
24
25
26
einkaufmitpreis <- merge(kartoffeln,einkauf)
einkaufmitpreis$zuzahlen <- einkaufmitpreis$kgpreis * einkaufmitpreis$kg
einkaufmitpreis
#     sorte kgpreis   kunde kg zuzahlen
# 1 Adretta    1.44   Huber 10    14.40
# 2 Adretta    1.44   Meyer 23    33.12
# 3 Amflora    0.76   Meyer  5     3.80
# 4 Amflora    0.76   Huber  6     4.56
# 5 Markies    1.10 Schmidt  8     8.80

Das sieht doch schon ganz knorke aus. Die kritiker werden vielleicht anmerken, dass jetzt ja jeder Kunde nur den zu zahlenden Preis für die Bestellung einer Kartoffelsorte weiß. Dem widmen wir uns später.
Zunächst schauen wir, was passiert, wenn ein Kunde eine Bestellung von gleicher Kartoffelsorte hinzufügt.

30
31
32
33
34
35
36
37
38
39
40
einkauf2 <- rbind(einkauf,cbind.data.frame(sorte="Adretta",kunde="Huber",kg=4))
einkauf2mitpreis <- merge(kartoffeln,einkauf2)
einkauf2mitpreis$zuzahlen <- einkauf2mitpreis$kgpreis * einkauf2mitpreis$kg
einkauf2mitpreis
#     sorte kgpreis   kunde kg zuzahlen
# 1 Adretta    1.44   Huber 10    14.40
# 2 Adretta    1.44   Meyer 23    33.12
# 3 Adretta    1.44   Huber  4     5.76
# 4 Amflora    0.76   Meyer  5     3.80
# 5 Amflora    0.76   Huber  6     4.56
# 6 Markies    1.10 Schmidt  8     8.80

Auch das funktioniert zuverlässig. Die Bestellung ist jetzt allerdings zwei mal in der Tabelle. Das bekommen wir jedoch leicht mit aggregate() heraus.

42
43
44
45
46
47
48
aggregate(einkauf2mitpreis[,c("kg","zuzahlen")],by=einkauf2mitpreis[,c("sorte","kunde")],FUN=sum)
#    sorte   kunde kg zuzahlen
#1 Adretta   Huber 14    20.16
#2 Amflora   Huber  6     4.56
#3 Adretta   Meyer 23    33.12
#4 Amflora   Meyer  5     3.80
#5 Markies Schmidt  8     8.80

Jetzt sind die Bestellungen zusammengefasst. Derjenige, der die Kartoffeln einkauft interessiert sich vielleicht eher für:

50
51
52
53
54
aggregate(kg~sorte,data=einkauf2mitpreis,sum)
#    sorte kg
#1 Adretta 37
#2 Amflora 11
#3 Markies  8

So sollte es auch nicht schwer sein, andere Fragestellungen zu beantworten.

Wenn nicht alles passt

dann macht merge meistens vieles richtig. Fällt z.B. die Bestellung mit der Sorte Markies weg…

55
56
57
58
59
60
61
62
einkauf3 <- einkauf2[-4,]
merge(x=kartoffeln ,y=einkauf3)
#     sorte kgpreis kunde kg
# 1 Adretta    1.44 Huber 10
# 2 Adretta    1.44 Meyer 23
# 3 Adretta    1.44 Huber  4
# 4 Amflora    0.76 Meyer  5
# 5 Amflora    0.76 Huber  6

…dann ist sie auch einfach nicht mehr im Ergebnis.
Was ist jedoch, wenn es die Sorte Adretta nicht mehr gäbe. Nicht auszudenken, wenn dann die Bestellungen einfach wegfallen.

64
65
66
67
68
69
70
71
72
73
wenigerkartoffeln <- kartoffeln[-1,]
merge(x=einkauf2, y=wenigerkartoffeln) #Das ist blöd, denn wir wollen die Bestellung ja nicht vergessen
merge(x=einkauf2, y=wenigerkartoffeln, all.x=T) #Schon besser.
#     sorte   kunde kg kgpreis
# 1 Adretta   Huber 10      NA
# 2 Adretta   Meyer 23      NA
# 3 Adretta   Huber  4      NA
# 4 Amflora   Meyer  5    0.76
# 5 Amflora   Huber  6    0.76
# 6 Markies Schmidt  8    1.10

Gut. Dieses Problem ist geklärt. Doch weiteres Unheil kann auf uns zukommen! Im echten Leben stimmen die Spaltennamen selten überein. Was nun?

77
78
79
80
81
82
83
84
85
86
einkauf4 <- einkauf2
colnames(einkauf4)[1] <- "Kartoffelsorte"
merge(x=einkauf4, y=wenigerkartoffeln, all.x=T, by.x="Kartoffelsorte", by.y="sorte") #perfekt
#     sorte   kunde kg kgpreis
# 1 Adretta   Huber 10      NA
# 2 Adretta   Meyer 23      NA
# 3 Adretta   Huber  4      NA
# 4 Amflora   Meyer  5    0.76
# 5 Amflora   Huber  6    0.76
# 6 Markies Schmidt  8    1.10

Kleine Anmerkung zum Schluß: Natürlich kann man auch all.x=T und all.y=T gleichzeitig nutzen.

Geokoordinaten mit R und Google finden (einfach, ohne API)

Dies ist eine verblüffend leichte Übung, da Google einem direkt ein csv-File (Beispiel) ausspuckt, welches natürlich sehr einfach zu lesen ist. Weil es so einfach ist hier nur wenig Text und der Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
reisetrip <- c("Berlin","Potsdam","Magdeburg","Hannover","Minden","Bielefeld","Dortmund")
breiten <- NULL
langen <- NULL
for(i in seq_along(reisetrip)){
  query <- paste(strsplit(reisetrip[i]," ")[[1]], collapse="%20") #Leerzeichen mit %20 codieren
  query <- paste("http://maps.google.com/maps/geo?q=",query,"&output=csv&num=1",sep="")
  res <- read.table(query,sep=",")
  cat("Ort #",i,"von",length(reisetrip),";",reisetrip[i],"\n")
  Sys.sleep(runif(1,0.1,2.5)) #Wir warten ein wenig, damit Google nicht sauer wird.
  langen[i] <- res[,4] #longitude
  breiten[i] <- res[,3] #latitude
}
plot(langen,breiten,type="l", ylim=range(breiten)+c(-0.1,+0.1), xlim=range(langen)+c(-2,+0.5))
points(langen,breiten,pch=19)
text(langen,breiten,reisetrip,pos=2)

Warnungen bezüglich unvollständiger letzter Zeilen können getrost vernachlässigt werden

plot()

Zu den Suchparametern

findet man hier eine sehr gute Übersicht. Viele sind natürlich nicht für output=csv von Bedeutung. Der im Quelltext stehende Parameter num=1 zeigt, dass wir nur das erste Ergebnis haben wollen. Habe aber auch noch nicht die Beobachtung machen können, dass Google bei CSV mehr als ein Ergebnis ausgibt. Das Aneinanderreihen von Suchparametern erfolgt immer nach dem Schema: http://URL?query1=par1&query2=par2&... Also als erstes ein Fragezeichen und dann alle mit & verbinden.

Jetzt will man diese Punkte natürlich am liebsten auf einer Karte sehen. Dazu komme ich später mal.

19,9°

Berlin Neukölln