diff --git a/NAMESPACE b/NAMESPACE index 2bdb74bb..a89a4c9c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,6 +5,7 @@ export(create_latex_table) export(create_rda) export(export_rda) export(extract_caps_alttext) +export(extract_sis_data) export(filter_data) export(plot_aa) export(plot_abundance_at_age) diff --git a/R/extract_sis_data.R b/R/extract_sis_data.R new file mode 100644 index 00000000..9176af07 --- /dev/null +++ b/R/extract_sis_data.R @@ -0,0 +1,397 @@ +#' Extract data from model results to send to SIS +#' +#' Semi-automate the extraction of key quantities from model results and time +#' series data from exported figure/table `.rda` files for eventual transmittance +#' to SIS via `asar::export_to_sis()`. +#' +#' @param sis_data_dir Path. Location to save the sis_assmt_template.csv and +#' sis_ts_template.csv files or, if present, the location of the +#' existing sis_assmt_template.csv file. +#' +#' Default: The working directory. +#' +#' @param key_quantities_dir Path. Location of the existing key_quantities.csv +#' file. +#' +#' Default: The working directory. +#' +#' @param figures_tables_dir Path. Location of the existing 'figures' and +#' 'tables' directories. +#' +#' Default: The working directory. +#' +#' @details See the "Automating data extraction for SIS" vignette for a +#' detailed overview of the workflow and requirements for this function. +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' extract_sis_data( +#' sis_data_dir = getwd(), +#' key_quantities_dir = "my_dir", +#' figures_tables_dir = "my_other_dir" +#' ) +#' } +#' +extract_sis_data <- function(sis_data_dir = getwd(), + key_quantities_dir = getwd(), + figures_tables_dir = getwd() + ) { + # check if existing figures and tables folders exist; if both absent, throw an error + if (!dir.exists(fs::path(figures_tables_dir, "figures")) & !dir.exists(fs::path(figures_tables_dir, "tables"))) { + cli::cli_abort("Neither 'figures' nor 'tables' folders were found in {figures_tables_dir}. Please check the `figures_tables_dir` path, or export figures and tables, and then try again.") + } + + # Check if existing data files exist; if not, start from blank templates + if (!file.exists(fs::path(sis_data_dir, "sis_assmt_template.csv"))) { + assmt_dat <- utils::read.csv(system.file("resources", "sis_assmt_template.csv", package = "stockplotr")) + cli::cli_alert_info("No existing sis_assmt_template.csv found in {sis_data_dir}. Using blank template.") + } else { + assmt_dat <- read.csv(fs::path(sis_data_dir, "sis_assmt_template.csv"), stringsAsFactors = FALSE) + cli::cli_alert_success("Found existing sis_assmt_template.csv in {sis_data_dir}.") + } + + ts_dat <- utils::read.csv(system.file("resources", "sis_ts_template.csv", package = "stockplotr")) + + # extract key quantities from csv and assign to variables + kqs_path <- fs::path(key_quantities_dir, "key_quantities.csv") + if (file.exists(kqs_path)){ + kqs <- read.csv(fs::path(key_quantities_dir, + "key_quantities.csv"), + stringsAsFactors = FALSE) + cli::cli_alert_success("Found existing key_quantities.csv in {key_quantities_dir}.") + } else { + cli::cli_alert_warning("No existing key_quantities.csv found in {key_quantities_dir}.") + cli::cli_alert_info("To obtain key quantities relevant to the sis_assmt_template.csv, run the following functions and specify `make_rda = TRUE`:") + cli::cli_bullets(c( + "*" = "plot_fishing_mortality()", + "*" = "plot_biomass()" + )) + } + + if (file.exists(kqs_path)){ + # insert values into the sis_assmt_template.csv file + mapping <- tibble::tribble( + ~key_quantity, ~Variable, + "F.end.year", "AS_LAST_DATA_YEAR", + "F.MSY.terminal", "AS_FMSY", + "B.msy", "AS_BMSY", + "B.msy.min", "AS_BMSY_MIN", + "B.msy.max", "AS_BMSY_MAX", + "B.terminal.year", "AS_B_YEAR", + "F.terminal.year", "AS_F_YEAR", + "F.target", "AS_FTARGET", + "F.limit", "AS_FLIMIT", + "B.terminal.est", "AS_B_BEST", + "F.terminal.est", "AS_F_BEST", + "B.terminal.min", "AS_B_MIN", + "B.terminal.max", "AS_B_MAX", + "F.terminal.min", "AS_F_MIN", + "F.terminal.max", "AS_F_MAX", + "F.MSY.terminal.max", "AS_FMSY_MAX", + "F.MSY.terminal.min", "AS_FMSY_MIN" + ) + + # Extract values and format into a key-value matching table + new_vals <- kqs |> + dplyr::inner_join(mapping, by = "key_quantity") |> + dplyr::mutate(value = as.numeric(value)) |> + dplyr::select(Variable, new_value = value) + + # Update assmt_dat + assmt_dat <- assmt_dat |> + dplyr::left_join(new_vals, by = "Variable") |> + dplyr::mutate(Value = ifelse(!is.na(new_value), new_value, Value)) |> + dplyr::select(-new_value) + } + + # obtain time series data + if (!dir.exists(fs::path(figures_tables_dir, "figures"))) { + cli::cli_alert_info("'figures' folder not found in {sis_data_dir}.") + cli::cli_alert_danger("Some time series data will not be extracted.") + } else { + fig_ts <- TRUE + # BIOMASS (fits under "abundance" category) + tryCatch( + { + load(fs::path(figures_tables_dir, "figures", "biomass_figure.rda")) |> suppressWarnings() + biomass <- rda[["figure"]][["layers"]][["geom_line"]]$data + biomass <- biomass |> + dplyr::group_by(year) |> + dplyr::summarise(sum = sum(estimate)) |> + dplyr::rename(Biomass = sum) + }, + error = function(e) { + cli::cli_alert_warning("Biomass data was not extracted from the 'biomass_figure.rda' file.") + biomass <<- NULL + } + ) + + # SPAWNERS (fits under "abundance" category) + tryCatch({ + load(fs::path(figures_tables_dir, "figures", "spawning_biomass_figure.rda")) |> suppressWarnings() + sb <- rda[["figure"]][["layers"]][["geom_line"]]$data + spawning_biomass <- sb |> + dplyr::group_by(year) |> + dplyr::summarise(sum = sum(estimate)) |> + dplyr::rename(Spawners = sum) + }, + error = function(e) { + cli::cli_alert_warning("Spawning biomass data was not extracted from the 'spawning_biomass_figure.rda' file.") + spawning_biomass <<- NULL + } + ) + + # RECRUITMENT + tryCatch({ + load(fs::path(figures_tables_dir, "figures", "abundance_at_age_figure.rda")) |> suppressWarnings() + rec <- rda[["figure"]][["layers"]][["geom_point"]][["data"]] + recruitment <- rec |> + dplyr::filter(age == 1) |> + dplyr::group_by(year) |> + dplyr::summarise(sum = sum(estimate)) |> + dplyr::rename(Recruitment = sum) + }, + error = function(e) { + cli::cli_alert_warning("Recruitment (age 1) data was not extracted from the 'abundance_at_age_figure.rda' file.") + recruitment <<- NULL + + # this is for recruitment in mt- not age-1, which is what SIS expects + # load(fs::path(figures_tables_dir, "figures", "recruitment_figure.rda")) |> suppressWarnings() + # rec <- rda[["figure"]][["layers"]][["geom_line"]]$data + # recruitment <- rec |> + # dplyr::group_by(year) |> + # dplyr::summarise(sum = sum(predicted_recruitment)) |> + # dplyr::rename(Recruitment = sum) + # }, + # error = function(e) { + # cli::cli_alert_warning("Recruitment data was not extracted from the 'recruitment_figure.rda' file.") + # recruitment <<- NULL + }) + + # FISHING MORTALITY + tryCatch({ + load(fs::path(figures_tables_dir, "figures", "fishing_mortality_figure.rda")) |> suppressWarnings() + fm <- rda[["figure"]][["layers"]][["geom_line"]]$data + fishing_mortality <- fm |> + dplyr::group_by(year) |> + dplyr::summarise(mean = mean(estimate)) |> + dplyr::rename(Fmort = mean) + }, + error = function(e) { + cli::cli_alert_warning("Fishing mortality data was not extracted from the 'fishing_mortality_figure.rda' file.") + fishing_mortality <<- NULL + }) + + # INDEX + tryCatch({ + load(fs::path(figures_tables_dir, "figures", "index_figure.rda")) |> suppressWarnings() + index <- rda[["figure"]][["layers"]][["geom_line"]]$data + index <- index |> + dplyr::group_by(year) |> + dplyr::summarise(mean = mean(estimate)) |> + dplyr::rename(Index = mean) + }, + error = function(e) { + cli::cli_alert_warning("Index data was not extracted from the 'index_figure.rda' file.") + index <<- NULL + }) + } + if (!dir.exists(fs::path(figures_tables_dir, "tables"))) { + cli::cli_alert_info("'tables' folder not found in {sis_data_dir}.") + cli::cli_alert_danger("Some time series data will not be extracted.") + catch <- NULL + } else { + table_ts <- TRUE + tryCatch( + { + load(fs::path(figures_tables_dir, "tables", "total_catch_table.rda")) |> suppressWarnings() + catch <- rda[["table"]][["_data"]] + catch_cols <- colnames(catch) + cols_without_catch <- c("Sex", "Area", "Season", "Type") + if (any(cols_without_catch %in% catch_cols)) { + catch <- catch |> + dplyr::select(-dplyr::any_of(cols_without_catch)) + } + catch <- catch |> + # remove values in parentheses, if present + dplyr::mutate(dplyr::across(!Year, ~ stringr::str_remove_all(.x, "\\s*\\(.*?\\)"))) |> + dplyr::mutate(dplyr::across(!Year, ~ stringr::str_remove_all(.x, ","))) |> + dplyr::mutate(dplyr::across(!Year, ~ as.numeric(.x))) |> + # summarize non-Year rows + dplyr::rowwise() |> + dplyr::mutate(Catch = sum(dplyr::c_across(!Year), na.rm = TRUE)) |> + dplyr::ungroup() |> + dplyr::select(Year, Catch) + }, + error = function(e) { + cli::cli_alert_warning("Catch data will not be extracted from the 'total_catch_table.rda' file.") + catch <<- NULL + } + ) + } + + if(exists("fig_ts")){ + summaries <- c("biomass", "spawning_biomass", "recruitment", "fishing_mortality", "index") + + summaries <- summaries[sapply(summaries, function(x) !is.null(get(x)))] + + # join all summaries by year + all_summaries <- c() + for (i in seq_along(summaries)) { + if (i == 1) { + all_summaries <- get(summaries[i]) + } else { + all_summaries <- dplyr::full_join(all_summaries, + get(summaries[i]), + by = "year") + } + } + } + if (exists("table_ts") & !is.null(catch)){ + if (!is.null(all_summaries) & exists("fig_ts")){ + catch <- catch |> + dplyr::rename(year = Year) + all_summaries <- dplyr::full_join(all_summaries, + catch, + by = "year") + summaries <- c(summaries, "catch") + } else { + all_summaries <- get("catch") + summaries <- "catch" + } + } + + ts_options <- summaries[!is.na(summaries)] + cli::cli_alert_info("The following time series summaries were extracted:") + cli::cli_ul(ts_options) + primary_options1 <- c("fishing_mortality", "recruitment", "catch") + primary_options2 <- c("spawning_biomass", "biomass") + + if (length(ts_options) == 0) { + primary <- NA + cli::cli_alert_info("Zero time series summaries were extracted. Please check the figures and tables directories.") + } else { + cli::cli_inform("Extracted time series summaries:") + cli::cli_ul(ts_options) + } + primary <- ts_options[ts_options %in% c(primary_options1)] + # if both values in primary_options2 are present in ts_options, ask user to choose one + if (all(primary_options2 %in% ts_options)) { + cli::cli_alert_info("One of the following categories can be a Primary time series:") + cli::cli_ul(primary_options2) + if (interactive()) { + primary1 <- readline("Which category should be chosen? ") + if (primary1 %notin% primary_options2) { + cli::cli_abort("Invalid Primary category specified. Please choose from: {primary_options2}.") + } else { + primary <- c(primary, primary1) + } + } else { + primary <- c(primary, primary_options2[1]) + cli::cli_alert_info("Primary categor{?y/ies} set to {primary} by default in non-interactive mode.") + } + } else if (any(primary_options2 %in% ts_options)) { + primary <- c(primary, ts_options[ts_options %in% primary_options2]) + } + + if (length(primary) == 0) {primary <- NA} + + category_pairs <- list( + "fishing_mortality" = "Fmort", + "recruitment" = "Recruitment", + "catch" = "Catch", + "spawning_biomass" = "Spawners", + "biomass" = "Biomass", + "index" = "Index" + ) + + primary <- unlist(lapply(primary, function(x) category_pairs[[x]])) + + # identify sis_assmt_template AS_B_UNIT values if coded + b_unit <- assmt_dat$Value[assmt_dat$Variable == "AS_B_UNIT"] + if (as.numeric(b_unit) %in% 1:8) { + b_unit <- switch(b_unit, + "1" = "Metric Tons", + "2" = "Thousand Metric Tons", + "3" = "Adult spawners - Natural & Hatchery - Escapement", + "4" = "Adult spawners - Hatchery - Escapement", + "5" = "Adult spawners - Natural - Escapement", + "6" = "Number of Eggs", + "7" = "kg / tow", + "8" = "Number of Fish" + ) + } + + # identify sis_assmt_template AS_F_UNIT values if coded + f_unit <- assmt_dat$Value[assmt_dat$Variable == "AS_F_UNIT"] + if (as.numeric(f_unit) %in% 1:7) { + f_unit <- switch(f_unit, + "1" = "Apical F", + "2" = "Fully-selected F", + "3" = "Exploitation Rate", + "4" = "Relative F", + "5" = "Metric Tons", + "6" = "1 - SPR", + "7" = "Z - M" + ) + } + + + ts_dat_filled <- all_summaries |> + dplyr::rename_with(~ "Year", .cols = matches("^year$")) |> + tidyr::pivot_longer(cols = -Year, names_to = "Category", values_to = "Value") |> + dplyr::mutate(Primary = ifelse(tolower(Category) %in% tolower(primary), "Y", "")) |> + dplyr::mutate(Description = dplyr::case_when( + Category == "Biomass" ~ "Biomass", + Category == "Spawners" ~ "SSB", + Category == "Recruitment" ~ "Recruits - Age 1", + Category == "Fmort" ~ "F for Fully-Selected Fish", + Category == "Index" ~ "Index of Abundance", + Category == "Catch" ~ "Estimated Total Catch", + TRUE ~ NA + )) |> + dplyr::mutate(Unit = dplyr::case_when( + Category == "Biomass" ~ as.character(b_unit), + Category == "Spawners" ~ as.character(b_unit), + Category == "Fmort" ~ as.character(f_unit), + Category == "Index" ~ "", + Category == "Catch" ~ ifelse(kqs$value[kqs$key_quantity == "tot.catch.units"] == " (mt)", "Metric Tons", kqs$value[kqs$key_quantity == "tot.catch.units"]), + Category == "Recruitment" ~ "Number of Fish", + TRUE ~ NA + )) |> + dplyr::relocate(Value, .after = Unit) + + # Ensure ts_dat_filled has same cols as ts_dat + if (isFALSE(any(colnames(ts_dat_filled) == colnames(ts_dat)))) { + cli::cli_abort("Time series data does not match template structure.") + } + + # if assmt_dat$Value is NA and Default is 95, change it to Default + for (i in seq_len(nrow(assmt_dat))) { + if (is.na(assmt_dat$Value[i]) & assmt_dat$Default[i] == 95) { + assmt_dat$Value[i] <- assmt_dat$Default[i] + } + } + + # export files + assmt_dat_path <- fs::path(sis_data_dir, "sis_assmt_template.csv") + ts_dat_path <- fs::path(sis_data_dir, "sis_ts_template.csv") + + if (file.exists(assmt_dat_path) | file.exists(ts_dat_path)) { + cli::cli_alert_info("Existing sis_assmt_template.csv or sis_ts_template.csv found in {sis_data_dir}.") + overwrite <- readline("Do you want to overwrite the existing files? (y/n): ") + if (tolower(overwrite) == "y") { + write.csv(assmt_dat, assmt_dat_path, row.names = FALSE) + write.csv(ts_dat_filled, ts_dat_path, row.names = FALSE) + cli::cli_alert_success("Files overwritten successfully.") + } else { + cli::cli_alert_info("Files not overwritten. Please rename the files, then rerun this function to save the data extracted in this function.") + } + } else { + write.csv(assmt_dat, assmt_dat_path, row.names = FALSE) + write.csv(ts_dat_filled, ts_dat_path, row.names = FALSE) + cli::cli_alert_success("Files saved successfully in {sis_data_dir}.") + } +} diff --git a/inst/WORDLIST b/inst/WORDLIST index 7062e059..2d22da2f 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,8 +1,12 @@ BAM +BMSY Birthseason CPUE FIMS -FIMSdiags +FLIMIT +FMSY +FTARGET +Fmort Hennen Lifecycle MSY @@ -12,13 +16,17 @@ ORCID Petrale Pre Preformatted +Rceattle Rda Rmarkdown Rmd +Spawners +StockSmart Subseason aa alttext asar +assmt bam birthseas cha @@ -40,7 +48,6 @@ gp hline href is’ -kable kableExtra kobe kqs @@ -52,7 +59,6 @@ nsim obsvpred pkgdown preformatted -qmd rceattle rda rda's diff --git a/inst/resources/sis_assmt_template.csv b/inst/resources/sis_assmt_template.csv new file mode 100644 index 00000000..600ad6d6 --- /dev/null +++ b/inst/resources/sis_assmt_template.csv @@ -0,0 +1,77 @@ +Variable,Explanation,Optional,Default,Value +model_identifier,"Argument used to distinguish between base model and a new, updated model sent in subsequent submission. Options: 'base', 'updated_model_1', 'updated_model_2', etc.",YES,base, +AS_POINT_OF_CONTACT,"The lead/corresponding author for a stock assessment, formatted as an email address.",NO,None, +AS_CATCH_DATA,"Categorical classification describing the availability of catch data for use in the stock assessment. This level should be based on the data that was actually used in the final version of the assessment model. Options: 0 (No quantitative catch data available), 1 (Some catch data, but major gaps for some fishery sectors or historical periods), 2 (Enough catch data to establish magnitude/trends for a major fishery sector for data-limited methods or closed fisheries), 3 (Catch data generally available for all sectors, but some gaps exist), 4 (No data gaps substantially impede assessment, but catch is not without uncertainty), 5 (Very complete knowledge of total catch).",NO,None, +AS_ABUNDANCE_DATA,"Categorical classification describing the availability of abundance data for use in the stock assessment. This level should be based on the data that was actually used in the final version of the assessment model. Options: 0 (No indicator of stock abundance/trend), 1 (Fishery-dependent CPUE available with high uncertainty, or expert opinion), 2 (Fishery-dependent CPUE sufficiently standardized for full assessments; no/insufficient fishery-independent data), 3 (Limited fishery-independent survey(s) provide relative abundance; limited spatiotemporal coverage or high variability), 4 (Complete fishery-independent survey(s) provide relative abundance covering large spatial extent over several years), 5 (Calibrated fishery-independent survey(s) or tag-recapture provide absolute abundance).",NO,None, +AS_BIOLOGICAL_DATA,"Categorical classification describing the availability of biological/life history data for use in the stock assessment. This level should be based on the data that was actually used in the final version of the assessment model. Options: 0 (No life history data), 1 (Most life history factors not based on empirical data; derived using proxies/meta-analyses/borrowed), 2 (Some factors based on empirical data, but at least one derived via proxies/meta-analyses/borrowed), 3 (Most factors based on stock-specific empirical data), 4 (Data sufficient to track changes over time in at least growth), 5 (No major gaps in life history knowledge).",NO,None, +AS_ECOSYSTEM_DATA,"Categorical classification describing the usage of ecosystem linkage data in the stock assessment. This level should be based on the data that was actually used in the final version of the assessment model. Options: 0 (No linkage/consideration of ecosystem dynamic/properties), 1 (Ecosystem-based hypotheses inform structure/inputs, but no explicit linkage to drivers), 2 (Includes some form of variability/effect to account for unidentified ecosystem dynamics), 3 (One or more features linked to dynamic data from environment, climate, habitat, or predator-prey), 4 (Linked to dynamic data supported directly by process studies), 5 (Configured to be coupled or linked with an ecosystem process).",NO,None, +AS_COMP_DATA,"Categorical classification describing the availability of size/age composition data for use in the stock assessment. This level should be based on the data that was actually used in the final version of the assessment model. Options: 0 (No composition data collected), 1 (Some collected, but major gaps and not used), 2 (Enough collected to enable data-limited approaches), 3 (Enough collected over sufficient time series to be informative in age/size structured models), 4 (Enough age composition collected over sufficient time series to enable age-structured methods), 5 (Very complete age and size composition data).",NO,None, +AS_MODEL_CAT,"Category of model used to complete the stock assessment (see Table 5.1; NOAA, 2018). Focuses on population dynamics structure, data requirements, and management advice types. If an ensemble approach was used, select the highest category describing one or more models in the ensemble. Options: 1 (Data-limited), 2 (Index-based), 3 (Aggregate Biomass Dynamics), 4 (Virtual Population Analysis), 5 (Statistical Catch-at-Length), 6 (Statistical Catch-at-Age).",NO,None, +AS_TYPE,"Type of stock assessment, with regards to approach, technique, effort level, and complexity (NOAA, 2018). Assigned automatically by SIS. Options: 'Research Stock Assessment' (development/revision of data type/method), 'Research/Operational Stock Assessment' (management advice + substantial revision), 'Operational Assessment' (scientific advice focusing on stock status/catch limits), 'Stock Monitoring Update' (stock-level advice between assessments without changes to methods/data).",NO,None, +AS_REVIEW_TYPE,"Final status of the assessment, chosen from a set of values found in the SIS manual. Options: 1 (Not Reviewed), 2 (Accept Previous Approach, Remand New Attempt), 3 (Full Acceptance), 4 (Partial Acceptance, Fishing Mortality Estimates), 5 (Partial Acceptance, Biomass Estimates), 6 (Partial Acceptance, Status Determinations Only), 7 (Reject, Data Insufficient for Assessment), 8 (Reject, Results Too Uncertain To Be Considered Accurate), 9 (Remand).",NO,None, +ASSESSMENT_ID,Unique numeric identifier assigned to all stock assessment records. Assigned automatically by SIS.,NO,None, +ENTITY_ID,Entity unique identifier value. Assigned automatically by SIS.,NO,None, +AS_YEAR,Year the assessment was completed. Assigned automatically by SIS.,NO,None, +AS_MONTH,Month the assessment was completed. Assigned automatically by SIS.,NO,None, +AS_LAST_DATA_YEAR,Year of the 'latest' data used in the assessment.,NO,Extracted as F.end.year from key_quantities.csv, +AS_B_BASIS,"The basis of the biomass unit. Options: Spawning Stock Biomass, Total Stock Biomass, Survey-Estimated Biomass, Escapement, Stock Reproductive Output, Survey Index, Total Stock Abundance.",NO,None, +AS_F_BASIS,"The basis of the Fishing Mortality unit. Options: 1 (Max F at Age), 2 (F for Fully-Selected Fish), 3 (Catch / Biomass), 4 (Catch / Exploitable Biomass), 5 (Catch), 6 (Fishing Intensity), 7 (True F).",NO,None, +AS_FMSY,Estimated and/or calculated value of Fishing Mortality at MSY.,NO,Extracted as F.MSY.terminal from key_quantities.csv, +AS_F_FLIMIT_RATIO,The ratio of F / Flimit (i.e., AS_F_BEST / AS_FLIMIT). Automatically calculated by SIS but user can provide if desired.,YES,NULL, +AS_F_BEST,"Best estimate of Fishing Mortality. Typically, Best F = Terminal F for the stock assessment unless transformed (e.g., averaging or retrospective adjustment).",NO,Extracted as F.terminal.est from key_quantities.csv, +AS_FLIMIT_BASIS,"Basis for the recommended fishing mortality limit, calculated or directly estimated. Only utilized in Alaska as assessments utilize catch projections in the current year. Most stocks utilize Flimit = Fmsy. Example: 'F from 2024 asmt corresponding to 2023 OFL'.",YES,NULL, +AS_B_YEAR,Year of the Biomass estimate for the stock.,NO,Extracted as B.terminal.year from key_quantities.csv, +AS_B_MAX,Maximum estimated value within the approved confidence interval of the Biomass estimate. Equivalent to the value of Best B Confidence Interval Upper estimate.,NO,Extracted as B.terminal.max from key_quantities.csv, +AS_BMSY,"Estimated stock size that would, on average, produce the maximum sustainable yield when fished at a level equal to FMSY.",NO,Extracted as B.msy from key_quantities.csv, +AS_B_BMSY_RATIO,Ratio of B / Bmsy (i.e., AS_B_BEST / AS_BMSY). Automatically calculated by SIS but user can provide if desired.,YES,NULL, +AS_STOCK_LEVEL_BMSY,"Whether the stock is above, near, or below Bmsy based upon the value provided in the AS_B_BMSY_RATIO field. Automatically calculated by SIS but user can provide if desired. Options: 'Above', 'Near' (between 80% and 99%), 'Below' (<80%).",YES,NULL, +AS_B_MIN,Minimum estimated value within the approved confidence interval of the Biomass estimate. Equivalent to the value of Best B Confidence Interval Lower estimate.,NO,Extracted as B.terminal.max from key_quantities.csv, +AS_B_BEST,"Best estimate of Biomass. Typically, Best B = Terminal B for the stock assessment unless transformed (e.g., averaging or retrospective adjustment).",NO,Extracted as B.terminal.est from key_quantities.csv, +AS_BMSY_BASIS,Basis for the estimated BMSY value. Example: 'B35%'.,NO,None, +AS_FMSY_BASIS,"Estimated fishing mortality rate that, on average, would produce the maximum sustainable yield from a stock at BMSY. Example: 'F35% as proxy'.",NO,None, +AS_F_FMSY_RATIO,The ratio of F / Fmsy (i.e., AS_F_BEST / AS_FMSY). Automatically calculated by SIS but user can provide if desired.,YES,NULL, +AS_FLIMIT,"Recommended fishing mortality limit from the assessment, above which the stock would be considered to be experiencing overfishing.",NO,Extracted as F.limit from key_quantities.csv, +AS_F_YEAR,Terminal year estimate of stock Fishing Mortality. Always corresponds to the year of the Best estimate of Fishing Mortality (AS_F_BEST).,NO,Extracted as F.terminal.year from key_quantities.csv, +AS_F_UNIT,"Unit of measure corresponding to the fishing mortality estimate. Linked to F Basis selections. Options: 1 (Apical F = Max F at Age), 2 (Fully-selected F = F for Fully-Selected Fish), 3 (Exploitation Rate = Catch / Biomass), 4 (Relative F = Catch / Exploitable Biomass), 5 (Metric Tons = Catch), 6 (1 - SPR = Fishing Intensity), 7 (F = Z - M = True F).",NO,None, +AS_B_UNIT,"Unit of measure corresponding to the biomass estimate. Linked to B Basis selections. Options: 1 (Metric Tons = SSB / Total Biomass / Survey-Estimated Biomass), 2 (Thousand Metric Tons = SSB / Total Biomass / Survey-Estimated Biomass), 3 (Adult spawners - Natural & Hatchery - Escapement), 4 (Adult spawners - Hatchery - Escapement), 5 (Adult spawners - Natural - Escapement), 6 (Number of Eggs - Stock Reproductive Output), 7 (kg / tow - Survey Index), 8 (Number of Fish - Total Stock Abundance).",NO,None, +AS_MODEL,Model software package used to complete the final version of the assessment. Example: 'SS'.,NO,None, +AS_MODEL_VERSION,Version of the software package used to complete the final stock assessment. Example: '3.30.22'.,NO,None, +AS_ENSEMBLE_FLAG,"Whether the assessment was completed using an ensemble or multimodeling approach. Options: 'Y' (yes), 'N' (no).",NO,None, +AS_F_TRANSFORM,"Indicator identifying Fishing Mortality best estimates that include terminal year transformations (e.g., retrospective corrections or multi-year averaging). Options: 'Y' (yes), 'N' (no).",NO,None, +AS_B_RANGE_BASIS,"Approach used to calculate the confidence intervals provided for the stock assessment. Options: 'Asymptotic', 'Credible', 'Bootstrapped', user-specified.",YES,NULL, +AS_B_RANGE,Percentile range of the confidence intervals provided for the stock assessment.,YES,95, +AS_B_TRANSFORM,"Indicator identifying Biomass best estimates that include terminal year transformations (e.g., retrospective corrections or multi-year averaging). Options: 'Y' (yes), 'N' (no).",NO,None, +AS_F_MAX,Maximum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to Best F CI Upper estimate.,NO,Extracted as F.terminal.max from key_quantities.csv, +AS_F_MIN,Minimum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to Best F CI Lower estimate.,NO,Extracted as F.terminal.min from key_quantities.csv, +AS_F_RANGE_BASIS,"Approach used to calculate the confidence intervals provided for the stock assessment. Options: 'Asymptotic', 'Credible', 'Bootstrapped', user-specified.",YES,NULL, +AS_F_RANGE,Percentile range of the confidence intervals provided for the stock assessment.,YES,95, +AS_FMSY_MAX,Maximum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to Fmsy CI Upper estimate.,NO,Extracted as F.MSY.terminal.max from key_quantities.csv, +AS_FMSY_MIN,Minimum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to Fmsy CI Lower estimate.,NO,Extracted as F.MSY.terminal.min from key_quantities.csv, +AS_FMSY_RANGE_BASIS,"Approach used to calculate the confidence intervals provided for the stock assessment. Options: 'Asymptotic', 'Credible', 'Bootstrapped', user-specified.",YES,NULL, +AS_FMSY_RANGE,Percentile range of the confidence intervals provided for the stock assessment.,YES,95, +AS_FTARGET,Value of the Ftarget estimate produced by a stock assessment. Often used for stocks in a rebuilding plan.,NO,Extracted as F.target from key_quantities.csv, +AS_FTARGET_BASIS,Approach used to calculate the Ftarget estimate produced by a stock assessment.,NO,None, +AS_MSY,Value of the MSY estimated by the assessment.,NO,None, +AS_MSY_UNIT,"Unit associated with the MSY value. Options: Metric tons, Thousand metric tons, lbs, Thousand lbs, Number of fish.",NO,None, +AS_MSY_MAX,Maximum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to MSY CI Upper estimate.,NO,None, +AS_MSY_MIN,Minimum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to MSY CI Lower estimate.,NO,None, +AS_MSY_RANGE_BASIS,"Approach used to calculate the confidence intervals provided for the stock assessment. Options: 'Asymptotic', 'Credible', 'Bootstrapped', user-specified.",YES,NULL, +AS_MSY_RANGE,Percentile range of the confidence intervals provided for the stock assessment.,YES,95, +AS_BMSY_MAX,Maximum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to Bmsy CI Upper estimate.,NO,Extracted as B.msy.max from key_quantities.csv, +AS_BMSY_MIN,Minimum estimated value within the approved confidence interval of the Fishing Mortality estimate. Equivalent to Bmsy CI Lower estimate.,NO,Extracted as B.msy.min from key_quantities.csv, +AS_BMSY_RANGE_BASIS,"Approach used to calculate the confidence intervals provided for the stock assessment. Options: 'Asymptotic', 'Credible', 'Bootstrapped', user-specified.",YES,NULL, +AS_BMSY_RANGE,Percentile range of the confidence intervals provided for the stock assessment.,YES,95, +AS_BLIMIT,"Stock size threshold, below which the stock is considered to be overfished.",NO,None, +AS_BLIMIT_BASIS,"Basis for the Blimit estimate. Examples: (0.7*Bmsy), B25%, etc.",NO,None, +AS_B_COMMENT,"Specific comments associated with the best estimate of biomass for this assessment. 1,000 character limit.",NO,None, +AS_F_COMMENT,"Specific comments associated with the best estimate of fishing mortality for this assessment. 1,000 character limit.",NO,None, +AS_IAS_FLIMIT,International commission F limit estimate.,YES,NULL, +AS_IAS_FLIMIT_BASIS,International commission estimate of Flimit estimation method. Example: 'msy'.,YES,NULL, +AS_IAS_FMSY,International commission estimate of Fmsy.,YES,NULL, +AS_IAS_FMSY_BASIS,International commission estimate of Fmsy estimation method.,YES,NULL, +AS_IAS_FTARGET,International commission estimate of Ftarget.,YES,NULL, +AS_IAS_FTARGET_BASIS,International commission estimate of Ftarget estimation method.,YES,NULL, +AS_IAS_BLIMIT,International commission biomass limit estimate.,YES,NULL, +AS_IAS_BLIMIT_BASIS,International commission estimate of Blimit estimation method.,YES,NULL, +AS_IAS_BMSY,International commission estimate of Bmsy.,YES,NULL, +AS_IAS_BMSY_BASIS,International commission estimate of Bmsy estimation method.,YES,NULL, \ No newline at end of file diff --git a/inst/resources/sis_ts_template.csv b/inst/resources/sis_ts_template.csv new file mode 100644 index 00000000..a74c1ad5 --- /dev/null +++ b/inst/resources/sis_ts_template.csv @@ -0,0 +1 @@ +Year,Category,Primary,Description,Unit,Value diff --git a/man/extract_sis_data.Rd b/man/extract_sis_data.Rd new file mode 100644 index 00000000..7f7042dc --- /dev/null +++ b/man/extract_sis_data.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_sis_data.R +\name{extract_sis_data} +\alias{extract_sis_data} +\title{Extract data from model results to send to SIS} +\usage{ +extract_sis_data( + sis_data_dir = getwd(), + key_quantities_dir = getwd(), + figures_tables_dir = getwd() +) +} +\arguments{ +\item{sis_data_dir}{Path. Location to save the sis_assmt_template.csv and +sis_ts_template.csv files or, if present, the location of the +existing sis_assmt_template.csv file. + +Default: The working directory.} + +\item{key_quantities_dir}{Path. Location of the existing key_quantities.csv +file. + +Default: The working directory.} + +\item{figures_tables_dir}{Path. Location of the existing 'figures' and +'tables' directories. + +Default: The working directory.} +} +\description{ +Semi-automate the extraction of key quantities from model results and time +series data from exported figure/table `.rda` files for eventual transmittance +to SIS via `asar::export_to_sis()`. +} +\details{ +See the "Automating data extraction for SIS" vignette for a +detailed overview of the workflow and requirements for this function. +} +\examples{ +\dontrun{ +extract_sis_data( + sis_data_dir = getwd(), + key_quantities_dir = "my_dir", + figures_tables_dir = "my_other_dir" +) +} + +} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 6ad964d2..4961ab42 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -27,6 +27,8 @@ navbar: href: articles/how-caps-alttext-are-made.html - text: Standardizing Assessment Model Output href: articles/convert_output_details.html + - text: Automating data extraction for SIS + href: articles/extract-sis-data.html cheatsheet: text: Cheatsheet diff --git a/tests/testthat/test-extract_sis_data.R b/tests/testthat/test-extract_sis_data.R index e69de29b..60a09c1f 100644 --- a/tests/testthat/test-extract_sis_data.R +++ b/tests/testthat/test-extract_sis_data.R @@ -0,0 +1,48 @@ +test_that("extract_sis_data() runs with all possible rdas present", { + save_all_plots(stockplotr::example_data) + table_total_catch(stockplotr::example_data, + module = "TIME_SERIES", + interactive = FALSE, + make_rda = TRUE) + + files <- c( + fs::path("figures", + c("biomass_figure.rda", "spawning_biomass_figure.rda", "abundance_at_age_figure.rda", "fishing_mortality_figure.rda", "index_figure.rda") + ), + fs::path("tables", + "total_catch_table.rda") + ) + + expect_true(all(file.exists(files))) + + extract_sis_data() + + # erase temporary testing files + file.remove(fs::path(getwd(), "sis_assmt_template.csv")) + file.remove(fs::path(getwd(), "sis_ts_template.csv")) + file.remove(fs::path(getwd(), "captions_alt_text.csv")) + file.remove(fs::path(getwd(), "key_quantities.csv")) + unlink(fs::path(getwd(), "figures"), recursive = T) + unlink(fs::path(getwd(), "tables"), recursive = T) +}) + +test_that("extract_sis_data() runs with some of all possible rdas present", { + plot_biomass(stockplotr::example_data, module = "TIME_SERIES", interactive = FALSE, make_rda = TRUE) + plot_fishing_mortality(stockplotr::example_data, interactive = FALSE, make_rda = TRUE) + expect_no_error(extract_sis_data()) + + # test that extract_sis_data() exports csvs + expect_true(file.exists(fs::path(getwd(), "sis_assmt_template.csv"))) + expect_true(file.exists(fs::path(getwd(), "sis_ts_template.csv"))) + + # erase temporary testing files + file.remove(fs::path(getwd(), "sis_assmt_template.csv")) + file.remove(fs::path(getwd(), "sis_ts_template.csv")) + file.remove(fs::path(getwd(), "captions_alt_text.csv")) + file.remove(fs::path(getwd(), "key_quantities.csv")) + unlink(fs::path(getwd(), "figures"), recursive = T) +}) + +test_that("extract_sis_data() fails with no rdas present", { + expect_error(extract_sis_data()) +}) diff --git a/vignettes/extract-sis-data.Rmd b/vignettes/extract-sis-data.Rmd new file mode 100644 index 00000000..7d61084f --- /dev/null +++ b/vignettes/extract-sis-data.Rmd @@ -0,0 +1,137 @@ +--- +title: "Automating data extraction for SIS" +output: + html_document: + toc: true +vignette: > + %\VignetteIndexEntry{Automating data extraction for SIS} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +# Background + +After completing a stock assessment report, authors submit key assessment data to the Stock Information System (SIS) for inclusion in StockSmart. + +This process has traditionally entailed entering data through a GUI interface. The `stockplotr::extract_sis_data()` function makes this process more efficient and reproducible by automating the extraction of data from model results files and populating templates for SIS submission. Afterwards, the `asar::export_to_sis()` function sends the data to SIS. Below, we explain how this process works. + +# Workflow Overview + +This process fits within the following workflow: + +1. SIS automatically assigns metadata and identifiers to the assessment. + +2. Obtain two CSV files containing placeholders for all of the data required by SIS: `sis_assmt_template.csv` (assessment summary data) and `sis_ts_template.csv` (time series data) through one of three ways: + + a. Run `stockplotr::extract_sis_data()` to generate, populate, and export the templates with data originating from a converted model results file. At least one of the following `.rda` files must be present in the `figures/` and/or `tables/` folders: + * `biomass_figure.rda` + * `spawning_biomass_figure.rda` + * `abundance_at_age_figure.rda` + * `fishing_mortality_figure.rda` + * `index_figure.rda` + * `total_catch_table.rda` + Ideally, the `key_quantities.csv` file will also be present (in the `key_quantities_dir`). + + b. Generate blank template files by running `asar::create_blank_sis()`. + + c. Locate blank template files in the `report/` folder generated by running `asar::create_template()`. + +3. Open the templates and manually fill in any remaining required fields (including the metadata/IDs from Step 1). + +4. Run `asar::export_to_sis()` to format and upload this data to a specific Google Drive folder. In the future, this process may occur via an API instead. + +5. The SIS administrator will obtain the files and finalize the records. + +# How to run `extract_sis_data()` + +## Familiarize yourself with the two templates + +### sis_assmt_template.csv + +This file contains metadata and identifiers for the assessment, as well as summary data for the stock. The columns are as follows: + +* **Variable**: SIS database variable name. +* **Explanation**: Variable description. Often includes options. +* **Optional**: Whether the variable is optional or required. If "N", it must be populated before the data can be submitted to SIS. +* **Default**: Default value, if any. +* **Value**: Value of the variable. + +### sis_ts_template.csv + +This file is formatted to contain time series data for the stock. The columns are as follows: + +* **Year**: Observation year. +* **Category**: Time series category. Options include: "Biomass", "Spawners", "Recruitment (age 1)", "Fmort", "Index of Abundance", and "Catch". +* **Primary**: Whether the category is a primary time series category ("Y" or "N"). The following categories can be primary: "Fmort", "Recruitment (age 1)", "Catch", and either "Biomass" OR "Spawners". This function will automatically set the Primary column to "Y" for the applicable time series in each of these categories, and ask the user to confirm which time series should be primary if both "Biomass" and "Spawners" are present. More time series can be added to the template after running `extract_sis_data()`; please see the [Provide additional time series data] section below for more information. +* **Description**: Brief description (e.g., Description for "Fmort" is "F for Fully-Selected Fish"). +* **Unit**: Unit of measurement. +* **Value**: Value. + +## Export at least one relevant figure/table as an `.rda` file + +As the main purpose of this function is to automate the extraction of data into a SIS template, the function requires some data to be present in the `figures/` and/or `tables/` folders. The function will extract data from the `.rda` files created by setting `make_rda = TRUE` in the following plotting functions: + +* **Biomass**: `stockplotr::plot_biomass()` +* **Spawning Biomass**: `stockplotr::plot_spawning_biomass()` +* **Recruitment at Age 1**: `stockplotr::plot_abundance_at_age()` +* **Fishing Mortality**: `stockplotr::plot_fishing_mortality()` +* **Index of Abundance**: `stockplotr::plot_index()` +* **Catch**: `stockplotr::plot_total_catch()` + +## Provide the path to the `key_quantities.csv` file + +If you've exported at least one `stockplotr` figure or table, you should have a "key_quantities.csv" file. Data in this file will also be extracted as follows: + +### Time series + +* The "tot.catch.units" value is used to populate the "Unit" column for the "Catch" time series category. + +### Assessment data + +These values are extracted and inserted into the key_quantities.csv by running `plot_fishing_mortality()` and `plot_biomass()`: + +| key_quantity | Assessment template "Value" | +| :--- | :--- | +| F.end.year | AS_LAST_DATA_YEAR | +| F.MSY.terminal | AS_FMSY | +| B.msy | AS_BMSY | +| B.msy.min | AS_BMSY_MIN | +| B.msy.max | AS_BMSY_MAX | +| B.terminal.year | AS_B_YEAR | +| F.terminal.year | AS_F_YEAR | +| F.target | AS_FTARGET | +| F.limit | AS_FLIMIT | +| B.terminal.est | AS_B_BEST | +| F.terminal.est | AS_F_BEST | +| B.terminal.min | AS_B_MIN | +| B.terminal.max | AS_B_MAX | +| F.terminal.min | AS_F_MIN | +| F.terminal.max | AS_F_MAX | +| F.MSY.terminal.max | AS_FMSY_MAX | +| F.MSY.terminal.min | AS_FMSY_MIN | + +* Units for the **Index of Abundance** and **Recruitment (age 1)** time series categories are set to *blank* and "Number of Fish", respectively. + +## Provide assessment data before or after running `extract_sis_data()` to populate additional fields + +You will probably need to add information to the `sis_assmt_template.csv` to ensure all required information is added. You can do this before or after running `extract_sis_data()`. + +A benefit of doing this before running `extract_sis_data()` is that the function will use the information provided to populate additional fields in the `sis_ts_template.csv` file. For example, if you provide the following information in the assessment template: + +* "AS_B_UNIT" is used to populate the **Unit** column for the **Biomass** AND **Spawners** time series categories. +* "AS_F_UNIT" is used to populate the **Unit** column for the **Fmort** time series category. + +## Provide additional time series data + +The `extract_sis_data()` workflow extracts at most one time series for each category. However, you may have multiple time series for a given category. You can add additional time series to the `sis_ts_template.csv` file as per these rules: + +* **Fmort, Recruitment, & Catch**: Multiple time series allowed per category, but exactly one must be marked Primary = Y per category. +* **Spawners & Biomass**: Multiple time series allowed across both categories, but only one time series combined across both Spawners and Biomass can be marked Primary = Y. +* **Index**: Multiple time series allowed, but none can be marked Primary = Y (always N). \ No newline at end of file