<- function(t, n, parms) {
interspec_dens_dep_growth with(as.list(parms), {
<- r1 * n[1] * (1 - a11 * n[1] - a12 * n[2])
dn1dt <- r2 * n[2] * (1 - a22 * n[2] - a21 * n[1])
dn2dt list(c(dn1dt, dn2dt))
}) }
Competition
Lotka-volterra Interspecific Continuous Logistic Growth Model
Today we will focus on “Interspecific Continuous Logistic Growth” which is common in nature. Let’s break this term into individual parts:
- Inter: Between
- Specific: Species
- Continuous: Not discrete
- Logistic: Density dependent
From lab 3, do you remember what the equation for continuous logistic growth?
\[ rN(1-\frac{N}{K}) \]
Why do we use \(r\) in this equations instead of \(\lambda\)?
- Because we are modeling continuous population growth.
Why did we incorporate \(K\)?
- Because we are modeling logistic population growth.
We can rearrange to help us convert this to relate to interspecific competition:
\[ rN(\frac{K}{K} - \frac{N}{K}) \]
\[ rN(\frac{K-N}{K}) \]
This continuous logistic growth equation is currently for a single species, but our model today should be interspecific. In order to do so, we can add the term \(\alpha\) which is the effect of species 2 on the population growth of species 1 (i.e. how many individuals of species 2 are equivalent to one individual of species 1 in regards to resource usage).
\[ \frac{dN_{1}}{dt} = r_{1}N_{1}(\frac{K_{1} - N_{1} - \alpha N_{2}}{K_{1}}) \]
We can simplify this equation by integrating carrying capacity \(K\) into the term \(\alpha_{11}\) representing conspecific/intraspecific density-dependence.
We can also combine terms into interspecific density dependence or “interspecific competition coefficient” \(\alpha _{12}\).
The result is the “Lotka-Volterra Interspecific Density-dependent Growth” model:
\[ \frac{dN_{1}}{dt} \ =\ r_{1} N_{1} \ ( \ 1\ -\ \alpha _{11} N_{1} \ -\ \alpha _{12} N_{2}) \]
\[ \frac{dN_{2}}{dt} \ =\ r_{2} N_{2} \ ( \ 1\ -\ \alpha _{22} N_{1} \ -\ \alpha _{21} N_{1}) \]
Now let’s create a function for this model in R. Once again, we can write differential equations in R using the ordinary differential equation (ODE) package deSolve
(Soetaert, Petzoldt, and Setzer 2010) following the format:
Write a function to represent interspecific density-dependent growth between 2 species
Run your function with the following parameters:
- Intrinsic rate of growth of species A is 1 and of species B is 0.1.
- The interspecific negative density dependence of species A on B is 0.1. Vice versa is 0.01.
- The intraspecific density dependence is 0.2 and 0.02 of species A and B respectively.
- Initial population sizes of species A and B are 2 and 1 respectively
- Time range will be 1:100
library(deSolve)
Warning: package 'deSolve' was built under R version 4.3.2
<- 1
r1 <- 0.1
r2 <- 0.2
a11 <- 0.1
a21 <- 0.02
a22 <- 0.01
a12
<- c(1:100)
times
<- 2
N0_1 <- 1
N0_2
<- c(r1 = r1, r2 = r2, a11 = a11, a21 = a21, a22 = a22, a12 = a12)
parms <- c(N0_1, N0_2)
initialN
<- deSolve::ode(y = initialN, times = times, func = interspec_dens_dep_growth, parms = parms) out
Now plot your results:
library(dplyr)
Warning: package 'dplyr' was built under R version 4.3.2
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(ggplot2)
Warning: package 'ggplot2' was built under R version 4.3.2
<- out %>%
ode_df as.data.frame() %>% # Convert to dataframe for ggplot2
rename(time = 1, V1 = 2, V2 = 3) # Rename because numbered column names don't work well with ggplot2
ggplot(data = ode_df, aes(x = time)) +
geom_line(aes(y = V1, colour = "Species 1")) +
geom_line(aes(y = V2, colour = "Species 2")) +
labs(x = "Years", y = "Population Size", colour = "Species")
Describe what you observe in the output of this model with the given parameters. How are the populations of species A and B growing/declining? How are they influencing one another?
# A: Species 1 is strongly influenced by the abundance of species 2. At first species 1 increases following a logistic growth model, however once species 2 population is high enough it competes with resources with species 1 and species 1 population drops. If this continued, species 1 woud eventually go extinct
# Meanwhile, the population of species 2 continues to grow exponentially since it has not met its carrying capacity yet.
Instability
By modifying the parameters a12
, a21
, a11
, and a22
, can you find values where species A goes extinct? Can you find values where species B goes extinct?
<- 1
r1 <- 0.2
r2 <- 0.2
a11 <- 0.1
a21 <- 0.02
a22 <- 0.03
a12
<- c(r1 = r1, r2 = r2, a11 = a11, a21 = a21, a22 = a22, a12 = a12)
parms <- c(2, 1)
initialN
<- ode(y = initialN, times = 1:100, func = interspec_dens_dep_growth, parms = parms)
out
<- out %>%
ode_df as.data.frame() %>% # Convert to dataframe for ggplot2
rename(time = 1, V1 = 2, V2 = 3) # Rename because numbered column names don't work well with ggplot2
ggplot(data = ode_df, aes(x = time)) +
geom_line(aes(y = V1, colour = "Species 1")) +
geom_line(aes(y = V2, colour = "Species 2")) +
labs(x = "Years", y = "Population Size", colour = "Species")
# A: We can use this function to find various combinations of values where one species goes extinct and the other reaches a carrying capacity (shown above). This further demonstrates how the populations of these two species influence one another. It is also possible to find combinations where neither species goes extinct.
Neutral Equilibria
Set your parameters to the following conditions:
<- r2 <- 1
r1 <- a21 <- 0.01
a11 <- a12 <- 0.015
a22 <- c(2, 1) initialN
What do you observe? Is this result stable or unstable? Or something more nuanced? Explain.
<- r2 <- 1
r1 <- a21 <- 0.01
a11 <- a12 <- 0.015
a22 <- c(2, 1)
initialN
<- c(r1 = r1, r2 = r2, a11 = a11, a21 = a21, a22 = a22, a12 = a12)
parms
<- deSolve::ode(y = initialN, times = 1:100, func = interspec_dens_dep_growth, parms = parms)
out
<- out %>%
ode_df as.data.frame() %>% # Convert to dataframe for ggplot2
rename(time = 1, V1 = 2, V2 = 3) # Rename because numbered column names don't work well with ggplot2
ggplot(data = ode_df, aes(x = time)) +
geom_line(aes(y = V1, colour = "Species 1")) +
geom_line(aes(y = V2, colour = "Species 2")) +
labs(x = "Years", y = "Population Size", colour = "Species")
# A: We observe something called a "neutral saddle." The relationship is not considered stable because after perturbation although the ratio between the population sizes of the two species will remain constant, a new equilibrium point will be established for each species' population size.
Interspecific Continuous Logistic Growth With Real Data
From year 2000 to 2014, (Reif et al. 2019)(Reif et al. 2018) collected nightingale (n=552) distribution data from 959 plots, focusing on sibling species Common Nightingale (Luscinia megarhynchos) and the Thrush Nightingale (L. luscinia). This study was conducted in Poland where these species overlap in range:
Along with population counts, researchers noted the following relationships:
Allopatric: Two or more related species which have separate distributions geographically.
Sympatric: Two or more related species which have the same or overlapping distributions geographically.
Syntopic: Two or more related species which have the same or overlapping distributions geographically, and share the same habitats within that distribution
Allotopic: Two or more related species which have the same or overlapping distributions geographically, but do not share the same habitats within that distribution
Let’s load in the nightingales.csv
data and assign to the variable nightingales_raw
. (Hint: if you are having trouble loading the data, view the file in Rstudio and not the separator. You may need to define the sep
paramater in your data loading function) Briefly explore the dataset:
<- read.csv("nightingales.csv", sep = ";")
nightingales_raw
head(nightingales_raw)
plot count co.occ year env.grad obs lon lat
1 1 1 CN.synt 2010 -132.3607 100 14.97798 52.35997
2 1 3 TN.synt 2010 -132.3607 100 14.97798 52.35997
3 2 0 TN.alot 2009 -87.9261 162 15.80042 52.38422
4 2 4 CN.alot 2009 -87.9261 162 15.80042 52.38422
5 3 1 CN.alot 2010 -125.6040 110 14.56317 52.74558
6 3 0 TN.alot 2010 -125.6040 110 14.56317 52.74558
summarize(nightingales_raw)
data frame with 0 columns and 1 row
Clean the data using the separate()
function from the (tidyr
)(Wickham and Wickham 2017) package to separate species
and co_occurence_status
.
library(tidyr)
Warning: package 'tidyr' was built under R version 4.3.2
# Cleaning data
<- nightingales_raw %>%
nightingales_disag ::separate(col = co.occ, into = c("species", "co_occurrence_status"), sep = "\\.")
tidyr
head(nightingales_disag)
plot count species co_occurrence_status year env.grad obs lon lat
1 1 1 CN synt 2010 -132.3607 100 14.97798 52.35997
2 1 3 TN synt 2010 -132.3607 100 14.97798 52.35997
3 2 0 TN alot 2009 -87.9261 162 15.80042 52.38422
4 2 4 CN alot 2009 -87.9261 162 15.80042 52.38422
5 3 1 CN alot 2010 -125.6040 110 14.56317 52.74558
6 3 0 TN alot 2010 -125.6040 110 14.56317 52.74558
To help us visualize our dataset, let’s map our data and facet by co_occurence_status
# Map
ggplot(data = nightingales_disag, aes(x = lon, y = lat, color = species)) +
geom_point() +
facet_wrap(~co_occurrence_status)
Let’s aggregate our data by year:
<- nightingales_raw %>%
nightingales_agg separate(col = co.occ, into = c("species", "co_occurrence_status"), sep = "\\.") %>%
group_by(species, year, co_occurrence_status) %>%
summarize(avg_count = mean(count)) %>%
ungroup() %>%
arrange(species, year)
`summarise()` has grouped output by 'species', 'year'. You can override using
the `.groups` argument.
# We are using summarize() instead of mutate because summarize() will make the group_by() permanent and make the `avg_count` permanent. If we used mutate() in this case, the column created would disappear once summarize() was called.
nightingales_agg
# A tibble: 90 × 4
species year co_occurrence_status avg_count
<chr> <int> <chr> <dbl>
1 CN 2000 alop 1
2 CN 2000 alot 0.333
3 CN 2000 synt 0.688
4 CN 2001 alop 2.13
5 CN 2001 alot 0.541
6 CN 2001 synt 0.630
7 CN 2002 alop 1.45
8 CN 2002 alot 0.452
9 CN 2002 synt 0.686
10 CN 2003 alop 1.12
# ℹ 80 more rows
Inspect the dimensions, do they make sense?
- (2 species) x (3 conditions) x (15 years) = 90 rows
(dplyr
)(Wickham et al. 2022)’s group_by()
function is a powerful and convenient way to separate data into groups and perform operations on each group. However, it is important to remember to always use ungroup()
after you are done with your group operations. This is because group_by()
will continue to affect your data until you use ungroup()
. This is even more dangerous because visually you will not see any difference in your data after using group_by()
.
Our goal is to find the mean of the count
column for all rows. Let’s group_by()
the column co.occ
:
<- nightingales_raw %>%
nightingales_groupedgroup_by(co.occ)
head(nightingales_grouped)
# A tibble: 6 × 8
# Groups: co.occ [4]
plot count co.occ year env.grad obs lon lat
<int> <int> <chr> <int> <dbl> <int> <dbl> <dbl>
1 1 1 CN.synt 2010 -132. 100 15.0 52.4
2 1 3 TN.synt 2010 -132. 100 15.0 52.4
3 2 0 TN.alot 2009 -87.9 162 15.8 52.4
4 2 4 CN.alot 2009 -87.9 162 15.8 52.4
5 3 1 CN.alot 2010 -126. 110 14.6 52.7
6 3 0 TN.alot 2010 -126. 110 14.6 52.7
How does the dataset look different after grouping?
Besides the small “Groups: co.occ [4]” at the top of the output, the data does not appear any different after grouping
Now let’s try to meet our goal and find the mean of all count rows.
summarize(nightingales_grouped, mean(count))
# A tibble: 6 × 2
co.occ `mean(count)`
<chr> <dbl>
1 CN.alop 1.65
2 CN.alot 0.491
3 CN.synt 0.723
4 TN.alop 1.25
5 TN.alot 0.453
6 TN.synt 0.999
We have 6 mean values instead of the desired overall mean!
This is because we did not ungroup()
like we always should. So even thought there is no visual difference after using group_by()
, not using ungroup()
affects your code downstream.
Now let’s try the same but use ungroup()
appropriately:
<- nightingales_raw %>%
nightingales_ungrouped group_by(co.occ) %>%
ungroup()
head(nightingales_ungrouped)
# A tibble: 6 × 8
plot count co.occ year env.grad obs lon lat
<int> <int> <chr> <int> <dbl> <int> <dbl> <dbl>
1 1 1 CN.synt 2010 -132. 100 15.0 52.4
2 1 3 TN.synt 2010 -132. 100 15.0 52.4
3 2 0 TN.alot 2009 -87.9 162 15.8 52.4
4 2 4 CN.alot 2009 -87.9 162 15.8 52.4
5 3 1 CN.alot 2010 -126. 110 14.6 52.7
6 3 0 TN.alot 2010 -126. 110 14.6 52.7
summarize(nightingales_ungrouped, mean(count))
# A tibble: 1 × 1
`mean(count)`
<dbl>
1 0.940
Now plot avg_count
across time for nightingales_agg
using geom_point()
and geom_smooth
, faceted by co_occurrence_status
ggplot(data = nightingales_agg, aes(x = year, y = avg_count, color = species)) +
geom_point() +
geom_smooth() +
facet_wrap(~co_occurrence_status)
`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
It looks like there might be a difference between co-occurrence statuses. Let’s see if competition is occurring. In order to do so we will need to extract the following parameters from our data:
- r1 = Intrinsic rate of growth of species CN
- r2 = Intrinsic rate of growth of species TN
- a21 = The interspecific negative density dependence of species CN on TN
- a12 = The interspecific negative density dependence of species TN on CN
- a11 = The intraspecific density dependence of species CN
- a22 = The intraspecific density dependence of species TN
- N0 = Initial population sizes of each species
- times = Time range of dataset in years
Time Range
<- c(1:(max(nightingales_agg$year) - min(nightingales_agg$year) + 1))
times
times
[1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Initial Population Sizes
<- nightingales_agg %>%
N0_CN filter(species == "CN") %>%
pull(avg_count) %>%
first()
N0_CN
[1] 1
<- nightingales_agg %>%
N0_TN filter(species == "TN") %>%
pull(avg_count) %>%
first()
N0_TN
[1] 1.236842
That will be sufficient for most datasets, however in this case we will now repeat for the other conditions:
<- nightingales_agg %>%
N0_CN_alop filter(species == "CN", co_occurrence_status == "alop") %>%
pull(avg_count) %>%
first()
<- nightingales_agg %>%
N0_CN_alot filter(species == "CN", co_occurrence_status == "alot") %>%
pull(avg_count) %>%
first()
<- nightingales_agg %>%
N0_CN_synt filter(species == "CN", co_occurrence_status == "synt") %>%
pull(avg_count) %>%
first()
<- nightingales_agg %>%
N0_TN_alop filter(species == "TN", co_occurrence_status == "alop") %>%
pull(avg_count) %>%
first()
<- nightingales_agg %>%
N0_TN_alot filter(species == "TN", co_occurrence_status == "alot") %>%
pull(avg_count) %>%
first()
<- nightingales_agg %>%
N0_TN_synt filter(species == "TN", co_occurrence_status == "synt") %>%
pull(avg_count) %>%
first()
Intrinsic Population Growth Rates
A simplified estimation of intrinsic population growth is to calculate the average value of intrinsic growth rate within a given interval:
\[ r = frac{log(\frac{\bar N_{t_{final}}}{\bar N_{0}})}{\bar N_{t_{final}}-{\bar N_{0}}} \]
<- nightingales_agg %>%
r1 filter(co_occurrence_status == "alop", species == "CN") %>%
summarise(r = log(last(avg_count) / first(avg_count)) / (last(year) - first(year))) %>%
pull(r)
<- nightingales_agg %>%
r2 filter(co_occurrence_status == "alop", species == "TN") %>%
summarise(r = log(last(avg_count) / first(avg_count)) / (last(year) - first(year))) %>%
pull(r)
r1
[1] 0.05923424
r2
[1] -0.008801167
Let’s plot to check to see if this is a decent approximation:
# Calculate predicted count using our intrinsic growth rates
<- nightingales_agg %>%
nightingales_pred mutate(predicted_count = case_when(
== "CN" ~ N0_CN_alop * exp(r1 * (year - min(year))),
species == "TN" ~ N0_TN_alop * exp(r2 * (year - min(year)))
species %>%
)) filter(co_occurrence_status == "alop")
%>%
nightingales_agg filter(co_occurrence_status == "alop") %>%
ggplot(aes(x = year, y = avg_count, color = species)) +
geom_point() +
geom_line(data = nightingales_pred, aes(x = year, y = predicted_count), linetype = "dashed") +
geom_smooth() +
labs(x = "Year", y = "Average Count", color = "Species", title = "Actual vs. Predicted Population Counts")
`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
It seems like our simplified estimate will be sufficient for this analysis, however note that the curves are not an excellent match.
Here are some examples of how a more accurate model of intrinsic growth rate could be made, by fixing to a non-linear model and using the continuous exponential population growth function we covered earlier this semester:
\[ Ne^{rt} \]
<- nightingales_disag %>%
alop_data filter(co_occurrence_status == "alop") %>%
mutate(min_year = min(year)) %>%
mutate(year_since_min = year - min_year) %>%
select(-min_year) %>%
group_by(species) %>%
mutate(N0 = if_else(year == min(year), count, NA_real_)) %>%
mutate(N0 = if_else(is.na(N0), ave(N0, FUN = function(x) mean(x, na.rm = TRUE)), N0)) %>%
ungroup()
# Setup the exponential growth model function for nls fitting
<- function(data, N0_col, time_col = "year_since_min", count_col = "count") {
fit_growth_rate <- as.formula(sprintf("%s ~ %s * exp(r * %s)", count_col, N0_col, time_col))
formula <- list(r = 0.1) # Starting value for 'r', this is a "best guess"
start_list <- nls(formula, data = data, start = start_list, algorithm = "port")
model return(model)
}
# Fit the model for each species
<- lapply(split(alop_data, alop_data$species), function(species_data) {
models fit_growth_rate(species_data, N0_col = "N0")
})
# Print the models to see the fitted parameters
print(models)
$CN
Nonlinear regression model
model: count ~ N0 * exp(r * year_since_min)
data: data
r
0.0562
residual sum-of-squares: 2406
Algorithm "port", convergence message: relative convergence (4)
$TN
Nonlinear regression model
model: count ~ N0 * exp(r * year_since_min)
data: data
r
5.365e-05
residual sum-of-squares: 5673
Algorithm "port", convergence message: relative convergence (4)
<- nightingales_agg %>%
alop_data filter(co_occurrence_status == "alop") %>%
group_by(species) %>%
mutate(N0 = avg_count[which.min(year)]) %>%
ungroup() %>%
mutate(year_since_min = year - min(year))
# Define the exponential growth model function
<- function(t, r, N0) {
exponential_growth_model * exp(r * t)
N0
}
# Setup the nls fitting function
<- function(data, time_col = "year_since_min", count_col = "avg_count") {
fit_growth_rate <- as.formula(paste(count_col, "~", "N0", "* exp(r *", time_col, ")")) # Using "N0" directly instead of N0_col
formula <- list(r = 0.1) # Starting value for 'r'
start_list <- nls(formula, data = data, start = start_list, algorithm = "port")
model return(summary(model))
}
# Fit the model for each species
<- lapply(split(alop_data, alop_data$species), function(species_data) {
models fit_growth_rate(species_data)
})
# Print the summary of the models to see the fitted parameters
lapply(models, print)
Formula: avg_count ~ N0 * exp(r * year_since_min)
Parameters:
Estimate Std. Error t value Pr(>|t|)
r 0.055988 0.006361 8.802 4.43e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.3807 on 14 degrees of freedom
Algorithm "port", convergence message: relative convergence (4)
Formula: avg_count ~ N0 * exp(r * year_since_min)
Parameters:
Estimate Std. Error t value Pr(>|t|)
r 0.001142 0.004100 0.278 0.785
Residual standard error: 0.1636 on 14 degrees of freedom
Algorithm "port", convergence message: relative convergence (4)
$CN
Formula: avg_count ~ N0 * exp(r * year_since_min)
Parameters:
Estimate Std. Error t value Pr(>|t|)
r 0.055988 0.006361 8.802 4.43e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.3807 on 14 degrees of freedom
Algorithm "port", convergence message: relative convergence (4)
$TN
Formula: avg_count ~ N0 * exp(r * year_since_min)
Parameters:
Estimate Std. Error t value Pr(>|t|)
r 0.001142 0.004100 0.278 0.785
Residual standard error: 0.1636 on 14 degrees of freedom
Algorithm "port", convergence message: relative convergence (4)
Intraspecific Density Dependencies
For intraspecific density dependencies, we can perform a linear regression. Again, a non-linear regression would likely be better for this dataset, however this will provide a simplified estimate:
<- nightingales_agg %>%
alop_data filter(co_occurrence_status == "alop") %>% # Only selecting "alop" so that interspecies dynamics at play. If your dataset is not separated into alopatric/sympatric samples, you can skip this step
arrange(species, year) %>%
group_by(species) %>%
mutate(lag_avg_count = lag(avg_count)) %>% # Add a lagged population size column
filter(!is.na(lag_avg_count)) %>% # Remove the first year since it won't have a lagged population size
mutate(change_in_count = avg_count - lag_avg_count) # Calculate the change in population size
# Perform a linear regression to estimate the intraspecific density dependence
# For CN (a11)
<- lm(change_in_count ~ lag_avg_count, data = filter(alop_data, species == "CN"))
a11_model <- unname(coef(a11_model)["lag_avg_count"])
a11
# For TN (a22)
<- lm(change_in_count ~ lag_avg_count, data = filter(alop_data, species == "TN"))
a22_model <- unname(coef(a22_model)["lag_avg_count"])
a22
# Output the results
a11
[1] -0.4923871
a22
[1] -1.054365
Interspecific Negative Density Dependencies
As a simple way to estimate interspecific negative density dependencies if you do not have allopatric vs. sympatric data, you can still calculate a12
and a21
like we did in lecture, by taking the mean population size of each species and dividing them by one another:
\[ a_{12} = \frac{\bar N_{2}}{\bar N_{1}} \]
<- nightingales_agg %>%
CN_avg filter(species == "CN", co_occurrence_status == "synt") %>% # Only selecting "synt" so that interspecies dynamics are strongest. If your dataset is not separated into alopatric/sympatric samples, you can skip this step
summarise(avg = mean(avg_count)) %>%
pull(avg)
<- nightingales_agg %>%
TN_avg filter(species == "TN", co_occurrence_status == "synt") %>% # Only selecting "synt" so that interspecies dynamics are strongest. If your dataset is not separated into alopatric/sympatric samples, you can skip this step
summarise(avg = mean(avg_count)) %>%
pull(avg)
<- TN_avg / CN_avg
a12 <- CN_avg / TN_avg
a21
a12
[1] 1.522671
a21
[1] 0.6567406
However with this dataset we can take a more complex approach. We can calculate the mean population sizes under ‘alop’ and ‘synt’ conditions and then calculate the differences. We can then use these differences to estimate a12 and a21:
# Calculate the mean population sizes under 'alop' and 'synt' conditions
<- nightingales_agg %>%
alop_data filter(co_occurrence_status == "alop") %>%
group_by(species, year) %>%
summarise(avg_count_alop = mean(avg_count), .groups = 'drop')
<- nightingales_agg %>%
synt_data filter(co_occurrence_status == "synt") %>%
group_by(species, year) %>%
summarise(avg_count_synt = mean(avg_count), .groups = 'drop')
# Join the datasets and calculate the differences
<- left_join(alop_data, synt_data, by = c("species", "year")) %>%
comparison mutate(difference = avg_count_synt - avg_count_alop)
# Estimate a12 and a21 by looking at the differences
# a21: effect of CN on TN
<- comparison %>%
a21 filter(species == "TN") %>%
summarise(a21 = mean(difference, na.rm = TRUE)) %>%
pull(a21)
# a12: effect of TN on CN
<- comparison %>%
a12 filter(species == "CN") %>%
summarise(a12 = mean(difference, na.rm = TRUE)) %>%
pull(a12)
# Output the results
a21
[1] -0.1859028
a12
[1] -0.875976
Now let’s do the same for each co-occurence status separately:
# Calculate mean population sizes for CN and TN under 'alop' condition
<- nightingales_agg %>%
comparison_alop filter(co_occurrence_status == "alop") %>%
group_by(year, species) %>%
summarise(avg_count = mean(avg_count)) %>%
pivot_wider(names_from = species, values_from = avg_count) %>%
mutate(difference = TN - CN) %>%
ungroup() %>%
summarise(a21_alop = mean(difference),
a12_alop = -mean(difference)) # Negative because it's the effect of TN on CN
`summarise()` has grouped output by 'year'. You can override using the
`.groups` argument.
<- comparison_alop$a12_alop
a12_alop <- comparison_alop$a21_alop
a21_alop
a12_alop
[1] 0.3237545
a21_alop
[1] -0.3237545
# Calculate mean population sizes for CN and TN under 'alot' condition
<- nightingales_agg %>%
comparison_alot filter(co_occurrence_status == "alot") %>%
group_by(year, species) %>%
summarise(avg_count = mean(avg_count)) %>%
pivot_wider(names_from = species, values_from = avg_count) %>%
mutate(difference = TN - CN) %>%
ungroup() %>%
summarise(a21_alot = mean(difference),
a12_alot = -mean(difference)) # Negative because it's the effect of TN on CN
`summarise()` has grouped output by 'year'. You can override using the
`.groups` argument.
<- comparison_alot$a12_alot
a12_alot <- comparison_alot$a21_alot
a21_alot
a12_alot
[1] -0.01047506
a21_alot
[1] 0.01047506
# Calculate mean population sizes for CN and TN under 'synt' condition
<- nightingales_agg %>%
comparison_synt filter(co_occurrence_status == "synt") %>%
group_by(year, species) %>%
summarise(avg_count = mean(avg_count)) %>%
pivot_wider(names_from = species, values_from = avg_count) %>%
mutate(difference = TN - CN) %>%
ungroup() %>%
summarise(a21_synt = mean(difference),
a12_synt = -mean(difference)) # Negative because it's the effect of TN on CN
`summarise()` has grouped output by 'year'. You can override using the
`.groups` argument.
<- comparison_synt$a12_synt
a12_synt <- comparison_synt$a21_synt
a21_synt
a12_synt
[1] -0.3663187
a21_synt
[1] 0.3663187
Lotka-Voltera Interspecific Competition Model
Now let’s input our values into our Lotka-Voltera interspecific competition model:
<- function(t, n, parms) {
interspec_dens_dep_growth with(as.list(parms), {
<- r1 * n[1] * (1 - a11 * n[1] - a12 * n[2])
dn1dt <- r2 * n[2] * (1 - a22 * n[2] - a21 * n[1])
dn2dt list(c(dn1dt, dn2dt))
}) }
Set parameters:
# Set parameters
<- c(r1 = r1, r2 = r2, a11 = a11, a21 = a21, a22 = a22, a12 = a12)
parms
# Set N0
<- c(N0_CN, N0_TN)
N0
# Set times
<- times times
Run the model while adding the parameter maxsteps = 1e5
:
# Run the ODE model
<- deSolve::ode(y = N0, times = times, func = interspec_dens_dep_growth, parms = parms, maxsteps = 1e5) out
Plot your results:
# Convert to dataframe for plotting
<- out %>%
ode_df as.data.frame() %>%
rename(time = 1, V1 = 2, V2 = 3)
# Plot the results
ggplot(data = ode_df, aes(x = time)) +
geom_line(aes(y = V1, colour = "CN")) +
geom_line(aes(y = V2, colour = "TN")) +
labs(x = "Years", y = "Population Size", colour = "Species")
In general, are these species suppressing the populations of one another? Is competition occurring?
From this analysis alone, it appears that as TN decreases, CN increases in a seemingly density independent manner. TN may be controlling CN.
Modify the parameters to see if the models agrees with your expectations:
<- c(0, mean(N0_TN_alop, N0_TN_alot, N0_TN_synt))
N0
<- c(1:(max(nightingales_agg$year) - min(nightingales_agg$year) + 1))
times
<- deSolve::ode(y = N0, times = times, func = interspec_dens_dep_growth, parms = parms, maxsteps = 1e5)
out
<- out %>%
ode_df as.data.frame() %>%
rename(time = 1, V1 = 2, V2 = 3)
ggplot(data = ode_df, aes(x = time)) +
geom_line(aes(y = V1, colour = "CN")) +
geom_line(aes(y = V2, colour = "TN")) +
labs(x = "Years", y = "Population Size", colour = "Species")
<- c(mean(N0_CN_alop,N0_CN_alot, N0_CN_synt), 0)
N0
<- c(1:(max(nightingales_agg$year) - min(nightingales_agg$year) + 1))
times
<- deSolve::ode(y = N0, times = times, func = interspec_dens_dep_growth, parms = parms, maxsteps = 1e5)
out
<- out %>%
ode_df as.data.frame() %>%
rename(time = 1, V1 = 2, V2 = 3)
ggplot(data = ode_df, aes(x = time)) +
geom_line(aes(y = V1, colour = "CN")) +
geom_line(aes(y = V2, colour = "TN")) +
labs(x = "Years", y = "Population Size", colour = "Species")
Without CN present, TN population levels continues to drop. When TN is removed, CN population levels continue to grow.
Now let’s look at the results across the different co-occurrence contexts to see if these results hold.
<- list(
N0.s alop = c(N0_CN_alop, N0_TN_alop),
alot = c(N0_CN_alot, N0_TN_alot),
synt = c(N0_CN_synt, N0_TN_synt)
)
<- list(
a12.s alop = a12_alop,
alot = a12_alot,
synt = a12_synt
)
<- list(
a21.s alop = a21_alop,
alot = a21_alot,
synt = a21_synt
)
for (condition in c("alop", "alot", "synt")) {
<- N0.s[[condition]]
N0 <- a12.s[[condition]]
a12 <- a21.s[[condition]]
a21
<- c(1:(max(nightingales_agg$year) - min(nightingales_agg$year) + 1))
times
<- deSolve::ode(y = N0, times = times, func = interspec_dens_dep_growth, parms = parms, maxsteps = 1e5)
out
<- out %>%
ode_df as.data.frame() %>%
rename(time = 1, V1 = 2, V2 = 3)
<- ggplot(data = ode_df, aes(x = time)) +
plot geom_line(aes(y = V1, colour = "CN")) +
geom_line(aes(y = V2, colour = "TN")) +
labs(title = condition, x = "Years", y = "Population Size", colour = "Species")
print(plot)
}
What are your observations about interspecific competition in these populations from our model?
Based on our model, what do you think the status of each population will be in 2124?
for (condition in c("alop", "alot", "synt")) {
<- N0.s[[condition]]
N0 <- a12.s[[condition]]
a12 <- a21.s[[condition]]
a21
<- c(1:100)
times
<- deSolve::ode(y = N0, times = times, func = interspec_dens_dep_growth, parms = parms, maxsteps = 1e5)
out
<- out %>%
ode_df as.data.frame() %>%
rename(time = 1, V1 = 2, V2 = 3)
<- ggplot(data = ode_df, aes(x = time)) +
plot geom_line(aes(y = V1, colour = "CN")) +
geom_line(aes(y = V2, colour = "TN")) +
labs(title = condition, x = "Years", y = "Population Size", colour = "Species")
print(plot)
}
Zero Growth Isoclines
<- nightingales_disag %>%
nightingales_isocline filter(co_occurrence_status == "synt") %>% # Only selecting "synt" so that interspecies dynamics are strongest. If your dataset is not separated into alopatric/sympatric samples, you can skip this step
group_by(plot, year, species) %>%
summarise(count = sum(count)) %>%
spread(key = species, value = count, fill = 0) %>%
ungroup()
`summarise()` has grouped output by 'plot', 'year'. You can override using the
`.groups` argument.
head(nightingales_isocline)
# A tibble: 6 × 4
plot year CN TN
<int> <int> <dbl> <dbl>
1 1 2010 1 3
2 13 2003 2 1
3 15 2007 2 0
4 15 2008 2 1
5 15 2009 1 1
6 15 2010 3 0
ggplot(nightingales_isocline, aes(x = CN, y = TN)) +
geom_point() +
geom_smooth(method = "lm", color = "red", se = FALSE) + # CN x TN
geom_smooth(aes(x = TN, y = CN), method = "lm", color = "blue", se = FALSE) + # TN x CN
labs(title = "Zero Growth Isoclines for CN and TN",
x = "CN",
y = "TN")
`geom_smooth()` using formula = 'y ~ x'
`geom_smooth()` using formula = 'y ~ x'
From this model, which species do you think is the dominant competitor? Do you think there is an equilibrium point?
- It appear like there is an equilibrium point around 5 CN: 1 TN. Interspecific dynamics would push populations towards this equilibrium point. From this plot we could also calculate estimated
K
values from the intercepts.