# in das Objekt "youtube" speichern wir die Datenspenden
<-
youtube # zunächst listen wir alle Dateien in unserem Datenspende-Ordner auf und lassen die Funktion
# read_csv() über alle Dateien in diesem Ordner laufen
# und verbinden alles zu einem tibble mit der Funktion list_rbind()
map(list.files("../dd_data/youtube/", full.names = TRUE), ~ read_csv(.x, show_col_types = FALSE)) |>
set_names(list.files("../dd_data/youtube/") |> str_extract(".*(?=_2025)")) |>
list_rbind(names_to = "participant") |>
distinct() |>
# anschließend stellen wir sicher, dass der Zeitstempel in den Datenspenden in der
# gleichen Zeitzone codiert ist wie unsere Befragungsdaten
mutate(time = lubridate::as_datetime(timestamp) |> with_tz("UTC") + hours(2)) |>
select(-day, -hour, -weekday) |>
rowwise() |>
# auch hier wird zur Wahrung der Anonymität der participant code pseudonymisiert
mutate(participant = digest::digest(participant, algo = "md5")) |>
ungroup() |>
# zuletzt stellen wir noch sicher, dass wir nur Daten mit korrekten participant codes
# beibehalten (also dieser code auch in den Befragunsgdaten vorkommt)
filter(participant %in% d$participant)
7 Data Linkage
7.1 Datenspenden einlesen
Wir wollen nun unsere Datenspenden mit den Befragungsdaten verbinden. Hierzu müssen wir zunächst alle Datenspenden, die in separaten .csv
-Dateien abgespeichert sind, einlesen. Im folgenden illustrieren wir dies anhand der YouTube-Datenspenden. Weiterhin nehmen wir an, dass der Befragungsdatensatz in einem Objekt mit der Bezeichnung d
bereits in das Environment geladen wurde.
7.2 Datensätze verbinden und aggregieren
In einem zweiten Schritt erfolgt nun das verbinden der Datensätze:
# wir erstellen ein neues Objekt "d_dd", in welches wir den verbundenen Datensatz speichern
<-
d_dd # zunächst gruppieren wir unseren Befragungsdatensatz nach participant und sortieren ihn nach Zeit
|>
d group_by(participant) |>
arrange(time) |>
mutate(
# anschließend erstellen wir eine neue Spalte im Datensatz mit dem Namen youtube_session
# diese ist eine Liste von tibbles mit der gleichen Länge wie unser Befragungssatz
# (also pro Zeile ein tibble)
# Im ersten Schritt speichern wir in jedem tibble alle Youtube Videos, die zeitlich nach der
# vorherigen Befragung, aber vor der aktuellen Befragung angeschaut wurden
youtube_session = map2(lag(time), time, ~ filter(youtube, time >= .x & time <= .y), .progress = TRUE),
# Bislang enthält dieses tibble jedoch noch alle Videos, auch solche, die zwar zeitlich passen,
# allerdings von einer anderen Person geschaut wurden
# Daher filtern wir im zweiten Schritt jedes tibble, sodass darin nur noch solche Videos enthalten sind,
# die auch von der Person geschaut wurden, zu der die jeweilige Zeile gehört
youtube_session = map2(youtube_session, participant, ~ filter(.x, participant == .y), .progress = TRUE),
# Als letzten Schritt könnten wir nun noch, auf der Grundlage von zusätzlich erhobenen Metadaten,
# wie z.B. Genres der jeweiligen Videos, Informationen auf Session-Ebene aggregieren.
# Wenn wir z.B. im Vorfeld eine Funktion compute_session() definieren, die uns einen Mittelwert für
# die Variable genre_action berechnet, welche für jedes Video TRUE oder FALSE sein kann, und diese
# Funktion auf jedes tibble in der Spalte youtube_session anwenden, dann erhalten wir in dem neuen Objekt
# session data für jede Session den prozentualen Anteil an Action Videos in dieser Session.
# session_data = map(youtube_session, ~ compute_session(.x), .progress = TRUE),
.after = time
|>
) ungroup() |>
# wenn wir ein aggregiertes Objekt session_data erstellt haben,
# können wir dieses mit der folgenden Funktion in den Datensatz einbinden
# unnest_wider(session_data) |>
distinct() |>
arrange(participant, time)
8 Metadaten scrapen (Beispiel Netflix)
Doch wie erhalten wir entsprechende Meta-Daten, die wir auf Session-Ebene aggregieren können? Für YouTube haben wir uns dies bereits angesehen, wir können dazu bspw. die YouTube Data API oder das Programm yt-dlp
nutzen (siehe hierzu Materialien zur Sitzung Digitale Verhaltensdaten).
Eine weitere Möglichkeit wäre das sogenannte Webscraping, also das Herunterladen von Informationen aus dem Internet. Das schauen wir uns im Folgenden kurz für unsere Netflix-Datenspenden an (dieses Verfahren haben wir bereits in einem Projekt angewendet, für weitere Informationen, siehe https://osf.io/5updj/).
# angenommen, wir haben unsere Netflix-Datenspenden eingelesen, wie oben anhand des YouTube-Beispiels illustriert, dann können wir nun alle Titel der Filme bzw. Serien in einem Objekt abspeichern:
<- netflix_data_donations |>
unique_titles distinct(main_title)
# Wir ersellen nun eine neue csv-Datei, in der wir die Metdadaten später abspeichern werden
if (!file.exists("data/03_movie_data/jw_movie_info.csv")) {
|>
unique_titles mutate(
# mit dieser URL können wir die Titel auf der Filmseite justwatch finden
jw_search_url = str_c(
"https://www.justwatch.com/de/Suche?q=",
str_replace_all(main_title, pattern = " ", replacement = "%20")
),jw_item_url = "TBD",
jw_is_on_netflix = "TBD"
|>
) write_csv("data/03_movie_data/jw_movie_info.csv")
}
# Error Handling
<- possibly(rvest::read_html, otherwise = NA)
safe_read_html <- possibly(rvest::html_attr, otherwise = NA)
safe_html_attr <- possibly(rvest::html_nodes, otherwise = NA)
safe_html_nodes
# Scraping Function definieren
<- function(data) {
scrape_jw # Eine URL aus dem Datensatz auswählen
<- data |>
one_url sample(1)
# Suche auf der Webseite durchführen
<- safe_read_html(one_url)
html
# Die URL des ersten Treffers auswählen
<-
jw_item_url_x |>
html safe_html_nodes(".title-list-row__column-header") |>
pluck(1) |>
safe_html_attr("href") |>
str_c("https://www.justwatch.com", .)
# Prüfen, ob das gefundene Item auf Netflix verfügbar ist
<-
jw_is_on_netflix_x |>
html safe_html_nodes(".price-comparison__grid__row--stream") |>
safe_html_nodes(".price-comparison__grid__row__element__icon") |>
safe_html_nodes(".price-comparison__grid__row__icon") |>
safe_html_attr("title") |>
str_detect("Netflix") |>
as_tibble() |>
summarise(
sum = sum(value),
jw_is_on_netflix = if_else(sum > 0, TRUE, FALSE)
|>
) pull(jw_is_on_netflix)
# Im Datensatz abspeichern
read_csv("data/03_movie_data/jw_movie_info.csv",
show_col_types = FALSE,
col_types = "cccc",
progress = FALSE
|>
) mutate(
jw_item_url = if_else(jw_search_url == one_url, jw_item_url_x, jw_item_url),
jw_is_on_netflix = if_else(jw_search_url == one_url, as.character(jw_is_on_netflix_x), jw_is_on_netflix)
|>
) write_csv("data/03_movie_data/jw_movie_info.csv", progress = FALSE)
# höfliches Warten
Sys.sleep(sample(1:5, 1))
}
# Eine zufälle Stichprobe von n = 100 scrapen
<- 100
n read_csv("data/03_movie_data/jw_movie_info.csv",
show_col_types = FALSE,
col_types = "cccc"
|>
) filter(jw_item_url == "TBD") |>
sample_n(n) |>
pull(jw_search_url) |>
walk(scrape_jw, .progress = TRUE)
Wir haben nun für jeden Titel eine (hoffentlich) passende JustWatch-Seite gefunden. Im zweiten Schritt können wir nun die Metadaten scrapen:
# Neue Datei für zweiten Schritt anlegen
if (!file.exists("data/03_movie_data/jw_movie_info_2.csv")) {
read_csv("data/03_movie_data/jw_movie_info.csv", show_col_types = FALSE) |>
filter(!is.na(jw_item_url)) |>
mutate(
jw_desc = "TBD",
jw_genre = "TBD",
jw_runtime = "TBD",
jw_fsk = "TBD",
imdb_id = "TBD"
|>
) write_csv("data/03_movie_data/jw_movie_info_2.csv")
}
# noch mehr error handling
<- possibly(rvest::read_html, otherwise = NA)
safe_read_html <- possibly(rvest::html_attr, otherwise = NA)
safe_html_attr <- possibly(rvest::html_nodes, otherwise = NA)
safe_html_nodes <- possibly(rvest::html_text, otherwise = NA)
safe_html_text <- possibly(rvest::html_children, otherwise = NA)
safe_html_children
# Zweite Scraping Funktion definieren
<- function(data) {
scrape_jw2 # Eine URL auswählen
<- data |>
one_url sample(1)
# Seite laden
<- safe_read_html(one_url)
html
# Filmbeschreibung extrahieren
<-
jw_desc_x |>
html safe_html_nodes(".text-wrap-pre-line[data-v-45036d4d]") |>
safe_html_text() |>
ifelse(length(.) == 0, NA, .)
# Genres extrahieren
<-
jw_genre_x |>
html safe_html_nodes(".detail-infos__value") |>
pluck(2) |>
safe_html_text() |>
ifelse(length(.) == 0, NA, .)
# Laufzeit extrahieren
<-
jw_runtime_x |>
html safe_html_nodes(".detail-infos__value") |>
pluck(3) |>
safe_html_text() |>
ifelse(length(.) == 0, NA, .)
# FSK-Rating extrahieren
<-
jw_fsk_x |>
html safe_html_nodes(".detail-infos__value") |>
pluck(4) |>
safe_html_text() |>
ifelse(length(.) == 0, NA, .)
# IMDB ID extrahieren
<-
imdb_id_x |>
html safe_html_nodes(".jw-scoring-listing__rating") |>
pluck(2) |>
safe_html_children() |>
safe_html_attr("href") |>
str_extract("(?<=https://www.imdb.com/title/)(.*)(?=/)") |>
ifelse(length(.) == 0, NA, .)
# In Datensatz speichern
read_csv("data/03_movie_data/jw_movie_info_2.csv",
show_col_types = FALSE,
col_types = "ccccccccc",
progress = FALSE
|>
) mutate(
jw_desc = if_else(jw_item_url == one_url, jw_desc_x, jw_desc),
jw_genre = if_else(jw_item_url == one_url, jw_genre_x, jw_genre),
jw_runtime = if_else(jw_item_url == one_url, jw_runtime_x, jw_runtime),
jw_fsk = if_else(jw_item_url == one_url, jw_fsk_x, jw_fsk),
imdb_id = if_else(jw_item_url == one_url, imdb_id_x, imdb_id)
|>
) write_csv("data/03_movie_data/jw_movie_info_2.csv", progress = FALSE)
# höfliches Warten
Sys.sleep(sample(1:5, 1))
}
# Eine zufälle Stichprobe von n = 100 scrapen
<- 100
n read_csv("data/03_movie_data/jw_movie_info_2.csv",
show_col_types = FALSE,
col_types = "ccccccccc"
|>
) filter(jw_desc == "TBD") |>
sample_n(n) |>
pull(jw_item_url) |>
walk(scrape_jw2, .progress = TRUE)