Tidyverse and NHANES

Vorlesung “Datenanalyse in der Biologie”

Einführung zu Tidyverse

Geschichte von R

R ist alt: Die erste Version wurde 1993 veröffentlicht, und war anfangs eine “Kopie” von S, das seit 1976 entwickelt wurde. Viele Konstrukte sind daher schon lange nicht mehr “state of the art”. Andererseits wurde R immer wieder modernisiert, und es wurden neue, bequemer zu benutzende Funktionen eingeführt, um ältere, umständlichere oder weniger leistungsfähige Funktionen und Pakete zu ersetzen – ohne aber die alten Funktionen zu entfernen. Wenn man R-Code verstehen will, hilft es daher oft, zu wissen, in welcher “geschichtlichen Schicht” man sich bewegt.

Tidyverse

Die “aktuelle Schicht” ist das “Tidyverse”, entwickelt von Hadley Wickham, seit ca. 2014 und weit verwendet seit ca. 2017.

“Tidyverse” ist ein Versuch, das “unordentlich” gewordene “Universum” der vielen R-Funktionen aus verschiedenen Zeiten zu ersetzen. “Tidyverse” besteht aus einer Reihe von Paketen, die einer einheitlichen und logischen Design-Philosophie folgen.

Zentraler Datentyp in Tidyverse ist die Tabelle, die aber oft nicht (wie in “base R”) Data Frame genannt wird, sondern “tibble” (Kunstwort für “tidy table”).

Das Buch “R for Data Science” (“r4ds”) dient als Lehrbuch zur Datenanalyse mit R und Tidyverse.

Wir werden von nun an so weit wie möglich nur Tidyverse verwenden.

Um Tidyverse zu verwenden, muss es stets erst geladen werden:

library( tidyverse )
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.3     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.4.4     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.0
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

NHANES

Als Beispieldaten werden wir in der Vorlesung oft Ergebnisse der NHANES-Studie des CDC nutzen. NHANES führt alle 2 Jahre eine Erhebung zur VOlksgesundheit durch, bei der eine repräsentante Stichprobe von knapp 10,000 Einwohnern der USA befragt und medizinisch untersucht werden. Näheres hier: https://www.cdc.gov/nchs/nhanes

Wir verwenden den Durchgang 2017/18 (“J”), spezifisch die Tabellen “DEMO_J” und “BMX_J” mit demoghraphischen Daten und den Körpermaßen (Body Measures) der Probanden.

Ich habe einige Spalten ausgewählt und zusammengestellt in dieser Tabelle: https://papagei.bioquant.uni-heidelberg.de/simon/Vl2021/nhanes.csv

Wir laden diese Datei mit der Tidyverse-Fuktion read_csv:

nhanes <- read_csv( "Downloads/nhanes.csv")
Rows: 8704 Columns: 6
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): gender, ethnicity
dbl (4): subjectId, age, height, weight

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Wir sehen uns die Tabelle an:

nhanes
# A tibble: 8,704 × 6
   subjectId gender   age height weight ethnicity  
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>      
 1     93703 female     2   88.6   13.7 NH Asian   
 2     93704 male       2   94.2   13.9 NH White   
 3     93705 female    66  158.    79.5 NH Black   
 4     93706 male      18  176.    66.3 NH Asian   
 5     93707 male      13  158.    45.4 Other/Mixed
 6     93708 female    66  150.    53.5 NH Asian   
 7     93709 female    75  151.    88.8 NH Black   
 8     93710 female     0   NA     10.2 NH White   
 9     93711 male      56  171.    62.1 NH Asian   
10     93712 male      18  173.    58.9 Mexican    
# ℹ 8,694 more rows

Eine Erklärung der Spalten findet sich hier: https://wwwn.cdc.gov/Nchs/Nhanes/2017-2018/BMX_J.htm

Aufgabe

Laden Sie die Tabelle vom o.g. Link herunter und laden Sie sie in Ihre R-Sitzung. Lassen Sie sich die Tabelle in RStudio anzeigen.

Tidyverse-Verb “mutate”

In Tidyverse wandelt jeder ARbeitsschritt eine Tabelle in eine andere Tabelle um. Zu jedem Schritt gehört ein “Tidyverse-Verb”, d.h. eine Funktion, die eine Tibble als erstes Argument verlangt und eine abgeänderte Tibble zurückgibt.

Das Tidyverse-Verb mutate ändert eine Spalte ab oder fügt eine neue Spalte hinzu.

BMI

Wir möchten der Tabelle eine Spalte bmi mit dem Body Mass Index (BMI) hinzufügen. Der BMI ist wie folgt definiert:

\[ \text{BMI}=\frac{\text{Körpergewicht in kg}}{\text{(Körpergröße in m)}^2}. \] Unsere Tabelle enthält die Größe in cm, daher müssen wir durch 100 teilen.

Wir schreiben also:

mutate( nhanes, bmi = weight / (height/100)^2 )
# A tibble: 8,704 × 7
   subjectId gender   age height weight ethnicity     bmi
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>       <dbl>
 1     93703 female     2   88.6   13.7 NH Asian     17.5
 2     93704 male       2   94.2   13.9 NH White     15.7
 3     93705 female    66  158.    79.5 NH Black     31.7
 4     93706 male      18  176.    66.3 NH Asian     21.5
 5     93707 male      13  158.    45.4 Other/Mixed  18.1
 6     93708 female    66  150.    53.5 NH Asian     23.7
 7     93709 female    75  151.    88.8 NH Black     38.9
 8     93710 female     0   NA     10.2 NH White     NA  
 9     93711 male      56  171.    62.1 NH Asian     21.3
10     93712 male      18  173.    58.9 Mexican      19.7
# ℹ 8,694 more rows

Piping

Den vorstehenden Befehl kann man auch so schreiben:

nhanes %>% mutate( bmi = weight / (height/100)^2 )
# A tibble: 8,704 × 7
   subjectId gender   age height weight ethnicity     bmi
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>       <dbl>
 1     93703 female     2   88.6   13.7 NH Asian     17.5
 2     93704 male       2   94.2   13.9 NH White     15.7
 3     93705 female    66  158.    79.5 NH Black     31.7
 4     93706 male      18  176.    66.3 NH Asian     21.5
 5     93707 male      13  158.    45.4 Other/Mixed  18.1
 6     93708 female    66  150.    53.5 NH Asian     23.7
 7     93709 female    75  151.    88.8 NH Black     38.9
 8     93710 female     0   NA     10.2 NH White     NA  
 9     93711 male      56  171.    62.1 NH Asian     21.3
10     93712 male      18  173.    58.9 Mexican      19.7
# ℹ 8,694 more rows

Das Pipe-Zeichen %>% bedeutet hierbei: Nimm die Daten links vom Pfeil und füge sie in dem Funktionsaufruf rechts vom Pfeil als erstes Argument ein.

Tidyverse-Verb “filter”

Die Tabelle hat 8704 Zeilen, enthält also Daten von 8704 Probanden. Wie viele davon sind erwachen, also min. 18 Jahre alt? Mit der tidyverse-Funktion filter können wir die Tabelle filtern, d.h. nur die Zeilen behalten, die eine gegebene Bedingung erfüllen, hier: Alter mindestens 18 Jahre.

nhanes %>% filter( age >= 18 )
# A tibble: 5,533 × 6
   subjectId gender   age height weight ethnicity  
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>      
 1     93705 female    66   158.   79.5 NH Black   
 2     93706 male      18   176.   66.3 NH Asian   
 3     93708 female    66   150.   53.5 NH Asian   
 4     93709 female    75   151.   88.8 NH Black   
 5     93711 male      56   171.   62.1 NH Asian   
 6     93712 male      18   173.   58.9 Mexican    
 7     93713 male      67   179.   74.9 NH White   
 8     93714 female    54   148.   87.1 NH Black   
 9     93715 male      71   171.   65.6 Other/Mixed
10     93716 male      61   159.   77.7 NH Asian   
# ℹ 5,523 more rows

Die gefilterte Tabelle hat noch 5533 Zeilen. Der Anteil Erwachsener an der Gesamtzahl von Probanden beträgt also in Prozent:

5533 / 8704 * 100
[1] 63.56847

Eine einfache Pipeline

Nun können wir fragen, wie viele dieser 5366 Erwachsenen übergewichtig sind. Wir verwenden hierzu die WHO-Definition, die Übergewicht als BMI>25 definiert.

nhanes %>%
filter( age >= 18 ) %>%
mutate( bmi = weight / (height/100)^2 ) %>%
filter( bmi > 25 )
# A tibble: 3,952 × 7
   subjectId gender   age height weight ethnicity        bmi
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>          <dbl>
 1     93705 female    66   158.   79.5 NH Black        31.7
 2     93709 female    75   151.   88.8 NH Black        38.9
 3     93714 female    54   148.   87.1 NH Black        39.9
 4     93716 male      61   159.   77.7 NH Asian        30.7
 5     93721 female    60   154    85.1 Mexican         35.9
 6     93726 female    67   154.   74.3 Mexican         31.1
 7     93728 male      53   188.  118.  NH Black        33.4
 8     93729 male      42   173.   82.8 NH Black        27.6
 9     93730 male      57   168.   80.2 Other Hispanic  28.6
10     93731 male      20   180.   89.2 Mexican         27.6
# ℹ 3,942 more rows

Hier habven wir nun eine kurze “Pipeline” gebaut, in der die Daten mit dem Pipe-Pfeil %>% durch drei Arbeitsschritte hindurch geschoben werden:

  • Wir beginnen mit der Tabelle, so wir sie geladen haben
  • Dann filtern wir, um nur die Erwachsenen zu behalten
  • Dann fügen wir die Spalte mit dem BMI hinzu
  • Zuletzt filtern wir nochmals, um nur die Übergewichtigen zu behalten

Nun hat die Tabelle noch 3952 Zeilen

Der Anteil Übergewichtiger unter den erwachsenen Probanden ist also

3952 / 5533
[1] 0.7142599

Zuweisen einer Tabelle zu einer Variable

Wir haben uns eine Tabelle aller Erwachsenen Probanden erstellt und diese durch eine BMI-Spalte ergänzt. Wir können diese modifizierte Tabelle wieder in eine neue Variable zurückschreiben, indem wir den Zuweisungs-Pfeil -> verwenden:

nhanes %>%
filter( age >= 18 ) %>%
mutate( bmi = weight / (height/100)^2 ) -> nhanes_adults

Merke: Wenn nichts anderes verlangt ist, gibt R das Ergebnis auf dem Bildschirm aus und verwirft es dann. Wenn man es behalten will, muss man es ausdrücklich eienr Variable zuweisen. Dann wird es aber nicht mehr ausgegeben. Um den Inhalt der Variablen auszugeben, gibt man einfach den Namen der Variablen ein und drückt Enter (oder Strg-Enter).

Aufgabe

Bei einem BMI über 30 spricht wird Übergewicht als Krankheit (Adipositas, Fettsucht, obesity) angesehen. Wie viele der Frauen und wie viele der Männer sind adipös? Berechnen Sie die Prozentsätze, indem Sie die Tabelle so filtern, dass Sie jeweils alle Männer, alle Frauen, alle adipösen Männer, alle adipösen Frauen erhalten, notieren Sie die Anzahl der Zeilen und berechnen Sie die Anteile in Prozent.

Plots mit ggplot

In R gibt es verschiedene Pakete, um Daten zu visualisieren. Wir werden “ggplot2” verwenden, dass wir Tidyverse von H. Wickham entwickelt wurde und nun Teil des Tidyverse ist.

Der folgende Code erzeugt zwei Histogramme:

nhanes_adults %>%
ggplot( aes( x=bmi ) ) + 
  geom_histogram( bins=50 ) +
  facet_grid( rows="gender" ) +
  scale_x_continuous( breaks=seq( 15, 80, 5 ) )
Warning: Removed 99 rows containing non-finite values (`stat_bin()`).

Hier erkennen Sie schon die allgemeine Form eines typischen ggplot-Aufrufs:

tibble %>%
ggplot( aes( x=..., y=..., ... ) ) +
   geom_...( ... ) +
   ...
  • Am Anfang steht eine Tabelle, entweder aus einer Variablen gelesen, oder mit einer Tidyverse-Pipeline erzeugt.
  • Diese wird mit dem Pipe-Pfeil %>% zur Funktion ggplot gesandt.
  • Die ggplot-Funktion hat ein Argument, das mit aes erzeugt wird.
    • Innerhalb des aes-Blocks wird festgelegt, aus welchen Tabellenspalten die Plot-Aspekte (z.B. x- und y-Koordinaten) entnommen werden sollen.
  • Darauf folgen ggplot-Komponenten, die mit + hinzugefügt werden und Details über den Plot spezifizieren.
  • Mindestens eine Koponente muss eine geom_-Funktion sein. Die Geoms wandeln Daten in Plot-Element um. Für jede Art von Plot gibt es ein zugehöriges Geom.
  • Neben geoms gibt es noch weitere Dinge, die man mit + hinzufügen kann.

In unserem Beispiel:

  • Wir möchten ein Histogramm, daher haben wir + geom_histogram geschrieben.
  • Für Histogramme brauch man nur eine aes-Spezifikation, nämlich, was die x-Achse sein soll. Wir haben x=bmi gewählt, d.h. die Tabellenspalte bmi soll durch die x-Achse repräsentiert werden.
  • Der Wertebereich der x-Achse soll in 50 “Bins” aufgeteilt werden.

Als Besonderheit haben wir Facetting verwendet:

  • Der Plot wird auf zwei Facets aufgeteilt, die untereinander (d.h. in zwei “rows” angeordnet sind).
  • Jede “row” ist für einen Teil der Daten zuständig, und zwar sollen die Daten gemäß der Spalte “gender” auf die Facet-Rows aufgeteilt werden.
  • Deshalb ist jede der beiden Rows am rechten rand mit einem der beiden Werte in “gender”, also “male” und “female” beschriftet.

Viele Arten von Plots

Die R Graph Gallery zeigt anhand vieler Beispiele, welche Art von Plots mit ggplot2 erzeugt werden können

Streudiagramme

Streudiagramme (engl. scatte plots) stellen jeden Datenpunkt (jede Tabellenzeile) durch einen Punkt dar. Da nun x- und y-Koordinate verfügbar sind, können wir zwei Tabellen-Zeilen gegeneinander auftragen. Die Farbe des Punktes können wir für eine dritte Spalte nutzen

Hier ein Beispiel:

nhanes %>%
ggplot( aes( x=age, y=height, color=gender ) ) +
  geom_point( size=.2 )
Warning: Removed 688 rows containing missing values (`geom_point()`).

Können Sie hier wieder die Elemente eines ggplot-Aufrufs sehen?

Das Geom geom_point erwartet zwwi Zuweisungen im aes-Block, nämlich x und y. Falls vorhanden, beachtet es auch noch weitere, z.B. colour.

Hausaufgaben

Experimentieren mit dem Histogramm

Betrachten Sie nochmal das Histogramm mit den BMI-Werten.

  • Vergleichen Sie die beiden Histogramme. Was können Sie über die Unterschiede zwischen Männern und Frauen in der Untersuchung ablesen?

  • Was geschieht, wenn Sie das + facet_grid(...) weglassen?

  • Was geschieht, wenn Sie + scale_x_continuous( breaks=seq( 15, 80, 5 ) ) anfügen?

  • Was geschieht, wenn Sie statt geom_histogram( bins = 50 ) einfach nur geom_histogram() schreiben?

  • Experimentieren Sie mit der Anzahl der Bins. Wie sieht das Histogramm mit nur 10 Bins, oder mit vielleicht 200 Bins aus? Was halten Sie für eine gute Anzahl?

Non-finite values

Vielleicht haben Sie bemerkt, dass R beim Zeichnen des Histogramms warnt: “Removed 99 rows containing non-finite values.” Sehen Sie sich die Tabelle an (z.B. in dem Sie sie in der Environment-Pane anklicken). Können Sie die 99 fehlerhaften Werte finden? Wo liegt das Problem?

Wenn Sie sie nicht finden: Schauen Sie sich z.B. den Probanden mit der SubjectID 93935 an. Können Sie mit filter diese Zeile aus der Tabelle isolieren?

Noch ein Streudiagramm

Hier sehen Sie zwei Darstellung der Daten aus der murders-Tabelle vom Dienstag.

Zunächst ein einfaches Streudiagramm:

Nun eine verbesserte version desselben Plots:

Versuchen Sie zunächst, den ersten Plot selbst zu erzeugen.

Wenn Sie wollen, können Sie versuchen, den Plot zu verschönern, um zum zweiten Diagramm zu kommen:

  • Statt Punkten habe ich die Abkürzungen der Staaten verwendet (die in der Spalte abb stehen). Hierzu habe ich geom_point durch geom_text ersetzt. Letzteres erwartet neben x und y noch eine dritte aes-Angabe, nämlich label, d.h., die Spalte, aus der die Beschriftungen entnommen werden sollen.

  • Die Achsenbeschriftungen kann man mit ... + xlab( "Beschriftung" ) + ylab( "Bescriftung" ) ändern. Mit + ggtitle( "Beschriftung" ) legt man eine Überschrift fest.

  • Durch + scale_x_log10() macht man die x-Achse logarithmisch.

Sehen Sie, wie weit Sie kommen und laden Sie Ihren Plot auf Moodle hoch. Benutzen Sie dazu den “Export”-Button in der Plot-Pane, um Ihren Plot in eine Datei zu speichern, die Sie auf Moodle hoch laden können.