Kapitel 8 Daten I/O

8.1 Überblick

Im letzten Abschnitt haben wir die Gapminder-Daten als tibble aus dem gapminder Paket geladen. Dabei haben wir dann weder Daten, noch abgeleitete Ergebnisse, explizit in eine Datei geschrieben. Im wirklichen Leben werdet ihr aber ständig Daten, die in Tabellenform vorliegen, in R ein- und auslesen. Manchmal muss das sogar für Daten geschehen, die nicht in Tabellenform vorliegen.

Wie macht man das? Worauf muss man aufpassen?

8.1.1 Daten Import

Für das Importieren von Daten gibt es im Allgemeinen zwei Szenarien:

  • “Überrasche mich!” Diese Haltung müsst ihr einnehmen, wenn ihr einen Datensatz erhaltet und zum ersten Mal versucht diesen einzulesen. Man muss froh sein, wenn man die Daten ohne Fehlermeldung importieren kann. Im nächsten Schritt schaut man sich das Ergebnis an und entdeckt vermutlich Fehler in den Daten und/oder beim Import. Anschließend behebt man die Fehler und beginnt nochmal von vorne.

  • “Ein weiterer Tag im Paradies.” Das wird vermutlich euer Gefühl sein, wenn ihr versucht einen aufgeräumten Datensatz einzulesen (den jemand vorher in einem oder mehreren “Reinigungsskripten” aufgeräumt hat). Beim Einlesen solcher Daten sollte es keine Überraschungen geben.

Im zweiten Fall, und im weiteren Verlauf des ersten Falles, lernt ihr tatsächlich eine Menge darüber, wie die Daten strukturiert sind/sein sollten.

Ein wichtiger Import-Ratschlag: Verwende die Argumente der Importfunktion, um so weit wie möglich und so schnell wie möglich zu kommen. Macht man dies nicht, so sind oft nach dem Einlesen der Daten noch eine Reihe von weiteren Schritten nötig, bevor man mit der eigentlichen Analyse beginnen kann. Daher lest die Hilfe zu den Importfunktionen und nutzt die Argumente maximal aus, um den Import zu steuern.

8.1.2 Daten Export

Es wird viele Gelegenheiten geben, bei denen ihr Daten aus R exportieren wollt. Zwei wichtige Beispiele:

  • einen gesäuberten Datensatz, der bereit ist, analysiert zu werden

  • ein numerisches Ergebnis aus einer Datenaggregation oder Modellierung oder einer statistischen Schlussfolgerung

Erster Tipp: Der Output von heute ist der Input von morgen. Denkt an all die Schmerzen zurück, die ihr selbst beim Import von fremden Daten erlitten habt, und fügt euch nicht selbst solche Schmerzen zu!

Zweiter Tipp: Seid nicht zu clever. Eine einfache Textdatei, die von einem Menschen in einem Texteditor lesbar ist, sollte euer Standard sein, bis es einen guten Grund dafür gibt, dass dies nicht ausreichend ist. Das Lesen und Schreiben in exotische Formate wird das Erste sein, was möglicherweise in Zukunft oder auf einem anderen Computer nicht mehr funktioniert. Zudem schafft es Barrieren für jeden, der ein anderes Toolkit hat als ihr.

Wie passt das zu unserer Betonung der dynamischen Berichterstattung über R Markdown? Es gibt für alles eine Zeit und einen Ort. Es gibt Projekte und Dokumente, bei denen ihr euch intensiv mit knitr und rmarkdown beschäftigen könnt/wollt/müsst. Aber es gibt viele gute Gründe, warum (Teile) einer Analyse nicht (nur) in einen dynamischen Bericht eingebettet werden sollten. Vielleicht wollt ihr Daten bereinigen, um einen Datensatz für eine nachfolgende Analyse zu erzeugen. Vielleicht leistet ihr einen kleinen, aber entscheidenden Beitrag zu einem gigantischen Multi-Autoren-Papier, usw. ….

Denkt zudem daran, dass es natürlich auch noch andere Werkzeuge und Arbeitsabläufe gibt, um etwas reproduzierbar zu machen: z.B. make.

8.2 readr

Zur Einlesen und Ausgeben von Datensätzen verwenden wir das readr Paket, welches Alternativen zu den Standardfunktionen read.table() und write.table() bietet. readr ist Teil des tidyverse und daher führen wir standardmäßig einfach wieder

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.1
## ✔ 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

aus.

Einlesen der Gapminder Daten

Die Gapminder Daten könnten wir natürlich wie zuvor über das Laden des gapminder Pakets verfügbar machen. Da es in diesem Abschnitt aber um das Einlesen von Daten geht, versuchen wir die Daten als .tsv Datei (Tab getrennte Werte - so sind die Daten im Paket gespeichert) einzulesen. Aber dies bedeutet natürlich, dass wir die entsprechende .tsv Datei erst mal finden müssen. Dabei hilft uns glücklicherweise das fs Paket.

library(fs)
(gap_tsv <- path_package("gapminder", "extdata", "gapminder.tsv"))
## /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library/gapminder/extdata/gapminder.tsv

Nachdem wir jetzt den Speicherort der Datei kennen, können wir versuchen sie einzulesen.

8.3 Einlesen von Daten in Tabellenform

Die Haupt-Funktion zum Einlesen von Daten in readr ist read_delim(). Hier verwenden wir eine Variante, read_tsv(), für Tab getrennte Daten:

gapminder <- read_tsv(gap_tsv)
## Rows: 1704 Columns: 6
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: "\t"
## chr (2): country, continent
## dbl (4): year, lifeExp, pop, gdpPercap
## 
## ℹ 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.
gapminder
## # A tibble: 1,704 × 6
##    country     continent  year lifeExp      pop gdpPercap
##    <chr>       <chr>     <dbl>   <dbl>    <dbl>     <dbl>
##  1 Afghanistan Asia       1952    28.8  8425333      779.
##  2 Afghanistan Asia       1957    30.3  9240934      821.
##  3 Afghanistan Asia       1962    32.0 10267083      853.
##  4 Afghanistan Asia       1967    34.0 11537966      836.
##  5 Afghanistan Asia       1972    36.1 13079460      740.
##  6 Afghanistan Asia       1977    38.4 14880372      786.
##  7 Afghanistan Asia       1982    39.9 12881816      978.
##  8 Afghanistan Asia       1987    40.8 13867957      852.
##  9 Afghanistan Asia       1992    41.7 16317921      649.
## 10 Afghanistan Asia       1997    41.8 22227415      635.
## # ℹ 1,694 more rows

Wie wir sehen, wurde standardmäßig der komplette Datensatz eingelesen. Sind aber nur Teile eines Datensatzes relevant für die angestrebte Analyse, so besteht auch keine Notwendigkeit den kompletten Datensatz zu laden. In solchen Fällen kann man mit dem col_types Argument arbeiten

gapminder_short <- read_tsv(gap_tsv, col_types = cols_only(
  country = col_character(),
  continent = col_factor(),
  year = col_double(),
  lifeExp = col_double()
))
gapminder_short
## # A tibble: 1,704 × 4
##    country     continent  year lifeExp
##    <chr>       <fct>     <dbl>   <dbl>
##  1 Afghanistan Asia       1952    28.8
##  2 Afghanistan Asia       1957    30.3
##  3 Afghanistan Asia       1962    32.0
##  4 Afghanistan Asia       1967    34.0
##  5 Afghanistan Asia       1972    36.1
##  6 Afghanistan Asia       1977    38.4
##  7 Afghanistan Asia       1982    39.9
##  8 Afghanistan Asia       1987    40.8
##  9 Afghanistan Asia       1992    41.7
## 10 Afghanistan Asia       1997    41.8
## # ℹ 1,694 more rows

Zur Auswahl eines Teils der Variablen haben wir cols_only() verwendet. Diese Funktion erwartet bei der Auswahl der Variablen die Definition des Typs. In diesem Beispiel haben wir continten (anders als im Standardfall) zu einer Faktorvariable transformiert. Dadurch enthält die Variable zusätzlich die Information über die verschiedenen Ausprägungen der Variable

levels(gapminder_short$continent)
## [1] "Asia"     "Europe"   "Africa"   "Americas" "Oceania"

Über den Tabulator Spalten in einer Datentabelle zu trennen, ist natürlich nur eine Möglichkeit von vielen. Weitere Alternativen sind:

Für volle Flexibilität bei der Angabe des Trennzeichens kann aber jederzeit direkt read_delim() verwendet werden.

Der auffälligste Unterschied zwischen den readr-Funktionen und der Standardfunktion read.table()ist, dass readr immer ein Tibble erzeugt statt eines Data Frames. Da wir Tibbles bevorzugen ist unser

Fazit: Benutzt readr::read_delim() und “Freunde”.

Die Gapminder-Daten sind zu sauber und einfach, um die großartigen Funktionen von readr zur Geltung zu bringen. Ein Blick in Introduction to readr zeigt aber noch viele weitere Anpassungsmöglichkeiten der readr Funktionen.

8.4 Daten exportieren

Bevor wir etwas exportieren können, müssen wir natürlich (was so sicher nicht richtig ist - niemand zwingt uns dazu 😉) etwas berechnen, das es wert ist, exportiert zu werden. Lasst uns daher eine Zusammenfassung der maximalen Lebenserwartung auf Länderebene erstellen.

gap_life_exp <- gapminder |>  
  group_by(country, continent) |> 
  summarise(life_exp = max(lifeExp)) |> 
  ungroup()
## `summarise()` has grouped output by 'country'. You can override using the
## `.groups` argument.
gap_life_exp
## # A tibble: 142 × 3
##    country     continent life_exp
##    <chr>       <chr>        <dbl>
##  1 Afghanistan Asia          43.8
##  2 Albania     Europe        76.4
##  3 Algeria     Africa        72.3
##  4 Angola      Africa        42.7
##  5 Argentina   Americas      75.3
##  6 Australia   Oceania       81.2
##  7 Austria     Europe        79.8
##  8 Bahrain     Asia          75.6
##  9 Bangladesh  Asia          64.1
## 10 Belgium     Europe        79.4
## # ℹ 132 more rows

Das Objekt gap_life_exp betrachten wir nun als Zwischenergebnis, das wir für die Zukunft und für nachgelagerte Analysen oder Visualisierungen speichern wollen.

Die Haupt-Exportfunktion in readr ist write_delim(). Für verschiedene Dateiformate gibt es auch hier wieder verschiedene Komfortfunktionen. Mithilfe von write_csv() können wir den Inhalt von gap_life_exp in einer kommagetrennten Datei abspeichern.

write_csv(gap_life_exp, "data/gap_life_exp.csv")

Schauen wir uns die ersten paar Zeilen von gap_life_exp.csv an. Dazu können wir entweder die Datei öffnen oder z.B. im Terminal head verwenden

Das sieht recht ordentlich aus, obwohl es keine sichtbare Ausrichtung oder Trennung in Spalten gibt. Hätten wir die Basisfunktion read.csv() benutzt, würden wir Zeilennamen und viele Anführungszeichen sehen, es sei denn, wir hätten diese Features explizit abgeschaltet. Das schönere Standardverhalten ist daher der Hauptgrund, warum wir readr::write_csv() gegenüber write.csv() bevorzugen.

Bemerkung: Es ist auch nicht wirklich fair, sich über den Mangel an sichtbarer Ausrichtung zu beklagen. Schließlich erzeugen wir Dateien, die der Computer lesen soll. Falls ihr aber wirklich in der Datei “herumstöbern” wollt, benutzt View() in RStudio. Oder öffnet die Datei mit einem Spreadsheet Programm (!). Aber erliegt NIE der Versuchung, dort Datenmanipulationen vorzunehmen … kehrt zurück zu R und schreibt dort die entsprechenden Befehle, die ihr die nächsten 15 Mal (oder so oft wie nötig) ausführen könnt, wenn ihr diesen Datensatz (oder Datensätze derselben Form) importieren/bereinigen/aggregieren/exportieren wollt.

8.5 Daten über eine API

APIs (Application Programming Interface) sind eine sehr nützliche Methode, um auf interessante Daten zuzugreifen, die online zur Verfügung gestellt werden.

Anstatt einen Datensatz herunterladen zu müssen, ermöglichen APIs Daten direkt von bestimmten Webseiten über eine Schnittstelle anzufordern. Viele große Webseiten wie Twitter und Facebook ermöglichen über APIs den Zugriff auf Teile ihrer Daten.

Wir werden die Grundlagen des Zugriffs auf eine API besprechen. Dazu benötigt ihr aber kein Vorwissen bzgl. APIs.

8.5.1 Einführung

API ist ein allgemeiner Begriff für den Ort, an dem ein Computerprogramm mit einem anderen oder mit sich selbst interagiert. Wir sprechen über Web-APIs, bei denen zwei verschiedene Computer - ein Client und ein Server - miteinander interagieren, um Daten anzufordern bzw. bereitzustellen.

APIs bieten eine ausgefeilte Möglichkeit Daten von einer Webseite anzufordern. Wenn eine Webseite wie Twitter eine API einrichtet, richten sie im Wesentlichen einen Computer ein, der auf Datenanfragen wartet.

Sobald dieser Computer eine Datenabfrage empfängt, verarbeitet er die Daten selbst und sendet sie an den Computer, der sie angefordert hat. Unsere Aufgabe wird es sein R Code zu schreiben, der die Anfrage erstellt und dem Computer, auf dem die API läuft, mitteilt, was wir benötigen. Dieser Computer liest dann unseren Code, verarbeitet die Anfrage und gibt schön formatierte Daten zurück, die mithilfe existierender R Pakete verarbeitet werden können.

8.5.2 Erstellen von API-Abfragen in R

Um mit APIs in R zu arbeiten, müssen wir ein paar neue Pakete laden (und vorher natürlich installieren). Konkret werden wir mit den Paketen httr und jsonlite arbeiten. Sie spielen bei der Einbindung der APIs unterschiedliche Rollen, aber beide sind unverzichtbar.

Vermutlich habt ihr die beiden Pakete bisher nicht installiert. Daher starten wir mit dem Installieren dieser beiden Pakete

install.packages(c("httr", "jsonlite"))

und laden sie anschließend

library(httr)
library(jsonlite)
## 
## Attaching package: 'jsonlite'
## The following object is masked from 'package:purrr':
## 
##     flatten

8.5.3 Unsere erste API-Anfrage stellen

Der erste Schritt, um Daten von einer API zu erhalten, ist die eigentliche Anfrage in R. Diese Anfrage wird an den Server geschickt, der über die API verfügt, und wenn alles reibungslos verläuft, wird er uns eine Antwort zurücksenden.

Es gibt verschiedene Arten von Anfragen, die man an einen API-Server stellen kann. Diese verschiedenen Typen von Anfragen entsprechen verschiedenen Aktionen, die der Server ausführen soll.

Für unsere Zwecke fragen wir lediglich nach Daten, was einer GET-Anfrage entspricht. Andere Arten von Anfragen sind z.B. POST (post file) und PUT (send put request), aber diese sind für uns nicht von Interesse und werden wir daher nicht weiter besprechen.

Um eine GET-Anfrage zu erstellen, müssen wir die GET() Funktion aus dem httr Paket verwenden. Die GET() Funktion benötigt als Input eine URL, die die Adresse des Servers angibt, an den die Anforderung gesendet werden soll.

Als Beispiel werden wir mit der Open Notify API arbeiten, die Daten zu verschiedenen NASA-Projekten enthält. Mithilfe der Open Notify API können wir uns über den Standort der Internationalen Raumstation informieren und erfahren, wie viele Personen sich derzeit im Weltraum aufhalten.

Wir beginnen damit, dass wir unsere Anfrage mit der GET() Funktion stellen und die URL der API angeben:

jdata <- GET("http://api.open-notify.org/astros.json")

Die Ausgabe der Funktion GET() ist eine Liste, die alle Informationen enthält, die vom API-Server zurückgegeben werden.

8.5.4 GET() Ausgabe

Schauen wir uns an, wie die Variable jdata in der R-Konsole aussieht:

jdata
## Response [http://api.open-notify.org/astros.json]
##   Date: 2025-01-08 22:52
##   Status: 200
##   Content-Type: application/json
##   Size: 587 B

Als erstes fällt auf, dass die URL enthalten ist, an die die GET-Anfrage gesendet wurde. Außerdem erkennen wir das Datum und die Uhrzeit, zu der die Anfrage gestellt wurde, sowie die Größe der Antwort.

Die Information Content-Type gibt uns eine Vorstellung davon, welche Form die Daten haben. Diese spezielle Antwort besagt, dass die Daten ein JSON-Format annehmen, womit auch klar ist warum wir das Paket jsonlite geladen haben.

Der Status verdient eine besondere Aufmerksamkeit. Status bezieht sich auf den Erfolg oder Misserfolg der API-Anfrage, und er wird in Form einer Zahl angegeben. Die zurückgegebene Nummer gibt Auskunft darüber, ob die Anfrage erfolgreich war oder nicht. Dort können auch Gründe für einen möglichen Misserfolg enthalten sein.

Die Zahl 200 ist das, was wir sehen wollen. Sie entspricht einem erfolgreichen Antrag, und das ist es, was wir hier haben. Eine Übersicht über weitere Status Codes findet man z.B. auf dieser Webseite.

8.5.5 Handling JSON Data

JSON steht für JavaScript Object Notation. Während JavaScript eine weitere Programmiersprache ist, liegt unser Schwerpunkt bei JSON auf seiner Struktur. JSON ist nützlich, weil es von einem Computer leicht lesbar ist, und aus diesem Grund ist es zur primären Art und Weise geworden, wie Daten über APIs transportiert werden. Die meisten APIs senden ihre Antworten im JSON-Format.

JSON ist als eine Reihe von Schlüssel-Werte-Paaren formatiert, wobei ein bestimmtes Wort (“Schlüssel”) mit einem bestimmten Wert assoziiert ist. Ein Beispiel für diese Schlüssel-Wert-Struktur ist unten dargestellt:

{
    “name”: “Jane Doe”,
    “number_of_skills”: 2
}

In ihrem aktuellen Zustand sind die Daten in der Variablen jdata nicht verwendbar. Die Daten sind als Unicode-Rohdaten in jdata enthalten, und müssen in das JSON-Format konvertiert werden.

Dazu müssen wir zunächst den rohen Unicode in character Daten konvertieren, die dem oben gezeigten JSON-Format ähneln. Die Funktion rawToChar() führt genau diese Aufgabe aus:

rawToChar(jdata$content)
## [1] "{\"people\": [{\"craft\": \"ISS\", \"name\": \"Oleg Kononenko\"}, {\"craft\": \"ISS\", \"name\": \"Nikolai Chub\"}, {\"craft\": \"ISS\", \"name\": \"Tracy Caldwell Dyson\"}, {\"craft\": \"ISS\", \"name\": \"Matthew Dominick\"}, {\"craft\": \"ISS\", \"name\": \"Michael Barratt\"}, {\"craft\": \"ISS\", \"name\": \"Jeanette Epps\"}, {\"craft\": \"ISS\", \"name\": \"Alexander Grebenkin\"}, {\"craft\": \"ISS\", \"name\": \"Butch Wilmore\"}, {\"craft\": \"ISS\", \"name\": \"Sunita Williams\"}, {\"craft\": \"Tiangong\", \"name\": \"Li Guangsu\"}, {\"craft\": \"Tiangong\", \"name\": \"Li Cong\"}, {\"craft\": \"Tiangong\", \"name\": \"Ye Guangfu\"}], \"number\": 12, \"message\": \"success\"}"

Die resultierende Zeichenfolge sieht zwar recht unordentlich aus, aber es liegt wirklich die JSON-Struktur vor.

Ausgehend von diesem character Vektor können wir nun mit fromJSON(), aus dem jsonlite Paket, alles in ein Listenformat transformieren.

Die fromJSON() Funktion benötigt einen character Vektor, der die JSON-Struktur enthält, die wir aus der Ausgabe von rawToChar() erhalten haben. Wenn wir also diese beiden Funktionen nacheinander anwenden, erhalten wir die gewünschten Daten in einem Format, das wir in R leicht bearbeiten können.

data <-  fromJSON(rawToChar(jdata$content))
glimpse(data)
## List of 3
##  $ people :'data.frame': 12 obs. of  2 variables:
##   ..$ craft: chr [1:12] "ISS" "ISS" "ISS" "ISS" ...
##   ..$ name : chr [1:12] "Oleg Kononenko" "Nikolai Chub" "Tracy Caldwell Dyson"..
##  $ number : int 12
##  $ message: chr "success"

Die Liste data hat drei Elemente. Uns interessiert in erster Linie das Data Frame people.

data$people
##       craft                 name
## 1       ISS       Oleg Kononenko
## 2       ISS         Nikolai Chub
## 3       ISS Tracy Caldwell Dyson
## 4       ISS     Matthew Dominick
## 5       ISS      Michael Barratt
## 6       ISS        Jeanette Epps
## 7       ISS  Alexander Grebenkin
## 8       ISS        Butch Wilmore
## 9       ISS      Sunita Williams
## 10 Tiangong           Li Guangsu
## 11 Tiangong              Li Cong
## 12 Tiangong           Ye Guangfu

Also, da haben wir unsere Antwort: Zum Zeitpunkt des letzten Updates Jan 08, 2025 von R4ews befanden sich 12 Personen im Weltraum. Aber wenn ihr den Code zu einem späteren Zeitpunkt ausprobiert, könnten es auch schon wieder andere Namen und eine andere Anzahl sein. Das ist einer der Vorteile von APIs - im Gegensatz zu Datensätzen, die man im Spreadsheet Format herunterladen kann, werden sie in der Regel in Echtzeit oder nahezu in Echtzeit aktualisiert. APIs bieten somit die Möglichkeit leicht auf sehr aktuelle Daten zuzugreifen.

In diesem Beispiel haben wir einen sehr unkomplizierten API-Workflow durchlaufen. Die meisten APIs fordern, dass man demselben allgemeinen Muster folgt, aber dabei können die jeweilgen Aufrufe/Befehle durchaus deutlich komplexer sein.

In unserem Beispiel war es ausreichend nur die URL anzugeben. Aber einige APIs verlangen mehr Informationen vom Benutzer. Darauf gehen wir aber erstmal nicht weiter ein. Stattdessen fragen wir noch nach dem Ort der ISS im Moment der Abfrage

jdata <-  GET("http://api.open-notify.org/iss-now.json",)
data <- fromJSON(rawToChar(jdata$content))
data$iss_position
## $latitude
## [1] "46.2802"
## 
## $longitude
## [1] "48.1053"
data$timestamp
## [1] 1736376735

Diese API gibt uns die Zeit in Form von Unixzeit zurück. Unixzeit ist die Zeitspanne, die seit dem 1. Januar 1970 vergangen ist. Mithilfe der Funktion as_datetime() aus dem lubridate Paket können wir die Unixzeit aber leicht umrechnen

lubridate::as_datetime(data$timestamp)
## [1] "2025-01-08 22:52:15 UTC"

Damit wollen wir den Abschnitt zu APIs beenden. Macht euch bewusst, dass wir hier wirklich nur die Basics in Bezug auf APIs eingeführt haben. Aber hoffentlich hat euch diese Einführung trotzdem ausreichend Vertrauen gegeben, sich mit einigen komplexeren und leistungsfähigeren APIs auseinanderzusetzen.

8.6 Weiteres Material

Wer noch mehr zum Thema Daten Import lesen will, der soll einen Blick in das Kapitel Data import im Buch R for Data Science von Hadley Wickham und Garrett Grolemund (2016) werfen.