Challenge 02: Daten einlesen und Aufbereiten – Lösung

“Eine Hackathon-Variante zur Evaluation der Klickdaten des KI-Tools ‘HaNS’”

Autor:in

Sebastian Sauer

Veröffentlichungsdatum

12. Januar 2026

1 Daten einlesen

1.1 Setup

library(tidyverse)
#library(stringr)  # Strings verarbeiten
library(here)  # liest aktuelles Verzeichnis aus
library(visdat)
library(janitor)
library(tictoc)

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_list

Wie 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 reinwerfen

1.2.2 Leere Strings in NAs umwandeln

Anteil NAs:

mean(is.na(d))

Verduetlichung:

leerer_string <- c("")
leerer_string

Echte 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 NA wenn 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

MIT

Zitat

Mit BibTeX zitieren:
@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}
}
Bitte zitieren Sie diese Arbeit als:
Sauer, Sebastian. 2026. “Challenge 02: Daten einlesen und Aufbereiten -- Lösung.” January 12, 2026. https://sebastiansauer.github.io/hans-hackathon2025/challenge02-solution.html.