1 Libraries

library(tidyverse)
library(lubridate)
library(sf)
library(ctmm)

2 Study individuals

past_ids_vec <- c(
  "AG082", "AG083", "AG088", "AG089", "AG096", "AG432",
  "st2010-1048_2", "st2010-1052", "st2010-1053"
)

present_ids_vec <- c(
  "171604 - adWBV05", "171605 - Matira",    "171606 - Ilkeliani",
  "171607 - Baldrick", "171608 - adWBV06",  "171609 - Charlie",
  "171612 - Olarro",   "171613 - Tipilikwani"
)

3 Vulture GPS cleaning

Fixes are flagged as erroneous based on visual inspection of individual trajectories (speed > 100 km/h or step > 50 km with turning angle > 150°). Individual st2010-1054 is excluded entirely due to insufficient data quality. Timestamps for st2010-1048_2 recorded as 2010 are corrected to 2011.

remove_timestamps <- tribble(
  ~individual.local.identifier,  ~timestamp,
  # st2010-1048_2
  "st2010-1048_2", "2010-11-17 09:00:00",
  "st2010-1048_2", "2010-12-07 07:00:00",
  # st2010-1052
  "st2010-1052",   "2010-06-12 17:00:00",
  "st2010-1052",   "2010-06-13 05:00:00",
  # st2010-1057
  "st2010-1057",   "2011-01-14 09:00:00",
  # adWBV05
  "171604 - adWBV05", "2017-09-14 06:00:00",
  "171604 - adWBV05", "2018-02-11 06:00:00",
  # Matira
  "171605 - Matira",  "2018-11-03 06:00:00",
  # Ilkeliani
  "171606 - Ilkeliani", "2019-04-22 06:00:00",
  "171606 - Ilkeliani", "2019-08-17 06:00:00",
  # Baldrick
  "171607 - Baldrick",  "2018-07-05 06:00:00",
  "171607 - Baldrick",  "2020-01-14 06:00:00",
  # adWBV06
  "171608 - adWBV06",   "2017-12-27 06:00:00",
  # Charlie
  "171609 - Charlie",   "2018-09-08 06:00:00",
  "171609 - Charlie",   "2019-11-02 06:00:00",
  # Olarro
  "171612 - Olarro",    "2020-03-18 06:00:00",
  # Tipilikwani
  "171613 - Tipilikwani", "2019-06-22 06:00:00",
  "171613 - Tipilikwani", "2021-02-14 06:00:00"
) %>%
  mutate(timestamp = as.POSIXct(timestamp, tz = "UTC"))
# Load merged raw GPS (all individuals, 2h-resampled by Movebank)
wbv_raw <- read.csv(
  "/Users/oli/University/Zoology/5th_Year_MSc/MSC_Thesis/Thesis_R/WBV_resampled_2h.csv"
) %>%
  mutate(timestamp = as.POSIXct(timestamp, tz = "UTC"))

cat("Raw rows:", nrow(wbv_raw), "\n")
## Raw rows: 12506
cat("Unique individuals:", n_distinct(wbv_raw$individual.local.identifier), "\n")
## Unique individuals: 19
# Exclude st2010-1054 entirely; apply timestamp removal list
wbv_clean <- wbv_raw %>%
  filter(individual.local.identifier != "st2010-1054") %>%
  filter(individual.local.identifier %in% c(past_ids_vec, present_ids_vec)) %>%
  anti_join(remove_timestamps,
            by = c("individual.local.identifier", "timestamp")) %>%
  # Correct year error: st2010-1048_2 fixes recorded as 2010 should be 2011
  mutate(
    timestamp = if_else(
      individual.local.identifier == "st2010-1048_2" &
        year(timestamp) == 2010,
      timestamp + years(1),
      timestamp
    )
  ) %>%
  # Derived columns
  mutate(
    month  = month(timestamp),
    season = if_else(month %in% c(11, 12, 1, 2, 3, 4), "Wet", "Dry"),
    period = if_else(individual.local.identifier %in% past_ids_vec,
                     "Past", "Present"),
    year   = year(timestamp)
  ) %>%
  arrange(individual.local.identifier, timestamp)

cat("Cleaned rows:", nrow(wbv_clean), "\n")
## Cleaned rows: 12323
cat("Individuals:", n_distinct(wbv_clean$individual.local.identifier), "\n")
## Individuals: 17
saveRDS(wbv_clean, "WBV_resampled_2h.rds")

# Summary table
wbv_clean %>%
  group_by(individual.local.identifier, period) %>%
  summarise(
    n_fixes = n(),
    start   = min(timestamp),
    end     = max(timestamp),
    duration_months = as.numeric(difftime(max(timestamp), min(timestamp),
                                          units = "days")) / 30.44,
    .groups = "drop"
  ) %>%
  arrange(period, individual.local.identifier) %>%
  print(n = Inf)
## # A tibble: 17 × 6
##    individual.local.ide…¹ period n_fixes start               end                
##    <chr>                  <chr>    <int> <dttm>              <dttm>             
##  1 AG082                  Past       362 2009-07-06 00:00:00 2010-07-11 00:00:00
##  2 AG083                  Past       294 2009-07-06 00:00:00 2010-04-26 00:00:00
##  3 AG088                  Past       287 2009-07-09 00:00:00 2010-05-04 00:00:00
##  4 AG089                  Past       339 2009-07-09 00:00:00 2010-06-18 00:00:00
##  5 AG096                  Past       316 2009-08-07 00:00:00 2010-06-18 00:00:00
##  6 AG432                  Past       214 2010-06-16 00:00:00 2011-01-15 00:00:00
##  7 st2010-1048_2          Past       260 2011-01-01 00:00:00 2011-12-31 00:00:00
##  8 st2010-1052            Past       263 2010-08-09 00:00:00 2011-05-03 00:00:00
##  9 st2010-1053            Past       281 2010-08-10 00:00:00 2011-05-20 00:00:00
## 10 171604 - adWBV05       Prese…    1697 2018-04-15 00:00:00 2022-12-12 00:00:00
## 11 171605 - Matira        Prese…     498 2017-11-19 00:00:00 2019-03-31 00:00:00
## 12 171606 - Ilkeliani     Prese…    1043 2018-06-18 00:00:00 2021-04-25 00:00:00
## 13 171607 - Baldrick      Prese…    1713 2018-01-12 00:00:00 2022-10-04 00:00:00
## 14 171608 - adWBV06       Prese…    1909 2018-04-15 00:00:00 2023-07-10 00:00:00
## 15 171609 - Charlie       Prese…     529 2018-04-16 00:00:00 2019-09-26 00:00:00
## 16 171612 - Olarro        Prese…    1212 2018-06-18 00:00:00 2022-01-18 00:00:00
## 17 171613 - Tipilikwani   Prese…    1106 2018-06-18 00:00:00 2021-06-28 00:00:00
## # ℹ abbreviated name: ¹​individual.local.identifier
## # ℹ 1 more variable: duration_months <dbl>

4 Wildebeest GPS cleaning

Resident wildebeest only (migrant == 0). Fixes with speed > 100 km/h between consecutive locations are removed as GPS errors.

wb_raw <- read.csv(
  "/Users/oli/University/Zoology/5th_Year_MSc/MSC_Thesis/Thesis_R/Wildebeest_V1.csv",
  stringsAsFactors = FALSE
) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%d/%m/%Y %H:%M", tz = "UTC"))

cat("Raw wildebeest rows:", nrow(wb_raw), "\n")
## Raw wildebeest rows: 891068
wb <- wb_raw %>%
  filter(
    migrant == 0,
    year %in% c(2010, 2011, 2017:2023),
    !is.na(UTM_X), !is.na(UTM_Y), !is.na(datetime)
  ) %>%
  mutate(
    period = if_else(year %in% 2010:2011, "Past", "Present"),
    month  = month(datetime),
    season = if_else(month %in% c(11, 12, 1, 2, 3, 4), "Wet", "Dry")
  ) %>%
  arrange(AID, datetime)

# Remove individuals with < 100 fixes
keep_ids <- wb %>%
  count(AID) %>%
  filter(n >= 100) %>%
  pull(AID)

wb <- wb %>% filter(AID %in% keep_ids)
cat("After fix-count filter:", nrow(wb), "fixes,",
    n_distinct(wb$AID), "individuals\n")
## After fix-count filter: 176858 fixes, 33 individuals
# Speed-based outlier removal
wb <- wb %>%
  group_by(AID) %>%
  arrange(datetime, .by_group = TRUE) %>%
  mutate(
    step_km   = sqrt((UTM_X - lag(UTM_X))^2 + (UTM_Y - lag(UTM_Y))^2) / 1000,
    dt_h      = as.numeric(difftime(datetime, lag(datetime), units = "hours")),
    speed_kmh = step_km / dt_h
  ) %>%
  filter(is.na(speed_kmh) | speed_kmh <= 100) %>%
  ungroup()

cat("After outlier removal:", nrow(wb), "fixes\n")
## After outlier removal: 176858 fixes
WB_clean <- wb %>%
  select(AID, datetime, UTM_X, UTM_Y, period, year, month, season, sex)

saveRDS(WB_clean, "WB_clean.rds")
cat("Saved: WB_clean.rds\n")
## Saved: WB_clean.rds
WB_clean %>%
  group_by(period) %>%
  summarise(n_fixes = n(), n_individuals = n_distinct(AID), .groups = "drop")
## # A tibble: 2 × 3
##   period  n_fixes n_individuals
##   <chr>     <int>         <int>
## 1 Past      94243            15
## 2 Present   82615            18

5 Migratory wildebeest — Past proxy (2002–2004)

No migratory wildebeest were tracked during the Past vulture period (2010–2011). We use data from 2002–2004 as a proxy for the migratory distribution, retaining individuals with ≥ 100 fixes.

wb_mig_past <- wb_raw %>%
  filter(
    migrant == 1,
    year %in% 2002:2004,
    !is.na(UTM_X), !is.na(UTM_Y), !is.na(datetime)
  ) %>%
  mutate(
    month  = month(datetime),
    season = if_else(month %in% c(11, 12, 1, 2, 3, 4), "Wet", "Dry")
  ) %>%
  arrange(AID, datetime)

keep_mig <- wb_mig_past %>% count(AID) %>% filter(n >= 100) %>% pull(AID)
wb_mig_past <- wb_mig_past %>% filter(AID %in% keep_mig)

cat("Migratory wildebeest proxy (2002–2004):",
    nrow(wb_mig_past), "fixes,",
    n_distinct(wb_mig_past$AID), "individuals\n")
## Migratory wildebeest proxy (2002–2004): 26866 fixes, 13 individuals
saveRDS(wb_mig_past, "WB_mig_past_proxy.rds")
cat("Saved: WB_mig_past_proxy.rds\n")
## Saved: WB_mig_past_proxy.rds