3 Deskriptive Statistik in R

Visualisierung ist natürlich schon gut um einen ersten schnellen Überblick über Daten zu bekommen, aber früher oder später geht es ans Eingemachte und es müssen harte und genaue Zahlen her. Einige der wichtigen Zahlen, die unsere Daten beschreiben sind sogenannte deskriptive Statistiken. Darunter fallen so einfache Dinge wie der Mittelwert, aber auch andere Werte wie Standardabweichung, Median, Minimum, Maximum und Quartile oder auch seltener gebrauchte Werte wie der Modus, Kurtosis und Varianz.

3.1 Lernziele

In diesem Kapitel werden folgende Themen besprochen:

  • deskriptive Statistiken in R erstellen
  • nach Gruppen aufgeteilte deskriptive Statistiken erstellen
  • „tidyverse“-Funktionen nutzen, um tiefere Einblicke in die Daten zu erhalten
  • Boxplots erstellen und verstehen
  • nach Gruppen getrennte Grafiken erstellen

3.2 Deskriptive Statistiken mit Basis R

Erfreulicherweise lassen sich die meisten der Werte für einfache deskriptive Statistiken mit Basis R Funktionen berechnen. Die entsprechenden Funktionen sind nach dem zu berechnenden Wert benannt und im Grunde selbsterklärend.

min(daten$dlp)
## [1] 1.2
mean(daten$dlp)
## [1] 1126.547
median(daten$dlp)
## [1] 576
max(daten$dlp)
## [1] 5979.3

Es wäre natürlich insbesondere in großen Tabellen müßig, jede Spalte einzeln aufzurufen. Die Funktion summary() hilft hier und berechnet diese Werte und einige mehr gleich für alle Spalten eines Dataframes. Besonders praktisch hieran: für Spalten, die keine numerischen Werte enthalten, erhält man trotzdem einige praktische quantitative Werte.

summary(daten)
##       tag             untersuchung             kv             mas        
##  Min.   :2018-01-01   Length:2441        Min.   : 12.0   Min.   :   2.0  
##  1st Qu.:2018-02-24   Class :character   1st Qu.:120.0   1st Qu.:  92.0  
##  Median :2018-04-13   Mode  :character   Median :120.0   Median : 150.0  
##  Mean   :2018-04-14                      Mean   :114.5   Mean   : 156.9  
##  3rd Qu.:2018-06-04                      3rd Qu.:120.0   3rd Qu.: 206.0  
##  Max.   :2018-07-31                      Max.   :801.0   Max.   :1231.0  
##     ctdi.vol          dlp        
##  Min.   : 0.05   Min.   :   1.2  
##  1st Qu.: 5.50   1st Qu.: 244.6  
##  Median : 8.00   Median : 576.0  
##  Mean   :10.30   Mean   :1126.5  
##  3rd Qu.:14.30   3rd Qu.:1795.3  
##  Max.   :39.60   Max.   :5979.3

Mit den zwei Funktionen str(), die uns einen Überblick über die Struktur eines Dataframes gibt, und der summary() Funktion ergibt sich in den meisten Fällen ein recht guter Eindruck der vorliegenden Daten.

Aber oftmals ist nicht so sehr der Überblick über die gesamten Daten gefragt, vielmehr wären nach Gruppen getrennte deskriptive Statistiken von Interesse. Hierzu bietet Basis R die Funktion by, die in der Benutzung zwar etwas unintuitiv ist, aber genau diese Funktionsweise abbildet. Als ersten Parameter erwartet by() einen Vektor (meist also eine Spalte eines Dataframes), auf den nach dem als zweiten Parameter übergebenen Gruppenvektor die als dritter Parameter übergebene Funktion angewandt wird.

# erster Parameter: die zu untersuchenden Werte
# zweiter Parameter: die Gruppenvariable
# dritter Parameter: die anzuwendende Funktion (ohne runde Klammern!)
by(daten$dlp, daten$untersuchung, median)
## daten$untersuchung: Abdomen
## [1] 714.85
## ------------------------------------------------------------ 
## daten$untersuchung: Becken
## [1] 341.8
## ------------------------------------------------------------ 
## daten$untersuchung: Gesicht
## [1] 379.4
## ------------------------------------------------------------ 
## daten$untersuchung: Kopf
## [1] 1433
## ------------------------------------------------------------ 
## daten$untersuchung: Thorax
## [1] 219
## ------------------------------------------------------------ 
## daten$untersuchung: Traumaspirale
## [1] 2674.9
# die Ausgabe kann bei vielen Gruppen und Funktionen, die lange
# Ausgaben produzieren mitunter recht unübersichtlich werden.
# Die Ausgabe dieses Befehls wird deshalb hier nicht gezeigt.
by(daten, daten$untersuchung, summary)

3.3 Deskriptive Statistiken im Tidyverse

Das Berechnen deskriptiver Statistiken ist keine Aufgabe, für die im Tidyverse per se spezielle Funktionen bereitstehen. Trotzdem bieten einige Funktionen umfangreiche Möglichkeiten fast spielerisch mit den Daten zu interagieren und einfach und schnell Daten zu selektieren, zu gruppieren und auszuwerten.

library(tidyverse)

Die wichtigsten Funktionen sind hierfür die Funktionen select() (wählt Spalten aus), filter() (wählt Zeilen aus), group_by() (bildet Gruppen) und summarise() (führt Berechnungen für Gruppen aus). Mithilfe von arrange() können wir die entstehende Ausgabe auch noch nach einer Spalte sortieren. Diese Funktionen lassen sich in nahezu beliebiger Weise mit %>% verketten und sind trotzdem relativ einfach zu lesen.

Für ein erstes Beispiel wollen wir zum Beispiel unsere Daten nach Untersuchungsbeschreibung gruppieren, dann den Median von CTDI (in mGy wie in der ursprünglichen Tabelle und direkt auch durch 10 geteilt in cGy) und DLP berechnen, sowie die Anzahl der jeweiligen Untersuchungen. Und um die Daten noch leichter fassbar zu machen, soll die Ausgabe nach dem Median des CTDI sortiert werden. Der Übersichtlichkeit halber lohnt es sich statt lange Zeilen zu schreiben, die einzelnen Teile der Befehle auf mehrere Zeilen aufzuteilen.

daten %>%
  group_by(untersuchung) %>%
  summarise(
    median_ctdi_mGy = median(ctdi.vol),
    median_ctdi_cGy = median(ctdi.vol) / 10,
    median_dlp = median(dlp),
    anzahl = n()
    ) %>%
  arrange(-median_ctdi_mGy)
## # A tibble: 6 × 5
##   untersuchung  median_ctdi_mGy median_ctdi_cGy median_dlp anzahl
##   <chr>                   <dbl>           <dbl>      <dbl>  <int>
## 1 Traumaspirale            16.9            1.69      2675.    273
## 2 Gesicht                  12.2            1.22       379.     97
## 3 Abdomen                  10.0            1.01       715.    682
## 4 Becken                    9.3            0.93       342.     49
## 5 Kopf                      7.7            0.77      1433     523
## 6 Thorax                    4.8            0.48       219     817

In ähnlicher Weise könnten wir auch bspw. zunächst die Daten nach Untersuchungen filtern, bei denen genau 100kV am Gerät eingestellt waren, dann einige der Werte aus dem letzten Beispiel berechnen und zuletzt in der Ausgabe nur diejenigen Untersuchungensbezeichnungen auflisten, die weniger als 100 mal mit 100kV durchgeführt wurden. Zu guter letzt könnte man auch noch die Spalte Anzahl aus der Ausgabe entfernen, sodass nur die berechneten Werte bleiben.

daten %>%
  filter(kv == 100) %>%
  group_by(untersuchung) %>%
  summarise(
    median_ctdi_mGy = median(ctdi.vol),
    median_dlp = median(dlp),
    anzahl = n()
    ) %>%
  filter(anzahl < 100) %>%
  select(-anzahl)
## # A tibble: 4 × 3
##   untersuchung  median_ctdi_mGy median_dlp
##   <chr>                   <dbl>      <dbl>
## 1 Abdomen                  5.95       796.
## 2 Becken                  11.2        474.
## 3 Gesicht                 12.2       1194.
## 4 Traumaspirale           10.7       2448.

Die Stärke des Tidyverse liegt hier sicher in der Möglichkeit, interaktiv Befehle zu verketten. Man schreibt einige Befehle auf, von denen man glaubt, dass sie das gewünschte Ergebnis bringen könnten oder den Weg dorthin darstellen, führt diese aus und betrachtet die Ausgabe. Dann passt man die Befehle an oder erweitert die Kette um weitere bis man schließlich das gewünschte Ergebnis erhält.

3.4 Deskriptive Statistiken mit anderen Paketen

Wie bereits erwähnt, ist eine der herausragenden Eigenschaften von R, dass unzählige Pakte existieren, die verschiedene Funktionen bereitstellen und so das Arbeiten vereinfachen. Zwei sehr gute Pakete für deskriptive Statisiken sind psych und summarytools. Diese können, wie Tidyverse im vorherigen Kapitel, über die entsprechenden Funktionen von RStudio oder über die Konsole installiert werden.

3.4.1 Psych

Aus dem Paket psych können wir die Funktion describe() benutzen, um eine Vielzahl von Maßzahlen zu berechnen.

library(psych)
describe(daten)
##               vars    n    mean      sd median trimmed    mad   min    max
## tag              1 2441     NaN      NA     NA     NaN     NA   Inf   -Inf
## untersuchung*    2 2441    3.64    1.81      4    3.68   1.48  1.00    6.0
## kv               3 2441  114.47   18.77    120  117.36   0.00 12.00  801.0
## mas              4 2441  156.93   92.65    150  150.06  85.99  2.00 1231.0
## ctdi.vol         5 2441   10.30    6.98      8    9.44   5.93  0.05   39.6
## dlp              6 2441 1126.55 1142.93    576  960.73 622.10  1.20 5979.3
##                 range  skew kurtosis    se
## tag              -Inf    NA       NA    NA
## untersuchung*    5.00 -0.49    -1.30  0.04
## kv             789.00 19.45   731.66  0.38
## mas           1229.00  1.79    13.34  1.88
## ctdi.vol        39.55  1.21     1.40  0.14
## dlp           5978.10  1.18     0.51 23.13

Die üblichen Quartile erhält man, wenn man der describe()-Funktion einen entsprechenden zusätzlichen Parameter übergibt. Ebenfalls kann man describe() mitteilen, dass nicht-numerische Werte übersprungen werden sollen.

describe(daten, quant=c(.25,.75), omit = TRUE)
##          vars    n    mean      sd median trimmed    mad   min    max   range
## kv          3 2441  114.47   18.77    120  117.36   0.00 12.00  801.0  789.00
## mas         4 2441  156.93   92.65    150  150.06  85.99  2.00 1231.0 1229.00
## ctdi.vol    5 2441   10.30    6.98      8    9.44   5.93  0.05   39.6   39.55
## dlp         6 2441 1126.55 1142.93    576  960.73 622.10  1.20 5979.3 5978.10
##           skew kurtosis    se Q0.25  Q0.75
## kv       19.45   731.66  0.38 120.0  120.0
## mas       1.79    13.34  1.88  92.0  206.0
## ctdi.vol  1.21     1.40  0.14   5.5   14.3
## dlp       1.18     0.51 23.13 244.6 1795.3

Eine ebenfalls sehr praktische Funktion ist die der Base-R Funktion by() (s.o.) nachempfundenen describe.by() Funktion. Dieser können die selben Parameter mitgegeben werden, wie der einfachen describe() Funktion.

# Da auch hier die Ausgabe sehr lang würde, wird sie hier nicht abgebildet.
describe.by(daten, daten$untersuchung, quant=c(.25,.75), omit = TRUE)

3.4.2 Summarytools

Auch im Paket summarytools existieren Funktionen, die ähnliche Ausgaben erzeugen wie die Funktion describe() aus dem psych Paket. Welche Pakete man letztenendes benutzen will, ist Frage des persönlichen Geschmacks. Die entsprechenden Funktionen in summarytools sind dscr() bzw. stby()

library(summarytools)
descr(daten)
## Descriptive Statistics  
## daten  
## N: 2441  
## 
##                     ctdi.vol       dlp        kv       mas
## ----------------- ---------- --------- --------- ---------
##              Mean      10.30   1126.55    114.47    156.93
##           Std.Dev       6.98   1142.93     18.77     92.65
##               Min       0.05      1.20     12.00      2.00
##                Q1       5.50    244.60    120.00     92.00
##            Median       8.00    576.00    120.00    150.00
##                Q3      14.30   1795.30    120.00    206.00
##               Max      39.60   5979.30    801.00   1231.00
##               MAD       5.93    622.10      0.00     85.99
##               IQR       8.80   1550.70      0.00    114.00
##                CV       0.68      1.01      0.16      0.59
##          Skewness       1.21      1.18     19.45      1.79
##       SE.Skewness       0.05      0.05      0.05      0.05
##          Kurtosis       1.40      0.51    731.66     13.34
##           N.Valid    2441.00   2441.00   2441.00   2441.00
##         Pct.Valid     100.00    100.00    100.00    100.00
# Da auch hier die Ausgabe sehr lang würde, wird sie hier nicht abgebildet.
stby(daten, daten$untersuchung, descr)

Noch viel praktischer, aber leider nicht nach Gruppen aufgetrennt durchführbar, ist die Funktion dfSummary(), insbesondere wenn man ihre Ausgabe weiterleitet an die Funktion view() aus dem summarytools Paket. Diese produziert dann eine ansprechend formatierte HTML-Datei, in der verschiedene Maßzahlen aufgeführt werden.

Um nach Gruppen getrennte Ausgaben zu bekommen, kann man beispielsweise die tidyverse filter() Funktion benutzen, muss dann aber die Befehle für jede Gruppe erneut ausführen.

# die Ausgaben dieser Funktionen erscheinen nicht auf der Konsole,
# sondern im Viewer-Pane von RStudio. Dort findet man auch einen Button,
# um die Datei in einem Internetbrowser zu öffnen.

# einfacher Fall, um einen Überblick über alle Daten zu erhalten
dfSummary(daten) %>% view()

# innerhalb einer tidyverse-Kette  um bspw. einen Überblick über
# die Traumaspiralen zu erhalten
daten %>%
  filter(untersuchung == "Traumaspirale") %>%
  dfSummary() %>%
  view()

3.5 Mehr bunte Bilder

Um deskriptive Statistiken auch grafisch nach Gruppen zu trennen, bietet sich die Funktion facet_wrap() an, die ebenfalls Teil von ggplot2 ist. Im Prinzip kann man sich die Funktionsweise ähnlich wie die by() Funktion vorstelen, wobei die Handhabung noch etwas simpler ist. Man definiert einfach wie gewohnt die Grafik die man mit ggplot() erstellen möchte, und fügt facet_wrap() einfach mit + in die Kette der Grafikfunktionen. Innerhalb von facet_wrap() muss noch etwas umständlich die entsprechende Gruppenvariable in die Funktion vars() eingeschlossen werden, aber die Ergebnisse sind sehr ansprechend.

# ähnliche Grafik wie am Ende des letzten Kapitels, aber mit
# nach Untersuchung getrennten Ausgaben, damit die Punkte
# weniger überlappen.
daten %>% 
  ggplot(aes(x = ctdi.vol, y = dlp, color = untersuchung)) +
    geom_point() +
    facet_wrap(vars(untersuchung)) +
    labs(
      x = "CTDIvol in mGy",
      y = "DLP in mGy*cm",
      title = "Zusammenhang CTDIvol / DLP",
      color = "Untersuchungsart"
    ) +
    theme_bw()

Natürlich funktioniert facet_wrap() nicht nur mit Punktwolken, sondern letztlich mit beliebigen Grafiken. Da sich für deskriptive Statistiken insbesondere Boxplots anbieten, im nachfolgenden Beispiel eine etwas komplexere Grafik. In dem Fall wird zunächst die Variable kv mithilfe der Funktion mutate() statt als numerische Variable als ordinale Variable definiert. Zur besseren Übersichtlichkeit werden nur die Fälle betrachtet mit 80kV, 100kV und 120kV. Der Rest ist dann wie gewohnt die Definition der Grafik und das Aufteilen mittels facet_wrap().

daten %>% 
  mutate(kv = as.factor(kv)) %>% 
  filter(kv %in% c(80, 100, 120)) %>% 
  ggplot(aes(x = kv, y = dlp, fill = kv)) +
    geom_boxplot() +
    facet_wrap(vars(untersuchung)) +
    labs(
      x = "kV-Einstellung",
      y = "DLP in mGy*cm",
      title = "DLP in Abhängigkeit von kV-Einstellung",
      fill = "kV-Einstellung"
    ) +
    theme_bw()