Skip to contents

Overview

This vignette walks through the core FishSET workflow on the bundled scallop example data. It starts with raw data loading, moves through QA/QC and data reshaping, and ends with model fitting and diagnostics.

Setup and data loading

We first load FishSET and the example datasets, then place the project in a temporary working folder so the vignette can run without user-specific paths. This keeps the example reproducible while still using the same database-backed workflow as a real project.

The example data cover three pieces of the workflow: trip-level records, port locations, and the zone polygons used to assign fishing choices. A quick look at their structure helps confirm the variables needed later in the workflow are present.

str(scallop[, c("TRIPID", "DATE_TRIP", "PERMIT.y", "DDLAT", "DDLON",
                "ZoneID", "LANDED_OBSCURED", "DOLLAR_OBSCURED")])
## tibble [10,000 × 8] (S3: tbl_df/tbl/data.frame)
##  $ TRIPID         : int [1:10000] 22 65 466 12 6 830 43 57 399 363 ...
##  $ DATE_TRIP      : POSIXct[1:10000], format: "2007-05-01 05:00:00" "2007-05-01 06:00:00" ...
##  $ PERMIT.y       : int [1:10000] 55 305 126 198 224 218 1 356 383 162 ...
##  $ DDLAT          : num [1:10000] 39 38.5 39.8 38.5 38.6 ...
##  $ DDLON          : num [1:10000] -73.7 -74.1 -72.5 -74.1 -73.9 ...
##  $ ZoneID         : num [1:10000] 387312 387446 397224 387446 387331 ...
##  $ LANDED_OBSCURED: num [1:10000] 18273 14899 15277 16493 18945 ...
##  $ DOLLAR_OBSCURED: num [1:10000] 124276 100568 106939 111328 135897 ...
str(scallop_ports)
## tibble [40 × 3] (S3: tbl_df/tbl/data.frame)
##  $ port_name: chr [1:40] "New Bedford city" "Newport News city" "Cape May city" "Township 2" ...
##  $ lon      : num [1:40] -70.9 -76.4 -74.9 -76.7 -75.1 ...
##  $ lat      : num [1:40] 41.6 37 38.9 35.1 38.3 ...
str(tenMNSQR, max.level = 1)
## Classes 'sf' and 'data.frame':   5267 obs. of  10 variables:
##  $ AREA     : num  0.001 0.007 0.008 0.001 0 0.001 0.002 0 0.021 0.008 ...
##  $ PERIMETER: num  0.175 0.774 0.522 0.131 0.038 0.244 0.229 0.045 0.757 0.684 ...
##  $ TEN_     : num  2 3 4 5 6 7 8 9 10 11 ...
##  $ TEN_ID   : num  456412 456413 457315 456416 456416 ...
##  $ LL       : int  456421 456431 457351 456461 456461 456441 456451 457341 456432 457342 ...
##  $ LAT      : int  4555 4555 4555 4555 4555 4555 4555 4555 4545 4545 ...
##  $ LON      : int  6445 6435 7315 6405 6405 6425 6415 7325 6435 7325 ...
##  $ TEMP     : int  2 3 5 6 6 4 5 4 3 4 ...
##  $ LOC      : int  45556445 45556435 45557315 45556405 45556405 45556425 45556415 45557325 45456435 45457325 ...
##  $ geometry :sfc_POLYGON of length 5267; first list element: List of 1
##   ..- attr(*, "class")= chr [1:3] "XY" "POLYGON" "sfg"
##  - attr(*, "sf_column")= chr "geometry"
##  - attr(*, "agr")= Factor w/ 3 levels "constant","aggregate",..: NA NA NA NA NA NA NA NA NA
##   ..- attr(*, "names")= chr [1:9] "AREA" "PERIMETER" "TEN_" "TEN_ID" ...

Now we load the raw data into FishSET using a single project name. The functions save each table to the project database and make the working table available in the R session for the next steps.

load_maindata(dat = scallop, project = project, over_write = TRUE)
## Table saved to database
## 
## ! Data saved to database as scallop_vignetteMainDataTable20260904 (raw) and scallop_vignetteMainDataTable (working). 
## Table is also in the working environment. !
load_port(dat = scallop_ports, port_name = "port_name", project = project)
## Port table saved to database
load_spatial(spat = tenMNSQR, project = project, name = "tenMNSQR")
## Writing layer `scallop_vignettetenMNSQRSpatTable' to data source 
##   `C:\Users\Paul.Carvalho\AppData\Local\Temp\RtmpqUr8Xy//scallop_vignette/data/spat/scallop_vignettetenMNSQRSpatTable.geojson' using driver `GeoJSON'
## Writing 5267 features with 9 fields and geometry type Polygon.
## Writing layer `scallop_vignettetenMNSQRSpatTable20260904' to data source 
##   `C:\Users\Paul.Carvalho\AppData\Local\Temp\RtmpqUr8Xy//scallop_vignette/data/spat/scallop_vignettetenMNSQRSpatTable20260904.geojson' using driver `GeoJSON'
## Writing 5267 features with 9 fields and geometry type Polygon.
## Spatial table saved to project folder as scallop_vignettetenMNSQRSpatTable

Quality assurance and quality control

We begin QA/QC by checking for missing values in the columns that matter for modeling. Removing those rows up front ensures the later formatting and model-fitting steps do not fail on incomplete observations.

scallop_clean <- na_filter(
  dat = "scallop_vignetteMainDataTable",
  project = project,
  x = c("previous_port_lon", "previous_port_lat", "ZoneID"),
  remove = TRUE,
  over_write = TRUE
)
## The following columns contain NAs: previous_port_lat, previous_port_lon, port_name. Consider using na_filter to replace or remove NAs.
## ZoneID  do not contain NAs.All rows containing NAs have been removed from the dataframe.

Next we run the spatial checks to identify observations that fall on land or outside the intended zone domain. This also parses the date column so we can summarize spatial issues over time. Note: removing observations on land or outside the zone polygons is easier in the FishSET GUI.

qaqc_out <- spatial_qaqc(
  dat = "scallop_vignetteMainDataTable",
  project = project,
  spat = "scallop_vignettetenMNSQRSpatTable",
  lon.dat = "DDLON",
  lat.dat = "DDLAT",
  date = "DATE_TRIP"
)
## Warning: Spatial reference EPSG codes for the spatial and primary datasets do not match. 
##             The detected projection in the spatial file will be used unless epsg is specified.
## Spherical geometry (s2) switched off
## although coordinates are longitude/latitude, st_intersects assumes that they
## are planar
## although coordinates are longitude/latitude, st_intersects assumes that they
## are planar
## Warning: 10 observations (0.1%) occur on land.
## although coordinates are longitude/latitude, st_intersects assumes that they
## are planar
## although coordinates are longitude/latitude, st_intersects assumes that they
## are planar
## Warning: 696 observations (7%) occur on boundary line between regulatory zones.
## 10 observations (0.1%) occur on land.
## 696 observations (7%) occur on boundary line between regulatory zones.
qaqc_out$spatial_summary
## # A tibble: 13 × 6
##     YEAR     n EXPECTED_LOC ON_LAND ON_ZONE_BOUNDARY  perc
##    <int> <int>        <int>   <int>            <int> <dbl>
##  1  2007   770          725       2               43  7.71
##  2  2008   819          768       0               51  8.20
##  3  2009   859          819       1               39  8.61
##  4  2010   863          808       1               54  8.65
##  5  2011   818          761       0               57  8.19
##  6  2012   784          745       0               39  7.85
##  7  2013   602          561       1               40  6.03
##  8  2014   523          487       1               35  5.24
##  9  2015   601          551       0               50  6.02
## 10  2016   680          616       0               64  6.81
## 11  2017   786          725       0               61  7.87
## 12  2018   895          829       0               66  8.97
## 13  2019   982          881       4               97  9.84
qaqc_out$land_plot

# Remove observations on land
scallop_spat_clean <- qaqc_out$dataset[which(qaqc_out$dataset$ON_LAND != TRUE), ]

# Removing extra variables that are no longer needed helps speed up the modeling process
cols_to_drop <- c("YEAR", "ON_LAND", "ON_ZONE_BOUNDARY", "EXPECTED_LOC")
scallop_spat_clean <- scallop_spat_clean[, !(names(scallop_spat_clean) %in% cols_to_drop)]

# Save updated data to FishSET database
load_maindata( scallop_spat_clean, project = project, over_write=TRUE)
## Table saved to database
## 
## ! Data saved to database as scallop_vignetteMainDataTable20260904 (raw) and scallop_vignetteMainDataTable (working). 
## Table is also in the working environment. !

To see how the cleaned trips are distributed across zones, we summarize the observations against the ten-minute-square grid. The tabular output and static plot give a quick read on zone coverage before we reshape the data for modeling.

zone_out <- zone_summary(
  dat = "scallop_vignetteMainDataTable",
  spat = "scallop_vignettetenMNSQRSpatTable",
  project = project,
  zone.dat = "ZoneID",
  zone.spat = "TEN_ID",
  output = "tab_plot",
  plot_type = "static",
  dat_lon = "DDLON",
  dat_lat = "DDLAT"
)

zone_out$table
## # A tibble: 461 × 2
##    ZoneID     n
##    <chr>  <int>
##  1 416965   265
##  2 387332   259
##  3 387331   226
##  4 387322   209
##  5 406932   194
##  6 387314   193
##  7 406926   192
##  8 387446   164
##  9 406915   151
## 10 387323   148
## # ℹ 451 more rows
zone_out$plot

FishSET provides several other exploratory functions. Here are a few examples:

# Vessel count by year
vessel_count(scallop_vignetteMainDataTable, 
             project,
             v_id = "PERMIT.y",
             date = "DATE_TRIP",
             period = "year", 
             type = "line",
             output= "plot")
## Joining with `by = join_by(DATE_TRIP)`
## Warning: Setting row names on a tibble is deprecated.

# Scale LANDED_OBSCURED to thousands of pounds
scallop_vignetteMainDataTable$landed_thousands <- 
  scallop_vignetteMainDataTable$LANDED_OBSCURED / 1000

# Create Catch per Unit Effort variables
cpue_out <- cpue(dat =scallop_vignetteMainDataTable, 
                 project,
                 xWeight = "landed_thousands",
                 xTime = "TRIP_LENGTH",
                 name = "cpue")
## Warning: xWeight must a measurement of mass. CPUE calculated.
## Warning: xTime should be a measurement of time. Use the create_duration
## function. CPUE calculated.
# Average CPUE by year and gear code
species_catch(cpue_out, project,
              species = "cpue",
              date = "DATE_TRIP", 
              group = "GEARCODE",
              period = "year",
              fun = "mean",
              type = "line",
              output= "plot")
## Joining with `by = join_by(GEARCODE, DATE_TRIP)`

More FishSET functions can be found on the references page.

Prepare and format data

The model needs a centroid table so it can measure distances from the observed trip location to each alternative zone. Creating the zonal centroid table also confirms that the spatial data and the trip zones line up correctly.

zone_centroid <- create_centroid(
  spat = "scallop_vignettetenMNSQRSpatTable",
  project = project,
  spatID = "TEN_ID",
  type = "zonal centroid",
  output = "centroid table"
)
## Warning in find_centroid(spat = spatdat, project = project, spatID = spatID, :
## Duplicate centroids found for at least one zone. Using first centroid.
## Geographic centroid saved to fishSET database

We then define two alternative-choice sets with different minimum-haul filters. Using two versions lets us compare how the available choice set changes as we tighten the inclusion rules.

create_alternative_choice(
  dat = "scallop_vignetteMainDataTable",
  project = project,
  alt_name = "alt1",
  zoneID = "ZoneID",
  occasion = "lon-lat",
  occasion_var = c("previous_port_lon", "previous_port_lat"),
  alt_var = "zonal centroid",
  min_haul = 5
)
## Alternative choice list 'alt1' saved to FishSET database under table scallop_vignetteAltMatrix
create_alternative_choice(
  dat = "scallop_vignetteMainDataTable",
  project = project,
  alt_name = "alt2",
  zoneID = "ZoneID",
  occasion = "lon-lat",
  occasion_var = c("previous_port_lon", "previous_port_lat"),
  alt_var = "zonal centroid",
  min_haul = 200
)
## Alternative choice list 'alt2' saved to FishSET database under table scallop_vignetteAltMatrix

Expected catch matrices translate the recent catch history into moving window averages for model inputs. We create one matrix for each alternative-choice definition so the later model specification can use the matching expectation set.

create_expectations(
  dat = "scallop_vignetteMainDataTable",
  project = project,
  name = "exp_catch1",
  alt_name = "alt1",
  catch = "LANDED_OBSCURED",
  temp_var = "DATE_TRIP",
  temporal = "daily",
  temp_window = 7,
  day_lag = 1,
  year_lag = 0,
  empty_catch = NA,
  empty_expectation = 1e-4
)
## Expected catch/revenue matrix saved to FishSET database
create_expectations(
  dat = "scallop_vignetteMainDataTable",
  project = project,
  name = "exp_catch2",
  alt_name = "alt2",
  catch = "LANDED_OBSCURED",
  temp_var = "DATE_TRIP",
  temporal = "daily",
  temp_window = 7,
  day_lag = 1,
  year_lag = 0,
  empty_catch = NA,
  empty_expectation = 1e-4
)
## Expected catch/revenue matrix saved to FishSET database

The formatted data step reshapes the project tables into the long format used by the RTMB model code within fishset_fit(). We keep the variables needed for the choice model and distance calculation, then repeat the process for the second alternative-choice set.

format_model_data(
  project = project,
  name = "format_1",
  alt_name = "alt1",
  zone_id = "ZoneID",
  unique_obs_id = "TRIPID",
  select_vars = c("TRIPID", "DATE_TRIP", "ZoneID", "LANDED_OBSCURED"),
  expectations = "exp_catch1",
  distance = TRUE,
  distance_units = "mi"
)
## Warning: CRS is not specfied, distance matrix will be created using WGS 84
## (4326).
## Warning: package 'sf' was built under R version 4.5.3
## Linking to GEOS 3.14.1, GDAL 3.12.1, PROJ 9.7.1; sf_use_s2() is FALSE
## Design object saved to: C:\Users\Paul.Carvalho\AppData\Local\Temp\RtmpqUr8Xy//scallop_vignette/Models/FormattedData/scallop_vignetteLongFormatData.qs2
format_model_data(
  project = project,
  name = "format_2",
  alt_name = "alt2",
  zone_id = "ZoneID",
  unique_obs_id = "TRIPID",
  select_vars = c("GEARCODE", "ZoneID", "DOLLAR_OBSCURED", "LANDED_OBSCURED"),
  expectations = "exp_catch2",
  distance = TRUE,
  distance_units = "mi"
)
## Warning: CRS is not specfied, distance matrix will be created using WGS 84
## (4326).
## Design object saved to: C:\Users\Paul.Carvalho\AppData\Local\Temp\RtmpqUr8Xy//scallop_vignette/Models/FormattedData/scallop_vignetteLongFormatData.qs2

Model design and fit

The first design is a standard conditional logit with expected catch and distance as fixed effects. The second adds area-specific constants so we can compare a more flexible zonal logit specification.

fishset_design(
  formula = chosen ~ exp_catch1 + distance,
  project = project,
  model_name = "clogit1",
  formatted_data_name = "format_1",
  unique_obs_id = "TRIPID",
  zone_id = "ZoneID",
  scale = TRUE
)
## Design object saved to: C:\Users\Paul.Carvalho\AppData\Local\Temp\RtmpqUr8Xy//scallop_vignette/Models/ModelDesigns/clogit1.qs2
fishset_design(
  formula = chosen ~ exp_catch2 + distance + ZoneID,
  project = project,
  model_name = "zlogit1",
  formatted_data_name = "format_2",
  unique_obs_id = "TRIPID",
  zone_id = "ZoneID",
  scale = TRUE
)
## Design object saved to: C:\Users\Paul.Carvalho\AppData\Local\Temp\RtmpqUr8Xy//scallop_vignette/Models/ModelDesigns/zlogit1.qs2

The model-fitting step can take longer than a vignette render budget, so we keep the fitting code in the document but do not evaluate it here. This still shows the exact commands a user would run locally to estimate and print both model tables. The printed tables include estimated coefficients, standard errors, p-values, log-likelihood, AIC, Pseudo R2, and accuracy.

clogit1_fit <- fishset_fit(project = project, model_name = "clogit1")
print(clogit1_fit)

zlogit1_fit <- fishset_fit(project = project, model_name = "zlogit1")
print(zlogit1_fit)

Model diagnostics and validation

Once the fits exist, we can test the IIA assumption with the Hausman-McFadden test and then check whether residuals show spatial correlation. These diagnostics help determine whether the final model is well specified.

clogit1_iia <- fishset_iia_test(project = project, model_name = "clogit1")
print(clogit1_iia)

zlogit1_iia <- fishset_iia_test(project = project, model_name = "zlogit1")
print(zlogit1_iia)
clogit1_resid <- model_resid_corr(
  project = project,
  model_name = "clogit1",
  spat = "scallop_vignettetenMNSQRSpatTable",
  spat_id = "TEN_ID")

print(clogit1_resid)
plot(clogit1_resid)

zlogit1_resid <- model_resid_corr(
  project = project,
  model_name = "zlogit1",
  spat = "scallop_vignettetenMNSQRSpatTable",
  spat_id = "TEN_ID"
)
print(zlogit1_resid)

Reproducibility

FishSET was designed with the aim of reproducibility. All function calls are logged in a dated file. Log files are stored in the src folder. Each log call has a functionID and a list of parameters supplies (args). Some logged functions includes kwargs, optional arguments, an output, or a message. The message section is used to save text output from a function call that users may want to reference later, such as the number of number of rows with missing data.

For example, the function call

filter_table(dat = 'scallop_vignetteMainDataTable',
             project = project, 
             x = 'GEARCODE', 
             exp = 'GEARCODE==1')

returns the following log entry:

{
  "functionID": "filter_table",
  "args": [
    "scallop_vignetteMainDataTable",
    "scallop_vignette",
    "GEARCODE",
    "GEARCODE=='DREDGE-SCALLOP'"
  ],
  "kwargs": [],
  "output": "",
  "msg": [
    {
      "dataframe": "scallop_vignetteMainDataTable",
      "vector": "GEARCODE",
      "FilterFunction": "GEARCODE=='DREDGE-SCALLOP'"
    }
  ]
}

Log entries are written in JSON. Future version of FishSET will include a function that will read the log files and rerun function calls with current or updated data.

Logging is built into FishSET functions. However, it is possible to start a new log file using log_reset. New log files are started each day. User-created functions, such as likelihoods, can be saved for future use and logged using log_func_model.