Activity patterns and temporal overlap

R
circular statistics
camera traps
ecology tutorial
ggplot2
Camera-trap activity times as circular data: von Mises kernel density estimation, the coefficient of overlap for temporal niche partitioning, and a bootstrap interval in base R.
Author

Tidy Ecology

Published

2026-08-18

Camera traps record when animals are active, and time of day is circular: a detection at 23:30 and one at 00:30 are an hour apart, not twenty-three. Once times are wrapped onto the circle, two questions follow naturally. What does a species’ daily activity look like, and how much do two species overlap in time, a common proxy for competition, predator avoidance or temporal niche partitioning. This post estimates activity curves with a circular kernel density and measures overlap with the coefficient of overlap, all in base R, and then is honest about how much that overlap number can be trusted.

From clock time to angles

Map a time of day in hours to an angle by stretching the 24-hour day around the circle. Midnight sits at the top, noon at the bottom.

hour2rad <- function(h) 2 * pi * h / 24
rad2hour <- function(r) (r %% (2 * pi)) * 24 / (2 * pi)

# two synthetic species: A nocturnal (one peak at midnight), B crepuscular (dawn and dusk peaks)
set.seed(4252)
A <- rvm(120, mu = hour2rad(0), kappa = 2)                                   # 120 detections
B <- rmix(100, mus = hour2rad(c(6, 18)), kappas = c(4, 4), w = c(0.5, 0.5))  # 100 detections

Species A is nocturnal, so its detections cluster around midnight with a mean resultant length of 0.68 and a mean activity time near 00:21. Species B is crepuscular, with peaks at dawn and dusk, and here the tools from the earlier posts earn their keep: its mean resultant length is only 0.05, because the two opposite peaks cancel. A single mean direction would be meaningless for B, and the Rayleigh test would call it uniform. The activity pattern is not a summary statistic; it is a whole curve, and that is what a density estimate provides.

A circular kernel density

The circular analogue of a smoothed histogram places a small von Mises bump on each observation and averages them. The concentration of that bump is the bandwidth: a large concentration gives a wiggly curve that chases every detection, a small one gives a smooth curve that can wash out real peaks.

dvm  <- function(x, mu, kappa) exp(kappa * cos(x - mu)) / (2 * pi * besselI(kappa, 0))

ckde <- function(eval, data, kappa_bw) {           # von Mises kernel density
  sapply(eval, function(e) mean(dvm(e, data, kappa_bw)))
}

kappa_bw <- 20                                     # smoothing concentration (about a one-hour kernel)
grid <- seq(0, 2 * pi, length.out = 512)
fA <- ckde(grid, A, kappa_bw)
fB <- ckde(grid, B, kappa_bw)

The coefficient of overlap

Temporal overlap between two activity curves is the area under the lower of the two densities: where both species are active, they overlap; where only one is, they do not. The coefficient of overlap runs from zero (no shared activity time) to one (identical activity). The natural estimator integrates the minimum of the two kernel densities over the circle (Ridout and Linkie 2009).

overlap_D4 <- function(A, B, kappa_bw, ngrid = 512) {   # grid estimator, for larger samples
  g <- seq(0, 2 * pi, length.out = ngrid + 1)[-(ngrid + 1)]; dg <- 2 * pi / ngrid
  sum(pmin(ckde(g, A, kappa_bw), ckde(g, B, kappa_bw))) * dg
}
overlap_D1 <- function(A, B, kappa_bw) {                # data-point estimator, for small samples
  0.5 * (mean(pmin(1, ckde(A, B, kappa_bw) / ckde(A, A, kappa_bw))) +
         mean(pmin(1, ckde(B, A, kappa_bw) / ckde(B, B, kappa_bw))))
}

D4 <- overlap_D4(A, B, kappa_bw)
D1 <- overlap_D1(A, B, kappa_bw)
round(c(Dhat4 = D4, Dhat1 = D1), 3)
Dhat4 Dhat1 
0.459 0.449 

The grid estimator gives an overlap of 0.46, and the data-point estimator, which Ridout and Linkie recommend when the smaller sample is below about fifty, gives 0.45. The two agree closely here because both samples are reasonably large. An overlap near one half says these species share roughly half of their active time, which fits the picture: the nocturnal species is active through the dusk and dawn shoulders where the crepuscular species peaks.

Left panel is a 24-hour clock with green points bunched at the top near midnight and amber points at the six and eighteen o'clock positions. Right panel plots two activity density curves against hour of day; the green curve peaks at midnight, the amber curve has two peaks at dawn and dusk, and the overlap between them is shaded grey.
Figure 1: Left: detection times on a 24-hour clock, nocturnal species A in green clustered at midnight, crepuscular species B in amber at dawn and dusk. Right: the two kernel density estimates unrolled onto a linear day, with the shared area (the coefficient of overlap) shaded.

A bootstrap interval

A single overlap number needs an interval. The activity times are the sampling unit, so resample detections within each species with replacement, recompute the overlap, and take the percentiles of the bootstrap distribution.

set.seed(11)
Dboot <- replicate(2000, {
  a <- sample(A, replace = TRUE); b <- sample(B, replace = TRUE)
  overlap_D4(a, b, kappa_bw, ngrid = 256)
})
ci <- quantile(Dboot, c(0.025, 0.975))
round(c(estimate = D4, lower = ci[[1]], upper = ci[[2]]), 3)
estimate    lower    upper 
   0.459    0.362    0.534 

The bootstrap gives a 95 per cent interval of 0.36 to 0.53, a width of 0.17. That is wide: on the evidence here, the shared activity time could be anywhere from a third to a half. Overlap coefficients are noisy, and reporting the point estimate alone would overstate what a couple of hundred detections can pin down.

What the overlap number hides

Two cautions travel with any overlap estimate, and both are choices rather than facts about the animals. The first is the bandwidth. The same data give a range of answers depending on how much the densities are smoothed:

sapply(c(5, 10, 20, 40, 80), function(k) round(overlap_D4(A, B, k), 3))
[1] 0.545 0.488 0.459 0.443 0.431

Smoothing harder, with a smaller kernel concentration, inflates the overlap, because two broad curves share more area than two sharp ones. The estimate swings from about 0.43 to 0.54 across sensible bandwidths, and there is no bandwidth that is objectively correct. The second caution is sample size: the overlap estimator is biased, usually upward, and the bias is worst when detections are few. With small camera-trap samples the reported overlap can sit well above the truth even before the wide interval is taken into account.

Left panel is a histogram of bootstrap overlap estimates centred near 0.45, spanning roughly 0.35 to 0.55, with dashed lines marking the interval. Right panel plots the overlap estimate against kernel concentration, a curve falling from about 0.55 at concentration five to about 0.43 at concentration eighty.
Figure 2: Left: the bootstrap distribution of the overlap estimate, with the point estimate (solid) and the 95 per cent interval (dashed). Right: the same overlap estimate as a function of the smoothing bandwidth; harder smoothing (smaller concentration) inflates the overlap, and no single value is objectively right.

Both cautions are quantified in the checking post that closes this series, which simulates the bias and the bandwidth sensitivity against a known truth. The short version: an overlap coefficient is a useful, comparable summary, but it is a smoothed estimate with a real bias and a wide interval, not a precise measurement of shared time.

References

Batschelet E 1981 Circular Statistics in Biology. Academic Press. ISBN 978-0-12-081050-9.

Fisher NI 1993 Statistical Analysis of Circular Data. Cambridge University Press. ISBN 978-0-521-56890-6.

Pewsey A, Neuhauser M, Ruxton GD 2013 Circular Statistics in R. Oxford University Press. ISBN 978-0-19-967113-7.

Ridout MS, Linkie M 2009 Journal of Agricultural, Biological, and Environmental Statistics 14(3):322-337 (10.1198/jabes.2009.08038).

Linkie M, Ridout MS 2011 Journal of Zoology 284(3):224-229 (10.1111/j.1469-7998.2011.00801.x).

Rowcliffe JM, Kays R, Kranstauber B, Carbone C, Jansen PA 2014 Methods in Ecology and Evolution 5(11):1170-1179 (10.1111/2041-210X.12278).

Taylor CC 2008 Computational Statistics and Data Analysis 52(7):3493-3500 (10.1016/j.csda.2007.11.003).

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.