Fitting length data and related species-specific body-mass relationships – MLEbins method

Andrew Edwards

Last rendered on 02 June, 2026

This vignette for the sizeSpectraFit R package is for the MLEbins method, which we introduced in our MEPS paper. We use an example data set from the Mediterranean from our new Global Ecology and Conservation paper.

The MLEbins method calculates the maximum likelihood estimate of the size-spectrum exponent, \(b\), when the data are collected as lengths and we have species-specific length-weight coefficients. So individual body masses are not available; if they were we could use the MLE method in the fit-data.html vignette. If you have binned body masses but the bins overlap, then you also need to use MLEbins.

The lengths may already be binned, for example a length bin could be 10-15 cm. And even when measured more accurately, the resolution of the measurements essentially adds uncertainty to a length. For example, a fish of length 15 cm measured to the nearest cm has a true length in the range 14.5-15.5 cm, which then gets converted into a body-mass bin using that species specific length-weight coefficients. The resulting body-mass bins can overlap each other (unlike for the MLEbin method), because the length bins of differently shaped species get converted to different body-mass bins. See the MEPS paper paper for further explanation (rather than it being repeated here, where we focus on applying the method).

The raw data

We use real data of measured lengths from experimental trawl surveys conducted in the Northwestern Mediterranean Sea, as part of the research projects RESNEP and BITER by the Instituto de Ciencias del Mar (ICM-CSIC), Spain; data courtesy of Juliana Quevedo, Joan B. Company, Nixon Bahamon, and Jordi Ribera Altimir, as used in Quevedo et al. (2026).

The full raw data are available in sizeSpectraFit as

mediterranean_data
  # A tibble: 17,850 × 5
     strata   group          species                   length number
     <fct>    <fct>          <fct>                      <dbl>  <dbl>
   1 baseline Actinopterygii Phycis blennoides          124    30.3 
   2 baseline Crustacea      Nephrops norvegicus         31.0  15.2 
   3 baseline Crustacea      Nephrops norvegicus         30.4   7.58
   4 baseline Actinopterygii Phycis blennoides          122    30.3 
   5 baseline Actinopterygii Helicolenus dactylopterus  140     7.58
   6 baseline Crustacea      Nephrops norvegicus         31.0  15.2 
   7 baseline Actinopterygii Phycis blennoides          128    30.3 
   8 baseline Crustacea      Nephrops norvegicus         38.1   7.58
   9 baseline Actinopterygii Phycis blennoides          148    30.3 
  10 baseline Actinopterygii Phycis blennoides          127    30.3 
  # ℹ 17,840 more rows

for which each row has strata (baseline, ntr: the no-take reserve four years after its implementation, or fg:the fishing grounds four years after the reserve’s implementation), species group given as group, species, number of individuals of that species that were measured as length length (mm). Values of number can be non-integer because they are standardised by the area trawled in each tow; the MLEbins method can explicitly deal with this.

A summary of the data is

summary(mediterranean_data)
        strata                group                          species    
   fg      :6270   Actinopterygii:7228   Parapenaeus longirostris:2986  
   ntr     :7550   Cephalopoda   : 607   Nephrops norvegicus     :2550  
   baseline:4030   Chondrichthyes:1310   Plesionika heterocarpus :2023  
                   Crustacea     :8705   Phycis blennoides       :1930  
                                         Scyliorhinus canicula   :1307  
                                         Trigla lyra             : 806  
                                         (Other)                 :6248  
       length            number       
   Min.   :  12.10   Min.   :  6.006  
   1st Qu.:  27.05   1st Qu.:  7.353  
   Median :  40.00   Median : 14.706  
   Mean   : 102.20   Mean   : 24.301  
   3rd Qu.: 164.00   3rd Qu.: 29.594  
   Max.   :1000.00   Max.   :170.374  
  

We also have the species-specific length-weight coefficients \(\alpha\) and \(\beta\) for each species:

mediterranean_length_weight_coefficients
  # A tibble: 53 × 3
     species                      alpha  beta
     <fct>                        <dbl> <dbl>
   1 Abralia veranyi             0.689   1.42
   2 Aegaeon lacazei             0.0275  2.44
   3 Argentina sphyraena         0.0047  3.05
   4 Arnoglossus rueppelii       0.0051  3.01
   5 Bathypolypus sponsalis      0.562   2.56
   6 Capros aper                 0.0138  3.11
   7 Chlorotocus crassicornis    0.585   2.43
   8 Conger conger               0.0006  3.21
   9 Eusergestes arcticus        0.287   2.31
  10 Gaidropsarus macrophthalmus 0.0075  2.85
  # ℹ 43 more rows

where the coefficients are for the equation \(w = \alpha_s l^{\beta_s}\) relating weights (g) to lengths (mm) for each species \(s\).

The above shows what kind of data are needed to implement the MLEbins method.

Wrangling the data

We first wrangle the data into the format required for using the MLEbins method. Similar wrangling would need to be done for users’ data to get the required inputs for the likelihood calculations.

For demonstration purposes we will fit the size spectrum for just one species group (Cephalopoda) and for one strata, the fishing grounds (fg). So we first filter to obtain just those values:

dat <- dplyr::filter(mediterranean_data,
                     strata == "fg",
                     group == "Cephalopoda")
dat
  # A tibble: 157 × 5
     strata group       species              length number
     <fct>  <fct>       <fct>                 <dbl>  <dbl>
   1 fg     Cephalopoda Abralia veranyi          42  58.8 
   2 fg     Cephalopoda Todarodes sagittatus    294   7.35
   3 fg     Cephalopoda Todarodes sagittatus    185   7.35
   4 fg     Cephalopoda Todarodes sagittatus    269   7.35
   5 fg     Cephalopoda Todaropsis eblanae      116   7.35
   6 fg     Cephalopoda Todaropsis eblanae      126   7.35
   7 fg     Cephalopoda Todaropsis eblanae       87   7.35
   8 fg     Cephalopoda Todaropsis eblanae      130   7.35
   9 fg     Cephalopoda Todaropsis eblanae       94   7.35
  10 fg     Cephalopoda Todaropsis eblanae      120   7.35
  # ℹ 147 more rows
summary(dat)
        strata               group                     species       length      
   fg      :157   Actinopterygii:  0   Illex coindetii     :53   Min.   : 17.00  
   ntr     :  0   Cephalopoda   :157   Todaropsis eblanae  :38   1st Qu.: 46.00  
   baseline:  0   Chondrichthyes:  0   Abralia veranyi     :37   Median : 95.00  
                  Crustacea     :  0   Rossia macrosoma    :11   Mean   : 90.62  
                                       Todarodes sagittatus: 9   3rd Qu.:115.00  
                                       Sepia orbignyana    : 7   Max.   :335.00  
                                       (Other)             : 2                   
       number      
   Min.   : 7.151  
   1st Qu.: 7.204  
   Median : 7.353  
   Mean   :12.505  
   3rd Qu.: 7.435  
   Max.   :61.345  
  

Thus, dat contains data for just one strata and one group, but multiple species.

The lengths are in mm, with integer values measured to nearest mm using standard lab metric tape. We convert these length values to minima and maxima of bins, with the measured lengths representing the midpoints. This keeps track of the uncertainty in the ‘true’ length of individuals and is needed for the MLEbins method, which also allows for other species groups having different resolution.

This conversion is done using calc_bin_breaks(), which takes a tibble that contains a column of measurements and appends minimum and maximum values for each bin (see ?calc_bin_breaks):

dat_with_breaks <- calc_bin_breaks(dat,
                                   bin_width = 1) %>%
  dplyr::rename(bin_count = number)
dat_with_breaks
  # A tibble: 157 × 7
     strata group       species         length bin_count length_bin_min length_bin_max
     <fct>  <fct>       <fct>            <dbl>     <dbl>          <dbl>          <dbl>
   1 fg     Cephalopoda Abralia veranyi     42     58.8            41.5           42.5
   2 fg     Cephalopoda Todarodes sagi…    294      7.35          294.           294. 
   3 fg     Cephalopoda Todarodes sagi…    185      7.35          184.           186. 
   4 fg     Cephalopoda Todarodes sagi…    269      7.35          268.           270. 
   5 fg     Cephalopoda Todaropsis ebl…    116      7.35          116.           116. 
   6 fg     Cephalopoda Todaropsis ebl…    126      7.35          126.           126. 
   7 fg     Cephalopoda Todaropsis ebl…     87      7.35           86.5           87.5
   8 fg     Cephalopoda Todaropsis ebl…    130      7.35          130.           130. 
   9 fg     Cephalopoda Todaropsis ebl…     94      7.35           93.5           94.5
  10 fg     Cephalopoda Todaropsis ebl…    120      7.35          120.           120. 
  # ℹ 147 more rows

We then convert data of counts of species in length bins to counts in body-mass bins calculated using the species-specific length-weight coefficients. This is done using length_bins_to_body_mass_bins()

dat_joined <-
  length_bins_to_body_mass_bins(dat_with_breaks,
                                mediterranean_length_weight_coefficients,
                                length_data_unit = "mm")
dat_joined
  # A tibble: 157 × 11
     strata group  species length bin_count length_bin_min length_bin_max  alpha  beta
     <fct>  <fct>  <fct>    <dbl>     <dbl>          <dbl>          <dbl>  <dbl> <dbl>
   1 fg     Cepha… Abrali…     42     58.8            4.15           4.25 0.689   1.42
   2 fg     Cepha… Todaro…    294      7.35          29.4           29.4  0.0102  3.31
   3 fg     Cepha… Todaro…    185      7.35          18.4           18.6  0.0102  3.31
   4 fg     Cepha… Todaro…    269      7.35          26.8           27.0  0.0102  3.31
   5 fg     Cepha… Todaro…    116      7.35          11.6           11.6  0.0996  2.79
   6 fg     Cepha… Todaro…    126      7.35          12.6           12.6  0.0996  2.79
   7 fg     Cepha… Todaro…     87      7.35           8.65           8.75 0.0996  2.79
   8 fg     Cepha… Todaro…    130      7.35          13.0           13.0  0.0996  2.79
   9 fg     Cepha… Todaro…     94      7.35           9.35           9.45 0.0996  2.79
  10 fg     Cepha… Todaro…    120      7.35          12.0           12.0  0.0996  2.79
  # ℹ 147 more rows
  # ℹ 2 more variables: weight_bin_min <dbl>, weight_bin_max <dbl>

We then retain just the columns needed to fit using the MLEbins method for input into determine_xmin_and_fit_mlebins() and also sum the counts for the same combinations of species and weight bins, which is done in mediterranean_for_mlebins(). That function should work for similarly structured data (requires group and strata, but the function can be easily adapted if needed). Since we repeated the same types of analysis for multiple species groups and strata combinations, it made sense to write a function. As an extra step here we also are going to look at only small cephalopods below 20.9 g; see page 8 of Appendix B of [Quevedo et al. (2026)] for details.

data_cephsmall_fg <- mediterranean_for_mlebins(dat_joined) %>%
  dplyr::filter(bin_min < 20.9) %>%
  dplyr::arrange(bin_min)
data_cephsmall_fg
  # A tibble: 29 × 4
     species         bin_min bin_max bin_count
     <fct>             <dbl>   <dbl>     <dbl>
   1 Abralia veranyi    2.46    2.60     61.3 
   2 Abralia veranyi    2.60    2.75     64.6 
   3 Abralia veranyi    2.89    3.05      7.35
   4 Illex coindetii    3.14    3.35     43.9 
   5 Abralia veranyi    3.35    3.51     14.8 
   6 Abralia veranyi    3.51    3.67     64.8 
   7 Abralia veranyi    3.67    3.83     79.4 
   8 Illex coindetii    3.79    4.03     43.2 
   9 Abralia veranyi    3.83    3.99     58.4 
  10 Abralia veranyi    3.99    4.16     29.4 
  # ℹ 19 more rows

Note the overlapping bins in rows 7 and 8, and also in rows 8 and 9, because the bins correspond to different species which have different length-weight coefficients. Hence the need for the MLEbins method.

We are now ready to fit the data. The determine_xmin_and_fit_mlebins() function calculates a histogram to find the peak from which to start the bounded power-law fit (Quevedo et al., 2026). It then fits the size spectrum using fit_size_spectrum_mlebins(); if you wish to specify x_min then you would just use fit_size_spectrum_mlebins(). This can take a few minutes, so I have saved res_cephsmall_fg as a data object in sizeSpectraFit (this next chunk is not actually run, don’t tell anyone…; so do make sure to take out eval = FALSE if you use this code as a template):

res_cephsmall_fg <- determine_xmin_and_fit_mlebins(data_cephsmall_fg)

The results are

res_cephsmall_fg
  $mlebins_fit
  $b_mle
  [1] -2.997532
  
  $b_conf
  [1] -3.162692 -2.836292
  
  $data
  # A tibble: 25 × 7
     species         bin_min bin_max bin_count count_gte_bin_min low_count high_count
     <fct>             <dbl>   <dbl>     <dbl>             <dbl>     <dbl>      <dbl>
   1 Illex coindetii    3.14    3.35     43.9               952.      908.       952.
   2 Abralia veranyi    3.35    3.51     14.8               908.      893.       908.
   3 Abralia veranyi    3.51    3.67     64.8               893.      829.       893.
   4 Abralia veranyi    3.67    3.83     79.4               829.      706.       829.
   5 Illex coindetii    3.79    4.03     43.2               749.      618.       829.
   6 Abralia veranyi    3.83    3.99     58.4               706.      648.       749.
   7 Abralia veranyi    3.99    4.16     29.4               648.      618.       691.
   8 Abralia veranyi    4.16    4.33     75.9               618.      542.       618.
   9 Abralia veranyi    4.33    4.49      7.44              542.      535.       542.
  10 Abralia veranyi    4.49    4.67     14.7               535.      520.       535.
  # ℹ 15 more rows
  
  $x_min
  [1] 3.139746
  
  $x_max
  [1] 18.04885
  
  $n
  [1] 951.977
  
  $method
  [1] "MLEbins"
  
  attr(,"class")
  [1] "size_spectrum_mlebins" "list"                 
  
  $h
  $breaks
   [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18
  
  $mids
   [1]  0.5  1.5  2.5  3.5  4.5  5.5  6.5  7.5  8.5  9.5 10.5 11.5 12.5 13.5 14.5 15.5
  [17] 16.5 17.5
  
  $counts
   [1]   0.000   0.000 133.257 333.841 289.859 175.430  58.278   0.000  65.590   7.312
  [11]   0.000   0.000   0.000   0.000   7.204   0.000   7.312   7.151
  
  $xname
  [1] "Total counts in each bin"
  
  $equidist
  [1] TRUE
  
  attr(,"class")
  [1] "histogram"
  
  attr(,"class")
  [1] "determine_xmin_and_fit_mlebins" "list"

This is a list of two objects (each is a list): $mlebins_fit contains the details of the maximum likelihood fitting similar to the other methods, and \(h\) is an object of class histogram that contains details of the histogram used to determine \(x_{min}\). (Use remove_hist(res_cephsmall_fg) to remove the histogram object and retain the class; this also works on lists of results from multiple applications of determine_xmin_and_fit_mlebins()).

The object has the class determine_xmin_and_fit_mlebins such that our customised plot.determine_xmin_and_fit_mlebins() automatically gets used:

plot(res_cephsmall_fg)

The top panel is a histogram of total counts of minimum body sizes, using 1-g body-size bins, used to determine \(x_{min}\). The left-most red bin is the modal bin, and \(x_{x_min}\) for this group-strata combination is the minimum value within that 1-g bin.

Middle and bottom panels (as suggested in Figure 7 of [2]) are the fit of the bounded power-law distribution using the MLEbins method with linear and logarithmic y-axes, respectively. For each body-mass bin, the green horizontal line show the range of body sizes, with its value on the y-axis corresponding to the total number of individuals in bins whose minima are \(\geq\) the bin’s minimum; the green lines help to distinguish each bin when bins are overlapping. The vertical span of each grey rectangle shows the possible range of number of individuals with body masses \(\geq\) body mass of individuals in that bin (horizontal span matches the green line). Red curves are fits of the MLE, with dashed lines showing 95% confidence intervals.

These figure are admittedly a little complex, but retain the full uncertainty arising from binned data and conversions through species-specific length-weight relationships. As can be seen, some of the resulting rectangles are quite large, and simply using a single point for each would suggest higher accuracy than is warranted.

Do see the help files for relevant functions, and feel free to email me (or make an issue) with any questions.

Session information

sessionInfo()
  R version 4.5.1 (2025-06-13 ucrt)
  Platform: x86_64-w64-mingw32/x64
  Running under: Windows 11 x64 (build 22631)
  
  Matrix products: default
    LAPACK version 3.12.1
  
  locale:
  [1] LC_COLLATE=English_United States.utf8  LC_CTYPE=English_United States.utf8   
  [3] LC_MONETARY=English_United States.utf8 LC_NUMERIC=C                          
  [5] LC_TIME=English_United States.utf8    
  
  time zone: America/Vancouver
  tzcode source: internal
  
  attached base packages:
  [1] stats     graphics  grDevices utils     datasets  methods   base     
  
  other attached packages:
  [1] sizeSpectraFit_0.0.99 pacea_1.0.0           testthat_3.2.3       
  [4] devtools_2.4.5        usethis_3.2.1        
  
  loaded via a namespace (and not attached):
   [1] gtable_0.3.6       xfun_0.55          bslib_0.9.0        ggplot2_4.0.1     
   [5] htmlwidgets_1.6.4  remotes_2.5.0      vctrs_0.6.5        tools_4.5.1       
   [9] generics_0.1.4     proxy_0.4-27       tibble_3.3.0       pkgconfig_2.0.3   
  [13] KernSmooth_2.23-26 RColorBrewer_1.1-3 pals_1.10          S7_0.2.1          
  [17] desc_1.4.3         lifecycle_1.0.4    compiler_4.5.1     farver_2.1.2      
  [21] brio_1.1.5         mapproj_1.2.12     codetools_0.2-20   httpuv_1.6.16     
  [25] class_7.3-23       maps_3.4.3         htmltools_0.5.9    sass_0.4.10       
  [29] yaml_2.3.12        later_1.4.4        pillar_1.11.1      jquerylib_0.1.4   
  [33] urlchecker_1.0.1   tidyr_1.3.2        ellipsis_0.3.2     classInt_0.4-11   
  [37] cachem_1.1.0       sessioninfo_1.2.3  iterators_1.0.14   foreach_1.5.2     
  [41] mime_0.13          tidyselect_1.2.1   digest_0.6.39      sf_1.0-21         
  [45] dplyr_1.1.4        purrr_1.2.0        rprojroot_2.1.1    fastmap_1.2.0     
  [49] grid_4.5.1         colorspace_2.1-2   cli_3.6.5          magrittr_2.0.4    
  [53] utf8_1.2.6         dichromat_2.0-0.1  pkgbuild_1.4.8     e1071_1.7-16      
  [57] withr_3.0.2        waldo_0.6.2        scales_1.4.0       promises_1.5.0    
  [61] timechange_0.3.0   lubridate_1.9.4    rmarkdown_2.30     httr_1.4.7        
  [65] otel_0.2.0         memoise_2.0.1      shiny_1.12.1       evaluate_1.0.5    
  [69] knitr_1.51         miniUI_0.1.2       profvis_0.4.0      rlang_1.1.6       
  [73] Rcpp_1.1.0         DBI_1.2.3          xtable_1.8-4       glue_1.8.0        
  [77] pkgload_1.4.1      rstudioapi_0.17.1  jsonlite_2.0.0     R6_2.6.1          
  [81] units_0.8-7        fs_1.6.6