Phenological trends and temperature

R
phenology
climate change
ggplot2
ecology tutorial
Estimate phenological sensitivity to temperature in R, and see how rising effort in warm years inflates the first-date trend while the mean date holds.
Author

Tidy Ecology

Published

2026-07-22

The flagship result of phenological research is a sensitivity: a plant flowers so many days earlier per degree of spring warming, or a species advances so many days per decade. These numbers are estimated by regressing a timing statistic on temperature or on year. The previous tutorial showed that the first date advances with monitoring effort on its own. This post follows that bias into the trend, because effort is rarely constant across years, and shows how it inflates the sensitivity estimated from first dates while leaving the mean date untouched.

A monitored population across warm and cold years

Here is a thirty-year record. Each year has a standardised spring temperature anomaly, and the true mean flowering date advances by three days for every degree of warming. That is the truth we will try to recover. The catch is that effort covaries with temperature: warm springs bring people outdoors, so warm years are watched by more recorders than cold years.

set.seed(303)
Y <- 30
beta_true <- 3                                   # true days earlier per degree
sdev <- 12
temp <- rnorm(Y, 0, 1)                            # standardised spring temperature
mean_true <- 130 - beta_true * temp               # true mean date each year
n_year <- round(exp(3.0 + 0.9 * temp))            # warm years -> more recorders

first_obs <- mean_obs <- numeric(Y)
for (y in 1:Y) {
  x <- draw <- round(rnorm(n_year[y], mean_true[y], sdev))
  first_obs[y] <- min(x); mean_obs[y] <- mean(x)
}
range(n_year)
[1]   6 161

Effort ranges from 6 plants in the coldest year to 161 in the warmest. That is the confound in a nutshell: temperature drives both the flowering date and the number of eyes on the population.

Two sensitivities from the same data

Now regress each timing statistic on temperature. The slope is the estimated sensitivity in days per degree.

fit_mean  <- lm(mean_obs  ~ temp)
fit_first <- lm(first_obs ~ temp)
s_mean  <- coef(fit_mean)[2];  ci_mean  <- confint(fit_mean)[2, ]
s_first <- coef(fit_first)[2]; ci_first <- confint(fit_first)[2, ]
rbind(mean_date  = c(slope = s_mean,  ci_mean),
      first_date = c(slope = s_first, ci_first))
           slope.temp      2.5 %    97.5 %
mean_date   -2.507621  -3.877902 -1.137340
first_date  -7.521069 -10.047079 -4.995059

The mean-date slope is -2.51 days per degree. It is a noisy estimate, as thirty years always will be, but it is unbiased: its confidence interval (-3.88 to -1.14) comfortably contains the true value of -3. The first-date slope is -7.52 days per degree. This is not merely noisier. Its interval (-10.05 to -5.00) excludes the truth entirely, so the first date does not just add scatter, it points confidently to the wrong answer. It reports the population advancing 3 times faster than the mean date does, and 2.5 times faster than the truth.

plotdf <- rbind(
  data.frame(temp = temp, day = mean_obs,  metric = "mean date"),
  data.frame(temp = temp, day = first_obs, metric = "first date"))
ggplot(plotdf, aes(temp, day, colour = metric)) +
  geom_point(size = 2.2, alpha = 0.8) +
  geom_smooth(method = "lm", se = FALSE, linewidth = 0.9, formula = y ~ x) +
  scale_colour_manual(values = c("mean date" = forest, "first date" = terra)) +
  labs(x = "standardised spring temperature", y = "day of year", colour = NULL)
A scatter of day of year against standardised spring temperature. Mean-date points scatter around a gentle downward line; first-date points scatter lower with a much steeper downward line.
Figure 1: Sensitivity of two timing statistics to spring temperature. The mean-date regression recovers the true three-day-per-degree advance; the first-date regression is far steeper because warm years are also better monitored.

Proving the confound: hold effort constant

Is the first date inflated because it is a first date, or because effort tracked temperature? Rerun the same years and the same true dates, but monitor a constant 30 plants every year, warm or cold.

set.seed(404)
first_const <- numeric(Y)
for (y in 1:Y) first_const[y] <- min(round(rnorm(30, mean_true[y], sdev)))
s_const <- coef(lm(first_const ~ temp))[2]
round(s_const, 2)
 temp 
-3.06 

With effort fixed, the first-date slope is -3.06 days per degree, right back on the true value of -3. The bias was never a property of the first date on its own; it was the interaction between the first date and effort that changed alongside temperature. Remove the changing effort and the bias disappears.

cdf <- rbind(
  data.frame(temp = temp, day = first_obs,   effort = "effort tracks warmth"),
  data.frame(temp = temp, day = first_const, effort = "effort held constant"))
ggplot(cdf, aes(temp, day, colour = effort, linetype = effort)) +
  geom_point(size = 2.2, alpha = 0.85) +
  geom_smooth(method = "lm", se = FALSE, linewidth = 0.9, formula = y ~ x) +
  scale_colour_manual(values = c("effort tracks warmth" = terra,
                                 "effort held constant" = forest)) +
  scale_linetype_manual(values = c("effort tracks warmth" = 2,
                                    "effort held constant" = 1)) +
  labs(x = "standardised spring temperature", y = "first observed date", colour = NULL,
       linetype = NULL)
First observed date against standardised spring temperature. Open points with a steep dashed line show the confounded case; filled points with a shallower solid line show constant effort recovering the true slope.
Figure 2: The first date against temperature under two effort regimes. When effort rises in warm years (open points, dashed fit) the slope is steep; with constant effort (solid points, solid fit) it recovers the true sensitivity.

The honest boundary

Temperature sensitivities and long-term trends are among the most cited numbers in climate-change ecology. Sparks and colleagues related flowering times to Central England temperature over half a century, Menzel and colleagues mapped a coherent European advance, and Parmesan and Yohe assembled these into a global fingerprint. All of that rests on the timing statistic being well behaved. A first-date sensitivity is not, whenever effort and the driver move together, which they usually do: warmer decades are also more heavily recorded decades. A mean date, a median date, or a percentile date sidesteps the trap, because those summaries do not chase the sample size. The number to distrust is not the trend itself but the statistic it was built from.

Where to go next

Estimating a phenological trend involves several assumptions that can quietly fail: effort, the survey window, and the choice of summary. The final tutorial turns each of those into a concrete diagnostic you can run on your own data before you trust a reported shift.

References

Sparks TH, Jeffree EP, Jeffree CE 2000. International Journal of Biometeorology 44(2):82-87 (10.1007/s004840000049)

van Strien AJ, Plantenga WF, Soldaat LL, van Swaay CAM, WallisDeVries MF 2008. Oecologia 156(1):227-235 (10.1007/s00442-008-0959-4)

Parmesan C, Yohe G 2003. Nature 421(6918):37-42 (10.1038/nature01286)

Newsletter

Get new tutorials by email

New R and QGIS tutorials for ecologists, straight to your inbox. No spam; unsubscribe anytime.

By subscribing you agree to receive these emails and confirm your address once. See the privacy policy.