5  Das tidyverse-Paket

Dieses Kapitel orientiert sich zum Teil an dem Kapitel “The tidyverse” des eBuchs Introduction to Data Science von Rafael A. Irizarry. Sehr lesenswert ist außerdem das Buch Data Science for R von Hadley Wickham und Garrett Grolemund. Beide Bücher gehen jedoch über das hinaus, was in diesem Kurs behandelt wird.

5.1 Tidy data

In diesem Kurs gehen wir stets davon aus, dass die Daten als sogenannte tidy data, also als “saubere Daten” vorliegen. Saubere Daten liegen vor, wenn jede Zeile eine Beobachtung beinhaltet und die Spalten die verschiedenen Variablen repräsentieren, die für jede Beobachtung erhoben wurden. In der praktischen empirischen Arbeit hat man natürlich nicht immer saubere Daten und muss häufig viel Mühe aufwenden, um die Daten in ein sauberes Format zu überführen.

Für den Umgang mit sauberen Daten gibt es eine Reihe von R-Paketen, die in dem Paket tidyverse zusammengefasst sind. Um mit ihnen arbeiten zu können, muss das tidyverse-Paket einmalig installiert werden. In wird beschrieben, wie man Pakete installiert. Sollte das tidyverse-Paket bei Ihnen noch nicht installiert sein, holen Sie das nun bitte nach.

Wenn das Paket installiert ist, kann man es zu Beginn einer R-Sitzung durch den Befehl library(tidyverse) aktivieren. Diese Aktivierung muss bei jeder neuen Benutzung von R erneut erfolgen.

5.2 Transformationen

Das Paket tidyverse bietet Funktionen, mit denen Transformationen von Dataframes durchgeführt werden können. Dazu gehört die Auswahl von Variablen oder Beobachtungen, das Errechnen neuer Variablen aus den bisherigen Variablen, Sortieren etc.

Zur Illustration der Befehle laden wir den Datensatz worldbank.csv, den Sie auch auf der Learnweb-Seite finden. Der Datensatz wird in beschrieben. Die Weltbank hat einen API-Daten-Zugang, für den es in R das Paket wbstat gibt. Aus dem umfangreichen Datenbestand ist ein kleiner Auszug in der Datei worldbank.csv gespeichert, den Sie auch auf der Learnweb-Seite des Kurses finden. Eine genauere Beschreibung der Variablen gibt .

Zum Einlesen des Dataframes verwenden wir eine der Methoden aus , z.B. den interaktiven Import () oder den kommandogesteuerten Import (). Der Dataframe wird unter dem Namen worldbank abgelegt.

worldbank <- read_csv("../data/worldbank.csv")

(Wenn Sie den Datensatz auf Ihrem Computer laden wollen, muss der Pfad entsprechend angepasst werden.) Der Anfang des Datensatzes sieht so aus:

head(worldbank)
# A tibble: 6 × 5
  country  year   pop gdp_pc   co2
  <chr>   <dbl> <dbl>  <dbl> <dbl>
1 ABW      1990 62753 21733. 0.189
2 ABW      1991 65896 23100. 0.215
3 ABW      1992 69005 23889. 0.239
4 ABW      1993 73685 24576. 0.241
5 ABW      1994 77595 25791. 0.266
6 ABW      1995 79805 26255. 0.295

Die Daten liegen als tidy data vor. In jeder Zeile werden die Variablenwerte der Einwohnerzahl (pop), des Bruttoinlandsprodukts pro Einwohner (gdp_pc in kaufkraftbereinigten 2021-US-Dollar) und des gesamten CO2-Ausstoßes (co2, in Megatonnen CO2-Äquivalente, ohne Land-Use Change and Forestry) im Jahr year für das Land country angegeben. Das Land wird durch die übliche ISO-Abkürzung (3-Steller) bezeichnet.

5.2.1 filter

Mit der Funktion filter kann man bestimmte Beobachtungen aus einem Dataframe auswählen. Angenommen, wir möchten aus dem Dataframe worldbank nur die Beobachtungen für Deutschland (mit der Abkürzung “DEU”) sehen. Die Funktion filter hat als erstes Argument den Dataframe und als weiteres Argument die Bedingung. Ein Zeilenumbruch zwischen den Argumenten erleichtert die Lesbarkeit, ist aber nicht notwendig.

filter(worldbank, 
       country == "DEU")

Die ersten Zeilen des Outputs sehen so aus:

# A tibble: 6 × 5
  country  year      pop gdp_pc   co2
  <chr>   <dbl>    <dbl>  <dbl> <dbl>
1 DEU      1990 79433029 19521. 1008.
2 DEU      1991 80013896 21058.  987.
3 DEU      1992 80624598 21806.  933.
4 DEU      1993 81156363 21940.  925.
5 DEU      1994 81438348 22887.  910.
6 DEU      1995 81678051 23615.  906.

Wir sehen beispielsweise, dass in Deutschland im Jahr 1991 ziemlich genau 80 Millionen Einwohner hatte.

Man kann auch komplexere Bedingungen eingeben, z.B. alle Beobachtungen aus Deutschland oder Frankreich zwischen 2015 und 2018:

filter(worldbank, 
       (country=="DEU" | country=="FRA") & 
       year >= 2015 & 
       year <= 2018)
# A tibble: 8 × 5
  country  year      pop gdp_pc   co2
  <chr>   <dbl>    <dbl>  <dbl> <dbl>
1 DEU      2015 81686611 48545.  780.
2 DEU      2016 82348669 51570.  784.
3 DEU      2017 82657002 54110.  769.
4 DEU      2018 82905782 56273.  745.
5 FRA      2015 66548272 40905.  332.
6 FRA      2016 66724104 42880.  335.
7 FRA      2017 66918020 44469.  339.
8 FRA      2018 67158348 46381.  328.

Eine ODER-verknüpfung erfolgt durch den senkrechten Strich (|), eine UND-Verknüpfung durch das Et-Zeichen (&). Negationen werden durch das Ausrufezeichen (!) gekennzeichnet. Die Relationen sind:

  • Kleiner als: <
  • Größer als: >
  • Kleiner als oder gleich: <=
  • Größer als oder gleich: >=
  • Gleich: ==
  • Ungleich: !=

Beachten Sie, dass eine Gleichheitsbedingung immer mit einem doppelten Gleichheitszeichen geschrieben werden muss (==). Das einfache Gleichheitszeichen ist eine andere, synonyme Schreibweise für den Zuweisungsoperator <-. Es ist ein häufiger Fehler (der einem leider immer wieder passiert), in einer Bedingung das einfache Gleichheitszeichen zu schreiben.

Wenn bei einer Variable Werte fehlen, ist es manchmal sinnvoll, alle Beobachtungen mit fehlenden Werten aus der weiteren Betrachtung auszuschließen. Dabei hilft die Funktion is.na (für: is there any not available?). Mit ihr kann man feststellen, ob eine Beobachtung fehlt oder nicht. Um zum Beispiel nur Beobachtungen zu behalten, in denen die Variable gdp_pc nicht fehlt, lautet der Befehl

filter(worldbank, 
       !is.na(gdp_pc))

Das Ausrufezeichen steht hierbei für den logischen NICHT-Operator. Den Output der Funktion filter kann man als neuen Dataframe abspeichern. Um die ausgewählten Beobachtungen als Dataframe mit dem Namen wb_auswahl zu speichern, gibt man folgendes ein:

wb_auswahl <- filter(worldbank,
                     BEDINGUNG)

Wenn man sicher ist, dass im weiteren Verlauf der Datenanalyse nur noch die ausgewählten Beobachtungen benötigt werden, dann kann man den Output auch dem alten Dataframe-Namen zuweisen. In diesem Fall wird der alte Dataframe überschrieben und die nicht ausgewählten Beobachtungen gehen verloren.

worldbank <- filter(worldbank,
                    BEDINGUNG)

5.2.2 select

Wenn man nicht alle Variablen (Spalten) anzeigen lassen will, kann man mit der Funktion select einzelne Spalten auswählen. Die Syntax ist ähnlich wie bei filter: Das erste Argument ist der Dataframe, die weiteren Argumente sind die Variablen, die man auswählen will.

Um von dem Beispieldatensatz worldbank nur die Spalten country, year und co2 anzuzeigen, lautet der Befehl

select(worldbank, 
       country,
       year,
       co2)

Die Zeilenumbrüche erleichtern die Lesbarkeit, sie sind aber nicht unbedingt erforderlich. Als Output erhält man (hier werden nur die ersten paar Zeilen angezeigt):

# A tibble: 6 × 3
  country  year   co2
  <chr>   <dbl> <dbl>
1 ABW      1990 0.189
2 ABW      1991 0.215
3 ABW      1992 0.239
4 ABW      1993 0.241
5 ABW      1994 0.266
6 ABW      1995 0.295

Es ist auch möglich, Variablen auszuschließen. Dazu setzt man ein Minuszeichen vor den Variablennamen. Mit select(worldbank, -co2) erhält man einen Dataframe mit allen Variablen bis auf co2.

Den Output des select-Befehls kann man als eigenen Dataframe unter einem neuen Namen speichern, z.B.

wb_auswahl <- select(worldbank, 
                     VARIABLE1,
                     VARIABLE2)

Wenn die nicht ausgewählten Spalten im weiteren Verlauf der Analyse nicht mehr gebraucht werden, kann man den Output unter dem alten Dataframe-Namen speichern.

worldbank <- select(worldbank, 
                    VARIABLE1,
                    VARIABLE2)

Achtung: In diesem Fall gehen alle nicht ausgewählten Variablen verloren.

5.2.3 mutate

Wir berechnen nun aus den vorhandenen Variablen eine neue Variable und fügen sie dem Datensatz hinzu. Im Gegensatz zu filter und select wird der Dataframe also nicht kleiner, sondern größer. Berechnet werden soll zunächst die der CO2-Ausstoß pro Kopf (in Tonnen). Dazu wird der gesamte CO2-Ausstoß in Megatonnen (also Millionen Tonnen) mit 1000000 multipliziert und anschließend durch die Anzahl der Einwohner dividiert.

co2_pc = co2 * 1000000 / pop

Die Funktion zum Berechnen neuer Variablen aus schon vorhandenen Variablen lautet mutate. Der Output von mutate ist der alte Dataframe ergänzt um eine (oder auch mehrere) Spalten mit der neuen Variablen. Es ist oft sinnvoll, den Output unter dem alten Dataframe-Namen zu speichern. Auf diese Weise wächst der Dataframe um eine Spalte (oder mehrere).

worldbank <- mutate(worldbank, 
                    co2_pc = co2 * 1000000 / pop)

Wie bei select und filter hat die Funktion mutate als erstes Argument einen Dataframe, das folgende Argument gibt an, welche neue Variable auf welche Weise aus welchen bisherigen Variablen errechnet werden soll. Die Funktion mutate gibt den alten Dataframe mit der neuen zusätzlichen Spalte als Output zurück. Wir speichern den neuen, größeren Dataframe wieder unter dem Namen worldbank. Der Dataframe ist nun um eine Variable breiter geworden.

head(worldbank)
# A tibble: 6 × 6
  country  year   pop gdp_pc   co2 co2_pc
  <chr>   <dbl> <dbl>  <dbl> <dbl>  <dbl>
1 ABW      1990 62753 21733. 0.189   3.02
2 ABW      1991 65896 23100. 0.215   3.27
3 ABW      1992 69005 23889. 0.239   3.46
4 ABW      1993 73685 24576. 0.241   3.27
5 ABW      1994 77595 25791. 0.266   3.43
6 ABW      1995 79805 26255. 0.295   3.69

Als nächstes berechnen wir die Höhe des Bruttoinlandsprodukts (in Mrd. Dollar) , indem wir das BIP pro Kopf mit der Einwohnerzahl multiplizieren und durch 1e9 dividieren (1e9 ist die Kurzschreibweise für 109, also für eine Milliarde). Der zugehörige mutate-Befehl lautet

worldbank <- mutate(worldbank, 
                    gdp = gdp_pc * pop / 1e9)

Man kann auch mehrere neue Spalten auf einmal erzeugen. Die Definitionen der neuen Variablen werden dann einfach durch Kommas getrennt. Um nicht nur die CO2-Emissionen pro Kopf, sondern auch das gesamte Bruttoinlandsprodukt auszurechnen, wäre der Befehl (den wir hier jedoch nicht ausführen):

worldbank <- mutate(worldbank, 
                    co2_pc = co2 * 1000000 / pop,
                    gdp = gdp_pc * pop / 1e9) 

Was passiert, wenn man als Variablennamen für die neue Variable den Namen einer schon vorhandenen Variable eingibt? In diesem Fall wird die alte Variable überschrieben. Solche Änderungen sind zwar erlaubt, aber meist nicht empfehlenswert, weil man leicht vergisst, ob schon die neue oder noch die alte Definition gilt.

5.2.4 Pipe %>%

Der sogenannte Pipe-Operator %>% erlaubt es, eine Reihe von Funktionen in übersichtlicher Form nacheinander so auszuführen, dass der Output einer Funktion als Input für die nächste Funktion dient.

Im Abschnitt zu der Funktion filter haben wir alle Beobachtungen aus Deutschland ausgewählt. Wenn wir vom Output dieser Funktion nur die Spalten year, co2 und gdp_pc ausgegeben haben möchten, können wir das durch select erreichen. Die Funktionen werden also nacheinander ausgeführt:

dataframe → filter → select

Für solche hintereinander geschalteten Funktionen kann man den Pipe-Operator verwenden. Der Code sieht dann so aus:

worldbank %>% filter(country == "DEU") %>% select(year, co2, gdp_pc)
# A tibble: 33 × 3
    year   co2 gdp_pc
   <dbl> <dbl>  <dbl>
 1  1990 1008. 19521.
 2  1991  987. 21058.
 3  1992  933. 21806.
 4  1993  925. 21940.
 5  1994  910. 22887.
 6  1995  906. 23615.
 7  1996  935. 24160.
 8  1997  904. 24644.
 9  1998  903. 25444.
10  1999  869. 26518.
# ℹ 23 more rows

Für eine bessere Lesbarkeit sind Zeilenumbrüche hilfreich. Ein Zeilenumbruch sollte immer nach dem Pipezeichen %>% erfolgen. Die gleiche Befehlsabfolge sieht dann so aus:

worldbank %>% 
    filter(country == "DEU") %>% 
    select(year, 
           co2, 
           gdp_pc)

Was genau macht der Pipe-Operator?

Allgemein gesprochen schickt der Pipe-Operator das Ergebnis (Output) der Funktion auf der linken Seite als erstes Argument in die Funktion auf der rechten Seite. Hier ist ein sehr einfaches Beispiel:

16 %>% sqrt()
[1] 4

Die Reihe lässt sich verlängern:

16 %>% sqrt() %>% log()
[1] 1.386294

Dieser Ausdruck entspricht ln(16).

Vergessen Sie beim Pipe-Operator nicht, dass das erste Argument in der Funktion auf der rechten Seite entfällt. In den Funktionen mutate, filter und select wird also der Dataframe (als erstes Argument) nicht mehr angegeben. Betrachten Sie noch einmal die Pipe-Folge:

worldbank %>% 
    filter(country == "DEU") %>% 
    select(year, 
           co2, 
           gdp_pc)

Hier dient worldbank als erstes Argument der Funktion filter, und der Output der filter-Funktion dient als erstes Argument der select-Funktion. Der Pipe-Operator ist immer dann besonders einfach einzusetzen, wenn das erste Argument der Funktionen ein Dataframe ist. Die Funktionen des Pakets tidyverse (wie mutate, filter, select und einige weitere Funktionen, die wir erst später behandeln werden) haben diese Struktur.

5.3 Sortieren

Beim ersten Arbeiten mit einem Datensatz ist es oft hilfreich und interessant, den Dataframe auf verschiedene Weise zu sortieren. Beispielsweise können wir den Dataframe worldbank nach der Größe des Pro-Kopf-Bruttoinlandsprodukts (in US-Dollar, inflationsbereinigt) aufsteigend sortieren (und nur die ersten Zeilen ausgeben lassen):

worldbank %>% 
    arrange(gdp_pc) %>% 
    head()
# A tibble: 6 × 7
  country  year      pop gdp_pc   co2 co2_pc   gdp
  <chr>   <dbl>    <dbl>  <dbl> <dbl>  <dbl> <dbl>
1 LBR      1995  2169162   254. 0.259 0.119  0.552
2 LBR      1994  2149928   263. 0.254 0.118  0.565
3 LBR      1996  2232289   282. 0.283 0.127  0.630
4 MOZ      1992 13613316   292. 1.16  0.0853 3.97 
5 MOZ      1990 13094537   296. 1.16  0.0887 3.88 
6 RWA      1994  6792352   311. 0.479 0.0706 2.11 

Offensichtlich war das Pro-Kopf-Inlandsprodukt in den 1990er Jahren in Liberia, Mozambique und Ruanda besonders niedrig.

Wenn wir absteigend (“descending”) sortieren, ist der Output:

worldbank %>% 
    arrange(desc(gdp_pc)) %>% 
    head()
# A tibble: 6 × 7
  country  year     pop  gdp_pc   co2 co2_pc   gdp
  <chr>   <dbl>   <dbl>   <dbl> <dbl>  <dbl> <dbl>
1 QAT      2012 1722438 180939. 81.9   47.6  312. 
2 QAT      2011 1624761 174620. 78.3   48.2  284. 
3 QAT      2013 1916426 169203. 88.9   46.4  324. 
4 QAT      2010 1616832 151646. 69.3   42.8  245. 
5 MAC      2013  591900 149794.  1.83   3.10  88.7
6 QAT      2014 2151745 148389. 95.3   44.3  319. 

Das höchste Pro-Kopf-Inlandsprodukt erzielte Qatar (QAT) im Jahr 2012. Auch in Macao (MAC) war das Pro-Kopf-Inlandsprodukt sehr hoch.

Die Funktion top_n gibt nur die n Zeilen mit den größten oder kleinsten Werten einer Variable aus. Zum Beispiel sind die fünf Beobachtungen mit den höchsten CO2-Emissionen in dem Dataframe worldbank:

worldbank %>% 
    top_n(5, co2)
# A tibble: 5 × 7
  country  year        pop gdp_pc    co2 co2_pc    gdp
  <chr>   <dbl>      <dbl>  <dbl>  <dbl>  <dbl>  <dbl>
1 CHN      2018 1402760000 16007. 11554.   8.24 22454.
2 CHN      2019 1407745000 17262. 11819.   8.40 24301.
3 CHN      2020 1411100000 17891. 12037.   8.53 25247.
4 CHN      2021 1412360000 20407. 12718.   9.00 28822.
5 CHN      2022 1412175000 22510. 12667.   8.97 31788.

Von allen Staaten emittiert also China (CHN) in den letzten Jahren die größte Menge an CO2 (absolut, nicht pro Kopf).

5.4 Kennzahlen

Große Datensätze sind oftmals schwer zugänglich, man spricht manchmal auch von “Datenfriedhöfen”. Um die Daten wirklich zum Sprechen zu bringen, brauchen wir Werkzeuge. Ein sehr wichtiges Werkzeug der Datenanalyse sind Kennzahlen (“summaries”). Die bekannteste Kennzahl ist sicherlich der Durchschnitt, auf den wir später im noch ausführlicher eingehen werden. Auch die Summe ist in vielen Situationen eine nützliche Kennzahl. In diesem Abschnitt lernen Sie zwei neue Funktionen kennen, mit denen die Datenanalyse vereinfacht wird: summarise und group_by. Außerdem wird noch die Funktion pull vorgestellt, mit der die Resultate für die Weiterverarbeitung in R vorbereitet werden.

5.4.1 summarise

Die summarise-Funktion erlaubt es, Kennzahlen auf eine intuitive und leicht lesbare Art zu berechnen. Wir starten mit einem einfachen Beispiel. Der Dataframe worldbank enthält Angaben zu CO2-Emissionen in allen Ländern von 1990 bis 2022 (für viele Länder ist der Datenbestand leider nicht vollständig). Wir beschränken den Datensatz in diesem Beispiel auf alle Beobachtungen des Jahres 2020 und speichern die Auswahl unter dem Namen wb2020.

wb2020 <- filter(worldbank, year == 2020)

Der folgende Code berechnet die Summe der CO2-Emissionen aller Länder (in Megatonnen) und die Summe der Bruttoinlandsprodukte aller Länder (in Billionen US-Dollar). Die Schreibweise 1e12 steht für 1×1012, also eine Billion.

wb2020 %>% 
    summarise(co2welt = sum(co2),
              gdpwelt = sum(gdp_pc*pop)/1e12) -> s
print(s)
# A tibble: 1 × 2
  co2welt gdpwelt
    <dbl>   <dbl>
1  34500.    137.

Was passiert hier? Durch den summarise-Befehl wird ein neuer Dataframe erzeugt und unter dem Namen s gespeichert. Der neue Dataframe hat zwei Spalten und nur eine Zeile. Die Spaltennamen haben wir in der summarise-Funktion festgelegt, andere Namen wären natürlich ebenso möglich gewesen. Die Funktion sum ist eine interne R-Funktion.

Achtung: Am Ende der Pipe wird der endgültige Dataframe mit dem Pfeilsymbol -> unter einem neuen Dataframe-Namen gespeichert. Hier darf man nicht das Pipe-Zeichen %>% verwenden. Es ist auch möglich diese Zuweisung anders herum an den Anfang der Pipe zu setzen:

s <- worldbank %>% 
        filter(year == 2020) %>%
        summarise(co2welt = sum(co2),
                  gdpwelt = sum(gdp_pc*pop)/1e12)

Welche Variante man bevorzugt, ist Geschmackssache.

5.4.2 group_by

Sehr oft berechnet man Kennzahlen nicht nur für die gesamte Population, sondern auch für Teilgruppen. Zum Beispiel können wir die Summe der CO2-Emissionen für jedes Jahr getrennt errechnen. In diesem Fall werden die Beobachtungen nach der Variable year in Gruppen zusammengefasst. Für jede Gruppe berechnen wir dann die Summe (also die CO2-Weltemissions des jeweiligen Jahres). Um die Gruppen zu definieren, verwenden wir die Funktion group_by.

s <- worldbank %>% 
        group_by(year) %>%
        summarise(weltco2 = sum(co2))

Die letzten 10 Zeilen des Outputs sehen so aus:

tail(s,10)
# A tibble: 10 × 2
    year weltco2
   <dbl>   <dbl>
 1  2013  34339.
 2  2014  34504.
 3  2015  34348.
 4  2016  34416.
 5  2017  34995.
 6  2018  35924.
 7  2019  35953.
 8  2020  34500.
 9  2021  36521.
10  2022  36845.

Der Befehl group_by(year) verändert den Dataframe kaum sichtbar. Er legt nur fest, dass die Beobachtungen des Dataframes nach Jahren gruppiert werden. Man spricht daher auch von einem “gruppierten Dataframe”. Viele Funktionen des tidyverse-Pakets reagieren auf die Gruppierung. So berechnet die Funktion summarise die Kennzahlen jetzt für jede Gruppe einzeln und erstellt einen entsprechenden neuen Dataframe (der hier s genannt wird). Der neue Dataframe, den der summarise-Befehl liefert, hat diese Eigenschaften:

  • Der neue Dataframe hat so viele Zeilen wie es Gruppen gibt.
  • Die erste Spalte des neuen Dataframes zeigt alle Ausprägungen der Gruppierungsvariable.
  • Die zweite Spalte enthält die Kennzahlen, die für die Gruppen mit summarise berechnet wurden.
  • Wenn mehrere Kennzahlen berechnet werden, gibt es für jede Kennzahl eine Spalte.

5.4.3 pull

Als Ergebnis der summarise-Funktion erhalten wir - rein formal gesehen - immer einen Dataframe, selbst wenn es sich nur um eine einzige Zahl handelt. Das kann problematisch werden, wenn wir die Ergebnisse in anderen R-Funktionen weiterverwenden wollen, die numerische Vektoren als Input erwarten. Es gibt jedoch einen einfachen Weg, die Werte einer Spalte als numerischen Vektor aus einem Dataframe heraus zu ziehen. Die Funktion, die das leistet, heißt pull. Ein Beispiel zur Illustration:

worldbank %>%
    filter(year == 2020) %>%
    summarise(weltco2 = sum(co2)) %>%
    pull(weltco2) -> x

print(x)
[1] 34500.43
class(x)
[1] "numeric"

Die Pipe-Kette wählt aus dem Dataframe worldbank alle Beobachtungen des Jahres 2020 aus. Anschließend wird die Summe aller CO2-Emissionen (in 1000 t) berechnet. An dieser Stelle wird normalerweise ein Dataframe (mit nur einer Zeile und einer Spalte) ausgegeben. Durch die Weiterleitung in die pull-Funktion wird aber der Inhalt der Spalte weltco2 als numerisches Objekt herausgezogen und letztlich als x gespeichert. Die class-Funktion zeigt, dass es sich bei x nicht mehr um einen Dataframe, sondern um eine Zahl handelt.

5.5 Doppelungen

Ein Fehler, der in Datensätzen gelegentlich auftritt, sind Doppelungen. Eine Beobachtung, die nur als eine Zeile im Datensatz vorkommen sollte, wird versehentlich in einer zweiten, identischen Zeile wiederholt. Das kann natürlich auch mehrere Beobachtungen betreffen. In großen Datensätzen kann man dieses Problem nicht “per Hand” lösen. Die Funktion distinct aus dem tidyverse-Paket hilft weiter.

Das erste Argument der Funktion distinct ist (wie bei quasi allen Funktion aus dem tidyverse-Paket) der Dataframe. Wenn keine weiteren Argument angegeben sind, werden alle Zeilen aus dem Dataframe entfernt, bei denen alle Variablenwerte identisch sind. Das lässt sich am einfachsten an einem kleinen Beispiel veranschaulichen.

Beispiel:

Der Dataframe D enthält nur sechs Beobachtungen von vier Variablen.

D <- data.frame(Var1=c(3,4,5,4,2,4),
                Var2=c(1999,2000,2004,2000,2003,2000),
                Var3=c(834,1221,1204,1221,976,857),
                Var4=c(0,1,1,1,0,0))
D
  Var1 Var2 Var3 Var4
1    3 1999  834    0
2    4 2000 1221    1
3    5 2004 1204    1
4    4 2000 1221    1
5    2 2003  976    0
6    4 2000  857    0

In diesem Datensatz sind die Zeilen 2 und 4 komplett identisch. Die Doppelung wird durch die Funktion distinct entfernt:

distinct(D)
  Var1 Var2 Var3 Var4
1    3 1999  834    0
2    4 2000 1221    1
3    5 2004 1204    1
4    2 2003  976    0
5    4 2000  857    0

In den meisten Fällen wird man den bereinigten Datensatz unter dem gleichen Namen (hier: D) oder unter einem neuen Namen abspeichern.

D_neu <- distinct(D)

Mit der Funktion distinct kann man auch Zeilen entfernen, in denen nur manche Variablen identische Werte annehmen, andere Variablen hingegen nicht. So sind in dem Beispiel von oben die ersten beiden Variablenwerte der sechsten Zeile identisch zu den Werten in den Zeilen 2 und 4 (die Variablen Var3 und Var4 nehmen jedoch andere Werte an). Wenn eine Doppelung nur für bestimmte Variablen relevant ist, kann man diese Variablen als Argumente in der distinct-Funktion angeben.

distinct(D, Var1, Var2)
  Var1 Var2
1    3 1999
2    4 2000
3    5 2004
4    2 2003

In diesem Fall werden die restlichen Variablen standardmäßig entfernt. Um sie im Datensatz zu belassen, dient die Option .keep_all (mit einem Punkt am Anfang). Das sieht dann so aus:

distinct(D, Var1, Var2, .keep_all=TRUE)
  Var1 Var2 Var3 Var4
1    3 1999  834    0
2    4 2000 1221    1
3    5 2004 1204    1
4    2 2003  976    0

Wie alle Funktionen aus dem tidyverse-Paket, lässt sich auch die Funktion distinct in eine Pipe integrieren.

5.6 Merging

Wenn Daten, die man gemeinsam analysieren will, in zwei getrennten Dataframes vorliegen, müssen sie zusammengespielt werden (engl. merge). Oftmals möchte man auch zusätzliche Informationen, die aus einem Datensatz errechnet wurden, an einen Dataframe anfügen. Zur Erinnerung: In einem Dataframe in “sauberem Format” ist jede Spalte eine Variable und jede Zeile eine Beobachtung. Wenn wir zwei Dataframes zusammenführen möchten, sind darum zwei grundsätzlich unterschiedliche Situationen zu unterscheiden:

  • Es kommen weitere Beobachtungen hinzu, d.h. der Dataframe erhält neue Zeilen, er wird länger.

  • Es kommen weitere Variablen hinzu, d.h. der Dataframe erhält neue Spalten, er wird breiter.

Wir behandeln diese beiden Fälle nacheinander. Beide Fälle können recht komplex werden und es gibt mehrere Möglichkeiten, damit umzugehen. Wir beschränken uns auf zwei einfache und für die praktische Anwendung wichtige Situationen.

5.6.1 bind_rows

Mit dem Befehl bind_rows werden Beobachtungen (Zeilen) ergänzt. Der Dataframe wird dadurch länger. Das ist beispielsweise relevant, wenn ein Dataframe aktualisiert werden muss, weil neue Beobachtungen erhoben wurden, die unten an den bereits bestehenden Dataframe angehängt werden sollen. Achten Sie darauf, dass die Spaltennamen der beiden Dataframes exakt gleich sein müssen, wenn Sie sie zusammenführen, denn sonst werden die Spalten als unterschiedliche Variablen interpretiert.

Beispiel:

Die letzten sechs Zeilen des Dataframes worldbank sehen so aus:

tail(worldbank)
# A tibble: 6 × 7
  country  year      pop gdp_pc   co2 co2_pc   gdp
  <chr>   <dbl>    <dbl>  <dbl> <dbl>  <dbl> <dbl>
1 ZWE      2015 14399013  2647. 12.5   0.871  38.1
2 ZWE      2016 14600294  2797. 10.9   0.749  40.8
3 ZWE      2017 14812482  7045. 10.2   0.691 104. 
4 ZWE      2018 15034452  2614. 11.9   0.789  39.3
5 ZWE      2019 15271368  3211. 10.9   0.711  49.0
6 ZWE      2020 15526888  3511.  8.83  0.568  54.5

Wir möchten nun zwei Zeilen ergänzen, und zwar die Daten für Zimbabwe (zwe) von 2021 und 2022. Diese beiden Beobachtungen liegen in dem Dataframe wb_neu vor, der wie folgt aussieht:

wb_neu
# A tibble: 2 × 7
  country  year      pop gdp_pc   co2 co2_pc gdp  
  <chr>   <dbl>    <dbl>  <dbl> <dbl> <lgl>  <lgl>
1 ZWE      2021 15797210  3185.  9.64 NA     NA   
2 ZWE      2022 16069056  3560. 10.2  NA     NA   

Die neuen Beobachtungen werden nun durch bind_rows an den Dataframe worldbank angehängt. Der neue, längere Dataframe kann unter dem alten Namen (worldbank) oder unter einem neuen Namen abgespeichert werden (hier z.B. unter dem Namen wb_longer).

wb_longer <- bind_rows(worldbank, wb_neu)
tail(wb_longer,8)
# A tibble: 8 × 7
  country  year      pop gdp_pc   co2 co2_pc   gdp
  <chr>   <dbl>    <dbl>  <dbl> <dbl>  <dbl> <dbl>
1 ZWE      2015 14399013  2647. 12.5   0.871  38.1
2 ZWE      2016 14600294  2797. 10.9   0.749  40.8
3 ZWE      2017 14812482  7045. 10.2   0.691 104. 
4 ZWE      2018 15034452  2614. 11.9   0.789  39.3
5 ZWE      2019 15271368  3211. 10.9   0.711  49.0
6 ZWE      2020 15526888  3511.  8.83  0.568  54.5
7 ZWE      2021 15797210  3185.  9.64 NA      NA  
8 ZWE      2022 16069056  3560. 10.2  NA      NA  

5.6.2 left_join

Das Anspielen einer oder mehrerer neuer Variable an einen Dataframe ist fehleranfällig und nicht ganz leicht, weil sichergestellt sein muss, dass jede neue Beobachtung in der richtigen Zeile gespeichert wird. Oft hat der Dataframe mit den neuen Variablen eine andere Länge als der Dataframe, an den die Variablen angehängt werden sollen. Oder die Beobachtungen liegen in einer anderen Reihenfolge vor. Um ganz sicher zu sein, dass jede Beobachtung der richtigen Zeile zugewiesen wird, hilft ein Identifier (ID-Variable). Der Identifier dient dazu, die Beobachtungen in den beiden Dataframes selbst dann eindeutig zu identifizieren, wenn sie nicht in der gleichen Zeile stehen.

Das lässt sich am besten an einem einfachen Beispiel illustrieren.

Beispiel:

Die Daten in einem kleinen Dataframe D sehen so aus:

D <- data.frame(id=1:5, 
                x=c(3,4,3,1,1), 
                y=c(20,20,15,25,22))
print(D)
  id x  y
1  1 3 20
2  2 4 20
3  3 3 15
4  4 1 25
5  5 1 22

In einem weiteren Datframe D2 gibt Angaben zu den drei Variablen a, b und c, und zwar folgende:

D2 <- data.frame(id=c(2,1,5,4), 
                 a=c(1,4,3,2), 
                 b=c(5,5,4,5), 
                 c=c(9,9,8,8))
print(D2)
  id a b c
1  2 1 5 9
2  1 4 5 9
3  5 3 4 8
4  4 2 5 8

Die Variable id ist der Identifier (jeder beliebige Variablenname ist hier möglich). Er kommt in beiden Datensätzen vor und dient dazu, die Beobachtungen richtig zuzuordnen. Zum Beispiel sagt die erste Zeile von D2, dass die zugehörigen Werte von a, b und c in die zweite Zeile von D gehören. Offenbar enthält D2 die Beobachtungen in einer anderen Reihenfolge, und eine Beobachtung mit id=3 fehlt.

Der Dataframe D2 wird nun mit dem Befehl left_join an D angespielt. Es ergibt sich:

print(left_join(D, D2, by="id"))
  id x  y  a  b  c
1  1 3 20  4  5  9
2  2 4 20  1  5  9
3  3 3 15 NA NA NA
4  4 1 25  2  5  8
5  5 1 22  3  4  8

Der neue, breitere Dataframe kann unter dem alten Namen oder unter einem neuen Namen abgespeichert werden (hier im Beispiel wird er gar nicht gespeichert).

Noch ein Beispiel:

Der Identifier muss nicht unbedingt eindeutig sein. Es ist durchaus erlaubt, dass mehrere Beobachtungen den gleichen Wert des Identifiers haben, wie dies Beispiel zeigt. Der (fiktive) Dataframe X enthält Angaben zum nominalen (d.h. nicht inflationsbereinigten) Einkommen von drei Personen in drei Jahren.

X <- data.frame(jahr=rep(2020:2022,3),
                person=rep(1:3,each=3),
                nominal=c(1000,1200,1250,800,900,1000,2500,2400,2500))
X
  jahr person nominal
1 2020      1    1000
2 2021      1    1200
3 2022      1    1250
4 2020      2     800
5 2021      2     900
6 2022      2    1000
7 2020      3    2500
8 2021      3    2400
9 2022      3    2500

Um die nominalen Größen in reale Größen umzurechnen, verwendet man einen Preisindex (an dieser Stelle fragen wir nicht danach, wie ein solcher Index gebildet wird). Die realen Größen sind der Quotient aus dem nominalen Wert und dem Preisindex, der in dem Jahr galt.

Die (fiktiven) Werte des Preisindex sind im Dataframe preise enthalten:

preise <- data.frame(jahr=c(2020,2021,2022), 
                     index=c(1.00,1.05,1.11))
preise
  jahr index
1 2020  1.00
2 2021  1.05
3 2022  1.11

Mit Hilfe des Befehls left_join können wir den Index für jedes Jahr als neue Variable an den Dataframe X anfügen. Zum Verknüpfen dient die Spalte jahr, die in beiden Dataframes vorkommt und hier als Verknüpfungsvariable dient. Es ist also durchaus möglich, dass ein Wert an mehrere Beobachtungen angespielt wird.

X <- left_join(X, preise, by="jahr")
X
  jahr person nominal index
1 2020      1    1000  1.00
2 2021      1    1200  1.05
3 2022      1    1250  1.11
4 2020      2     800  1.00
5 2021      2     900  1.05
6 2022      2    1000  1.11
7 2020      3    2500  1.00
8 2021      3    2400  1.05
9 2022      3    2500  1.11

Wie der Befehlsname left_join schon nahelegt, gibt es noch andere Möglichkeiten, Dataframes zusammenzuführen, z.B. right_join oder full_join. Wir werden sie in diesem Kurs aber nicht brauchen. Wer sich für andere Arten des Anspielens interessiert, kann sich über die Hilfsfunktionen und Cheatsheets informieren.

Drittes Beispiel: Worldbank Lebenserwartungsdaten

In diesem dritten Beispiel wird gezeigt, wie man weitere Variablen an den worldbank-Datensatz anspielt. Hier ist die Schwierigkeit, dass es zwei Identifier gibt, nämlich das Land (country) und das Jahr (year). Wir nehmen als Ausgangspunkt den Dataframe worldbank, dessen ersten Zeilen so aussehen:

head(worldbank)
# A tibble: 6 × 7
  country  year   pop gdp_pc   co2 co2_pc   gdp
  <chr>   <dbl> <dbl>  <dbl> <dbl>  <dbl> <dbl>
1 ABW      1990 62753 21733. 0.189   3.02  1.36
2 ABW      1991 65896 23100. 0.215   3.27  1.52
3 ABW      1992 69005 23889. 0.239   3.46  1.65
4 ABW      1993 73685 24576. 0.241   3.27  1.81
5 ABW      1994 77595 25791. 0.266   3.43  2.00
6 ABW      1995 79805 26255. 0.295   3.69  2.10

An diesen Dataframe soll nun ein zweiter Dataframe mit Informationen zur Lebenserwartung angespielt werden. Wir laden diesen zweiten Datensatz (der auch im Learnweb zu finden ist).

wb_lifeexp <- read_csv("../data/wb_lifeexp.csv")

Die ersten Zeilen sehen so aus:

head(wb_lifeexp)
# A tibble: 6 × 4
  country  year lifeexpF lifeexpM
  <chr>   <dbl>    <dbl>    <dbl>
1 AFG      2022     66.2     59.8
2 AFG      2021     65.3     58.9
3 AFG      2020     65.4     59.9
4 AFG      2019     66.7     60.6
5 AFG      2018     66.5     59.9
6 AFG      2017     66.1     60.1

Der Datensatz enthält die beiden Variablen lifexpF und lifexpM (Lebenserwartung der Frauen und Männer). Der Dataframe wb_lifeexp wird nun an den Dataframe worldbank angefügt. Es gibt zwei Identifier-Variablen, nämlich country und year. Sie werden durch die Funktion c zusammengefasst und als Argument by übergeben. Der neue, breitere Dataframe wird unter dem Namen wb_wider gespeichert.

wb_wider <- left_join(worldbank, wb_lifeexp,
                      by=c("country","year"))

Dieser neue Dataframe hat die Variablen (Spalten)

names(wb_wider)
[1] "country"  "year"     "pop"      "gdp_pc"   "co2"      "co2_pc"   "gdp"     
[8] "lifeexpF" "lifeexpM"

Die ersten Zeilen des verbreiterten Dataframes lauten:

head(wb_wider)
# A tibble: 6 × 9
  country  year   pop gdp_pc   co2 co2_pc   gdp lifeexpF lifeexpM
  <chr>   <dbl> <dbl>  <dbl> <dbl>  <dbl> <dbl>    <dbl>    <dbl>
1 ABW      1990 62753 21733. 0.189   3.02  1.36     76.2     70.1
2 ABW      1991 65896 23100. 0.215   3.27  1.52     76.1     70.1
3 ABW      1992 69005 23889. 0.239   3.46  1.65     76.2     70.2
4 ABW      1993 73685 24576. 0.241   3.27  1.81     76.2     70.2
5 ABW      1994 77595 25791. 0.266   3.43  2.00     76.3     70.3
6 ABW      1995 79805 26255. 0.295   3.69  2.10     76.4     70.3