Non-Linear Models With BSSM
Non-Linear Models With BSSM
Introduction
This vignette shows how to model general non-linear state space models with bssm . The general non-linear Gaussian model in bssm has
following form:
𝑦𝑡 = 𝑝𝑡 + 𝜖𝑡 ,
exp(𝑟𝑡 𝑑𝑡)
𝑝𝑡+1 = 𝐾 𝑝𝑡 + 𝜉𝑡 ,
𝐾 + 𝑝𝑡 (exp(𝑟𝑡 𝑑𝑡) − 1)
exp 𝑟𝑡′
𝑟𝑡 = ′ ,
1 + exp 𝑟𝑡
𝑟′𝑡+1 = 𝑟𝑡′ + 𝜂𝑡 ,
with constant carrying capacity 𝐾 = 500 , initial population size 𝑝1 = 50, initial growth rate on logit scale 𝑟′1 = −1.5, 𝑑𝑡 = 0.1,
𝜉 ∼ 𝑁(0, 1), 𝜂 ∼ 𝑁(0, 0.05) , and 𝜖 ∼ 𝑁(0, 1).
Let’s first simulate some data, with 𝜎𝑟 = 𝜎𝑝 = 0 :
set.seed(1)
#observation times
t <- seq(0.1, 30, dT)
n <- length(t)
r <- plogis(cumsum(c(-1.5, rnorm(n - 1, sd = R_1))))
p <- numeric(n)
p[1] <- p1
for(i in 2:n)
p[i] <- rnorm(1, K * p[i-1] * exp(r[i-1] * dT) / (K + p[i-1] * (exp(r[i-1] * dT) - 1)), R_2)
# observations
y <- p + rnorm(n, 0, H)
Model in bssm
The functions determining the model need to be written in C++. Some example models which can be used as a template are given by the
function cpp_example_model which returns pointers usable as an input to nlg_ssm . For this growth model, we could call
cpp_example_model("nlg_growth") . In general, you need to define the functions matching the model components, log-density of the prior
and few other functions. For example, in case of our model, the function T_fn defines the state transition function 𝑇 (⋅) :
// [[Rcpp::export]]
arma::vec T_fn(const unsigned int t, const arma::vec& alpha, const arma::vec& theta,
const arma::vec& known_params, const arma::mat& known_tv_params) {
double dT = known_params(0);
double K = known_params(1);
arma::vec alpha_new(2);
alpha_new(0) = alpha(0);
double r = exp(alpha(0)) / (1.0 + exp(alpha(0)));
alpha_new(1) = K * alpha(1) * exp(r * dT) /
(K + alpha(1) * (exp(r * dT) - 1));
return alpha_new;
}
The name of this function does not matter, but it should always return Armadillo vector ( arma::vec ), and have the same signature (i.e. the
order and types of the function’s parameters) should always be like above, even though some of the parameters were not used in the body of
the function. Note that all of these functions can also depend on some known parameters, given as known_params (vector) and
known_tv_params (matrix) arguments to ssm_nlg function (which are then passed to individual C++ snippets). For details of using
Armadillo, see Armadillo documentation. After defining the appropriate model functions, the cpp file should also contain a function for
creating external pointers for the aforementioned functions. Why this is needed is more technical issue, but fortunately you can just copy the
function from the example file without any modifications.
After creating the file for C++ functions, you need to compile the file using Rcpp 1:
Rcpp::sourceCpp("ssm_nlg_template.cpp")
This takes a few seconds. let’s define our initial guess for 𝜃 , the logarithms of the standard deviations of observational and process level
noise, and define the prior distribution for 𝛼1 (we use log-scale in sampling for efficiency reasons, but define priors for the standard
deviations, see the template file at the appendix):
# dT, K, a1 and the prior variances of 1st and 2nd state (logit r and and p)
known_params <- c(dT = dT, K = K, a11 = -1, a12 = 50, P11 = 1, P12 = 100)
If you have used line // [[Rcpp::export]] before the model functions, you can now test that the functions work as intended:
## [,1]
## [1,] 100.000
## [2,] 212.111
library("bssm")
model <- ssm_nlg(y = y, a1=pntrs$a1, P1 = pntrs$P1,
Z = pntrs$Z_fn, H = pntrs$H_fn, T = pntrs$T_fn, R = pntrs$R_fn,
Z_gn = pntrs$Z_gn, T_gn = pntrs$T_gn,
theta = initial_theta, log_prior_pdf = pntrs$log_prior_pdf,
known_params = known_params, known_tv_params = matrix(1),
n_states = 2, n_etas = 2, state_names = c("logit_r", "p"))
Let’s first run Extended Kalman filter and smoother using our initial guess for 𝜃 :
ts.plot(plogis(cbind(out_filter$att[, 1],
out_smoother$alphahat[, 1])), col = 1:2)
Using the as.data.frame method we can convert the state samples to a data frame for further processing with the dplyr package
(Wickham et al. 2020) (we could do this automatically with summary method as well):
library("dplyr")
library("diagis")
d1 <- as.data.frame(mcmc_is, variable = "states")
d2 <- as.data.frame(mcmc_ekf, variable = "states")
d1$method <- "is2-psi"
d2$method <- "approx ekf"
## `summarise()` has grouped output by 'time'. You can override using the
## `.groups` argument.
## `summarise()` has grouped output by 'time'. You can override using the
## `.groups` argument.
Above we used the weighted versions of mean and quantile functions provided by the diagis (Helske, n.d.) package as our IS-MCMC
algorithm produces weighted samples of the posterior. Alternatively, we could have used argument output_type = "summary" , in which
case the run_mcmc returns posterior means and covariances of the states instead of samples (these are computed using the full output of
particle filter so these estimates are more accurate).
Using ggplot2 (Wickham 2016) we can compare our two estimation methods:
library("ggplot2")
ggplot(r_summary, aes(x = time, y = mean)) +
geom_ribbon(aes(ymin = lwr, ymax = upr, fill = method),
colour = NA, alpha = 0.25) +
geom_line(aes(colour = method)) +
geom_line(data = data.frame(mean = r, time = seq_along(r))) +
theme_bw()
In this example, EKF approximation performs well compared to exact method, while being considerably faster:
mcmc_is$time
mcmc_ekf$time
Appendix
This is the full ssm_nlg_template.cpp file (identical with nlg_growth accessible with cpp_example_model ):
#include <RcppArmadillo.h>
// [[Rcpp::depends(RcppArmadillo)]]
// [[Rcpp::interfaces(r, cpp)]]
arma::vec a1(2);
a1(0) = known_params(2);
a1(1) = known_params(3);
return a1;
}
// Function for the prior covariance matrix of alpha_1
// [[Rcpp::export]]
arma::mat P1_fn(const arma::vec& theta, const arma::vec& known_params) {
// Z function
// [[Rcpp::export]]
arma::vec Z_fn(const unsigned int t, const arma::vec& alpha, const arma::vec& theta,
const arma::vec& known_params, const arma::mat& known_tv_params) {
arma::vec tmp(1);
tmp(0) = alpha(1);
return tmp;
}
// Jacobian of Z function
// [[Rcpp::export]]
arma::mat Z_gn(const unsigned int t, const arma::vec& alpha, const arma::vec& theta,
const arma::vec& known_params, const arma::mat& known_tv_params) {
arma::mat Z_gn(1, 2);
Z_gn(0, 0) = 0.0;
Z_gn(0, 1) = 1.0;
return Z_gn;
}
// T function
// [[Rcpp::export]]
arma::vec T_fn(const unsigned int t, const arma::vec& alpha, const arma::vec& theta,
const arma::vec& known_params, const arma::mat& known_tv_params) {
double dT = known_params(0);
double K = known_params(1);
arma::vec alpha_new(2);
alpha_new(0) = alpha(0);
double r = exp(alpha(0)) / (1.0 + exp(alpha(0)));
alpha_new(1) = K * alpha(1) * exp(r * dT) /
(K + alpha(1) * (exp(r * dT) - 1));
return alpha_new;
}
// Jacobian of T function
// [[Rcpp::export]]
arma::mat T_gn(const unsigned int t, const arma::vec& alpha, const arma::vec& theta,
const arma::vec& known_params, const arma::mat& known_tv_params) {
double dT = known_params(0);
double K = known_params(1);
double tmp = exp(r * dT) / std::pow(K + alpha(1) * (exp(r * dT) - 1), 2);
return Tg;
}
return log_pdf;
}
// typedef for a pointer of nonlinear function of model equation returning vec (T, Z)
typedef arma::vec (*nvec_fnPtr)(const unsigned int t, const arma::vec& alpha,
const arma::vec& theta, const arma::vec& known_params, const arma::mat& known_tv_params);
// typedef for a pointer of nonlinear function returning mat (Tg, Zg, H, R)
typedef arma::mat (*nmat_fnPtr)(const unsigned int t, const arma::vec& alpha,
const arma::vec& theta, const arma::vec& known_params, const arma::mat& known_tv_params);
return Rcpp::List::create(
Rcpp::Named("a1_fn") = Rcpp::XPtr<a1_fnPtr>(new a1_fnPtr(&a1_fn)),
Rcpp::Named("P1_fn") = Rcpp::XPtr<P1_fnPtr>(new P1_fnPtr(&P1_fn)),
Rcpp::Named("Z_fn") = Rcpp::XPtr<nvec_fnPtr>(new nvec_fnPtr(&Z_fn)),
Rcpp::Named("H_fn") = Rcpp::XPtr<nmat_fnPtr>(new nmat_fnPtr(&H_fn)),
Rcpp::Named("T_fn") = Rcpp::XPtr<nvec_fnPtr>(new nvec_fnPtr(&T_fn)),
Rcpp::Named("R_fn") = Rcpp::XPtr<nmat_fnPtr>(new nmat_fnPtr(&R_fn)),
Rcpp::Named("Z_gn") = Rcpp::XPtr<nmat_fnPtr>(new nmat_fnPtr(&Z_gn)),
Rcpp::Named("T_gn") = Rcpp::XPtr<nmat_fnPtr>(new nmat_fnPtr(&T_gn)),
Rcpp::Named("log_prior_pdf") =
Rcpp::XPtr<prior_fnPtr>(new prior_fnPtr(&log_prior_pdf)));
Helske, Jouni. n.d. diagis: Diagnostic Plot and Multivariate Summary Statistics of Weighted Samples from Importance Sampling.
https://github.com/helske/diagis.
Vihola, Matti, Jouni Helske, and Jordan Franks. 2020. “Importance Sampling Type Estimators Based on Approximate Marginal MCMC.”
Scandinavian Journal of Statistics. https://doi.org/10.1111/sjos.12492.
Wickham, Hadley. 2016. Ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. https://ggplot2.tidyverse.org.
Wickham, Hadley, Romain François, Lionel Henry, and Kirill Müller. 2020. Dplyr: A Grammar of Data Manipulation.
1. As repeated calls to compile same cpp file can sometimes lead to memory issues, it is good practice to define unique cache directory
using the cacheDir argument(see issue in Github). But the CRAN does not like this approach so we do not use it here.↩︎