Fit data and plot results - MLE and MLEbin methods

Andrew Edwards

Last rendered on 02 June, 2026

Fitting a size spectrum to a data set using likelihood is done using the function fit_size_spectrum(). The function automatically selects the method to use based on the class of the data, as described by Table 2 in our MEPS paper.

If you have data that are not simply declining (when plotted as a histogram), which is assumed for a size spectrum fit, you can have the \(x_{min}\) value determined based on the data.

Measurements of individual body sizes – MLE method

The simplest data consists of just a vector of measurements, such as body masses or body lengths. We fit the size spectrum by calculating the maximum likelihood estimate (the MLE method) of the size-spectrum exponent, along with its 95% confidence intervals.

As an example we use the simulated data from a bounded power-law distribution as used for Figures 1 and 2 of our MEE paper. The values are included in sizeSpectraFit as the data object sim_vec (which stands for simulated vector), and we assume they are individual body masses measured in grammes. Note that the fit will be great because the data are sampled from the expected power-law distribution.

First, verify that there are 1000 values and print the first 20 values to take a look:

length(sim_vec)
  [1] 1000
sim_vec[1:20]
   [1] 11.613220 15.658847  1.400273  5.869136  2.786321  2.077175  3.785753  1.155444
   [9]  2.909813  3.382489  1.842584  3.551050 15.091509  1.342594  1.858151 16.413491
  [17] 43.952570  1.132977  1.903030  2.271552

The main fitting function is fit_size_spectrum(), which will automatically use the MLE method here (via fit_size_spectrum.numeric()) because the data are a numeric vector of individual values:

res <- fit_size_spectrum(sim_vec)
res
  $b_mle
  [1] -2.029697
  
  $b_conf
  [1] -2.097417 -1.964317
  
  $x
   [1] 11.613220 15.658847  1.400273  5.869136  2.786321  2.077175  3.785753  1.155444
   [9]  2.909813  3.382489
  
  $x_min
  [1] 1.000239
  
  $x_max
  [1] 398.7787
  
  $n
  [1] 1000
  
  $method
  [1] "MLE"

The resulting res is a list that contains:

The list res also has class size_spectrum_numeric which helps with our plotting and printing functions.

To plot the PLB fit and the original data as an Individual Size Distribution (ISD):

plot(res)

as recommended in Figure 6b of our MEE paper. Each individual body mass is shown as a point (note the log scales), the solid red line is the maximum likelihood estimate (MLE) of the size-spectrum exponent \(b\), and the dashed lines are the fits from using the low and high values of the 95% confidence interval of \(b\). The MLE for \(b\) and the sample size \(n\) are also explicitly given. The default x-axis label assumes the body masses are in g, but this can easily be changed.

The plotting is very customisable as described in the help function. Since the res object has:

class(res)
  [1] "size_spectrum_numeric" "list"

the plot(res) command uses our custom function plot.size_spectrum_numeric(); this is an R feature. Thus, the help function needed is ?plot.size_spectrum_numeric().

The style argument gives other styles of plotting, for example:

plot(res,
     style = "linear_y_axis")

gives the same plot but with a linear y-axis.

To show both the above plots together, with panels automatically labelled as (a) and (b):

plot(res,
     style = "both_y_axes")

which is similar to Figure 7 of our MEPS paper but for nonbinned data.

The fourth option is only suitable when the data represent body masses. It shows the normalised biomass:

plot(res,
     style = "biomass")

The black horizontal lines indicate the normalised biomass within each bin on the y-axis, and the span on the x-axis shows the range of the bin. We use bars, rather than the usual points (e.g. Figure 6(f) of our MEE paper) to emphasise that the actual values of body size in each bin span a range; the points somewhat imply that the midpoint of the bin is more relevant.

Successive bins double in size, such that they appear of equal width on the log scale. The normalised biomass within each one is calculated as the sum of the biomass in the bin divided by the bin width; there is no uncertainty in this calculation here because we know the individual body sizes (this is not so if the data are already binned, see later). The horizontal red lines show the estimated normalised biomass in each bin based on the MLE of the exponent \(b\), and the pink boxes span the normalised biomass estimated for all values within the 95% confidence interval of \(b\). The straight lines are also shown for the MLE of \(b\) (solid line) and the 95% confidence interval estimates (dashed lines). Note that the straight lines fit the PLB distribution, whose definition includes the \(x_{min}\) and \(x_{max}\) bounds (see MEE paper equation 1) so the lines are bounded, but the top bin here goes beyond \(x_{max}\), even though there are no actual body masses there (the bin happens to extend out further).

The fit here is clearly very good, but remember this is expected as the data are simulated from a known PLB distribution and are not noisy, unlike real data.

To show the biomass plot and the ISD plot, which is an improved version of the suggested plot in Figure 6 of our MEE paper:

plot(res,
     style = "biomass_and_isd")

Determine \(x_{min}\) when the data are not simply declining

A simple histogram of your data may not look like a declining function; in fisheries examples this can be due to selectivity, with the fishing gear not catching all the smaller organisms. If you have a similar type of justification for this happening and still want to fit a declining PLB distribution to a vector of values, we have the built in function determine_xmin_and_fit() to determine a value of \(x_{min}\) to use and then fit using the MLE method. This is the nonbinned version of the approach described in Quevedo et al. (2026).

Let’s make up a mock dataset, with some uniformly distributed values between 0.25 and 10, and then a PLB distribution for values \(\geq 8\):

set.seed(42)
mock_data <- c(runif(100, 0.25, 2),
               rPLB(1000, -2, x_min = 2, x_max = 100))
hist(mock_data, breaks = 40)

So even a rudimentary histogram shows a unimodal distribution. To fit this:

res_mock <- determine_xmin_and_fit(mock_data)

We can plot and then print the results:

plot(res_mock)

The red bars in the histogram show the values that are used to fit the PLB distribution, with \(x_{min}\) being the lowest value in the lowest red bin.

res_mock
  $mle_fit
  $b_mle
  [1] -2.056786
  
  $b_conf
  [1] -2.135446 -1.980196
  
  $x
   [1]  5.177597  2.540697  2.538831  3.231885 26.180265 35.308150  7.274266  7.106840
   [9]  4.210935  2.004465
  
  $x_min
  [1] 2.000794
  
  $x_max
  [1] 97.04711
  
  $n
  [1] 1000
  
  $method
  [1] "MLE"
  
  
  $h
  $breaks
   [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
  [27] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
  [53] 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
  [79] 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
  
  $counts
   [1]  38  62 366 172 105  54  43  31  26  25  19  12  12   7   7   8   7   9   7   9
  [21]   8   6   4   4   4   3   3   6   2   4   1   2   1   0   2   5   1   2   1   0
  [41]   1   1   0   0   0   0   0   0   1   3   3   0   0   2   1   0   0   0   0   0
  [61]   1   0   1   0   0   0   0   0   1   0   0   0   1   0   2   0   0   0   0   0
  [81]   0   0   0   1   0   1   0   0   0   0   0   0   0   1   0   0   0   1
  
  $density
   [1] 0.0345454545 0.0563636364 0.3327272727 0.1563636364 0.0954545455 0.0490909091
   [7] 0.0390909091 0.0281818182 0.0236363636 0.0227272727 0.0172727273 0.0109090909
  [13] 0.0109090909 0.0063636364 0.0063636364 0.0072727273 0.0063636364 0.0081818182
  [19] 0.0063636364 0.0081818182 0.0072727273 0.0054545455 0.0036363636 0.0036363636
  [25] 0.0036363636 0.0027272727 0.0027272727 0.0054545455 0.0018181818 0.0036363636
  [31] 0.0009090909 0.0018181818 0.0009090909 0.0000000000 0.0018181818 0.0045454545
  [37] 0.0009090909 0.0018181818 0.0009090909 0.0000000000 0.0009090909 0.0009090909
  [43] 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000
  [49] 0.0009090909 0.0027272727 0.0027272727 0.0000000000 0.0000000000 0.0018181818
  [55] 0.0009090909 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000
  [61] 0.0009090909 0.0000000000 0.0009090909 0.0000000000 0.0000000000 0.0000000000
  [67] 0.0000000000 0.0000000000 0.0009090909 0.0000000000 0.0000000000 0.0000000000
  [73] 0.0009090909 0.0000000000 0.0018181818 0.0000000000 0.0000000000 0.0000000000
  [79] 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0009090909
  [85] 0.0000000000 0.0009090909 0.0000000000 0.0000000000 0.0000000000 0.0000000000
  [91] 0.0000000000 0.0000000000 0.0000000000 0.0009090909 0.0000000000 0.0000000000
  [97] 0.0000000000 0.0009090909
  
  $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 18.5 19.5 20.5 21.5 22.5 23.5 24.5 25.5 26.5 27.5 28.5 29.5 30.5 31.5
  [33] 32.5 33.5 34.5 35.5 36.5 37.5 38.5 39.5 40.5 41.5 42.5 43.5 44.5 45.5 46.5 47.5
  [49] 48.5 49.5 50.5 51.5 52.5 53.5 54.5 55.5 56.5 57.5 58.5 59.5 60.5 61.5 62.5 63.5
  [65] 64.5 65.5 66.5 67.5 68.5 69.5 70.5 71.5 72.5 73.5 74.5 75.5 76.5 77.5 78.5 79.5
  [81] 80.5 81.5 82.5 83.5 84.5 85.5 86.5 87.5 88.5 89.5 90.5 91.5 92.5 93.5 94.5 95.5
  [97] 96.5 97.5
  
  $xname
  [1] "Body length (x), mm"
  
  $equidist
  [1] TRUE
  
  attr(,"class")
  [1] "histogram"
  
  attr(,"class")
  [1] "determine_xmin_and_fit" "list"

The res_mock object is list of two objects:

The object is of class determine_xmin_and_fit, and so the plotting function plot.determine_xmin_and_fit() is automatically used by plot(res_mock) to give the above plot.

Note that the true value of \(b = -2\) falls within the confidence interval.

Measurements that are already binned – MLEbin method

Often, data are only available in binned form, motivating our MLEbin and MLEbins methods (see our MEPS paper for details).

Saved in sizeSpectraFit is a binned version of the simulated data set used above:

sim_vec_binned
  # A tibble: 9 × 3
    bin_min bin_max bin_count
      <dbl>   <dbl>     <int>
  1       1       2       528
  2       2       4       228
  3       4       8       113
  4       8      16        75
  5      16      32        33
  6      32      64        14
  7      64     128         6
  8     128     256         2
  9     256     512         1

which has information on counts within each body-mass bin, rather than values of individual body masses. For example, the first row represent the first bin, which ranges from 1-2 g (bin_min to bin_max), and body masses in this range were observed for 528 individuals (bin_count). The bins here progressively double in width.

To fit the PLB distribution, we again just use the fit_size_spectrum() function (which automatically detects that we want to use the MLEbin method because our data are a data frame rather than a simple vector of individual body masses, so the function fit_size_spectrum.data.frame() gets used):

res_mlebin <- fit_size_spectrum(sim_vec_binned)
res_mlebin
  $b_mle
  [1] -2.035029
  
  $b_conf
  [1] -2.104249 -1.968419
  
  $data
  # A tibble: 9 × 6
    bin_min bin_max bin_count count_gte_bin_min low_count high_count
      <dbl>   <dbl>     <int>             <int>     <int>      <int>
  1       1       2       528              1000       472       1000
  2       2       4       228               472       244        472
  3       4       8       113               244       131        244
  4       8      16        75               131        56        131
  5      16      32        33                56        23         56
  6      32      64        14                23         9         23
  7      64     128         6                 9         3          9
  8     128     256         2                 3         1          3
  9     256     512         1                 1         0          1
  
  $x_min
  [1] 1
  
  $x_max
  [1] 512
  
  $n
  [1] 1000
  
  $method
  [1] "MLEbin"
  
  attr(,"class")
  [1] "size_spectrum_mlebin" "list"

So res_mlebin is a list object that contains objects:

Our default plot for results from the MLEbin method is simply:

plot(res_mlebin)

As for the MLE results above, the style argument changes the style of plot:

plot(res_mlebin,
     style = "linear_y_axis")

gives the same plot but with a linear y-axis.

To show both the above plots together:

plot(res_mlebin,
     style = "both_y_axes")

which is similar to, but simpler than, Figure 7 of our MEPS paper because here we do not have species-specific overlapping body-mass bins.

The fourth option is only suitable when the data represent body masses. It shows the normalised biomass:

plot(res_mlebin,
     style = "biomass")

and is interpreted the same way as the earlier plot for the MLE method (but here the bins are already predefined, because that is how the data are provided).

And to show this plot and the first one as two panels:

plot(res_mlebin,
     style = "biomass_and_log")

The y-axes happen to have the same numbers (though they mean different things) for this simulated data set, though this will not generally be the case.

Both figures show that the PLB distribution is an excellent fit for these data the red curves go through the top-left and bottom-right corners of the grey boxes. Of course, the data are simulated from a PLB distribution and so should be a good fit, but these figures show the benefit of the plotting approach (which may not have been so obvious in our MEPS Figure 7 for data with a complex bin structure).

All the plots are customisable, for example to add extra tick labels to pretty up a final plot:

plot(res_mlebin,
     x_small_ticks_labels = c(5, 50, 500))

See ?plot.size_spectrum_mlebin for details.

Determine \(x_{min}\) for MLEbin method when the data are not simply declining

Similar to the example above for a vector of values, we can determine a value of \(x_{min}\) to use for the MLEbin method if the data look non-decreasing.

For a mock dataset we can just use the above mock_data and then bin the values with bin widths that double in size, using the bin_data function:

mock_binned <- bin_data(mock_data,
                        bin_width = "2k")$bin_vals %>%
                 dplyr::select(bin_min,
                               bin_max,
                               bin_count)
mock_binned
  # A tibble: 9 × 3
    bin_min bin_max bin_count
      <dbl>   <dbl>     <int>
  1    0.25     0.5        15
  2    0.5      1          23
  3    1        2          62
  4    2        4         538
  5    4        8         233
  6    8       16         116
  7   16       32          79
  8   32       64          26
  9   64      128           8

Now use the explicit fitting function:

res_mock_binned <- determine_xmin_and_fit_mlebin(mock_binned)
plot(res_mock_binned)

The red bars in the histogram show the values that are used to fit the PLB distribution, with \(x_{min}\) being the lowest bin break of the lowest red bin; note the three grey bins that are not used for the fitting. This histogram shows the density in each bin, not the counts, because the bin widths are not equal; density is the count in a bin divided by the bin width.

res_mock_binned
  $mlebin_fit
  $b_mle
  [1] -2.061935
  
  $b_conf
  [1] -2.141015 -1.985265
  
  $data
  # A tibble: 6 × 6
    bin_min bin_max bin_count count_gte_bin_min low_count high_count
      <dbl>   <dbl>     <int>             <int>     <int>      <int>
  1       2       4       538              1000       462       1000
  2       4       8       233               462       229        462
  3       8      16       116               229       113        229
  4      16      32        79               113        34        113
  5      32      64        26                34         8         34
  6      64     128         8                 8         0          8
  
  $x_min
  [1] 2
  
  $x_max
  [1] 128
  
  $n
  [1] 1000
  
  $method
  [1] "MLEbin"
  
  attr(,"class")
  [1] "size_spectrum_mlebin" "list"                
  
  $h
  $breaks
   [1]   0.25   0.50   1.00   2.00   4.00   8.00  16.00  32.00  64.00 128.00
  
  $mids
  [1]  0.375  0.750  1.500  3.000  6.000 12.000 24.000 48.000 96.000
  
  $counts
  [1]  15  23  62 538 233 116  79  26   8
  
  $density
  [1]  60.0000  46.0000  62.0000 269.0000  58.2500  14.5000   4.9375   0.8125   0.1250
  
  $xname
  [1] "Normalised counts in each bin"
  
  $equidist
  [1] FALSE
  
  attr(,"class")
  [1] "histogram"
  
  attr(,"class")
  [1] "determine_xmin_and_fit_mlebin" "list"

The res_mock_binned object is list of two objects:

The object is of class determine_xmin_and_fit_mlebin, and so the plotting function plot.determine_xmin_and_fit_mlebin() is automatically used by plot(res_mock_binned) to give the above plot.

And then also consider that we want to also allow for a tibble that has year or another heading (maybe allow it to be called strata, or let user add that as an option - yes, strata = year as the default, or maybe strata = NULL is the default).

Digging into the details

All the function code is in the R/ folder so look there for the code if you want to dig deeper. Start with the function being used and then see what other functions it calls. Please email me with any questions/clarifications.

Our MEE paper

Our MEPS paper

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