Estimate abundance and biomass from backscatter#
This method provides a means for converting \(\textit{NASC}\) into number densities (animals nmi-2) via acoustic inversion by relating fish length to acoustic backscatter. One basic type of inversion is using the linear relationship between the average fish TS-length relationship. In practice, this means that the stratified length distributions can be leveraged to calculate a mean \(\textit{TS}\) for each stratum.
Import necessary modules#
With all of the biological and acoustic data ingested and processed, we can use the InversionLengthTS-class from the inversion sub-package to convert our integrated backscatter into population estimates.
from echopop.inversion import InversionLengthTS
The InversionLengthTS class#
The InversionLengthTS class is initialized using a parameter dictionary with the following required keys:
"ts_length_regression": A dictionary with values for the slope and intercept of the \(\textit{TS}\)-length regression"stratify_by": A single or list of column names for which to compute the mean \(\sigma_\text{bs}\) (and therefore \(\textit{TS}\)).
In addition to these mandatory keys, there are additional optional keys as well:
"expected_strata": An array of specific strat to expect in the biological data. This can be further used to define any particular strata that should be explicitly included in the analysis."impute_missing_strata": A boolean variable that determines whether or not to impute any missing \(\sigma_\text{bs}\) from any strata. This usually occurs when there may be insufficient biological data in a particular stratum."haul_replicates": Another boolean variable that whenTrue, individual hauls are used as replicates instead of individual lengths. This results in treating the average \(\sigma_\text{bs}\) of each haul as an individual sample, which are then averaged over their respective strata. This approach has the benefit of avoiding statistical issues due to pseudoreplication.
So now we can initialize our InversionLengthTS-class:
MODEL_PARAMETERS = {
"ts_length_regression": {
"slope": 20.,
"intercept": -68.
},
"stratify_by": ["stratum_ks"],
"expected_strata": df_dict_strata["ks"].stratum_num.unique(),
"impute_missing_strata": True,
"haul_replicates": True,
}
# Initiate object to perform inversion
invert_hake = InversionLengthTS(MODEL_PARAMETERS)
We can verify that the parameterization we initialized the object are actually being used via the model_params attribute:
invert_hake.model_params
{'ts_length_regression': {'slope': 20.0, 'intercept': -68.0},
'stratify_by': ['stratum_ks'],
'expected_strata': array([1, 2, 3, 4, 0, 5, 6, 7, 8]),
'impute_missing_strata': True,
'haul_replicates': True}
The next step is to run the inversion via the invert method. This has two required arguments:
"nasc_data": Apandas.DataFramewith the transect \(\textit{NASC}\) data."length_data": An individualpandas.DataFrame, or list ofpandas.DataFrames, containing length measurements sorted by each haul ("haul_num") and strata (e.g.,"stratum_ks").
Since there are two separate length datasets in dict_df_bio["length"] and dict_df_bio["specimen"], these can both be used to construct the overall length distribution required for the inversion:
df_nasc_all_ages = invert_hake.invert(nasc_data=df_nasc_all_ages,
length_data=[dict_df_bio["length"], dict_df_bio["specimen"]])
Before investigating the results, we can inspect the mean \(\sigma_\text{bs}\) for each stratum via the sigma_bs_strata attribute:
invert_hake.sigma_bs_strata
| sigma_bs | |
|---|---|
| stratum_ks | |
| 0 | 0.000425 |
| 1 | 0.000084 |
| 2 | 0.000070 |
| 3 | 0.000114 |
| 4 | 0.000191 |
| 5 | 0.000295 |
| 6 | 0.000247 |
| 7 | 0.000384 |
| 8 | 0.000481 |
The resulting df_nasc_all_ages output from invert_hake now has the column "number_density", which represents the number of animals nmi-2.
df_nasc_all_ages.columns
Index(['stratum_ks', 'transect_num', 'region_id', 'distance_s', 'distance_e',
'latitude', 'longitude', 'transect_spacing', 'layer_mean_depth',
'layer_height', 'bottom_depth', 'nasc', 'haul_num', 'mean length',
'year', 'stratum_inpfc', 'stratum name', 'nasc_proportion',
'geostratum_ks', 'geostratum_inpfc', 'number_density'],
dtype='object')
Estimating abundance and biomass#
The new number density estimates can be used to estimate abundance (number of animals) and biomass (kg). First, we have to estimate the along-transect distances between intervals. This is required for later estimating the area-per-interval. The compute_interval_distance function from the survey.transect module serves this purpose.
from echopop.survey import transect
transect.compute_interval_distance(nasc_data=df_nasc_all_ages, interval_threshold=0.05)
The interval_threshold argument is used for detecting (and removing) erroneously small or large intervals relative to the median interval distance. Let \(d\) and \(t\) represent interval distance (nmi) and transect, respectively. This is expressed as:
where \(\Delta\) is the distance threshold.
With the interval distances calculated, the areas can be readily computed via:
df_nasc_all_ages["area_interval"] = (
df_nasc_all_ages["transect_spacing"] * df_nasc_all_ages["distance_interval"]
)
Now the number densities can be combined with the areas for each interval to calculate along-transect abundances and biomasses. This can be done in a variety of ways, but in this case we can use the FEAT-specific functions compute_abundance and compute_biomass functions from the survey.biology module. For abundance, we need to use the previously calculated number proportions that serve to “normalize” the length distributions:
from echopop.survey import biology
biology.compute_abundance(
transect_data=df_nasc_all_ages,
exclude_filter={"sex": "unsexed"},
number_proportions=dict_ds_number_proportion,
)
The argument group_columns, when not an empty list, tells the function whether or not to apportion the abundance values by some contrast. In this case, not only was the column "abundance" appended to our DataFrame df_nasc_all_ages, but also "abundance_female" and "abundance_male". This also applies to the column "number_density":
df_nasc_all_ages.columns
Index(['stratum_ks', 'transect_num', 'region_id', 'distance_s', 'distance_e',
'latitude', 'longitude', 'transect_spacing', 'layer_mean_depth',
'layer_height', 'bottom_depth', 'nasc', 'haul_num', 'mean length',
'year', 'stratum_inpfc', 'stratum name', 'nasc_proportion',
'geostratum_ks', 'geostratum_inpfc', 'number_density',
'distance_interval', 'area_interval', 'abundance',
'number_density_female', 'number_density_male', 'abundance_female',
'abundance_male'],
dtype='object')
When a dictionary is supplised to the exclude_filter argument, they defined keys (e.g., sex: "unsexed") are removed from the biological data, and their respective proportions are redistributed among the non-excluded groups. So in this case, exclude_filter = {"sex": "unsexed"} instructed the function to remove all unsexed fish.
With these abundances, we can then shift to calculated the biomass and areal biomass densities (kg nmi-2). This first requires calculating the mean weight per stratum usingstratum_averaged_weight <echopop.survey.proportions.stratum_averaged_weight> from the survey.proportions module:
da_averaged_weight = get_proportions.stratum_averaged_weight(
number_proportions=dict_ds_number_proportion,
length_weight_data=da_binned_weight_table,
stratum_dim="stratum_ks",
)
This produces an xarray.DataArray indexed by "stratum_ks". Since the length weight data in da_binned_weight_table are further indexed by "sex", that is inherited when calculating the stratum-averaged weights. Consequently, the resulting xarray.DataArray in da_averaged_weight yields:
<xarray.DataArray (sex: 3, stratum_ks: 9)> Size: 216B
array([[1.38492575, 0.14030398, 0.1086261 , 0.25189047, 0.52947958,
1.0036961 , 0.78918411, 1.48631963, 2.17001698],
[0.76318331, 0.07101744, 0.0541192 , 0.14396151, 0.29124258,
0.51529254, 0.38927587, 0.81059302, 1.18625558],
[0.62174244, 0.06822671, 0.0543958 , 0.10305031, 0.23744678,
0.4763583 , 0.39311801, 0.66606293, 0.96221913]])
Coordinates:
* sex (sex) <U6 72B 'all' 'female' 'male'
* stratum_ks (stratum_ks) int64 72B 0 1 2 3 4 5 6 7 8These can then be combined with the abundances to get the biomass and biomass density estimates:
biology.compute_biomass(
transect_data=df_nasc_all_ages,
stratum_weights=da_averaged_weight,
)
Just like for "number_density" and "abundance", not only are the columns "biomass" and "biomass_density" appended to df_nasc_all_ages, but so are columns with the suffixes "_female" and "_male":
df_nasc_all_ages.columns
Index(['stratum_ks', 'transect_num', 'region_id', 'distance_s', 'distance_e',
'latitude', 'longitude', 'transect_spacing', 'layer_mean_depth',
'layer_height', 'bottom_depth', 'nasc', 'haul_num', 'mean length',
'year', 'stratum_inpfc', 'stratum name', 'nasc_proportion',
'geostratum_ks', 'geostratum_inpfc', 'number_density',
'distance_interval', 'area_interval', 'abundance',
'number_density_female', 'number_density_male', 'abundance_female',
'abundance_male', 'biomass_density_female', 'biomass_density_male',
'biomass_density', 'biomass_female', 'biomass_male', 'biomass'],
dtype='object')