Depth-Stratified Fishing Effort from Global Fishing Watch
Source:vignettes/articles/gfw-fishing-effort-3d.Rmd
gfw-fishing-effort-3d.RmdDepth-Stratified Fishing Effort from Global Fishing Watch
This article turns Global Fishing Watch (GFW) fishing-effort data from a flat 2D map into a 3D stack with one layer per depth. You can then overlap that stack with shark and ray ranges using [calc_volume_overlap()].
Three tools do the work:
- gfwr fetches the effort data from the GFW API.
- [gfw_effort_to_raster()] puts it on your study grid.
- [as_envelope()] and [envelope_to_voxel()] extend it into 3D.
The depth at which each gear fishes is an assumption, not a measurement. This article therefore defines it as a lookup table you edit, instead of the package shipping one.
What GFW gives you, and what is missing
GFW (Kroodsma et al. 2018, Science) publishes apparent fishing effort as hours per grid cell, time period, flag state, and gear type. The product is 2D. A longline fishing at 80 m and a bottom trawl working the seafloor at 400 m both appear as the same kind of effort pixel. If a species uses only part of the water column, measuring its overlap against flat effort will over- or under-state the risk.
The workflow has three steps:
-
Fetch effort for a region with
gfwr::gfw_ais_fishing_hours(), grouped by gear type. - Rasterise it onto your study grid with [gfw_effort_to_raster()], one layer per gear.
-
Extend into 3D. Give each gear a depth window, then
turn that window into depth layers with [as_envelope()] and
[envelope_to_voxel()]. The result is a stack named
effort_<geartype>_depth=<value>.
Before you start
You need three things:
-
A GFW API token. Register at https://globalfishingwatch.org/our-apis/tokens, then set
the key as the
GFW_TOKENenvironment variable.gfwr::gfw_auth()reads it from there. - A local bathymetry file. The examples use GEBCO.
- Your own file paths. The paths below point to one machine. Replace them with yours.
The code chunks do not run when this article is built, so every output shown is an illustration rather than a live result.
Step 1: Fetch apparent fishing effort
gfw_ais_fishing_hours() returns a long tibble with one
row per cell, time bucket, and group level. For an EEZ, MPA, or RFMO
region, first convert the region name into a numeric ID with
gfwr::gfw_region_id(). The API needs the ID, not an ISO3
code.
# Resolve the Japan EEZ to its numeric region ID.
jpn_eez <- gfw_region_id(region = "JPN", region_source = "EEZ")$id
effort <- gfw_ais_fishing_hours(
spatial_resolution = "HIGH", # 0.01 deg
temporal_resolution = "YEARLY", # one bucket per year in [start, end]
start_date = "2022-01-01",
end_date = "2022-12-31",
region = jpn_eez,
region_source = "EEZ",
group_by = "GEARTYPE"
)
head(effort)
#> # A tibble: 6 x 6
#> Lat Lon `Time Range` geartype `Vessel IDs` `Apparent Fishing Hours`
#> <dbl> <dbl> <dbl> <chr> <dbl> <dbl>
#> 1 35.4 140.1 2022 trawlers 1 0.95
#> 2 34.6 138.9 2022 inconclusive 1 1.85
#> 3 35.1 139.7 2022 fishing 1 3.05
#> 4 33.2 137.4 2022 drifting_longlines 1 2.38
#> ...GFW returns gear labels in lower case, such as trawlers
and drifting_longlines. Vessels the classifier cannot
assign get the label inconclusive. The lookup below marks
those as unknown, and step 3 skips them.
Step 2: Rasterise onto the study grid
Choose a target grid, then rasterise the effort tibble onto it. Use
the same grid you use for species ranges and WOA extraction. The
function returns one layer per level of layer_by.
# Use the bathymetry grid as the canonical study grid.
bathy <- load_gebco_bathymetry(
here("/home/jay/Programming_Projects/Big_Data/gebco_2025_sub_ice_topo/GEBCO_2025_sub_ice.nc")
)
# Crop bathymetry to the effort footprint (with a small buffer) before
# any heavy raster ops. GEBCO is global at 15 arc-seconds; cropping first
# turns a multi-GB reproject/aggregate into a few-MB one.
effort_extent <- terra::ext(
min(effort$Lon) - 0.5, max(effort$Lon) + 0.5,
min(effort$Lat) - 0.5, max(effort$Lat) + 0.5
)
bathy_crop <- terra::crop(bathy, effort_extent)
# Canonical study grid: positive-down depth on the effort footprint,
# coarsened to ~0.04 deg for the worked example.
study_grid <- (terra::project(bathy_crop, "EPSG:4326") * -1) |>
terra::aggregate(fact = 10)
effort_by_gear <- gfw_effort_to_raster(
effort = effort,
grid = study_grid,
layer_by = "geartype",
value = "Apparent Fishing Hours",
fun = "sum"
)
names(effort_by_gear)
#> [1] "effort_drifting_longlines" "effort_set_gillnets"
#> [3] "effort_trawlers" "effort_pots_and_traps"
#> ...To group the data differently, for example one layer per flag state,
re-fetch it with a different group_by. That is cheaper and
more accurate than combining layers yourself.
Step 3: Extend into 3D
This step needs a depth_lookup table giving each gear
class a depth band. The package does not ship one, and no
package function reads one. Operating depths vary by region,
fleet, and time period, so both the table and the code that uses it
belong to your analysis. The values below are placeholders that show the
mechanics. Replace them with values from the literature or from your own
fishery.
The table has five columns:
-
geartype— the GFW gear label. Must match thegeartypevalues in the effort tibble. -
depth_min,depth_max— shallowest and deepest fishing depth in metres, measured downward from the surface. UseNAfor benthic gears, whose band comes from bathymetry, and for unknown gears. -
mode—"pelagic","benthic", or"unknown". -
benthic_buffer— for benthic gears, how many metres above the seafloor the gear fishes.NAotherwise.
# Placeholder values for demonstration only — substitute literature- or
# fishery-specific operating depths for real analyses.
depth_lookup <- data.frame(
geartype = c(
"drifting_longlines", "set_longlines", "set_gillnets",
"trawlers", "dredge_fishing", "pole_and_line",
"trollers", "purse_seines", "tuna_purse_seines",
"other_purse_seines", "seiners", "squid_jigger",
"pots_and_traps", "fixed_gear", "fishing", "inconclusive"
),
depth_min = c( 0, NA, 0, NA, NA, 0, 0, 0, 0, 0, 0, 0, NA, NA, NA, NA),
depth_max = c(400, NA, 140, NA, NA, 20, 150, 200, 250, 100, 200, 100, NA, NA, NA, NA),
mode = c(
"pelagic", "benthic", "pelagic",
"benthic", "benthic", "pelagic",
"pelagic", "pelagic", "pelagic",
"pelagic", "pelagic", "pelagic",
"benthic", "benthic", "unknown", "unknown"
),
benthic_buffer = c( NA, 50, NA, 50, 10, NA, NA, NA, NA, NA, NA, NA, 50, 50, NA, NA),
stringsAsFactors = FALSE
)How the conversion works
Each row of the lookup describes an envelope: for every cell, the shallowest and deepest depth that gear fishes. [as_envelope()] builds the envelope, and [envelope_to_voxel()] spreads the cell’s fishing hours down it.
The two modes differ only in where the depth limits come from:
- Pelagic gears fish a fixed band, so the limits are two numbers.
-
Benthic gears fish close to the seafloor, so the
limits are rasters:
[seafloor − buffer, seafloor].
Two arguments to [envelope_to_voxel()] carry assumptions you should know about:
-
profile = profile_equalsplits each cell’s hours evenly across the depths its window reaches, so the total number of hours is preserved. Use [profile_flat()] to write the full value at every depth instead. That double-counts, and suits footprint maps only. -
bounds = "midpoint"treats each depth level as the water halfway to its neighbours. This is the World Ocean Atlas convention, which thestandard_depthsbelow follow. A window that falls between two levels still lands in the level whose band contains it, so no hours are lost.
# Use WOA's 57 standard depths (0–1500 m) as the vertical grid.
standard_depths <- c(
0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100,
125, 150, 175, 200, 225, 250, 275, 300, 325, 350, 375, 400, 425, 450, 475, 500,
550, 600, 650, 700, 750, 800, 850, 900, 950, 1000,
1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500
)
# `study_grid` already carries positive-down depth on the cropped
# footprint; clamp non-ocean cells to 0 to get the seafloor reference.
seafloor <- terra::clamp(study_grid, lower = 0)Benthic gear: trawlers
Trawls fish close to the seafloor, so the window follows bathymetry instead of fixed numbers. Both limits are rasters, and every cell gets its own window.
trawl_effort <- effort_by_gear[["effort_trawlers"]]
trawl_buffer <- depth_lookup$benthic_buffer[depth_lookup$geartype == "trawlers"]
trawl_buffer
#> [1] 50
# Step 1 — the 2.5D envelope. A band `trawl_buffer` metres thick riding the
# seafloor, clamped at the surface so a cell shallower than the buffer does
# not produce a negative depth. `trawl_effort` is also the footprint: cells
# with no trawling are NA, so they get no window.
trawl_env <- as_envelope(
trawl_effort,
depth_min = terra::clamp(seafloor - trawl_buffer, lower = 0),
depth_max = seafloor
)
names(trawl_env)
#> [1] "depth_min" "depth_max"
# Step 2 — the 3D voxel. `values` is what gets spread down the window, and
# `profile_equal` splits each cell's hours evenly across the depths its
# window reaches, so the stack conserves total effort-hours.
trawlers_3d <- envelope_to_voxel(
trawl_env,
depths = standard_depths,
values = trawl_effort,
profile = profile_equal,
bounds = "midpoint",
varname = "effort_trawlers"
)
names(trawlers_3d)[1:5]
#> [1] "effort_trawlers_depth=0"
#> [2] "effort_trawlers_depth=5"
#> [3] "effort_trawlers_depth=10"
#> [4] "effort_trawlers_depth=15"
#> [5] "effort_trawlers_depth=20"
# Nothing created, nothing lost.
sum(terra::global(trawlers_3d, "sum", na.rm = TRUE)$sum, na.rm = TRUE)
#> [1] <hours>
terra::global(trawl_effort, "sum", na.rm = TRUE)$sum
#> [1] <the same number>The standard depths are 5 m apart near the surface, but 25 to 50 m apart below 100 m. The same 50 m band therefore resolves differently with depth: a shelf cell at 80 m spreads its hours over about a dozen layers, while a slope cell at 1200 m spreads them over two or three. A band that contains no standard depth at all still lands in the level whose band holds it, so its hours are kept.
Pelagic gear: purse seines
A purse seine fishes a fixed band near the surface. The limits are the same everywhere, so they come straight from the lookup as two numbers. The rest of the code is identical to the trawler example: both modes use the same pair of functions.
seine_effort <- effort_by_gear[["effort_purse_seines"]]
seine_band <- depth_lookup[depth_lookup$geartype == "purse_seines", ]
seine_band[, c("depth_min", "depth_max")]
#> depth_min depth_max
#> 8 0 200
# Step 1 — the 2.5D envelope. Constant limits, so `as_envelope()` broadcasts
# them across the footprint instead of reading a raster per cell.
seine_env <- as_envelope(
seine_effort,
depth_min = seine_band$depth_min,
depth_max = seine_band$depth_max
)
# Step 2 — the 3D voxel. Identical to the trawler call; only the envelope
# and the variable name differ.
purse_seines_3d <- envelope_to_voxel(
seine_env,
depths = standard_depths,
values = seine_effort,
profile = profile_equal,
bounds = "midpoint",
varname = "effort_purse_seines"
)
# A 0–200 m band reaches the first 25 of the 57 standard depths, so every
# fished cell spreads its hours over 25 layers at 1/25 each.
sum(!is.na(terra::values(purse_seines_3d)[1, ]))
#> [1] 25The two modes are kept in separate stacks because they behave so differently. A benthic cell fills a handful of layers; a pelagic cell fills twenty-five. That matters for the 3D scene below.
Scaling to the whole fleet
The two blocks above are the whole pattern: pick the window, build
the envelope, convert it. For a full analysis, repeat them for every row
of depth_lookup and combine the results with
c().
Two practical points:
-
Work one gear at a time. Memory then scales with
one gear’s stack rather than the whole fleet’s. Write each stack to a
tempfile with
terra::writeRaster()before combining them. -
A combined stack is no longer a voxel. Each
single-gear stack is a [SpatVoxel-class]: one variable, one layer per
depth, ordered shallow to deep. Combining gears breaks that order, but
c()keeps the class label anyway, so you get an object that claims to be a voxel and is not. ThewriteRaster()step avoids the problem, because it returns a plain SpatRaster. Keep the single-gear stacks if you need the voxel class for [calc_volume_overlap()] or [voxel_to_envelope()].
Gears with mode = "unknown" have no depth information,
so skip them. Placing their effort at the surface would invent a result
rather than estimate one.
Visualising the benthic stack
A 3D scene is the clearest way to read trawlers_3d. The
cubes sit on the seafloor, so you can see whether effort concentrates on
the shelf or in deeper canyons, and the bathymetry surface gives each
cube a reference. We use plotly to keep the scene
interactive in the rendered article.
Only benthic effort is drawn. purse_seines_3d has
twenty-five layers under every fished cell, and that many cubes exceeds
what plotly can serialise.
library(plotly)
library(tidyr)
library(dplyr)
library(stringr)
# `trawlers_3d` comes from the benthic chunk above. Only the benthic stack
# is rendered; the helpers below are written to take any stack following the
# `{variable}_depth=<value>` convention.
# Vertical extent of each depth band: half-way to neighbouring standard
# depths (so adjacent cubes meet without gaps or overlap).
sd_sorted <- sort(unique(standard_depths))
n_sd <- length(sd_sorted)
mids <- (sd_sorted[-n_sd] + sd_sorted[-1]) / 2
bands <- data.frame(
depth = sd_sorted,
z_low = c(max(sd_sorted[1] - (sd_sorted[2] - sd_sorted[1]) / 2, 0), mids),
z_high = c(mids, sd_sorted[n_sd] +
(sd_sorted[n_sd] - sd_sorted[n_sd - 1]) / 2)
)
# Concatenate one mesh3d trace covering all cubes. Each cube has 8 vertices
# and 12 triangles; vertex indices in plotly's mesh3d are 0-indexed.
make_cube_mesh <- function(voxels, dx, dy) {
n <- nrow(voxels)
off <- as.matrix(expand.grid(sx = c(-1, 1), sy = c(-1, 1), sz = c(0, 1)))
rows <- voxels[rep(seq_len(n), each = 8), ]
o <- off[rep(seq_len(8), times = n), ]
vx <- rows$x + o[, "sx"] * dx
vy <- rows$y + o[, "sy"] * dy
vz <- ifelse(o[, "sz"] == 0, rows$z_low, rows$z_high)
tris <- rbind(
c(0, 1, 2), c(1, 3, 2), # shallow face
c(4, 5, 6), c(5, 7, 6), # deep face
c(0, 4, 1), c(1, 4, 5), # south face
c(2, 3, 6), c(3, 7, 6), # north face
c(0, 2, 4), c(2, 6, 4), # west face
c(1, 5, 3), c(3, 5, 7) # east face
)
base_v <- rep(seq(0L, by = 8L, length.out = n), each = 12)
list(
x = vx, y = vy, z = vz,
i = rep(tris[, 1], n) + base_v,
j = rep(tris[, 2], n) + base_v,
k = rep(tris[, 3], n) + base_v,
intensity = rows$effort
)
}
# Turn a multi-layer effort stack into mesh3d cubes by collapsing across
# gears at each (x, y, depth) and joining per-depth z-band extents.
stack_to_cubes <- function(stack, dx, dy, bands) {
voxels <- terra::as.data.frame(stack, xy = TRUE, na.rm = FALSE) |>
pivot_longer(cols = -c(x, y), names_to = "layer", values_to = "effort") |>
filter(!is.na(effort) & effort > 0) |>
mutate(depth = as.numeric(str_extract(layer, "(?<=depth=)-?[0-9.]+"))) |>
group_by(x, y, depth) |>
summarise(effort = sum(effort), .groups = "drop") |>
left_join(bands, by = "depth")
make_cube_mesh(voxels, dx = dx, dy = dy)
}
# Only benthic effort is rendered. Pelagic gears fill dozens of depths per
# cell, so the cube count exceeds plotly's JSON serializer (R's 2 GB string
# limit) even after thinning by threshold.
res <- terra::res(study_grid)
benthic_cubes <- stack_to_cubes(trawlers_3d, res[1] / 2, res[2] / 2, bands)
# Log-scale colour mapping. Plotly's mesh3d colourscale is linear in
# `intensity`, so we feed it log10(effort) directly, then relabel the
# colourbar ticks back to the original-scale values (1, 10, 100, ...).
benthic_log_intensity <- log10(benthic_cubes$intensity)
log_breaks <- 10 ^ seq(floor(min(benthic_log_intensity)),
ceiling(max(benthic_log_intensity)))
# Bathymetry surface and a flat sea surface at z = 0. Plotly expects the
# i-th row of z to align with y[i], so flip both together south-to-north.
bathy_x <- terra::xFromCol(seafloor, seq_len(terra::ncol(seafloor)))
bathy_y <- terra::yFromRow(seafloor, seq_len(terra::nrow(seafloor)))
bathy_mat <- terra::as.matrix(seafloor, wide = TRUE)
bathy_mat <- bathy_mat[nrow(bathy_mat):1, ]
bathy_y <- rev(bathy_y)
sea_z <- matrix(0, nrow = length(bathy_y), ncol = length(bathy_x))
# Coastline: contour the *native-resolution* `bathy_crop` (positive
# elevation on land, negative below sea level) at 0 — the actual coast.
# Using `seafloor` instead loses the coast geometry: aggregation by
# fact=10 flattens 10×10 GEBCO cells, and clamp(lower = 0) snaps
# negative-elevation cells onto the same plane as sea-level cells, so
# the contour zigzags through the coarsened grid rather than tracing
# real shorelines. Project onto the same CRS as the plot if needed.
coast_lines <- terra::as.contour(bathy_crop, levels = 0)
if (!terra::same.crs(coast_lines, study_grid)) {
coast_lines <- terra::project(coast_lines, terra::crs(study_grid))
}
# Flatten each line segment into one (x, y) sequence and separate
# segments with NAs so plotly's scatter3d breaks the line at NA,
# preserving disconnected coastline pieces (islands, archipelagos).
coast_xy <- sf::st_coordinates(sf::st_as_sf(coast_lines))
coast_split <- split(seq_len(nrow(coast_xy)),
paste(coast_xy[, "L1"], coast_xy[, "L2"]))
coast_x <- unlist(lapply(coast_split, function(idx) c(coast_xy[idx, "X"], NA)))
coast_y <- unlist(lapply(coast_split, function(idx) c(coast_xy[idx, "Y"], NA)))
coast_z <- ifelse(is.na(coast_x), NA_real_, 0)
# Namespace-qualify every plotly call: `terra` (loaded earlier in this
# vignette) exports a `layout` generic, and `graphics::layout` is on the
# search path too. Either can mask `plotly::layout` and trigger
# "unused argument (scene = ...)".
build_benthic_scene <- function(camera = NULL) {
scene <- list(
xaxis = list(title = "Longitude"),
yaxis = list(title = "Latitude"),
zaxis = list(title = "Depth (m)", autorange = "reversed"),
# Compress the z-axis so depth doesn't dominate the scene. Tune the
# `z` value down for less vertical stretch, up for more.
aspectmode = "manual",
aspectratio = list(x = 1, y = 1, z = 0.05)
)
if (!is.null(camera)) scene$camera <- camera
plotly::plot_ly() |>
plotly::add_surface(
x = bathy_x, y = bathy_y, z = bathy_mat,
# Single surface keeps the coast geometry continuous — splitting
# land/ocean into two NA-masked surfaces drops faces that bridge
# the boundary, leaving ragged edges. Plotly interpolates colours
# across faces, so the green→tan transition smears across the
# 1-cell-wide coastal band; the coastline scatter trace below
# overlays a sharp dark line that visually anchors the boundary.
colorscale = list(
c(0, "rgb(140, 170, 110)"), # land (z = 0): muted green
c(1e-4, "rgb(180, 170, 150)"), # shallow ocean: tan
c(1, "rgb( 60, 45, 30)") # deep ocean: dark brown
),
cmin = 0,
cmax = max(bathy_mat, na.rm = TRUE),
showscale = FALSE, opacity = 1, name = "Seafloor"
) |>
plotly::add_trace(
type = "scatter3d", mode = "lines",
x = coast_x, y = coast_y, z = coast_z,
line = list(color = "rgb(30, 25, 20)", width = 3),
hoverinfo = "skip", showlegend = FALSE, name = "Coastline"
) |>
plotly::add_trace(
type = "mesh3d",
x = benthic_cubes$x, y = benthic_cubes$y, z = benthic_cubes$z,
i = benthic_cubes$i, j = benthic_cubes$j, k = benthic_cubes$k,
intensity = benthic_log_intensity,
colorscale = "Viridis", opacity = 0.5, flatshading = TRUE,
colorbar = list(
title = "Effort\n(hours)",
tickvals = log10(log_breaks),
ticktext = format(log_breaks, big.mark = ",", scientific = FALSE,
trim = TRUE)
),
name = "Benthic effort"
) |>
plotly::layout(scene = scene)
}
build_benthic_scene()Figure 1. Depth-stratified benthic fishing effort in the Japanese EEZ (2022), rendered as an interactive 3D scene. Each cube is one study-grid cell spanning one World Ocean Atlas standard depth band, coloured by apparent fishing effort in hours — summed across benthic gear types — on a log₁₀ scale. The shaded surface is GEBCO bathymetry, from muted green land through tan shelf to dark brown deep water, and the dark line traces the coastline. The depth axis is reversed so the sea surface sits at the top and the seafloor at the bottom.
Overlapping with a species range
trawlers_3d and purse_seines_3d follow the
{variable}_depth=<value> naming convention, so they
work with the rest of the package exactly as WOA rasters do:
# Example: silky shark (epipelagic, and heavily taken as bycatch in
# tuna purse seines around FADs).
# The IUCN shark/ray/chimaera shapefile carries one row per species range
# polygon. Filter at read time with SQL so the in-memory object is small.
iucn_shp <- "/home/jay/Programming_Projects/Big_Data/SHARKS_RAYS_CHIMAERAS/SHARKS_RAYS_CHIMAERAS.shp"
sp_range <- sf::st_read(
iucn_shp,
query = "SELECT * FROM SHARKS_RAYS_CHIMAERAS WHERE sci_name = 'Carcharhinus falciformis'",
quiet = TRUE
)
# Depth limits are placeholder values for demonstration; in practice pull
# them per-species via fetch_species_assessments() / fill_missing_depths()
# from the IUCN Red List API.
range_rast <- vect_to_envelope(
polygon = sp_range,
template = study_grid,
depth_min = 0,
# The seafloor is just another depth_max constraint: per cell the shallowest
# of the two wins, so the range never extends below the bed.
depth_max = list(500, seafloor)
)
# Mask the 3D effort stack to cells + depths inside the species' per-cell
# depth window. mask() is depth-aware for an envelope: it places the range on
# the effort stack's own `effort_<gear>_depth=<value>` levels and keeps each
# layer's effort only where the species is present at that depth.
# `purse_seines_3d` is already one gear, so it goes in whole; a multi-gear
# stack would be subset by layer name first.
exposed_effort <- mask(purse_seines_3d, range_rast)
# Total purse-seine effort-hours overlapping the silky shark's 3D range.
total_exposed <- sum(
terra::global(exposed_effort, "sum", na.rm = TRUE)$sum,
na.rm = TRUE
)
total_exposed
#> [1] <hours>
# Per-depth exposure profile (effort-hours summed across cells, per layer).
per_depth <- terra::global(exposed_effort, "sum", na.rm = TRUE)
per_depth$depth <- depths(rownames(per_depth))
per_depth[!is.na(per_depth$sum) & per_depth$sum > 0,
c("depth", "sum")]Caveats
There are two groups: limits of the GFW AIS effort product itself, and limits of the depth step added here. The first group is drawn from the GFW data documentation.
Inherited from GFW AIS effort
- AIS coverage favours large, offshore vessels. AIS is mainly required on vessels over 24 m working offshore, so small-scale and artisanal fleets are largely missing. SAR dark-vessel detections (Paolo et al. 2024) cover that gap.
- Class A and Class B reception differ. Smaller vessels carry Class B units, which transmit at lower power. Satellites detect them less reliably, so smaller-vessel effort is under-represented even within the AIS fleet.
- AIS can be disabled or spoofed. Deliberate gaps and falsified identities produce missing or misattributed positions.
- Signal interference in busy areas. In high-traffic regions overlapping AIS transmissions degrade satellite detection, reducing apparent effort.
- “Apparent” fishing is inferred from behaviour. The classifier flags movement patterns that look like searching, setting, soaking, and hauling. It does not confirm that gear went in the water. Expect false positives where vessels slow down or turn for other reasons.
- Vessel-type and gear misclassification. Registry data is inconsistent; MMSI recycling can mix two vessels’ tracks; vessels that switch gears are inherently hard to classify. Treat per-gear effort as a best-available label, not a measurement.
- Comparisons over time are confounded by AIS uptake. Year-on-year changes in apparent effort partly reflect changes in AIS adoption, satellite coverage, and shore-station access, not only changes in real fishing.
Added by this package’s depth-stratification
-
The depth lookup is the analyst’s responsibility.
The package does not ship a default
depth_lookup. GFW does not publish set depth, so any per-gear band is an assumption — and the right assumption depends on the region, fleet, and time period being analysed. -
GFW does not always separate pelagic and benthic gear
variants. Midwater and bottom trawls are the clearest case. In
2D the difference is invisible, but in 3D the two gears occupy
completely different parts of the water column. Whatever you assign in
depth_lookupcarries that uncertainty. - [profile_equal()] preserves total effort-hours but assumes gear distributes evenly across the depths its window reaches; [profile_flat()] double counts and is only appropriate for footprint maps.
-
Depth windows are widened to whole levels. A 10 m
benthic band can be assigned to a level tens of metres thick. This keeps
the hours in the stack, but it means the vertical precision of the
result is the spacing of
standard_depths, not the precision ofdepth_lookup.
Function summary
| Function | Purpose |
|---|---|
gfwr::gfw_ais_fishing_hours() |
Fetch GFW apparent fishing effort for a region, aggregated
server-side by group_by
|
| [gfw_effort_to_raster()] | Rasterise the gfwr tibble onto the study grid, one layer per group level |
| [as_envelope()] | Turn a gear’s effort footprint plus its depth limits into a 2.5D envelope |
| [envelope_to_voxel()] | Spread each cell’s effort down the standard depths its window reaches |
| [profile_equal()] | Allocation that splits a cell’s hours evenly, conserving the total |