Checking a source-sink analysis

ecology tutorial
R
population ecology
model diagnostics
conservation
Three checks for a source-sink study in R: why counts mislead, separating local recruits from immigrants, and defining a source by contribution not just growth.
Author

Tidy Ecology

Published

2026-07-02

Modified

2026-10-07

Corrected 7 October 2026: the post said Runge et al. formalised the ranking by total contribution and defined a habitat’s role through reproductive value; their metric is a per-capita contribution that counts surviving emigrants, and the text now says so and that it is the weighting by habitat size that puts source B ahead.

Corrected 27 September 2026: the post described the immigration figure as a sink held at a fixed size, but a sink of fixed size shows a count ratio of exactly one. It now says what the figure computes: one year’s count ratio for a sink that currently holds 300 animals. The alt text of the first figure also said it showed differing birth and death rates; it shows only the two equal density bars, and the alt text now says so.

Setup: packages, colours and plot theme
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 = "#f5f4ee", colour = NA),
        plot.background  = element_rect(fill = "#f5f4ee", colour = NA),
        panel.grid.minor = element_blank(),
        plot.title = element_text(face = "bold", size = 12))
theme_set(theme_te)
col_source <- "#3a6b52"; col_sink <- "#b5651d"; col_ref <- "#555555"

l_local <- 0.84; imm <- 48
n_sink <- imm/(1 - l_local)
app_lambda <- (n_sink*l_local + imm)/n_sink      # apparent growth from counts at equilibrium

Everything in this series has been a warning that a sink does not look like a sink. It can be crowded, it can be preferred, it can be excellent habitat having a bad year. So how do you actually tell a source from a sink? Not from the things easiest to measure. These three checks are about what to measure instead, and each rules out a tempting shortcut.

Check 1: counts and occupancy cannot classify a habitat

The first check is really a prohibition. Source and sink are defined by the local balance of births and deaths, and that balance is invisible in a headcount. Two habitats can hold identical numbers, be equally occupied year after year, and be demographic opposites.

source_hab <- c(density = 120, births = 0.9, deaths = 0.5)   # births > deaths: a source
sink_hab   <- c(density = 120, births = 0.4, deaths = 0.8)   # deaths > births: a sink
rbind(source = source_hab, sink = sink_hab)
       density births deaths
source     120    0.9    0.5
sink       120    0.4    0.8

Both habitats hold 120 animals. The first has births outrunning deaths and is a source; the second has deaths outrunning births and is a sink held up by immigration. A survey that records density, or presence and absence, sees two identical habitats. Occupancy models, distribution maps, and abundance counts, the workhorses of monitoring, are all blind to the distinction, because the distinction lives in the vital rates, not the numbers.

d <- data.frame(habitat = c("source", "sink"), density = c(120, 120), kind = c("source", "sink"))
ggplot(d, aes(habitat, density, fill = kind)) +
  geom_col(width = 0.55) +
  scale_fill_manual(values = c("source" = col_source, "sink" = col_sink), guide = "none") +
  labs(x = NULL, y = "Density (identical)", title = "Same count, opposite demography")
Two equal-height density bars at 120, an orange one labelled sink and a green one labelled source; no birth or death rates are drawn, so nothing in the chart tells the two habitats apart.
Figure 1: Two habitats with the same density and occupancy, one a source and one a sink. A count cannot separate them; only the birth and death rates can.

Check 2: are the recruits local, or immigrants?

Suppose you do measure vital rates by counting young. A subtler trap waits: the young and the newcomers in a habitat are a mix of locally-produced individuals and immigrants, and if you credit them all to local reproduction you inflate the local growth rate and can promote a sink to a source. The cleanest way to see it is to compute growth the way a monitoring programme would, from year-to-year counts, at a sink sitting at its stable equilibrium.

l_local <- 0.84; imm <- 48                 # true local growth rate, and annual immigrants
n_sink  <- imm/(1 - l_local)               # equilibrium sink size
apparent <- (n_sink*l_local + imm)/n_sink  # growth inferred from total counts
c(true_local_lambda = l_local, apparent_lambda_from_counts = apparent, sink_size = n_sink)
          true_local_lambda apparent_lambda_from_counts 
                       0.84                        1.00 
                  sink_size 
                     300.00 

The true local growth rate is 0.84: a sink. But at equilibrium the counts are steady, so growth inferred from the total, next year’s count over this year’s, is 1.00, exactly one. A monitoring programme watching the numbers would conclude the population is stable and self-sustaining, when in fact it is a sink kept level by 48 immigrants a year. Only by marking individuals, or using natal-origin markers like genetics or isotopes, can you separate the animals this habitat produced from the ones it merely received, and only that separation gives the local growth rate.

The size of the illusion grows with the subsidy. For a sink that currently holds 300 animals, the more immigrants arrive, the higher the growth rate a count-based analysis reports, until it climbs past one and the sink is misread as a thriving source.

N0 <- 300
di <- data.frame(imm = seq(0, 120, 2))
di$apparent <- (N0*l_local + di$imm)/N0
ggplot(di, aes(imm, apparent)) +
  geom_hline(yintercept = 1, colour = col_ref, linetype = "dotted") +
  geom_hline(yintercept = l_local, colour = col_sink, linetype = "dashed") +
  geom_line(colour = col_source, linewidth = 1) +
  annotate("text", x = 2, y = l_local - 0.03, label = "true local rate", hjust = 0, colour = col_sink, size = 3.4) +
  labs(x = "Immigrants per year", y = "Growth rate inferred from counts",
       title = "Immigration inflates the apparent local growth")
A rising line of count-based growth rate against immigration, crossing one, while a dashed line marks the constant true local growth rate below one.
Figure 2: Growth rate inferred from counts, against the number of immigrants arriving at a sink of 300 animals, over one year. The true local rate (dashed) stays below one; the count-based rate climbs through one as immigration rises.

Check 3: is “source” the same as “important”?

The last check is conceptual. It is tempting to define a source as any habitat with a local growth rate above one and stop there, but that conflates self-sufficiency with contribution. What a habitat is worth to the wider landscape is the surplus it actually sends out that recruits elsewhere, and two self-sufficient habitats can differ enormously in that.

# two sources, both self-sufficient (lambda > 1), very different net contributions
sA <- c(lambda = 1.20, size = 40);  sB <- c(lambda = 1.04, size = 400)
c(A_emigrants = unname(sA["size"]*(sA["lambda"]-1)), B_emigrants = unname(sB["size"]*(sB["lambda"]-1)))
A_emigrants B_emigrants 
          8          16 

Source A has the higher local growth rate, 1.20, and by the simple definition is the “better” source. But it is small, so it exports 8 surplus animals a year. Source B grows only just above replacement yet is large, and exports 16, twice as many. Ranked by per-capita growth, A wins; ranked by contribution to the landscape, B wins. Runge et al. 2006 The American Naturalist 167(6):925-938 made the related point that a habitat’s role should be judged by its per-capita contribution to the wider population, counting emigrants that survive elsewhere, rather than by local survival and recruitment alone; weighting that contribution by habitat size, as here, is what puts B ahead of A. For deciding where conservation effort goes, contribution is the currency, not the local rate.

What a real classification needs

The through-line of all three checks is that source-sink status is a demographic claim, and demographic claims need demographic data: local survival and fecundity, partitioned from immigration by marked individuals, and read as contribution to the landscape rather than as a local rate in isolation. That is why the question is usually answered with capture-recapture and integrated population models, not with the abundance surveys that first raised it. When a study labels a habitat a source or a sink from counts, occupancy, or density alone, it has not measured the thing the label names.

References

  • Pulliam HR 1988. The American Naturalist 132(5):652-661 (10.1086/284880).
  • Runge JP, Runge MC, Nichols JD 2006. The American Naturalist 167(6):925-938 (10.1086/503531).

Newsletter

Get updates by email

An occasional email when tutorials are added or substantially corrected. No spam; unsubscribe anytime.

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