knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
The purpose of this vignette is to learn how to estimate trophic position for multiple species or groups using stable isotope data ($\delta^{13}C$ and $\delta^{15}N$). We can estimate trophic position using a one source model based on equations from Post (2002).
The equation for a one source model consists of the following:
$$ \text{Trophic Position} = \lambda + \frac{(\delta^{15}N_c - \delta^{15}N_b)}{\Delta N} $$
Where $\lambda$ is the trophic position of the baseline (e.g., 2
), $\delta^{15}N_c$ is the $\delta^{15}N$ of the consumer, $\delta^{15}N_b$ is the mean $\delta^{15}N$ of the baseline, and $\Delta N$ is the trophic enrichment factor (e.g., 3.4).
To use this model with a Bayesian framework, we need to rearrange this equation to the following:
$$ \delta^{15}N_c = \delta^{15}N_b + \Delta N \times (\text{Trophic Position} - \lambda) $$
The function one_source_model()
uses this rearranged equation.
First we need to organize the data prior to running the model. To do this work we will use {dplyr} and {tidyr} but we could also use {data.table}.
When running the model we will use {trps} and {brms} and iterative processes provided by {purrr}.
Once we have run the model we will use {bayesplot} to assess models and then extract posterior draws using {tidybayes}. Posterior distributions will be plotted using {ggplot2} and {ggdist} with colours provided by {viridis}.
First we load all the packages needed to carry out the analysis.
{ library(bayesplot) library(brms) library(dplyr) library(ggplot2) library(ggdist) library(grid) library(purrr) library(tidybayes) library(tidyr) library(trps) library(viridis) }
In {trps} we have a data set that has consumer and baseline data already joined for two ecoregions (combined_iso
) using the same methods in
getting started with trps.
Let's look at this data frame.
combined_iso
We can see that this data frame has isotope data for a second baseline (dreissenids; d13c_b2
and d15n_b2
) as well as the mean values for both baselines (c1
-n2
). These columns for the second baseline are useful when estimating trophic position using a two source model but we do not need them for this analysis and they can be removed.
We can also confirm that this data set has one species, lake trout.
unique(combined_iso$common_name)
collected from two ecoregions in Lake Ontario.
unique(combined_iso$ecoregion)
Let's remove the columns we don't need, d13c_b2
, d15n_b2
, c2
, n2
, and add $\lambda$ to the data frame (l1
). To do so we make a name
column that will be the two groups we have, common_name
and ecoregion
pasted together. We are doing this to make the iterative processes easier.
combined_iso_update <- combined_iso %>% dplyr::select(-c(d13c_b2, d15n_b2, c2, n2)) %>% mutate( l1 = 2, name = paste(ecoregion, common_name, sep = "_") ) %>% dplyr::select(id, common_name, ecoregion, name, d13c:l1)
Let's view our completed data set.
combined_iso_update
This example data is now ready to be analyzed.
We will use similar structure used in getting started with trps
to model trophic position, however, we first split()
the data into a list
for all groups and then use map()
from {purrr}
to run the model for each group.
You will notice that the brm()
call is
exactly the same as
when we ran the model for one group.
The only difference here is when using map()
,
the data
argument in brm()
needs to be
replaced with .x
to tell brm()
where
to get the data.
Let's run the model!
model_output_os_mg <- combined_iso_update %>% split(.$name) %>% map( ~ brm( formula = one_source_model(), prior = one_source_priors(), stanvars = one_source_priors_params(), data = .x, family = gaussian(), chains = 2, iter = 4000, warmup = 1000, cores = 4, seed = 4, control = list(adapt_delta = 0.95) ), .progress = TRUE )
Let's look at the summary of both models.
model_output_os_mg
We can see that $\hat R$ is 1, meaning that the variance among and within chains are equal (see {rstan} docmentation on $\hat R$) and that ESS is quite large for both groups. Overall, this means that both models are converging and fitting accordingly.
Let's look at the trace plots and distributions. We use iwalk()
instead of map()
, as iwalk()
invisibly returns .x
which is handy when you want to call a function (e.g., plot()
) for its side effects rather than its returned value. I have also added grid.text()
from {grid}
to add the group names to each plot.
model_output_os_mg %>% iwalk(~ { plot(.x) grid.text(.y, x = 0.50, y = 0.98) })
We can see that the trace plots look "grassy" meaning the model is converging!
Let's again look at the summary output from the model.
model_output_os_mg
We can see that, for lake trout from the Anthropogenic
ecoregion, $\Delta N$ is estimated to be 3.38
with l-95% CI
of 2.88
, and u-95% CI
of 3.87
. If we move down to trophic position (tp
) we see trophic position is estimated to be 4.82
with l-95% CI
of 4.44
, and u-95% CI
of 5.29
.
We can see that, for lake trout from the Embayment
ecoregion, $\Delta N$ is estimated to be 3.37
with l-95% CI
of 2.89
, and u-95% CI
of 3.86
. If we move down to trophic position (tp
) we see trophic position is estimated to be 4.54
with l-95% CI
of 4.21
, and u-95% CI
of 4.96
.
We can check how well the model is predicting the $\delta^{15}N$ of the consumer
using pp_check()
from {bayesplot}
. We have to use map()
from {purrr}
to iterate over the list that has our model objects.
model_output_os_mg %>% map(~ .x %>% pp_check() )
We can see that posteriors draws ($y_{rep}$; light lines) for both groups are are effectively modeling $\delta^{15}N$ of the consumer ($y$; dark line).
We use functions from {tidybayes} to do this work. First we look at the the names of the variables we want to extract using get_variables()
. Considering we have multiple models in model_output_os_mg
that all have the same structure, we can just look at the names of the first model object in model_output_os_mg
.
get_variables(model_output_os_mg[[1]])
You will notice that "b_tp_Intercept"
is the name of the variable that we are wanting to extract. Next we extract posterior draws using gather_draws()
, and rename "b_tp_Intercept"
to tp
.
Again, considering we have multiple models in model_output_os_mg
we need to use map()
to iterate over model_output_os_mg
to get the posterior draws. Once we have iterated over model_output_os_mg
to extract draws we can combine the results using bind_rows()
from {dplyr}. The variable name
will have the name of the ecoregion and common name of the species pasted to together by a _
. We need to separate this string into the two variables we want, being ecoregion
and common_name
. We can do this by using separate_wider_delim()
from {tidyr}. When using this function it will separate the columns and keep them as characters
, hence why the last step is to convert ecoregion
into a factor
.
For your data you will likely have category names other than ecoregion
and common_name
. Please replace with the columns that fit your data structure.
post_draws_mg <- model_output_os_mg %>% map(~ .x %>% gather_draws(b_tp_Intercept) %>% mutate( .variable = "tp" ) %>% ungroup() ) %>% bind_rows(.id = "name") %>% separate_wider_delim(name, names = c("ecoregion", "common_name"), delim = "_", cols_remove = FALSE) %>% mutate( ecoregion = factor(ecoregion, levels = c("Anthropogenic", "Embayment")), )
Let's view the post_draws_mg
post_draws_mg
We can see that the posterior draws data frame consists of seven variables:
ecoregion
common_name
.chain
.iteration
(number of samples after burn-in).draw
(number of samples from iter
).variable
(this will have different variables depending on what is supplied to gather_draws()
).value
(estimated value)Note - the names of and items in the first two columns will vary depending on the names you split your data into.
Considering we are likely using this information for a paper or presentation, it is nice to be able to report the median and credible intervals (e.g., equal-tailed intervals; ETI). We can extract and export these values using spread_draws()
and median_qi
from {tidybayes}.
Again, because model_output_os_mg
is a list
of our model objects, we need to map()
over the list to calculate these values. Then we do the same procedures we have done before to combine and restructure the outputs. Lastly, we use mutate_if()
to round all columns that are numeric to two decimal points.
post_medians_ci <- model_output_os_mg %>% map(~ .x %>% spread_draws(b_tp_Intercept) %>% median_qi() %>% rename( tp = b_tp_Intercept ) ) %>% bind_rows(.id = "name") %>% separate_wider_delim(name, names = c("ecoregion", "common_name"), delim = "_", cols_remove = FALSE) %>% mutate( ecoregion = factor(ecoregion, levels = c("Anthropogenic", "Embayment")), ) %>% mutate_if(is.numeric, round, digits = 2)
Let's view the output.
post_medians_ci
I like to use {openxlsx} to export these values into a table that I can use for presentations and papers. For the vignette I am not going to demonstrate how to do this but please check out {openxlsx}
.
Now that we have our posterior draws extracted we can plot them. For comparing trophic position among species or groups, I like using either violin plots, interval points, or slab plots for posteriors. We can access violins through {ggplot2} with the later being available in {ggdist}.
Let's first look at the violin plot.
ggplot(data = post_draws_mg, aes(x = common_name, y = .value, fill = ecoregion)) + geom_violin() + stat_summary(fun = median, geom = "point", size = 3, position = position_dodge(0.9) ) + scale_fill_viridis_d(name = "Ecoregion", option = "G", begin = 0.35, end = 0.75, alpha = 0.65) + theme_bw(base_size = 15) + theme( panel.grid = element_blank(), legend.position = "inside", legend.position.inside = c(0.85, 0.86) ) + labs( x = "Species", y = "P(Trophic Position | X)" )
Next, we'll look at the point interval plot – but first we need to create our colour palette.
viridis_colours <- viridis(2, option = "G", begin = 0.35, end = 0.75, alpha = 0.65)
Now let's plot the point intervals.
ggplot(data = post_draws_mg, aes(x = common_name, y = .value, group = ecoregion)) + stat_pointinterval( aes(point_fill = ecoregion), point_size = 4, interval_colour = "grey60", position = position_dodge(0.4), shape = 21, ) + scale_fill_manual(aesthetics = "point_fill", values = viridis_colours, name = "Ecoregion") + theme_bw(base_size = 15) + theme( panel.grid = element_blank(), legend.position = "inside", legend.position.inside = c(0.85, 0.86) ) + labs( x = "Species", y = "P(Trophic Position | X)" )
Congratulations we have successfully run a Bayesian one source trophic position model for one species in two ecoregions of Lake Ontario!
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.