Komplexe CSV-Dateien einlesen

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 werden. 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 :)


#Script #Data

Reply to this post
I used to have a comments box on this blog, but found that maintaining another system and dealing with the cookies is cumbersome. So, if you have any thoughts on what I wrote in this article, please write me an email and we can have a conversation about it.
reply via email ✉️

Enjoyed reading this?
I put a lot of work into maintaining this blog and I really enjoy the interactions I get with you, dear readers. I you liked what you just read and want to do me a little favor, please tip me a cup of coffee as it's the fuel that keeps me going:
buy me a coffee ☕️