6  Initial sand tiger shark GAM exploration

Published

June 10, 2026

library(arrow) # Data import
library(dplyr) # Data manipulation
library(sf) # Spatial
library(ggplot2) # Data visualization
library(mgcv) # GAM fitting
library(gratia) # GAM visualization

theme_set(theme_minimal())

6.1 Sand tiger shark Carcharias taurus

st_filepath <- file.path(Sys.getenv("DATA_DIR"), "data/RAW/sandtigers.csv")

sandtiger <- open_dataset(st_filepath, format = "csv") |>
  mutate(
    datetime = strptime(DateandTime, format = "%m/%d/%Y %H:%M", tz = "UTC"),
    wk = week(datetime),
    yr = year(datetime)
  ) |>
  group_by(Transmitter, wk, yr) |>
  summarize(
    med_lon = median(Longitude, na.rm = TRUE),
    med_lat = median(Latitude, na.rm = TRUE)
  ) |>
  collect()
ggplot(sandtiger) +
  geom_point(aes(x = wk, y = med_lat), alpha = 0.4, color = "orange") +
  facet_wrap(~yr) +
  labs(x = "Week of year", y = "Weekly median latitude") +
  theme(axis.title = element_text(size = 12))

Weekly median latitude of sand tiger sharks.
# A tibble: 1 × 2
   lon0  lat0
  <dbl> <dbl>
1 -75.1  38.7
proj_string <- paste0(
  "+proj=aeqd +lon_0=",
  round(projection_center$lon0, 3),
  " +lat_0=",
  round(projection_center$lat0, 3)
)

proj_string
[1] "+proj=aeqd +lon_0=-75.117 +lat_0=38.743"
sandtiger_sf <- sandtiger |>
  st_as_sf(coords = c("med_lon", "med_lat"), crs = 4326) |>
  st_transform(proj_string)

sandtiger_sf |>
  distinct(geometry) |>
  ggplot() +
  geom_sf() +
  coord_sf(datum = proj_string)

Distribution of sand tiger detections in an AEQD projection centered on the median receiver location.
migration_azimuth <- sandtiger_sf |>
  # Extract AEQD coordinates
  st_coordinates() |>
  # Run PCA
  prcomp() |>
  # Extract PCA loadings
  _$rotation |>
  # Transpose matrix
  t() |>
  # Find coordinates of a second location on the X axis.
  # Here we use 1 km but it could be any value
  (\(.) c(1e3, 0) %*% .)() |>
  # Find the angle between true north and migration north
  (\(.) atan2(.[1], .[2]) * 180 / pi)()

# Measure clockwise from north
migration_azimuth <- ifelse(
  migration_azimuth < 0,
  180 + migration_azimuth,
  migration_azimuth
)

migration_azimuth
[1] 25.09121
st_crs(sandtiger_sf) <- "+proj=aeqd +lat_0=90 +lon_0=0"
proj_rotated <- paste0("+proj=aeqd +lat_0=90 +lon_0=", migration_azimuth)

sandtiger_migration_north <- sandtiger_sf |>
  st_transform(proj_rotated) |>
  # extract coordinates for modeling purposes
  mutate(x = st_coordinates(geometry)[, 1], y = st_coordinates(geometry)[, 2])


ggplot(sandtiger_migration_north) +
  geom_sf() +
  coord_sf(datum = proj_rotated)

ggplot(sandtiger_migration_north) +
  geom_point(aes(x = wk, y = x), alpha = 0.4, color = "orange") +
  facet_wrap(~yr) +
  labs(
    title = "Migration synchrony",
    x = "Week of year",
    y = "Distance along the migration vector"
  ) +
  theme(axis.title = element_text(size = 12))

ggplot(sandtiger_migration_north) +
  geom_point(aes(x = wk, y = y, color = group), alpha = 0.4, color = "orange") +
  facet_wrap(~yr) +
  labs(
    title = "Migration front",
    x = "Week of year",
    y = "Distance across the migration vector"
  ) +
  theme(axis.title = element_text(size = 12))

6.1.1 Initial GAM

6.1.1.1 Migration vector

model_data <- sandtiger_migration_north |>
  mutate(yr_f = factor(yr))

m1_st <- model_data |>
  gam(
    x ~ s(wk, k = 27, bs = "cc") + s(yr_f, bs = "re"),
    knots = list(wk = c(1, 53)),
    data = _,
    method = "REML"
  )

summary(m1_st)

Family: gaussian 
Link function: identity 

Formula:
x ~ s(wk, k = 27, bs = "cc") + s(yr_f, bs = "re")

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)  
(Intercept)   -55224      24767   -2.23   0.0258 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
          edf Ref.df       F p-value    
s(wk)   24.01     25 4328.72  <2e-16 ***
s(yr_f) 10.64     11   43.91  <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.574   Deviance explained = 57.7%
-REML =  71046  Scale est. = 2.9382e+09  n = 5759
draw(m1_st, select = "s(wk)", residuals = TRUE)

m2_st <- model_data |>
  gam(
    x ~ yr_f + s(wk, by = yr_f, k = 27, bs = "cc"),
    knots = list(wk = c(1, 53)),
    data = _,
    method = "REML"
  )


summary(m2_st)

Family: gaussian 
Link function: identity 

Formula:
x ~ yr_f + s(wk, by = yr_f, k = 27, bs = "cc")

Parametric coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept)    5237.4     1973.4   2.654  0.00798 ** 
yr_f2011       -321.4     2166.4  -0.148  0.88208    
yr_f2013     -42477.6     3112.9 -13.645  < 2e-16 ***
yr_f2014     -20452.7     2331.6  -8.772  < 2e-16 ***
yr_f2012      -5507.1     2157.1  -2.553  0.01071 *  
yr_f2010        277.5     2211.9   0.125  0.90017    
yr_f2008      -3719.8    27071.3  -0.137  0.89071    
yr_f2015    -122129.3   262896.7  -0.465  0.64227    
yr_f2018      41996.4    15893.0   2.642  0.00825 ** 
yr_f2017      51717.8   452208.6   0.114  0.90895    
yr_f2019      74448.6   219252.5   0.340  0.73420    
yr_f2016     -25218.7    11448.8  -2.203  0.02765 *  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
                     edf Ref.df       F  p-value    
s(wk):yr_f2009  0.035947     14   0.000 0.934471    
s(wk):yr_f2011  6.510823     20   1.297 0.000109 ***
s(wk):yr_f2013 18.143015     21 215.473  < 2e-16 ***
s(wk):yr_f2014 24.356384     25 599.666  < 2e-16 ***
s(wk):yr_f2012 20.902270     23 268.428  < 2e-16 ***
s(wk):yr_f2010  0.101991     16   0.005 0.432037    
s(wk):yr_f2008  0.004463      3   0.000 0.888653    
s(wk):yr_f2015  6.471024      8 147.123  < 2e-16 ***
s(wk):yr_f2018 13.014395     19 118.190  < 2e-16 ***
s(wk):yr_f2017 15.675741     18 141.659  < 2e-16 ***
s(wk):yr_f2019  1.962773      2  51.893  < 2e-16 ***
s(wk):yr_f2016 11.258557     16  54.144  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.874   Deviance explained = 87.7%
-REML =  67705  Scale est. = 8.7017e+08  n = 5759
draw(m2_st, select = c("s(wk)"), partial_match = TRUE, residuals = TRUE)

6.1.1.2 Migration front

We’ll do the same with the migration front (the deviation from the migration vector).

m1_mf <- model_data |>
  gam(
    y ~ s(wk, k = 27, bs = "cc") + s(yr_f, bs = "re"),
    knots = list(wk = c(1, 53)),
    data = _,
    method = "REML"
  )

summary(m1_mf)

Family: gaussian 
Link function: identity 

Formula:
y ~ s(wk, k = 27, bs = "cc") + s(yr_f, bs = "re")

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)
(Intercept)   -29480      18807  -1.568    0.117

Approximate significance of smooth terms:
          edf Ref.df       F p-value    
s(wk)   21.36     25 4167.30  <2e-16 ***
s(yr_f) 10.58     11   45.34  <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.598   Deviance explained =   60%
-REML =  69922  Scale est. = 2.0046e+09  n = 5759
draw(m1_mf, select = "s(wk)", residuals = TRUE)

m2_mf <- model_data |>
  gam(
    y ~ yr_f + yr_f + s(wk, by = yr_f, k = 27, bs = "cc"),
    knots = list(wk = c(1, 53)),
    data = _,
    method = "REML"
  )

summary(m2_mf)

Family: gaussian 
Link function: identity 

Formula:
y ~ yr_f + yr_f + s(wk, by = yr_f, k = 27, bs = "cc")

Parametric coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept)   29397.8     3671.6   8.007 1.42e-15 ***
yr_f2011      -3948.4     3761.5  -1.050 0.293909    
yr_f2013     -56068.8     4319.1 -12.982  < 2e-16 ***
yr_f2014     -12463.7     3841.2  -3.245 0.001182 ** 
yr_f2012     -13519.1     3745.4  -3.610 0.000309 ***
yr_f2010       -198.6     3964.9  -0.050 0.960059    
yr_f2008     -38222.2    33346.1  -1.146 0.251750    
yr_f2015    -124464.3    82008.5  -1.518 0.129146    
yr_f2018     -28914.0    10141.0  -2.851 0.004371 ** 
yr_f2017     -59549.6    74722.2  -0.797 0.425516    
yr_f2019    -271979.3    45919.4  -5.923 3.35e-09 ***
yr_f2016     -32481.3     9276.1  -3.502 0.000466 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
                    edf Ref.df       F  p-value    
s(wk):yr_f2009  2.70670     14   1.612 7.75e-06 ***
s(wk):yr_f2011  7.35684     20   5.225  < 2e-16 ***
s(wk):yr_f2013 18.67490     21 369.195  < 2e-16 ***
s(wk):yr_f2014 23.80834     25 356.027  < 2e-16 ***
s(wk):yr_f2012 20.52964     23 322.082  < 2e-16 ***
s(wk):yr_f2010  8.36187     16   4.589  < 2e-16 ***
s(wk):yr_f2008  0.01339      4   0.000    0.742    
s(wk):yr_f2015  5.10558      8  47.384  < 2e-16 ***
s(wk):yr_f2018  9.47040     19  81.634  < 2e-16 ***
s(wk):yr_f2017 11.28888     18  66.331  < 2e-16 ***
s(wk):yr_f2019  1.37134      3   8.950 1.99e-07 ***
s(wk):yr_f2016  7.84278     16  23.450  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.855   Deviance explained = 85.8%
-REML =  67116  Scale est. = 7.2302e+08  n = 5759
draw(m2_mf, select = "s(wk)", residuals = TRUE, partial_match = TRUE)

6.1.2 Model evaluation

appraise(m1_st)

appraise(m2_st)

appraise(m1_mf)

appraise(m2_mf)