1 The problem: a lake with water lilies

Imagine that we arrive at a lake and count the water lilies.

  • Day 0: \(N_0 = 100\) water lilies
  • Day 1: \(N_1 = 110\) 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:

  1. Observation → rate of change (discrete thinking)
  2. Discrete → continuous (we take the limit and write the differential equation \(\frac{dN}{dt} = rN\))
  3. Solving the differential equation (separation of variables → \(N(t) = N_0 e^{rt}\))

2 From the observation to the rate of change

2.1 The change in the population

Over a time \(\Delta t = 1\) day, the change in the population is:

\[ \Delta N \;=\; N_1 - N_0 \;=\; 110 - 100 \;=\; 10 \]

2.2 The per capita rate \(r\)

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.

# Computing r from the data
N0 <- 100
N1 <- 110
dt <- 1   # days

r <- (N1 - N0) / (N0 * dt)
r
## [1] 0.1

3 From the discrete to the continuous

3.1 Discrete equation

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 \]

3.1.1 example values for r=0.1 (10%)

N ΔΝ/Δt
100 10
200 20
1000 100
5000 500

This is a discrete approximation: we measure the population at discrete steps (every day).

3.2 The limit \(\Delta t \to 0\)

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.


4 Solving the differential equation

We will solve \(\frac{dN}{dt} = rN\) using the method of separation of variables.

4.1 Step 1 — Separate the variables

\[ \frac{dN}{N} \;=\; r\,dt \]

(All the \(N\) on the left-hand side, all the \(t\) on the right.)

4.2 Step 2 — Integrate both sides

\[ \int \frac{dN}{N} \;=\; \int r\,dt \quad\Longrightarrow\quad \ln(N) \;=\; r\,t + C \]

where \(C\) is a constant of integration.

4.3 Step 3 — Solve for \(N\)

\[ N(t) \;=\; e^{rt + C} \;=\; e^{C} \cdot e^{rt} \]

4.4 Step 4 — Find the constant from the initial condition

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 \]

4.5 The final formula

\[ \boxed{\;N(t) \;=\; N_0\, e^{r t}\;} \]

This is the exponential growth formula.


5 Implementation in R

5.1 Analytical solution

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.

5.2 Numerical solution with deSolve

Very 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)

5.3 Comparison of the analytical and numerical solutions

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)"
  )

# Maximum error
max(abs(df_compare$Analytical - df_compare$Numerical))
## [1] 0.008490642

The error is negligible — the two solutions coincide.


6 A subtle but very important observation

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?

6.1 The discrete formula

If every day the population is multiplied by \(1.10\):

\[ N(t) \;=\; N_0 \cdot (1+r)^{t} \;=\; 100 \cdot 1.1^{t} \]

6.2 Comparison of the two models

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)
)

6.3 Why do they differ?

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.

6.4 How are they connected?

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 \]

log(1.1)   # in R, log() is the natural logarithm (ln)
## [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.


7 The logarithmic axis: the biologist’s diagnostic tool

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))"
  )

7.1 Estimating \(r\) from data with linear regression

# 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
cat("Estimated r =", round(coef(fit)[2], 4),
    "  (true r = 0.1)\n")
## Estimated r = 0.0983   (true r = 0.1)
cat("Estimated N0 =", round(exp(coef(fit)[1]), 2),
    "  (true N0 = 100)\n")
## Estimated N0 = 103.01   (true N0 = 100)

8 Doubling time

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} \]

T2 <- log(2) / r
cat("Doubling time of the water lilies:", round(T2, 2), "days\n")
## Doubling time of the water lilies: 6.93 days

So the water lilies double roughly every 7 days.


9 Exercises

  1. 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?

  2. Negative \(r\). What happens if \(r < 0\)? Plot the case \(r = -0.05\) and interpret it biologically.

  3. Estimation from real data. You are given the following measurements:

    day <- c(0, 3, 7, 14, 21, 28)
    N   <- c(50, 73, 109, 218, 437, 870)

    Estimate \(r\) with linear regression on \(\ln N\) and predict the population at 60 days.

  4. 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?


10 Next step: the law returns to reality

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.


11 R session information

sessionInfo()
## 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