Dealing with convergence issues when fitting a complex Bayesian nonlinear model to a small dataset using brms in R

,

Hello, I am fitting a nonlinear model with seven parameters to a small dataset (27 observations) using a Bayesian approach. I use the brms package in R, which interfaces with Stan for sampling. The nonlinear model is bimodal, with two distinct peaks.

Each parameter has a biological interpretation. Specifically, I was able to determine reasonable prior means for the normal distributions by visually inspecting the data (see figure below). The parameters to estimate are:

a = Day of the onset of the first peak (day of the year)
b = Day of the onset of the second peak (day of the year)
c = Height of the first peak
d = Height of the second peak
e = Duration from the onset to the first peak (in days)
f = Duration from the onset to the second peak (in days)
g = Width of the peak

Despite these biologically informed priors, I am currently facing serious convergence issues during MCMC sampling. The following warnings were reported:

Warning messages: 1: There were 11733 divergent transitions after warmup. See https://mc-stan.org/misc/warnings.html#divergent-transitions-after-warmup to find out why this is a problem and how to eliminate them. 2: There were 3 chains where the estimated Bayesian Fraction of Missing Information was low. See https://mc-stan.org/misc/warnings.html#bfmi-low 3: Examine the pairs() plot to diagnose sampling problems 4: The largest R-hat is 1.93, indicating chains have not mixed. Running the chains for more iterations may help. See https://mc-stan.org/misc/warnings.html#r-hat 5: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable. Running the chains for more iterations may help. See https://mc-stan.org/misc/warnings.html#bulk-ess 6: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable. Running the chains for more iterations may help. See https://mc-stan.org/misc/warnings.html#tail-ess

I have already tried adjusting adapt_delta, max_treedepth, and the number of iterations, but the issues persist. I am aware that the model may be too complex relative to the limited dataset (which I cannot expand), but it is essential for me to obtain parameter estimates, as no published values are currently available in the literature.

Since I have limited experience with Bayesian modeling, I would greatly appreciate any advice or suggestions for improving model stability.

Here is the dataset:

df <- structure(list(day_of_year = c(128, 136, 137, 138, 142, 143, 
                               144, 162, 163, 164, 166, 167, 173, 197, 198, 199, 200, 201, 232, 
                               233, 235, 236, 253, 255, 256, 299, 308), qa = c(0, 0.0153846153846154, 
                                                                               0.0192307692307692, 0, 0.0192307692307692, 0.00384615384615385, 
                                                                               0.00384615384615385, 0.00384615384615385, 0, 0.00769230769230769, 
                                                                               0.00384615384615385, 0.00384615384615385, 0.00769230769230769, 
                                                                               0, 0, 0, 0.0230769230769231, 0.00384615384615385, 0, 0, 0, 0, 
                                                                               0, 0, 0, 0.103846153846154, 0.0384615384615385)), class = "data.frame", row.names = c(NA, 
                                                                                                                                                                     27L))

Here is the code used to specify and fit the model:

model_formula <- brms::bf(
  qa ~ if_else(day_of_year <= b, c * exp(-0.5 * ((day_of_year - a) / e)^2), c * exp(-0.5 * ((day_of_year - a) / e)^2) + d * exp(-0.5 * (((log((day_of_year - b)/f))^2) / g^2))),
  a + b + c + d + e + f + g ~ 1, nl = TRUE)

priors <- c(
  brms::prior(normal(138, 5), nlpar = "a", lb = 0),   
  brms::prior(normal(273, 5), nlpar = "b", lb = 0),  
  brms::prior(normal(0.01, 0.1), nlpar = "c", lb = 0),  
  brms::prior(normal(0.2, 0.1), nlpar = "d", lb = 0),  
  brms::prior(normal(5, 1), nlpar = "e", lb = 0),  
  brms::prior(normal(13, 1), nlpar = "f", lb = 0),  
  brms::prior(normal(0.8, 0.1), nlpar = "g", lb = 0),  
  brms::prior(student_t(3, 0, 0.1), class = "sigma")  
)

fit <- brms::brm(
  formula = model_formula,
  data = df,
  prior = priors,
  iter = 6000,
  chains = 2,
  control = list(adapt_delta = 0.9999, max_treedepth = 20),
  init = 0.1
)

Here is the figure that served as the basis for selecting biologically informed prior means:

Thanks very much for your help.

I have a few bits of advice right away!

  • you can do prior predictive checks by setting sample_prior = "only" in the brm function. This is useful to make sure your model does what you think it does!
  • the use of log in the second peak is really throwing me off. I gather that you want something asymmetrical there, but I think it would be way simpler to just model the second peak as symmetrical, and least at first
  • you’re probably also having trouble because of the use of if_else. it seems to me that just the second part of that statement alone is the function you want
  • if you want to constrain the second peak to come after the first, consider reparameterizing the model. e.g. instead of defining b as the number of days since start of year, instead set b2 as the number of days after a.
  • another good simplification is to consider your parameters e and f to be equal, at least at first
  • I don’t think you need / want the paramter g, which seems to me redundant with e and f

here’s a quick example of a prior predictive check, using some of the things I’ve suggested so far

library(brms)
#> Loading required package: Rcpp
#> Loading 'brms' package (version 2.21.0). Useful instructions
#> can be found by typing help('brms'). A more detailed introduction
#> to the package is available through vignette('brms_overview').
#> 
#> Attaching package: 'brms'
#> The following object is masked from 'package:stats':
#> 
#>     ar
library(tidybayes)
#> 
#> Attaching package: 'tidybayes'
#> The following objects are masked from 'package:brms':
#> 
#>     dstudent_t, pstudent_t, qstudent_t, rstudent_t
library(ggplot2)

df <- structure(list(day_of_year = c(128, 136, 137, 138, 142, 143, 
                               144, 162, 163, 164, 166, 167, 173, 197, 198, 199, 200, 201, 232, 
                               233, 235, 236, 253, 255, 256, 299, 308), qa = c(0, 0.0153846153846154, 
                                                                               0.0192307692307692, 0, 0.0192307692307692, 0.00384615384615385, 
                                                                               0.00384615384615385, 0.00384615384615385, 0, 0.00769230769230769, 
                                                                               0.00384615384615385, 0.00384615384615385, 0.00769230769230769, 
                                                                               0, 0, 0, 0.0230769230769231, 0.00384615384615385, 0, 0, 0, 0, 
                                                                               0, 0, 0, 0.103846153846154, 0.0384615384615385)), class = "data.frame", row.names = c(NA, 
                                                                                                                                                                     27L))

df |> 
  ggplot(aes(x = day_of_year, y = qa)) + 
  geom_point()


model_formula <- brms::bf(
  qa ~ 
    c * exp(-0.5 * ((day_of_year - a)^2 / e^2)) + d * exp(-0.5 * ((day_of_year - b)^2/f^2)),
  a + b + c + d + e + f ~ 1, nl = TRUE)

priors <- c(
  brms::prior(normal(138, 5), nlpar = "a", lb = 0),   
  brms::prior(normal(273, 5), nlpar = "b", lb = 0),  
  brms::prior(normal(0.01, 0.1), nlpar = "c", lb = 0),  
  brms::prior(normal(0.2, 0.1), nlpar = "d", lb = 0),  
  brms::prior(normal(5, 1), nlpar = "e", lb = 0),  
  brms::prior(normal(13, 1), nlpar = "f", lb = 0), 
  brms::prior(student_t(3, 0, 0.1), class = "sigma")  
)

fit <- brms::brm(
  formula = model_formula,
  data = df,
  prior = priors,
  sample_prior = "only",
  iter = 6000,
  chains = 2,
  control = list(adapt_delta = 0.9999, max_treedepth = 20),
  init = 0.1
)
#> Compiling Stan program...
#> Trying to compile a simple C file
#> Running /usr/lib/R/bin/R CMD SHLIB foo.c
#> using C compiler: ‘gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0’
#> gcc -I"/usr/share/R/include" -DNDEBUG   -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/Rcpp/include/"  -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/RcppEigen/include/"  -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/RcppEigen/include/unsupported"  -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/BH/include" -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/StanHeaders/include/src/"  -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/StanHeaders/include/"  -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/RcppParallel/include/"  -I"/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/rstan/include" -DEIGEN_NO_DEBUG  -DBOOST_DISABLE_ASSERTS  -DBOOST_PENDING_INTEGER_LOG2_HPP  -DSTAN_THREADS  -DUSE_STANC3 -DSTRICT_R_HEADERS  -DBOOST_PHOENIX_NO_VARIADIC_EXPRESSION  -D_HAS_AUTO_PTR_ETC=0  -include '/home/andrew/R/x86_64-pc-linux-gnu-library/4.4/StanHeaders/include/stan/math/prim/fun/Eigen.hpp'  -D_REENTRANT -DRCPP_PARALLEL_USE_TBB=1      -fpic  -g -O2 -ffile-prefix-map=/build/r-base-JpkSDg/r-base-4.4.3=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2  -c foo.c -o foo.o
#> In file included from /home/andrew/R/x86_64-pc-linux-gnu-library/4.4/RcppEigen/include/Eigen/Core:19,
#>                  from /home/andrew/R/x86_64-pc-linux-gnu-library/4.4/RcppEigen/include/Eigen/Dense:1,
#>                  from /home/andrew/R/x86_64-pc-linux-gnu-library/4.4/StanHeaders/include/stan/math/prim/fun/Eigen.hpp:22,
#>                  from <command-line>:
#> /home/andrew/R/x86_64-pc-linux-gnu-library/4.4/RcppEigen/include/Eigen/src/Core/util/Macros.h:679:10: fatal error: cmath: No such file or directory
#>   679 | #include <cmath>
#>       |          ^~~~~~~
#> compilation terminated.
#> make: *** [/usr/lib/R/etc/Makeconf:195: foo.o] Error 1
#> Start sampling
#> 
#> SAMPLING FOR MODEL 'anon_model' NOW (CHAIN 1).
#> Chain 1: 
#> Chain 1: Gradient evaluation took 1.6e-05 seconds
#> Chain 1: 1000 transitions using 10 leapfrog steps per transition would take 0.16 seconds.
#> Chain 1: Adjust your expectations accordingly!
#> Chain 1: 
#> Chain 1: 
#> Chain 1: Iteration:    1 / 6000 [  0%]  (Warmup)
#> Chain 1: Iteration:  600 / 6000 [ 10%]  (Warmup)
#> Chain 1: Iteration: 1200 / 6000 [ 20%]  (Warmup)
#> Chain 1: Iteration: 1800 / 6000 [ 30%]  (Warmup)
#> Chain 1: Iteration: 2400 / 6000 [ 40%]  (Warmup)
#> Chain 1: Iteration: 3000 / 6000 [ 50%]  (Warmup)
#> Chain 1: Iteration: 3001 / 6000 [ 50%]  (Sampling)
#> Chain 1: Iteration: 3600 / 6000 [ 60%]  (Sampling)
#> Chain 1: Iteration: 4200 / 6000 [ 70%]  (Sampling)
#> Chain 1: Iteration: 4800 / 6000 [ 80%]  (Sampling)
#> Chain 1: Iteration: 5400 / 6000 [ 90%]  (Sampling)
#> Chain 1: Iteration: 6000 / 6000 [100%]  (Sampling)
#> Chain 1: 
#> Chain 1:  Elapsed Time: 0.315 seconds (Warm-up)
#> Chain 1:                0.416 seconds (Sampling)
#> Chain 1:                0.731 seconds (Total)
#> Chain 1: 
#> 
#> SAMPLING FOR MODEL 'anon_model' NOW (CHAIN 2).
#> Chain 2: 
#> Chain 2: Gradient evaluation took 9e-06 seconds
#> Chain 2: 1000 transitions using 10 leapfrog steps per transition would take 0.09 seconds.
#> Chain 2: Adjust your expectations accordingly!
#> Chain 2: 
#> Chain 2: 
#> Chain 2: Iteration:    1 / 6000 [  0%]  (Warmup)
#> Chain 2: Iteration:  600 / 6000 [ 10%]  (Warmup)
#> Chain 2: Iteration: 1200 / 6000 [ 20%]  (Warmup)
#> Chain 2: Iteration: 1800 / 6000 [ 30%]  (Warmup)
#> Chain 2: Iteration: 2400 / 6000 [ 40%]  (Warmup)
#> Chain 2: Iteration: 3000 / 6000 [ 50%]  (Warmup)
#> Chain 2: Iteration: 3001 / 6000 [ 50%]  (Sampling)
#> Chain 2: Iteration: 3600 / 6000 [ 60%]  (Sampling)
#> Chain 2: Iteration: 4200 / 6000 [ 70%]  (Sampling)
#> Chain 2: Iteration: 4800 / 6000 [ 80%]  (Sampling)
#> Chain 2: Iteration: 5400 / 6000 [ 90%]  (Sampling)
#> Chain 2: Iteration: 6000 / 6000 [100%]  (Sampling)
#> Chain 2: 
#> Chain 2:  Elapsed Time: 0.334 seconds (Warm-up)
#> Chain 2:                0.444 seconds (Sampling)
#> Chain 2:                0.778 seconds (Total)
#> Chain 2:

library(tidybayes)
df |> 
  add_epred_draws(fit, ndraws = 6) |>
  ggplot(aes(x = day_of_year, y = .epred, group = .draw)) + 
  geom_line()

Created on 2025-06-05 with reprex v2.1.1

1 Like