# Rohdaten laden
path_raw_data <- "raw_data/ssz_30042025_labels.csv"
df_input <- read_csv(path_raw_data, show_col_types = FALSE)2 Datenaufbereitung
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:
- Die Labelinformationen sind nicht als Metadaten abgelegt, sondern in den ersten beiden Zeilen der Datendatei enthalten und müssen zunächst extrahiert und korrekt zugeordnet werden.
- Sämtliche Labels liegen in englischer Sprache vor. Für die Auswertung steht eine separate Übersetzungsdatei zur Verfügung, die in die Daten integriert werden muss.
- Einige Fragestellungen erlauben Mehrfachantworten, welche in einer einzelnen Spalte als kommaseparierte Werte gespeichert sind und entsprechend berücksichtigt werden müssen.
- Fragen zur Zustimmung oder Ablehnung (Likert-Skalen) sind ausschliesslich textbasiert (z. B. Strongly agree) erfasst. Die Likert-Skala-Werte werden zur einfacheren Auswertung in numerische Werte umcodiert.
2.1 Datenimport und erste Anpassungen
Daten laden:
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")