Grouping and Summerizing

Vorlesung “Datananalyse in der Biologie”

Erinnerung: Summarizing functions

Wir laden wieder unsere NHANES-Tabelle:

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
read_csv( "Downloads/nhanes.csv" ) -> nhanes
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.
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

Heute lernen wir, wie man “summerizing functions” auf Tabellen anwendet. Wichtiger Zweck solcher Funktionen ist die Erstellungs beschreibender Statistiken (descriptive statistics) wie z.B. Mittelwert, Median, etc.

Zur Erinnerung: Eine summerizing function fasst einen Vektor mit vielen Zahlen (z.B. eine Tabellen-Spalte) zu einer Zahl zusammen.

Beispiele: Mittelwert (mean), Summe (sum), Median (median), Varianz (var), Standardabweichung (sd), aber auch Maximum (max), Minimum (min), Anzahl der Werte (length, oder, nur in Tidyverse, n), uvm.

Unsere Aufgabe: Berechne die mittlere Körpergröße der erwachsenen Frauen und Männer.

NA-Filtern

Eigentlich können wir das mit dem Gelernten schon lösen:

Wir erstellen erst eine Tabelle aller erwachsenen Männer:

nhanes %>% filter( gender == "male", age >= 18 ) -> nhanes_adult_men

nhanes_adult_men
# A tibble: 2,672 × 6
   subjectId gender   age height weight ethnicity  
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>      
 1     93706 male      18   176.   66.3 NH Asian   
 2     93711 male      56   171.   62.1 NH Asian   
 3     93712 male      18   173.   58.9 Mexican    
 4     93713 male      67   179.   74.9 NH White   
 5     93715 male      71   171.   65.6 Other/Mixed
 6     93716 male      61   159.   77.7 NH Asian   
 7     93717 male      22   174.   74.4 NH White   
 8     93718 male      45   157.   54.4 NH Black   
 9     93723 male      64   170.   64.9 NH White   
10     93727 male      70   162.   62.7 NH Asian   
# ℹ 2,662 more rows

Nun können wir mean auf die Spalte anwenden:

mean( nhanes$height )
[1] NA

Warum hat das nicht geklappt? Anscheinend fehlt bei ein paar Männern die Angabe der Größe.

Wir können mean anweisen, diese fehlenden Werte (die in der Tabelle mit NA, für not available, merkiert sind) zu überspringen, indem wir das Zusatz-Argument na.rm (für NA removal) angeben:

mean( nhanes$height, na.rm=TRUE )
[1] 156.5934

Eine andere Lösung ist, unseren Filter zu erweitern, um die störenden Zeilen aus der Tabelle ganz zu entfernen:

nhanes %>% filter( gender == "male", age >= 18, !is.na(height) ) -> nhanes_adult_men

nhanes_adult_men
# A tibble: 2,630 × 6
   subjectId gender   age height weight ethnicity  
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>      
 1     93706 male      18   176.   66.3 NH Asian   
 2     93711 male      56   171.   62.1 NH Asian   
 3     93712 male      18   173.   58.9 Mexican    
 4     93713 male      67   179.   74.9 NH White   
 5     93715 male      71   171.   65.6 Other/Mixed
 6     93716 male      61   159.   77.7 NH Asian   
 7     93717 male      22   174.   74.4 NH White   
 8     93718 male      45   157.   54.4 NH Black   
 9     93723 male      64   170.   64.9 NH White   
10     93727 male      70   162.   62.7 NH Asian   
# ℹ 2,620 more rows

Nun haben wir in filter drei Filter-Kriterien: männlich; erwachsen; mit Angabe der Körpergröße.

Der dritte Filter funktioniert so: Die Funktion is.na prüft, ob der Wert NA (not available) ist:

is.na( c( 2, 3, NA, 6, 5, NA, 7 ) )
[1] FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE

Da filter die Zeilen behält, bei denen die Bedingung wahr (TRUE) ist, müssem wir die Wahrheitswerte invertieren, mit dem Ausrufezeichen, das als “Nicht-Operator” (not operator) wirkt:

!is.na( c( 2, 3, NA, 6, 5, NA, 7 ) )
[1]  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE

Also:

nhanes %>% filter( gender == "male", age >= 18, !is.na(height) ) -> nhanes_adult_men

mean( nhanes_adult_men$height )
[1] 173.4809

Jetzt funktioniert es.

Unschön ist aber, dass wir die mean-Funktion nicht in unsere Tidyverse-Pipeline eingebaut haben.

Das Tidverse-Verb “summarise”

So ist es schöner:

nhanes %>% 
filter( gender == "male", age >= 18, !is.na(height) ) %>%
summarise( mean(height) )
# A tibble: 1 × 1
  `mean(height)`
           <dbl>
1           173.

Das Tidyverse-Verb summarise (oder summarize) erlaubt es, summerizing functions in einer Tidyverse-Pipeline einzubauen. Man erhält dann eine Tabelle mit nur einer Zeile.

Man kann auch mehrere Summarisierungen durchführen:

nhanes %>% 
filter( gender == "male", age >= 18, !is.na(height) ) %>%
summarise( mean(height), min(height), max(height) )
# A tibble: 1 × 3
  `mean(height)` `min(height)` `max(height)`
           <dbl>         <dbl>         <dbl>
1           173.          148.          198.

Gruppieren: Das Tidyverse-Verb “group_by”

Wie können wir die Summarisierung auf einmal für beide Geschlechter machen?

Wir entfernen zunächst den Filter für die Männer:

nhanes %>% 
filter( age >= 18, !is.na(height) ) %>%
summarise( mean(height), min(height), max(height) )
# A tibble: 1 × 3
  `mean(height)` `min(height)` `max(height)`
           <dbl>         <dbl>         <dbl>
1           166.          138.          198.

Nun fügen wir eine Gruppierung ein:

nhanes %>% 
filter( age >= 18, !is.na(height) ) %>%
group_by( gender ) %>%
summarise( mean(height), min(height), max(height) )
# A tibble: 2 × 4
  gender `mean(height)` `min(height)` `max(height)`
  <chr>           <dbl>         <dbl>         <dbl>
1 female           160.          138.          189.
2 male             173.          148.          198.

Das group_by( gender ) hat die Zeilend er Tabelle in zwei Gruppen eingeteilt, gemäß dem Inhalt der Spalte gender, die entweder male oder female enthält.

Das nachfolgende summerise führt dann die Sumamrisierung für jede Gruppe getrennt durch. Die erzeugt Tabelle enthält dann eine Zeile pro Gruppe.

Wir können die Gruppen auch feiner einteilen:

nhanes %>% 
filter( age >= 18, !is.na(height) ) %>%
group_by( gender, ethnicity ) %>%
summarise( mean(height), min(height), max(height) )
`summarise()` has grouped output by 'gender'. You can override using the
`.groups` argument.
# A tibble: 12 × 5
# Groups:   gender [2]
   gender ethnicity      `mean(height)` `min(height)` `max(height)`
   <chr>  <chr>                   <dbl>         <dbl>         <dbl>
 1 female Mexican                  157.          138.          181.
 2 female NH Asian                 156.          140.          177.
 3 female NH Black                 162.          139           188.
 4 female NH White                 161.          141.          189.
 5 female Other Hispanic           157.          140.          182.
 6 female Other/Mixed              162.          147.          175.
 7 male   Mexican                  170.          148.          192.
 8 male   NH Asian                 170.          152.          188 
 9 male   NH Black                 176.          153.          198.
10 male   NH White                 175.          152.          196.
11 male   Other Hispanic           169.          148.          187 
12 male   Other/Mixed              176.          158.          195.

Nun haben wir 12 Gruppen (2 Geschlechter x 6 Ethnien) und entsprechend 12 Zeilen in der Ausgabe von summarize. Die Ausgabe hat 5 Zeilen, nämlich

  • zwei Spalten mit den Gruppen-Labels, die in group_by angegeben wurden (gender, ethnicity)
  • drei Spalten mit Ergebnissen der drei Summarisierungs-Operationen, die in summerize angegeben wurden

Merke: group_by und summarize werden meist zusammen verwendet. Ersteres teilt die Zeilen in Gruppen ein, letzteres fasst die Zeilen jeder Gruppe zu einer einzelnen Zeile zusammen.

Zählen in Gruppen

Der Mittelwert (mean) ist eine einfache Statistik; die Anzahl der Zeilen (n) eine noch einfachere.

Wir verwenden dies, um zu unsere Frage zurück zu kommen, welcher Prozentsatz der Frauen und Männer übergewichtig bzw. adipös ist.

Aufgabe: Erstellen Sie dafür eine Tabelle wir folgt:

  • Beginnen Sie mit der nhanes-Tabelle
  • Filtern Sie, dass nur die Erwachsenen Probanden/Probandinnen verbleiben
  • Fügen Sie eine Spalte mit dem BMI hinzu (wie zuvor)
  • Fügen Sie eine Spalte obese hinzu, die angibt, ob die person adipös ist, d.h., einen BMI über 30 hat:

So soltle die Tabelle aussehen:

# A tibble: 5,533 × 8
   subjectId gender   age height weight ethnicity     bmi obese
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>       <dbl> <lgl>
 1     93705 female    66   158.   79.5 NH Black     31.7 TRUE 
 2     93706 male      18   176.   66.3 NH Asian     21.5 FALSE
 3     93708 female    66   150.   53.5 NH Asian     23.7 FALSE
 4     93709 female    75   151.   88.8 NH Black     38.9 TRUE 
 5     93711 male      56   171.   62.1 NH Asian     21.3 FALSE
 6     93712 male      18   173.   58.9 Mexican      19.7 FALSE
 7     93713 male      67   179.   74.9 NH White     23.5 FALSE
 8     93714 female    54   148.   87.1 NH Black     39.9 TRUE 
 9     93715 male      71   171.   65.6 Other/Mixed  22.5 FALSE
10     93716 male      61   159.   77.7 NH Asian     30.7 TRUE 
# ℹ 5,523 more rows

Aufgabe: Teilen Sie die Tabellenzeilen nun mit group_by in vier Gruppen ein, nämlich gemäß dem Geschlecht (male/female) und nach dem Adipositas-Status (obese TRUE/FALSE). Lassen Sie dann mit summerize für jede Gruppe die Summarisierungs-Operation n() ausführen, die einfach zählt, wie viele Zeilen in der Gruppe sind.

So soltle Ihr Ergebnis aussehen:

nhanes %>%
filter( age >= 18 ) %>%
mutate( bmi = weight / (height/100)^2 ) %>%
filter( !is.na(bmi) ) %>%
mutate( obese = bmi > 30 ) %>%
group_by( gender, obese ) %>%
summarise( n() )

Faktoren und Diskretisierung

Faktoren

Unsere Tabelle hat zwei Arten von Daten:

  • Werte wie Größe und Gewicht sind kontinuierlich (“continuous”), d.h., es gibt beliebige Zwischenwerte.
  • Werte wie Geschlecht und Ethnie sind kategorisch (“categorical”): Es gibt eine vorgegebene Liste von Werten (z.B. “male” und “female”) und andere als diese sind nicht möglich. Manchmal spricht man auch von “diskreten” Werten.

Bei kategorischen Werten macht es oft Sinn, nicht jedesmal das Wort (“male” oder “female”) anzugeben, sondern einfach einen Code (eine Zahl) zu speichern, zusammen mit einer Kodierungstabelle (z.B. 1=“male”, 2=“female”). Einen auf diese Art angelegten Vektor nennt man in R einen “Faktor” (factor), die möglichen Werte nennt man Levels.

Diskretisierung

Oft macht es Sinn, kontinuierliche Werte anhand eines Rasters aus Grenzen zu diskretisieren.

Beispiel 1: Beim Histogramm bildet R den Wertebereich auf ein Raster von “Bins” (Eimern) ab, und ordnet jeden Wert in eine Bin, die dann als ein Balken im Diagramm dargestellt wird.

Beispiel 2: Die WHO schlägt vor, BMI-Werte wie folgt zu interpretieren:

  • unter 18.5: untergewichtig
  • 18.5 bis 25: normal
  • 25 - 30: übergewichtig
  • über 30: adipös

Die R-Funktion cut ermöglicht uns, die BMI-Werte entlang dieser Grenzen zu diskretisieren.

nhanes %>%
filter( age >= 18 ) %>%
mutate( bmi = weight / (height/100)^2 ) %>%
filter( !is.na(bmi) ) -> nhanes_adults
nhanes_adults %>%
mutate( weight_state = cut( bmi, breaks = c( 0, 18.5, 25, 30, Inf ) ) )
# A tibble: 5,434 × 8
   subjectId gender   age height weight ethnicity     bmi weight_state
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>       <dbl> <fct>       
 1     93705 female    66   158.   79.5 NH Black     31.7 (30,Inf]    
 2     93706 male      18   176.   66.3 NH Asian     21.5 (18.5,25]   
 3     93708 female    66   150.   53.5 NH Asian     23.7 (18.5,25]   
 4     93709 female    75   151.   88.8 NH Black     38.9 (30,Inf]    
 5     93711 male      56   171.   62.1 NH Asian     21.3 (18.5,25]   
 6     93712 male      18   173.   58.9 Mexican      19.7 (18.5,25]   
 7     93713 male      67   179.   74.9 NH White     23.5 (18.5,25]   
 8     93714 female    54   148.   87.1 NH Black     39.9 (30,Inf]    
 9     93715 male      71   171.   65.6 Other/Mixed  22.5 (18.5,25]   
10     93716 male      61   159.   77.7 NH Asian     30.7 (30,Inf]    
# ℹ 5,424 more rows

Beachte: Die Liste mit den Grenzwerten füë cut muss auch die “äußeren” Grenzen enthalten, hier 0 und Inf (= infinity).

Wir können auch Labels (Faktor-Levels) angeben:

nhanes_adults %>%
mutate( weight_state = cut( bmi, 
     breaks = c( 0, 18.5, 25, 30, Inf ),
     labels = c( "underweight", "normal", "overweight", "obese" ) ) )
# A tibble: 5,434 × 8
   subjectId gender   age height weight ethnicity     bmi weight_state
       <dbl> <chr>  <dbl>  <dbl>  <dbl> <chr>       <dbl> <fct>       
 1     93705 female    66   158.   79.5 NH Black     31.7 obese       
 2     93706 male      18   176.   66.3 NH Asian     21.5 normal      
 3     93708 female    66   150.   53.5 NH Asian     23.7 normal      
 4     93709 female    75   151.   88.8 NH Black     38.9 obese       
 5     93711 male      56   171.   62.1 NH Asian     21.3 normal      
 6     93712 male      18   173.   58.9 Mexican      19.7 normal      
 7     93713 male      67   179.   74.9 NH White     23.5 normal      
 8     93714 female    54   148.   87.1 NH Black     39.9 obese       
 9     93715 male      71   171.   65.6 Other/Mixed  22.5 normal      
10     93716 male      61   159.   77.7 NH Asian     30.7 obese       
# ℹ 5,424 more rows

Nun können wir nochmals zählen:

nhanes_adults %>%
mutate( weight_state = cut( bmi, 
     breaks = c( 0, 18.5, 25, 30, Inf ),
     labels = c( "underweight", "normal", "overweight", "obese" ) ) ) %>%
group_by( gender, weight_state ) %>%
summarize( n = n() )
`summarise()` has grouped output by 'gender'. You can override using the
`.groups` argument.
# A tibble: 8 × 3
# Groups:   gender [2]
  gender weight_state     n
  <chr>  <fct>        <int>
1 female underweight     57
2 female normal         743
3 female overweight     795
4 female obese         1216
5 male   underweight     43
6 male   normal         639
7 male   overweight     930
8 male   obese         1011

Hier haben wir bei summarise einen Spaltennamen angegeben, nämlich n (statt bisher n()), indem wir den gewünschten Spaltennamen, gefolgt von einem =, vor die Summarisierungs-Operation gesetzt haben.

Nun möchten wir die diese Tabelle noch um eine Spalte mit Prozenten ergänzen:

nhanes_adults %>%
mutate( weight_state = cut( bmi, 
     breaks = c( 0, 18.5, 25, 30, Inf ),
     labels = c( "underweight", "normal", "overweight", "obese" ) ) ) %>%
group_by( gender, weight_state ) %>%
summarize( n = n() ) %>%
  
group_by( gender ) %>%
mutate( percent  = n / sum(n) * 100 )
`summarise()` has grouped output by 'gender'. You can override using the
`.groups` argument.
# A tibble: 8 × 4
# Groups:   gender [2]
  gender weight_state     n percent
  <chr>  <fct>        <int>   <dbl>
1 female underweight     57    2.03
2 female normal         743   26.4 
3 female overweight     795   28.3 
4 female obese         1216   43.3 
5 male   underweight     43    1.64
6 male   normal         639   24.4 
7 male   overweight     930   35.5 
8 male   obese         1011   38.5 

Wie funktioniert dies?

  • Das zweite group_by teilt die 8 Zeilen in zwei Gruppen (male, female) zu je 4 Zeilen ein.
  • Das nachfolgende mutate wird für jede Gruppe getrennt durchgeführt. Daher ergibt sum(n) die Summe der Spalte n nur über die Zeilen der Gruppe. Jedes n wird also durch die Summe der vier n-Werte des jeweiligen Geschlechts geteilt. Somit addieren sich die Geschlechter jeweils getrennt zu 100%.

Hausaufgabe

Hier ist ein Plot mit der Durschschnitts-Größe der Probanden jedes Jahrgangs, aufgeschlüsselt nach Geschlecht:

`summarise()` has grouped output by 'age'. You can override using the `.groups`
argument.
Warning: Removed 104 rows containing missing values (`geom_point()`).
Warning: Removed 104 rows containing missing values (`geom_line()`).

Versuchen Sie, diesen Plot selbst zu erstellen.

Bauen Sie dazu eine Tidyverse-Pipeline, die die folgenden Schritte durchführt:

  • Beginnen Sie mit der NHANES-Tabelle
  • Entfernen Sie alle Tabellen-Zeilen, bei denen die Körpergröße fehlt (d.h., als NA markiert ist)
  • Gruppieren sie die Tabelle mit group_by, so dass jede Gruppe jeweils Zeilen enthält, die das selbe Geschlecht und dasselbe Lebensalter in Jahren aufweist. Da die Alters-Zahlen von 4 bis 80 gehen (also 77 verschiedene Werte), soltlen Sie 2x77=158 Gruppen erhalten.
  • Berechnen Sie für jede Gruppe den mittelwert von height und nennen Sie diese neue Spalte mean_height. Nun sollle Ihre Tabelle so aussehen:
nhanes %>%
filter( !is.na(height) ) %>%
group_by( age, gender ) %>%
summarise( mean_height= mean(height) ) 
  • Geben Sie diese Tabelle an `gplot weiter. Legen Sie per aes fest, aus welchen Spalten für x, y und color entnommen werden sollen.
  • Als Geom können Sie geom_point verwenden, um ein Streudiagramm (scatter plot) zu erhalten. Wenn Sie als zweites Geom noch geom_line hinzu addieren, werden die Punkte durch Linien verbunden.
  • Mit xlim können Sie in die x-Achse “hinein zoomen”, so dass man die Jahre, in denen die Probanden noch nicht ausgewachsen sind, besser sehen kann.

Intepretieren Sie den Plot:

  • Wie entsteht der Unterschied zwischen Frauen und Männern aus? Wann sind Jungen und Mädchen ausgewachsen>?

  • Wann sind Jungen größer und wann Mädchen? Glauben Sie, dass es unter 13 Jahren einen Unterschied gibt?

Facetting:

  • Können Sie den Plot so erweitern, dass Sie einen Plot pro Ethnie haben? Benutzen Sie facet_grid.

Laden Sie Ihren Plot und Ihren Code bitte auf Moodle hoch.