Students have mastered the theory and use it for their argument, with references • Students show understanding of theory
What has been done in terms of literature review?
In dieser Projektarbeit untersuchen wir das Flugverhalten von Brieftauben auf Grundlage hochaufgelöster GPS-Daten. Mithilfe von Sinuosität, Geschwindigkeitsanalysen und Ähnlichkeitsmassen vergleichen wir die unterschiedlichen Flugphasen und Flugrouten. Zusätzlich analysieren wir das «Lead and Follow»-Muster innerhalb des Taubenschwarms. Ziel ist es, wiederkehrende Bewegungsmuster durch die Anwendung verschiedener Analysekonzepte zu identifizieren.
Forschungsfragen
Das Ziel der Projektarbeit ist die Untersuchung der folgenden Forschungsfragen:
Wie lassen sich Flugbahnen von Brieftauben anhand von Sinuosität und Ähnlichkeitsmassen konzeptualisieren und vergleichen?
Gibt es wiederkehrende Muster in der Fluggeschwindigkeit von Brieftauben über unterschiedliche Flugphasen und wie können diese quantifiziert werden?
Kann in den Bewegungsmustern von Brieftauben eine «Lead and Follow»-Dynamik identifiziert werden?
Erwartungen
Wir erwarten, dass die Brieftauben in der Nähe der Freilassungsstelle und des Taubenschlags eine kurvenreiche Flugbahn mit hoher Sinuosität zeigen [@laube2007] und nach einer Anfangsphase geradliniger und mit höherer Geschwindigkeit fliegen [@schiffner2009]. Trotz der leicht unterschiedlichen Freilassungsstellen und variierenden Flugrouten wird er- wartet, dass die aufgenommenen Flüge aufgrund ähnlicher Flugmuster hohe Ähnlichkeitsmasse aufweisen. Zudem erwarten wir, dass es innerhalb des Taubenschwarms Individuen gibt, welche deutlich häufiger die Führung des Schwarms übernehmen [@nagy2010].
Methoden
Für die Bearbeitung der Forschungsfragen gehen wir wie folgt vor:
Datenaufbereitung und Resampling (Auflösung auf eine Sekunde hochskalieren)
Filterung des Datensatz auf die zu untersuchenden Individuen und Flüge:
Flug Nr. 4 zum Vergleich innerhalb eines Fluges
Individuum S zum Vergleich der Flüge
Aufteilung und Bearbeitung der Forschungsfragen in R in die vier Teilanalysen zu den Themen Sinuosität, Ähnlichkeit, Geschwindigkeit und «Lead and Follow»
Daten
Wir verwenden einen Datensatz mit Bewegungsdaten zum Flugverhalten von Brieftauben aus dem Paper von [@santos2014]. Der Datensatz enthält GPS-Daten von fünf Heimflügen, die jeweils mit denselben 9-10 Individuen über einen Zeitraum von 12 Tagen durchgeführt wurden. Die Daten sind über die gesamte Flugzeit von durchschnittlich 15 bis 20 Minuten hochaufgelöst mit einer Lokalisierung jede Viertelsekunde pro Individuum. Die Fragestellungen beantworten wir mit den folgenden Attributen aus dem beschriebenen Datensatz: Zeitstempel, Koordinaten, Flugnummer und Individuum-Nummer. Der Datensatz enthält zusätzlich für jede Messung Angaben zur Flughöhe über dem Referenzellipsoid und der Fluggeschwindigkeit.
Flugrouten (pink) der Brieftauben aus dem Datensatz
Resampling kurz begründen/erklären.
Die angegebene Fluggeschwindigkeit wird zur maximalen Kontrolle und Transparenz [@laube2014] nicht verwendet und selbst aus den Daten berechnet.
Begrenzung auf Flug 4 und Individuum «S» begründen / erklären.
Begrenzung unserer Berechnungen auf den zweidimensionalen Raum
Weitere Informationen / Erkenntnisse unsererseits zu den Daten: - Flugrichtung - Start und Ende der Flüge wurden aus dem Datensatz entfernt.
Für die Auswertung werden keine zusätzlichen Daten benötigt.
Der Datensatz von Santos et al. 2014 ist öffentlich über die Movebank Plattform [@kays2022] erhältlich.
Datenaufbereitung
Resampling
Code
# CSV-Datei importierenFlugdaten =read_delim("Leadership in homing pigeon flocks.csv")# Aufteilung in separate FlügeFlugliste =split(Flugdaten, Flugdaten$comments)for (name innames(Flugliste)) {assign(paste0(name), Flugliste[[name]])}# Umbenennen der Data Framesalte_namen <-ls(pattern ="^homing flight [1-5]$")for (name in alte_namen) { nummer <-strsplit(name, " ")[[1]][3] neuer_name <-paste0("flug", nummer)assign(neuer_name, get(name))rm(list = name)}# Resample auf 1-Sekunden-Intervalleresample <-function(df) { gemeinsame_zeiten <-Reduce(intersect, split(df$timestamp, df$`tag-local-identifier`)) df |>filter(timestamp %in% gemeinsame_zeiten) |>group_by(`tag-local-identifier`) |>arrange(timestamp) |>mutate(rn =row_number()) |>filter(rn %%4==1) |>ungroup() |>select(-rn) |>arrange(`tag-local-identifier`, timestamp)}# Anwenden auf alle Flüge for (n inls(pattern ="^flug[1-5]$")) {assign(paste0(n, "_resample"), resample(get(n)))}
Die Daten liegen in hoher zeitlicher Auflösung mit Messwerten alle 0,25 Sekunden vor. Für die statistischen Analysen wird die Datenmenge durch Resampling auf eine Messung pro Sekunde reduziert. Dabei werden nur jene Zeitpunkte berücksichtigt, die bei allen Individuen eines Flugs vorhanden sind, um eine vergleichbare zeitliche Basis sicherzustellen.
Begrenzung der Daten auf Flug 4 und Individuum S
Code
# Analyse wird vereinfacht durchgeführt mit: Flug 4, Individuum "S"Alle <-read_delim("Alle_Fluege_resample.csv")Flug_4 <- Alle |>filter(comments =="homing flight 4")Ind_S <- Alle |>filter(`individual-local-identifier`=="S")
Zur Vereinfachung und Fokussierung der Analyse wurde aus den fünf verfügbaren Flügen zufällig ein einzelner Flug (Flug 4) ausgewählt, um die Flugtrajektorien der verschiedenen Individuen miteinander zu vergleichen. Analog dazu wurde für den Vergleich über alle fünf Flüge hinweg ein einzelnes Individuum (Individuum S) zufällig gewählt. Dies ermöglicht eine exemplarische Analyse im gegebenen zeitlichen und methodischen Rahmen und hält den Umfang der Untersuchung klar definiert.
Ableitung Attribut Geschwindigkeit
Hier Bild Ausschnitt von Laupe 2014 «Moving Window» einfügen
Code Herleitung Ground Speed Moving Window
Alle <-read_delim("Alle_Fluege_resample.csv")# Functions moving windowsdifftime_secs <-function(x, y){as.numeric(difftime(x, y, units ="secs"))}distance_by_element <-function(later, now){as.numeric(st_distance(later, now, by_element =TRUE) )}# SF Object ---------------------Alle_geschw <- Alle |>st_as_sf(coords =c('location-lat', 'location-long'), crs =4326) |>select('event-id', timestamp)# Calculate speed at scale 1 (2s = offset 1) ---------------# 1. timelag:now <- Alle_geschw$timestampprev <-lag(now)later <-lead(now)Alle_geschw <- Alle_geschw |>mutate(timelag =difftime_secs(later, prev) )# 2. steplenght:now <- Alle_geschw$geometryprev <-lag(now)later <-lead(now)Alle_geschw <- Alle_geschw |>mutate(steplenght =distance_by_element(later, prev) )# 3. speed:Alle_geschw <- Alle_geschw |>mutate(speed_2s = steplenght/timelag,speed_2s =ifelse(speed_2s <0, NA, speed_2s) )## Calculate speed at scale 2 (4s = offset 2) --------------# 1. timelag:now <- Alle_geschw$timestampprev <-lag(lag(now))later <-lead(lead(now))Alle_geschw <- Alle_geschw |>mutate(timelag2 =difftime_secs(later, prev) )# 2. steplenght:now <- Alle_geschw$geometryprev <-lag(lag(now))later <-lead(lead(now))Alle_geschw <- Alle_geschw |>mutate(steplenght2 =distance_by_element(later, prev) )# 3. speed:Alle_geschw <- Alle_geschw |>mutate(speed_4s = steplenght2/timelag2,speed_4s =ifelse(speed_4s <0, NA, speed_4s) )## Calculate speed at scale 3 (8s = offset 4) -------------# 1. timelag:now <- Alle_geschw$timestampprev <-lag(now, n =4) # use n = ... rather than lag(lag(...)).later <-lead(now, n =4)Alle_geschw <- Alle_geschw |>mutate(timelag3 =difftime_secs(later, prev) )# 2. steplenght:now <- Alle_geschw$geometryprev <-lag(now, n =4)later <-lead(now, n =4)Alle_geschw <- Alle_geschw |>mutate(steplenght3 =distance_by_element(later, prev) )# 3. speed:Alle_geschw <- Alle_geschw |>mutate(speed_8s = steplenght3/timelag3,speed_8s =ifelse(speed_8s <0, NA, speed_8s) )## Compare speed across scales ---------------# Alle_geschw |> # st_drop_geometry() |> # select(timestamp, speed_2s, speed_4s, speed_8s)# To compare with ggplot (Boxplots), we need a long format:Alle_geschw_2 <- Alle_geschw |># simplify dataframest_drop_geometry() |>select(timestamp, speed_2s, speed_4s, speed_8s)Alle_geschw_long <- Alle_geschw_2 |># pivot longpivot_longer(c(speed_2s, speed_4s, speed_8s))# # ggplot:# ggplot(Alle_geschw_long, aes(name, value)) +# # we remove outliers to increase legibility, analogue# # Laube and Purves (2011)# geom_boxplot(outliers = FALSE) +# labs(x = "Type of speed-(window)", y = "speed [m/s]") +# theme_classic()# Join back to Alle Dataset:Alle_geschw_subset <- Alle_geschw |>st_drop_geometry() |>select('event-id', speed_2s, speed_4s, speed_8s)# Left Join anhand der event-idAlle <- Alle |>left_join(Alle_geschw_subset, by ='event-id')Alle_geschw_comp <- Alle |>select('event-id', timestamp, 'ground-speed', 'individual-local-identifier', speed_2s, speed_4s, speed_8s)# ggplot:Alle_geschw_long <- Alle |># pivot longpivot_longer(c('ground-speed', speed_2s, speed_4s, speed_8s))
Die berechneten Geschwindigkeiten (speed_2s bis speed_8s) unterschieden sich signifikant von der Logger-Geschwindigkeit (ground-speed). Diese Differenz kann durch die vernachlässigte Dimension der Höhe sowie die unterschiedliche Schätzung/Berechnung der Geschwindigkeit im Vergleich zum GPS-Logger erklärt werden.
Code Vergleich Ground Speed
ggplot(Alle_geschw_long, aes(name, value)) +# we remove outliers to increase legibility, analogue# Laube and Purves (2011)geom_boxplot(outliers =FALSE) +labs(x ="Type of speed", y ="speed [m/s]") +theme_classic()
Die durch die “Moving-Window-Methode” berechneten Geschwindigkeiten unterscheiden sich untereinander kaum. Die mittlere Geschwindigkeit nimmt vom “Window” von zwei Sekunden im Vergleich zum “Window” von acht Sekunden geringfügig etwas ab.
Code Übertragung Ground Speed aus Herleitung
Alle <- Alle |>mutate(`ground-speed`= speed_4s) |># ground-speed überschreibenselect(-starts_with("speed_")) # alle anderen speed-Spalten löschen
Für die weitere Analysen wird mit der Geschwindigkeit vom “Moving-Window” von vier Sekunden weitergearbeitet.
Zur Analyse der Trajektorienähnlichkeit wurden vier Ähnlichkeitsmasse eingesetzt: DTW, Edit Distance, Fréchet und LCSS. Die GPS-Daten wurden pro Trajektorie auf eine feste Punktzahl resampelt (n = 250). Die Berechnung erfolgte mit dem R-Paket SimilarityMeasures (Toohey, 2015). DTW und EditDist berechnen die kumulierte Punktdistanz entlang optimierter Vergleichspfade; Fréchet ermittelt die maximale Abweichung entlang des räumlichen Kurvenverlaufs zweier Trajektorien. LCSS zählt die Anzahl ähnlicher Punkte entlang monotoner Pfade, wobei innerhalb des Fluges 4 ein räumlicher Toleranzwert (ε = 0.1 m) verwendet wurde. Die LCSS-Analyse für die Anwendung auf mehrere Flüge (Individuum S) war zu rechenintensiv und konnte deshalb nicht durchgeführt werden.
Geschwindigkeit
Die verschiedenen Flüge des Individuums “S” Ind_S sowie alle Individuen aus Flug 4 Flug_4 wurden jeweils in gleiche Zeitsegmente eingeteilt. Dazu wurde die Funktion detect_segments_limited() eingesetzt, die auf jedem Flug oder Individuum die relative Zeit (time_rel) ermittelt und daraus gleich grosse Segmente bildet. Die Flüge wurden je in eine Anzahl von acht Segmenten unterteilt. Hierdurch konnten Flugverläufe unterschiedlicher Dauer vergleichbar gemacht werden. Für die Visualisierung wurden zwei Ansätze gewählt:
Zum einen wurden Trajektorien der Flüge kartiert, wobei die mittlere Geschwindigkeit pro Segment farblich abgestuft dargestellt wurde.
Zum anderen wurde die mittlere Segmentgeschwindigkeit in Abhängigkeit von der relativen Flugposition (Start bis Ende) geplottet. Zur Glättung der Geschwindigkeitsverläufe wurde ein loess-Glätter verwendet, ein nichtparametrisches Regressionsverfahren, das lokale lineare Fits anwendet.
Code Segmentierung alle Flüge des Individuums S
# Folgend werden die Geschwindigkeitsmuster für die erste Analyseeinheit (alle Flüge des Individuums "S") berechnet:# Max. Anzahl Segmente pro Flugmax_segments <-8max_cpts <- max_segments -1# Liste der Flügefluege <-unique(Ind_S$comments)# Funktion zur Einteilung der Flüge in gleiche Zeitsegmentedetect_segments_limited <-function(flug_name) { df <- Ind_S |>filter(comments == flug_name) |>arrange(timestamp)if (nrow(df) <5) return(NULL) # Optional: Wenn zu wenig Daten df <- df |>mutate(total_time =as.numeric(difftime(max(timestamp), min(timestamp), units ="secs")),time_rel =as.numeric(difftime(timestamp, min(timestamp), units ="secs")) / total_time,segment =cut(time_rel, breaks =seq(0, 1, length.out = max_segments +1),labels =FALSE, include.lowest =TRUE),flug = flug_name )return(df)}# Alle Flüge in Segmente aufteilenseg_df_limited <-map_df(fluege, detect_segments_limited)# Segmentindex als Grundlage für diverse Plots:seg_summary <- seg_df_limited |>group_by(flug, segment) |>summarise(mean_speed =mean(`ground-speed`, na.rm = T),segment_index =first(segment),n_segments =n_distinct(segment),.groups ="drop") |>group_by(flug) |>mutate(segment_position = segment_index /max(segment_index))ggplot(Ind_S, aes(x = comments, y =`ground-speed`)) +geom_boxplot(fill ="lightblue", alpha =0.7) +coord_flip() +labs(title ="Geschwindigkeitsverteilung pro Flug",x ="Flug", y ="Geschwindigkeit (m/s)") +theme_minimal()
Code Segmentierung alle Flüge des Individuums S
# Plot Segmentierung über alle Flüge inkl. Geschwindigkeitsverläufe:ggplot(seg_df_limited, aes(x = timestamp, y =`ground-speed`)) +geom_line(aes(group = segment, color =as.factor(segment)), size =1) +geom_vline(data = seg_df_limited |>group_by(flug, segment) |>summarise(x =min(timestamp), .groups ="drop"),aes(xintercept =as.numeric(x)),color ="black", linetype ="dashed") +geom_segment(data = seg_df_limited |>group_by(flug, segment) |>summarise(x_start =min(timestamp),x_end =max(timestamp),y =mean(`ground-speed`, na.rm = T), .groups ="drop"),aes(x = x_start, xend = x_end, y = y, yend = y),inherit.aes =FALSE,color ="black", linetype ="solid", size =0.8) +facet_wrap(~ flug, scales ="free_x", ncol =1) +scale_color_brewer(palette ="Set2") +labs(title ="Relative Segmentierung mit 8 Segmenten pro Flug",x ="Zeit", y ="Geschwindigkeit (m/s)", color ="Segment") +theme_minimal(base_size =14)
Code Segmentierung alle Individuen des Fluges Nr. 4
# Folgend werden die Geschwindigkeitsmuster für die zweite Analyseeinheit (Alle Individuen des Fluges 4) berechnet:# Max. Anzahl Segmente pro Flugmax_segments <-8max_cpts <- max_segments -1# Liste der Flügefluege <-unique(Flug_4$`individual-local-identifier`)# Funktion zur Einteilung der Flüge in gleiche Zeitsegmentedetect_segments_limited <-function(indiv_name) { df <- Flug_4 |>filter(`individual-local-identifier`== indiv_name) |>arrange(timestamp)if (nrow(df) <5) return(NULL) # Optional: Wenn zu wenig Daten df <- df |>mutate(total_time =as.numeric(difftime(max(timestamp), min(timestamp), units ="secs")),time_rel =as.numeric(difftime(timestamp, min(timestamp), units ="secs")) / total_time,segment =cut(time_rel, breaks =seq(0, 1, length.out = max_segments +1),labels =FALSE, include.lowest =TRUE),indiv = indiv_name )return(df)}# Alle Flüge in Segmente aufteilenseg_df_limited <-map_df(fluege, detect_segments_limited)# Segmentindex als Grundlage für diverse Plots:seg_summary <- seg_df_limited |>group_by(indiv, segment) |>summarise(mean_speed =mean(`ground-speed`, na.rm = T),segment_index =first(segment),n_segments =n_distinct(segment),.groups ="drop") |>group_by(indiv) |>mutate(segment_position = segment_index /max(segment_index))ggplot(Flug_4, aes(x =`individual-local-identifier`, y =`ground-speed`)) +geom_boxplot(fill ="lightblue", alpha =0.7) +coord_flip() +labs(title ="Geschwindigkeitsverteilung pro Individuum",x ="ID", y ="Geschwindigkeit (m/s)") +theme_minimal()
Code Segmentierung alle Individuen des Fluges Nr. 4
# Plot Segmentierung über alle Flüge inkl. Geschwindigkeitsverläufe:ggplot(seg_df_limited, aes(x = timestamp, y =`ground-speed`)) +geom_line(aes(group = segment, color =as.factor(segment)), size =1) +geom_vline(data = seg_df_limited |>group_by(indiv, segment) |>summarise(x =min(timestamp), .groups ="drop"),aes(xintercept =as.numeric(x)),color ="black", linetype ="dashed") +geom_segment(data = seg_df_limited |>group_by(indiv, segment) |>summarise(x_start =min(timestamp),x_end =max(timestamp),y =mean(`ground-speed`, na.rm = T), .groups ="drop"),aes(x = x_start, xend = x_end, y = y, yend = y),inherit.aes =FALSE,color ="black", linetype ="solid", size =0.8) +facet_wrap(~ indiv, scales ="free_x", ncol =1) +scale_color_brewer(palette ="Set2") +labs(title ="Relative Segmentierung mit 8 Segmenten pro Individuum",x ="Zeit", y ="Geschwindigkeit (m/s)", color ="Segment") +theme_minimal(base_size =14)
ChatGPT wurde beigezogen.
Sinuosität
Code
# -------------------------## Daten einlesen# -------------------------## CSV-Dateien ladenflug4 <-read.csv("Flug4_Segmente.csv")alle_fluege <-read.csv("Alle_Fluege_Segmente.csv")# Spaltennamen vereinheitlichennames(flug4) <-gsub("-", ".", names(flug4))names(alle_fluege) <-gsub("-", ".", names(alle_fluege))# Zeitformat konvertierenflug4$timestamp <-as.POSIXct(flug4$timestamp, format ="%Y-%m-%dT%H:%M:%SZ", tz ="UTC")alle_fluege$timestamp <-as.POSIXct(alle_fluege$timestamp, format ="%Y-%m-%dT%H:%M:%SZ", tz ="UTC")# -------------------------## Funktion zur Sinuositätsberechnung# -------------------------## Sinuosität = zurückgelegte Strecke / Luftlinieberechne_sinuositaet <-function(df) { df <- df |>arrange(timestamp) coords <- df[, c("location.long", "location.lat")]if (nrow(coords) <2) return(NA) luftlinie <-distHaversine(coords[1, ], coords[nrow(coords), ]) strecke <-sum(distHaversine(coords[-nrow(coords), ], coords[-1, ]), na.rm =TRUE)if (luftlinie ==0) return(NA)return(strecke / luftlinie)}# -------------------------## Vergleich innerhalb des Flugs 4# -------------------------## Sinuosität pro Individuum und Segment berechnenflug4_summary <- flug4 |>group_by(tag.local.identifier, segment) |>summarise(sinuosity =berechne_sinuositaet(cur_data()), .groups ="drop") |>filter(!is.na(sinuosity))# Mittelwerte der Sinuosität pro Segment berechnenmittelwerte_flug4 <- flug4_summary |>group_by(segment) |>summarise(mean_sinuosity =mean(sinuosity), .groups ="drop")# Plot mit Segment 3ggplot(flug4_summary, aes(x =as.factor(segment), y = sinuosity, fill = tag.local.identifier)) +geom_bar(stat ="identity", position =position_dodge(width =0.8), width =0.7) +geom_line(data = mittelwerte_flug4,aes(x =as.factor(segment), y = mean_sinuosity, group =1, color ="Mittelwert"),inherit.aes =FALSE, linewidth =1.2) +geom_point(data = mittelwerte_flug4,aes(x =as.factor(segment), y = mean_sinuosity, color ="Mittelwert"),inherit.aes =FALSE, size =2) +labs(x ="Flugsegmente", y ="Sinuosität (log2)", fill ="Individuen", color ="") +scale_y_continuous(trans ="log2") +scale_fill_d3("category20") +scale_color_manual(values ="black", labels ="Mittelwert") +theme_minimal(base_size =14) +theme(legend.position ="right") +ggtitle("Sinuosität Flug 4 - Übersichtsplot")# Segment 3 ausschließenflug4_summary_filtered <- flug4_summary |>filter(segment !=3)mittelwerte_flug4_filtered <- mittelwerte_flug4 |>filter(segment !=3)# Plot ohne Segment 3ggplot(flug4_summary_filtered, aes(x =as.factor(segment), y = sinuosity, fill = tag.local.identifier)) +geom_bar(stat ="identity", position =position_dodge(width =0.8), width =0.7) +geom_line(data = mittelwerte_flug4_filtered,aes(x =as.factor(segment), y = mean_sinuosity, group =1, color ="Mittelwert"),inherit.aes =FALSE, linewidth =1.2) +geom_point(data = mittelwerte_flug4_filtered,aes(x =as.factor(segment), y = mean_sinuosity, color ="Mittelwert"),inherit.aes =FALSE, size =2) +labs(x ="Flugsegmente", y ="Sinuosität (log2)", fill ="Individuen", color ="") +scale_y_continuous(trans ="log2", breaks =c(1, 1.25, 1.5, 1.75, 2), limits =c(1, 2)) +scale_fill_d3("category20") +scale_color_manual(values ="black", labels ="Mittelwert") +theme_minimal(base_size =14) +theme(legend.position ="right") +ggtitle("Sinuosität Flug 4 - fokussierter Plot")# -------------------------## Sinuosität des Individuums S auf Flug 4 - Karte# -------------------------## Filter nach Flug 4 und Individuum Sind_s_flug4 <- flug4 |>filter(tag.local.identifier =="S") |>arrange(timestamp)# Sinuosität pro Segment berechnens_sinuo <- ind_s_flug4 |>group_by(segment) |>summarise(sinuosity =berechne_sinuositaet(cur_data()), .groups ="drop")# Sinuositätswerte mit Segmentdaten verknüpfenind_s_flug4 <- ind_s_flug4 |>left_join(s_sinuo, by ="segment")# Farbverlauf und Skalierung festlegenfarben <-c("blue", "skyblue", "lightblue", "mistyrose", "red")max_sinuo <-max(ind_s_flug4$sinuosity, na.rm =TRUE)werte <-c(1, 1.15, 1.5, 2, max_sinuo)werte_log <- scales::rescale(log(werte), to =c(0, 1))# Mittelpunkte für Segmentbeschriftung berechnenlabels <- ind_s_flug4 |>group_by(segment) |>summarise(lon =mean(location.long),lat =mean(location.lat),label =as.character(segment),.groups ="drop" )# Plotggplot(ind_s_flug4, aes(x = location.long, y = location.lat, group = segment, color = sinuosity)) +geom_path(linewidth =1.2) +geom_text(data = labels,aes(x = lon, y = lat, label = label),inherit.aes =FALSE,size =4.5, fontface ="bold", color ="black") +coord_equal() +scale_color_gradientn(colours = farben,values = werte_log,trans ="log2",limits =c(1, max_sinuo),breaks =c(1, 2, 5, 10, round(max_sinuo)),labels =c("1", "2", "5", "10", as.character(round(max_sinuo))),oob = squish,name ="Sinuosität (log2)" ) +labs(x ="Longitude", y ="Latitude") +theme_minimal(base_size =14)# -------------------------## Vergleich: Individuum S über alle Flüge# -------------------------## Filter nach Individuum Sind_s <- alle_fluege |>filter(tag.local.identifier =="S")# Sinuosität pro Segment und Flug berechnens_summary <- ind_s |>group_by(comments, segment) |>summarise(sinuosity =berechne_sinuositaet(cur_data()), .groups ="drop") |>filter(!is.na(sinuosity) & sinuosity >=1)# Flugbezeichnungen umcodierens_summary <- s_summary |>mutate(comments =recode(comments,"homing flight 1"="Flug 1","homing flight 2"="Flug 2","homing flight 3"="Flug 3","homing flight 4"="Flug 4","homing flight 5"="Flug 5" ))# Mittelwerte pro Segment berechnenmittelwerte_s <- s_summary |>group_by(segment) |>summarise(sinuosity =mean(sinuosity), .groups ="drop") |>mutate(comments ="Mittelwert")# Plotdaten zusammenführens_plotdata <-bind_rows(s_summary, mittelwerte_s)# Plotggplot(s_plotdata, aes(x = segment, y = sinuosity, color = comments, group = comments)) +geom_line(linewidth =1.2) +geom_point(size =2) +scale_color_manual(values =c(setNames(RColorBrewer::brewer.pal(5, "Set1"), paste0("Flug ", 1:5)),"Mittelwert"="black" ) ) +scale_x_continuous(breaks =1:8, labels =as.character(1:8)) +scale_y_continuous(trans ="log2",breaks =c(1, 1.5, 2, 4, 8, 16),labels =c("1", "1.5", "2", "4", "8", "16") ) +labs(x ="Flugsegmente",y ="Sinuosität (log2)",color ="Flug" ) +theme_minimal(base_size =14)# -------------------------## Karte: Individuum S über alle Flüge# -------------------------## Filter nach Individuum Sind_s_all <- alle_fluege |>filter(tag.local.identifier =="S") |>arrange(comments, timestamp)# Sinuosität pro Segment und Flug berechnensinuo_s_all <- ind_s_all |>group_by(comments, segment) |>summarise(sinuosity =berechne_sinuositaet(cur_data()), .groups ="drop") |>filter(!is.na(sinuosity))# Sinuositätswerte mit Segmentdaten verknüpfenind_s_all <- ind_s_all |>left_join(sinuo_s_all, by =c("comments", "segment"))# Farbverlauf und Skalierung definierenfarben <-c("blue", "skyblue", "lightblue", "mistyrose", "red")werte <-c(1, 1.25, 1.5, 2, 22)werte_log <- scales::rescale(log2(werte), to =c(0, 1))# Farbskalen-Beschriftungen definierenbreaks_custom <-c(1, 2, 5, 10, 22)labels_custom <-as.character(breaks_custom)# Plotggplot(ind_s_all, aes(x = location.long, y = location.lat, group =interaction(comments, segment))) +geom_path(aes(color = sinuosity), linewidth =0.8, alpha =0.95) +scale_color_gradientn(colours = farben,values = werte_log,trans ="log2",limits =c(1, 22),breaks = breaks_custom,labels = labels_custom,oob = squish,name ="Sinuosität (log2)" ) +coord_equal() +labs(x ="Longitude", y ="Latitude") +theme_minimal(base_size =14)
Die Sinuosität wurde als Mass für die Richtungsabweichung innerhalb von Flugtrajektorien berechnet. Sie ergibt sich aus dem Verhältnis der tatsächlich zurückgelegten Strecke zur Luftlinie zwischen Start- und Endpunkt eines Segments. Die Berechnung erfolgte segmentweise für jeweils acht vordefinierte Abschnitte pro Flug. Innerhalb jedes Segments wurde die Streckenlänge als Summe der Haversine-Distanzen zwischen aufeinanderfolgenden Punkten berechnet (distHaversine(), Paket geosphere), und die Luftliniendistanz ebenfalls mit distHaversine() bestimmt. Die Auswertung erfolgte in R, gruppiert nach Segment und Individuum (Flug 4) bzw. nach Segment und Flug (Individuum S), um Unterschiede in der Sinuosität sowohl zwischen Individuen als auch zwischen Flügen zu vergleichen.
Führungsdynamik
Der Datensatz des Fluges Nr. 4 aller Individuen wurde zunächst nach der individuellen Kennung individual-local-indentifier der Tauben und dem Zeitstempel timestamp sortiert, um eine klare zeitliche Reihenfolge zu gewährleisten. Anschliessend wurde für jedes Individuum der Positionsunterschied zwischen aufeinander folgenden Zeitpunkten berechnet, indem die geographische Koordinate des nächsten Punktes (location-long, location-lat) für jede Taube ermittelt wurde. Dies ermöglichte es, die Bewegungsrichtung der einzelnen Individuen durch Berechnung der Differenzwerte dx und dy festzulegen.
Im nächsten Schritt wurde der mittlere Schwarmvektor für jeden Zeitstempel berechnet, um die allgemeine Flugrichtung des gesamten Schwarms zu bestimmen. Dieser Schwarmvektor setzte sich aus dem Mittelwert der Bewegungsdifferenzen (dx, dy) aller Individuen zu einem bestimmten Zeitpunkt zusammen. Um zu erfassen, wie sich jedes Individuum im Verhältnis zum Schwarm verhält, wurde für jedes Individuum der relative Positionsvektor zum Schwarmmittelpunkt berechnet. Mit diesem Vektor wurde die Projektion auf die Schwarmbewegungsrichtung berechnet, um zu ermitteln, wie stark jedes Individuum in die Richtung des Schwarms fliegt.
Anschliessend wurde für jeden Zeitstempel das Individuum identifiziert, dessen Projektionswert den höchsten Wert aufwies, was darauf hinweist, dass es sich in diesem Zeitraum in der Führungsposition befand. Diese Informationen ermöglichten es, zu bestimmen, welche Taube zu welchem Zeitpunkt die Führungsrolle übernahm.
Zur Visualisierung des Führungsverhaltens wurden die Führungsanteile über die Zeit in einem Histogramm dargestellt. Zusätzlich wurden die Führungspositionen auf einer Karte mit leaflet visualisiert, wobei die Individuen durch unterschiedliche Farben dargestellt wurden. Eine statische Darstellung mit ggplot2 und sf ermöglichte eine detaillierte Analyse der Führungsmarkierungen über den gesamten Flugverlauf hinweg.
ChatGPT wurde beigezogen.
Resultate
What was achieved overall?
How well are the results presented?
How well are the results discussed in the light of the theory?
Ähnlichkeitmasse
Dynamic Time Warping (DTW)
Für den Flug 4 lassen sich die Trajektorien auf Basis ihres räumlichen Verlaufs in drei grobe Klassen einteilen, wobei insbesondere die Individuen N und Q räumliche Abweichungen zu den anderen Individuen zeigen, aber jeweils einen ähnlichen Verlauf haben. Die weiteren beiden Cluster, welche untereinander ähnliche räumliche Trajektorien aufweisen, sind zum einen R, S und U und zum anderen M, O, P und T. Der Vergleich zwischen den Flügen zeigt, dass sich der räumliche Verlauf der Trajektorien der Flüge 3, 4 und 5 sehr ähnlich sind, während sich die Flüge 1 und 2 von den anderen Flügen unterscheiden und insbesondere auch voneinander sehr unterschiedlich sind.
Normiertes Ähnlichkeitsmass DTW der Trajektorien aller Individuen auf Flug 4
Normiertes Ähnlichkeitsmass DTW der Trajektorien aller fünf Flüge des Individuums S
Edit Distance
Für den Flug 4 lassen lassen sich die Trajektorien wie bereits bei DTW in drei grobe Cluster einteilen, basierend auf der raum-zeitlichen Ähnlichkeit von GPS-Messpunkten. Wiederum unterscheiden sich die Trajektorien der Individuen N und Q raum-zeitlich von den anderen Individuen und haben im Vergleich zueinander einen ähnlichen Verlauf. Auch die beiden anderen Cluster - R, S und U sowie M, O, P und T – stimmen mit den Ergebnissen des DTW überein. Der Vergleich zwischen den Flügen zeigt, dass mit EditDist keine raum-zeitliche Ähnlichkeit der GPS-Messpunkte erfasst wird.
Normiertes Ähnlichkeitsmass EditDist der Trajektorien aller Individuen auf Flug 4
Normiertes Ähnlichkeitsmass EditDist der Trajektorien aller fünf Flüge des Individuums S
Fréchet Distance
Anders als bei den bisherigen Ähnlichkeitsmassen lassen sich die Trajektorien für den Flug 4 in zwei grobe Cluster einteilen, P, R, S und U sowie M, N, O, Q und T. Die neuen Cluster sind erklärbar dadurch, dass die Fréchet-Distance als Methode sich von den bisherigen Ähnlichkeitsmassen stark unterscheidet und sich über den maximalen minimalen Abstand der Trajektorien definiert. Der Vergleich zwischen den Flügen zeigt jedoch ein ähnliches Muster wie DTW, der maximale minimale Abstand zwischen den Flügen 3, 4 und 5 ist vergleichsweise gering, bei den Flügen 1 und 2 im Vergleich mit den anderen Flügen ist er hoch und sehr hoch zwischen den Flügen 1 und 2.
Normiertes Ähnlichkeitsmass Fréchet der Trajektorien aller Individuen auf Flug 4
Normiertes Ähnlichkeitsmass Fréchet der Trajektorien aller fünf Flüge des Individuums S
Longest Common Subsequence (LCSS)
Für den Flug 4 lassen sich die Trajektorien auf Basis der Länge von gemeinsamen Teilsequenzen wiederum in die drei Cluster einteilen, welche mit den Clustern von DTW und EditDist übereinstimmen. Auffällig ist wiederum, dass sich die Trajektorien N und Q stark von den anderen Trajektorien unterscheiden, untereinander jedoch sehr ähnlich sind. Der Vergleich zwischen den Flügen konnte aufgrund des hohen Rechenaufwands der Methode nicht durchgeführt werden.
Normiertes Ähnlichkeitsmass LCSS der Trajektorien aller Individuen auf Flug 4
Geschwindigkeit
Text im Entwurf Zur Analyse der Fluggeschwindigkeit wurden die mittleren Segmentgeschwindigkeiten in Abhängigkeit der relativen Position im Flugverlauf untersucht. In den entsprechenden Abbildungen (Verweis) ist erkennbar, dass die Geschwindigkeit der Brieftauben typischerweise einem wiederkehrenden Muster folgt: Zu Beginn des Fluges ist die Geschwindigkeit meist relativ gering, was vermutlich auf eine Orientierungsphase in der Nähe der Freilassungsstelle zurückzuführen ist. Im weiteren Verlauf steigt die Geschwindigkeit deutlich an und erreicht häufig im mittleren Abschnitt des Fluges ein Maximum. Gegen Ende des Fluges zeigt sich bei vielen Flügen wieder ein leichter Abfall der Geschwindigkeit. Dieses Muster ist sowohl bei der gruppenübergreifenden Betrachtung verschiedener Flüge (Verweis Scatterplot Ind S) als auch bei den Einzelflügen aller Individuen eines Fluges (Verweis Scatterplot Flug 4) deutlich zu erkennen. Insgesamt bestätigen diese Befunde die in der Literatur und in unseren Erwartungen formulierte Annahme, dass Brieftauben nach einer kurvenreicheren Anfangsphase zunehmend geradliniger und schneller fliegen, bevor sie in der Endphase möglicherweise wieder Tempo verlieren [vgl. @schiffner2009].
Code Geschwindigkeitsanalysen des Individuums S
# Liste der Flügefluege <-unique(Ind_S$comments)# Funktion zur Einteilung der Flüge in gleiche Zeitsegmentedetect_segments_limited <-function(flug_name) { df <- Ind_S |>filter(comments == flug_name) |>arrange(timestamp)if (nrow(df) <5) return(NULL) # Optional: Wenn zu wenig Daten df <- df |>mutate(total_time =as.numeric(difftime(max(timestamp), min(timestamp), units ="secs")),time_rel =as.numeric(difftime(timestamp, min(timestamp), units ="secs")) / total_time,segment =cut(time_rel, breaks =seq(0, 1, length.out = max_segments +1),labels =FALSE, include.lowest =TRUE),flug = flug_name )return(df)}# Alle Flüge in Segmente aufteilenseg_df_limited <-map_df(fluege, detect_segments_limited)# Segmentindex als Grundlage für diverse Plots:seg_summary <- seg_df_limited |>group_by(flug, segment) |>summarise(mean_speed =mean(`ground-speed`, na.rm = T),segment_index =first(segment),n_segments =n_distinct(segment),.groups ="drop") |>group_by(flug) |>mutate(segment_position = segment_index /max(segment_index))# Plot: Trajektorien einfärben nach der mittleren Segment-Geschwindigkeitseg_avg_speed <- seg_df_limited |>group_by(flug, segment) |>summarise(avg_speed =mean(`ground-speed`, na.rm =TRUE)) |>ungroup()seg_df_limited <- seg_df_limited |>left_join(seg_avg_speed, by =c("comments"="flug", "segment"="segment"))ggplot(seg_df_limited |>arrange(timestamp),aes(x =`location-long`, y =`location-lat`, color = avg_speed)) +geom_path(aes(group = comments), size =1.2) +scale_color_gradientn(colors =c("blue", "lightblue", "mistyrose", "red")) +labs(title ="Flugtrajektorien mit mittlerer Segment-Geschwindigkeit", x ="Längengrad", y ="Breitengrad", color ="Mittlere Geschwindigkeit (m/s)") +theme_minimal(base_size =14) +theme(legend.position ="right")
Code Geschwindigkeitsanalysen des Individuums S
ggplot(seg_summary, aes(x = segment_position, y = mean_speed, color = flug)) +geom_point(size =3) +geom_smooth(method ="loess", se =FALSE) +scale_color_brewer(palette ="Set1") +labs(title ="Mittlere Segmentgeschwindigkeit relativ zur Flugposition",x ="Relative Position im Flug (0 = Start, 1 = Ende)",y ="Mittlere Geschwindigkeit (m/s)", color ="Flug") +theme_minimal(base_size =14)
Code Geschwindigkeitsanalysen des Fluges Nr. 4
# Liste der Flügefluege <-unique(Flug_4$`individual-local-identifier`)# Funktion zur Einteilung der Flüge in gleiche Zeitsegmentedetect_segments_limited <-function(indiv_name) { df <- Flug_4 |>filter(`individual-local-identifier`== indiv_name) |>arrange(timestamp)if (nrow(df) <5) return(NULL) # Optional: Wenn zu wenig Daten df <- df |>mutate(total_time =as.numeric(difftime(max(timestamp), min(timestamp), units ="secs")),time_rel =as.numeric(difftime(timestamp, min(timestamp), units ="secs")) / total_time,segment =cut(time_rel, breaks =seq(0, 1, length.out = max_segments +1),labels =FALSE, include.lowest =TRUE),indiv = indiv_name )return(df)}# Alle Flüge in Segmente aufteilenseg_df_limited <-map_df(fluege, detect_segments_limited)# Segmentindex als Grundlage für diverse Plots:seg_summary <- seg_df_limited |>group_by(indiv, segment) |>summarise(mean_speed =mean(`ground-speed`, na.rm = T),segment_index =first(segment),n_segments =n_distinct(segment),.groups ="drop") |>group_by(indiv) |>mutate(segment_position = segment_index /max(segment_index))# Plot: Trajektorien einfärben nach der mittleren Segment-Geschwindigkeitseg_avg_speed <- seg_df_limited |>group_by(indiv, segment) |>summarise(avg_speed =mean(`ground-speed`, na.rm =TRUE)) |>ungroup()seg_df_limited <- seg_df_limited |>left_join(seg_avg_speed, by =c("individual-local-identifier"="indiv", "segment"="segment"))ggplot(seg_df_limited, aes(x =`location-long`, y =`location-lat`, color = avg_speed)) +geom_path(aes(group =`individual-local-identifier`), size =1.2) +facet_wrap(~`individual-local-identifier`) +scale_color_gradientn(colors =c("blue", "lightblue", "mistyrose", "red")) +labs(title ="Flugtrajektorien mit mittlerer Segment-Geschwindigkeit", x ="Längengrad", y ="Breitengrad", color ="Mittlere Geschwindigkeit (m/s)") +theme_minimal(base_size =14) +theme(legend.position ="right")
Code Geschwindigkeitsanalysen des Fluges Nr. 4
ggplot(seg_summary, aes(x = segment_position, y = mean_speed, color = indiv)) +geom_point(size =3) +geom_smooth(method ="loess", se =FALSE) +scale_color_brewer(palette ="Set1") +labs(title ="Mittlere Segmentgeschwindigkeit relativ zur Flugposition",x ="Relative Position im Flug (0 = Start, 1 = Ende)",y ="Mittlere Geschwindigkeit (m/s)", color ="Flug") +theme_minimal(base_size =14)
Sinuosität
Die Sinuosität im Flug 4 ist insbesondere in den Flugsegmenten 1 und 3, Segemente zu Beginn des Fluges, sehr hoch (Verlinkung). Zugleich ist ersichtlich, dass sich die Individuen untereinander kaum in der Sinuosität der Flugtrajektorien unterscheiden.
Logarithmierte Sinuosität der Trajektorien aller Individuen in Flug 4, aufgeteilt nach Flugsegmenten
Logarithmierte Sinuosität der Trajektorien aller Individuen in Flug 4, aufgeteilt nach Flugsegmenten, ohne Flugsegment 3 mit sehr hoher Sinuosität
Die Karte der Flugtrajektorie des Fluges 4, Individuum S zeigt, dass die hohe Sinuosität des Segements 3 durch eine geflogene Schleife verursacht wird. Weiter ist die hohe Sinuosität des Flugsegmentes 1 ersichtlich.
Die Sinuosität der Flugtrajektorien des Individuums S über alle Flüge zeigt das tendenzielle Muster aus dem Flug 4, dass sie Sinuosität zu Beginn des Fluges sehr hoch ist und danach abnimmt, sehr deutlich. Insbesondere die Flüge 2 und 4 haben in den ersten Flugphasen eine hohe Sinuosität. Einzig bei Flug 1 zeigt sich zu Beginn des Fluges keine höhere Sinuosität.
Das gleiche Muster der hohen Sinuosität zu Beginn des Fluges ist wiederum in der Karte der Sinuosität der Flugtrajektorien des Individuum S ersichtlich.
Führungsverhalten
Text folgt Die Auswertung der Führungszeiten zeigt eine deutliche Hierarchie: Das Individuum «M» übernahm mit Abstand am häufigsten die Führungsposition, gefolgt von den Individuen «O» und «T». Demgegenüber zeigten andere Individuen wie «N» oder «R» nur selten Führungsverhalten. Diese klaren Unterschiede stützen die Hypothese, dass innerhalb des Schwarms stabile „Lead and Follow“-Dynamiken existieren, wie sie auch in früheren Studien beschrieben wurden [vgl. @nagy2010]. Die Resultate deuten darauf hin, dass bestimmte Individuen überproportional häufig die Navigation des Schwarms übernehmen.
Code Führungsdynamik Flug 4
# Anwendung auf Flug 4 - braucht mehrere Individuen zur Analyse.Flug_4 <- Flug_4 |>arrange(`individual-local-identifier`, timestamp) |>group_by(`individual-local-identifier`) |>mutate(lon_next =lead(`location-long`),lat_next =lead(`location-lat`),dx = lon_next -`location-long`,dy = lat_next -`location-lat` ) |>ungroup()# Berechne pro Zeitstempel den mittleren Schwarmvektorschwarm <- Flug_4 |>group_by(timestamp) |>summarise(mean_dx =mean(dx, na.rm =TRUE),mean_dy =mean(dy, na.rm =TRUE),center_lon =mean(`location-long`, na.rm =TRUE),center_lat =mean(`location-lat`, na.rm =TRUE) )# Join Schwarmrichtung wieder zurückFlug_4 <- Flug_4 |>left_join(schwarm, by ="timestamp") |>rowwise() |>mutate(rel_lon =`location-long`- center_lon,rel_lat =`location-lat`- center_lat,projection = (rel_lon * mean_dx + rel_lat * mean_dy) /sqrt(mean_dx^2+ mean_dy^2) ) |>ungroup()# Wer hat pro Zeit den höchsten Projektionswert?Flug_4 <- Flug_4 |>group_by(timestamp) |>mutate(is_leader = projection ==max(projection, na.rm =TRUE) )# Farbzuordnung für jedes individuelle Identifikationsmerkmalcolor_palette <-scale_fill_manual(values = RColorBrewer::brewer.pal(length(unique(Flug_4$`individual-local-identifier`)), "Set1"))Flug_4 |>filter(is_leader) |>count(`individual-local-identifier`) |>ggplot(aes(x =reorder(`individual-local-identifier`, -n), y = n, fill =`individual-local-identifier`)) +geom_col() + color_palette +labs(title ="Führungsanteile über die Zeit", x ="Taube", y ="Anzahl Sekunden in Führung") +theme_minimal() +theme(legend.position ="none")
Code Führungsdynamik Flug 4
Flug_4 |>filter(is_leader) |>count(timestamp, `individual-local-identifier`) |>ggplot(aes(x = timestamp, fill =`individual-local-identifier`)) +geom_histogram(binwidth =5, position ="stack") + color_palette +labs(title ="Führungszeitpunkte je Taube", x ="Zeit", y ="Anzahl Zeitpunkte (pro Bin)") +theme_minimal() +theme(legend.position ="bottom")
Code Führungsdynamik Flug 4
# Farbzuordnung für jedes individuelle Identifikationsmerkmalcolor_palette <-colorFactor(palette ="Set1", domain =unique(Flug_4$`individual-local-identifier`))# Visualisierung mit Leaflet: Nur die Punkte der Führer einfärbenleaflet(data = Flug_4 |>filter(is_leader)) |>addTiles() |>addCircleMarkers(lng =~`location-long`, lat =~`location-lat`,color =~color_palette(`individual-local-identifier`), radius =5,stroke =FALSE,fillOpacity =0.7,label =~paste("Leader: ", `individual-local-identifier`, "<br>Time: ", timestamp) ) |>addLegend(position ="bottomright", pal = color_palette,values =unique(Flug_4$`individual-local-identifier`),title ="Individuen" )
Code Führungsdynamik Flug 4
# Konvertiere die Daten in ein sf-Objekt für ggplot2Flug_4_sf <- Flug_4 |>filter(is_leader) |>st_as_sf(coords =c("location-long", "location-lat"), crs =4326)
Diskussion
Are the conclusions drawn from the project adequate and coherent?
Are problems that showed up been reported and alternative solutions proposed?
Have data science choices been discussed in the light of the theory?
Ähnlichkeit hat gezeigt, dass insbesondere 2 Individuen (N und Q) sich von den anderen Individuen unterscheiden im Flug 4. Bei den Flügen unterscheiden sich Flug 1 und 2 von den sich ähnelnden Flügen 3-5, wobei Flug 1 und 2 sehr unterschiedlich voneinander sind. Herausfordernd war, dass die Unterschiede zwischen den Individuen eines Fluges sehr gering sind und zwischen den Flügen sehr hoch, was nur durch Logarithmisieren lösbar war. Als besonders geeignet für die vorliegende Untersuchung wird das Dynamic Time Warping beurteilt.
Hohe Sinuosität zu Beginn, deckt sich mit Literatur (Orientierungsphase). Keine Sinuosität zum Schluss, warum? Nur Gruppenflug in den Daten, Individuen haben sich gegen Ende aufgeteilt – letzter Teil des Fluges ist in den Daten nicht verfügbar – Aussage
Anhang
How was research implemented addressing the Research questions?
What about problems and limitations and strategies overcoming these?
Simple solutions vs. own original techniques
Is the code properly commented/annotated?
Verwendung von AI transparent dokumentiert?
XX ## Wordcount
Code
# wordcountaddin::word_count("index.qmd")
How much effort was put in documenting the work?
Is the structure reasonable and clear? Length of report (approx. 15000 char (incl. spaces, incl. References list, excl. Code listing), 20000 char max)
Is the language clear and correct?
Are references used, correctly cited and listed?
If used, is the use of Generative AI (e.g. ChatGPT) documented and transparent?
Are figures and tables clear and produced to high standards?