Imagine that we arrive at a lake and count the water lilies.
Returning the next day we observe a 10% increase. Our goal is:
How, starting from this simple observation, do we arrive at the exponential growth formula by way of a differential equation?
The journey has three stops:
Over a time \(\Delta t = 1\) day, the change in the population is:
\[ \Delta N \;=\; N_1 - N_0 \;=\; 110 - 100 \;=\; 10 \]
We are not interested in the absolute change (10 water lilies), because it depends on how many water lilies we had at the start. We are interested in how much the population changed per individual, per unit of time:
\[ r \;=\; \frac{\Delta N}{N \cdot \Delta t} \;=\; \frac{10}{100 \cdot 1} \;=\; 0.1 \;=\; 10\%/\text{day} \]
\(r\) is called the intrinsic rate of increase of the population. It is the fundamental parameter of all classical population dynamics.
## [1] 0.1
If we assume that this rate stays constant, then at every time step \(\Delta t\):
\[ \Delta N \;=\; r \, N \, \Delta t \quad\Longleftrightarrow\quad \frac{\Delta N}{\Delta t} \;=\; r N \]
| N | ΔΝ/Δt |
|---|---|
| 100 | 10 |
| 200 | 20 |
| 1000 | 100 |
| 5000 | 500 |
This is a discrete approximation: we measure the population at discrete steps (every day).
But the water lilies do not wait for dawn to grow — they grow continuously. Taking the limit as the time step becomes infinitesimal:
\[ \lim_{\Delta t \to 0} \frac{\Delta N}{\Delta t} \;=\; \frac{dN}{dt} \]
We arrive at the differential equation of exponential growth:
\[ \boxed{\;\frac{dN}{dt} \;=\; r\,N\;} \]
Intuitive interpretation: The rate of change of the population at every moment is proportional to the current size of the population. The more water lilies there are, the faster new ones appear.
We will solve \(\frac{dN}{dt} = rN\) using the method of separation of variables.
\[ \frac{dN}{N} \;=\; r\,dt \]
(All the \(N\) on the left-hand side, all the \(t\) on the right.)
\[ \int \frac{dN}{N} \;=\; \int r\,dt \quad\Longrightarrow\quad \ln(N) \;=\; r\,t + C \]
where \(C\) is a constant of integration.
\[ N(t) \;=\; e^{rt + C} \;=\; e^{C} \cdot e^{rt} \]
At \(t = 0\) we have \(N(0) = N_0\), so:
\[ N_0 \;=\; e^{C} \cdot e^{0} \;=\; e^{C} \quad\Longrightarrow\quad e^{C} = N_0 \]
\[ \boxed{\;N(t) \;=\; N_0\, e^{r t}\;} \]
This is the exponential growth formula.
Let’s plot \(N(t) = 100 \cdot e^{0.1\,t}\) for 60 days:
N0 <- 100
r <- 0.1
t <- seq(0, 60, by = 0.1)
N_analytical <- N0 * exp(r * t)
df_analytical <- data.frame(t = t, N = N_analytical)
ggplot(df_analytical, aes(x = t, y = N)) +
geom_line(linewidth = 1, colour = "steelblue") +
labs(
title = "Exponential growth of the water lily population",
subtitle = expression(paste(N(t) == N[0] * e^{r*t}, ", ",
N[0] == 100, ", ", r == 0.1)),
x = "Time (days)",
y = "Population N(t)"
)In 60 days the population reaches 4.0343^{4} water lilies — unrealistic for a finite lake! This is the problem of exponential growth that we will address later with the logistic model.
deSolveVery often in biology differential equations cannot be solved analytically. In those cases we use numerical integrators. For this particular model, which we already know analytically, this gives us an opportunity to check whether the numerical method agrees.
# 1. Define the differential equation as a function
exponential_model <- function(t, state, parameters) {
with(as.list(c(state, parameters)), {
dN <- r * N # dN/dt = rN
list(dN)
})
}
# 2. Parameters, initial condition, time
parameters <- c(r = 0.1)
state <- c(N = 100)
times <- seq(0, 60, by = 0.1)
# 3. Solve
out <- ode(y = state, times = times, func = exponential_model, parms = parameters)
out_df <- as.data.frame(out)
head(out_df)df_compare <- data.frame(
t = times,
Analytical = N0 * exp(r * times),
Numerical = out_df$N
)
ggplot(df_compare, aes(x = t)) +
geom_line(aes(y = Analytical), linewidth = 1.2, colour = "steelblue") +
geom_point(aes(y = Numerical),
data = df_compare[seq(1, nrow(df_compare), by = 50), ],
colour = "tomato", size = 2) +
labs(
title = "Analytical (line) versus numerical (dots) solution",
x = "Time (days)",
y = "Population N(t)"
)## [1] 0.008490642
The error is negligible — the two solutions coincide.
Notice something: we started by saying “10% increase per day”. That is a discrete statement. But then we wrote \(r = 0.1\) inside a continuous differential equation. Are the two really equal?
If every day the population is multiplied by \(1.10\):
\[ N(t) \;=\; N_0 \cdot (1+r)^{t} \;=\; 100 \cdot 1.1^{t} \]
days <- 0:30
N_discrete <- 100 * (1 + 0.1)^days
N_continuous <- 100 * exp(0.1 * days)
df_dc <- data.frame(
day = rep(days, 2),
N = c(N_discrete, N_continuous),
model = factor(rep(c("Discrete N0·(1+r)^t",
"Continuous N0·exp(rt)"), each = length(days)))
)
ggplot(df_dc, aes(x = day, y = N, colour = model)) +
geom_line(linewidth = 1) +
geom_point(size = 1.5) +
labs(
title = "Discrete versus continuous exponential growth",
subtitle = "With the same value r = 0.1 the two models do **not** coincide",
x = "Days", y = "Population", colour = ""
) +
theme(legend.position = "bottom")data.frame(
Day = c(1, 5, 10, 20, 30),
Discrete = round(100 * 1.1^c(1, 5, 10, 20, 30), 1),
Continuous = round(100 * exp(0.1 * c(1, 5, 10, 20, 30)), 1)
)In the discrete model, “10% increase/day” means: at the end of the day the population is 110% of the initial one.
In the continuous model with \(r = 0.1\), the instantaneous rate is 10%, but because the newly born water lilies immediately produce new water lilies themselves (continuous compounding!), at the end of the day we have:
\[ N(1) \;=\; 100 \cdot e^{0.1} \;\approx\; 110.52 \]
that is, a little more than a 10% increase.
If we want the continuous model to give exactly a 10% increase per day, we need:
\[ e^{r_{\text{cont}}} = 1.1 \quad\Longrightarrow\quad r_{\text{cont}} \;=\; \ln(1.1) \;\approx\; 0.0953 \]
## [1] 0.09531018
In ecological practice, most of the time we work with the continuous model and take \(r\) to be the instantaneous rate. The difference is small for small \(r\), but it is good to know that it exists.
Taking the logarithm of the formula \(N(t) = N_0 e^{rt}\):
\[ \ln N(t) \;=\; \ln N_0 \;+\; r\,t \]
That is, \(\ln N\) is a linear function of \(t\) with slope equal to \(r\). This is extremely useful in practice:
If your data on a semi-log plot (\(\ln N\) against \(t\)) form a straight line, then the population is growing exponentially, and the slope of the line gives you \(r\).
ggplot(df_analytical, aes(x = t, y = N)) +
geom_line(linewidth = 1, colour = "steelblue") +
scale_y_log10() +
labs(
title = "Exponential growth on a semi-log axis",
subtitle = "The straight line indicates exponential growth",
x = "Time (days)",
y = "log10(N(t))"
)# Simulated data with a little noise
set.seed(42)
sim_days <- 0:30
sim_N <- 100 * exp(0.1 * sim_days) * exp(rnorm(length(sim_days), 0, 0.05))
# Linear regression log(N) ~ t
fit <- lm(log(sim_N) ~ sim_days)
summary(fit)$coefficients## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.63477789 0.021337509 217.21270 3.966003e-48
## sim_days 0.09829637 0.001221782 80.45327 1.212714e-35
## Estimated r = 0.0983 (true r = 0.1)
## Estimated N0 = 103.01 (true N0 = 100)
A useful quantity: how long does it take for the population to double?
\[ 2 N_0 = N_0 e^{r T_2} \;\Longleftrightarrow\; T_2 = \frac{\ln 2}{r} \]
## Doubling time of the water lilies: 6.93 days
So the water lilies double roughly every 7 days.
Play with \(r\). Plot on the same graph the exponential growth for \(r = 0.05\), \(r = 0.1\) and \(r = 0.2\). How does the doubling time change?
Negative \(r\). What happens if \(r < 0\)? Plot the case \(r = -0.05\) and interpret it biologically.
Estimation from real data. You are given the following measurements:
Estimate \(r\) with linear regression on \(\ln N\) and predict the population at 60 days.
The mathematical danger. With \(r = 0.1\) and \(N_0 = 100\) water lilies, in how many days will the water lilies exceed the number of humans on Earth (\(\approx 8 \times 10^{9}\))? What does this tell us about the limitations of the exponential model?
The exponential model cannot hold forever. In a real lake there are constraints: space, light, nutrients. At some point the population will saturate.
In the next lesson we will modify the differential equation so that it includes the carrying capacity \(K\) of the environment:
\[ \frac{dN}{dt} \;=\; r N \left(1 - \frac{N}{K}\right) \]
This is the logistic equation — and it is the subject of the next laboratory.
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0 LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Europe/Athens
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] deSolve_1.42 ggplot2_4.0.3
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.7.3 cli_3.6.6 knitr_1.51 rlang_1.2.0
## [5] xfun_0.57 otel_0.2.0 S7_0.2.2 jsonlite_2.0.0
## [9] labeling_0.4.3 glue_1.8.1 htmltools_0.5.9 sass_0.4.10
## [13] scales_1.4.0 rmarkdown_2.31 grid_4.6.1 evaluate_1.0.5
## [17] jquerylib_0.1.4 fastmap_1.2.0 yaml_2.3.12 lifecycle_1.0.5
## [21] compiler_4.6.1 RColorBrewer_1.1-3 farver_2.1.2 digest_0.6.39
## [25] R6_2.6.1 bslib_0.10.0 tools_4.6.1 withr_3.0.2
## [29] gtable_0.3.6 cachem_1.1.0