library(tidyverse)
#library(stringr) # Strings verarbeiten
library(here) # liest aktuelles Verzeichnis aus
library(visdat)
library(janitor)
library(tictoc)Challenge 02: Daten einlesen und Aufbereiten – Lösung
“Eine Hackathon-Variante zur Evaluation der Klickdaten des KI-Tools ‘HaNS’”
1 Daten einlesen
1.1 Setup
1.2 Lösungen
1.2.1 Importieren
Wir definieren die Liste der zu importierenden CSV-Dateien:
datafiles_list <- list.files(path = paste0(here(),"/", "data/SoSe25"),
pattern = "csv$",
recursive = TRUE,
full.names = TRUE)
datafiles_listWie viele Tage, d.h. Dateinamen, sind das?
length(datafiles_list)Und dann importieren wir die CSV-Dateien und “binden” sie “zeilenweise” in einen großen, Gesamt-Data-Frame:
tic()
d <-
datafiles_list |>
map_dfr(read.csv, .id = "file_id") # dauert etwas ...
toc()Über die Performanz sollte man sich noch Gedanken machen …
Mit .id bekommt man eine laufende Nummer für jede eingehende CSV-Datei.
map_XXX(list, fun) wendet die Funktion fun auf jedes Element von list an. map_dfr ist eine spezielle Variante, die die Ergebnisse in einem Dataframe zusammenführt und zwar zeilenweise (row), daher _dfr.
Alternative:
purrr::map_df(
dateien,
~ read_csv(.x, col_types = cols(.default = "c"), show_col_types = FALSE),
.id = "file_id"
)Synonym zur Tilde-Schreibweise kann man schreiben:
purrr::map_df(
dateien,
function(.x) {
read_csv(.x, col_types = cols(.default = "c"), show_col_types = FALSE)
},
.id = "quelle_datei"
)Die Tilde ist also nur eine Abkürzung für eine Formel.
Wie groß ist der resultierende Datensatz?
dim(d)Check:
d_small <-
d |>
select(1:50)
d_small |> glimpse()Scheint zu passen.
Alternativ kann man den Namen der Datei hinzufügen:
tic()
d <-
datafiles_list |>
map_dfr(~ read.csv(.x) |>
mutate(filename = basename(.x))) # dauert etwas ...
toc()Die Tilde-Notation ~ ist eine Kurzschreibweise für eine Funktion. Man könnte also auch schreiben:
meine_funktion <- function(x) {
read.csv(x) |>
mutate(filename = basename(x))
}
datafiles_list |>
map_dfr(meine_funktion)Exkurs: Noch etwas Erklärung zu map. map ist eine Art von “Schleife”: Die darauf bezogene Funktion wird für jedes Element der Liste ausgeführt. Im folgenden Beispiel wird die Funktion basename auf jedes Element der Liste datafiles_list angewendet, also der Dateiname ohne Pfad zurückgegeben.
datafiles_list |>
map(basename)Check:
d_small <-
d |>
select(filename, everything()) |> # `filename` nach vorne ziehen
select(1:50)
d_small |> glimpse() # Blick reinwerfen1.2.2 Leere Strings in NAs umwandeln
Anteil NAs:
mean(is.na(d))Verduetlichung:
leerer_string <- c("")
leerer_stringEchte NAs:
d_with_true_nas <-
d |>
mutate(across(where(is.character), ~ na_if(., "")))Diese Syntax heißt sinngemäß auf Deutsch:
- Hey R, nimm den Datensatz
d - transformiere durch alle Spalte, wo der Typ der Spalte “Text” ist wie folgt:
- Setze
NAwenn der Wert in der jeweiligen Zelle""ist, also ein leerer Text
mean(is.na(d_with_true_nas))Puh! Das ist ein großer Anteil.
Prüfen wir das lieber noch einmal.
d_with_true_nas_small <-
d_with_true_nas |>
select(1:100)vis_dat(d_with_true_nas_small)Hm, sieht ja gar nicht nach so vielen NAs aus…
Vielleicht kommen die NAs erst weiter hinten?
d_with_true_nas_small <-
d_with_true_nas |>
select(3500:3700)vis_dat(d_with_true_nas_small)Tatsächlich!
Was sagt uns dieser Befund?
1.2.3 Leere Spalten und Zeilen entfernen
d_no_empty_cols_no_empty_rows <-
d_with_true_nas |>
remove_empty(which = c("rows", "cols"))Alternativ:
d_no_empty_cols_no_empty_rows %>%
select(where(~ dplyr::n_distinct(.x, na.rm = TRUE) > 1))Check:
dim(d_with_true_nas)dim(d_no_empty_cols_no_empty_rows)Ein paar Spalten haben wir eingespart.
Es würde Sinn machen, sich diese komplett leeren Spalten näher anzuschauen. Warum sind sie überhaupt enthalten?
1.2.4 Konstanten entfernen
d_no_constants <-
d_no_empty_cols_no_empty_rows |>
remove_constant(quiet = FALSE)Es bietet sich an, das Ergebnis, d_no_constants abzuspeichern, um damit dann später wieder weiterzuarbeiten.
Man kann es als CSV-Datei abspeichern:
write.csv(d_no_constants, "data-processed/d_no_constants.csv")Oder als R-Datendatei:
write_rds(d_no_constants, "data-processed/d_no_constants.rds")1.2.5 Excel – Pro und Contra
Eine (große) Menge an Tabellen zu einer Master-Exceltabelle zusammenzufügen ist schwierig. Mit einem Copy-Paste-Ansatz ist es nicht gesichert, dass die richtigen Spalten untereinander gesetzt werden, zumindest prüft es Excel nicht. Bei großen Tabellen wird die Sache unpraktisch (viel Scrollen) und langsam.
Schließlich – vielleicht am wichtigsten – ist das händische Vorgehen mit Excel nicht reproduzierbar. Es ist also nicht präzise zu beschreiben, was man (genau) gemacht hat. Daher fällt es auch schwierig, Fehler zu finden und den Prozess zu verbessern. Eine klare Kommunikation über das Vorgehen ist kaum möglich.
1.2.6 Irrelevante Spalten identifizieren
names(d) |> head(100)- file_id
- idSite
- visitIp
- actionDetails_0_timeSpentPretty - nur ein “hübsches” Format
- actionDetails_0_pageLoadTime - unabhängig von Nutzerverhalten
- actionDetails_0_pageId._pageId - vermutlich redundant
- actionDetails_0_pageviewPosition - unabhängig von Nutzerverhalten
- actionDetails_0_icon - unabhängig von Nutzerverhalten
- actionDetails_0_iconSVG - unabhängig von Nutzerverhalten
- actionDetails_1_pageIdAction - unabhängig von Nutzerverhalten
- actionDetails_1_idpageview - unabhängig von Nutzerverhalten
- actionDetails_1_pageviewPosition - unabhängig von Nutzerverhalten
- actionDetails_2_timeSpentPretty - nur ein “hübsches” Format
1.2.7 Irrelevante Spalten entfernen
Irrelevante Spalten entfernen, die im letzten Schritt identifiziert wurden.
d_names <- names(d)
cols_parts_irrelavant <- c(
"timeSpentPretty",
"pageLoadTime",
"pageId._pageId",
"_pageviewposition",
"icon",
"iconSVG",
"pageIdAction",
"idpageview",
"pageviewPosition",
"timeSpentPretty"
)Entfernen:
d_no_irrelevant_cols <- d %>%
select(!matches(paste(cols_parts_irrelavant, collapse = "|")))Check:
dim(d_no_irrelevant_cols)Ok, das sind weniger.
1.2.8 Nur wenige Spalten behalten
id_cols <-
c("idVisit", "fingerprint")fingerprint und visitorId sind vermutlich redundant, daher nur eine der beiden Spalten behalten.
target_cols_parts <- c("subtitle", "timestamp")all_targets <- c(id_cols, target_cols_parts)
d_few_cols <- d_no_irrelevant_cols %>%
select(matches(paste(all_targets, collapse = "|")))Check:
dim(d_few_cols)1.2.9 Datum reparieren
d_repair_dates <-
d_few_cols |>
mutate(across(contains("timestamp"), ~ as_datetime(as.numeric(.x))))d_repair_dates |>
select(contains("timestamp")) |>
select(1:10) |>
head()1.2.10 Bildspalten entfernen
… schon erledigt oben …
1.2.11 Zeilen nur mit Studis behalten
Die Frage ist, ob man “role=undefined” auch behalten will?. Es sieht so aus, als ob das meistens Studis sind.
d_students_only <-
d_repair_dates |>
filter(!str_detect(actionDetails_0_subtitle, "=admin|=developer|=lecturer"))Check:
d_students_only |> nrow()
d_repair_dates |> nrow()2 Abschluss - Daten abspeichern
write.csv(d_students_only, "data/d_students_only.csv")Wiederverwendung
Zitat
@online{sauer2026,
author = {Sauer, Sebastian},
title = {Challenge 02: Daten einlesen und Aufbereiten -\/- Lösung},
date = {2026-01-12},
url = {https://sebastiansauer.github.io/hans-hackathon2025/challenge02-solution.html},
langid = {de-DE}
}