---
title: "Checking a storage-effect analysis"
description: "Three checks for the storage effect in R: is growth buffered, do species respond differently, and is variability alone being mistaken for coexistence."
date: "2026-08-02 12:00"
categories: [ecology tutorial, R, community ecology, coexistence, model diagnostics]
image: thumbnail.png
image-alt: "The rare-species growth rate collapsing to zero whenever any one of the three storage-effect ingredients is removed."
---
```{r setup}
#| include: false
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE,
dev = "png", dpi = 120, fig.path = "figures/",
fig.width = 7, fig.height = 4.4)
library(ggplot2)
theme_te <- theme_minimal(base_size = 12) +
theme(panel.background = element_rect(fill = "white", colour = NA),
plot.background = element_rect(fill = "white", colour = NA),
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 12))
theme_set(theme_te)
col_yes <- "#3a6b52"; col_no <- "#b5651d"; col_ref <- "#555555"
states <- list(c(3, 0.5), c(0.5, 3)) # differential response (negatively correlated)
states_id <- list(c(3, 3), c(0.5, 0.5)) # identical response (positively correlated)
states_hi <- list(c(6, 6), c(0.25, 0.25)) # larger variance, still identical response
inv_growth <- function(d, st) mean(log(sapply(st, function(s) (1 - d) + d * s[1] / s[2])))
r_buffered <- inv_growth(0.1, states)
r_nobuffer <- inv_growth(1.0, states)
r_identical <- inv_growth(0.1, states_id)
r_hi_ident <- inv_growth(0.1, states_hi)
r_const <- log((1 - 0.1) + 0.1 * 1) # frozen environment, mean ratio 1
```
The storage effect is one of the most cited ideas in modern coexistence theory, and one of the easiest to invoke without earning. A study finds species coexisting in a variable environment, notes that the variability must be helping, and calls it the storage effect. But the mechanism has three specific requirements, and if any one is missing the effect is exactly zero no matter how much the environment fluctuates. These three checks are what separate a real storage effect from a hopeful label.
## Check 1: is population growth actually buffered?
The storage effect needs a life-history stage that carries a good year forward: long-lived adults, a seed bank, a resting egg bank, anything that stops a windfall from being spent all at once. Without it there is nothing to *store*. The clean test is to strip the buffer out and see whether the rare-species advantage survives. In the lottery model the buffer is adult survival, so set the death rate to one, every adult dies each year, generations no longer overlap, and recompute the invasion growth rate.
```{r buffer}
states <- list(c(3, 0.5), c(0.5, 3))
inv_growth <- function(d, st) mean(log(sapply(st, function(s) (1 - d) + d * s[1] / s[2])))
c(buffered_longlived = inv_growth(0.1, states), # adults survive, gains are stored
annual_no_buffer = inv_growth(1.0, states)) # all adults die each year, nothing stored
```
With long-lived adults the rare invader grows at `r sprintf("%+.3f", r_buffered)`; make the population annual with no carry-over and it drops to `r sprintf("%.3f", r_nobuffer)`, exactly zero. The environment is fluctuating just as hard in both cases, and the species still respond differently, but with nothing to store the advantage vanishes. This is why the presence of a seed bank or overlapping generations is not a detail in a storage-effect study; it is the mechanism, and a claim that omits it has not been made.
```{r fig-buffer}
#| fig-cap: "The rare invader's growth rate with and without a buffer. Long-lived adults store good years and the invader grows; strip the buffer to an annual life cycle and the same fluctuations yield nothing."
#| fig-alt: "A two-bar chart of invader growth rate: a positive bar for buffered long-lived adults and a zero bar for an annual no-buffer life cycle."
db <- data.frame(life = c("long-lived\n(buffered)", "annual\n(no buffer)"),
r = c(r_buffered, r_nobuffer))
ggplot(db, aes(life, r, fill = life)) +
geom_col(width = 0.55) +
geom_hline(yintercept = 0, colour = col_ref, linewidth = 0.4) +
scale_fill_manual(values = c("long-lived\n(buffered)" = col_yes, "annual\n(no buffer)" = col_no),
guide = "none") +
labs(x = NULL, y = "Rare invader's growth rate",
title = "No buffer, no storage effect")
```
## Check 2: do the species really respond differently?
The second requirement is that a good year for one species is not a good year for all. If the competitors rise and fall together, tracking the same good and bad years in lockstep, then being rare buys no respite: your good years are also your competitor's, and there is no window in which you alone prosper. Test it by making the environmental responses identical, so the two species' outputs move up and down together instead of in opposition.
```{r response}
states_diff <- list(c(3, 0.5), c(0.5, 3)) # good for one is bad for the other
states_same <- list(c(3, 3), c(0.5, 0.5)) # good and bad years shared by both
c(different_response = inv_growth(0.1, states_diff),
identical_response = inv_growth(0.1, states_same))
```
Differential responses give the familiar `r sprintf("%+.3f", r_buffered)`; identical responses give `r sprintf("%.3f", r_identical)`, zero again. When both species boom and bust on the same schedule the ratio of their outputs never changes, so a rare species gains nothing from the fluctuation. Species-specific responses to the environment are what create the private good years the storage effect banks, and a study that has not shown the competitors respond differently has not shown the mechanism can operate.
## Check 3: is variability itself being mistaken for coexistence?
The most seductive error is to treat environmental variance as if it were coexistence. It is not. Variance is a necessary input, but on its own it does nothing; it has to act through buffered growth and differential responses to become a rare-species advantage. Take the identical-response species and simply crank up the variance.
```{r variance}
states_bigvar <- list(c(6, 6), c(0.25, 0.25)) # much larger swings, but still identical response
c(more_variance_same_response = inv_growth(0.1, states_bigvar),
frozen_environment = r_const)
```
Quadrupling the swings while leaving the responses identical still gives `r sprintf("%.3f", r_hi_ident)`, zero, no better than the `r sprintf("%.3f", r_const)` of a frozen environment. Variability is not a coexistence mechanism; it is fuel that only burns when the other two ingredients are present. This is why the honest way to attribute coexistence to the storage effect is the [invasion partition](../partitioning-invasion-growth/): compute the rare-species growth rate with the environment fluctuating and again with it frozen at the mean, and only the difference between them belongs to the fluctuations. A positive growth rate in a variable environment is not evidence of a storage effect until you have shown it disappears when you switch the variability off.
```{r fig-grid}
#| fig-cap: "The rare-species growth rate under each scenario. It is positive only when growth is buffered and the species respond differently; remove either, or add variance without them, and it collapses to zero."
#| fig-alt: "A four-bar chart of invader growth rate: a positive bar for the full mechanism and zero bars for no buffer, identical response, and high variance with identical response."
dg <- data.frame(
scenario = factor(c("buffered +\ndifferent response", "no buffer", "identical\nresponse",
"high variance,\nidentical response"),
levels = c("buffered +\ndifferent response", "no buffer", "identical\nresponse",
"high variance,\nidentical response")),
r = c(r_buffered, r_nobuffer, r_identical, r_hi_ident))
ggplot(dg, aes(scenario, r, fill = r > 1e-6)) +
geom_col(width = 0.62) +
geom_hline(yintercept = 0, colour = col_ref, linewidth = 0.4) +
scale_fill_manual(values = c("TRUE" = col_yes, "FALSE" = col_no), guide = "none") +
labs(x = NULL, y = "Rare invader's growth rate",
title = "All three ingredients, or nothing") +
theme(axis.text.x = element_text(size = 8))
```
## The weight of the caveats
Three checks, one message: the storage effect is a conjunction, not a vibe. It needs buffered growth, species-specific environmental responses, and enough variance to matter, and the failure of any one sends the rare-species advantage to zero while every surface feature of the story, a variable environment and coexisting species, stays intact. None of this makes the storage effect rare or unimportant; it is very likely a real force in plankton, in desert annuals with seed banks, in coral reef fish, the systems it was built for. It does mean that invoking it is a claim with parts, and the parts are checkable: show the buffer, show the differential response, and show, by freezing the environment, that the coexistence actually depends on the fluctuations. Do those three and the storage effect is earned; skip them and it is only a name for a pattern you have not explained.
## Related tutorials
- [The storage effect and coexistence](../the-storage-effect/)
- [Relative nonlinearity of competition](../relative-nonlinearity-of-competition/)
- [Partitioning the invasion growth rate](../partitioning-invasion-growth/)
- [Bet-hedging and geometric mean fitness](../bet-hedging-and-geometric-mean-fitness/)
## References
- Chesson P 2000. Annual Review of Ecology and Systematics 31:343-366 (10.1146/annurev.ecolsys.31.1.343).
- Chesson PL, Warner RR 1981. The American Naturalist 117(6):923-943 (10.1086/283778).