Update
Turns out the error was due to an overrepresentation of values near zero in my MRE. I have slightly altered the MRE and got the model to run:
# generate data
set.seed(10)
x <- as.numeric(0:100)
r <- rnorm(x, mean = 0, sd = ifelse(x == 0, 1, x^-0.8))
c0 <- 2
a <- 0.01
b <- 0.07
k <- 0.05
y <- c0 * exp(-(a * x - b/k * (exp(-k * x) - 1))) + r
require(tidyverse)
require(magrittr)
df <- tibble(x = x, y = y)
stan_code <- "
data {
int n; // Number of observations
vector[n] y; // Response variable
vector[n] x; // Predictor variable
}
parameters {
real<lower=0> sigma; // Standard deviation
real<lower=0> a; // Intercept
real<lower=0> tau; // Precision of random step
vector[n] k; // Decay rate
}
model {
// Priors
sigma ~ exponential( 1 );
a ~ lognormal( log(2) , 0.05 );
k[1] ~ lognormal( log(0.08) , 0.05 );
tau ~ lognormal( log(0.001) , 0.2 );
// Random walk
for ( i in 2:n ) {
k[i] ~ normal( k[i - 1] , tau );
}
// Prediction function
vector[n] mu; // Mean prediction for y
for( i in 1:n ) {
mu[i] = a * exp( -k[i] * x[i] ); // Calculate mu using updated k
}
// Likelihood
y ~ normal( mu, sigma );
}
"
# draw samples using RStan
require(rstan)
require(tidybayes)
rw.mod <- stan(model_code = stan_code,
data = compose_data(df), iter = 2e4,
chains = 8, cores = parallel::detectCores())
# check Rhat, n_eff and chains
options(max.print = 1e4)
rw.mod
require(rethinking)
trankplot(rw.mod)
# extract prior and posterior distributions
rw.mod.samples <- rw.mod %>%
spread_draws(sigma, a, tau, k[x]) %>%
ungroup() %>%
mutate(
predictor = rep(df$x, length(unique(.chain)) * length(unique(.iteration))), # recover predictor variable
sigma_prior = rep(rexp(length(unique(.chain)) * length(unique(.iteration)), 1), # add priors
each = length(unique(x))),
a_prior = rep(rlnorm(length(unique(.chain)) * length(unique(.iteration)), log(2), 0.05),
each = length(unique(x))),
tau_prior = rep(rlnorm(length(unique(.chain)) * length(unique(.iteration)), log(0.001), 0.2),
each = length(unique(x))),
k_prior = rep(rlnorm(length(unique(.chain)) * length(unique(.iteration)), log(0.08), 0.05),
each = length(unique(x)))
)
# prior-posterior comparison
rw.mod.samples %>%
filter(x == 1) %>%
pivot_longer(cols = c("sigma", "a", "tau", "k",
"sigma_prior", "a_prior", "tau_prior", "k_prior"),
names_to = c("parameter", "distribution"),
names_sep = "_",
values_to = "samples") %>%
mutate(parameter = fct(parameter),
distribution = fct(ifelse(is.na(distribution),
"posterior", distribution))) %>%
ggplot(aes(samples, fill = distribution)) +
facet_wrap(~parameter, scales = "free", nrow = 1) +
geom_density(colour = NA, alpha = 0.5) +
theme_void() +
theme(legend.position = "top", legend.justification = 0)
The predictions for k
(black) do not match the underlying function (orange) perfectly but describe the general trend:
# summarise and plot predictions for k as a function of x
rw.mod.samples %>%
group_by(predictor) %>%
summarise(k.mean = mean(k),
k.pi_lwr50 = PI(k, 0.5)[1],
k.pi_upr50 = PI(k, 0.5)[2],
k.pi_lwr80 = PI(k, 0.8)[1],
k.pi_upr80 = PI(k, 0.8)[2],
k.pi_lwr90 = PI(k, 0.9)[1],
k.pi_upr90 = PI(k, 0.9)[2]) %>%
pivot_longer(cols = starts_with("k.pi"),
names_to = c("limit", "percentile"),
names_pattern = ".*_([a-z]+)([0-9]+)") %>%
pivot_wider(names_from = "limit", values_from = "value") %>%
ggplot(aes(x = predictor, y = k.mean, ymin = lwr, ymax = upr,
alpha = percentile)) +
geom_line() +
geom_ribbon() +
stat_function(fun = function(x) { a + b * exp( -k * x ) },
colour = "orange") +
scale_alpha_manual(values = c(0.5, 0.4, 0.3), guide = "none") +
lims(y = c(0, NA)) +
theme_minimal()
The predictions for
mu
(black) nonetheless fit the data better than the underlying function (orange):
# summarise and plot predictions for mu
rw.mod.samples %>%
mutate(mu = a * exp( -k * predictor )) %>%
group_by(predictor) %>%
summarise(mu.mean = mean(mu),
mu.pi_lwr50 = PI(mu, 0.5)[1],
mu.pi_upr50 = PI(mu, 0.5)[2],
mu.pi_lwr80 = PI(mu, 0.8)[1],
mu.pi_upr80 = PI(mu, 0.8)[2],
mu.pi_lwr90 = PI(mu, 0.9)[1],
mu.pi_upr90 = PI(mu, 0.9)[2]) %>%
pivot_longer(cols = starts_with("mu.pi"),
names_to = c("limit", "percentile"),
names_pattern = ".*_([a-z]+)([0-9]+)") %>%
pivot_wider(names_from = "limit", values_from = "value") %>%
ggplot(aes(x = predictor, y = mu.mean)) +
geom_line() +
geom_ribbon(aes(ymin = lwr, ymax = upr, alpha = percentile)) +
stat_function(fun = function(x) { c0 * exp(-(a * x - b/k * (exp(-k * x) - 1))) },
colour = "orange") +
scale_alpha_manual(values = c(0.5, 0.4, 0.3), guide = "none") +
geom_point(data = df, aes(x, y)) +
theme_minimal()
With the issue of getting the model to run solved, I focussed my attention on emulating the term
delta
in the LibBi function
sub transition
. This term defines the step size of the random walk. Stan doesn’t allow decimalised sequences in for loops and the number of steps seems to be limited to
n
. A more realistic MRE with gaps in the data provides a case in point:
# generate data with gaps
set.seed(10)
x <- rep(seq(0, 100, 25), each = 3)
r <- rnorm(x, mean = 0, sd = 0.1)
c0 <- 2
a <- 0.01
b <- 0.07
k <- 0.05
y <- c0 * exp(-(a * x - b/k * (exp(-k * x) - 1))) + r
require(tidyverse)
require(magrittr)
df <- tibble(x = x, y = y)
# plot data with underlying function
ggplot(df, aes(x, y)) +
geom_point() +
stat_function(fun = function(x) { c0 * exp(-(a * x - b/k * (exp(-k * x) - 1))) }) +
lims(y = c(0, NA)) +
theme_minimal()
Since Stan estimates
n
steps in
k
and
n
no longer corresponds directly to
x
, it is unclear how to predict the random walk, i.e. interpolate the gaps in the data. However, this is clearly possible as shown in the
paper I initially cited.
@sbfnk, perhaps you can provide some insight into the workings of LibBi’s sub transition(delta = )
and how this may be translated into Stan?