Komplexe CSV-Dateien einlesen

Datum:
tags: R
GitHub: bei GitHub bearbeiten

Natürlich gibt es einfachere Dinge, als Daten in R einzulesen. Und trotzdem ist es zumindest seit der Einführung des tidyverse und den damit propagierten tidy data principles sehr viel einfacher geworden saubere Daten zu laden. Leider hat nicht jeder Sensor-Hersteller das Paper von Hadley Wickem gelesen.

Dieser Tage ist eine sehr spannende Aufgabe auf meinem Schreibtisch gelandet. Es klang zunächst so einfach: Wir haben da eine Maschine. Die misst etwas und schreibt ihre Ergebnisse in eine Textdatei. Diese müsste eingelesen und weiterverarbeitet. Ich dachte, dass sich das relativ leicht mit read_delim() lösen lassen können müsste. Dachte ich…

Das Problem

Die Sache hatte natürlich einen Haken. Den Vorteil, dass die Maschine ihre Messwerte in eine CSV-Datei (und kein proprietäres Format) schrieb, machte sie damit zunichte, dass sie in einer Datei mehrere Header einfügte. “Wie meinen?”, denkt ihr vielleicht jetzt.

Stellen wir uns dazu vor, dass mit der Maschine die Inhaltsstoffe verschiedener Burger gemessen werden sollen. Man zielt also auf einen Burger und bekommt als Ergebnis die Anteile an Bun, Patty, Salat, etc. die in diesem Burger verwurstelt wurden. Das Problem ist nun, dass die Maschine beispielsweise bei einem vegetarischen Burger den Eintrag für Patty nicht einfach leer lässt sondern gar nicht erst notiert. Je nach Anzahl der Zutaten im Burger hat die CSV also unterschiedlich viele Spalten.

Immernoch unklar? Dann werft doch mal einen Blick auf das folgende Beispiel.

ID Anteil Bun Anteil Patty Anteil Salat Anteil Tomate
Hamburger .25 .25 .25 .25
Cheeseburger .20 .20 .25 .35
.
ID Anteil Bun Anteil Salat Anteil Tomate
Veggi-Burger .20 .20 .25

Beispiel des Burger-CSVs


Unschön, nicht? Und obwohl ich in den vergangen Jahren wirklich einige R-Tricks gelernt habe, hat mich das einige Arbeit gekostet.

Die Lösung

Meine Lösung ist sicher nicht die beste, aber funktioniert und hilft vielleicht der ein oder anderen, die ein ähnliches Problem hat.

Zunächst speichere ich mir den String, der den Start eines neuen Headers identifiziert (erster Spaltenname im Datensatz) und lese das gesamte File mittels readLines() ein. Das setzt natürlich voraus, dass zumindest die erste Spalte immer gemessen wird.

# load packages -----------------------------------------------------------
require("tidyverse")

# define header identification --------------------------------------------
header_id <- "ID"

# read raw data -----------------------------------------------------------
RAW <- readLines(file.choose())

(Ich verwende file.chose(), um die Benutzung für den Endanwender so komfortabel wie möglich zu halten)

Dann baue ich einen Datensatz, der genausoviele Zeilen wie das originale CSV-File hat und ermittle (mit header_id) welche davon Header-Zeilen sind. Mittels split() fülle ich die restlichen Zeilen mit der dazugehörigen Header-Nummer auf.

# find lines to split at --------------------------------------------------
header_lines <- which(substr(RAW, 1, nchar(header_id)) == header_id)

# create a list of all datasets within the CSV file -----------------------
DATASETS <- tibble(
  Row = 1:length(RAW),
  Data = RAW,
  Split = ifelse(Row %in% header_lines, header_lines, NA)
) %>%
  fill(Split) %>% 
  split(f = .$Split)

Schließlich schleife ich einmal über die jeweiligen Headernummern und picke mir aus den Rohdaten alle Informationen zusammen.

# extract relevant data ---------------------------------------------------
BURGERS <- tibble(
  ID = character(),
  Bun = numeric(), 
  Patty = numeric(),
  Salat = numeric(),
  Tomate = numeric()
)

for(i in 1:length(DATASETS)){
  # print loop ............................................................
  print(i)
  
  # extract data ..........................................................
  tmp_str <- DATASETS[[i]] %>% select(Data) %>% unlist()
  tmp_data <- read_delim(paste(tmp_str, collapse="\n"), delim = ";", na = "<LOD") %>% 
    select(
      ID
      matches(names(BURGERS))
      ) %>%
    filter(!is.na(ID))
  
  # append to results .....................................................
  BURGERS <- BURGERS %>% bind_rows(tmp_data) 
}

Et voilà, wir haben einen Datensatz mit dem man arbeiten kann :)


Das könnte dich auch interessieren ...

  • TidyTuesday Submission CW26
    Letzte Woche habe ich zum ersten Mal an einem TidyTuesday teilgenommen und dabei einiges über UFO-Sichtungen gelernt. Der TidyTuesday ist ein von der R4DS Community (sprich: &ldquo;R for Data Science&...

Zeit für deinen Senf ...