Faktoren in R: Anwendungen

In der Statistik muss man oft Daten gruppieren und die gruppierten Daten auswerten. In R wird eine derartige Gruppierung mit einem Faktor (oder mehreren Faktoren) vorgenommen. Das Kapitel beschreibt R-Funktionen, die diese Aufgaben erleichtern, und zeigt ihre praktische Anwendung.

Einordnung des Artikels

Weitere Anwendungen mit Faktoren, die ähnliche Funktionen wie tapply() einsetzen, finden sich in Die Familie der apply-Funktionen in R Teil 3: Weitere mit apply() verwandte Funktionen.

Einführung

In Faktoren in R: der Datentyp factor wurden die Eigenschaften von Faktoren erläutert und gezeigt, wie man Faktoren erzeugt. In diesem Kapitel werden Funktionen vorgestellt, die im praktischen Umgang mit Faktoren eine große Rolle spielen.

Wenn hier von Faktoren die Rede ist, ist damit immer gemeint, dass es sich auch um einen geordneten Faktor handeln kann.

Die Funktionen head() und tail()

Die Funktionen head() und tail() wurden schon für Vektoren (und andere Datentypen) vorgestellt und haben angewendet auf Faktoren ähnliches Verhalten: Mit head() kann man aus einem Faktor die ersten n Komponenten auswählen, mit tail() entsprechend die letzten n Komponenten.

Das folgende Beispiel zeigt die Anwendung der beiden Funktionen im Beispiel der Sonntagsfrage – es handelt sich wieder um die Stichprobe mit 20 Befragungen, wobei die Antwort D zwar als Level gesetzt ist, aber nicht in der Stichprobe enthalten ist:

sample <- c("A", "A", "E", "C", "B", "A", "E", "C", "A", "E", "C", "B", "A", "B", "E", "C", "A", "B", "E", "C")
length(sample)    # 20

sf <- factor(x = sample, levels = c("A", "B", "C", "D", "E"), ordered = TRUE)
sf
# [1] A A E C B A E C A E C B A B E C A B E C
# Levels: A < B < C < D < E

# Auswahl der ersten 9 Elemente
sf_9 <- head(sf, n = 9)
sf_9
# [1] A A E C B A E C A
# Levels: A < B < C < D < E

length(sf_9)    # 9

# Auswahl der letzten 11 Elemente
sf_11 <- tail(sf, n = 11)
sf_11
# [1] E C B A B E C A B E C
# Levels: A < B < C < D < E

length(sf_11)    # 11

Man sieht, dass durch die Anwendung von head() beziehungsweise tail() ein neuer Faktor erzeugt wird:

Die Funktionen split() und unsplit()

Aufspalten eines Vektors mit den Levels eines Faktors durch split()

Eine einfache Vorbereitung zu einer weiteren Verarbeitung ist es, einen Vektor gemäß der Levels aufzuspalten. Da dabei mehrere Vektoren unterschiedlicher Länge entstehen, ist der einzige Datentyp, in dem das Ergebnis gespeichert werden kann, eine Liste.

Das folgende Skript zerlegt den Vektor sample aus der Sonntagsfrage oben in die Teilvektoren (Aufruf der Funktion split() in Zeile 7); diese werden anschließend untersucht:

sample <- c("A", "A", "E", "C", "B", "A", "E", "C", "A", "E", "C", "B", "A", "B", "E", "C", "A", "B", "E", "C")
sf_ord <- factor(x = sample, levels = c("A", "B", "C", "D", "E"), ordered = TRUE)
sf_ord
# [1] A A E C B A E C A E C B A B E C A B E C
#Levels: A < B < C < D < E

parts <- split(x = sample, f = sf_ord)

is.list(parts)
# [1] TRUE

lengths(parts)
# A B C D E 
# 6 4 5 0 5 

str(parts)
# List of 5
# $ A: chr [1:6] "A" "A" "A" "A" ...
# $ B: chr [1:4] "B" "B" "B" "B"
# $ C: chr [1:5] "C" "C" "C" "C" ...
# $ D: chr(0) 
# $ E: chr [1:5] "E" "E" "E" "E" ...

parts
# $A
# [1] "A" "A" "A" "A" "A" "A"
# 
# $B
# [1] "B" "B" "B" "B"
# 
# $C
# [1] "C" "C" "C" "C" "C"
# 
# $D
# character(0)
# 
# $E
# [1] "E" "E" "E" "E" "E"

Es sollte klar sein, dass man die Funktion split() nicht nur auf den Vektor sample anwenden kann, sondern auf einen beliebigen Vektor, der sich gemäß dem Faktor zerlegen lässt.

Man erkennt im Skript:

  1. Dass in der entstehenden Liste Namen gesetzt sind (Attribut names), nämlich die Bezeichnungen der Levels.
  2. Dass die entstehende Liste 5 character-Vektoren enthält, wobei der vierte Vektor aber die Länge 0 besitzt, da der Level D nicht in sample enthalten ist.

Möchte man dies vermeiden, kann man vor der Anwendung von split() die fehlenden Levels mit droplevels() beseitigen.

Es geht sogar noch einfacher: Die Funktion split() besitzt das weitere Argument drop, das per default auf FALSE gesetzt ist; setzt man es TRUE, werden ebenfalls die fehlenden Levels beseitigt:

# sample und sf_ord wie oben

parts.4 <- split(x = sample, f = sf_ord, drop = TRUE)

str(parts.4)
# List of 4
# $ A: chr [1:6] "A" "A" "A" "A" ...
# $ B: chr [1:4] "B" "B" "B" "B"
# $ C: chr [1:5] "C" "C" "C" "C" ...
# $ E: chr [1:5] "E" "E" "E" "E" ...

lengths(x = parts.4)
# A B C E 
# 6 4 5 5

Die Umkehrung von split() durch unsplit()

Die Funktion split() hat einen Vektor mit Hilfe der Level eines Faktors in eine Liste verwandelt, wobei jede Komponente der Liste derjenige Teilvektor ist, der dem Level zugeordnet ist. Mit der Funktion unlist() kann man aus der Liste den Vektor wieder zurückgewinnen:

sample <- c("A", "A", "E", "C", "B", "A", "E", "C", "A", "E", "C", "B", "A", "B", "E", "C", "A", "B", "E", "C")
sf_ord <- factor(x = sample, levels = c("A", "B", "C", "D", "E"), ordered = TRUE)

parts <- split(x = sample, f = sf_ord)

lengths(parts)
# A B C D E 
# 6 4 5 0 5 

v <- unsplit(value = parts, f = sf_ord)
str(v)
# chr [1:20] "A" "A" "E" "C" "B" "A" "E" "C" "A" "E" "C" "B" "A" "B" "E" "C" "A" "B" "E" "C"

Setzen der Levels als Intervalle bei einem quantitativen Merkmalstyp mit der Funktion cut()

Bisher wurde als Beispiel immer die Sonntagsfrage betrachtet, bei der ein nominaler Merkmalstyp vorliegt. In Anwendungen mit ordinalen (diskreten oder stetigen) Merkmalstypen entsteht häufig ein Problem, das man bei nominalen Merkmalstypen schwer diskutieren kann: Die Zahlenwerte der Rohdaten sind mit einer Genauigkeit gegeben, die man bei der Auswertung nicht berücksichtigen möchte; stattdessen begnügt man sich mit einer Vergröberung. Dabei wird der Wertebereich einer Größe in Intervalle unterteilt; ein Faktor soll dann angeben, in welchem Intervall jeder Messwert liegt.

Zum Beispiel könnte eine Temperatur-Messreihe gegeben sein, bei der Temperaturen im Lauf des Tages aufgenommen und dabei auf Zehntel Grad Celsius genau gemessen wurden:

12.4, 13.8, 15.0, 18.2, 20.3, 22.8, 21.1, 21.7, 18.6, 16.4

Für die Weiterverarbeitung werden die zehn Temperaturen T – etwas willkürlich – in drei Klassen eingeteilt:

kalt T ≤ 15
lau 15 < T ≤ 20
warm 20 < T

In Abbildung 1 sind sowohl die Messwerte als auch die drei Kategorien – angedeutet durch die Grenzen bei 15 und 20 °C – zu sehen. Man erkennt, dass ein Messwert genau auf einer Intervallgrenze liegt und daher seine Zuordnung besonders beachtet werden muss.

Abbildung 1: Die Temperatur-Messreihe zusammen mit den Grenzen der Intervalle, die die Klassen definieren.Abbildung 1: Die Temperatur-Messreihe zusammen mit den Grenzen der Intervalle, die die Klassen definieren.

Übergibt man die ursprüngliche Messreihe an die Funktion factor(), wird für jeden Zahlenwert eine Klasse angelegt, da kein Messwert mehrfach vorkommt. Wie kann man die Zuordnung der Messwerte zu je einer der drei Klassen realisieren? Man kann natürlich mit geeigneten Vektor-Operationen und logischen Abfragen einen Vektor der Länge 10 erzeugen, der die Zuordnung zu den Intervallen vornimmt.

Einfacher geht es mit der Funktion cut():

cut(x, breaks, labels = NULL,
        include.lowest = FALSE, right = TRUE, dig.lab = 3,
        ordered_result = FALSE, ...)

Hier werden nur die wichtigste Eingabewerte besprochen (default-Werte sind angegeben, sofern sie gesetzt sind):

  1. Der Vektor x , der für die Messwerte steht.
  2. Die Intervallgrenzen breaks , die unten am Beispiel näher erklärt werden.
  3. Es können sofort Bezeichnungen für die Levels gesetzt werden mit labels = NULL (per default werden als Bezeichnungen die Intervallgrenzen angegeben, siehe Beispiel unten).
  4. Mit right = TRUE wird festgelegt, ob die Intervalle rechts geschlossen und links offen sind (default-Wert) oder umgekehrt.
  5. Die Angabe, ob ein geordneter Faktor erzeugt werden soll mit ordered_result = FALSE

Der Rückgabewert der Funktion cut() ist ein Faktor, dessen Levels aus den Intervallen bestehen, die durch die Intervallgrenzen breaks festgelegt wurden.

Um die Intervalle zu definieren, werden die Grenzen eingegeben, dazu muss man wissen:

Im folgenden Skript wird zunächst der geordnete Faktor aus der ursprünglichen Temperatur-Messreihe erzeugt; es entstehen 10 Levels:

# Temperatur-Messreihe
temp <- c(12.4, 13.8, 15.0, 18.2, 20.3, 22.8, 21.1, 21.7, 18.6, 16.4)

f.temp <- ordered(x = temp)
f.temp
# [1] 12.4 13.8 15   18.2 20.3 22.8 21.1 21.7 18.6 16.4
# Levels: 12.4 < 13.8 < 15 < 16.4 < 18.2 < 18.6 < 20.3 < 21.1 < 21.7 < 22.8

Zeile 2: Eingabe der Temperatur-Messreihe.

Zeile 4: Definition eines geordneten Faktors ohne ausdrückliche Definition der Levels.

Zeile 5 bis 7: Die Levels sind jetzt die unterschiedlichen Temperatur-Messwerte aus der Messreihe.

Das folgende Skript zeigt zum Unterschied den Einsatz von cut(), womit die gewünschte Vergröberung erzeugt wird. Zunächst werden noch keine Bezeichnungen für die Intervalle vergeben, um den Einsatz von breaks zu erläutern:

# Temperatur-Messreihe
temp <- c(12.4, 13.8, 15.0, 18.2, 20.3, 22.8, 21.1, 21.7, 18.6, 16.4)

# Intervallgrenzen
brk <- c(-20, 15, 20, 40)

f.temp.coarse <- cut(x = temp, breaks = brk, ordered_result = TRUE)
f.temp.coarse
# [1] (-20,15] (-20,15] (-20,15] (15,20]  (20,40]  (20,40]  (20,40]  (20,40]  (15,20]  (15,20] 
# Levels: (-20,15] < (15,20] < (20,40]

table(f.temp.coarse)
# (-20,15]  (15,20]  (20,40] 
# 3        3        4

Zeile 2: Eingabe der Temperatur-Messreihe.

Zeile 5: Festlegen der Intervallgrenzen; da es drei Intervalle geben soll, müssen vier Grenzen als Vektor eingegeben werden (linker und rechter Rand der Temperatur-Skala müssen gesetzt werden).

Zeile 7: Aufruf der Funktion cut(), wobei drei Argumente gesetzt sind:

  1. Die Temperatur-Messreihe wird dem Argument x übergeben.
  2. Die Intervallgrenzen aus Zeile 5 werden dem Argument breaks übergeben.
  3. Durch ordered_result = TRUE wird ein geordneter Faktor erzeugt.

Zeile 8 bis 10: Die Ausgabe des Faktors zeigt, dass die Intervallgrenzen als Bezeichnung für die Levels eingesetzt werden. Man erkennt auch, dass die Intervalle links offen und rechts geschlossen sind (default-Wert von right). Die Ausgabe der Levels in Zeile 10 zeigt ihre Anordnung.

Zeile 12 bis 14: Die bekannte tabellarische Ausgabe des geordneten Faktors.

Das letzte Skript wird nahezu identisch wiederholt; zusätzlich werden Bezeichnungen für die Levels vergeben (Argument labels):

# Temperatur-Messreihe
temp <- c(12.4, 13.8, 15.0, 18.2, 20.3, 22.8, 21.1, 21.7, 18.6, 16.4)

# Intervallgrenzen
brk <- c(-20, 15, 20, 40)

# Vereinbarung von Namen für die Temperatur-Intervalle
lev <- c("cold", "mild", "warm")

f.temp.coarse <- cut(x = temp, breaks = brk, labels = lev, ordered_result = TRUE)
f.temp.coarse
# [1] cold cold cold mild warm warm warm warm mild mild
# Levels: cold < mild < warm

table(f.temp.coarse)
# cold mild warm 
# 3    3    4 

plot(x = f.temp.coarse, xlab = "Temperatur", ylab = "Häufigkeit", col = "blue", main = "Temperaturmessreihe")

Zeile 8: Jetzt werden Namen für die drei Levels vereinbart.

Zeile 10: In der Funktion cut() werden diese Namen für die Levels gesetzt; die anderen Argumente sind wie oben gesetzt.

Zeile 11: Die Ausgabe des Faktors erfolgt jetzt mit den vereinbarten Bezeichnungen (anstelle der Intervalle).

Zeile 15: Ebenso werden in der tabellarischen Ausgabe die Bezeichnungen der Levels eingesetzt.

Zeile 19: Erzeugen des Plots für den Faktor f.temp.coarse, der in Abbildung 2 zu sehen ist.

Abbildung 2: Die Temperatur-Messreihe mit den Häufigkeiten in den oben definierten Intervallen.Abbildung 2: Die Temperatur-Messreihe mit den Häufigkeiten in den oben definierten Intervallen.

Die Funktion unclass()

In Faktoren in R: der Datentyp factor wurde im Abschnitt Faktor als Kodierung eines Vektors erklärt, dass man sich einen Faktor auch als die Kodierung eines Vektors vorstellen kann. Denn intern wird der Vektor nicht mit seinen Levels sondern mit ganzen Zahlen gespeichert.

Die Beispiele zu cut() haben gezeigt, wie man eine Vergröberung definiert und dass man anschließend im Faktor nur noch die vereinbarten Bezeichnungen und nicht mehr die ursprünglichen Temperatur-Messwerte sieht. Benötigt man nicht die vereinbarten Bezeichnungen sondern nur die ganzen Zahlen, die intern verwendet werden, kann man mit Hilfe der Funktion unclass() auf diese Kodierung zugreifen. Im folgenden Skript wird wie oben die Temperatur-Messreihe vergröbert, anschließend wird ein Vektor v aus der vergröberten Messreihe gewonnen:

temp <- c(12.4, 13.8, 15.0, 18.2, 20.3, 22.8, 21.1, 21.7, 18.6, 16.4)
brk <- c(-20, 15, 20, 40)
lev <- c("cold", "mild", "warm")

# geordneter Faktor mit cut() und drei Intervallen
f.temp.coarse <- cut(x = temp, breaks = brk, labels = lev, ordered_result = TRUE)
f.temp.coarse
# [1] cold cold cold mild warm warm warm warm mild mild
# Levels: cold < mild < warm

v <- unclass(x = f.temp.coarse)
v
# [1] 1 1 1 2 3 3 3 3 2 2

str(v)
# atomic [1:10] 1 1 1 2 3 3 3 3 2 2
# - attr(*, "levels")= chr [1:3] "cold" "mild" "warm"

storage.mode(v)
# [1] "integer"

y <- as.vector(f.temp.coarse)
y
# [1] "cold" "cold" "cold" "mild" "warm" "warm" "warm" "warm" "mild" "mild"

Zeile 1 bis 9: Die Definition des geordneten Faktors wie oben mit den drei Bezeichnungen cold, mild, warm.

Zeile 11 bis 13: Mit der Funktion unclass() wird der kodierte Vektor erzeugt.

Zeile 15 bis 17: An der Ausgabe der Struktur von v erkennt man, dass das Attribut levels gesetzt ist, wodurch man den Faktor noch vollständig rekonstruieren kann (natürlich nicht die ursprüngliche Temperatur-Messreihe temp).

Zeile 19: Es handelt sich bei v tatsächlich um einen integer-Vektor.

Zeile 22 bis 24: Im Unterschied zu unclass() erzeugt die Funktion as.vector() aus dem Faktor einen character-Vektor mit den vereinbarten Bezeichnungen als Komponenten. Wenn die Bedeutung der Levels klar ist, wird man daher den mit unclass() erzeugten Vektor vorziehen.

Die Funktion tapply()

Oben wurde die Funktion split() vorgestellt, mit der ein Vektor in Teilvektoren zerlegt wurde, wobei die Zerlegung durch die Levels eines Faktors definiert wurde. Der Rückgabewert von split() ist eine Liste dieser Teilvektoren, wodurch die weitere Auswertung erschwert wird.

Die Funktion tapply() ist ein sehr mächtiges Instrument, das es erlaubt zwei Aufgaben gleichzeitig zu erledigen:

  1. Die Zerlegung eines Vektors in Teilvektoren durch einen Faktor (es können sogar mehrere Faktoren eingesetzt werden).
  2. Die Anwendung einer Funktion auf die Teilvektoren.

In den folgenden Unterabschnitten werden zwei Beispiele ausführlich erläutert, an denen die Arbeitsweise von tapply() deutlich werden sollte. Es wird dabei auch darauf eingegangen, welche Möglichkeiten die Funktion table() bietet und wie tapply() darüber hinausgeht.

Die Beispiele sind:

  1. Auswertung der Temperatur-Messreihe aus Abbildung 1.
  2. Auswertung eines Turnieres.

Im ersten Beispiel wird nur ein Faktor eingesetzt, um einen Vektor in Teilvektoren zu zerlegen, im zweiten Beispiel werden dann mehrere Faktoren eingesetzt.

Das Beispiel mit mehreren Faktoren erklärt dann auch das t im Namen der Funktion tapply(): es steht für table und soll darauf hinweisen, dass eine Funktion angewendet wird, um tabellarische Daten auszuwerten.

1. Beispiel: Auswertung der Temperatur-Messreihe

In Abbildung 1 war eine Temperatur-Messreihe zu sehen, wobei die Temperaturen in drei Intervalle eingeteilt wurden. Diese Einteilung wird durch einen Faktor vorgenommen; er wird erzeugt, indem man der Funktion cut() die Intervallgrenzen übergibt.

Eine naheliegende Fragestellung ist: Wie groß sind die Temperatur-Mittelwerte innerhalb der Teilvektoren?

Mit den bisher vorgestellten Funktionen lässt sich das Problem nur sehr umständlich lösen:

Die Funktion tapply() bietet die Möglichkeit, diese Auswertung ohne den Umweg über die Liste vorzunehmen.

Das folgende Skript zeigt zunächst die Vorbereitung (es wurde oben bei cut() schon angeführt und erläutert):

# Temperatur-Messreihe
temp <- c(12.4, 13.8, 15.0, 18.2, 20.3, 22.8, 21.1, 21.7, 18.6, 16.4)

# Intervallgrenzen
brk <- c(-20, 15, 20, 40)

# Vereinbarung von Namen für die Temperatur-Intervalle
lev <- c("cold", "mild", "warm")

f.temp.coarse <- cut(x = temp, breaks = brk, labels = lev, ordered_result = TRUE)
f.temp.coarse
# [1] cold cold cold mild warm warm warm warm mild mild
# Levels: cold < mild < warm

table(f.temp.coarse)
# cold mild warm 
# 3    3    4

Nun die eigentliche Anwendung der Funktion tapply(), deren Eingabewerte zuvor gezeigt werden:

tapply(X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)

Vorerst sind nur die ersten drei Argumente relevant:

  1. Der Vektor X.
  2. Der Faktor INDEX.
  3. Die Funktion FUN, die auf die Teilvektoren angewendet werden soll.

Das folgende Skript zeigt die Anwendung auf die Temperatur-Messreihe:

temp.mw <- tapply(X = temp, INDEX = f.temp.coarse, FUN = mean)

str(temp.mw)
# num [1:3(1d)] 13.7 17.7 21.5
# - attr(*, "dimnames")=List of 1
# ..$ : chr [1:3] "cold" "mild" "warm"

is.array(temp.mw)
# [1] TRUE

temp.mw
# cold     mild     warm 
# 13.73333 17.73333 21.47500 

tapply(X = temp, INDEX = f.temp.coarse, FUN = min)
# cold mild warm 
# 12.4 16.4 20.3 

tapply(X = temp, INDEX = f.temp.coarse, FUN = max)
# cold mild warm 
# 15.0 18.6 22.8

Zeile 1: Die Funktion tapply() wird mit den drei wichtigsten Argumenten aufgerufen:

Das Ergebnis von tapply() wird in der Variable temp.mw abgespeichert.

Zeile 3 bis 9: Die Struktur von temp.mw zeigt, dass es kein Vektor, sondern ein eindimensionales Feld ist; die Bezeichnungen der Levels sind als Attribut dimnames gesetzt.

Zeile 11 bis 13: Die drei Mittelwerte für die drei Temperatur-Intervalle werden per default mit 5 Stellen nach dem Komma ausgegeben.

Zeile 15 und 19: Anstelle der Funktion mean werden jetzt die Funktionen min beziehungsweise max an tapply() übergeben. Die Ergebnisse lassen sich leicht an Abbildung 1 nachvollziehen.

Man wird sich nach diesem Beispiel sicher fragen: Welche Funktionen können an die Funktion tapply() im Argument FUN übergeben werden? Es sind drei Arten von Funktionen möglich:

  1. Funktionen aus den Basis-Paketen oder anderen installierten Paketen.
  2. Selbstdefinierte Funktionen (werden erst besprochen, wenn erklärt wird, wie man selber Funktionen definiert).
  3. Anonyme Funktionen.

Für alle drei Arten von Funktionen gilt, dass sie einen geeigneten Eingabewert besitzen, um den Teilvektor zu verarbeiten, der beim Aufruf von FUN übergeben wird.

In den ersten beiden Fällen wird einfach der Name der Funktion an das Argument übergeben (wie oben in den Beispielen mean, max, min). Bei einer anyonymen Funktion – daher die Bezeichnung anonym – existiert kein Name; daher muss die Implementierung sofort erfolgen. Das folgende Skript zeigt das Beispiel für eine anonyme Funktion, die berechnet, bei welchem Index (innerhalb des jeweiligen Teilvektors) das Maximum der Temperatur angenommen wird:

# temp und f.temp.coarse wie oben

tapply(X = temp, INDEX = f.temp.coarse, FUN = function(X){which(X == max(X))} )
# cold mild warm 
# 3    2    2

Die Funktion mit Implementierung which(X == max(X)) berechnet für einen gegebenen Vektor X, den Index der maximalen Komponente (siehe Abbildung 1; der Messpunkt mit 15 °C gehört zum Intervall cold).

Die Bezeichnung X für den Funktionswert soll darauf hindeuten, dass der Eingabewert der anonymen Funktion aus dem Vektor X gebildet wird (nämlich der Teilvektor, der für jedes Intervall ausgewählt wird). Die Bezeichnung des Eingabewertes ist irrelevant und kann beliebig gewählt werden (hier y statt X):

tapply(X = temp, INDEX = f.temp.coarse, FUN = function(y){which(y == max(y))} )
# cold mild warm 
# 3    2    2

Dieses Beispiel mit der anonymen Funktion wirft sofort eine weitere Frage auf: Die Indizes, die berechnet werden, müssen nicht eindeutig sein. Wenn es mehrere pro Intervall gibt, können sie eigentlich nur zu einem Vektor zusammengefasst werden. Aber wie werden dann die (unterschiedlich langen) Vektoren der Temperatur-Intervalle zu einem Rückgabewert zusammengefasst?

Das folgende Beispiel verwendet eine Temperatur-Messreihe mit mehrdeutigem Maximum, aber den bekannten Faktor:

# alte Messreihe c(12.4, 13.8, 15.0, 18.2, 20.3, 22.8, 21.1, 21.7, 18.6, 16.4)
idx <- tapply(X = c(12.4, 13.8, 15.0, 19, 23, 23, 21.1, 21.7, 19, 16.4), INDEX = f.temp.coarse, FUN = function(y){which(y == max(y))} )

str(idx)
# List of 3
# $ cold: int 3
# $ mild: int [1:2] 1 2
# $ warm: int [1:2] 1 2
# - attr(*, "dim")= int 3
# - attr(*, "dimnames")=List of 1
# ..$ : chr [1:3] "cold" "mild" "warm"

idx
# $cold
# [1] 3
# 
# $mild
# [1] 1 2
# 
# $warm
# [1] 1 2

Zeile 1: Die Temperatur-Messreihe besteht wieder aus 10 Messwerten, die ersten drei liegen im ersten Intervall cold, es folgt ein Wert in cold, vier Werte in warm und nochmal zwei Werte in mild. Das Maximum wird einmal (15) und je zweimal (19 beziehungsweise 23) angenommen.

Zeile 3: Die Struktur von idx zeigt, dass die Ergebnisse für die drei Teilvektoren zu einer Liste mit drei Komponenten zusammengefasst werden.

Zeile 13: Die Ausgabe zeigt die entsprechenden Indizes.

Dieses letzte Beispiel hilft, das Argument simplify mit default-Wert TRUE zu verstehen:

2. Beispiel: Auswertung eines Turnieres

Die Turnier-Ergebnisse

In den Beispielen, die bisher vorgestellt wurden, wurde meist ein Merkmal gemessen und dieses wurde verwendet um einen Faktor zu bilden – um überhaupt mit dem Datentyp Faktor vertraut zu werden, wurden die Beispiele absichtlich so einfach gewählt. In der Praxis hat man meist komplexere Situationen: Bei einer Befragung werden mehrere Fragen gestellt und man wird Gruppierungen der Daten vornehmen wollen.

Dazu werde folgendes einfache Beispiel betrachtet:

Man kann jetzt folgende Fragen stellen:

  1. Wie groß ist die Summe der Punkte, die die Spieler eines Vereines erzielen?
  2. Wie groß ist die Summe der Punkte, die alle Junioren beziehungsweise alle Senioren erzielen?
  3. Wie groß ist die Summe der Punkte, die die Junioren beziehungsweise Senioren eines Vereines erzielen?

Es ist klar, dass man hier zwei Anforderungen hat:

  1. Die Daten, die das Ergebnis des Turniers beschreiben, müssen geeignet gruppiert werden.
  2. Die Funktion sum(), mit der man die Punktesumme für eine Gruppe von Spielern berechnen kann, muss an die entsprechende Gruppe weitergereicht werden.

Für die erste Anforderung sind die Faktoren zuständig, für die zweite Anforderung gibt es die Funktion tapply(), die im ersten Beispiel zur Auswertung der Temperatur-Messreihe schon vorgestellt wurde. Wie man sie für komplexere Fragen einsetzt, wird im Folgenden gezeigt.

Das Turnier-Ergebnis ist in der Tabelle unten dargestellt (Namen der Teilnehmer sind nicht genannt, sie können durch ihre Platzierung eindeutig identifiziert werden):

Platz Punkte Verein Altersklasse
1 28 A J
2 27 C S
3 27 G S
4 24 B S
5 23 A S
6 23 B J
7 21 D S
8 18 A J
9 18 F J
10 18 E S
11 17 C J
12 17 D S
13 17 B S
14 16 G J
15 14 B S
16 14 C J
17 13 E J
18 13 D S
19 13 A J
20 11 B S
21 11 G J
22 10 E S
23 9 F S
24 9 F S
25 8 D J
26 8 C S
27 4 F S
28 3 A J
29 1 B S
30 0 G S

Beschreibung des Turnier-Ergebnisses mit R

Die Ergebnisse des Turniers könnten durch drei Vektoren score, club und age gegeben sein; die drei Vektoren stehen für die erzielten Punkte, die Vereinszugehörigkeit und die Altersklasse und sind jeweils entsprechend der Platzierung sortiert. Da es 30 Teilnehmer gibt, haben diese drei Vektoren jeweils die Länge 30. Die Vektoren werden zu einem Data Frame result zusammengefasst:

score <- c(28, 27, 27, 24, 23, 23, 21, 18, 18, 18, 17 , 17, 17, 16, 14, 14, 13, 13, 13, 11, 11, 10, 9, 9, 8, 8, 4, 3, 1, 0)
club <- c("A", "C", "G", "B", "A", "B", "D", "A", "F", "E", "C", "D", "B", "G", "B", "C", "E", "D", "A", "B", "G", "E", "F", "F", "D", "C", "F", "A", "B", "G")
age <- c("J", "S", "S", "S", "S", "J", "S", "J", "J", "S", "J", "S", "S", "J", "S", "J", "J", "S", "J", "S", "J", "S", "S", "S", "J", "S", "S", "J", "S", "S")

length(score)           # 30
length(club)            # 30
length(age)             # 30
sum(score)              # 435 = 15*29 = 29 + 28 + ... + 1

result <- data.frame(score, club, age)

str(result)
# 'data.frame': 30 obs. of  3 variables:
#   $ score: num  28 27 27 24 23 23 21 18 18 18 ...
# $ club : Factor w/ 7 levels "A","B","C","D",..: 1 3 7 2 1 2 4 1 6 5 ...
# $ age  : Factor w/ 2 levels "J","S": 1 2 2 2 2 1 2 1 1 2 ...

Zeile 1 bis 3: Die Spalten aus der Tabelle des Turnier-Ergebnisses als Vektoren.

Zeile 5 bis 8: Die Längen der Vektoren sind jeweils gleich 30. Da jeder gegen jeden spielt, ist die Summe alle erzielten Punkte gleich 15 · 29 = 435.

Zeile 10: Die Vektoren werden zu einem Data Frame result zusammengefasst.

Zeile 12 bis 16: Die Ausgabe der Struktur von result zeigt, dass der Vektor score unverändert übernommen wurde und die beiden character-Vektoren club und age in Faktoren verwandelt wurden.

Definition von Faktoren für das Turnier-Ergebnis

Um Fragen der Art wie sie oben gestellt wurden zu beantworten, benötigt man Faktoren; da beim Erzeugen eines Data Frame character-Vektoren in Faktoren verwandelt werden, muss man eigentlich nur den geordneten Faktor für die Punktzahlen erzeugen:

# Data Frame result wie oben

f.score <- factor(x = score, ordered = TRUE)
f.score
# [1] 28 27 27 24 23 23 21 18 18 18 17 17 17 16 14 14 13 13 13 11 11 10 9  9  8  8  4  3  1  0 
# Levels: 0 < 1 < 3 < 4 < 8 < 9 < 10 < 11 < 13 < 14 < 16 < 17 < 18 < 21 < 23 < 24 < 27 < 28

f.club <- result[ , "club"]
f.club
# [1] A C G B A B D A F E C D B G B C E D A B G E F F D C F A B G
# Levels: A B C D E F G

f.age <- result[ , "age"]
f.age
# [1] J S S S S J S J J S J S S J S J J S J S J S S S J S S J S S
# Levels: J S

Wie oben beschrieben: Die Faktoren f.club und f.age müssen eigentlich nicht definiert werden; damit die Namen einheitlich sind, geschieht es trotzdem.

Die Funktion table für ein beziehungsweise zwei Faktoren

Mit Hilfe der Funktion table() kann man jetzt die Gruppierung anzeigen, die durch die drei Faktoren gegeben ist:

table(f.score)
# f.score
# 0  1  3  4  8  9 10 11 13 14 16 17 18 21 23 24 27 28 
# 1  1  1  1  2  2  1  2  3  2  1  3  3  1  2  1  2  1 

table(f.club)
# f.club
# A B C D E F G 
# 5 6 4 4 3 4 4 

table(f.age)
# f.age
# J  S 
# 12 18 

# Plots
plot(x = f.club, xlab = "Verein", ylab = "Häufigkeit", col = "blue", main = "Vereinszugehörigkeit")
plot(x = f.age, xlab = "Altersklasse", ylab = "Häufigkeit", col = "red", main = "Altersklasse der Teilnehmer", ylim = c(0, 20))

In Abbildung 3 und 4 sind die Häufigkeiten der Vereinszugehörigkeit und der Altersklassen dargestellt (die Diagramme wurden mit den Anweisungen in Zeile 17 und 18 erzeugt).

Abbildung 3: Die Vereinszugehörigkeit der Turnier-Teilnehmer.Abbildung 3: Die Vereinszugehörigkeit der Turnier-Teilnehmer.

Abbildung 4: Die Häufigkeiten der Altersklassen.Abbildung 4: Die Häufigkeiten der Altersklassen.

Man kann der Funktion table() aber auch zwei Faktoren übergeben:

table(f.score, f.club)
# f.club
# f.score A B C D E F G
# 0  0 0 0 0 0 0 1
# 1  0 1 0 0 0 0 0
# 3  1 0 0 0 0 0 0
# 4  0 0 0 0 0 1 0
# 8  0 0 1 1 0 0 0
# 9  0 0 0 0 0 2 0
# 10 0 0 0 0 1 0 0
# 11 0 1 0 0 0 0 1
# 13 1 0 0 1 1 0 0
# 14 0 1 1 0 0 0 0
# 16 0 0 0 0 0 0 1
# 17 0 1 1 1 0 0 0
# 18 1 0 0 0 1 1 0
# 21 0 0 0 1 0 0 0
# 23 1 1 0 0 0 0 0
# 24 0 1 0 0 0 0 0
# 27 0 0 1 0 0 0 1
# 28 1 0 0 0 0 0 0

table(f.score, f.age)
# f.age
# f.score J S
# 0  0 1
# 1  0 1
# 3  1 0
# 4  0 1
# 8  1 1
# 9  0 2
# 10 0 1
# 11 1 1
# 13 2 1
# 14 1 1
# 16 1 0
# 17 1 2
# 18 2 1
# 21 0 1
# 23 1 1
# 24 0 1
# 27 0 2
# 28 1 0

t.club.age <- table(f.club, f.age)
t.club.age
# f.age
# f.club J S
# A 4 1
# B 1 5
# C 2 2
# D 1 3
# E 1 2
# F 1 3
# G 2 2

# Plot der Tabelle table(f.club, f.age)
plot(x = t.club.age,  xlab = "Verein", ylab = "Altersklasse", col = "blue", main = "Vereinszugehörigkeit/Altersklasse", lwd = 2)

Abbildung 5 zeigt den Zusammenhang zwischen der Vereinszugehörigkeit und der Altersklasse, der in der letzten Tabelle ausgegeben wurde (Zeile 58).

Abbildung 5: Zusammenhang zwischen der Vereinszugehörigkeit und der Altersklasse. Absolute Anzahlen sind jetzt nicht mehr erkennbar. Stattdessen kann man ablesen, welcher Anteil an Junioren beziehungsweise Senioren jeder Verein stellt.Abbildung 5: Zusammenhang zwischen der Vereinszugehörigkeit und der Altersklasse. Absolute Anzahlen sind jetzt nicht mehr erkennbar. Stattdessen kann man ablesen, welcher Anteil an Junioren beziehungsweise Senioren jeder Verein stellt.

Aufgabe:

Wird in der Berechnung der Tabelle in Zeile 45 die Reihenfolge der Eingabewerte vertauscht, so wird die Tabelle anders ausgegeben und das Diagramm anders dargestellt. Beschreiben Sie die Unterschiede.

Auswertung mit Hilfe der Funktion tapply()

Mit diesen Tabellen aus dem letzten Skript sind aber noch nicht die Fragen von oben beantwortet; sie sind hier nochmal wiedergegeben:

  1. Wie groß ist die Summe der Punkte, die die Spieler eines Vereines erzielen?
  2. Wie groß ist die Summe der Punkte, die alle Junioren beziehungsweise alle Senioren erzielen?
  3. Wie groß ist die Summe der Punkte, die die Junioren beziehungsweise Senioren eines Vereines erzielen?

Man könnte jetzt von Hand die gesuchten Punktzahlen bestimmen und addieren. Einfacher ist es mit der Funktion tapply().

# 1. Wie groß ist die Summe der Punkte, die die Spieler eines Vereines erzielen?
p.club <- tapply(X = score, INDEX = f.club, FUN = sum)
p.club
# A  B  C  D  E  F  G 
# 85 90 66 59 41 40 54 

# Sortierung
sort(p.club, decreasing = TRUE)
# B  A  C  D  G  E  F 
# 90 85 66 59 54 41 40

Zeile 2: Der Funktion tapply() wird als auszuwertender Vektor die Punktzahl score übergeben, der Faktor, der dafür sorgt, dass gemäß der Vereinszugehörigkeit gruppiert wird und die Funktion sum(), um die Punktesumme zu berechnen.

Zeile 3 bis 5: Die berechneten Punkte werden unmittelbar ausgegeben.

Zeile 8 bis 10: Zur leichteren Interpretation werden die Punkte absteigend sortiert.

Man erkennt, dass der Verein B in der Summe die meisten Punkte erzielt, Verein F die wenigsten Punkte.

# 2. Wie groß ist die Summe der Punkte, die alle Junioren beziehungsweise alle Senioren erzielen?
p.age <- tapply(X = score, INDEX = f.age, FUN = sum)
p.age
# J   S 
# 182 253

Eine Sortierung ist hier nicht nötig.

Im Unterschied zum vorherigen Skript wird hier lediglich der Faktor f.age eingesetzt.

# 3. Wie groß ist die Summe der Punkte, die die Junioren beziehungsweise Senioren eines Vereines erzielen?
p.age.club <- tapply(X = score, INDEX = list(f.age, f.club), FUN = sum)
p.age.club
#    A  B  C  D  E  F  G
# J 62 23 31  8 13 18 27
# S 23 67 35 51 28 22 27

Zeile 2: Beim Aufruf der Funktion tapply() müssen jetzt zwei Faktoren an das Argument INDEX übergeben werden. Dies geschieht nicht mit c(f.age, f.club) , sondern mit list(f.age, f.club) . Denn Faktoren sind keine Vektoren, die mit c() aneinandergehängt werden können. Und in der Dokumentation zu tapply() steht ausdrücklich, dass INDEX eine Liste von einem oder mehreren Faktoren ist.

Vertauscht man die Reihenfolge der Faktoren in der Liste, wird die transponierte Tabelle ausgegeben.

Zeile 3 bis 6: Es werden jetzt alle möglichen Kombinationen von Vereinszugehörigkeiten und Altersklassen gebildet und deren Punktesumme berechnet. Man erkennt zum Beispiel, dass die Senioren des Vereins B die beste dieser Gruppen sind.

Oben wurde bereits erwähnt, dass die Funktion tapply() als Rückgabewert keinen Vektor besitzt sondern ein Feld (bei simplify = TRUE ). Das letzte Beispiel sollte klarmachen, warum dies sinnvoll ist: Da jetzt die Gruppierung durch zwei Faktoren erfolgt, müssen die berechneten Summen in einem zweidimensionalen Feld angeordnet werden.

Zusammenfassung

Die folgende Tabelle zeigt eine Kurzbeschreibung der in diesem Kapitel vorgestellten Funktionen. Man beachte dabei aber, dass die gezeigten Argumente meist nicht die komplette Liste der Eingabewerte darstellt. Es werden immer nur diejenigen Eingabewerte gezeigt, die hier auch besprochen wurden. Für die allgemeine Verwendung der Funktionen ist es erforderlich die Dokumentation zu Rate zu ziehen.

Funktion Beschreibung
head(x, n) Es werden nur die ersten n Komponenten des Faktors x verwendet.
tail(x, n) Es werden nur die letzten n Komponenten des Faktors x verwendet.
split(x, f) Der Vektor x wird gemäß der Levels des Faktors f in Teilvektoren aufgespalten.
unsplit(value, f) Die Liste von Vektoren value wird gemäß dem Faktor f zu einem Vektor zusammengesetzt.
cut(x, breaks, labels = NULL, include.lowest = FALSE, right = TRUE, dig.lab = 3, ordered_result = FALSE, ...) Der Vektor x wird in einen Faktor verwandelt. Dessen Levels sind durch die Intervallgrenzen breaks gegeben. Die Bezeichnungen der Levels werden durch labels gesetzt, right gibt an, ob die Intervalle rechts abgeschlossen und links offen sind, mit ordered_result kann ein geordneter Faktor erzeugt werden.
unclass(x) Der Faktor x ist intern mit ganzen Zahlen kodiert (die Levels werden durchnumeriert); mit unclass() kann man auf den kodierten Vektor zugreifen.
?class Weitere Informationen zu unclass().
tapply(X, INDEX, FUN) Ein Vektor X wird gemäß der Levels des Faktors INDEX gruppiert (INDEX kann auch eine Liste mit mehreren Faktoren sein); auf jede Gruppe wird die Funktion FUN angewendet.

Die folgende Tabelle beschreibt die Rückgabewerte der Funktionen und ihre Datentypen:

Funktion Rückgabewert Datentyp
head(x, n) Die ersten n Komponenten des Faktors x. Faktor der Länge n (vorderer Teil von x)
tail(x, n) Die letzten n Komponenten des Faktors x. Faktor der Länge n (hinterer Teil von x)
split(x, f) Liste von Teilvektoren von x (Gruppierung gemäß dem Faktor f). Liste von Vektoren
unsplit(value, f) Vektor zusammengesetzt aus den Teilvektoren in value gemäß dem Faktor f. Vektor
cut(x, breaks, labels = NULL, include.lowest = FALSE, right = TRUE, dig.lab = 3, ordered_result = FALSE, ...) (ungeordneter oder geordneter) Faktor mit Intervallen als Levels (gegeben durch die Intervallgrenzen breaks) Faktor oder geordneter Faktor
unclass(x) Ganzzahlige Kodierung des Faktors x. Vektor
tapply(X, INDEX, FUN) Feld, wobei jede Komponente dadurch entsteht, dass die Funktion FUN auf eine Gruppe des Vektors X angewendet wird (Gruppierung gemäß Faktor INDEX). Feld (array)