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.
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 |
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
# Install from CRAN (stable release)
install.packages("aLBI")
# Install the latest development version from GitHub
# install.packages("devtools") # required
devtools::install_github("Ataher76/aLBI")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))
}
}| 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
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 measurementsBefore 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.
| 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 |
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 cmFrequencyTable returns a list with two elements:
$lfqTable: The complete frequency
distribution table with class intervals (lower bound, upper bound),
midpoint, frequency, relative frequency (%), and cumulative
frequency.$lfreq: A condensed two-column data
frame (Length, Frequency) suitable for direct
use in FishPar.Tip: The
$lfreqcomponent is the recommended input format forFishPar. Save it as an Excel file using theoutput_fileargument for reproducibility.
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.
| 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 |
# 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 9The 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.
FishPar is the core function of the aLBI
package. It implements a rigorous two-stage simulation pipeline:
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.
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}]\]
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.
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.
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.
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 |
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.
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:
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.
| 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.
| 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 |
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
)# ── 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$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
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 |
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:
The contrast between Tier 2 (tight) and Tier 3 (wide) visually communicates the dominant role of allometric regression uncertainty in total indicator uncertainty.
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_Inputssheet is designed to streamline the workflow: all values required byFishSS()can be read directly from this sheet without manual extraction.
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:
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.
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 |
| 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) |
# 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_statusstock_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 patternThe 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.
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.
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:
| 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() |
# 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 objectThe LWR plot is annotated with:
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)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.
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# 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)# 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# 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%.The manual approach has four fundamental limitations:
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
FishParis the only approach capable of providing honest, defensible confidence intervals for length-based sustainability indicators in data-limited fisheries.
All MC and bootstrap simulations in FishPar use R’s
default pseudorandom number generator. For exactly reproducible results,
set a seed before calling FishPar:
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 |
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.
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:
The aLBI package provides a rigorous, reproducible
framework for data-limited fish stock assessment from catch
length-frequency data alone. Its key contributions are:
FishSS_Inputs Excel sheet eliminates manual value transfer
between FishPar and FishSS.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.
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
| Author | Ataher Ali |
| ataher.cu.ms@gmail.com | |
| Package | github.com/Ataher76/aLBI |
| CRAN | cran.r-project.org/package=aLBI |
Please report bugs and feature requests via the GitHub Issues page.