10 Tidyverse Funktionen, die den Tag retten können

In diesem Beitrag stellen wir 10 Tidyverse Funktionen vor, die von Beginnern häufig übersehen werden, sich im passenden Kontext jedoch als sehr nützlich erweisen. Dabei schildern wir zunächst ein Problem, welches uns in ähnlicher Form in der Praxis begegnet und erklären anschließend, wie uns das Tidyverse dabei hilft, dieses Problem zu lösen.

10 Tidyverse Funktionen, die den Tag retten können

In diesem Beitrag stellen wir 10 Tidyverse Funktionen vor, die von Beginnern häufig übersehen werden, sich im passenden Kontext jedoch als sehr nützlich erweisen. Dabei schildern wir stets zunächst ein Problem, welches uns in ähnlicher Form in der Praxis begegnet und erklären anschließend, wie uns das Tidyverse dabei hilft, dieses Problem zu lösen.

Für die Aufbereitung und Analyse von Daten in R haben sich die Tidyverse Pakete in den letzten Jahren als Branchenstandard etabliert. Wir bei eoda nutzen viele Funktionen aus dem Tidyverse, um unsere tägliche Arbeit effizienter zu gestalten.

Inhalt

Wir beginnen erwartungsgemäß mit dem Laden der notwendigen Bibliotheken:

library(tidyverse)

1. crossing

Problem:

Für das erste Beispiel betrachten wir eine statistische Anwendung. Ausgehend von zwei Vektoren numerischer Mittelwerte und Standardabweichungen sollen alle Kombination der auftretenden Werte in einem Data Frame gesammelt werden.

Lösung:

Die crossing() Funktion aus dem tidyr package erfüllt genau diesen Zweck. Sie nimmt eine beliebige Anzahl an Vektoren als Input und bildet alle möglichen Kombinationen der auftretenden Werte:

means <- c(-1, 0, 1)
standard_deviations <- c(0.1, 0.5, 1)

mean_sd_combinations <- crossing(means, standard_deviations)
mean_sd_combinationsCode-Sprache: R (r)
meansstandard_deviations
-10.1
-10.5
-11.0
00.1
00.5
01.0
10.1
10.5
11.0

Bonus:

crossing() kann nicht nur Vektoren, sondern auch Data Frames als Input nehmen. In diesem Fall werden alle Kombinationen der Zeilen gebildet.

Das ist besonders nützlich, wenn eines der Data Frames “globale” Informationen bereit stellt (im folgenden Beispiel population_data), welche für alle Beobachtungen gültig sind und das zweite Data Frame “lokale” Informationen, die sich zwischen Beobachtungen oder Gruppen unterscheiden (im Beispiel group_data).

population_data <- tibble(
  global_feature_1 = "e",
  global_feature_2 = 5,
)

population_dataCode-Sprache: R (r)
global_feature_1global_feature_2
e5
group_data <- tibble(
  group = 1:3,
  local_feature_1 = c(2, 5, 3),
  local_feature_2 = c(TRUE, FALSE, FALSE)
)

group_dataCode-Sprache: R (r)
grouplocal_feature_1local_feature_2
12TRUE
25FALSE
33FALSE

Als Ergebnis von crossing() erhalten wir ein einziges Data Frame, in dem jede Zeile sowohl die globalen als auch die gruppenspezifischen Werte enthält:

crossing(population_data, group_data)
global_feature_1global_feature_2grouplocal_feature_1local_feature_2
e512TRUE
e525FALSE
e533FALSE

2. rowwise

Problem:

Wir bleiben bei dem Anwendungsbeispiel aus Abschnitt 1. Für jede der Mittelwert-Standardabweichung Kombinationen sollen fünf zufällige Werte (samples) einer Standardnormalverteilung gezogen und dem Data Frame in einer neuen Spalte hinzugefügt werden.

Folglich müssen wir hier auf Zeilenebene agieren: Jede Zeile des Data Frames bildet eine zusammengehörende Einheit. Die neu generierten Werte der ersten Zeile basieren ausschließlich auf den restlichen Werten der ersten Zeile.

Eine weitere Besonderheit ist, dass wir nicht nur einen einzelnen, sondern mehrere Einträge pro Zelle hinzufügen. Damit dies mit der Struktur eines Data Frames kompatibel ist, müssen diese in einer Liste zusammengefasst werden. Die neue Spalte ist folglich eine list column – eine Spalte bestehend aus Listen.

Lösung:

Eine Möglichkeit besteht in der Nutzung der map() Familie aus dem purrr Paket. Die Spalten means und standard_­deviations, auf welche die rnorm() Funktion angewandt wird, werden durch die Platzhalter .x und .y referenziert:

random_samples_map <- mean_sd_combinations |> mutate(
  samples = map2(means, standard_deviations, ~ rnorm(n = 5, mean = .x, sd = .y))
)

random_samples_map |> head()
<em>## # A tibble: 6 × 3</em>
<em>##   means standard_deviations samples  </em>
<em>##   <dbl>               <dbl> <list>   </em>
<em>## 1    -1                 0.1 <dbl [5]></em>
<em>## 2    -1                 0.5 <dbl [5]></em>
<em>## 3    -1                 1   <dbl [5]></em>
<em>## 4     0                 0.1 <dbl [5]></em>
<em>## 5     0                 0.5 <dbl [5]></em>
<em>## 6     0                 1   <dbl [5]></em>Code-Sprache: R (r)

Jeder Eintrag der neuen samples Spalte besteht aus einer Liste mit fünf gezogenen Werten aus einer Standardnormalverteilung:

random_samples_map$samples[[1]]
<em>## [1] -1.0416796 -0.9907691 -0.9249944 -0.8859866 -1.0676741</em>Code-Sprache: R (r)

Für viele Anwendungsfälle bietet die rowwise() Funktion aus dem dplyr Paket eine anwendungsfreundlichere Alternative. Die Spaltennamen means und standard­­_deviations können hierbei direkt im Aufruf der rnorm() Funktion ohne die Verwendung von Platzhaltern genutzt werden.

Da die neue Spalte aus Listen besteht, muss der Aufruf von rnorm() innerhalb von list() erfolgen:

random_samples_map <- mean_sd_combinations |>
  dplyr::rowwise() |>
  mutate(samples = list(rnorm(n = 5, mean = means, sd = standard_deviations)))

random_samples_map$samples[[1]]
<em>## [1] -0.9437694 -0.9311953 -1.0259749 -1.0115392 -1.0614477</em>Code-Sprache: R (r)

Bonus:

Bei der Arbeit mit ‘list columns’ kann die dplyr Funktion nest_by() sehr nützlich sein, welche anders als tidyr::nest() zeilenweise Gruppen bildet.

Als Beispiel bilden wir eine eigene Gruppe für jeden cyl (cylinder) Wert aus dem mtcars Datensatz. Alle verbleibenden mtcars Spalten werden in einer neuen Spalte bestehend aus Data Frames gebündelt.

mtcars |> nest_by(cyl)
<em>## # A tibble: 3 × 2</em>
<em>## # Rowwise:  cyl</em>
<em>##     cyl                data</em>
<em>##   <dbl> <list<tibble[,10]>></em>
<em>## 1     4           [11 × 10]</em>
<em>## 2     6            [7 × 10]</em>
<em>## 3     8           [14 × 10]</em>Code-Sprache: R (r)

Hiervon ausgehend können wir eine neue Spalte mit linearen Modellen von mpg (miles per gallon) in Abhängigkeit von hp (horse power) hinzufügen.

In einem letzten Schritt extrahieren wir aus diesem die Steigungskoeffizienten, also je eine Zahl pro Zylinderwert. Das Ergebnis ist ein einziges Data Frame, in dem die ursprünglichen Daten, die Modellobjekte und die Steigungskoeffzienten enthalten sind:

mtcars |>
  nest_by(cyl) |>
  mutate(model = list(lm(mpg ~ hp, data = data))) |>
  mutate(slope = coef(model)[2])
<em>## # A tibble: 3 × 4</em>
<em>## # Rowwise:  cyl</em>
<em>##     cyl                data model     slope</em>
<em>##   <dbl> <list<tibble[,10]>> <list>    <dbl></em>
<em>## 1     4           [11 × 10] <lm>   -0.113  </em>
<em>## 2     6            [7 × 10] <lm>   -0.00761</em>
<em>## 3     8           [14 × 10] <lm>   -0.0142</em>Code-Sprache: R (r)

3. pluck

Problem:

Aus der genesteten Liste l soll der String "c" der untersten Ebene ausgewählt werden, d.h. der dritte Wert des Elements b im ersten Listenelement von a. Insgesamt müssen wir also einen Wert aus dem vierten Level der Liste extrahieren.

l <- list(a = list(c(1, 2, list(b = c("a", "b", "c")))))
l
<em>## $a</em>
<em>## $a[[1]]</em>
<em>## $a[[1]][[1]]</em>
<em>## [1] 1</em>
<em>## </em>
<em>## $a[[1]][[2]]</em>
<em>## [1] 2</em>
<em>## </em>
<em>## $a[[1]]$b</em>
<em>## [1] "a" "b" "c"</em>Code-Sprache: R (r)

Lösung:

Dies ist natürlich auch ohne zusätzliche Pakete möglich, jedoch vergleichsweise schwierig zu lesen:

l$a[[1]]$b[3]
<em>## [1] "c"</em>Code-Sprache: R (r)

pluck() aus dem purrr Package löst die Aufgabe hingegen sehr elegant und leicht verständlich. Der Name bzw. Index jedes Levels der Liste wird einfach sequentiell als Argument der Funktion übergeben:

l |> purrr::pluck("a", 1, "b", 3)
<em>## [1] "c"</em>Code-Sprache: R (r)

4. rownames_to_column & rowid_to_column

Problem 1:

Die Zeilennamen eines Datensatzes sollen in die erste Spalte geschrieben werden. Als Beispiel wählen wir den bekannten mtcars Datensatz. In diesem beschreiben die Zeilennamen das Modell des Autos, welche einer neuen model Spalte hinzugefügt werden sollen:

mtcars |> head()
mpgcyldisphpdratwtqsecvsamgearcarb
Mazda RX421.061601103.902.62016.460144
Mazda RX4 Wag21.061601103.902.87517.020144
Datsun 71022.84108933.852.32018.611141
Hornet 4 Drive21.462581103.083.21519.441031
Hornet Sportabout18.783601753.153.44017.020032
Valiant18.162251052.763.46020.221031

Lösung:

Das tibble Paket stellt die Funktion rownames_to_column() bereit. Dem Parameter var kann dabei ein String mit dem gewünschten neuen Spaltennamen übergeben werden. Die neue Spalte wird automatisch an die erste Position des Datensatzes gesetzt.

mtcars_model <- mtcars |> tibble::rownames_to_column(var = "model")
mtcars_model |> head()Code-Sprache: R (r)
modelmpgcyldisphpdratwtqsecvsamgearcarb
Mazda RX421.061601103.902.62016.460144
Mazda RX4 Wag21.061601103.902.87517.020144
Datsun 71022.84108933.852.32018.611141
Hornet 4 Drive21.462581103.083.21519.441031
Hornet Sportabout18.783601753.153.44017.020032
Valiant18.162251052.763.46020.221031

Problem 2:

Im zweiten Schritt soll eine index Spalte hinzugefügt werden, die jede Beobachtung eindeutig durch eine ID identifiziert. Dafür nummerieren wir einfach die Zeilen durch und schreiben die Zeilennummern in die neue Spalte.

Lösung:

Eine naheliegende Lösung erstellt mittels mutate() in Kombination mit nrow() oder dplyr::row_number() eine neue Spalte und setzt diese mit relocate() an die erste Position:

mtcars_model |>
  <em># alternativ: mutate(index = row_number()) |></em>
  mutate(index = 1:nrow(mtcars)) |>
  relocate(index) |>
  head()Code-Sprache: R (r)
indexmodelmpgcyldisphpdratwtqsecvsamgearcarb
1Mazda RX421.061601103.902.62016.460144
2Mazda RX4 Wag21.061601103.902.87517.020144
3Datsun 71022.84108933.852.32018.611141
4Hornet 4 Drive21.462581103.083.21519.441031
5Hornet Sportabout18.783601753.153.44017.020032
6Valiant18.162251052.763.46020.221031

Erneut liefert das tibble Paket eine kompaktere Lösung. rowid_to_column() erledigt unsere Aufgabe in einem Schritt. Genau wie zuvor kann durch das var Argument der Name der neuen Spalte bestimmt werden:

mtcars_model |>
  tibble::rowid_to_column(var = "index") |>
  head()Code-Sprache: R (r)
indexmodelmpgcyldisphpdratwtqsecvsamgearcarb
1Mazda RX421.061601103.902.62016.460144
2Mazda RX4 Wag21.061601103.902.87517.020144
3Datsun 71022.84108933.852.32018.611141
4Hornet 4 Drive21.462581103.083.21519.441031
5Hornet Sportabout18.783601753.153.44017.020032
6Valiant18.162251052.763.46020.221031

5. parse_number

Problem:

Bei der täglichen Arbeit mit Daten begegnen wir häufig Datensätzen, die vor der Weiterverwendung noch bereinigt werden müssen.

Der nachfolgende Datensatz enthält eine Spalte mit Produkten und eine weitere Spalte mit zugehörigen Preisen. Die Preise sind hierbei jedoch in einen String ohne feste Struktur eingebunden:

data_prices <- tibble(
  product = 1:3,
  costs = c("$10 -> expensive", "cheap: $2.50", "free, $0 !!")
)

data_pricesCode-Sprache: R (r)
productcosts
1$10 -> expensive
2cheap: $2.50
3free, $0 !!

Die Aufgabe besteht nun darin, aus den Strings die numerischen Preises für jedes Produkt zu separieren.

Lösung:

Eine funktionierende, aber häufig umständliche Lösung besteht in der Nutzung regulärer Ausdrücke. In diesem Beispiel suchen wir nach dem ersten Match mindestens einer Ziffer gefolgt von optionalem Punkt und Nachkommastellen. Ein Nachteil dieses Ansatzes ist, dass die Ergebnisspalte immer noch vom Typ character ist:

one_or_more_digits <- "\\d+"
optional_dot <- "\\.?"
optional_digits <- "\\d*"

data_prices |> mutate(price = stringr::str_extract(
  string = costs,
  pattern = paste0(one_or_more_digits, optional_dot, optional_digits)
))
<em>## # A tibble: 3 × 3</em>
<em>##   product costs            price</em>
<em>##     <int> <chr>            <chr></em>
<em>## 1       1 $10 -> expensive 10   </em>
<em>## 2       2 cheap: $2.50     2.50 </em>
<em>## 3       3 free, $0 !!      0</em>Code-Sprache: R (r)

Es gibt jedoch einen komfortableren Weg: Das readr package, welches üblicherweise zum Datenimport verwendet wird, stellt die Hilfsfunktion parse_number() bereit. Diese scannt einen Vektor mit Strings nach der ersten Zahl und extrahiert diese aus ihrem Kontext. Mögliche Nachkommastellen werden automatisch berücksichtigt.

Die neue price Spalte gehört in diesem Fall direkt dem Datentyp double an:

data_prices |> mutate(price = parse_number(costs))Code-Sprache: R (r)
productcostsprice
1$10 -> expensive10.0
2cheap: $2.502.5
3free, $0 !!0.0

6. fct_lump_*

Problem:

In diesem Beispiel arbeiten wir mit dem babynames Datensatz aus dem gleichnamigen R Paket, welches die beliebtesten Babynamen in den USA über mehrere Jahrzehnte auflistet. Die Spalte n gibt die absolute Häufigkeit des Namens innerhalb eines Jahres an:

babynames::babynames |> head()Code-Sprache: R (r)
yearsexnamenprop
1880FMary70650.0723836
1880FAnna26040.0266790
1880FEmma20030.0205215
1880FElizabeth19390.0198658
1880FMinnie17460.0178884
1880FMargaret15780.0161672

Wir interessieren uns dafür, auf welche Buchstaben Mädchennamen im Jahr 2000 am häufigsten enden:

names_2000 <- babynames::babynames |> filter(year == 2000)

last_letters_females <- names_2000 |>
  mutate(last_letter = stringr::str_sub(name, start = -1, end = -1)) |>
  filter(sex == "F") |>
  count(last_letter, wt = n, name = "num_babies", sort = TRUE)

last_letters_females |> head(10)Code-Sprache: R (r)
last_letternum_babies
a675963
e318399
n248450
y246324
h117324
l56623
r50769
i42591
s32603
t9796

Einige Buchstaben stehen erwartungsgemäß wesentlich häufiger an letzter Stelle als andere. Zu Übersichtszwecken sollen alle Buchstaben mit geringer Häufigkeit zu einer gemeinsamen Other Kategorie zusammengefasst werden.

Lösung:

Das forcats Paket hilft uns hierbei. Die fct_lump_*() Familie aggregiert seltenere Werte einer Faktor- (oder hier Character-) Variablen nach verschiedenen Kriterien:

  • fct_lump_n() behält die n häufigsten Werte und fasst alle anderen Werte zu einer neuen Kategorie zusammen.
  • fct_lump_min() fasst alle Werte zusammen, welche seltener als eine gegebene absolute Häufigkeit vorkommen.
  • fct_lump_prop() fasst alle Werte zusammen, welche seltener als eine gegebene relative Häufigkeit (Anteil zwischen 0 und 1) auftreten.
  • fct_lump_lowfreq() fasst automatisch die seltensten Werte zusammen, sodass die aggregierte Other Kategorie immer noch die geringste Häufigkeit unter den neuen Kategorien besitzt.

In unserem Beispiel nutzen wir fct_lump_n() und behalten die häufigsten fünf letzten Buchstaben bei:

last_letters_females_lumped <- last_letters_females |>
  mutate(last_letter = factor(last_letter) |> fct_lump_n(
    n = 5, w = num_babies, other_level = "Other"
  )) |>
  count(
    last_letter,
    wt = num_babies, name = "num_babies", sort = TRUE
  )

last_letters_females_lumpedCode-Sprache: R (r)
last_letternum_babies
a675963
e318399
n248450
y246324
Other208650
h117324

Der Parameter w (für weight) kann dabei optional eine Spalte angeben, deren Werte zur Bestimmung der Häufigkeit aufsummiert werden. Dies ist dann nützlich, wenn wie im obigen Beispiel jeder Buchstabe nur in einer Zeile vorkommt und die zugehörigen Häufigkeiten bereits berechnet wurden. Der Parameter wird nicht benötigt, falls die Häufigkeiten noch nicht berechnet wurden und jeder Buchstabe n Mal in der last_letter Spalte dupliziert wäre.

7. fct_reorder + geom_col

Problem:

Wir bleiben auch für dieses Beispiel bei dem babynames Datensatz und visualisieren die Anzahl der sechs häufigsten Mädchennamen in einem Balkendiagramm mit geom_col():

plot_color <- "#8bac37"

top_names_females <- names_2000 |>
  filter(sex == "F") |>
  slice_max(n, n = 6)

top_names_females |>
  ggplot(aes(n, name)) +
  geom_col(fill = plot_color) +
  labs(
    title = "Die 6 häufigsten Babynamen für Mädchen im Jahr 2000",
    x = "Häufigkeit", y = NULL,
  ) +
  theme_light() +
  theme(plot.title = element_text(hjust = 0.5))Code-Sprache: R (r)

Die Namen sind entlang der y-Achse nicht nach ihrer Häufigkeit geordnet!

Lösung:

Um dies zu erreichen, sortieren wir die name Spalte gemäß ihrer Häufigkeit (der Spalte n) neu.

Dieser Fall tritt in der Praxis so häufig auf, dass ich geom_col() fast ausschließlich in Kombination mit fct_reorder() aus dem forcats Paket verwende:

top_names_females |>
  mutate(name = fct_reorder(name, n)) |>
  ggplot(aes(n, name)) +
  geom_col(fill = plot_color) +
  labs(
    title = "Die 6 häufigsten Babynamen für Mädchen im Jahr 2000",
    x = "Häufigkeit", y = NULL,
  ) +
  theme_light() +
  theme(plot.title = element_text(hjust = 0.5))Code-Sprache: R (r)

Bonus:

Das obige Vorgehen funktioniert nicht mehr so einfach, wenn für jeden Wert einer weiteren Faktorvariablen ein eigenes Balkendiagramm in absteigender Häufigkeit geplottet werden soll. Als Beispiel betrachten wir nun zusätzlich die häufigsten Jungennamen:

top_names <- names_2000 |>
  group_by(sex) |>
  slice_max(n, n = 6)Code-Sprache: R (r)

Mit fct_reorder() werden die Balken in jedem Subplot stets gemäß ihrer Häufigkeit im gesamten Datensatz (und nicht nur innerhalb jedes Wertes der sex Variable) angeordnet.

Das tidytext Paket, welches primär zur Analyse von Textdaten verwendet wird, rettet uns an dieser Stelle.

Die Hilfsfunktionen reorder­_within() und scale_y_­reordered() erfüllen exakt den gewünschten Zweck und sortieren die Werte der Faktorvariablen innerhalb jedes Subplots:

top_names |>
  mutate(name = tidytext::reorder_within(name, by = n, within = sex)) |>
  ggplot(aes(n, name)) +
  geom_col(fill = plot_color) +
  labs(
    title = "Die 6 häufigsten Babynamen für Mädchen und Jungs im Jahr 2000",
    x = "Häufigkeit", y = NULL,
  ) +
  facet_wrap(facets = vars(sex), scales = "free_y") +
  tidytext::scale_y_reordered() +
  theme_light() +
  theme(plot.title = element_text(hjust = 0.5))Code-Sprache: R (r)

8. separate & separate_rows

Problem 1: Der folgende Datensatz soll die Ergebnisse verschiedener Länderspiele abbilden:

data_games <- tibble(
  country = c("Germany", "France", "Spain"),
  game = c("England - win", "Brazil - loss", "Portugal - tie")
)

data_gamesCode-Sprache: R (r)
countrygame
GermanyEngland – win
FranceBrazil – loss
SpainPortugal – tie

Die game Spalte umfasst jedoch zwei unterschiedliche Arten von Informationen: den Gegner sowie das Ergebnis.

Lösung:

Um das Data Frame tidy zu machen, splitten wir die game Spalte mit der separate() Funktion aus dem tidyr Paket in zwei Spalten auf:

data_games |> separate(col = game, into = c("opponent", "result"))Code-Sprache: R (r)
countryopponentresult
GermanyEnglandwin
FranceBrazilloss
SpainPortugaltie

Problem 2:

Ein ähnliches Problem tritt auf, wenn eine Spalte in jeder Zeile zwei Informationen des gleichen Typs enthält. Die opponent Spalte umfasst nun ausschließlich gegnerische Teams, jedoch gleich mehrere pro Zeile:

data_opponents <- tibble(
  country = c("Germany", "France", "Spain"),
  opponent = c("England, Switzerland", "Brazil, Denmark", "Portugal, Argentina")
)

data_opponentsCode-Sprache: R (r)
countryopponent
GermanyEngland, Switzerland
FranceBrazil, Denmark
SpainPortugal, Argentina

Der gewünschte Output enthält in diesem Fall nicht mehr Spalten, sondern mehr Zeilen, je eine pro Gegner.

Lösung:

separate_rows() splittet jede Zeile der opponent Spalte in mehrere Zeilen, die korrespondierenden country Werte werden entsprechend dupliziert:

data_opponents |> separate_rows(opponent)Code-Sprache: R (r)
countryopponent
GermanyEngland
GermanySwitzerland
FranceBrazil
FranceDenmark
SpainPortugal
SpainArgentina

9. str_flatten_comma

Problem:

Ein Vektor aus Strings soll zu einem einzigen String zusammengefasst werden. Alle Einträge werden dabei durch ein Komma voneinander getrennt, nur die beiden letzten sollen durch das Verbindungswort “and” verbunden werden.

animals <- c("cat", "dog", "mouse", "elephant")
animals
<em>## [1] "cat"      "dog"      "mouse"    "elephant"</em>Code-Sprache: R (r)

Lösung:

Ohne das stringr Package sind zwei Aufrufe von paste() erforderlich:

  1. Zunächst werden alle Einträge bis auf den letzten durch ein Komma zu einem einzigen String verbunden.
  2. Anschließend wird das Ergebnis aus Schritt 1 mit dem letzten Vektoreintrag verknüpft.
paste(animals[-1], collapse = ", ") |> paste(animals[length(animals)], sep = " and ")
<em>## [1] "dog, mouse, elephant and elephant"</em>Code-Sprache: R (r)

Das stringr package stellt mit str_flatten_comma() hierfür eine eigene Funktion mit dem sehr nützlichen last Parameter bereit:

str_flatten_comma(animals, last = " and ")
<em>## [1] "cat, dog, mouse and elephant"</em>Code-Sprache: R (r)

10. arrange + distinct

Problem:

Das finale Beispiel ist durch die Arbeit an einem aktuellen Projekt von eoda inspiriert. Es liegt ein Datensatz mit zwei Spalten vor, die erste Spalte (group) beinhaltet einen Indikator für die Gruppenzugehörigkeit jeder Beobachtung. Innerhalb jeder Gruppe soll lediglich eine einzige Zeile beibehalten werden: Diejenige mit dem höchsten numerischen Wert der zweiten (value) Spalte:

set.seed(123)

data_group_value <- tibble(
  group = c(1, 3, 2, 1, 1, 2, 3, 1),
  value = sample(1:100, size = 8, replace = FALSE)
)

data_group_valueCode-Sprache: R (r)
groupvalue
131
379
251
114
167
242
350
143

Lösung:

Eine mögliche Herangehensweise ist das Zusammenspiel von group_by() und slice_max():

data_group_value |>
  group_by(group) |>
  slice_max(value, n = 1)Code-Sprache: R (r)
groupvalue
167
251
379

Der Nachteil hierbei ist, dass für große Datensätze gegebenenfalls sehr viele Gruppen gebildet werden, was die Effizienz der Berechnung mindert. Zudem führt dieser Ansatz bei Duplikaten nicht zu dem gewünschten Ergebnis, da slice_max() alle Beobachtungen mit dem maximalen Wert auswählt:

data_group_value_duplicates <- data_group_value |>
  mutate(
    value = case_when(
      group == 1 ~ 20L,
      TRUE ~ value
    )
  )

data_group_value_duplicates |>
  group_by(group) |>
  slice_max(value, n = 1)Code-Sprache: R (r)
groupvalue
120
120
120
120
251
379

In diesem Fall wäre also ein zusätzlicher Aufruf von slice(1) erforderlich, um wirklich nur eine einzige Zeile pro Gruppe zu behalten.

Eine effizientere Lösung greift auf die dplyr Kombination von arrange() und distinct() zurück. Zuerst werden alle Zeilen innerhalb jeder Gruppe absteigend nach ihren value Werten sortiert. Der auszuwählende maximale Wert steht demnach stets an erster Stelle innerhalb jeder Gruppe.

Im zweiten Schritt ist ein Aufruf von distinct() ausreichend, da diese Funktion bei Duplikaten immer den zuerst auftretenden Wert beibehält und alle anderen aus der Spalte entfernt:

data_group_value_duplicates |>
  arrange(group, desc(value)) |>
  distinct(group, .keep_all = TRUE)Code-Sprache: R (r)
groupvalue
120
251
379

Fazit

In diesem Beitrag haben wir die Nützlichkeit ausgewählter Tidyverse-Funktionen anhand verschiedener Beispiele veranschaulicht.

Manche Problemstellungen wären auf auch anderem Wege zu lösen
– aber nur mit größeren Aufwand

Python, R & Shiny

Unsere Trainings ebnen Ihnen den Weg für Ihre nächsten Schritte. Machine Learning, Datenvisualisierung, Zeitreihenanalysen oder Shiny:
Finden Sie bei uns den richtigen Kurs für Ihre Anforderungen.

Nichts mehr verpassen – Jetzt den eoda-Newsletter abonnieren!