library(tidyverse)
library(lubridate)
library(skimr)
library(leaflet)
library(janitor)
library(geosphere)
library(igraph)
library(sf)
library(sfnetworks)
library(ggraph)
library(pheatmap)
library(tmap)
library(tmaptools)
library(report)
Bewegungsmuster des Europäischen Aals (Anguilla anguilla) in kanalisierten Gewässern
Projekt - Patterns & Trends in Environmental Data / Computational Movement Analysis Geo 880
Gleiche Bewertung gewünscht
1 Einleitung
Der Europäische Aal (Anguilla anguilla) ist eine katadrome Fischart, deren Bestände in den letzten Jahrzente drastisch zurückgegangen sind (Piper u. a. 2017; Verhelst u. a. 2018). Aufgrund seines komplexen Lebenszyklus und der Vielzahl an anthropogenen Einflüssen, die seine Wanderungen behindern (z. B. Querbauwerke, Habitatverlust, Wasserverschmutzung), gilt der Aal heute als stark gefährdet (Halvorsen u. a. 2020; Piper u. a. 2013). Vor diesem Hintergrund ist ein vertieftes Verständnis seines Bewegungsverhaltens in verschiedenen Gewässertypen entscheidend für die Entwicklung wirksamer Schutz- und Managementstrategien.
Ziel dieser Studie ist es, das Bewegungsverhalten der Aalen in einem kanalisierten Polder- und Flusssystem besser zu verstehen. Im Fokus stehen dabei sowohl individuelle als auch kollektive Verhaltensmuster, die durch akustische Telemetriedaten sichtbar gemacht werden. Daraus ergeben sich die folgenden zwei zentralen Forschungsfragen: erstens werden die zeitlichen Muster der Aale untersucht, wobei der Schwerpunkt auf ihrem zirkadianen Zyklus (Tag und Nacht) und den monatlichen Bewegungen liegt, um zu prüfen, ob es signifikante Unterschiede in ihren Bewegungen gibt; Zweitens wird untersucht, wie sich die Wahl des räumlichen Modells auf die Analyse des Aalbewegens auswirkt, indem der beschränkte Bewegungsraum (Constrained Movement Space, CMS), basierend auf einem entlang des hydrografischen Netzwerks verlaufenden Wegnetz, mit dem unbeschränkten Bewegungsraum (UCMS) verglichen wird, der auf linearen Entfernungen zwischen Vermessungspunkten basiert. Es soll geprüft werden, ob die Verwendung eines CMS einen Mehrwert liefert.
2 Methoden
Die Datengrundlage dieser Studie stammt aus dem Projekt 2012_LEOPOLDKANAAL des Research Institute for Nature and Forest (INBO) (Verhelst u. a. 2020). Über einen mehrmonatigen Zeitraum wurden 94 Individuen des Europäischen Aals mit akustischen Telemetriesendern markiert. Ihre Bewegungen wurden automatisch erfasst, sobald sie in den Erfassungsbereich von einem der 67 stationären Empfänger gelangten. Der daraus entstandene Datensatz umfasst rund 2,2 Millionen Registrierungspunkte und ist öffentlich über GBIF zugänglich (Verhelst u. a. 2024). Ergänzt wird dieser durch einen detaillierten Metadatensatz mit Informationen zu den besenderten Individuen. Für die Analyse benötigte Gewässerabschnitte wurden in QGIS vom Gewässernetz von Open Street Map (OpenStreetMap contributors 2017) selektiert, in R importiert und in ein ungerichtetes räumliches Netzwerk umgewandelt.
Die Bewegungen der Tiere wurden innerhalb eines beschränkten (Gewässernetz) und unbeschränktem Bewegungsraum untersucht. Darauf basierend wurden die Bewegungsparameter Geschwindigkeit und Migrationsdistanz berechnet.
Die Analyse des Bewegungsverhaltens erfolgte in R (R Core Team 2024), unter Verwendung der Pakete tidyverse (Wickham u. a. 2019), lubridate (Grolemund und Wickham 2011), skimr (Waring u. a. 2022), leaflet (Cheng u. a. 2024), janitor (Firke 2024), geosphere (Hijmans 2024), igraph (Csardi und Nepusz 2006), sf (Pebesma 2018; Pebesma und Bivand 2023), sfnetworks (Meer u. a. 2024), ggraph (Pedersen 2024), tmap (M. Tennekes 2018), und tmaptools (Martijn Tennekes 2025). Die massgeblichen Arbeits- und Analyseschritte wurden ohne generative Sprachmodelle erstellt. ChatGPT4o (OpenAI 2025) wurde jedoch als Coding- und Korrektur-Hilfe verwendet.
1.1 Packages
1.2 Datenimport
<- read_delim("detections.csv")
aalen <- read_delim("abwanderung_true.csv", ",")
sea_stations <- st_read("geodata/OSM_selected_010425_2.shp") |>
waterways_sf st_transform(3035) |>
select(full_id)
Reading layer `OSM_selected_010425_2' from data source
`C:\Users\markv\Desktop\Master\2 semester\Patterns and Trends in Environmental Data\semester_project\project_moos_brandenberg_FS25\geodata\OSM_selected_010425_2.shp'
using driver `ESRI Shapefile'
Simple feature collection with 64 features and 74 fields
Geometry type: LINESTRING
Dimension: XY
Bounding box: xmin: 2.94047 ymin: 51.20089 xmax: 3.795233 ymax: 51.34303
Geodetic CRS: WGS 84
1.3 Filter: Aale die das Meer erreichten Da der Datensatz sehr umfangreich ist und viele Fische analysiert wurden, wurde beschlossen, sich nur auf die Aale zu konzentrieren, die das Meer erreicht haben.
<- aalen |>
sea_arrivers mutate(animal_id = as.factor(animal_id)) |>
group_by(animal_id) |>
filter(any(station_name == "bh-37")) |>
ungroup()
1.4 DatenÜberblick Überblick über die Daten
# Kompakter Überblick über Spaltennamen, Datentypen und Beispielwerte
glimpse(sea_arrivers)
Rows: 423,945
Columns: 23
$ pk <dbl> 21986012, 21494419, 22624927, 21740163, 2138…
$ date_time <dttm> 2012-10-16 15:02:41, 2012-10-16 15:06:27, 2…
$ receiver_id <chr> "VR2W-112285", "VR2W-112285", "VR2W-112285",…
$ application_type <chr> "acoustic_telemetry", "acoustic_telemetry", …
$ network_project_code <chr> "leopold", "leopold", "leopold", "leopold", …
$ tag_id <chr> "A69-1303-3552", "A69-1303-3552", "A69-1303-…
$ tag_fk <dbl> 209, 209, 209, 209, 209, 209, 209, 209, 209,…
$ animal_id <fct> 231, 231, 231, 231, 231, 231, 231, 231, 231,…
$ animal_project_code <chr> "2012_leopoldkanaal", "2012_leopoldkanaal", …
$ scientific_name <chr> "Anguilla anguilla", "Anguilla anguilla", "A…
$ station_name <chr> "bh-31", "bh-31", "bh-31", "bh-31", "bh-31",…
$ deploy_latitude <dbl> 51.29037, 51.29037, 51.29037, 51.29037, 51.2…
$ deploy_longitude <dbl> 3.716447, 3.716447, 3.716447, 3.716447, 3.71…
$ sensor_type <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ sensor_value <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ sensor_unit <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ sensor_value_depth <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ sensor_value_acceleration <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ sensor_value_temperature <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ signal_to_noise_ratio <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ source_file <chr> "inbo_data_file", "inbo_data_file", "inbo_da…
$ qc_flag <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ deployment_fk <dbl> 1416, 1416, 1416, 1416, 1416, 1416, 1416, 14…
# Ausführliche Zusammenfassung
skim(sea_arrivers)
Name | sea_arrivers |
Number of rows | 423945 |
Number of columns | 23 |
_______________________ | |
Column type frequency: | |
character | 8 |
factor | 1 |
logical | 7 |
numeric | 6 |
POSIXct | 1 |
________________________ | |
Group variables | None |
Variable type: character
skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
---|---|---|---|---|---|---|---|
receiver_id | 0 | 1 | 11 | 11 | 0 | 56 | 0 |
application_type | 0 | 1 | 18 | 18 | 0 | 1 | 0 |
network_project_code | 0 | 1 | 3 | 10 | 0 | 4 | 0 |
tag_id | 0 | 1 | 13 | 14 | 0 | 33 | 0 |
animal_project_code | 0 | 1 | 18 | 18 | 0 | 1 | 0 |
scientific_name | 0 | 1 | 17 | 17 | 0 | 1 | 0 |
station_name | 0 | 1 | 3 | 10 | 0 | 54 | 0 |
source_file | 0 | 1 | 14 | 26 | 0 | 23 | 0 |
Variable type: factor
skim_variable | n_missing | complete_rate | ordered | n_unique | top_counts |
---|---|---|---|---|---|
animal_id | 0 | 1 | FALSE | 33 | 410: 139442, 403: 58318, 441: 36326, 414: 32913 |
Variable type: logical
skim_variable | n_missing | complete_rate | mean | count |
---|---|---|---|---|
sensor_type | 423945 | 0 | NaN | : |
sensor_value | 423945 | 0 | NaN | : |
sensor_unit | 423945 | 0 | NaN | : |
sensor_value_depth | 423945 | 0 | NaN | : |
sensor_value_acceleration | 423945 | 0 | NaN | : |
sensor_value_temperature | 423945 | 0 | NaN | : |
signal_to_noise_ratio | 423945 | 0 | NaN | : |
Variable type: numeric
skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
---|---|---|---|---|---|---|---|---|---|---|
pk | 0 | 1.0 | 24149375.63 | 6799330.35 | 20581624.00 | 21227280.00 | 21877117.00 | 22523802.00 | 78978072.00 | ▇▁▁▁▁ |
tag_fk | 0 | 1.0 | 225.60 | 29.34 | 155.00 | 227.00 | 236.00 | 240.00 | 263.00 | ▂▁▁▇▂ |
deploy_latitude | 0 | 1.0 | 51.28 | 0.04 | 51.00 | 51.24 | 51.26 | 51.33 | 51.42 | ▁▁▇▅▅ |
deploy_longitude | 0 | 1.0 | 3.73 | 0.06 | 3.54 | 3.75 | 3.75 | 3.77 | 5.11 | ▇▁▁▁▁ |
qc_flag | 381644 | 0.1 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ▁▁▇▁▁ |
deployment_fk | 0 | 1.0 | 1560.30 | 417.97 | 1327.00 | 1415.00 | 1415.00 | 1418.00 | 4495.00 | ▇▁▁▁▁ |
Variable type: POSIXct
skim_variable | n_missing | complete_rate | min | max | median | n_unique |
---|---|---|---|---|---|---|
date_time | 0 | 1 | 2012-07-19 16:08:35 | 2018-12-13 13:58:04 | 2013-01-11 00:35:21 | 380246 |
# Prüfung auf Duplikate
n_distinct(sea_arrivers$pk) == nrow(sea_arrivers) # Sollte TRUE sein
[1] TRUE
Die hohe Anzahl an NA
-Werten in den Sensorfeldern deutet darauf hin, dass viele Tags oder Empfänger keine erweiterten Sensordaten liefern. Entweder wurden einfache Tags verwendet oder entsprechende Sensoren waren nicht konfiguriert. Da der Fokus der Analyse auf Bewegungsmustern der Tiere liegt (z. B. Wanderungen, Verweildauer, Netzwerkbewegung), sind die erweiterten Sensorfelder (z. B. Temperatur, Beschleunigung) derzeit von untergeordneter Bedeutung. Zudem liegen sie für den Grossteil der Daten nicht vor. Daher werden diese Felder in den weiteren Analysen nicht berücksichtigt.
2.1 Netzwerkerstellung
Das Netzwerk wurde, orientiert an Meer u. a. (2024), vorbearbeitet und vereinfacht. Massgebende Schritte waren die Rundung von Koordinaten, Entfernung von mehrfach enthaltenen Kanten und Entfernung von Pseudo-Knoten. Für die stationären Empfänger wurden am jeweils nächstgelegenen Punkt des Netzwerks ein zusätzlicher Knoten eingefügt. Für die Knoten wurde schliesslich eine ungewichtete Origin-Destination-Matrix berechnet und darauf basierend zurückgelegte Distanz und Fortbewegungsgeschwindigkeiten für die Bewegungen der Aale kalkuliert.
Credits für Netzwerk-Workflow und Beschreibung der Funktionen: https://luukvdmeer.github.io/sfnetworks/. (Kursive Beschreibungen wurden) wortwörtlich übernommen.
2.1 Rounding coordinates
You might have a set of lines in which some endpoints are almost shared between two lines. However, the coordinates are stored with so much precision that there is a minor difference between the two points. When constructing a sfnetwork these lines will not be connected because the points are not exactly equal. We can pre-process the lines by reducing the precision of the coordinates such that the points become exactly equal.
st_geometry(waterways_sf) <- st_geometry(waterways_sf) |>
lapply(function(x) round(x, 0)) |>
st_sfc(crs = st_crs(waterways_sf))
# edge_colors = function(x) rep(sf.colors(12, categorical = TRUE)[-2], 2)[c(1:ecount(x))]
2.2 Netzwerkerstellung
<- as_sfnetwork(waterways_sf, directed = FALSE)
waterways waterways
# A sfnetwork with 75 nodes and 64 edges
#
# CRS: EPSG:3035
#
# An undirected multigraph with 12 components with spatially explicit edges
#
# Node data: 75 × 1 (active)
geometry
<POINT [m]>
1 (3829501 3147118)
2 (3828706 3147442)
3 (3887668 3145688)
4 (3887350 3145853)
5 (3849184 3158465)
6 (3849129 3158764)
# ℹ 69 more rows
#
# Edge data: 64 × 4
from to full_id geometry
<int> <int> <chr> <LINESTRING [m]>
1 1 2 w780936346 (3829501 3147118, 3829305 3147185, 3829223 3147222, 3…
2 3 4 w1062436337 (3887668 3145688, 3887609 3145700, 3887591 3145704, 3…
3 5 6 w909184452 (3849184 3158465, 3849129 3158764)
# ℹ 61 more rows
2.3 Netzwerkbereinigung
Simplify network
“A network may contain sets of edges that connect the same pair of nodes. Such edges can be called multiple edges. Also, it may contain an edge that starts and ends at the same node. Such an edge can be called a loop edge. In graph theory, a simple graph is defined as a graph that does not contain multiple edges nor loop edges. To obtain a simple version of our network, we can remove multiple edges and loop edges by calling tidygraphs edge filter functions tidygraph::edge_is_multiple() and tidygraph::edge_is_loop().”
Keine multiple Edges vorhanden, daher keine Änderung der Nodes- und Edges-Anzahl.
<- waterways |>
simplified activate("edges") |>
::arrange(edge_length()) |>
dplyr::filter(!tidygraph::edge_is_multiple()) |>
dplyr::filter(!tidygraph::edge_is_loop())
dplyr simplified
# A sfnetwork with 75 nodes and 63 edges
#
# CRS: EPSG:3035
#
# An unrooted forest with 12 trees with spatially explicit edges
#
# Edge data: 63 × 4 (active)
from to full_id geometry
<int> <int> <chr> <LINESTRING [m]>
1 34 43 w1078024209 (3886694 3146147, 3886690 3146145)
2 19 20 w1039743777 (3885294 3143943, 3885299 3143943)
3 14 63 w932092440 (3853816 3149715, 3853829 3149728)
4 8 63 w932092441 (3853804 3149701, 3853816 3149715)
5 32 48 w525661272 (3886192 3146432, 3886183 3146449)
6 17 18 w610504848 (3880280 3147303, 3880296 3147304, 3880296 3147308)
# ℹ 57 more rows
#
# Node data: 75 × 1
geometry
<POINT [m]>
1 (3829501 3147118)
2 (3828706 3147442)
3 (3887668 3145688)
# ℹ 72 more rows
Subdivide edges
When constructing a sfnetwork from a set of sf linestrings, the endpoints of those linestrings become nodes in the network. If endpoints are shared between multiple lines, they become a single node, and the edges are connected. However, a linestring geometry can also contain interior points that define the shape of the line, but are not its endpoints. It can happen that such an interior point in one edge is exactly equal to either an interior point or endpoint of another edge. In the network structure, however, these two edges are not connected, because they don’t share endpoints. If this is unwanted, we need to split these two edges at their shared point and connect them accordingly. The function to_spatial_subdivision() subdivides edges at interior points whenever these interior points are equal to one or more interior points or endpoints of other edges, and recalculates network connectivity afterwards.
<- simplified |>
subdivision ::convert(to_spatial_subdivision)
tidygraph subdivision
# A sfnetwork with 83 nodes and 93 edges
#
# CRS: EPSG:3035
#
# An undirected multigraph with 2 components with spatially explicit edges
#
# Edge data: 93 × 5 (active)
from to full_id geometry .tidygraph_edge_index
<int> <int> <chr> <LINESTRING [m]> <int>
1 1 2 w1078024209 (3886694 3146147, 3886690 31461… 1
2 3 4 w1039743777 (3885294 3143943, 3885299 31439… 2
3 5 6 w932092440 (3853816 3149715, 3853829 31497… 3
4 5 7 w932092441 (3853804 3149701, 3853816 31497… 4
5 8 9 w525661272 (3886192 3146432, 3886183 31464… 5
6 10 11 w610504848 (3880280 3147303, 3880296 31473… 6
# ℹ 87 more rows
#
# Node data: 83 × 2
geometry .tidygraph_node_index
<POINT [m]> <int>
1 (3886694 3146147) 34
2 (3886690 3146145) 43
3 (3885294 3143943) 19
# ℹ 80 more rows
# ggplot2::autoplot(simplified)
# ggplot2::autoplot(subdivision)
Pseudo-Nodes
A network may contain nodes that have only one incoming and one outgoing edge. For tasks like calculating shortest paths, such nodes are redundant, because they don’t represent a point where different directions can possibly be taken. Sometimes, these type of nodes are referred to as pseudo nodes. Note that their equivalent in undirected networks is any node with only two incident edges, since incoming and outgoing does not have a meaning there. To reduce complexity of subsequent operations, we might want to get rid of these pseudo nodes.
Es ware viele Pseudo-Nodes vorhanden und daher deutliche Vereinfachung des Netzwerks.
<- subdivision |>
smoothed ::convert(to_spatial_smooth)
tidygraph# 42 Nodes, 51 Edges smoothed
# A sfnetwork with 42 nodes and 52 edges
#
# CRS: EPSG:3035
#
# An undirected multigraph with 2 components with spatially explicit edges
#
# Edge data: 52 × 5 (active)
from to full_id .tidygraph_edge_index geometry
<int> <int> <chr> <list> <LINESTRING [m]>
1 2 3 w382434046 <int [1]> (3882977 3148479, 3882990 31485…
2 4 4 w777822385 <int [1]> (3849091 3145195, 3849091 31451…
3 4 5 w777822385 <int [1]> (3849091 3145195, 3849073 31452…
4 9 10 <NA> <int [1]> (3873537 3147308, 3873428 31473…
5 11 12 w777822345 <int [1]> (3829223 3147222, 3829198 31472…
6 14 14 w1062436267 <int [1]> (3886803 3146231, 3886803 31462…
# ℹ 46 more rows
#
# Node data: 42 × 2
geometry .tidygraph_node_index
<POINT [m]> <int>
1 (3853829 3149728) 6
2 (3882977 3148479) 19
3 (3882990 3148519) 20
# ℹ 39 more rows
# ggplot2::autoplot(smoothed)
# st_write(st_as_sf(activate(smoothed, "nodes")), "geodata/network.gpkg", layer = "smoothed")
2.4 Telemetriestationen blendern
For each POI, it finds the nearest location p on the nearest edge e. If p is an already existing node (i.e. p is an endpoint of e), it joins the information from the POI into that node. If p is not an already existing node, it subdivides e at p, adds p as a new node to the network, and joins the information from the POI into that new node. For this process, it does not matter if p is an interior point in the linestring geometry of e. The st_network_blend() function has a tolerance parameter, which defines the maximum distance a POI can be from the network in order to be blended in. Hence, only the POIs that are at least as close to the network as the tolerance distance will be blended, and all others will be ignored. The tolerance can be specified as a non-negative number. By default it is assumed its units are meters.
<- 500 # meter
blender_tolerance
# Stationen als sf-Layer
<- sea_arrivers |>
stations_sf distinct(station_name, deploy_latitude, deploy_longitude)|>
st_as_sf(coords = c("deploy_longitude", "deploy_latitude"), crs = 4326) |>
st_transform(3035)
<- smoothed |>
blended st_network_blend(st_geometry(stations_sf), tolerance = blender_tolerance)
# blended
# st_write(st_as_sf(activate(blended, "nodes")), "geodata/network.gpkg", layer = "blended")
2.5 OD-Matrix (Origin-Destination)
The shortest paths calculation is only supported for one-to-one and one-to-many routing. The alternative for many-to-many routing is the calculation of an origin-destination cost matrix. Instead of providing the individual paths, it returns a matrix in which entry i,j is the total cost (i.e. sum of weights) of the shortest path from node i to node j. The origin-destination cost matrix is usually an important starting point for further analysis. For example, it can serve as input to route optimization algorithms, spatial clustering algorithms and the calculation of statistical measures based on spatial proximity. The igraph function for this purpose is igraph::distances(), which in sfnetworks is wrapped by st_network_cost(), allowing again to provide sets of geospatial points as from and to locations. Note that the calculated costs refer to the paths between the nearest nodes of the input points. Their units are the same as the units of weights used in the calculation, in this case meters.
<- st_network_cost(blended)
cost_matrix # view(cost_matrix)
2.6 Antennenknoten bestimmen Jetzt möchten wir wissen, welche Nodes auch wirklich Antennenstandorte sind und nicht ursprüngliche Netzwerk-Nodes. Leider können keine Attribute direkt bei der st_network_blend-Funktion übernommen werden. Daher führen wir folgenden Workflow durch:
- Netzwerk-Edges mit der Blender-Toleranz buffern.
- Für Antennen innerhalb des Blender-Toleranz filtern. Da einige Antennen auch nicht von Interesse waren von uns (z.B. bereits im Meer) und daher auch nicht an das Netzwerk geblendert wurden.
- Nächste Nodes aller Antennen finden. Das kann leider nicht mit einem Column-Bind oder so durchgeführt werden, da nicht für alle Antennen eine neue Node erstellt wurde (z.B. wenn sich die Antenne am Ende eines Edges befanden, wurde kein neuer Nodes erstellt, da am nächtsten Punkt auf dem Netzwerk bereits ein ursprünglicher Node bestand).
<- blended |>
netw_buf activate("edges") |>
st_as_sf() |>
st_buffer(blender_tolerance)
<- blended |>
blended_nodes_sf activate("nodes") |>
st_as_sf() |>
mutate(
row_num = row_number()
)
# st_write(blended_nodes_sf, "geodata/network.gpkg", layer = "nodes", delete_layer = T)
# st_write(select(edges_sf, -".tidygraph_edge_index"), "geodata/network.gpkg", layer = "edges")
# st_write(stations_oi, "geodata/network.gpkg", layer = "stations_oi", delete_layer = T)
<- stations_sf |>
stations_oi filter(lengths(st_within(geometry, netw_buf)) > 0) |> # nur Stationen in Buffer
mutate(
node_nr = st_nearest_feature(geometry, blended_nodes_sf) # nächste Node
)
# tm_shape(netw_buf) +
# tm_polygons() +
# tm_shape(stations_oi) +
# tm_symbols() +
# tm_shape(blended_nodes_sf)+
# tm_symbols(col = "red")
Visualisierung
tmap_mode("view")
# Alle Nodes des Netzwerks
<- blended |>
nodes_sf activate("nodes") |>
st_as_sf() |>
filter(
!is.na(.tidygraph_node_index))
# Alle Edges des Netzwerks
<- blended |>
edges_sf activate("edges") |>
st_as_sf()
# st_write(edges_sf, "geodata/network.gpkg", layer = "edges_sf", delete_layer = T)
# Nodes für Stationen
<- blended |>
station_nodes_sf activate("nodes") |>
st_as_sf() |>
filter(
is.na(.tidygraph_node_index))
<- tm_shape(edges_sf) +
p_nw_uebersicht tm_lines(col = "black") +
tm_shape(nodes_sf) +
tm_symbols(fill = "black", size = 0.3) +
tm_shape(stations_oi) +
tm_symbols(fill = "blue", fill_alpha = 1, size = 0.5, col = "black" ) +
tm_shape(station_nodes_sf) +
tm_symbols(fill = "red", fill_alpha = 0.1, size = 0.5, col = "blue")
Das resultierende Netzwerk, welches zu Analyse verwendet wurde, enthält schliesslich Knoten und Kanten, welche das Netzwerk repräsentieren und zusätzliche Knoten für die stationären Telemetrieaantennen (Abbildung 1).
2.2 Filtrierung von stationären Phasen und falschen Bewegungen
Vor der Bewegungsanalyse wurden Filter auf den Datensatz angewandt. Die Ziele der Filterabläufe waren die Rechenzeit zu verkürzen und Effekte der Signalübertragung in den Gewässern zu vermindern. Damit ist die Problematik gemeint, dass akustische Sender bei geeigneten Bedingungen (Umgebungsgeräusche Wassertiefe, Sohlenmaterial, etc.) ein Reichweite von mehreren hundert Metern erreichen können (InnovaSea Systems 2025). Dies führt dazu, dass Individuen gleichzeitig von zwei oder mehr Empfängern registriert werden und unrealistische Bewegungsgeschwindigkeiten erreicht werden.
Um die Rechenzeit zu verkürzen wurden nur Individuen betrachtet, die ins Meer abgewandert und somit bei einem Empfänger im Meer registriert wurden. Die resultierte in einem Datensatz mit rund 424’000 Registrierungen.
Stationäre Phasen wurden zur Berechnung der Geschwindigkeiten entfernt. Als mobile Phasen wurden sich wechselnde Empfänger-Registrierungen definiert.
Oszillierende Bewegungsmuster zwischen zwei Empfängern innerhalb des maximalen Sendeintervalls der Sender (160 s) wurden mit einem Filter entfernt.
Ein Filter für unrealistisch hohe Fortbewegungsgeschwindigkeiten wurde in Ergänzung zu Filter iii) angewandt um komplexe oszillierende Bewegungsmuster zwischen >2 Antennen zu eliminieren. Die maximale Geschwindigkeit wurde als 0.8 m/s festgelegt (Quintella u. a. 2010; Tudorache u. a. 2015).
Telemetriestationen-Namen und Netzwerkknoten zusammenführen
Damit auf die Kostenmatrix zugegriffen werden kann, müssen zuerst den Stationen die entsprechenden Nodes hinzugefügt werden.
# Node-Nr. zu Station_Name joinen
<- sea_arrivers |>
sea_arrivers_network left_join(select(st_drop_geometry(stations_oi),
by = "station_name", keep = F) |>
station_name, node_nr), group_by(animal_id) |>
mutate(
days_since_deployment = as.numeric(days(date_time - min(date_time)))
)
3.1 Kein Antennenwechsel/Bewegung
options(scipen = 999)
<- sea_arrivers_network |> # ehemals: pseudo_movement
stationary_filter select(date_time, animal_id, station_name, node_nr, deploy_latitude, deploy_longitude) |>
group_by(animal_id) |>
arrange(date_time) |>
mutate(
current_node = node_nr,
before_node = lag(node_nr),
before_datetime = lag(date_time),
position_change = current_node != before_node) |>
filter(position_change == T) |>
ungroup()
3.2 Oszillierend/Unplausibles hin und herschwimmen
Sendeintervall der Tags ist auf max. 160 s eingestellt.
<- stationary_filter |>
oszill_filter group_by(animal_id) |>
arrange(date_time) |>
mutate(
previous_ziel = lag(before_node),
previous_time = lag(before_datetime),
osz_timedif = seconds(date_time - previous_time),
oszillierend = current_node == previous_ziel & osz_timedif < 165
|>
) filter(
!= T
oszillierend )
3.3 Geschwindigkeitfilter
Die Distanzen zwischen den Nodes werden aus der OD-Matrix entnommen und darauf basierend die Geschwindigkeit berechnet.
<- oszill_filter |>
speed_filtered select(date_time, animal_id, station_name, node_nr, deploy_latitude, deploy_longitude, current_node, before_node, before_datetime) |>
group_by(animal_id) |>
arrange(date_time) |>
mutate(
distance_m = mapply(function(i, j) cost_matrix[i, j], current_node, before_node),
time_difference_s = as.numeric(seconds(date_time - before_datetime)),
speed_m_s = distance_m/time_difference_s
|>
) filter(
!= 0 & speed_m_s < 0.8
distance_m |>
) ungroup()
2.3 Berechnung von Bewegungsparametern und zeitlichen Mustern
Auf Basis der gefilterten Daten wurden die individuellen zurückgelegten Strecken und die Geschwindigkeiten berechnet. Dies wurde für den beschränkten und unbeschränkten Bewegunsraum durchgeführt. Zudem wurden räumliche Unterschiede in der Geschwindigkeit pro Netzwerkkante in beschränkten Bewegungsraum berechnet.Zeitliche Aktivitätsmuster wurden ohne räumlichen Bezug, jedoch mit dem gefilterten Datensatz berechnet.
4.1 Mittlere Geschwindigkeit beschränkter Bewegungsraum
<- speed_filtered |>
speed_constrained_movement_space select(date_time, animal_id, station_name,node_nr, deploy_latitude, deploy_longitude) |>
arrange(animal_id, date_time) |>
group_by(animal_id) |>
mutate(
current_node = node_nr,
before_node = lag(node_nr),
before_datetime = lag(date_time),
distance_m = mapply(function(i, j) cost_matrix[i, j], current_node, before_node),
time_diff_s = as.numeric(difftime(date_time, lag(date_time), units = "secs")),
speed_m_s = distance_m/time_diff_s
)
mean(speed_constrained_movement_space$speed_m_s, na.rm = T)
[1] 0.09595338
sum(speed_constrained_movement_space$distance_m, na.rm = T)
[1] 439378
<- speed_constrained_movement_space |>
individual_params_cms st_drop_geometry() |>
group_by(animal_id) |>
summarise(
cms_mean_speed_m_s = mean(speed_m_s, na.rm = T),
cms_distance_m = sum(distance_m, na.rm = T)
)
4.2 Mittlere Geschwindigkeit ohne Constrained Movement
<- speed_filtered |>
speed_open_space st_as_sf(coords = c("deploy_longitude", "deploy_latitude"), crs = 4326) |>
st_transform(3035) |>
arrange(animal_id, date_time) |>
group_by(animal_id) |>
mutate(
lag_geometry = lag(geometry),
dist_m = st_distance(geometry, lag_geometry, by_element = TRUE),
time_diff_s = as.numeric(difftime(date_time, lag(date_time), units = "secs")),
speed_m_s = as.numeric(dist_m) / time_diff_s # in m/s
)
mean(speed_open_space$speed_m_s, na.rm = T)
[1] 0.09094664
sum(speed_open_space$dist_m, na.rm = T)
409280 [m]
<- speed_open_space |>
individual_params_os st_drop_geometry() |>
group_by(animal_id) |>
summarise(
os_mean_speed = mean(speed_m_s, na.rm = T),
os_distance_m = sum(dist_m, na.rm = T)
)
4.3 Bewegungsparameter der räumlichen Modell zusammenführen
Noch “Anzahl Tage unterwegs” hinzufügen
<- sea_arrivers_network |>
days group_by(animal_id) |>
mutate(
tage_unterwegs = difftime(max(date_time), min(date_time))
|>
) summarise(
tage_unterwegs = max(tage_unterwegs)
)
Parameters
# cms für Analyse mit Netzwerk
# os für Analyse über Luftlinie
<- cbind(individual_params_cms, individual_params_os[,2:3])
movement_individuals
<- movement_individuals |>
movement_individuals mutate(
cms_mean_speed_m_s = as.numeric(cms_mean_speed_m_s),
os_mean_speed = as.numeric(os_mean_speed),
cms_distance_m = as.numeric(cms_distance_m),
os_distance_m = as.numeric(os_distance_m),
speed_diff = cms_mean_speed_m_s - os_mean_speed,
dist_diff = cms_distance_m - os_distance_m,
dist_diff_dist = dist_diff/cms_distance_m
|>
) left_join(days, by = "animal_id") |>
mutate(
)
# cor(movement_individuals$cms_distance_m, movement_individuals$dist_diff_dist, use = "complete.obs")
4.3.1 Distanzen
<- pivot_longer(
movement_distance_long
movement_individuals, cols = c("cms_distance_m", "os_distance_m"),
names_to = "movement_space",
values_to = "distance_m"
)
$movement_space <- recode(movement_distance_long$movement_space,
movement_distance_long"cms_distance_m" = "CMS",
"os_distance_m" = "OS")
# Boxplot erstellen
<- ggplot(movement_distance_long, aes(x = movement_space, y = distance_m, fill = movement_space)) +
p_distanz geom_boxplot() +
labs(x = "Movement Space", y = "Distanz (m)") +
theme_minimal() +
theme(legend.position = "none")
Statistischer Test
t.test(distance_m ~ movement_space, data = movement_distance_long)
Welch Two Sample t-test
data: distance_m by movement_space
t = 0.23235, df = 53.795, p-value = 0.8172
alternative hypothesis: true difference in means between group CMS and group OS is not equal to 0
95 percent confidence interval:
-8201.312 10351.167
sample estimates:
mean in group CMS mean in group OS
15692.07 14617.14
4.3.2 Geschwindigkeit
<- pivot_longer(movement_individuals,
movement_speed_long cols = c("cms_mean_speed_m_s", "os_mean_speed"),
names_to = "movement_space",
values_to = "mean_speed_m_s")
$movement_space <- recode(movement_speed_long$movement_space,
movement_speed_long"cms_mean_speed_m_s" = "CMS",
"os_mean_speed" = "OS")
# Boxplot erstellen
<- ggplot(movement_speed_long, aes(x = movement_space, y = mean_speed_m_s, fill = movement_space)) +
p_geschwindigkeit geom_boxplot() +
labs(x = "Movement Space",
y = "Mittlere Geschwindigkeit (m/s)") +
theme_minimal() +
theme(legend.position = "none")
Statistischer Test
t.test(mean_speed_m_s ~ movement_space, data = movement_speed_long)
Welch Two Sample t-test
data: mean_speed_m_s by movement_space
t = 0.20731, df = 47.816, p-value = 0.8367
alternative hypothesis: true difference in means between group CMS and group OS is not equal to 0
95 percent confidence interval:
-0.0308116 0.0378949
sample estimates:
mean in group CMS mean in group OS
0.06257870 0.05903705
# Edges die beim Movement überquert wurden.
for (i in 1:nrow(speed_constrained_movement_space)) {
<- st_network_paths(
path
blended,from = speed_filtered$before_node[i],
to = speed_filtered$current_node[i]
)$path_edges[[i]] <- path$edge_paths
speed_filtered
}
<- blended |>
edges_sf activate("edges") |>
st_as_sf() |>
mutate(
edge_ID = row_number()
)
<- speed_filtered |>
speed_filtered_long mutate(path_edges = map(path_edges, ~ flatten_int(.x))) |>
unnest_longer(path_edges) |>
rename(edge_ID = path_edges)
<- speed_filtered_long |>
edge_speeds filter(
!= Inf
speed_m_s |>
) group_by(edge_ID) |>
summarise(
mean_speed_m_s = mean(speed_m_s)
)
# st_write(edge_speeds, "geodata/network.gpkg", layer = "edge_speeds", delete_layer = T)
<- edges_sf |>
edges_sf select(from, to, edge_ID) |>
left_join(edge_speeds, by = "edge_ID")
# st_write(edges_sf, "geodata/network.gpkg", layer = "edges_sf", delete_layer = T)
<- tm_shape(edges_sf) +
p_edge_speeds tm_lines(col = "mean_speed_m_s", palette = "viridis", lwd = 2, title.col = "Mittlere Geschwindigkeit (m/s)")
6.1 Aktivität nach Tageszeit
# Gefilterter Datensatz verwenden.
<- speed_filtered
sea_arrivers
<- sea_arrivers |>
p_tag_nacht mutate(tageszeit = if_else(hour(date_time) %in% 6:18, "Tag", "Nacht")) |>
count(tageszeit) |>
ggplot(aes(x = tageszeit, y = n, fill = tageszeit)) +
geom_col() +
labs(title = "Aktivitäten nach Tageszeiten", x = "Tageszeit", y = "Aktivitäts-Detektionen") +
theme_minimal()
# p_tag_nacht
Statistischer Test
<- sea_arrivers |>
tageszeit_counts mutate(tageszeit = if_else(hour(date_time) %in% 6:18, "Tag", "Nacht")) |>
count(tageszeit)
# Wilcox test
wilcox.test(n ~ tageszeit, data = tageszeit_counts)
Wilcoxon rank sum exact test
data: n by tageszeit
W = 1, p-value = 1
alternative hypothesis: true location shift is not equal to 0
6.2 Aktivität nach Monaten
<- sea_arrivers |>
p_monate mutate(monat = month(date_time, label = TRUE)) |>
count(monat) |>
ggplot(aes(x = monat, y = n)) +
geom_col() +
labs(title = "Aktivitäten nach Monaten", x = "Monat", y = "Aktivitäts-Detektionen") +
theme_minimal()
p_monate
Statistischer Test
<- sea_arrivers |>
monat_counts mutate(monat = month(date_time, label = TRUE)) |>
count(monat)
# Kruskal test
kruskal.test(n ~ monat, data = monat_counts)
Kruskal-Wallis rank sum test
data: n by monat
Kruskal-Wallis chi-squared = 5, df = 5, p-value = 0.4159
6.2 Aktivität nach Jahreszeiten
<- sea_arrivers |>
p_saisons mutate(monat = month(date_time),
jahreszeit = case_when(
%in% c(12,1,2) ~ "Winter",
monat %in% c(3,4,5) ~ "Frühling",
monat %in% c(6,7,8) ~ "Sommer",
monat %in% c(9,10,11) ~ "Herbst"
monat |>
)) count(jahreszeit) |>
ggplot(aes(x = jahreszeit, y = n, fill = jahreszeit)) +
geom_col() +
labs(title = "Aktivitäten nach Jahreszeiten", x = "Jahreszeit", y = "Aktivitäts-Detektionen") +
theme_minimal()
Statistischer Test
<- sea_arrivers |>
jahreszeit_counts mutate(monat = month(date_time),
jahreszeit = case_when(
%in% c(12,1,2) ~ "Winter",
monat %in% c(3,4,5) ~ "Frühling",
monat %in% c(6,7,8) ~ "Sommer",
monat %in% c(9,10,11) ~ "Herbst"
monat |>
)) count(jahreszeit)
# Kruskal test
kruskal.test(n ~ jahreszeit, data = jahreszeit_counts)
Kruskal-Wallis rank sum test
data: n by jahreszeit
Kruskal-Wallis chi-squared = 2, df = 2, p-value = 0.3679
3 Resultate
3.1 Muster der Aalbewegungen
Die Anzahl der Stationenwechsel während des Tages und der Nacht wurde erfasst. Es zeigte sich, dass die Aale im Vergleich zum Tag eine höhere Anzahl von Stationenwechseln in der Nacht aufwiesen (Abbildung 2).
Die Anzahl der Stationenwechsel wurde auch aufgeteilt auf die Monate. Hier zeigt sich, dass im Oktober eine höhere Anzahl an Wechseln stattfand als in den anderen Monaten (Abbildung 3).
Die Anzahl der Stationenwechsel wurde zusätzlich aufgeteilt auf die Jahreszeiten. Es wurde eine höhere Anzahl an Stationenwechseln im Herbst im Vergleich zum Winter und Sommer festgestellt (Abbildung 4).
Die mittlere Geschwindigkeit der Aale wurde je nach Netzwerkkante untersucht. Die Aale wiesen unterschiedliche mittlere Geschwindigkeiten auf, die sich je nach Netzwerkkante unterschieden (Abbildung 5).
3.2 Einfluss des räumlichen Modells auf die Analyse der Aalbewegungen
Die zurückgelegte Distanz aller Individuen wurde für den beschränkten Bewegungsraum (CMS) und den unbeschränkten Bewegungsraum (UCMS) verglichen. Die Aale legten im CMS tendenziell eine grössere Distanz zurück als im UCMS (Abbildung 6).
In einer weiteren Analyse wurde die mittlere Geschwindigkeit aller Individuen im CMS und UCMS untersucht, wobei die Aale auch hier im CMS leicht höhere Werte hatten als im UCMS (Abbildung 7)
4 Diskussion
4.1 Zeitliche Muster
Die Analyse ergab eine höhere Aktivität der Aale während der Nacht als während des Tages und eine deutliche Spitze in den Herbstmonaten. Diese Ergebnisse stehen im Einklang mit dem, was über das Abwanderungsverhalten des Europäischen Aals bekannt ist, nämlich dass er eine nachtaktive Art ist (Travade u. a. 2010; Vøllestad u. a. 1986; Verhelst u. a. 2018) und seine Abwanderung ins Meer hauptsächlich im Herbst beginnt (Bruijs und Durif 2009; Sandlund u. a. 2017; Verhelst u. a. 2018).
Auch wenn die statistischen Tests keine starken signifikanten Unterschiede zwischen Tag und Nacht (p = 0.057) oder den Jahreszeiten (p = 0.056) zeigten, deuten die beobachteten Tendenzen auf wichtige ökologische Muster hin, die für den Schutz der Art von Bedeutung sind. Zudem zeigen die erhaltenen Resultate einen klaren Trend, der auf relevante Variationen im Wanderverhalten hindeutet, auch wenn diese statistisch nicht solide bestätigt sind.
Diese Muster sind von grosser Bedeutung für den Schutz des Europäischen Aals, da sie helfen, die entscheidenden Zeiträume zu identifizieren, in denen Schutzmassnahmen verstärkt werden sollten, um die Wanderungen der Aale zu erleichtern und ihre Überlebenschancen zu erhöhen (Verhelst u. a. 2018).
4.2 Einfluss des räumlichen Modells auf die Analyse der Aalbewegung: CMS vs. UCMS
Die zurückgelegte Strecke sowie die Geschwindigkeit der Aale wurde in einem beschränkten (CMS) und unbeschränkten Bewegungsraum (UCMS) berechnet. Von Relevanz ist dabei die Strecke. Die Geschwindigkeit ist direkt davon abhängig, da die zeitlichen Dimension für beide Bewegungsräume gleich ist.
Die Ergebnisse zeigen, dass die mit dem CMS-Modell ermittelte Strecke leicht höher waren als die mit dem UCMS-Modell berechneten Strecke. Dies ist zu erwarten, da bei der UCMS-Methode die Luftlinie zwischen zwei Empfängern verwendet wird, was der kürzest möglichen Strecke entspricht. Diese Unterschiede erwiesen sich jedoch nicht als statistisch signifikant (Geschwindigkeit: p = 0.83; Strecke: p = 0.81). Das Gewässernetz, auf welchem das Netzwerk erstellt wurde, ist stark begradigt und enthält auch künstlich geschaffene Kanäle. In diesem Kontext ist die Verwendung eines CMS daher nicht notwendig. Falls das Gewässernetz stark mäandrierende Gewässer enthielte, wäre der Nutzen eines CMS grösser.
Beide Modelle eignen sich jedoch beide für die Verfolgung von migrierenden aquatischen Organismen.