<- function(
simul n_individuals = 1000,
n_occasions = 3, # release, first sighting, second sighting
n.states = 3, # juvenile, adult, dead
phi = 0.7,
psi = 0.8,
p = 0.3) {
# state matrix
<- matrix(c(
phi_state
0, phi, 1 - phi,
0, psi, 1 - psi,
0, 0, 1
nrow = n.states, byrow = TRUE)
),
# observation matrix
<- matrix(c(
phi_obs
0, 0, 1,
0, p, 1 - p,
0, 0, 1
nrow = n.states, byrow = TRUE)
),
# empty capture history list of matrices
<- ch_true <- matrix(NA,
ch nrow = n_individuals,
ncol = n_occasions)
# always seen at release
1] <- ch_true[, 1] <- 1
ch[,
# fill CH
for (i in 1:n_individuals) {
for (o in 2:n_occasions) {
# state transition
<- rmultinom(n = 1,
state_p size = 1,
prob = phi_state[ch_true[i, o - 1], ])
<- which(state_p == 1)
ch_true[i, o] # observation transition
<- rmultinom(n = 1,
event_p size = 1,
prob = phi_obs[ch_true[i, o], ])
<- which(event_p == 1)
ch[i, o] # o
} # i
}
# return
return(list("ch" = ch, "ch.true" = ch_true))
}
A popular way to estimate life-history rates, such as survival, reproduction and movement rates, is using Cormack Jolly Seber (CJS) models cast in state-space equations. These models vary in complexity, but generally build on two fairly simple equations representing state and observation processes:
\[ z_{i,t} \sim \mbox{Bernoulli}(z_{i,t - 1}, \varphi) \]
\[ y_{i,t} \sim \mbox{Bernoulli}(z_{i,t}, p) \]
The first equation is known as the state process and allows for the estimation of the transition rate, here survival \(\varphi\), from the state \(z\) of individual \(i\) and time \(t\) from their state at time \(t-1\). The second equation is known as the observation process and allows for the estimation of the recapture or resighting rate \(p\) of the individual in state \(z\) at time \(t\).
I’ve used CJS models in a number of capacities (see ), and for each I’ve written my own model in OpenBUGS
, JAGS
, Stan
or nimble
, and used Bayesian inference. However, Bayesian inference relies on sampling the parameter space using Monte-Carlo Markov Chains (MCMC), which can be time-consuming (despite ever-efficient algorithms and computation).
Recently, I attended fantastic Template Model Builder
training given by Anders Nielsen. It is promoted as a fast and flexible language for inference, whether frequentist or Bayesian.
I thought that it might be nice to write a simple CJS model in TMB, following the excellent examples by Olivier Gimenez, and in native R
too.
Here is my try…
Data simulation
We first build a function to simulate data from a multi-event (juvenile and adult) capture-recapture (CJS) model:
Likelihood function in native R
First, we need a small function to protect the log from “exploding” (thanks, Olivier Gimenez):
<- function(v) {
logprot <- 2.2204e-016
eps <- log(eps) * (1 + vector(length = length(v)))
u <- (v > eps)
index <- log(v[index])
u[index] return(u)
}
Then the likelihood (take the time to compare this function to the data simulation function - they’re similar!):
<- function(b, ch) {
cjs
# R version of the TMB function below - not used here
<- function(A, B) {
multvecmat <- nrow(B)
nrowb <- ncol(B)
ncolb <- vector("numeric", length = ncolb)
C for (i in 1:ncolb) {
<- 0
C[i] for (k in 1:nrowb) {
<- C[i] + A[k] * B[k, i]
C[i]
}
}return(C)
}
# parameters
<- nrow(ch)
n_individuals <- ncol(ch)
n_occasions
# transforms
<- plogis(b[1])
phi <- plogis(b[2])
psi <- plogis(b[3])
p
# state matrix
<- matrix(c(
A 0, phi, 1 - phi,
0, psi, 1 - psi,
0, 0, 1
nrow = 3, byrow = TRUE)
),
# observation matrix
<- matrix(c(
B 0, 0, 1,
0, p, 1 - p,
0, 0, 1
nrow = 3, byrow = TRUE)
),
# log likelihood
<- 0
l for (i in 1:n_individuals) {
<- c(1, 0, 0)
foo for (j in 2:n_occasions) {
<- (foo %*% A) * B[, ch[i, j]]
foo
}<- l + logprot(sum(foo))
l
}
# negative loglikelihood
<- -l
l
# return
return(l)
}
Likelihood function in TMB
Now, let me try and write the same thing in TMB:
<- "
tmb_model
// capture-recapture model
#include <TMB.hpp>
/* implement the vector - matrix product */
template<class Type>
vector<Type> multvecmat(vector<Type> A, matrix<Type> B) {
int nrowb = B.rows();
int ncolb = B.cols();
vector<Type> C(ncolb);
for (int i = 0; i < ncolb; i++) {
C(i) = Type(0);
for (int k = 0; k < nrowb; k++) {
C(i) += A(k) * B(k, i);
}
}
return C;
}
template<class Type>
Type objective_function<Type>::operator() () {
// data
DATA_IMATRIX(ch);
int n_individuals = ch.rows();
int n_occasions = ch.cols();
// DATA_VECTOR(fii);
// parameters
PARAMETER_VECTOR(b);
// transformations
int npar = b.size();
vector<Type> par(npar);
for (int i = 0; i < npar; i++) {
par(i) = Type(1.0) / (Type(1.0) + exp(-b(i)));
}
Type phi = par(0);
Type psi = par(1);
Type p = par(2);
// observation matrix
matrix<Type> A(3, 3);
A(0, 0) = Type(0.0);
A(0, 1) = phi;
A(0, 2) = Type(1.0) - phi;
A(1, 0) = Type(0.0);
A(1, 1) = psi;
A(1, 2) = Type(1.0) - psi;
A(2, 0) = Type(0.0);
A(2, 1) = Type(0.0);
A(2, 2) = Type(1.0);
// observation matrix
matrix<Type> B(3, 3);
B(0, 0) = Type(0.0);
B(0, 1) = Type(0.0);
B(0, 2) = Type(1.0);
B(1, 0) = Type(0.0);
B(1, 1) = p;
B(1, 2) = Type(1.0) - p;
B(2, 0) = Type(0.0);
B(2, 1) = Type(0.0);
B(2, 2) = Type(1.0);
// likelihood
Type ll;
Type nll;
for (int i = 0; i < n_individuals; i++) {
vector<Type> foo(3);
foo(0) = Type(1.0);
foo(1) = Type(0.0);
foo(2) = Type(0.0);
for (int j = 1; j < n_occasions; j++) {
foo = multvecmat(foo, A) * vector<Type> (B.col(ch(i, j)));
}
ll += log(sum(foo));
}
// negative loglikelihood
nll = -ll;
// return
return nll;
}"
Then, we write the code to disk, compile and load it, which requires the R
package TMB
:
write(tmb_model, file = "cjs_tmb.cpp")
library(TMB)
if (!file.exists("cjs_tmb.dll")) compile("cjs_tmb.cpp")
dyn.load(dynlib("cjs_tmb"))
Estimation
With that done, let’s see how estimates from the two approaches compare and their speed:
# data simulation
<- simul(
raw_data n_individuals = 10000,
phi = 0.6,
psi = 0.9,
p = 0.2
)
# initial parameter values
<- runif(3, -1, 1)
b
# extract the data
<- raw_data$ch
ch
# R - using optim
<- Sys.time()
tic <- optim(par = b,
faa fn = cjs,
gr = NULL,
hessian = TRUE,
ch, method = "BFGS")
<- (Sys.time() - tic)
r_time <- round(plogis(faa$par), digits = 3)
r_ests names(r_ests) <- c("phi", "psi", "p")
print(r_ests)
phi psi p
0.625 0.880 0.195
# TMB - using optim
<- Sys.time()
tic <- MakeADFun(data = list(ch = ch - 1), # subtract 1 for indexing from 0
obj parameters = list(b = b),
DLL = "cjs_tmb",
silent = TRUE)
<- do.call("optim", obj)
opt <- (Sys.time() - tic)
tmb_time <- round(plogis(opt$par), digits = 3)
tmb_ests names(tmb_ests) <- c("phi", "psi", "p")
print(tmb_ests)
phi psi p
0.625 0.880 0.195
# clean up
dyn.unload(dynlib("cjs_tmb"))
Warning: 3 external pointers will be removed
unlink("cjs_tmp.*")
# print times
print(r_time)
Time difference of 9.823374 secs
print(tmb_time)
Time difference of 0.09974098 secs
Conclusion
It looks like it works and could be a useful tool for running sensitivity analyses…
Reuse
Citation
@online{d. gregory2023,
author = {D. Gregory, Stephen},
title = {CJS Is Fast in {TMB}},
date = {2023-03-10},
url = {https://stephendavidgregory.github.io/posts/cjs-tmb-is-fast},
langid = {en}
}