aLBI: Length-Based Indicators and Fish Stock Assessment in R

A Comprehensive Guide to the aLBI Package

Ataher Ali

2026-09-27


1 Introduction

1.1 What is aLBI?

aLBI (Assessment of Length-Based Indicators) is an R package designed for data-limited fish stock assessment using only catch length-frequency data. The package operationalises the Froese (2004) length-based sustainability indicators and the Cope & Punt (2009) decision framework for estimating the probability of a stock falling below target and limit spawning biomass reference points.

Most fish stock assessments in tropical and coastal developing-country fisheries suffer from the absence of the age, tagging, or time-series abundance data required by classical assessment models. aLBI was developed to address this gap: given a single snapshot of the catch length distribution, it estimates key biological reference lengths, computes three sustainability indicators, and quantifies their uncertainty through a novel three-tier simulation framework.

1.2 What’s New in the Current Version

This version introduces several major methodological improvements to FishPar and FishSS, and adds two new functions (FreqTM, LWR):

Component Change
FishPar — Three-tier uncertainty Novel propagation of L_mat / L_opt uncertainty to P_mat, P_opt, P_mega; separate Bootstrap-only CI also reported
FishPar — save_output Single toggle (default FALSE) replaces previous auto-saving
FishPar — Excel output Six sheets now, including a dedicated FishSS_Inputs sheet
FishPar — Visualisation Zone-shaded 6-panel annotation; dumbbell Target vs. Catch; 9-panel three-tier histograms
FishSS — Trigger logic Robust if (Pobj >= 200) Popt else Pmat replaces fragile table-row check
FishSS — Return value Now returns Pobj, Px_trigger, Px_value, StockStatus, Selectivity
FreqTM Multi-month frequency tables with consistent bin structure
LWR Length–weight relationship fitting and visualisation

1.3 Package Citation

If you use aLBI in published research, please cite:

Ali, A., Sarker, M. R., & Alam, M. S. (2025). Development of a simple R package (aLBI) for the estimation of stock status from the length frequency data. Fisheries Research, 288, 107467. https://doi.org/10.1016/j.fishres.2025.107467


2 Installation

# Install from CRAN (stable release)
install.packages("aLBI")

# Install the latest development version from GitHub
# install.packages("devtools")   # required
devtools::install_github("Ataher76/aLBI")

2.0.1 Required Dependencies

aLBI depends on base R packages (stats, graphics, grDevices, utils). The openxlsx package is required only when save_output = TRUE in FishPar.

required_packages <- c("aLBI", "readxl", "openxlsx", "dplyr", "ggplot2")

for (pkg in required_packages) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    warning(paste("Package", pkg, "is required but not installed.",
                  "Install with: install.packages('", pkg, "')"))
  } else {
    suppressPackageStartupMessages(library(pkg, character.only = TRUE))
  }
}

3 Package Functions Overview

Function Purpose Key Input Key Output
FrequencyTable Length-frequency table from raw measurements Individual lengths Binned frequency table
FreqTM Length-frequency tables across multiple months Monthly length data Per-month frequency tables
FishPar Length parameters + Froese indicators with three-tier uncertainty Length-frequency table Parameters, three-tier CIs, plots
FishSS Stock status assessment via Cope & Punt (2009) FishPar outputs + CPdata Stock status probabilities
LWR Length–weight relationship Length & weight data Regression model + plot

The typical workflow is sequential: FrequencyTable → FishPar → FishSS


4 Data Requirements and Preparation

4.1 Input Data Formats

aLBI functions accept data in two general formats:

Raw individual measurements (for FrequencyTable, FreqTM, LWR):

Length (Weight)
12.3 24.1
11.8 21.7
… …

Aggregated length-frequency data (for FishPar):

Length Frequency
10 45
11 102
12 138
… …

Data may be loaded from .xlsx, .csv, or created directly in R. The package ships with example datasets in inst/exdata/.

# List all bundled example datasets
list.files(system.file("exdata", package = "aLBI"))
# ExData.xlsx    — raw individual fish lengths
# LC.xlsx        — length-frequency table (for FishPar)
# lenfreqM.xlsx  — multi-month individual lengths (for FreqTM)
# cpdata.xlsx    — Cope & Punt (2009) look-up table
# LWdata.xlsx    — paired length-weight measurements

5 FrequencyTable: Generating Length-Frequency Distributions

5.1 Methodological Background

Before any length-based analysis can be performed, individual fish length measurements must be grouped into discrete length classes (bins). The choice of bin width directly affects the shape of the apparent length-frequency distribution, the number of identifiable cohort modes, and the precision of derived indicators.

FrequencyTable uses the Optimum Bin Size (OBS) formula of Wang et al. (2020) to automatically select a biologically appropriate bin width when the user does not specify one. The OBS formula is:

\[h = 0.9 \cdot \min(s,\ IQR/1.34) \cdot n^{-1/5}\]

where s is the standard deviation of lengths, IQR is the interquartile range, and n is the sample size. The resulting h is rounded to the nearest integer for practical use.

The upper bound of each class is used as the class representative value (e.g., the class [9, 10) is represented by 10), consistent with common practice in fisheries length-frequency analysis.

5.2 Function Arguments

Argument Type Default Description
data Data frame or vector — Individual fish length measurements
bin_width Numeric NULL Fixed bin width (cm). If NULL, Wang’s OBS formula is used
Lmax Numeric NULL Maximum expected length. If NULL, observed maximum is used
output_file Character "FrequencyTable_Output.xlsx" Name of saved Excel file

5.3 Usage Example

library(readxl)

# Load the bundled example raw-length dataset
lenfreq_path <- system.file("exdata", "ExData.xlsx", package = "aLBI")

if (lenfreq_path == "") {
  stop("ExData.xlsx not found. Ensure aLBI is correctly installed.")
}

length_data <- readxl::read_excel(lenfreq_path)
cat("Dataset dimensions:", nrow(length_data), "rows x", ncol(length_data), "columns\n")
#> Dataset dimensions: 1177 rows x 1 columns
head(length_data, 8)
#> # A tibble: 8 × 1
#>   Length
#>    <dbl>
#> 1   75  
#> 2   66.7
#> 3   66  
#> 4   64  
#> 5   63.3
#> 6   63.2
#> 7   62.3
#> 8   62.2
# Run FrequencyTable with automatic bin width (Wang's OBS formula)
freq_result <- FrequencyTable(
  data        = length_data,
  bin_width   = NULL,     # auto-calculated via Wang's formula
  Lmax        = NULL,     # use observed maximum
  output_file = "FrequencyTable_Output.xlsx"
)

# Inspect outputs
freq_result$lfqTable   # Full frequency distribution table with class intervals
freq_result$lfreq      # Condensed table: upper class boundary and frequency
# Inline reproducible example with generated data
set.seed(42)
example_lengths <- data.frame(
  Length = round(rnorm(300, mean = 28, sd = 6), 1)
)
example_lengths$Length <- pmax(10, pmin(50, example_lengths$Length))

cat("Sample: n =", nrow(example_lengths),
    "| mean =", round(mean(example_lengths$Length), 2),
    "| range:", round(min(example_lengths$Length), 1),
    "–", round(max(example_lengths$Length), 1), "cm\n")
#> Sample: n = 300 | mean = 27.87 | range: 10 – 44.2 cm

5.4 Interpreting the Output

FrequencyTable returns a list with two elements:

Tip: The $lfreq component is the recommended input format for FishPar. Save it as an Excel file using the output_file argument for reproducibility.


6 FreqTM: Multi-Month Length-Frequency Tables

6.1 Methodological Background

Seasonal variation in catch length composition is ecologically informative — it can reflect recruitment pulses, migratory behaviour, or gear-based seasonal selectivity. FreqTM constructs length-frequency tables for each month in a dataset using a consistent bin structure derived once from the full dataset, ensuring that length classes are directly comparable across months.

This is critical for analyses such as ELEFAN (Electronic Length Frequency Analysis) where the same class structure must be maintained across all time periods.

6.2 Function Arguments

Argument Type Default Description
data Data frame — Columns for month and individual fish lengths (any column names)
bin_width Numeric NULL Bin width. If NULL, Wang’s OBS formula is applied to the full dataset
Lmax Numeric NULL Maximum expected length
date_config List list(day=1, year=2025) Sets day and year for converting month names to dates
output_file Character "FreqTM_Output.xlsx" Excel output file name

6.3 Usage Example

# Load multi-month length data
lenfreqM_path <- system.file("exdata", "lenfreqM.xlsx", package = "aLBI")

if (lenfreqM_path == "") {
  stop("lenfreqM.xlsx not found. Ensure aLBI is correctly installed.")
}

monthly_data <- readxl::read_excel(lenfreqM_path)
cat("Months present:", paste(unique(monthly_data[[1]]), collapse = ", "), "\n")
#> Months present: Jan, Feb, Mar, Apr, May, Jun, Jul, Aug
cat("Total observations:", nrow(monthly_data), "\n")
#> Total observations: 1500
head(monthly_data, 6)
#> # A tibble: 6 × 2
#>   Months Length
#>   <chr>   <dbl>
#> 1 Jan       9.3
#> 2 Jan      11.5
#> 3 Jan       8.7
#> 4 Jan       7.4
#> 5 Jan       8.6
#> 6 Jan       9
# Generate monthly frequency tables with consistent bin structure
monthly_freq <- FreqTM(
  data        = monthly_data,
  bin_width   = NULL,
  Lmax        = NULL,
  date_config = list(day = 15, year = 2024),
  output_file = "FreqTM_Output.xlsx"
)

# View results for the first month
monthly_freq[[1]]

6.4 Notes on Date Configuration

The date_config argument converts month names (e.g., "January", "February") into date objects formatted as day.month.year (e.g., 15.01.2024). This facilitates direct use in time-series growth analyses that require date-indexed length data.


7 FishPar: Length-Based Indicators with Three-Tier Uncertainty

7.1 Methodological Framework

FishPar is the core function of the aLBI package. It implements a rigorous two-stage simulation pipeline:

  1. Stage 1 — Monte Carlo (MC) simulation for biological reference lengths (L_max, L_inf, L_mat, L_opt).
  2. Stage 2 — Three-tier uncertainty propagation for Froese (2004) sustainability indicators (P_mat, P_opt, P_mega).

The separation of these two stages is methodologically important and reflects a key advance over both the classical manual calculation approach and earlier versions of the package.

7.2 Monte Carlo Simulation for Length Parameters

7.2.1 From L_max to L_inf

In data-limited contexts, the asymptotic growth length L_∞ cannot be estimated from age-at-length or tagging data. Instead, it is approximated from the maximum observed length in the catch via:

\[L_{\infty} = L_{\max} / 0.95\]

This reflects the empirical observation that the largest fish in a well-sampled catch attains approximately 95% of the asymptotic length (Froese & Pauly 2020). To propagate uncertainty from incomplete sampling of the largest size classes, L_max at each MC iteration is drawn from a truncated Gaussian distribution:

\[L_{\max,i} \sim \mathcal{N}(\bar{L}_{\max},\ 0.05 \cdot \bar{L}_{\max}), \quad \text{bounded to } [0.90 \cdot \bar{L}_{\max},\ 1.10 \cdot \bar{L}_{\max}]\]

7.2.2 From L_inf to L_mat

Length at first maturity L_mat is estimated using the cross-species allometric regression of Froese & Binohlan (2000):

\[\log_{10}(L_{\text{mat},i}) = 0.8979 \cdot \log_{10}(L_{\infty,i}) - 0.0782 + \varepsilon_{1,i}\]

where \(\varepsilon_{1,i} \sim \mathcal{N}(0,\ 0.015)\) represents regression residual uncertainty in log₁₀ space.

7.2.3 From L_mat to L_opt

The optimal fishing length L_opt — the body length at which yield per recruit is maximised — is estimated as:

\[\log_{10}(L_{\text{opt},i}) = 1.053 \cdot \log_{10}(L_{\text{mat},i}) - 0.0565 + \varepsilon_{2,i}\]

where \(\varepsilon_{2,i} \sim \mathcal{N}(0,\ 0.015)\). Both regressions were derived from 248 fish species and exhibit \(r^2 \geq 0.98\) (Froese & Binohlan 2000).

The boundaries of the optimal size range follow Froese (2004):

\[L_{\text{opt\_m10}} = 0.90 \cdot L_{\text{opt}}; \qquad L_{\text{opt\_p10}} = 1.10 \cdot L_{\text{opt}}\]

Point estimates for all six parameters are the means of the MC distributions; 95% confidence intervals are the 2.5th and 97.5th percentiles.

7.3 Froese (2004) Sustainability Indicators

Three length-based sustainability indicators are computed from the catch composition and the estimated reference lengths.

P_mat — proportion of mature fish in the catch:

\[P_{\text{mat}} = \frac{\sum_{k: L_k \geq L_{\text{mat}}} f_k}{N} \times 100\%\]

Target: P_mat = 100% (all caught fish should be mature).

P_opt — proportion of fish at optimal harvest size:

\[P_{\text{opt}} = \frac{\sum_{k: L_{\text{opt\_m10}} \leq L_k \leq L_{\text{opt\_p10}}} f_k}{N} \times 100\%\]

Target: P_opt = 100% (ideally all fish are in the optimal size window).

P_mega — proportion of mega-spawners (large, highly fecund fish):

\[P_{\text{mega}} = \frac{\sum_{k: L_k > L_{\text{opt\_p10}}} f_k}{N} \times 100\%\]

Target: P_mega ≥ 20% (at least 20% of the catch should be large, reproductively valuable fish).

where \(L_k\) is the midpoint of length class \(k\), \(f_k\) is its catch frequency, and \(N = \sum_k f_k\) is the total sample size.

7.3.1 The Composite Objective Index (P_obj)

Following Cope & Punt (2009):

\[P_{\text{obj}} = P_{\text{mat}} + P_{\text{opt}} + P_{\text{mega}}\]

This index ranges from 0 to 300 and governs the trigger indicator and look-up table selection in FishSS:

P_obj range Trigger indicator (P_x) Interpretation
< 100 P_mat Predominantly juvenile/sub-optimal catch
100 – 200 P_mat Catch spans the maturity ogive
≥ 200 P_opt Predominantly mature and optimally-sized catch

7.4 Three-Tier Uncertainty Propagation (Novel)

The critical methodological advance of the current FishPar is its three-tier uncertainty decomposition. This directly addresses the fundamental limitation of manual and classical bootstrap approaches: both compute P_mat, P_opt, and P_mega using fixed point-estimate values of L_mat and L_opt, thereby ignoring the substantial uncertainty in those parameters.

7.4.1 The Step-Function Problem

P_mat is not a smooth function of L_mat — it is a step function that jumps discontinuously whenever L_mat crosses a length class boundary. For example, if the length class width is 1 cm and there are 158 fish in the 12-cm class, then:

  • L_mat = 11.9 cm → P_mat = 61.9%
  • L_mat = 12.1 cm → P_mat = 43.1%

A 0.2 cm shift in L_mat causes an 18.7 percentage-point jump in P_mat. The MC distribution of L_mat will inevitably straddle such boundaries, so the expected P_mat across all MC iterations will differ from the P_mat computed at the mean L_mat alone. This is why model outputs systematically differ from manual calculations, and why the difference is methodologically correct, not an error.

7.4.2 The Three Tiers

Tier Parameters Catch data Uncertainty captured
Tier 1 — Parameter only MC-varying (L_mat_i, L_opt_i) Observed (fixed) Parameter estimation uncertainty only
Tier 2 — Data only Fixed at MC means Bootstrap resampled Catch-data sampling variability only
Tier 3 — Total (novel) MC-varying Bootstrap resampled Total = Parameter + Data

At each of the B iterations, the loop computes all three tiers simultaneously:

Tier 1 (i-th MC parameters × observed catch):

\[I_i^{(1)} = f(\mathbf{x}_{\text{obs}},\ \theta_i)\]

Tier 2 (fixed mean parameters × bootstrap resample):

\[I_i^{(2)} = f(\mathbf{x}_i^*,\ \bar\theta)\]

Tier 3 (i-th MC parameters × bootstrap resample):

\[I_i^{(3)} = f(\mathbf{x}_i^*,\ \theta_i)\]

Under approximate parameter–data independence, the total variance satisfies:

\[\text{Var}(I^{(3)}) \approx \text{Var}(I^{(1)}) + \text{Var}(I^{(2)})\]

All 95% CIs are computed as the 2.5th–97.5th percentiles of the B-iteration distributions — no normality assumption is imposed.

Reporting recommendation: Report Tier 3 as the primary CI in manuscripts (total uncertainty, most conservative). Tier 2 is appropriate only when length parameters are independently known. Tier 1 is most useful as a diagnostic showing the isolated sensitivity of indicators to allometric regression uncertainty.

7.5 Function Arguments

Argument Type Default Description
data Data frame — Two columns: Length and Frequency
resample Integer 1000 Number of MC / bootstrap iterations. Use ≥ 5000 for publication
progress Logical FALSE Display text progress bar
Linf Numeric NULL Known L_inf (overrides L_max/0.95 estimate)
Linf_sd Numeric 0.5 Gaussian noise SD added to each L_inf sample (cm)
Lmat Numeric NULL Known L_mat (overrides regression estimate)
Lmat_sd Numeric 0.5 Gaussian noise SD added to each L_mat sample (cm)
save_output Logical FALSE Write all PDFs and Excel to getwd(). Set TRUE to save

7.6 Usage Example

library(readxl)

# Load the bundled length-frequency dataset
lf_path <- system.file("exdata", "LC.xlsx", package = "aLBI")
if (lf_path == "") stop("LC.xlsx not found. Please reinstall aLBI.")

lf_data <- readxl::read_excel(lf_path)
cat("Length-frequency data:\n")
#> Length-frequency data:
print(lf_data)
#> # A tibble: 15 × 2
#>    LengthClass Frequency
#>          <dbl>     <dbl>
#>  1          12        26
#>  2          15       166
#>  3          18       244
#>  4          21       582
#>  5          24       973
#>  6          27      1067
#>  7          30       963
#>  8          33       511
#>  9          36       472
#> 10          39       286
#> 11          42       173
#> 12          45       171
#> 13          48        83
#> 14          51        36
#> 15          54        36
cat("\nTotal individuals:", sum(lf_data[[2]]), "\n")
#> 
#> Total individuals: 5789
# ── Basic usage (plots shown on screen; no files written) ──────────────────
results <- FishPar(
  data        = lf_data,
  resample    = 1000,
  progress    = FALSE,
  Linf        = NULL,    # derive L_inf from L_max
  Linf_sd     = 0.5,
  Lmat        = NULL,    # derive L_mat from allometric regression
  Lmat_sd     = 0.5,
  save_output = FALSE    # set TRUE to write PDFs + Excel to disk
)

# ── With known L_inf from FishBase or literature ──────────────────────────
results_linf <- FishPar(
  data        = lf_data,
  resample    = 1000,
  Linf        = 18.5,    # species-specific L_inf (cm)
  Linf_sd     = 0.5,
  save_output = FALSE
)

# ── Save all outputs to working directory ─────────────────────────────────
results_saved <- FishPar(
  data        = lf_data,
  resample    = 5000,    # increase for publication-quality CIs
  save_output = TRUE     # writes PDFs + FishPar_Results.xlsx
)

7.7 Accessing Results

# ── Length parameters (MC mean ± 95% CI) ─────────────────────────────────
results$estimated_length_par

# ── Three-tier Froese indicator CIs ──────────────────────────────────────
results$froese_par_mc         # Tier 1: parameter uncertainty only
results$froese_par_bootstrap  # Tier 2: data-sampling uncertainty only
results$froese_par_joint      # Tier 3: total uncertainty (recommended)

# ── Derived scalars ───────────────────────────────────────────────────────
results$LM_ratio    # L_mat / L_opt
results$Pobj        # P_mat + P_opt + P_mega (0–300 scale, Cope & Punt 2009)
results$Total_ind   # total observed individuals

# ── Target comparison ─────────────────────────────────────────────────────
results$froese_ind_vs_target

7.7.1 Expected Output Structure

$estimated_length_par
  Parameters Mean_estimate Lower_CI Upper_CI
1       Lmax         18.00    16.20    19.80
2       Linf         18.95    17.05    20.84
3       Lmat         11.71    10.45    13.09
4       Lopt         11.72    10.46    13.10
5   Lopt_p10         12.89    11.51    14.41
6   Lopt_m10         10.55     9.41    11.79

$froese_par_joint                 # Tier 3 — primary reporting CI
  Parameters  Mean Lower_CI Upper_CI          Source
1       Pmat 56.47    28.78    77.13  Tier3_Joint_total
2       Popt 38.38    26.78    52.96  Tier3_Joint_total
3      Pmega 36.40    14.22    62.68  Tier3_Joint_total

7.8 Generated Plots

When save_output = TRUE, FishPar writes ten PDF files to getwd():

File Description
Length_Frequency_Plot.pdf Bar histogram + Gaussian KDE smooth + weighted mean length
Main_Graph_Annotations.pdf 6-panel zone-shaded LFD with each parameter as a vertical line
Length_Parameters_Histograms.pdf 6-panel MC distributions with density overlay
Length_Parameters_Density.pdf 6-panel MC kernel densities with shaded fills
Length_Parameters_CI.pdf Forest plot: mean ± 95% CI for all six parameters
Froese_Indicators_Histograms.pdf 9-panel histograms (3 tiers × 3 indicators)
Froese_Indicators_Density.pdf 9-panel kernel densities (3 tiers × 3 indicators)
Froese_Indicators_CI.pdf Forest plot: Tier 3 CI with Froese (2004) target diamonds
Froese_Uncertainty_3Tier.pdf Three-panel comparative forest plot (one panel per tier)
Target_vs_Catch_Dumbbell.pdf Dumbbell chart: observed vs. target with management status colours

7.8.1 Reading the Three-Tier Uncertainty Plot

The Froese_Uncertainty_3Tier.pdf is the most informative diagnostic plot. It shows three panels side by side, one per tier, each displaying all three indicators (P_mat, P_opt, P_mega) on the x-axis. Each panel has an independently scaled y-axis, which is crucial for readability:

  • Tier 1 (red) and Tier 3 (green): y-axis spans the full wide CI range (often nearly 0–100%), reflecting substantial parameter uncertainty.
  • Tier 2 (blue): y-axis is zoomed in to the narrow CI range (typically ±1–3 percentage points), reflecting only the sampling precision of the catch data.

The contrast between Tier 2 (tight) and Tier 3 (wide) visually communicates the dominant role of allometric regression uncertainty in total indicator uncertainty.

7.9 The Excel Workbook (FishPar_Results.xlsx)

The Excel workbook contains six sheets:

Sheet Contents
Length_Parameters MC mean ± 95% CI for all six length parameters
Froese_Tier1_ParOnly Tier 1 CI — parameter uncertainty only
Froese_Tier2_DataOnly Tier 2 CI — data sampling uncertainty (classical bootstrap)
Froese_Tier3_Joint Tier 3 CI — total uncertainty (recommended for reporting)
Target_vs_Catch Observed P_mat, P_opt, P_mega vs. Froese (2004) targets
FishSS_Inputs LM_ratio, P_mat, P_opt, P_mega, P_obj, Total_ind — ready for FishSS()

The FishSS_Inputs sheet is designed to streamline the workflow: all values required by FishSS() can be read directly from this sheet without manual extraction.


8 FishSS: Stock Status Assessment

8.1 Methodological Background

FishSS implements the Cope & Punt (2009) decision framework for length-based stock status assessment. It uses the composite objective index P_obj and the length-maturity ratio LM_ratio to select the appropriate columns from the published look-up table (Table 5, Cope & Punt 2009), then interpolates to find:

8.1.1 Column Selection Logic

The look-up table has 10 probability columns (A–J) corresponding to different combinations of P_obj regime and LM_ratio category. In aLBI, the trigger indicator column (Tx) is converted to a percentage scale (0–100%, in steps of 5%) to maintain exact numerical alignment with FishPar indicator outputs:

P_obj LM_ratio Columns used Trigger P_x
< 100 ≤ 0.75 A (target), C (limit) P_mat
< 100 ≥ 0.90 B (target), D (limit) P_mat
100–200 ≤ 0.75 E (target), G (limit) P_mat
100–200 ≥ 0.90 F (target), H (limit) P_mat
≥ 200 Any I (target), J (limit) P_opt

The trigger indicator P_x is the catch proportion used as the “trigger value” to locate the appropriate row in the selected columns:

\[P_x = \begin{cases} P_{\text{mat}} & \text{if } P_{\text{obj}} < 200 \\ P_{\text{opt}} & \text{if } P_{\text{obj}} \geq 200 \end{cases}\]

Important fix in the current version: Earlier versions used a fragile if (p[[1,2]] > 0) check to determine the trigger indicator, which silently failed when CPdata was not sorted in descending order by Tx. The current version uses the direct rule above, which is robust to any row ordering of CPdata.

8.1.2 Selectivity Classification

Based on P_obj, FishSS classifies the catch selectivity pattern:

P_obj Pattern
< 100, P_opt = P_mega = 0 Fish small, immature
< 100, P_opt or P_mega > 0 Fish small and optimally-sized or all but biggest
100–200 Fish maturity ogive
≥ 200, P_opt < 100 Fish optimally-sized and bigger
≥ 200, P_opt = 100 Fish optimally-sized

8.2 Function Arguments

Argument Type Description
data Data frame Cope & Punt (2009) look-up table (CPdata, columns: Tx, A–J)
LM_ratio Numeric L_mat / L_opt ratio (from FishPar results)
Pmat Numeric Proportion of mature fish, % (from FishPar)
Popt Numeric Proportion at optimal size, % (from FishPar)
Pmega Numeric Proportion of mega-spawners, % (from FishPar)

8.3 Usage Example

# Load the Cope & Punt (2009) look-up table
cpdata_path <- system.file("exdata", "cpdata.xlsx", package = "aLBI")
if (cpdata_path == "") stop("cpdata.xlsx not found. Reinstall aLBI.")

cpdata <- readxl::read_excel(cpdata_path)
cat("CPdata structure:", nrow(cpdata), "rows ×", ncol(cpdata), "columns\n")
#> CPdata structure: 21 rows × 11 columns
cat("Columns:", paste(colnames(cpdata), collapse = ", "), "\n")
#> Columns: Tx, A, B, C, D, E, F, G, H, I, J
print(head(cpdata, 6))
#> # A tibble: 6 × 11
#>      Tx     A     B     C     D     E     F     G     H     I     J
#>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1   100     0     0     0     0     0     0     0     0   100   100
#> 2    95     0     0     0     0    22     0    11     0   100    93
#> 3    90     0     0     0     0   100    44    83    22   100    74
#> 4    85     0     0     0     0   100   100   100    67   100    63
#> 5    80     0     0     0     0   100   100   100   100    89    52
#> 6    75     0     0     0     0   100   100   100   100    74    37
# ── Method 1: Using FishPar results directly ──────────────────────────────
# (Recommended — uses Tier 3 joint means as point estimates)
stock_status <- FishSS(
  data     = cpdata,
  LM_ratio = results$LM_ratio,
  Pmat     = results$froese_par_joint$Mean[1],  # Tier 3 mean P_mat
  Popt     = results$froese_par_joint$Mean[2],  # Tier 3 mean P_opt
  Pmega    = results$froese_par_joint$Mean[3]   # Tier 3 mean P_mega
)

# ── Method 2: Using FishSS_Inputs sheet from saved Excel ─────────────────
library(openxlsx)
ss_inputs <- openxlsx::read.xlsx("FishPar_Results.xlsx",
                                  sheet = "FishSS_Inputs")

stock_status <- FishSS(
  data     = cpdata,
  LM_ratio = ss_inputs$Value[ss_inputs$Parameter == "LM_ratio"],
  Pmat     = ss_inputs$Value[ss_inputs$Parameter == "Pmat"],
  Popt     = ss_inputs$Value[ss_inputs$Parameter == "Popt"],
  Pmega    = ss_inputs$Value[ss_inputs$Parameter == "Pmega"]
)

# View the full stock status assessment
stock_status

8.3.1 Expected Return Value

stock_status

# $Pobj
# [1] 131.25     <- P_mat + P_opt + P_mega (0–300 scale)

# $Px_trigger
# [1] "Pmat"     <- trigger indicator (P_mat when Pobj < 200)

# $Px_value
# [1] 56.47      <- value of trigger indicator (%)

# $StockStatus
# p_below_target  p_below_limit
#            100            100   <- probabilities (%) from look-up table

# $Selectivity
# [1] "Fish maturity ogive"   <- catch selectivity pattern

8.4 Interpreting Stock Status

The two probability values returned in $StockStatus should be interpreted together:

p_below_target p_below_limit Likely stock status
< 50% < 10% Healthy — stock likely above target reference point
50–80% 10–40% Cautionary — stock may be below target; increase monitoring
> 80% > 40% Overfished — stock likely below target; reduce fishing pressure
> 95% > 70% Depleted — stock likely at or below limit reference point

Important: These probabilities are based on a simulation study integrating steepness values and assume that the catch length distribution is representative of the exploited population. They should be interpreted alongside biological knowledge of the target species.

8.4.1 Feeding Uncertainty into FishSS

The current FishSS uses point estimates of P_mat, P_opt, and P_mega. To propagate the full three-tier uncertainty through to the stock status probabilities, one can run FishSS multiple times using values sampled from the Tier 3 bootstrap distribution. This is facilitated by the froese_par_joint matrix returned by FishPar.


9 LWR: Length–Weight Relationship

9.1 Methodological Background

The length–weight relationship (LWR) describes the power-law relationship between fish body length (L) and weight (W):

\[W = a \cdot L^b\]

Taking logarithms linearises this to:

\[\ln(W) = \ln(a) + b \cdot \ln(L)\]

The parameter b is biologically interpretable:

9.2 Function Arguments

Argument Type Default Description
data Data frame — Two columns: length (col 1) and weight (col 2)
log_transform Logical TRUE Apply log–log transformation for linearisation
point_col Character "black" Data point colour
line_col Character "red" Regression line colour
shade_col Character "red" Confidence interval ribbon colour
point_size Numeric 2 Data point size
line_size Numeric 1 Regression line thickness
alpha Numeric 0.2 Confidence interval ribbon transparency
main Character "Length-Weight Relationship" Plot title
xlab Character NULL Custom x-axis label (auto-generated if NULL)
ylab Character NULL Custom y-axis label (auto-generated if NULL)
save_output Logical FALSE Save plot (PDF) and model summary (TXT) to getwd()

9.3 Usage Example

# Load the bundled length-weight dataset
lw_path <- system.file("exdata", "LWdata.xlsx", package = "aLBI")
if (lw_path == "") stop("LWdata.xlsx not found. Reinstall aLBI.")

LWdata <- readxl::read_excel(lw_path)
cat("LW dataset: n =", nrow(LWdata), "fish\n")
#> LW dataset: n = 554 fish
cat("Length range:", round(min(LWdata[[1]]), 1), "–",
    round(max(LWdata[[1]]), 1), "cm\n")
#> Length range: 11.4 – 58.7 cm
cat("Weight range:", round(min(LWdata[[2]]), 2), "–",
    round(max(LWdata[[2]]), 2), "g\n")
#> Weight range: 11 – 1120 g
head(LWdata)
#> # A tibble: 6 × 2
#>   Length Weight
#>    <dbl>  <dbl>
#> 1   58.7   1025
#> 2   58.6   1013
#> 3   54.6   1011
#> 4   54.3    941
#> 5   54.2    946
#> 6   53     1120
# Fit and plot the length-weight relationship
lwr_result <- LWR(
  data          = LWdata,
  log_transform = TRUE,      # log-log linearisation (recommended)
  point_col     = "black",
  line_col      = "#C0392B", # red regression line
  shade_col     = "#C0392B",
  point_size    = 2,
  line_size     = 1,
  alpha         = 0.2,
  main          = "Length-Weight Relationship",
  xlab          = NULL,      # auto-label: "log(Length)" or "Length"
  ylab          = NULL,      # auto-label: "log(Weight)" or "Weight"
  save_output   = FALSE      # set TRUE to write PDF + TXT to disk
)

# Model summary
lwr_result$model_summary   # intercept, slope, r², p-value
lwr_result$plot            # ggplot2 object

9.3.1 Output Annotation

The LWR plot is annotated with:

  • Regression equation in back-transformed form: W = a · L^b
  • R² — proportion of variance in weight explained by length
  • p-value — significance of the relationship
  • 95% confidence interval ribbon around the fitted line

10 Integrated Workflow: From Raw Data to Stock Status

This section demonstrates the complete aLBI workflow from raw length measurements to a stock status assessment.

# ─────────────────────────────────────────────────────────────────────────
# STEP 1: Build the length-frequency table from raw measurements
# ─────────────────────────────────────────────────────────────────────────
length_data <- readxl::read_excel("my_length_data.xlsx")

freq_result <- FrequencyTable(
  data        = length_data,
  bin_width   = 1,         # 1-cm bins (or NULL for Wang's OBS formula)
  save_output = FALSE
)

# Extract the frequency table for FishPar
lf_table <- freq_result$lfreq
print(lf_table)

# ─────────────────────────────────────────────────────────────────────────
# STEP 2: Estimate length parameters and Froese indicators
# ─────────────────────────────────────────────────────────────────────────
results <- FishPar(
  data        = lf_table,
  resample    = 5000,       # 5000 iterations for stable CIs
  save_output = FALSE       # writes FishPar_Results.xlsx + all PDFs
)

# Review key outputs
cat("=== Length Parameters ===\n"); print(results$estimated_length_par)
cat("\n=== Froese Indicators (Tier 3 — Total Uncertainty) ===\n")
print(results$froese_par_joint)
cat("\nPobj:", round(results$Pobj, 2), "(0–300 scale)\n")
cat("LM_ratio:", round(results$LM_ratio, 3), "\n")

# ─────────────────────────────────────────────────────────────────────────
# STEP 3: Assess stock status
# ─────────────────────────────────────────────────────────────────────────
cpdata <- readxl::read_excel(
  system.file("exdata", "cpdata.xlsx", package = "aLBI")
)

# Use Tier 3 means as point estimates for FishSS
stock_status <- FishSS(
  data     = cpdata,
  LM_ratio = results$LM_ratio,
  Pmat     = results$froese_par_joint$Mean[1],
  Popt     = results$froese_par_joint$Mean[2],
  Pmega    = results$froese_par_joint$Mean[3]
)

cat("\n=== Stock Status Assessment ===\n")
cat("P_obj:", stock_status$Pobj, "\n")
cat("Trigger indicator:", stock_status$Px_trigger,
    "(value =", round(stock_status$Px_value, 1), "%)\n")
cat("Selectivity pattern:", stock_status$Selectivity, "\n")
cat("P(biomass < 0.40 SB0):", stock_status$StockStatus["p_below_target"], "%\n")
cat("P(biomass < 0.25 SB0):", stock_status$StockStatus["p_below_limit"], "%\n")

# ─────────────────────────────────────────────────────────────────────────
# STEP 4 (optional): Length-weight relationship
# ─────────────────────────────────────────────────────────────────────────
lw_data <- readxl::read_excel("my_lw_data.xlsx")
lwr_result <- LWR(data = lw_data, log_transform = TRUE, save_output = FALSE)

11 Why Model Outputs Differ from Manual Calculations

11.1 The Core Problem: Fixed vs. Distributed Parameters

A common question from users (and a valid peer-review concern) is why the P_mat, P_opt, and P_mega values from FishPar differ from values computed by hand using the same Froese & Binohlan (2000) regressions. This section explains the discrepancy with a worked numerical example.

11.2 Worked Example

Consider the following dataset (N = 844 fish, bin width = 1 cm):

# Example length-frequency data
example_lf <- data.frame(
  Length    = 6:18,
  Frequency = c(2, 3, 22, 54, 110, 131, 158, 141, 90, 63, 45, 23, 2)
)
N_total <- sum(example_lf$Frequency)
cat("N =", N_total, "fish; length range:", min(example_lf$Length),
    "–", max(example_lf$Length), "cm\n")
#> N = 844 fish; length range: 6 – 18 cm
print(example_lf)
#>    Length Frequency
#> 1       6         2
#> 2       7         3
#> 3       8        22
#> 4       9        54
#> 5      10       110
#> 6      11       131
#> 7      12       158
#> 8      13       141
#> 9      14        90
#> 10     15        63
#> 11     16        45
#> 12     17        23
#> 13     18         2

11.2.1 Manual Calculation (Fixed Parameters)

# Step 1: derive fixed point-estimate parameters
Lmax_obs <- max(example_lf$Length)       # 18 cm
Linf_pt  <- Lmax_obs / 0.95             # 18.947 cm
Lmat_pt  <- 10^(0.8979 * log10(Linf_pt) - 0.0782)   # 11.71 cm
Lopt_pt  <- 10^(1.053  * log10(Lmat_pt) - 0.0565)   # 11.72 cm
Lopt_p10_pt <- Lopt_pt * 1.10                         # 12.89 cm
Lopt_m10_pt <- Lopt_pt * 0.90                         # 10.55 cm

cat(sprintf(
  "Fixed parameters:\n  Lmax = %.2f, Linf = %.3f\n  Lmat = %.2f, Lopt = %.2f\n  Lopt_m10 = %.2f, Lopt_p10 = %.2f\n",
  Lmax_obs, Linf_pt, Lmat_pt, Lopt_pt, Lopt_m10_pt, Lopt_p10_pt
))
#> Fixed parameters:
#>   Lmax = 18.00, Linf = 18.947
#>   Lmat = 11.72, Lopt = 11.72
#>   Lopt_m10 = 10.55, Lopt_p10 = 12.90

# Step 2: classify each length class
example_lf$Mature  <- example_lf$Length >= Lmat_pt
example_lf$Optimal <- example_lf$Length >= Lopt_m10_pt &
                      example_lf$Length <= Lopt_p10_pt
example_lf$Mega    <- example_lf$Length >  Lopt_p10_pt

# Step 3: compute manual Froese indicators
Pmat_manual  <- 100 * sum(example_lf$Frequency[example_lf$Mature])  / N_total
Popt_manual  <- 100 * sum(example_lf$Frequency[example_lf$Optimal]) / N_total
Pmega_manual <- 100 * sum(example_lf$Frequency[example_lf$Mega])    / N_total

cat(sprintf(
  "\nManual Froese Indicators:\n  Pmat  = %.2f%%\n  Popt  = %.2f%%\n  Pmega = %.2f%%\n  (No CIs possible)\n",
  Pmat_manual, Popt_manual, Pmega_manual
))
#> 
#> Manual Froese Indicators:
#>   Pmat  = 61.85%
#>   Popt  = 34.24%
#>   Pmega = 43.13%
#>   (No CIs possible)

11.2.2 Why the Step-Function Matters

# Demonstrate the step-function effect of Lmat on Pmat
Lmat_values <- seq(9, 17, by = 0.1)

compute_Pmat <- function(Lmat, lf) {
  100 * sum(lf$Frequency[lf$Length >= Lmat]) / sum(lf$Frequency)
}

Pmat_curve <- sapply(Lmat_values, compute_Pmat, lf = example_lf)

cat("How Pmat changes as Lmat varies (step-function):\n")
#> How Pmat changes as Lmat varies (step-function):
df_steps <- data.frame(
  Lmat_range  = c("≤ 10", "(10,11]", "(11,12]", "(12,13]", "(13,14]", "> 14"),
  Pmat        = c(
    compute_Pmat(10.0, example_lf),
    compute_Pmat(10.5, example_lf),
    compute_Pmat(11.5, example_lf),  # ← Manual falls here
    compute_Pmat(12.5, example_lf),
    compute_Pmat(13.5, example_lf),
    compute_Pmat(14.5, example_lf)
  ),
  Notes = c("", "", "← Manual (Lmat=11.71)", "", "", "")
)
print(df_steps)
#>   Lmat_range     Pmat                 Notes
#> 1       ≤ 10 90.40284                      
#> 2    (10,11] 77.36967                      
#> 3    (11,12] 61.84834 ← Manual (Lmat=11.71)
#> 4    (12,13] 43.12796                      
#> 5    (13,14] 26.42180                      
#> 6       > 14 15.75829

# The jump at the 12-cm class boundary
jump_12cm <- 100 * example_lf$Frequency[example_lf$Length == 12] / N_total
cat(sprintf(
  "\nJump at Lmat = 12.0 cm: %.1f pp (= 158 fish / 844 total)\n",
  jump_12cm
))
#> 
#> Jump at Lmat = 12.0 cm: 18.7 pp (= 158 fish / 844 total)
cat("A 0.2 cm shift in Lmat (11.9 → 12.1) changes Pmat by", jump_12cm, "pp\n")
#> A 0.2 cm shift in Lmat (11.9 → 12.1) changes Pmat by 18.72038 pp

11.2.3 Comparison: Manual vs. Model (Tier 3)

# Model Tier 3 outputs (from FishPar with this dataset)
comparison <- data.frame(
  Indicator = c("Pmat", "Popt", "Pmega"),
  Manual    = c(61.85, 34.24, 43.13),
  Model_T2  = c(61.85, 34.24, 43.13),  # Tier 2 ≈ manual
  Model_T3_Mean = c(56.47, 38.38, 36.40),
  T3_Lower95  = c(28.78, 26.78, 14.22),
  T3_Upper95  = c(77.13, 52.96, 62.68)
)
print(comparison)
#>   Indicator Manual Model_T2 Model_T3_Mean T3_Lower95 T3_Upper95
#> 1      Pmat  61.85    61.85         56.47      28.78      77.13
#> 2      Popt  34.24    34.24         38.38      26.78      52.96
#> 3     Pmega  43.13    43.13         36.40      14.22      62.68

cat("\nKey insight: Tier 2 ≈ Manual because both use the same fixed Lmat/Lopt.\n")
#> 
#> Key insight: Tier 2 ≈ Manual because both use the same fixed Lmat/Lopt.
cat("Tier 3 differs because it integrates over the MC distribution of Lmat,\n")
#> Tier 3 differs because it integrates over the MC distribution of Lmat,
cat("propagating the 2.64 cm uncertainty range [10.45, 13.09] through the\n")
#> propagating the 2.64 cm uncertainty range [10.45, 13.09] through the
cat("step function, which shifts the expected Pmat from 61.85% to 56.47%.\n")
#> step function, which shifts the expected Pmat from 61.85% to 56.47%.

11.3 Why Manual Calculation Cannot Produce Confidence Intervals

The manual approach has four fundamental limitations:

  1. Fixed parameters: L_mat = 11.71 cm is treated as the exact true value, ignoring that it is an estimate from a cross-species allometric regression.
  2. No resampling framework: Without MC simulation or bootstrapping, there is no distributional information from which a CI can be constructed.
  3. Step-function incompatibility: Even applying the delta method is impossible here — the step function is not differentiable at class boundaries, violating the smooth- function assumption that the delta method requires.
  4. Two interacting uncertainty sources: Parameter estimation uncertainty and catch-data sampling uncertainty cannot be disentangled analytically; only numerical simulation (the three-tier framework) can quantify both simultaneously.

Summary: The 5.4 pp difference between manual (61.85%) and model (56.47%) P_mat is not a computational error. It is the correct, quantified effect of propagating allometric regression uncertainty through the step-function classification. The three-tier framework in FishPar is the only approach capable of providing honest, defensible confidence intervals for length-based sustainability indicators in data-limited fisheries.


12 Reproducibility and Best Practices

12.1 Setting a Random Seed

All MC and bootstrap simulations in FishPar use R’s default pseudorandom number generator. For exactly reproducible results, set a seed before calling FishPar:

set.seed(123)
results <- FishPar(data = lf_data, resample = 1000)

12.2 Choosing the Number of Iterations

The default resample = 1000 is suitable for exploratory analysis and vignette building. For manuscript submission:

Purpose Recommended resample
Exploration and diagnostics 1 000
Preliminary results 2 000
Manuscript submission 5 000
High-precision intervals 10 000

12.3 Providing Species-Specific Parameters

When species-specific L_inf or L_mat values are available from FishBase, tagging studies, or otolith ageing, they should be provided via the Linf and Lmat arguments to override the allometric regression estimates:

# Species: Glossogobius giuris; Linf from FishBase = 21.0 cm
results_specific <- FishPar(
  data        = lf_data,
  resample    = 5000,
  Linf        = 21.0,   # FishBase value
  Linf_sd     = 1.0,    # uncertainty in the FishBase estimate
  save_output = TRUE
)

When Linf is supplied, the function adds Gaussian noise with SD = Linf_sd at each MC iteration, so uncertainty in the literature estimate is still propagated.

12.4 LM_ratio and the Cope & Punt Gap

The Cope & Punt (2009) framework accepts only two LM_ratio categories: ≤ 0.75 or ≥ 0.90. If the estimated LM_ratio falls between 0.75 and 0.90, FishSS returns a warning and NULL. In this case the user should:

  1. Check whether a species-specific L_mat from the literature would place LM_ratio clearly in one category.
  2. Run sensitivity analyses with LM_ratio set to both 0.75 and 0.90 to bracket the stock status assessment.

13 Conclusion

The aLBI package provides a rigorous, reproducible framework for data-limited fish stock assessment from catch length-frequency data alone. Its key contributions are:


14 Acknowledgements

The author expresses sincere gratitude to Dr. Mohammed Shahidul Alam for his unwavering guidance, expert supervision, and continuous encouragement throughout the development of this package. Heartfelt thanks are also extended to the reviewers, collaborators, and open-source R community members whose feedback has substantially improved both the methodology and the implementation.


15 References

Ali, A., Sarker, M. R., & Alam, M. S. (2025). Development of a simple R package (aLBI) for the estimation of stock status from the length frequency data. Fisheries Research, 288, 107467. https://doi.org/10.1016/j.fishres.2025.107467

Cope, J. M., & Punt, A. E. (2009). Length-based reference points for data-limited situations: Applications and restrictions. Marine and Coastal Fisheries, 1(1), 169–186. https://doi.org/10.1577/C08-025.1

Efron, B., & Tibshirani, R. J. (1993). An Introduction to the Bootstrap. Chapman & Hall, New York.

Froese, R. (2004). Keep it simple: Three indicators to deal with overfishing. Fish and Fisheries, 5(1), 86–91. https://doi.org/10.1111/j.1467-2979.2004.00144.x

Froese, R., & Binohlan, C. (2000). Empirical relationships to estimate asymptotic length, length at first maturity and length at maximum yield per recruit in fishes. Journal of Fish Biology, 56, 758–773. https://doi.org/10.1111/j.1095-8649.2000.tb00870.x

Froese, R., & Pauly, D. (Eds.) (2020). FishBase. World Wide Web electronic publication. https://www.fishbase.org

Silverman, B. W. (1986). Density Estimation for Statistics and Data Analysis. Chapman & Hall, London.

Wang, K., Zhang, C., Xu, B., Xue, Y., & Ren, Y. (2020). Selecting optimal bin size to account for growth variability in Electronic LEngth Frequency ANalysis (ELEFAN). Fisheries Research, 225, 105474. https://doi.org/10.1016/j.fishres.2019.105474


16 Contact

Author Ataher Ali
E-mail
Package github.com/Ataher76/aLBI
CRAN cran.r-project.org/package=aLBI

Please report bugs and feature requests via the GitHub Issues page.