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:
Komma:
read_csv()
Strichpunkt:
read_csv2()
Leerzeichen:
read_table()
…
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
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.