Wohin gehen Céline und Marilen? Eine Reise durch ihre Bewegungsmuster mit Google Maps Timeline

Project for Patterns and Trends in Environmental Data MSc ENR FS25

Autor:in

Marilen Wittensöldner und Céline Spitzli

Veröffentlichungsdatum

1. Mai 2025

Datenvorverarbeitung
# Daten Vorverarbeitung
# Vorgehen beim Daten einlesen und anschliessendem Angleich der beiden Datensätze

# Librarys laden

# install.packages("pacman")
library("pacman")

p_install("dplyr", force = FALSE)
p_install("ggplot2", force = FALSE)
p_install("readr", force = FALSE)
p_install("tidyr", force = FALSE)
p_install("sf", force = FALSE)
p_install("terra", force = FALSE)
p_install("tmap", force = FALSE)
p_install("zoo", force = FALSE)
p_install("units", force = FALSE)
p_install("plotly", force = FALSE)
p_install("patchwork", force = FALSE)
library("ggplot2")
library("dplyr")
library("readr")
library("sf")
library("readr")
library("sf")
library(lubridate)
library(jsonlite)
library(stringr)
library(tidyr)
library("wordcountaddin")
library(plotly)
library(fuzzyjoin)
library(mapview)
library(rnaturalearth)
library(rnaturalearthdata)
library(devtools)
library(spatstat.geom)
library(spatstat.explore)
library(viridis)
library(patchwork)

# Daten import
# Marilen

marilen_3 <- fromJSON("Quarto/datensaetze/alt/Zeitachse_mw.json", flatten = T, simplifyVector = T)
marilen_2 <- marilen_3$rawSignals
marilen <- marilen_3$semanticSegments
  
# extrahiert verschachtelte Spalte mit Location in eine neue Spalte
marilen$location <- marilen$visit.topCandidate.placeLocation

colnames(marilen)
##  [1] "startTime"                              
##  [2] "endTime"                                
##  [3] "timelinePath"                           
##  [4] "startTimeTimezoneUtcOffsetMinutes"      
##  [5] "endTimeTimezoneUtcOffsetMinutes"        
##  [6] "visit.hierarchyLevel"                   
##  [7] "visit.probability"                      
##  [8] "visit.topCandidate.placeId"             
##  [9] "visit.topCandidate.semanticType"        
## [10] "visit.topCandidate.probability"         
## [11] "visit.topCandidate.placeLocation.latLng"
## [12] "activity.distanceMeters"                
## [13] "activity.probability"                   
## [14] "activity.start.latLng"                  
## [15] "activity.end.latLng"                    
## [16] "activity.topCandidate.type"             
## [17] "activity.topCandidate.probability"      
## [18] "activity.parking.startTime"             
## [19] "activity.parking.location.latLng"       
## [20] "location"

# Celine
celine <- fromJSON("Quarto/datensaetze/alt/zeitreihe_celine.json", flatten = T, simplifyDataFrame = T)

## Das braucht es meiner Meinung nach nicht mehr....
# extrahiert verschachtelte Spalte mit Location in eine neue Spalte
# celine$location <- celine$visit.topCandidate.placeLocation

colnames(celine)
##  [1] "endTime"                           "startTime"                        
##  [3] "timelinePath"                      "visit.hierarchyLevel"             
##  [5] "visit.probability"                 "visit.isTimelessVisit"            
##  [7] "visit.topCandidate.probability"    "visit.topCandidate.semanticType"  
##  [9] "visit.topCandidate.placeID"        "visit.topCandidate.placeLocation" 
## [11] "activity.probability"              "activity.end"                     
## [13] "activity.distanceMeters"           "activity.start"                   
## [15] "activity.topCandidate.type"        "activity.topCandidate.probability"

## Entfernen unerwünschter Spalten
# Marilen

# entfernt unerwünschte Spalten wie timelinePath, visit.hierarchyLevel, visit.isTimelessVisit,  Spalte
marilen$timelinePath <- NULL
marilen$visit.hierarchyLevel <- NULL
marilen$visit.isTimelessVisit <- NULL
marilen$visit.topCandidate.semanticType <- NULL
marilen$visit.topCandidate.placeID <- NULL
marilen$visit.topCandidate.placeLocation <- NULL
marilen$activity.parking.location.latLng <- NULL
marilen$startTimeTimezoneUtcOffsetMinutes <- NULL
marilen$endTimeTimezoneUtcOffsetMinutes <- NULL
marilen$visit.topCandidate.placeId <- NULL
marilen$activity.parking.startTime <- NULL
marilen$visit.topCandidate.placeLocation.latLng <- NULL
marilen$visit.probability <- NULL
marilen$visit.topCandidate.probability <- NULL
marilen$location <- NULL
colnames(marilen)
## [1] "startTime"                         "endTime"                          
## [3] "activity.distanceMeters"           "activity.probability"             
## [5] "activity.start.latLng"             "activity.end.latLng"              
## [7] "activity.topCandidate.type"        "activity.topCandidate.probability"

# Celine

celine$timelinePath <- NULL
celine$visit.hierarchyLevel <- NULL
celine$visit.isTimelessVisit <- NULL
celine$visit.topCandidate.semanticType <- NULL
celine$visit.topCandidate.placeID <- NULL
celine$visit.topCandidate.placeLocation <- NULL
celine$activity.parking.location.latLng <- NULL
celine$startTimeTimezoneUtcOffsetMinutes <- NULL
celine$endTimeTimezoneUtcOffsetMinutes <- NULL
celine$visit.topCandidate.placeId <- NULL
celine$activity.parking.startTime <- NULL
celine$visit.topCandidate.placeLocation.latLng <- NULL
celine$visit.probability <- NULL
celine$visit.topCandidate.probability <- NULL
celine$location <- NULL
colnames(celine)
## [1] "endTime"                           "startTime"                        
## [3] "activity.probability"              "activity.end"                     
## [5] "activity.distanceMeters"           "activity.start"                   
## [7] "activity.topCandidate.type"        "activity.topCandidate.probability"

## Spalten ergänzen

# neue Spalte mit Name der Nutzerin zum später vergleichen nötig
marilen$user <- "Marilen"
celine$user <- "Celine"

## Daten löschen mit NA-Werten
# Wir verwenden nur jene Daten, welche ein Attribut in der Spalte "activity.topCandiate.type" haben

celine2 <- celine |> filter(!is.na(activity.topCandidate.type))

marilen2 <- marilen |>  filter(!is.na(activity.topCandidate.type))

## Koordinaten extrahieren
# Marilen
# Entferne Gradzeichen aus den Spalten "activity.start.latLng" und "activity.end.latLng"

marilen2 <- marilen2 |> 
  mutate(
    activity.start = str_remove_all(activity.start.latLng, "°"), # entfernt Gradzeichen und macht neue Spalte
    activity.end = str_remove_all(activity.end.latLng, "°")) |> 
  select(-activity.start.latLng, -activity.end.latLng)  # entfernt alte Spalte mit Gradzeichen

 # Lat und Long in acitvity.end und activit.start muss auseinander getrennt werden
marilen3 <- marilen2 |> 
  separate(activity.end, into = c("Lat.end", "Long.end"), sep = ",", convert = TRUE)

marilen3 <- marilen3 |> 
  separate(activity.start, into = c("Lat.start", "Long.start"), sep = ",", convert = TRUE)

# Koordinaten in LV95 umwandeln wurde mit Hilfe von ChatGPT gelöst.
# Frage: ich habe einen datensatz als csv in rstudio. in 4 spalten gibt es koordinaten in grad (WGS84). ich möchte diese spalten nun in das LV95 Koordinatensystem umwandeln: Antwort war dieser Code unten

# Zuerst eine sf-Objekt erstellen
# Erstelle zwei sf-Objekte für Punkt 1 und Punkt 2 (jeweils WGS84)
marilen_sf_start <- st_as_sf(marilen3, coords = c("Long.start", "Lat.start"), crs = 4326)
marilen_sf_end <- st_as_sf(marilen3, coords = c("Long.end", "Lat.end"), crs = 4326)

# Transformiere beide Punkte nach LV95 (EPSG:2056)
marilen_lv95_start <- st_transform(marilen_sf_start, crs = 2056)
marilen_lv95_end <- st_transform(marilen_sf_end, crs = 2056)

# Extrahiere die umgewandelten Koordinaten von Punkt 1 und Punkt 2
marilen_coords_start <- st_coordinates(marilen_lv95_start)
marilen_coords_end <- st_coordinates(marilen_lv95_end)

# Füge die umgewandelten Koordinaten zum ursprünglichen Datensatz hinzu
marilen4 <- marilen3 |> 
  mutate(
    ost_start = marilen_coords_start[, 1], nord_start = marilen_coords_start[, 2],
    ost_end = marilen_coords_end[, 1], nord_end = marilen_coords_end[, 2]
  )

# Entferne die WGS84 Spalten
marilen4$Lat.start <- NULL
marilen4$Lat.end <- NULL
marilen4$Long.end <- NULL
marilen4$Long.start <- NULL

# Celine

# Remove "geo:" prefix
 celine2 <- celine |> 
  mutate(activity.end = str_remove(as.character(activity.end), "^geo:"),
         activity.start = str_remove(as.character(activity.start), "^geo:"))

celine2 <- celine2 |> filter(!is.na(activity.topCandidate.type))

 # Lat und Long in acitvity.end und activit.start muss auseinander getrennt werden
celine3 <- celine2 |> 
  separate(activity.end, into = c("Lat.end", "Long.end"), sep = ",", convert = TRUE)

celine3 <- celine3 |> 
  separate(activity.start, into = c("Lat.start", "Long.start"), sep = ",", convert = TRUE)

# Koordinaten in LV95 umwandeln wurde mit Hilfe von ChatGPT gelöst.
# Frage: ich habe einen datensatz als csv in rstudio. in 4 spalten gibt es koordinaten in grad (WGS84). ich möchte diese spalten nun in das LV95 Koordinatensystem umwandeln: Antwort war dieser Code unten

# Zuerst eine sf-Objekt erstellen
# Erstelle zwei sf-Objekte für Punkt 1 und Punkt 2 (jeweils WGS84)
celine_sf_start <- st_as_sf(celine3, coords = c("Long.start", "Lat.start"), crs = 4326)
celine_sf_end <- st_as_sf(celine3, coords = c("Long.end", "Lat.end"), crs = 4326)

# Transformiere beide Punkte nach LV95 (EPSG:2056)
celine_lv95_start <- st_transform(celine_sf_start, crs = 2056)
celine_lv95_end <- st_transform(celine_sf_end, crs = 2056)

# Extrahiere die umgewandelten Koordinaten von Punkt 1 und Punkt 2
celine_coords_start <- st_coordinates(celine_lv95_start)
celine_coords_end <- st_coordinates(celine_lv95_end)

# Füge die umgewandelten Koordinaten zum ursprünglichen Datensatz hinzu
celine4 <- celine3 |> 
  mutate(
    ost_start = celine_coords_start[, 1], nord_start = celine_coords_start[, 2],
    ost_end = celine_coords_end[, 1], nord_end = celine_coords_end[, 2]
  )

# Entferne die WGS84 Spalten
celine4$Lat.start <- NULL
celine4$Lat.end <- NULL
celine4$Long.end <- NULL
celine4$Long.start <- NULL

## Datum Anpassungen
# auf den Zeitraum 19.02.25 bis 26.03.25 filtern + POSIXct anpassen + Wochentage als Spalte ergänzen

# Marilen
marilen4 <- marilen4 |> 
  filter(startTime >= as.Date("2025-02-19") & startTime <= as.Date("2025-03-26"))

# Datum in POSIXct wandeln in zwei Schritten:
# 1. Entfernt ":" in der Zeitzone
marilen4$startTime <- gsub("([+-]\\d{2}):?(\\d{2})$", "\\1\\2", marilen4$startTime)  

marilen4$endTime <- gsub("([+-]\\d{2}):?(\\d{2})$", "\\1\\2", marilen4$endTime)  

# 2. Datum in POSIXct wandeln
marilen4$startTime <- as.POSIXct(marilen4$startTime, format = "%Y-%m-%dT%H:%M:%OS%z", tz = "CET")

marilen4$endTime <- as.POSIXct(marilen4$endTime, format = "%Y-%m-%dT%H:%M:%OS%z", tz = "CET")

# Sprache Deutsch:
Sys.setlocale("LC_TIME", "de_DE.UTF-8")  # für Mac
## [1] "de_DE.UTF-8"

# Wochentage hinzufügen
marilen4$wochentag <- weekdays(marilen4$startTime)

# Celine
celine4 <- celine4 |> 
  filter(startTime >= as.Date("2025-02-19") & startTime <= as.Date("2025-03-26"))

# Datum in POSIXct wandeln in zwei Schritten:
# 1. Entfernt ":" in der Zeitzone
celine4$startTime <- gsub("([+-]\\d{2}):?(\\d{2})$", "\\1\\2", celine4$startTime)  

celine4$endTime <- gsub("([+-]\\d{2}):?(\\d{2})$", "\\1\\2", celine4$endTime)  

# 2. Datum in POSIXct wandeln
celine4$startTime <- as.POSIXct(celine4$startTime, format = "%Y-%m-%dT%H:%M:%OS%z", tz = "CET")

celine4$endTime <- as.POSIXct(celine4$endTime, format = "%Y-%m-%dT%H:%M:%OS%z", tz = "CET")

# Sprache Deutsch:
Sys.setlocale("LC_TIME", "de_DE.UTF-8")  # für Mac
## [1] "de_DE.UTF-8"

# Wochentage hinzufügen
celine4$wochentag <- weekdays(celine4$startTime)

# Dauer in Stunden berechnen
celine4 <- celine4 |> 
  mutate(
    duration_hours = as.numeric(difftime(endTime, startTime, units = "hours"))  # Dauer in Stunden berechnen
  )

marilen4 <- marilen4 |> 
mutate(
    duration_hours = as.numeric(difftime(endTime, startTime, units = "hours"))  # Dauer in Stunden berechnen
  )

# Kontrolle ob die Datensätze gleich sind
# Überprüfen, ob Strukturen gleich sind:
all.equal(names(marilen4), names(celine4))
## [1] "4 string mismatches"
setdiff(names(marilen4), names(celine4))
## character(0)
setdiff(names(marilen4), names(celine4))
## character(0)
colnames(marilen4)
##  [1] "startTime"                         "endTime"                          
##  [3] "activity.distanceMeters"           "activity.probability"             
##  [5] "activity.topCandidate.type"        "activity.topCandidate.probability"
##  [7] "user"                              "ost_start"                        
##  [9] "nord_start"                        "ost_end"                          
## [11] "nord_end"                          "wochentag"                        
## [13] "duration_hours"
colnames(celine4)
##  [1] "endTime"                           "startTime"                        
##  [3] "activity.probability"              "activity.distanceMeters"          
##  [5] "activity.topCandidate.type"        "activity.topCandidate.probability"
##  [7] "user"                              "ost_start"                        
##  [9] "nord_start"                        "ost_end"                          
## [11] "nord_end"                          "wochentag"                        
## [13] "duration_hours"

# Spalten-Reihenfolge bei Celine anpassen da nicht alles indentisch ist 

celine4 <- celine4 |> 
  select(startTime, endTime, activity.distanceMeters, activity.probability, everything())
all.equal(names(marilen4), names(celine4))
## [1] TRUE

# Marilen Datensatz Anpassen: Activity_Type umschreiben, wie bei Celine
marilen4$activity.topCandidate.type = str_to_lower(marilen4$activity.topCandidate.type)  # Alle Aktivitätsnamen in Kleinbuchstaben umwandeln

marilen4$activity.topCandidate.type = str_replace_all(marilen4$activity.topCandidate.type, "_", " ")  # Entfernen der Unterstriche

# Datensätze speichern
write_csv(marilen4, "datensatz_marilen_go.csv")
write_csv(celine4, "datensatz_celine_go.csv")

Abstract

Alltagsmobilität ist ein zentrales Element des täglichen Lebens und gibt Einblick in individuelle Verhaltensmuster sowie gesellschaftliche Strukturen. Zur Analyse von Bewegungsmustern kann die Ähnlichkeit von Trajektorien berechnet werden – eine grundlegende Methode der Bewegungsanalyse (Tao u. a. 2021). Durch die Verfügbarkeit digitaler Bewegungsdaten, wie Google Maps Timeline, ergeben sich neue Möglichkeiten zur detaillierte Analyse solcher Muster. In dieser Projektarbeit wird die Mobilität von zwei Personen über einen Zeitraum von fünf Wochen untersucht. Ziel ist es, mithilfe der Daten von Google Maps Timeline Unterschiede und Gemeinsamkeiten im Mobilitätsverhalten beider Personen zu identifizieren. Die Bewegungsdaten sind bereits in unterschiedliche Transportmittelarten (z.B. Fahrrad, Gehen) klassifiziert.

Die Analyse zeigt einerseits Ähnlichkeiten und Unterschiede der Bewegungsmuster beider Personen über den gesamten Zeitabschnitt auf und andererseits werden Bewegungsmuster unter der Woche, am Wochenende und mittwochs miteinander verglichen. Dazu wird analysiert, wie viel Zeit in welchem Transportmittel verbracht wird, in welchen Tagesabschnitten wer wie viel unterwegs ist und wie die Home Ranges der beiden Personen aussehen. Die Analyse zeigt unter anderem, dass die Anzahl Stunden unterwegs nicht zwingend mit der Grösse der Home Range einhergeht und mit einer Hotspot Analyse besser interpretiert werden kann. Die Zeitspanne der Datenerhebung spielt eine Entscheidende Rolle für die Auswertung der Ergebnisse.

Einleitung

Mobilität ist ein zentraler Bestandteil des menschlichen Alltags und spiegelt individuelle Lebensstile, soziale Routinen und umweltbezogene Entscheidungen wider (Sheller und Urry 2006). Mit der zunehmenden Urbanisierung und Digitalisierung der Welt werden Bewegungsmuster nicht nur komplexer, sondern auch datenbasiert messbar. Digitale Trackingtechnologien, wie beispielsweise Google Maps Timeline, eröffnen neue Möglichkeiten zur Erfassung und systematischen Analyse von Bewegungsdaten im Alltag (Kashifi u. a. 2022). Insbesondere im Bereich der Computational Movement Analysis bieten solche Daten neue Perspektiven zur Erforschung individueller Verhaltensmuster. Studien zeigen, dass Bewegungsprofile Hinweise auf soziale Rhythmen, Routinen und Entscheidungsprozesse geben können (Chen u. a. 2016).

Im Zentrum dieser Projektarbeit steht die Analyse und der Vergleich alltäglicher Bewegungsmuster zweier Personen über einen Zeitraum von fünf Wochen vom 19.02. bis 26.03.2025. Die Daten stammen von Google Maps Timeline (Google LLC n.d.). Google Maps Timeline liefert mit den GPS-Daten eine automatische Vor-Klassifikation der Fortbewegungsarten. Diese vorliegenden Klassifikationen dienen als Ausgangspunkt, werden jedoch hinsichtlich Plausibilität überprüft.

Mit der Analyse der Daten werden individuelle Bewegungsmuster aufgezeigt. Es werden einerseits Ähnlichkeiten zwischen Wochentagen und Wochenenden pro Person und andererseits Ähnlichkeiten der Bewegungsmuster beider Personen herausgearbeitet. Zudem wird in der Arbeit die Home Range der beiden Personen während den fünf Wochen eruiert und verglichen.

Die vorliegende Arbeit verfolgt einen datengetriebenen Ansatz zur Analyse, Klassifikation und Visualisierung von Alltagsmobilität. Die gewonnenen Erkenntnisse sollen nicht nur ein vertieftes Verständnis individueller Bewegungsmuster ermöglichen, sondern auch zur methodischen Diskussion über geeignete Vergleichsverfahren im Kontext raum-zeitlicher Mobilitätsdaten beitragen. Die Projektarbeit richtet sich an Interessierte im Bereich der Computational Movement Analysis, die sich mit Alltagsmobilität im urbanen Kontext befassen.

Forschungsfragen

Folgende Fragen werden in der Arbeit beantwortet:

  1. Wieviel Zeit wird in welcher Fortbewegungsart und in welchem Tagesabschnitt verbracht?

  2. Wie sehen die Home Ranges beider Personen aus und zeigen sich Ähnlichkeiten über den gesamten Zeitraum von fünf Wochen?

  3. Zeigen sich Ähnlichkeiten der Bewegungsmuster unter der Woche, am Wochenende und mittwochs?

  4. Gibt es einen Zusammenhang zwischen Anzahl Stunden unterwegs und der Grösse der Home Range?

Daten

Die in dieser Arbeit verwendeten Bewegungsdaten wurden über einen Zeitraum von fünf Wochen vom 19.02. bis zum 26.03.2025 mithilfe von Google Maps Timeline erhoben. Diese Funktion speichert nebst vielen verschiedenen Daten automatisch Aufenthaltsorte und erkennt Fortbewegungsarten wie Gehen, Fahrradfahren oder die Nutzung öffentlicher Verkehrsmittel, basierend auf GPS-, WLAN- und weiteren Sensordaten. Google Maps Timeline muss auf dem Handy aktiviert und die Daten gespeichert werden, um darauf zugreifen zu können. Die Daten wurden über Google Takeout als JSON-Datei exportiert und für die Analyse bereinigt und transformiert. Der Datensatz von Marilen besteht aus 134 Beobachtungspunkten. Der Datensatz von Céline besteht aus 216 Punkten. Die Analyse wird mit der Software RStudio (RStudio Team 2020) durchgeführt.

Die automatische Erkennung von Transportmitteln und Aufenthaltsorten durch Google Maps Timeline kann gewisse Ungenauigkeiten aufweisen. Eine manuelle Überprüfung oder Korrektur dieser Klassifizierungen wurde nicht vorgenommen, da dies den Rahmen der Arbeit überschritten hätte. Diese potenzielle Einschränkung wird bei der Interpretation der Ergebnisse berücksichtigt.

Methoden

Datenvorverarbeitung

Für die Datenvorverarbeitung wurden zwei JSON-Datensätze eingelesen, die Bewegungs- und Aufenthaltsinformationen von zwei Nutzerinnen enthalten. Die Daten stammen aus exportierten Zeitachsen von Google Maps Timeline und wurden mittels fromJSON() in R importiert und wie folgt aufbereitet:

Zunächst wurden aus den verschachtelten Datensätzen die relevanten Daten entnommen und daraus zwei neue, klar strukturierte Datensätze erstellt. Danach wurde jeder Nutzerin eine eigene Kennzeichnung mittels einer neuen Spalte (user) zugewiesen. Es wurden nur Daten mit gültigem Aktivitätstyp (activity.topCandidate.type) berücksichtigt. Geokoordinaten im WGS84-Format (Latitude, Longitude) wurden bereinigt und mithilfe des sf-Pakets ins schweizerische LV95-Koordinatensystem (EPSG:2056) transformiert.

Die Zeitstempel der Aktivitäten wurden standardisiert und in das POSIXct-Format umgewandelt. Der Analysezeitraum wurde auf den 19. Februar bis 26. März 2025 eingeschränkt und mit den Wochentagen ergänzt. Abschliessend wurde geprüft, ob beide Datensätze die gleiche Struktur aufweisen, um eine vergleichende Analyse zu ermöglichen. Die bereinigten und harmonisierten Datensätze wurden als CSV-Dateien gespeichert.

Tagesabschnitte

Für die Analyse der verbrachten Stunden pro Tagesabschnitt wurden die Daten beider Datensätze dem jeweiligen Tagesabschnitte zugeordnet. Die Einteilung der Tagesabschnitte erfolgte gemäss folgendem Schema:

  • Früh: 5:00 – 9:00 Uhr

  • Morgen: 9:00 – 11:00 Uhr

  • Mittag: 11:00 – 14:00 Uhr

  • Nachmittag: 14:00 – 17:00 Uhr

  • Abend: 17:00 – 20:00 Uhr

  • Nacht: 20:00 – 5:00 Uhr

Die Zuweisung der Tagesabschnitte wurde mit der Funktion mutate() aus dem dplyr-Paket durchgeführt. Dabei wurde die Stunde aus der Variable startTime extrahiert, und mit case_when() die jeweilige Aktivität einem der sechs definierten Tagesabschnitte zugeordnet. Die beiden Dataframes für Céline und Marilen wurden mit der Funktion bind_rows() in einen Dataframe zusammengeführt. Für jeden Tagesabschnitt pro Person wurde in der Datenvorverarbeitung die Dauer in Stunden berechnet. Im kombinierten Datensatz wurde die Summe der Stunden pro Tagesabschnitt und Person berechnet. Das Ergebnis wurde in einer neuen Spalte dauer_stunden gespeichert. Um die Tagesabschnitte in einer logischen Reihenfolge auf der x-Achse darzustellen, wurde die Spalte Tagesabschnitt in einen Faktor umgewandelt. Dabei wurde die Reihenfolge der Levels explizit festgelegt, sodass die Tagesabschnitte korrekt nach Früh, Morgen, Mittag, Nachmittag, Abend und Nacht sortiert wurden.

Transportmittel

Die Analyse der verbrachten Zeit pro Transportmittel erfolgte in mehreren Schritten. Zunächst wurden die Daten nach Transportmittel gruppiert, um die Gesamtstunden pro Transportmittel zu berechnen. Dafür wurde die duration_hours für jedes Transportmittel summiert.

Danach wurden die Daten für Werktage (Montag bis Freitag) gefiltert, um die Gesamtzeit pro Transportmittel für jeden Tag zu ermitteln. Die gleiche Berechnung erfolgte für das Wochenende (Samstag und Sonntag). Zusätzlich wurde für den Mittwoch, an dem die Vorlesung Patterns and Trends in Environmental Data stattfand, die durchschnittliche Zeit pro Transportmittel berechnet.

Zur Berechnung der durchschnittlichen Aufenthaltsdauer pro betrachteten Zeitraum wurden die Daten im Datensatz combined_tage entsprechend gefiltert. Nach der Gruppierung nach Benutzer, Transportmittel und Datum (as.Date(startTime)) wurde die Gesamtzeit pro Tag (daily_total_hours) berechnet und zur Bestimmung des Durchschnitts der Stunden pro Transportmittel verwendet. Der Datensatz combined_tage enthält alle relevanten Informationen zu den Transportmitteln und Zeitdauern. Der verwendete Code nutzt group_by(), summarise() und arrange() aus der dplyr-Bibliothek.

Home Range

Zur Analyse der räumlichen Bewegungsmuster der beiden Nutzerinnen wurde die Methode der Convex Hull (konvexe Hülle) verwendet, um die sogenannten Home Ranges zu bestimmen. Die Berechnungen wurden mit den Paketen sf, dplyr und ggplot2 durchgeführt. Zunächst wurden die Datensätze einzeln eingelesen und in sf-Objekte umgewandelt. Für beide Nutzerinnen wurde auf Basis der GPS-Startpunkte jeweils eine Convex Hull berechnet, um den individuellen Home Range darzustellen. Dies erfolgte mittels der Funktionen st_union() zur geometrischen Verschmelzung der Punkte pro Nutzerin und st_convex_hull() zur Erzeugung der Convex Hull.

Im Anschluss wurden die beiden Datensätze zu einem gemeinsamen sf-Objekt zusammengeführt. Zur weiteren Analyse wurden zusätzliche Variablen wie der Wochentag, ein Wochentagstyp (Wochenende/Werktag) sowie eine Variable für Mittwoch ergänzt. Danach erfolgte eine Gruppierung nach Nutzerin zur kombinierten Darstellung beider Home Ranges in einem Plot. Für den räumlichen Kontext wurden ausgewählte Städte aus der Schweiz in einem separaten Datensatz mit WGS84-Koordinaten erfasst un in dasselbe Koordinatensystem (LV95) transformiert. Die Visualisierung erfolgte mit ggplot2, wobei die Home Ranges farblich nach Nutzerin differenziert dargestellt wurden. Zusätzlich wurden die Städte als Punkte eingeblendet und mit Namen beschriftet.

Kernel Dichte Schätzung

Der Datensatz einer Person beinhaltet GPS-Daten einer Urlaubsreise, was die Ergebnisse in Bezug auf Alltagsmobilität verzerrt. Aus diesem Grund wurde zusätzlich eine bereinigte Kernel-Dichte-Schätzung (KDE) berechnet. Die Analyse erfolgte unter Verwendung einer Bandbreite von 10’000 Metern und einer Zellgröße von 1’000 Metern. Für die Visualisierung wurden nur Werte oberhalb des 95. Perzentils berücksichtigt und logarithmiert dargestellt, um extreme Dichteunterschiede besser sichtbar zu machen.

Plotly

Für die interaktive 3D Visualisierung der Trajektorien beider Nutzerinnen wurde das plotly-Paket verwendet. Die Datensätze von Céline und Marilen wurden durch einen Fuzzy Join (difference_inner_join()) kombiniert, um Treffpunkte innerhalb eines maximalen Abstands von 300 Metern zu identifizieren. Für die Identifikation von Treffpunkten wurde die Euklidische Distanz mit der Funktion euclidean_distance() zwischen den GPS-Koordinaten beider Personen berechnet. Dabei wurde geprüft, ob der Abstand zwischen den Personen innerhalb eines Radius von 100 Metern lag, was als Kriterium für ein Treffen definiert wurde.

Tagesabschnitte
# Tagesabschnitte
# berechnen und vergleichen der täglichen Bewegungsmuster von Celine und Marilen

## Daten einlesen
marilen <- read_csv("Quarto/datensaetze/datensatz_marilen_go.csv")

celine <- read_csv("Quarto/datensaetze/datensatz_celine_go.csv")

## 1. Datensatz einteilen nach Tagesabschnitt -> neue Spalte
# Früh: 5-9
# Morgen: 9-11
# Mittag: 11-14 
# Nachmittag: 14-17
# Abend: 17-20 
# Nacht: 20-5 

# Celine
# Tagesabschnitt zuweisen
celine_tage <- celine |> 
  mutate(
    Stunde = as.integer(format(startTime, "%H")),  # Extrahiert die Stunde als Zahl
    Tagesabschnitt = case_when(
      Stunde >= 5  & Stunde < 9  ~ "Früh",
      Stunde >= 9  & Stunde < 11 ~ "Morgen",
      Stunde >= 11 & Stunde < 14 ~ "Mittag",
      Stunde >= 14 & Stunde < 17 ~ "Nachmittag",
      Stunde >= 17 & Stunde < 20 ~ "Abend",
      TRUE ~ "Nacht"  # Fängt alles andere (20-5) als Nacht auf
    )
  )

# Plot mit den Anzahl Aktivitäten pro Tagesabschnitt -> wird nicht dargestellt, da nicht genützt
# ggplot(celine_tage, aes(x = Tagesabschnitt)) +
  # geom_bar(fill = "steelblue", color = "black") +
  # labs(title = "Häufigkeit der Tagesabschnitte", x = "Tagesabschnitt", y = "Anzahl") +
  # theme_minimal()

# Marilen
# Tagesabschnitt zuweisen
marilen_tage <- marilen |> 
  mutate(
    Stunde = as.integer(format(startTime, "%H")),  # Extrahiert die Stunde als Zahl
    Tagesabschnitt = case_when(
      Stunde >= 5  & Stunde < 9  ~ "Früh",
      Stunde >= 9  & Stunde < 11 ~ "Morgen",
      Stunde >= 11 & Stunde < 14 ~ "Mittag",
      Stunde >= 14 & Stunde < 17 ~ "Nachmittag",
      Stunde >= 17 & Stunde < 20 ~ "Abend",
      TRUE ~ "Nacht"  # Fängt alles andere (20-5) als Nacht auf
    )
  )

# Plot mit den Anzahl Aktivitäten pro Tagesabschnitt -> wird nicht dargestellt, da nicht genützt
# ggplot(marilen_tage, aes(x = Tagesabschnitt)) +
  # geom_bar(fill = "steelblue", color = "black") +
  # labs(title = "Häufigkeit der Tagesabschnitte", x = "Tagesabschnitt", y = "Anzahl") +
  # theme_minimal()

# Datensatz verbinden zum Vergleich
combined_tage <- bind_rows(celine_tage, marilen_tage)

colnames(combined_tage)
##  [1] "startTime"                         "endTime"                          
##  [3] "activity.distanceMeters"           "activity.probability"             
##  [5] "activity.topCandidate.type"        "activity.topCandidate.probability"
##  [7] "user"                              "ost_start"                        
##  [9] "nord_start"                        "ost_end"                          
## [11] "nord_end"                          "wochentag"                        
## [13] "duration_hours"                    "Stunde"                           
## [15] "Tagesabschnitt"

# Daten darstellen
stunden_pro_abschnitt <- combined_tage |> 
  group_by(user, Tagesabschnitt) |> 
  summarise(dauer_stunden = sum(duration_hours, na.rm = TRUE))

# Sortieren nach richtiger Tagesreihenfolge
stunden_pro_abschnitt$Tagesabschnitt <- factor(
  stunden_pro_abschnitt$Tagesabschnitt,
  levels = c("Früh", "Morgen", "Mittag", "Nachmittag", "Abend", "Nacht")
)
Transportmittel
# Transportmittel verbrachte Stunden darin berechnen
# Berechnung über den gesamten Datensatz
# Summe der Zeit pro Transportmittel
# Celine

celine_types_summary <- celine |> 
  group_by(user, activity.topCandidate.type) |>   # Gruppieren nach Person & Verkehrsmittel
  summarise(total_hours = sum(duration_hours, na.rm = TRUE), .groups = "drop") |> 
  arrange(user, desc(total_hours))  # Sortierung nach Person & Dauer

# Ergebnis anzeigen
# print(celine_types_summary)

# Marilen
marilen_types_summary <- marilen |> 
  group_by(user, activity.topCandidate.type) |>   # Gruppieren nach Person & Verkehrsmittel
  summarise(total_hours = sum(duration_hours, na.rm = TRUE), .groups = "drop") |> 
  arrange(user, desc(total_hours))  # Sortierung nach Person & Dauer

# Ergebnis anzeigen
# print(marilen_types_summary)

# Berechnung für Montag-Freitag
# Berechnung der täglichen durchschnittlichen Zeit pro Verkehrsmittel

# Filtern nach Montag bis Freitag (Werktage)
combined_weekdays <- combined_tage |> 
  filter(wochentag %in% c("Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"))

combined_daily_summary <- combined_weekdays |> 
  group_by(user, activity.topCandidate.type, as.Date(startTime)) |>  # Gruppieren nach Tag und Transportmittel
  summarise(daily_total_hours = sum(duration_hours, na.rm = TRUE), .groups = "drop")  # Stunden pro Tag summieren

# Berechnung des Durchschnitts der Stunden pro Transportmittel über die Werktage
average_hours_per_transport_weekdays <- combined_daily_summary |> 
  group_by(user, activity.topCandidate.type) |>  # Gruppieren nach Transportmittel
  summarise(average_hours = mean(daily_total_hours, na.rm = TRUE))  # Mittelwert der Stunden berechnen

# Ergebnis anzeigen
# print(average_hours_per_transport_weekdays)

# Berechnung für Samstag-Sonntag
# Mittelwert der Zeit pro Transportmittel
# Filtern nach Samstag bis Sonntag (Wochenende)
combined_weekends <- combined_tage |> 
  filter(wochentag %in% c("Samstag", "Sonntag"))

combined_weekend_summary <- combined_weekends |> 
  group_by(user, activity.topCandidate.type, as.Date(startTime)) |>  # Gruppieren nach Tag und Transportmittel
  summarise(weekend_total_hours = sum(duration_hours, na.rm = TRUE), .groups = "drop")  # Stunden pro Tag summieren

# Berechnung des Durchschnitts der Stunden pro Transportmittel über das Wochenende
average_hours_per_transport_weekend <- combined_weekend_summary |> 
  group_by(user, activity.topCandidate.type) |>  # Gruppieren nach Transportmittel
  summarise(average_hours = mean(weekend_total_hours, na.rm = TRUE))  # Mittelwert der Stunden berechnen

# Ergebnis anzeigen
# print(average_hours_per_transport_weekend)

# Berechnung für Mittwoch (Patterns and Trends Vorlesung)
# Mittelwert der Zeit pro Transportmittel
# Filtern nach Mittwoch
combined_wednesday <- combined_tage |> 
  filter(wochentag %in% c("Mittwoch"))

combined_wednesday_summary <- combined_wednesday |> 
  group_by(user, activity.topCandidate.type, as.Date(startTime)) |>  # Gruppieren nach Tag und Transportmittel
  summarise(wednesday_total_hours = sum(duration_hours, na.rm = TRUE), .groups = "drop")  # Stunden pro Tag summieren

# Berechnung des Durchschnitts der Stunden pro Transportmittel am Mittwoch
average_hours_per_transport_wednesday <- combined_wednesday_summary |> 
  group_by(user, activity.topCandidate.type) |>  # Gruppieren nach Transportmittel
  summarise(average_hours = mean(wednesday_total_hours, na.rm = TRUE))  # Mittelwert der Stunden berechnen

# Ergebnis anzeigen
# print(average_hours_per_transport_wednesday)
Home Range
# Daten einlesen
marilen <- read_csv("datensatz_marilen_go.csv")
celine <- read_csv("datensatz_celine_go.csv")
# user-Spalte auf "celine" setzen, da das é Probleme beim Code bereitet
celine$user <- "Celine"

# marilen
# In sf-Objekt umwandeln
marilen_sf <- st_as_sf(marilen, coords = c("ost_start", "nord_start"), crs = 2056)

# Plot wird hier nicht dargestellt, steht zur Vollständigkeit hier
# plot(st_geometry(marilen_sf))
homerange_marilen <- st_convex_hull(st_union(marilen_sf))
# Plot: Homerange drüberlegen
# plot(homerange_marilen, add = TRUE, border = "red", lwd = 2)

# celine
# 2. In sf-Objekt umwandeln
celine_sf <- st_as_sf(celine, coords = c("ost_start", "nord_start"), crs = 2056)
# plot(st_geometry(celine_sf))
homerange_celine <- st_convex_hull(st_union(celine_sf))
# # Plot: Homerange drüberlegen
# plot(homerange_celine, add = TRUE, border = "red", lwd = 2)

# Datensätze  zusammenführen
# Beide zusammenführen
datensatz_gesamt <- bind_rows(marilen, celine)

str(datensatz_gesamt)
## spc_tbl_ [350 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ startTime                        : POSIXct[1:350], format: "2025-02-19 10:47:21" "2025-02-19 11:17:15" ...
##  $ endTime                          : POSIXct[1:350], format: "2025-02-19 11:06:05" "2025-02-19 11:43:42" ...
##  $ activity.distanceMeters          : num [1:350] 1592 22747 135058 81099 0 ...
##  $ activity.probability             : num [1:350] 0.996 0.996 0.999 0.999 0.894 ...
##  $ activity.topCandidate.type       : chr [1:350] "walking" "in train" "in train" "in train" ...
##  $ activity.topCandidate.probability: num [1:350] 0.98 0.945 0.996 0.983 0.454 ...
##  $ user                             : chr [1:350] "Marilen" "Marilen" "Marilen" "Marilen" ...
##  $ ost_start                        : num [1:350] 2693994 2693572 2683014 2599952 2634159 ...
##  $ nord_start                       : num [1:350] 1230521 1231785 1248103 1199750 1127066 ...
##  $ ost_end                          : num [1:350] 2693652 2683097 2599912 2634172 2634159 ...
##  $ nord_end                         : num [1:350] 1231596 1248046 1199741 1127137 1127066 ...
##  $ wochentag                        : chr [1:350] "Mittwoch" "Mittwoch" "Mittwoch" "Mittwoch" ...
##  $ duration_hours                   : num [1:350] 0.312 0.441 0.867 0.587 0.406 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   startTime = col_datetime(format = ""),
##   ..   endTime = col_datetime(format = ""),
##   ..   activity.distanceMeters = col_double(),
##   ..   activity.probability = col_double(),
##   ..   activity.topCandidate.type = col_character(),
##   ..   activity.topCandidate.probability = col_double(),
##   ..   user = col_character(),
##   ..   ost_start = col_double(),
##   ..   nord_start = col_double(),
##   ..   ost_end = col_double(),
##   ..   nord_end = col_double(),
##   ..   wochentag = col_character(),
##   ..   duration_hours = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>

# Datensatz gesamt in sf Objekt umwandeln für die Berechnung der Convex Hull
# In sf-Objekt umwandeln:
datensatz_gesamt_sf <- st_as_sf(
  datensatz_gesamt,
  coords = c("ost_start", "nord_start"),  # deine Spalten mit den Start-Koordinaten
  crs = 2056                              # LV95 Projektion (Schweiz)
)

# Variable wochentag und eine Variable wochentags_typ hinzufügen
datensatz_gesamt_sf$wochentag <- weekdays(datensatz_gesamt$startTime)

datensatz_gesamt_sf$wochentags_typ <- ifelse(datensatz_gesamt$wochentag %in% c("Samstag", "Sonntag"), "Wochenende", "Werktag")

datensatz_gesamt_sf$mittwoch <- datensatz_gesamt_sf$wochentag == "Mittwoch"

# Homerange pro Person berechnen
# Gruppierung nach user
datensatz_gesamt_grouped <- group_by(datensatz_gesamt_sf, user)

# Geometrien für die beiden user Gruppen kombinieren
datensatz_gesamt_smry <- summarise(datensatz_gesamt_grouped, geometry = st_combine(geometry), .groups = "drop")

# Berechnung des Convex Hulls für beide Gruppen
mcp_celine_marilen <- st_convex_hull(datensatz_gesamt_smry)

# Plot wird hier nicht dargestellt, steht zur Vollständigkeit hier
# Plotten des Convex Hulls für beide Gruppen (celine und marilen)
# ggplot(mcp_celine_marilen, aes(fill = user)) +
#   geom_sf(alpha = 0.4) +
#   coord_sf(datum = 2056)

# Plot
# Städtekoordinaten im WGS84 (EPSG:4326)
city_coords <- data.frame(
  city = c("Zürich", "Basel", "Wädenswil", "Brig", "Uster", "Bern", "Näfels", "Zeiningen", "Aeschi", "Innertkirchen"),
  lat = c(47.3784, 47.5596, 47.2220, 46.3176, 47.3472, 46.9481, 47.0962, 47.5452, 46.6611, 46.7712),
  lon = c(8.5400, 7.5886, 8.6535, 7.9895, 8.7206, 7.4474, 9.0650, 7.9293, 7.6692, 8.3875)
)

# Städte in ein sf-Objekt umwandeln (mit WGS84 als Start-CRS)
city_coords_sf <- st_as_sf(city_coords, coords = c("lon", "lat"), crs = 4326)

# Jetzt in LV95 (EPSG:2056) transformieren
city_coords_sf_lv95 <- st_transform(city_coords_sf, crs = 2056)

# Plot wird hier nicht dargestellt, steht zur Vollständigkeit hier
# ggplot(mcp_celine_marilen, aes(fill = user)) +
#   geom_sf(alpha = 0.4) +
#   geom_sf(
#     data = city_coords_sf_lv95,
#     color = "black",
#     size = 2,
#     inherit.aes = FALSE  # <- HIER wichtig!
#   ) +
#   geom_text(
#     data = city_coords_sf_lv95,
#     aes(label = city, geometry = geometry),
#     stat = "sf_coordinates",
#     size = 4,
#     fontface = "bold",
#     vjust = -1,
#     hjust = 0.5,
#     inherit.aes = FALSE  # <- Auch hier schon korrekt!
#   ) +
#   coord_sf(datum = 2056) +
#   labs(
#     title = "Home Ranges von Celine und Marilen",
#     subtitle = "19.2. - 26.3.2025",
#     caption = "Datenquelle: Eigene Daten",
#     fill = "User",
#     x = "Ost (Meter)",
#     y = "Nord (Meter)"
#   ) +
#   theme_minimal() +
#   theme(
#     plot.title = element_text(size = 16, face = "bold", hjust = 0.5),  # Titel zentrieren und fett
#     plot.subtitle = element_text(size = 12, hjust = 0.5),  # Untertitel zentrieren
#     axis.title.x = element_text(size = 12),
#     axis.title.y = element_text(size = 12),
#     legend.title = element_text(size = 12),
#     legend.text = element_text(size = 10)
#   )
# 
# ggsave("convex_hull_celine_marilen_1.png")
Kernel Dichte Schätzung
# Kernel Density Analyse Celine und Marilen
needed_packages <- c("sf", "spatstat.geom", "spatstat.explore", "dplyr", "ggplot2", "viridis")

for (pkg in needed_packages) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    install.packages(pkg)
  }
}

lapply(needed_packages, library, character.only = TRUE)
## [[1]]
##  [1] "patchwork"         "viridis"           "viridisLite"      
##  [4] "spatstat.explore"  "nlme"              "spatstat.random"  
##  [7] "spatstat.geom"     "spatstat.univar"   "spatstat.data"    
## [10] "devtools"          "usethis"           "rnaturalearthdata"
## [13] "rnaturalearth"     "mapview"           "fuzzyjoin"        
## [16] "plotly"            "wordcountaddin"    "tidyr"            
## [19] "stringr"           "jsonlite"          "lubridate"        
## [22] "sf"                "readr"             "dplyr"            
## [25] "ggplot2"           "pacman"            "stats"            
## [28] "graphics"          "grDevices"         "utils"            
## [31] "datasets"          "methods"           "base"             
## 
## [[2]]
##  [1] "patchwork"         "viridis"           "viridisLite"      
##  [4] "spatstat.explore"  "nlme"              "spatstat.random"  
##  [7] "spatstat.geom"     "spatstat.univar"   "spatstat.data"    
## [10] "devtools"          "usethis"           "rnaturalearthdata"
## [13] "rnaturalearth"     "mapview"           "fuzzyjoin"        
## [16] "plotly"            "wordcountaddin"    "tidyr"            
## [19] "stringr"           "jsonlite"          "lubridate"        
## [22] "sf"                "readr"             "dplyr"            
## [25] "ggplot2"           "pacman"            "stats"            
## [28] "graphics"          "grDevices"         "utils"            
## [31] "datasets"          "methods"           "base"             
## 
## [[3]]
##  [1] "patchwork"         "viridis"           "viridisLite"      
##  [4] "spatstat.explore"  "nlme"              "spatstat.random"  
##  [7] "spatstat.geom"     "spatstat.univar"   "spatstat.data"    
## [10] "devtools"          "usethis"           "rnaturalearthdata"
## [13] "rnaturalearth"     "mapview"           "fuzzyjoin"        
## [16] "plotly"            "wordcountaddin"    "tidyr"            
## [19] "stringr"           "jsonlite"          "lubridate"        
## [22] "sf"                "readr"             "dplyr"            
## [25] "ggplot2"           "pacman"            "stats"            
## [28] "graphics"          "grDevices"         "utils"            
## [31] "datasets"          "methods"           "base"             
## 
## [[4]]
##  [1] "patchwork"         "viridis"           "viridisLite"      
##  [4] "spatstat.explore"  "nlme"              "spatstat.random"  
##  [7] "spatstat.geom"     "spatstat.univar"   "spatstat.data"    
## [10] "devtools"          "usethis"           "rnaturalearthdata"
## [13] "rnaturalearth"     "mapview"           "fuzzyjoin"        
## [16] "plotly"            "wordcountaddin"    "tidyr"            
## [19] "stringr"           "jsonlite"          "lubridate"        
## [22] "sf"                "readr"             "dplyr"            
## [25] "ggplot2"           "pacman"            "stats"            
## [28] "graphics"          "grDevices"         "utils"            
## [31] "datasets"          "methods"           "base"             
## 
## [[5]]
##  [1] "patchwork"         "viridis"           "viridisLite"      
##  [4] "spatstat.explore"  "nlme"              "spatstat.random"  
##  [7] "spatstat.geom"     "spatstat.univar"   "spatstat.data"    
## [10] "devtools"          "usethis"           "rnaturalearthdata"
## [13] "rnaturalearth"     "mapview"           "fuzzyjoin"        
## [16] "plotly"            "wordcountaddin"    "tidyr"            
## [19] "stringr"           "jsonlite"          "lubridate"        
## [22] "sf"                "readr"             "dplyr"            
## [25] "ggplot2"           "pacman"            "stats"            
## [28] "graphics"          "grDevices"         "utils"            
## [31] "datasets"          "methods"           "base"             
## 
## [[6]]
##  [1] "patchwork"         "viridis"           "viridisLite"      
##  [4] "spatstat.explore"  "nlme"              "spatstat.random"  
##  [7] "spatstat.geom"     "spatstat.univar"   "spatstat.data"    
## [10] "devtools"          "usethis"           "rnaturalearthdata"
## [13] "rnaturalearth"     "mapview"           "fuzzyjoin"        
## [16] "plotly"            "wordcountaddin"    "tidyr"            
## [19] "stringr"           "jsonlite"          "lubridate"        
## [22] "sf"                "readr"             "dplyr"            
## [25] "ggplot2"           "pacman"            "stats"            
## [28] "graphics"          "grDevices"         "utils"            
## [31] "datasets"          "methods"           "base"

my_kde <- function(points, cellsize, bandwidth, extent = NULL){
  points_ppp <- as.ppp(points)
  if(!is.null(extent)){
    Window(points_ppp) <- as.owin(st_bbox(extent))
  }
  points_density <- density.ppp(x = points_ppp, sigma = bandwidth, eps = cellsize)
  points_density_df <- as.data.frame(points_density)
  return(points_density_df)
}

# Landesgrenzen
# Falls du die Schweizer Landesgrenzen nochmal überprüfst:
swiss_border <- ne_countries(scale = "medium", country = "Switzerland", returnclass = "sf")
swiss_border <- st_transform(swiss_border, crs = 2056)  # Transformieren in EPSG:2056

marilen_kde <- my_kde(points = marilen_sf, cellsize = 1000, bandwidth = 10000, extent = swiss_border)
celine_kde  <- my_kde(points = celine_sf,  cellsize = 1000, bandwidth = 10000, extent = swiss_border)

# Marilen
q95 <- quantile(marilen_kde$value, probs = 0.95)

marilen_kde <- marilen_kde %>%
  mutate(
    value_new = ifelse(value > q95, value, NA),
    value_new = log10(value_new)
  )

# # ggplot() +
#   geom_raster(data = marilen_kde, aes(x, y, fill = value_new)) +
#   geom_sf(data = swiss_border, inherit.aes = FALSE, fill = NA) +
#   scale_fill_viridis_c(na.value = NA) +
#   theme_void()

# Celine
q95_celine <- quantile(celine_kde$value, probs = 0.95)

celine_kde <- celine_kde %>%
  mutate(
    value_new = ifelse(value > q95_celine, value, NA),
    value_new = log10(value_new)
  )

# ggplot() +
#   geom_raster(data = celine_kde, aes(x, y, fill = value_new)) +
#   geom_sf(data = swiss_border, inherit.aes = FALSE, fill = NA) +
#   scale_fill_viridis_c(na.value = NA) +
#   theme_void()

# plots perfektionieren:
# Plot für Marilen
plot_marilen <- ggplot() +
  geom_raster(data = marilen_kde, aes(x, y, fill = value_new)) +
  geom_sf(data = swiss_border, inherit.aes = FALSE, fill = NA) +
  scale_fill_viridis_c(na.value = NA, name = "Kernel-Dichte\n(log-transformiert)") +
  ggtitle("Marilen") +  # Titel für Marilen
  theme_void()

# Plot für Celine
plot_celine <- ggplot() +
  geom_raster(data = celine_kde, aes(x, y, fill = value_new)) +
  geom_sf(data = swiss_border, inherit.aes = FALSE, fill = NA) +
  scale_fill_viridis_c(na.value = NA, name = "Kernel-Dichte\n(log-transformiert)") +
  ggtitle("Céline") +  # Titel für Celine
  theme_void()
Plotly
# Datensatz verbinden (combine_tage) und Euklidische Distanz berechnen
# Datensätze verbinden

# Fuzzy Join nach Zeit (innerhalb von 5 Minuten)
celine_marilen_combined <- difference_inner_join(
  celine, marilen,
  by = "nord_start",
  max_dist = 300,  # 300 m maximaler Unterschied
  distance_col = "dist_diff"
)

# Spalten umbenennen: _Celine und _Marilen anfügen
colnames(celine_marilen_combined) <- gsub("\\.x$", "_Celine", colnames(celine_marilen_combined))
colnames(celine_marilen_combined) <- gsub("\\.y$", "_Marilen", colnames(celine_marilen_combined))

# Funktion für Euclidean Distance
euclidean_distance <- function(celine_marilen_combined) {
  sqrt(
    (celine_marilen_combined$ost_start_Celine - celine_marilen_combined$ost_start_Marilen)^2 + 
    (celine_marilen_combined$nord_start_Celine - celine_marilen_combined$nord_start_Marilen)^2
  )
}

# Anwenden der Funktion auf den Datensatz celine_marilen
celine_marilen_combined$Eu_distance <- euclidean_distance(celine_marilen_combined)

# Treffen ja oder nein
celine_marilen_combined <- celine_marilen_combined |> 
  mutate(
    is_meeting = Eu_distance <= 100 # Wahr, wenn die Distanz 100 m oder kleiner ist
  )

# Datensatz, wenn sich C und M treffen wahr ist
meets_data <- celine_marilen_combined |> filter(is_meeting == TRUE)

# Plot wird hier nicht dargestellt, steht zur Vollständigkeit hier
# ggplot für Aufenthaltsorte und Treffen beider Personen
# ggplot() +
#   geom_point(data = celine, aes(ost_start,nord_start, colour = "Celine"), alpha = 0.2) +
#   geom_point(data = marilen, aes(ost_start,nord_start, colour = "Marilen"), alpha = 0.2) +
#   geom_point(data = meets_data, aes(x = ost_start_Celine, y = nord_start_Celine, colour = "Meets"), shape = 21, fill = "NA", size = 3, stroke = 1) +
#    scale_color_manual(values = c("Celine" = "red", "Marilen" = "cyan", "Meets" = "black")) +
#   labs(colour = "Legende", x = "Ost-Koordinaten", y = "Nord-Koordinaten") +
#   theme_minimal()

Resultate

Tagesabschnitte

Die Auswertung der Aktivitäten zu unterschiedlichen Tagesabschnitten zeigt deutliche Unterschiede im Bewegungsverhalten der beiden Nutzerinnen über den Tagesverlauf hinweg (Abbildung 1). Céline war vorallem am Nachmittag aktiv (32.1 h), gefolgt von der Mittagszeit (17.7 h) und dem Abend (13.8 h). Ihre geringste Aktivität zeigt sich in der Nacht (6.75 h). Marilen zeigt ein generell kürzeres Bewegungsmuster. Ihre aktivsten Tagesabschnitte waren der Mittag (15.1 h) und der Nachmittag (7.82 h), während sie am Abend (2.35 h) und in der Nacht (1.56 h) wenig mobil war.

Der direkte Vergleich verdeutlicht, dass Céline ein insgesamt höheres Aktivitätsniveau aufweist und ihre Aktivitäten gleichmässiger über den Tag verteilt sind. Marilen’s Mobilität hingegen konzentriert sich stärker auf die Mittagszeit.

Transportmittel Resultate Anzahl Stunden
# Plot mit Anzahl Stunden pro Tagesabschnitt
ggplot(stunden_pro_abschnitt, aes(x = Tagesabschnitt, y = dauer_stunden, fill = user)) +
  geom_bar(stat = "identity", position = "dodge") +  # nebeneinander statt gestapelt
  labs(
       x = "Tagesabschnitt", y = "Anzahl Stunden", fill = "Nutzerin") +
  theme_minimal() +
  theme(axis.text = element_text(size = 12),
        plot.title = element_text(face = "bold", hjust = 0.5))
Abbildung 1: Darstellung der Anzahl Stunden pro Tagesabschnitt für Céline (rot) und Marilen (türkis)

Transportmittel

Die Analyse der verbrachten Zeit pro Transportmittel zeigt deutliche Unterschiede im Mobilitätsverhalten der beiden Nutzerinnen (Abbildung 2). Insgesamt war Céline deutlich länger unterwegs als Marilen. Besonders hervor sticht ihre Nutzung des Zuges mit insgesamt 32.8 Stunden sowie das Gehen mit 36.1 Stunden. Marilen hingegen verbrachte 12.3 Stunden im Zug und 9.3 Stunden zu Fuss.

Ein umgekehrtes Bild zeigt sich in der Nutzung des Fahrrads. Während Marilen insgesamt 4.5 Stunden mit dem Fahrrad unterwegs war, nutzte Céline dieses Transportmittel nur für 0.9 Stunden.

Transportmittel Resultate Anzahl gesamte Zeitspanne
# Plot: Anzahl Stunden pro TM pro Person für die gesamte Zeitspanne 
# Daten einzeln verbinden in 1 Datensatz
celine_marilen_types_summary <- bind_rows(celine_types_summary, marilen_types_summary)

ggplot(celine_marilen_types_summary, aes(x = activity.topCandidate.type, y = total_hours, fill = user)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(
    x = "Transportmittel",
    y = "Anzahl Stunden",
    fill = "Nutzerin"
  ) +
  theme_minimal() +
  theme(
    axis.text = element_text(size = 12),
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),
  )
Abbildung 2: Darstellung der kumulierten Anzahl Stunden pro Transportmittel für Céline (rot) und Marilen (türkis)

Die Analyse der durchschnittlich verbrachten Zeit in verschiedenen Verkehrsmitteln an Werktagen (Montag bis Freitag) zeigt Unterschiede zwischen den beiden Nutzerinnen (Abbildung 3). Céline verbrachte im Vergleich zu Marilen deutlich mehr Zeit im Zug, mit durchschnittlich über 2.6 Stunden werktags. Im Gegensatz dazu war Marilen deutlich länger mit dem Fahrrad unterwegs (0.8 h) als Céline (0.2 h). Bei der Busnutzung zeigte sich, dass Céline den Bus im Durchschnitt länger nutzte (0.5 h) als Marilen (0.2 h). Im Gegensatz dazu fuhr Marilen länger mit dem Tram, mit einem Durchschnitt von 0.7 Stunden werktags, während Céline nur 0.4 Stunden im Tram verbrachte.

Transportmittel Resultate Montag-Freitag
# Plot: Anzahl Stunden pro TM pro Person für die Mo-Fr
ggplot(average_hours_per_transport_weekdays, aes(x = activity.topCandidate.type, y = average_hours, fill = user)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(
    x = "Transportmittel",
    y = "Durchschnitt der Anzahl Stunden",
    fill = "Nutzerin"
  ) +
  theme_minimal() +
  theme(
    axis.text = element_text(size = 12),
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),
  )
Abbildung 3: Darstellung der durchschnittlichen Stundenzahl pro Transportmittel an Werktagen (Montag bis Freitag) für Céline (rot) und Marilen (türkis)

Der Vergleich der Wochenenden zeigt nur geringe Unterschiede im Mobilitätsverhalten der beiden Nutzerinnen im Vergleich zu den Werktagen (Abbildung 4). Am Wochenende verbrachte Marilen deutlich weniger Zeit im Zug (im Durchschnitt 0.7 h), auch Céline verbrachte mit 1.6 Stunden weniger Zeit im Zug. Im Gegensatz zu Marilen, die das Tram an den Wochenenden nicht nutzte, ist Céline in dieser Verkehrsmittelkategorie weiterhin aktiv. Eine auffällige Zunahme zeigt sich bei der Kategorie Gehen bei Céline (1.5 h), wobei sich bei Marilen kaum eine Änderung zeigte (0.3 h).

Transportmittel Resultate Samstag-Sonntag
# Plot: Anzahl Stunden pro TM pro PErson für die Sa-So
ggplot(average_hours_per_transport_weekend, aes(x = activity.topCandidate.type, y = average_hours, fill = user)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(
    x = "Transportmittel",
    y = "Durchschnitt der Anzahl Stunden",
    fill = "Nutzerin"
  ) +
  theme_minimal() +
  theme(
    axis.text = element_text(size = 12),
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),
  )
Abbildung 4: Darstellung der durchschnittlichen Stundenzahl pro Transportmittel an Wochenenden (Samstag und Sonntag) für Céline (rot) und Marilen (türkis)

Der letzte Vergleich der Transportmittel bezieht sich auf den Mittwoch, an dem die Vorlesung Patterns and Trends in Environmental Data stattfand (Abbildung 5). An diesem Tag zeigte sich bei Céline ein signifikantes Pendeln von Basel nach Wädenswil, wodurch sie mittwochs im Durchschnitt fast 2.5 Stunden im Zug verbrachte. Im Vergleich dazu nutzte Marilen an diesem Tag die öffentlichen Verkehrsmittel (Bus, Passagierfahrzeuge, Tram und auch den Zug) deutlich länger als an den anderen Tagen, was auf eine höhere Mobilität hinweist. Marilen verbrachte knapp 1.4 Stunden im Zug und 0.7 Stunden im Tram.

Transportmittel Resultate Mittwoch
# Plot: Anzahl Stunden pro TM pro PErson für die Mi
ggplot(average_hours_per_transport_wednesday, aes(x = activity.topCandidate.type, y = average_hours, fill = user)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(
    x = "Transportmittel",
    y = "Durchschnitt der Anzahl Stunden",
    fill = "Nutzerin"
  ) +
  theme_minimal() +
  theme(
    axis.text = element_text(size = 12),
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),
  )
Abbildung 5: Darstellung der durchschnittlichen Stundenzahl pro Transportmittel am Mittwoch für Céline (rot) und Marilen (türkis)

Home Range

In der Anlayse der Convex Hulls von Céline und Marilen zeigt sich, dass Marilen mit 7’863 km\(^2\) die grössere Home Range hat als Céline mit 6’984 km\(^2\). Die Überlappung der beiden Home Ranges ist gross (Abbildung 6). Die Home Range von Céline’s überlappt zu 61 % mit Marilen’s, während Marilen’s Home Range zu 54 % mit der von Céline überlappt. Beide Home Ranges befinden sich eher in der Mitte der Schweiz. Orte, an welchen Céline und Marilen beide hinreisten sind Zürich, Wädenswil und Bern. Die wichtigsten Orte wurden im Plot visualisiert (Abbildung 6). Céline war in diesen fünf Wochen unter anderem auch in Basel, Zeiningen, Aeschi und Innertkirchen unterwegs. Uster, Näfels und Brig bilden bei Marilen die äussersten Punkte ihrer Convex Hull.

Home Ranges über die Gesamtezeit
# Landesgrenzen
# Falls du die Schweizer Landesgrenzen nochmal überprüfst:
swiss_border <- ne_countries(scale = "medium", country = "Switzerland", returnclass = "sf")
swiss_border <- st_transform(swiss_border, crs = 2056)  # Transformieren in EPSG:2056

# Extrahieren der Koordinaten (x und y)
city_coords_xy <- st_coordinates(city_coords_sf_lv95)
city_coords_sf_lv95$x <- city_coords_xy[, 1]
city_coords_sf_lv95$y <- city_coords_xy[, 2]

# Hinzufügen der Farbinformation für jede Stadt
city_coords_sf_lv95$color <- c(
  "violet",  # Zürich
  "red",  # Basel
  "violet",  # Wädenswil
  "blue",  # Brig
  "blue",  # Uster
  "violet",  # Bern
  "blue",  # Näfels
  "red",  # Zeiningen
  "red",  # Aeschi
  "red"   # Innertkirchen
)

# Definiere die Farben manuell
user_colors <- c(
  "Celine" = "#F8766D",
  "Marilen" = "#00BFC4"
)

# ggsave("convex_hull_celine_marilen_2.png")

# Fläche berechnen total
# Fläche der Convex Hulls berechnen
flaechen <- st_area(mcp_celine_marilen)

# Ergebnisse anzeigen
# mcp_celine_marilen$user  # Benutzerzuordnung
# flaechen                 # Fläche in Quadratmetern (m²)

# Tabelle mit User und Fläche
flaechen_df <- mcp_celine_marilen |>
  mutate(flaeche_m2 = st_area(geometry)) |>
  st_drop_geometry()  # Falls du nur eine einfache Tabelle willst

# Fläche in Quadratkilometern (optional)
flaechen_df <- flaechen_df |>
  mutate(flaeche_km2 = as.numeric(flaeche_m2) / 1e6)

#  user     flaeche_m2 flaeche_km2
#   <chr>         [m^2]       <dbl>
# 1 celine  6984125978.       6984.
# 2 marilen 7862776160.       7863.

# Berechnung Überlappung total
overlap <- st_intersection(homerange_celine, homerange_marilen)

# Berechne die Fläche der Überlappung
overlap_area_m2 <- st_area(overlap)

# Wenn du die Fläche in Quadratkilometern umwandeln möchtest
overlap_area_km2 <- as.numeric(overlap_area_m2) / 1e6

# Anzeige der Ergebnisse
# print(overlap_area_m2)  # Fläche in Quadratmetern
# print(overlap_area_km2) # Fläche in Quadratkilometern
#  4262.079km2

# Überlappung von Celines homerange: 61%
# Überlappung von Marilens Homerange: 54%

# Plot
ggplot() +
  # Hintergrund: Schweizer Landesgrenzen
  geom_sf(data = swiss_border, fill = "grey80", color = "black", alpha = 0.4) +
  
  # MCP-Plot mit fix definierten Farben für die Nutzer
  geom_sf(data = mcp_celine_marilen, aes(fill = user), alpha = 0.4) +
  
  # Städtepunkte mit spezifischen Farben (aber NICHT über 'fill' von ggplot2 gesteuert)
  geom_sf(data = city_coords_sf_lv95, aes(geometry = geometry), size = 4, shape = 21, fill = city_coords_sf_lv95$color, stroke = 0) +
  
  # Beschriftungen der Städte
  geom_text(data = city_coords_sf_lv95, aes(x = x, y = y, label = city), size = 2.5, fontface = "bold", vjust = -1, hjust = 0.5) +
  
  # Koordinatenachsen und Projektionssystem
  coord_sf(datum = 2056) +
  
  # Manuelles Setzen der Farben für die Home Ranges
  scale_fill_manual(name = "Nutzerin", values = user_colors) +
  
  labs(x = "Koordinaten nach LV95 (Easting)", y = "Koordinaten nach LV95 (Northing)")+
  
  # Optional: Titel und Thema
  theme_minimal() +
  theme(
    axis.title = element_text(size = 12),  # Schriftgrösse für Achsentitel
    axis.text = element_text(size = 10),   # Schriftgrösse für Achsenbeschriftungen
     axis.title.x = element_text(margin = margin(t = 20)),  # Mehr Abstand zum unteren Rand für x-Achse
    axis.title.y = element_text(margin = margin(r = 20)),  # Mehr Abstand zum rechten Rand für y-Achse
    plot.margin = margin(t = 30)
  )
Abbildung 6: Convex Hull Céline (rot) und Marilen (türkis) über den gesamten Analysezeitraum (19.02.2025 - 26.03.2025)

Die Analyse der Home Ranges werktags und am Wochenende (Abbildung 7) zeigen bei Céline, dass die Home Range werktags viel grösser (6’890 km\(^2\)) als am Wochenende (1’339 km\(^2\)) ist. Bei Marilen hingegen zeigt sich nur ein moderater Unterschied in der Grösse der Home Range zwischen Werktagen (5’701 km\(^2\)) und am Wochenende (3’477 km\(^2\)).

Die Home Ranges von Céline ist werktags grösser und umfasst mehr Orte als Marilen’s Home Range. Am Wochenende sind beide Home Ranges in Form und Grösse ähnlich.

Home Ranges Vergleich: Werktage und Wochenende
#| output: hidden
#| collapse: true

homeranges_wochentag <- datensatz_gesamt_sf |>
  group_by(user, wochentags_typ) |>
  summarise(geometry = st_union(geometry), .groups = "drop") |>
  mutate(homerange = st_convex_hull(geometry))

# str(homeranges_wochentag)

# Die Farben manuell festlegen
user_colors <- c(
  "Celine" = "#F8766D",  # Rot-Orange
  "Marilen" = "#00BFC4"   # Türkis
)

# Umwandlung von Grad (WGS84) nach LV95 (CH1903+)
homeranges_wochentag_lv95 <- homeranges_wochentag %>%
  st_transform(crs = 2056)  # Umwandlung auf LV95 (EPSG:2056)

# Berechnung Fläche Wochentags_typ
# Berechnung der Flächen für Werktage und Wochenenden
homeranges_wochentag <- datensatz_gesamt_sf %>%
  group_by(user, wochentags_typ) %>%
  summarise(geometry = st_union(geometry), .groups = "drop") %>%
  mutate(homerange = st_convex_hull(geometry)) %>%
  mutate(flaeche_m2 = st_area(homerange)) %>%  # Fläche in Quadratmetern berechnen
  mutate(flaeche_km2 = flaeche_m2 / 1e6)  # Fläche in Quadratkilometern umrechnen

# Zeige die berechneten Flächen an
# print(homeranges_wochentag)

# Nur Mittwochs-Daten auswählen
mittwoch_daten <- datensatz_gesamt_sf %>%
  filter(wochentag == "Mittwoch")

# Berechnung des Homeranges (Convex Hull) für Mittwoch
homerange_mittwoch <- mittwoch_daten %>%
  group_by(user) %>%
  summarise(geometry = st_union(geometry), .groups = "drop") %>%
  mutate(homerange = st_convex_hull(geometry)) %>%
  mutate(flaeche_m2 = st_area(homerange)) %>%
  mutate(flaeche_km2 = flaeche_m2 / 1e6)

# Zeige die berechneten Flächen für Mittwoch an
# print(homerange_mittwoch)

# Celine: 613km2
# Marilen: 5666km2

ggplot() +
  geom_sf(data = swiss_border, fill = "grey80", color = "black", alpha = 0.4) + 
  geom_sf(data = homeranges_wochentag, aes(geometry = homerange, fill = user), alpha = 0.4) +
  geom_sf(data = homeranges_wochentag, aes(geometry = homerange), color = "black", fill = NA, size = 0.7, alpha = 0.5) +
  geom_sf(data = homeranges_wochentag, aes(geometry = geometry), color = "black", size = 0.5) +
  facet_wrap(~ user + wochentags_typ) +
  labs(
    fill = "User",
    x = "Koordinaten (LV95)",
    y = "Koordinaten (LV95)"
  ) +
  scale_fill_manual(name = "Nutzerin", values = user_colors) +
  theme_minimal() +
 theme(
  strip.text = element_text(size = 12),
  plot.title = element_text(hjust = 0.5),
  legend.position = "right",
  axis.text.x = element_text(angle = 45, vjust = 0.5, hjust = 1),
  axis.title.x = element_text(margin = margin(t = 20))
  ) +
  coord_sf(datum = 2056)
Abbildung 7: Convex Hull Vergleich zwischen den Werktagen und Wochenenden: Céline (rot) und Marilen (türkis)

Die erweiterte Analyse der Home Ranges am Mittwoch zeigt einen grossen Unterschied (Abbildung 8). Céline’s Home Range umfasst 613 km\(^2\), während Marilen’s Home Range 5’666 km\(^2\) beträgt. Die Überlappung ist nur eine kleine Fläche um Zürich und Wädenswil.

Home Ranges für den Mittwoch
user_colors <- c(
  "Celine" = "#F8766D",  # Rot-Orange
  "Marilen" = "#00BFC4"   # Türkis
)

ggplot() +
  # Schweizer Grenze
  geom_sf(data = swiss_border, fill = "grey80", color = "black", alpha = 0.4) +
  
  # Home Ranges (Mittwoch)
  geom_sf(data = homerange_mittwoch, aes(fill = user), alpha = 0.4) +
  
  # Ränder der Home Ranges
  geom_sf(data = homerange_mittwoch, aes(geometry = homerange, fill = user), alpha = 0.4, color = "black", size = 0.7, alpha = 0.8) +
  
  # Farbpalette (nur einmal!)
  scale_fill_manual(name = "Nutzerin", values = user_colors) +
  
  # Achsentitel und Plot-Titel
  labs(
    x = "Koordinaten LV95",
    y = "Koordinaten LV95",
    fill = "User"
  ) +
  
  theme_minimal() +
theme(
    axis.title = element_text(size = 12),  # Schriftgrösse für Achsentitel
    axis.text = element_text(size = 10),   # Schriftgrösse für Achsenbeschriftungen
    axis.text.x = element_text(angle = 45, vjust = 0.5, hjust = 1), # dreht die x-Achsenbeschriftung um 45 Grad
     axis.title.x = element_text(margin = margin(t = 20)),  # Mehr Abstand zum unteren Rand für x-Achse
    axis.title.y = element_text(margin = margin(r = 20)),  # Mehr Abstand zum rechten Rand für y-Achse
    plot.margin = margin(t = 30)
  )+
  coord_sf(datum = 2056)
Abbildung 8: Convex Hull mittwochs: Céline (rot) und Marilen (türkis)

Kernel Dichte Schätzung

Die Ergebnisse der KDE zeigen, dass bei Céline die höchste Dichte in und um Basel liegt, jedoch bei Zürich/Wädenswil auch eine relativ hohe Dichte aufweist (Abbildung 9). Dies lässt darauf schliessen, dass Céline oft zwischen den beiden Hotspots hin und her reist. Marilen hingegen bewegt sich vorwiegend in einem Hotspot (Uster/Zürich). Die Reise ins Wallis, welche in der Home Range eine grosse Bedeutung erhält, zeigt sich hier als Ausnahme.

Kernel Dichte Schätzung Resultate
plot_marilen | plot_celine
Abbildung 9: Kernel-Dichte Schätzung: Marilen (links) und Céline (rechts)

Die Resultate der Bewegungsmuster von Céline und Marilen zeigen deutlich, dass zwischen der Anzahl Stunden unterwegs und der Grösse der Home Ranges kein Zusammenhang besteht (siehe Abbildung 6 und Abbildung 2). Céline verbringt während den fünf Wochen mehr Stunden unterwegs und dies in einer kleineren Home Range als Marilen, die in diesem Zeitraum weniger Stunden unterwegs war als Céline.

Die gesamte Betrachtung der Resultate ergibt, dass sich sowohl die Aufenthaltsorte (Abbildung 10) als auch die Bewegungsmuster (Abbildung 11) von Céline und Marilen stark unterscheiden. Jedoch finden an gewissen Tagen Überschneidungen in den besuchten Orten (Wädenswil und Zürich) statt. Diese sind in orange auf der Abbildung 11 gekennzeichnet.

Interaktive Karte Resultate
farben <- c("Celine" = "#F8766D", "Marilen" = "#00BFC4")

mapview(datensatz_gesamt_sf, zcol = "user", col.regions = farben)
Abbildung 10: Interaktive Übersicht über die Aufenthaltsorte: Céline (rot) und Marilen (türkis)
Plotly Resultate
# Plotly für beide über alle Werte
plot_ly() |> 
  # Celine's Trajektorie (rote Linie)
  add_trace(
    data = celine, type = 'scatter3d',
    x = ~ost_start, y = ~nord_start, z = ~startTime,
    mode = "lines", name = "Celine",
    line = list(color = "#F8766D")
  ) |> 
  # Marilen's Trajektorie (blaue Linie)
  add_trace(
    data = marilen, type = 'scatter3d',
    x = ~ost_start, y = ~nord_start, z = ~startTime,
    mode = "lines", name = "Marilen",
    line = list(color = "#00BFC4")
  ) |> 
  # Treffpunkte (orange Punkte)
  add_trace(
    data = meets_data, type = 'scatter3d',
    x = ~ost_start_Celine, y = ~nord_start_Celine, z = ~startTime_Celine,
    mode = "markers", name = "Meets",
    marker = list(color = "orange", size = 4)
  ) |> 
  layout(
    title = "Trajektorien von Celine und Marilen",
    scene = list(
      xaxis = list(title = "Ost-Koordinate", tickformat = ".0f"),
      yaxis = list(title = "Nord-Koordinate", tickformat = ".0f"),
      zaxis = list(title = "Zeitpunkt")
    ),
    legend = list(title = list(text = "Person"))
  )
Abbildung 11: 3D-Visualisierung der Trajektorien-Pfade: Céline (rot), Marilen (türkis) und Treffpunkte (orange)

Diskussion

Die Analyse der Bewegungsdaten von zwei Personen über fünf Wochen zeigt sowohl Ähnlichkeiten als auch Unterschiede im Mobilitätsverhalten. Der Vergleich der Anzahl Stunden unterwegs, den Tagesabschnitten und den Home Ranges zeigen, dass die beiden Personen unterschiedlichen Alltagsstrukturen mit verschiedenen Mobilitätspräferenzen folgen.

Eine zentrale Erkenntnis ist, dass der gewählte Untersuchungszeitrum einen entscheidenden Einfluss auf die Ergebnisse hat (Byon, Abdulhai, und Shalaby 2009). Ereignisse wie Ferien oder grössere Ausflüge wirken sich unmittelbar auf die Bewegungsdaten aus und verändern damit die Berechnungen. Ein Beispiel für ein solches Ereignis bietet die erste Woche der Datenerhebung, in der Marilen Skiferien im Wallis machte. Diese Reise hat die Berechnung ihres Home Ranges deutlich beeinflusst und führt zu einer verzerrten Darstellung ihres üblichen Alltagsbewegungsmusters. Eine präzisere Definition der Forschungsfrage wäre daher in zukünftigen Studien essenziell, um festzulegen, ob Alltagsmobilität oder auch aussergewöhnliche Bewegungen untersucht werden sollen. Um solche Verzerrungen auszugleichen, wurde in der vorliegenden Analyse ergänzend eine Hotspotanalyse durchgeführt, die stark frequentierte Orte stärker gewichtet und so eine differenziertere Betrachtung ermöglicht.

Ein weiterer kritischer Punkt betrifft die Datenqualität. Die Datenquelle von Google Maps Timeline ist in ihrer Genauigkeit beschränkt. Darüber hinaus kann die Zuordnung einzelner Aktivitätstypen inkorrekt oder unvollständig sein (Sadeghian u. a. 2022), was bei der Interpretation der Ergebnisse berücksichtigt werden muss.

Diese Untersuchung liefert wertvolle Einblicke in das individuelle Mobilitätsverhalten von zwei Personen und zeigt zugleich die Limitationen der Datenerhebung mit GPS-Daten auf. Künftige Studien sollten auf präzise Forschungsfragen und eine kontinuierlichere und längerfristige Datensammlung mit grossem Sample setzen, um repräsentative Aussagen machen zu können.

Appendix

Wortzählung

Code
devtools::install_github("benmarwick/wordcountaddin")
wordcountaddin::text_stats("index.qmd")
Method koRpus stringi
Word count 2710 2601
Character count 19835 19992
Sentence count 209 Not available
Reading time 13.6 minutes 13 minutes

Literatur

Byon, Young-Ji, Baher Abdulhai, und Amer Shalaby. 2009. „Real-time transportation mode detection via tracking global positioning system mobile devices“. Journal of Intelligent Transportation Systems 13 (4): 161–70.
Chen, Cynthia, Jingtao Ma, Yusak Susilo, Yu Liu, und Menglin Wang. 2016. „The promises of big data and small data for travel behavior (aka human mobility) analysis“. Transportation research part C: emerging technologies 68: 285–99. 10.1016/j.trc.2016.04.005.
Google LLC. n.d. „Google Maps Timeline—Computer—Google Maps Help“. https://support.google.com/maps/answer/6258979.
Kashifi, Mohammad Tamim, Arshad Jamal, Mohammad Samim Kashefi, Meshal Almoshaogeh, und Syed Masiur Rahman. 2022. „Predicting the travel mode choice with interpretable machine learning techniques: A comparative study“. Travel Behaviour and Society 29: 279–96. 10.1016/j.tbs.2022.07.003.
RStudio Team. 2020. „RStudio (Version 4.4.2) [Software]“. https://www.rstudio.com/.
Sadeghian, Paria, Xiaoyun Zhao, Arman Golshan, und Johan Håkansson. 2022. „A stepwise methodology for transport mode detection in GPS tracking data“. Travel Behaviour and Society 26: 159–67.
Sheller, Mimi, und John Urry. 2006. „The new mobilities paradigm“. Environment and planning A 38 (2): 207–26. https://journals.sagepub.com/doi/10.1068/a37268.
Tao, Yaguang, Alan Both, Rodrigo I Silveira, Kevin Buchin, Stef Sijben, Ross S Purves, Patrick Laube, Dongliang Peng, Kevin Toohey, und Matt Duckham. 2021. „A comparative analysis of trajectory similarity measures“. GIScience & Remote Sensing 58 (5): 643–69.