2  Datenaufbereitung

Logo_OGDZH

Logo_UZH

In diesem Kapitel werden die Rohdaten systematisch aufbereitet, um eine konsistente und nachvollziehbare Datenbasis für die Analysen im folgenden Kapitel zu schaffen.

Die Datenaufbereitung adressiert dabei insbesondere folgende Aspekte:

2.1 Datenimport und erste Anpassungen

Daten laden:

# Rohdaten laden
path_raw_data <- "raw_data/ssz_30042025_labels.csv"
df_input <- read_csv(path_raw_data, show_col_types = FALSE)

2.1.1 Extraktion von Metadaten und Labelinformationen

# Die erste Zeile enthält die Labeltexte (Fragen und Antwortoptionen)
labels <- head(df_input, n = 1)

# Die zweite Zeile enthält pro Spalte eine JSON-Struktur mit der ImportId
# Diese wird ausgelesen und den entsprechenden Spaltennamen zugeordnet
import_row <- df_input[2, ]
import_id <- sapply(import_row, function(x) jsonlite::fromJSON(x)$ImportId)

import_id <- tibble(
  data_colnames = names(import_id),
  ImportId = unname(import_id)
)

# Entferne die beiden Metazeilen, sodass df_survey nur noch die Antwortdaten enthält
df_survey <- df_input[-c(1, 2), ]

2.1.2 Anpassung von Datenformaten und -typen

# Konvertiere Datums- und Zeitangaben in ein einheitliches POSIXct-Format
date_time_columns <- c("StartDate", "EndDate", "RecordedDate")
df_survey[date_time_columns] <- lapply(
  df_survey[date_time_columns],
  as.POSIXct,
  format = "%Y-%m-%d %H:%M:%S"
)

# Erzeuge eine zusätzliche Datumsspalte (ohne Zeitanteil) für spätere Auswertungen
df_survey$date <- as.Date(df_survey$EndDate)

# Konvertiere ausgewählte numerische Spalten in den Datentyp Integer
int_columns <- c("Progress", "Duration (in seconds)")
df_survey[int_columns] <- lapply(df_survey[int_columns], as.integer)

2.2 Filterung der Rohdaten

In diesem Schritt werden die Rohdaten bereinigt, indem Testeinträge, unvollständig ausgefüllte Fragebögen sowie Datensätze ohne Zustimmung zur Teilnahme und Datenerhebung entfernt werden.

Die Filterung stellt sicher, dass die anschliessende Analyse ausschliesslich auf vollständigen und gültigen Antworten basiert.

Ein paar Eckwerte zur Umfrage:

[1] "- Anzahl Teilnehmende an der Umfrage insgesamt:  101"
[1] "- Anzahl Teilnehmende mit vollständig ausgefüllten Fragebögen:  61"
[1] "- Anzahl Teilnehmende mit Einwilligung zur Datenerhebung:  59"

2.2.1 Ausreisser in der Bearbeitungsdauer

Im folgenden werden Ausreisser in der Bearbeitungsdauer der Umfrage identifiziert und entfernt.

Dazu wird die Tukey-Methode angewendet, bei der das erste und dritte Quartil bestimmt und ein Intervall von ± 3 × Interquartilsabstand (Interquartile Range, IQR) berechnet wird. Eine Beschreibung der Methode findet sich z. B. unter: https://en.wikipedia.org/wiki/Outlier#Tukey%27s_fences

Da lange Bearbeitungszeiten nicht notwendigerweise auf eine geringe Datenqualität hinweisen und es keine Zeitbeschränkung bei der Umfrage gab, wurden ausschliesslich Fälle mit ungewöhnlich kurzer Bearbeitungsdauer ausgeschlossen. Die Hauptergebnisse blieben unverändert, wenn auch die oberen Ausreisser ausgeschlossen wurden.

tukey_bounds <- function(x, multiplier = 3, na.rm = TRUE) {
  if (na.rm) x <- x[!is.na(x)]
  q1 <- quantile(x, 0.25)
  q3 <- quantile(x, 0.75)
  iqr <- q3 - q1
  list(
    lower = q1 - multiplier * iqr,
    upper = q3 + multiplier * iqr
  )
}

outlier_col <- "Duration (in seconds)"
bounds <- tukey_bounds(df_survey[[outlier_col]])

df_survey <- df_survey %>%
  filter(
    .data[[outlier_col]] >= bounds$lower
    # .data[[outlier_col]] <= bounds$upper
  )

2.3 Eckwerte zum Erhebungsverlauf und Antwortverhalten

In diesem Abschnitt werden kurz ein paar deskriptive Kennzahlen und grafische Übersichten zur Durchführung der Umfrage dargestellt, um einen kurzen Eindruck über den Erhebungsverlauf und das Antwortverhalten zu vermitteln.

2.3.1 Zeitlicher Verlauf der Umfrage

# Anzahl ausgefüllter Fragebögen pro Tag
counts_per_day <- df_survey %>%
  group_by(date) %>%
  summarise(count = n())

ggplot(counts_per_day, aes(x = date, y = count)) +
  geom_bar(stat = "identity", fill = color) +
  labs(
    x = "Datum",
    y = "Anzahl\nausgefüllte\nFragebögen",
    title = "Anzahl ausgefüllter Fragebögen pro Tag"
  )

Die Spitzen im Zeitverlauf dürften mit den Bewerbungswellen zusammenfallen: Ende 2024 und im Frühjahr 2025 wurde die Umfrage jeweils auf LinkedIn geteilt, was zu einem erhöhten Rücklauf in diesen Zeiträumen geführt hat.

2.3.2 Bearbeitungsdauer der Umfrage

Die folgende Darstellung zeigt die Verteilung der Zeit, welche die Teilnehmenden für das Ausfüllen der Umfrage benötigten.

df_survey$`Duration (in minutes)` <- df_survey$`Duration (in seconds)` / 60
ggplot(df_survey, aes(x = `Duration (in minutes)`)) +
  geom_histogram(binwidth = 1,fill = color) + # 600=10 Min
  labs(x = "Dauer in Minuten", y = "Häufigkeit", title = "Histogramm der Ausfülldauer")

Die mediane Ausfülldauer betrug 7.8 Minuten.

2.3.3 Sprachversion der Umfrage

Die Teilnehmenden konnten wählen, ob sie die Umfrage auf Deutsch oder Englisch ausfüllen wollen. Die folgende Grafik zeigt die prozentuale Verteilung nach Sprachversion.

# Häufigkeiten und Anteile nach Sprachversion berechnen
freq_lang <- df_survey %>%
  count(UserLanguage) %>%
  mutate(percentage = n / sum(n) * 100)

# Kreisdiagramm der Sprachverteilung
ggplot(freq_lang, aes(x = "", y = percentage, fill = UserLanguage)) +
  geom_bar(stat = "identity", width = 1) +
  coord_polar(theta = "y") +
  labs(
    x = NULL,
    y = NULL,
    fill = "Sprache",
    title = "Verteilung der Sprachversionen des Fragebogens"
  ) +
  theme_void() +
  geom_text(
    aes(label = paste0(round(percentage, 1), "%")),
    position = position_stack(vjust = 0.5)
  ) +
  scale_fill_manual(values = get_zuericolors(palette = "qual6"))

2.4 Übersetzungen integrieren

Da die Rohdaten aus Qualtrics in englischer Sprache vorliegen, wurden die deutschen Übersetzungen verwendet, um die Labels und Antwortwerte anzupassen.

2.4.1 Übersetzungsdatei einlesen

In diesem Schritt wird die Übersetzungstabelle geladen, welche englische Originaltexte den entsprechenden deutschen Übersetzungen zuordnet.

path_translations <- "raw_data/ssz_translations_3122024_clean.csv"
tr <- read_csv(path_translations, show_col_types = FALSE)

2.4.2 Antwortwerte im Datensatz übersetzen (EN → DE)

Anschliessend werden die Zellwerte im Antwortdatensatz df_survey durch ihre deutschen Entsprechungen ersetzt, sofern für einen Wert ein Eintrag in der Übersetzungstabelle vorhanden ist.

Hinweis: Dieser Schritt ersetzt exakte Zellwerte und funktioniert daher nicht bei Mehrfachantworten, die als kommaseparierte Listen innerhalb einer Zelle gespeichert sind.

# Sicherheitskopie (optional)
df_survey_en <- df_survey

for (col in names(df_survey)) {
  for (i in seq_len(nrow(df_survey))) {
    value <- df_survey[i, col]
    if (!is.na(value)) {
      idx <- match(value, tr$EN)
      if (!is.na(idx)) {
        df_survey[i, col] <- tr$DE[idx]
      }
    }
  }
}

2.4.3 Labeltexte und Antwortoptionen übersetzen

Neben den Antwortwerten werden auch die Labeltexte (Fragen und Antwortoptionen) übersetzt. Dazu wird die in PhraseID enthaltene ImportId aufgeteilt, den Datenspalten über das ImportId-Mapping zugeordnet und anschliessend in der Label-Zeile durch die deutschen Texte ersetzt.

tr_split <- tr %>%
  separate(PhraseID, into = c("ImportId", "Suffix"), sep = "_", remove = FALSE)
Warning: Expected 2 pieces. Missing pieces filled with `NA` in 2 rows [189,
190].
# Frage-Texte
tr_questions <- tr_split %>%
  filter(Suffix == "QuestionText") %>%
  left_join(import_id, by = "ImportId")

# Antwortoptionen (Choice*)
tr_choices <- tr_split %>%
  filter(str_starts(Suffix, "Choice")) %>%
  mutate(ImportId = str_c(ImportId, str_remove(Suffix, "^Choice"), sep = "_")) %>%
  left_join(import_id, by = "ImportId")

import_id_tr <- bind_rows(tr_questions, tr_choices)

labels_en <- labels

for (i in seq_len(nrow(import_id_tr))) {
  colname <- import_id_tr$data_colnames[i]
  new_text <- import_id_tr$DE[i]

  if (!is.na(colname) && colname %in% colnames(labels) && !is.na(new_text)) {
    labels[[colname]] <- new_text
  }
}

2.5 Likert-Skala umcodieren

In diesem Schritt werden die textbasierten Antworten der Likert-Skala (von Zustimmung bis Ablehnung) in numerische Werte umcodiert, damit sie rechnerisch ausgewertet werden können. Anschliessend werden die umcodierten Antworten ins Long-Format überführt und mit den Frage-Labels verknüpft, woraus der Analyse- und Grafik-Datensatz merged_data entsteht.

lickert_mapping <- c(
  "Stimme überhaupt nicht zu" = -2,
  "Stimme nicht ganz zu" = -1,
  "Neutral" = 0,
  "Stimme leicht zu" = 1,
  "Stimme voll und ganz zu" = 2,
  "Kann ich nicht beurteilen" = NA
)

recode_cols <- paste0("Q16_", 1:10)

for (col in recode_cols) {
  col_num <- paste0(col, "_num")
  df_survey <- df_survey %>%
    mutate(!!col_num := unname(lickert_mapping[.data[[col]]]))
}

num_cols <- paste0(recode_cols, "_num")

agree_pivot <- df_survey %>%
  pivot_longer(
    cols = all_of(num_cols),
    names_to = "variable",
    values_drop_na = TRUE
  ) %>%
  select(variable, value) %>%
  mutate(
    variable_name = str_remove(variable, "_num"),
    value_numeric = as.numeric(value)   # <- wieder drin
  )

labels_long <- labels %>%
  pivot_longer(
    cols = everything(),
    names_to = "variable_name",
    values_to = "label"
  )

merged_data <- agree_pivot %>%
  left_join(labels_long, by = "variable_name") %>%
  separate(label, into = c("question", "label_short"), sep = " - ")

2.6 Aufbereitete Dataframes speichern

Im letzten Schritt werden die aufbereiteten Datensätze dauerhaft gespeichert, damit sie in nachfolgenden QMDs ohne erneute Datenaufbereitung verwendet werden können. Die Dataframes werden im RDS-Format im Ordner wrangled_data abgelegt, wodurch Datentypen, Faktoren und Strukturen vollständig erhalten bleiben und die Weiterverarbeitung effizient und reproduzierbar ist.

dir.create("wrangled_data", showWarnings = FALSE, recursive = TRUE)

write_rds(df_survey, "wrangled_data/df_survey.rds")
write_rds(merged_data, "wrangled_data/merged_data.rds")
write_rds(labels, "wrangled_data/labels.rds")
write_rds(tr, "wrangled_data/tr.rds")