Tracks
/
R
R
/
Lehrplan
/
Dataframes
Da

Dataframes in R

1 Übung

Über Dataframes

In anderen Teilen des Lehrplans haben wir verschiedene Datentypen mit unterschiedlichen Eigenschaften kennengelernt.

  • Atomare Vektoren sind eindimensional und homogen im Typ.
  • Listen sind eindimensional, und ihre Elemente können von heterogenem Typ sein.
  • Matrizen und Arrays sind mehrdimensional und homogen.

In diesem Konzept geht es darum, wie sich mehrdimensionale, heterogene Daten speichern lassen. In der Praxis sehen die meisten Daten aus der realen Welt so aus. Damit kommen wir zum Kern dessen, wie R in der Praxis (größtenteils) eingesetzt wird.

Dataframe-Varianten

Im Laufe der Jahrzehnte hat R mehrere Datentypen für tabellarische Daten hinzugefügt.

Dieser Lehrplan konzentriert sich vor allem auf Tibbles, aber es ist nützlich, ein paar Alternativen zu kennen.

Die data.table

Tibbles (unten beschrieben) und data.tables sind beides hoch angesehene Versuche, das traditionelle data.frame in Basis-R zu verbessern. Es gibt unvermeidlich viel Streit darüber, welches „besser“ ist. Vielleicht besteht über die folgenden Punkte ein gewisser Konsens (auch wenn man sie als zu einfach kritisiert).

  • tibble ist vor allem auf einfache Nutzung und die Integration in das Tidyverse-Ökosystem optimiert.
  • data.table ist vor allem auf rohe Leistungsfähigkeit und Skalierbarkeit optimiert, besonders bei sehr großen Datensätzen.

Wie auch immer: data.table ist innerhalb von Exercism nicht verfügbar, deshalb wird es hier nur der Vollständigkeit halber erwähnt.

Der data.frame

In Basis-R ist ein data.frame eine list aus vectors gleicher Länge. Man kann sich das als rechteckige Datentabelle vorstellen, in der jede Spalte homogen ist, jede Zeile aber (und das ist meist der Fall) unterschiedliche Datentypen enthalten kann.

Ein Beispiel zur Veranschaulichung:

# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)

# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#>   languages created has.syllabus
#> 1   Fortran    1957        FALSE
#> 2         R    1993         TRUE
#> 3    Python    1991         TRUE
#> 4     Julia    2012         TRUE

# look at the structure
str(df)
#> 'data.frame':	4 obs. of  3 variables:
 #> $ languages   : chr  "Fortran" "R" "Python" "Julia"
 #> $ created     : num  1957 1993 1991 2012
 #> $ has.syllabus: logi  FALSE TRUE TRUE TRUE

Wir haben eine Spalte mit Strings, eine Spalte mit Zahlen und eine Spalte mit booleschen Werten. Auf größere Datenmengen übertragen ist das eine intuitive Art, viele Sammlungen von Daten aus der realen Welt darzustellen.

Das tibble

Das Design des data.frame ist alt.

Jahrzehntelange Erfahrung und veränderte Nutzungsmuster von R führten zu einem Redesign und damit zu einer modernisierten Alternative im Tidyverse: Tibbles.

Im Vergleich zu Basis-R bieten Tibbles:

  • Andere Standardeinstellungen, um häufige Probleme zu verringern.
  • Weniger Bereitschaft, Datentypen bei der Eingabe automatisch umzuwandeln.
  • Mehr und klarere Fehlermeldungen.
  • Andere, meist bessere Anzeigeformate.

Kurz gesagt, ein tibble will „weniger tun und mehr meckern“; das wird auch als „faul und mürrisch“ beschrieben.

Die Typen sind meist austauschbar: Jede Funktion, die ein data.frame akzeptiert, akzeptiert auch ein tibble, und umgekehrt.

Die Umwandlung zwischen den Typen ist einfach, mit as_tibble(df) und as.data.frame(tbl).

Bei neuen Projekten helfen dir Tibbles wahrscheinlich dabei, robusteren Code zu schreiben. In der R-Welt gibt es jedoch sehr viel Legacy-Code und Legacy-Daten, deshalb wird das data.frame wohl noch lange verbreitet bleiben.

# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE      
  
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#>  $ languages   : chr [1:4] "Fortran" "R" "Python" "Julia"
#>  $ created     : num [1:4] 1957 1993 1991 2012
#>  $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE

Beachte das Standard-Ausgabeformat: Die Kommentarzeile mit den Dimensionen wird automatisch ausgegeben, und auch die Spaltentypen werden angezeigt.

Mit Tibbles arbeiten

Tibbles sind ein Kernbestandteil des Tidyverse, also lädst du sie entweder mit library(tibble) oder mit library(tidyverse).

Die Dokumentation ist ziemlich umfangreich, ganz im Tidyverse-Stil:

Ein Tibble erstellen

Am einfachsten verwendest du die Funktion tibble(), um Spaltenvektoren zu verbinden, genau wie bei data.frame(). Ein Beispiel dafür gab es in einem vorherigen Abschnitt.

Wenn es bequemer ist, Werte zeilenweise einzugeben, ist die passende Funktion tribble().

tbl_r <- tribble(
  # column names marked with tilde prefix
  ~languages, ~created, ~has.syllabus,
  "Fortran", 1957, FALSE,
  "R", 1993, TRUE,
  "Python", 1991, TRUE,
  "Julia", 2012, TRUE
)

tbl_r
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

In der Praxis gibt es Dutzende Möglichkeiten, Tibbles zu erstellen, da sie das Standard-Ausgabeformat einer Vielzahl von Tidyverse-Funktionen sind. Darauf kommen wir in einem späteren Konzept zurück.

Ein Tibble bearbeiten

Im Konzept Funktionale Programmierung ging es um die Bibliothek purrr, mit der du Vektoren und Listen (eindimensionale Datenstrukturen) bearbeitest.

Für Dataframes (ob traditionell oder Tibbles) ist die passende Bibliothek dplyr.

dplyr haben wir schon im Switch-Konzept eingeführt. Dort haben wir nur ein paar Hilfsfunktionen verwendet, aber jetzt können wir den Rest dieser großen Bibliothek erkunden.

Teilmengen bilden

Dataframes, auch Tibbles, lassen sich wie Listen von Spaltenvektoren behandeln. Die Listenindizierung liefert also eine bestimmte Spalte zurück.

tbl
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

tbl$created
#> [1] 1957 1993 1991 2012

Du kannst einen Dataframe auch mit Matrix-artiger Indizierung ansprechen.

tbl[c(2, 4), 1:2]
  # A tibble: 2 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 R            1993
#> 2 Julia        2012

In modernem R mit dem Tidyverse-Ökosystem sind dplyr-Funktionen im Allgemeinen flexibler und bequemer. Auf sie konzentrieren wir uns im Rest dieses Konzepts.

Spaltenweise Operationen

Eine einzelne Spalte bekommst du mit pull() über den Namen oder die laufende Nummer (negative Zahlen zählen von rechts nach links).

tbl |> pull(created)
#> [1] 1957 1993 1991 2012

Das ist dasselbe Ergebnis wie tbl$created, nur mit einer Funktion, die sich gut in Pipelines einfügt.

Für mehrere Spalten ist die passende Funktion select(), die sehr vielseitig ist. Du holst (oder entfernst) Spalten anhand von Eigenschaften ihres Namens oder Typs.

# Range with position and/or name
tbl |> select(1:created)
  # A tibble: 4 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 Fortran      1957
#> 2 R            1993
#> 3 Python       1991
#> 4 Julia        2012

# Exclude a column
tbl |> select(!created)
  # A tibble: 4 × 2
#>   languages has.syllabus
#>   <chr>     <lgl>       
#> 1 Fortran   FALSE       
#> 2 R         TRUE        
#> 3 Python    TRUE        
#> 4 Julia     TRUE        

# Use type of column
tbl |> select(where(is.numeric))
  # A tibble: 4 × 1
#>   created
#>     <dbl>
#> 1    1957
#> 2    1993
#> 3    1991
#> 4    2012

Du kannst mehrere Kriterien kombinieren, mit den booleschen Operatoren &, | und ! (und, oder, nicht).

Spaltennamen, die gültige R-Bezeichner sind, brauchen innerhalb von select() keine Anführungszeichen. Ungültige Namen (z. B. solche mit Leerzeichen) kannst du in Backticks einschließen, auch wenn es besser wäre, sie umzubenennen.

Die Funktion select() kann mit einer Reihe von Hilfsfunktionen arbeiten, um Spaltennamen auszuwählen: starts_with, contains, num_range und einige weitere. matches ermöglicht vollständiges Matching per RegEx. Einzelheiten findest du in der Dokumentation.

Solche Macht wirkt bei unserem Spielzeug-Dataframe mit Sprachen ziemlich albern. Zum Glück ist das Tibble starwars in dplyr enthalten, damit wir etwas Größeres zum Üben haben.

# limit display to top 3 rows of non-list columns
starwars |> 
  select(!where(is.list)) |> 
  head(3)
  # A tibble: 3 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue              19 male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow           112 none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red               33 none  masculine Naboo     Droid  

# pick a subset of columns
starwars |> 
  select(name | ends_with("color")) |> 
  head(5)
  # A tibble: 5 × 4
#>   name           hair_color skin_color  eye_color
#>   <chr>          <chr>      <chr>       <chr>    
#> 1 Luke Skywalker blond      fair        blue     
#> 2 C-3PO          NA         gold        yellow   
#> 3 R2-D2          NA         white, blue red      
#> 4 Darth Vader    none       white       yellow   
#> 5 Leia Organa    brown      light       brown    

Zeilenweise Operationen

Note

dplyr bietet offensichtlich mächtige Möglichkeiten, Spalten über ihren Namen auszuwählen.

Können wir Ähnliches mit Zeilennamen machen?

Nein! Traditionelle R-Dataframes können Zeilennamen haben, aber (nach einer Geschichte voller Bugs und Performance-Probleme) sind Zeilennamen in tibbles nicht erlaubt.

Wenn du Namen willst, pack sie in eine <chr>-Spalte (typischerweise Spalte 1) und verwende sie wie jede andere Spalte. Importfunktionen wie as_tibble() erzeugen diese automatisch, wenn du Daten mit benannten Zeilen importierst.

Wenn dir diese Einschränkung bei Zeilennamen seltsam restriktiv vorkommt, denk daran, dass die meisten großen Datenbanksysteme Tabellen genauso behandeln: Oracle, SQL Server, PostgreSQL, MySQL...

Zeilen, die bestimmten Kriterien entsprechen, holst du mit filter(); mit filter_out() schließt du sie aus.

starwars |> 
  select(name:mass) |> 
  filter(between(height, 150, 165) & !is.na(mass))
  # A tibble: 4 × 3
#>   name               height  mass
#>   <chr>               <int> <dbl>
#> 1 Leia Organa           150    49
#> 2 Beru Whitesun Lars    165    75
#> 3 Nien Nunb             160    68
#> 4 Ben Quadinaros        163    65

Filterkriterien können beliebig komplex sein, basieren aber immer auf dem Inhalt der Zeilen.

Wenn die Nummern der Zeilen bekannt sind, können wir verschiedene slice()-Funktionen verwenden.

starwars |> 
  select(name | homeworld) |> 
  slice(20:25)
  # A tibble: 6 × 2
#>   name             homeworld
#>   <chr>            <chr>    
#> 1 Palpatine        Naboo    
#> 2 Boba Fett        Kamino   
#> 3 IG-88            NA       
#> 4 Bossk            Trandosha
#> 5 Lando Calrissian Socorro  
#> 6 Lobot            Bespin   

# random sample of rows
starwars |> 
  select(name | homeworld) |> 
  slice_sample(n = 4)
  # A tibble: 4 × 2
#>   name            homeworld
#>   <chr>           <chr>    
#> 1 Shaak Ti        Shili    
#> 2 Luminara Unduli Mirial   
#> 3 Grievous        Kalee    
#> 4 Palpatine       Naboo    

Um doppelte Zeilen zu entfernen, verwendest du distinct().

Ein Tibble verändern

Zunächst eine Einschränkung: Das Standardverhalten copy-on-modify bedeutet, dass das ursprüngliche Tibble normalerweise unverändert bleibt.

Die meisten Änderungen werden spaltenweise vorgenommen.

Spaltennamen änderst du mit rename(newname = oldname) oder mit rename_with(), um eine Funktion anzuwenden. Typisch ist es, importierte Namen aufzuräumen, damit sie sich in R leichter verwenden lassen: Leerzeichen entfernen und für verwandte Namen ein einheitliches Format erzwingen.

Beachte die Syntax innerhalb von rename(). Der Inhalt der Spalte oldname wird an den Namen newname gebunden, daher die Reihenfolge.

Die Spaltenreihenfolge änderst du mit relocate(). Angegebene Spalten werden standardmäßig an die äußerste linke Position verschoben, aber mit einem .before- oder .after-Argument kannst du die Position genauer festlegen.

sw <- starwars |> select(name:species) |> slice(1:4)
sw
  # A tibble: 4 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue            19   male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow         112   none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red             33   none  masculine Naboo     Droid  
#> 4 Darth Vader       202   136 none       white       yellow          41.9 male  masculine Tatooine  Human  

sw |> relocate(c(species, homeworld), .after = name)
  # A tibble: 4 × 11
#>   name           species homeworld height  mass hair_color skin_color  eye_color birth_year sex   gender   
#>   <chr>          <chr>   <chr>      <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>    
#> 1 Luke Skywalker Human   Tatooine     172    77 blond      fair        blue            19   male  masculine
#> 2 C-3PO          Droid   Tatooine     167    75 NA         gold        yellow         112   none  masculine
#> 3 R2-D2          Droid   Naboo         96    32 NA         white, blue red             33   none  masculine
#> 4 Darth Vader    Human   Tatooine     202   136 none       white       yellow          41.9 male  masculine

Für größere Änderungen kannst du mit mutate():

  • neue Spalten erstellen, die Funktionen vorhandener Spalten sind.
  • eine vorhandene Spalte ersetzen, indem du eine neue Spalte mit demselben Namen erstellst.
  • eine Spalte löschen, indem du ihren Wert auf NULL setzt.

mutate() ist offensichtlich mächtig, potenziell verwirrend und ein Grund, sehr dankbar für copy-on-modify zu sein.

Es gibt keinen offensichtlichen Grund, sich für den Body-Mass-Index von Star-Wars-Figuren zu interessieren, aber nur für den Fall:

starwars |> 
  select(c(name, species, height, mass)) |> 
  mutate(BMI = mass / (height / 100)^2) |> 
  head(4)
  # A tibble: 4 × 5
#>   name           species height  mass   BMI
#>   <chr>          <chr>    <int> <dbl> <dbl>
#> 1 Luke Skywalker Human      172    77  26.0
#> 2 C-3PO          Droid      167    75  26.9
#> 3 R2-D2          Droid       96    32  34.7
#> 4 Darth Vader    Human      202   136  33.3

Wenn du mit Funktionen wie mutate() nur auf einen Teil der Spalten arbeiten willst, ist die Sequenz select() |> mutate() aus dem obigen Beispiel eine Möglichkeit. Nur die ausgewählten Spalten landen im Ergebnis.

Alternativ kann es praktisch sein, pick() innerhalb des mutate()-Aufrufs zu verwenden:

starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.

Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:

```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
    # A tibble: 4 × 15
#>   name         height  mass hair_color skin_color eye_color birth_year sex   gender homeworld species films vehicles
#>   <chr>         <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr>  <chr>     <chr>   <lis> <list>  
#> 1 Luke Skywal…    172    77 blond      fair       blue            19   male  mascu… Tatooine  Human   <chr> <chr>   
#> 2 C-3PO           167    75 NA         gold       yellow         112   none  mascu… Tatooine  Droid   <chr> <chr>   
#> 3 R2-D2            96    32 NA         white, bl… red             33   none  mascu… Naboo     Droid   <chr> <chr>   
#> 4 Darth Vader     202   136 none       white      yellow          41.9 male  mascu… Tatooine  Human   <chr> <chr>   
   # ℹ 2 more variables: starships <list>, BMI <dbl>

Nur die mit pick ausgewählten Spalten werden bei der Mutation verwendet, aber es werden alle Spalten zurückgegeben.

Zeilenweise Operationen sind beim Ändern einzelner Tibbles weniger üblich (das Zusammenführen mehrerer Tibbles behandeln wir in einem späteren Konzept).

Eine Ausnahme: arrange() sortiert Zeilen nach den Werten in einer oder mehreren Spalten.

tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE  

tbl |> arrange(languages)
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 Julia        2012 TRUE        
#> 3 Python       1991 TRUE        
#> 4 R            1993 TRUE     

Zusammenfassung

Dataframes, ob traditionell oder Tibbles, sind zentral dafür, wie modernes R typischerweise verwendet wird.

Die meisten Tidyverse-Funktionen (nicht nur dplyr) nehmen Tibbles als Eingabe und/oder erzeugen sie als Ausgabe.

Dieses Konzept bot nur eine kurze Einführung und kratzt kaum an der Oberfläche dessen, was möglich ist.

Spätere Konzepte behandeln weitere Aspekte von Dataframes (im Rahmen der technischen Möglichkeiten von Exercism).

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab

Lerne Dataframes