diff --git a/DESCRIPTION b/DESCRIPTION index ccff2ea..774f6fb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: drjacoby Type: Package Title: Flexible Markov Chain Monte Carlo via Reparameterization -Version: 1.5.3 +Version: 1.5.4 Authors@R: c( person("Bob", "Verity", email = "r.verity@imperial.ac.uk", role = c("aut", "cre")), person("Pete", "Winskill", email = "p.winskill@imperial.ac.uk", role = c("aut")) @@ -26,7 +26,9 @@ Imports: rlang, cowplot, dplyr, - magrittr + magrittr, + GGally, + tidyr Suggests: testthat, covr, diff --git a/NAMESPACE b/NAMESPACE index dbb7d15..43db110 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,16 +6,20 @@ export(check_drjacoby_loaded) export(cpp_template) export(define_params) export(plot_autocorrelation) -export(plot_cor) export(plot_cor_mat) export(plot_credible) +export(plot_density) export(plot_mc_acceptance) -export(plot_par) +export(plot_pairs) export(plot_rung_loglike) +export(plot_scatter) +export(plot_trace) export(run_mcmc) export(sample_chains) import(dplyr) import(ggplot2) +importFrom(GGally,ggpairs) +importFrom(GGally,wrap) importFrom(Rcpp,sourceCpp) importFrom(coda,geweke.diag) importFrom(coda,mcmc) @@ -27,6 +31,7 @@ importFrom(stats,quantile) importFrom(stats,runif) importFrom(stats,setNames) importFrom(stats,var) +importFrom(tidyr,pivot_longer) importFrom(utils,setTxtProgressBar) importFrom(utils,txtProgressBar) useDynLib(drjacoby, .registration = TRUE) diff --git a/R/main.R b/R/main.R index 5261b86..d548e98 100644 --- a/R/main.R +++ b/R/main.R @@ -1,629 +1,629 @@ - -#------------------------------------------------ -#' @title Check that drjacoby package has loaded successfully -#' -#' @description Simple function to check that drjacoby package has loaded -#' successfully. Prints "drjacoby loaded successfully!" if so. -#' -#' @export - -check_drjacoby_loaded <- function() { - message("drjacoby loaded successfully!") -} - -#------------------------------------------------ -#' @title Define parameters dataframe -#' -#' @description Provides a convenient way of defining parameters in the format -#' required by \code{run_mcmc()}. Each parameter must have the following three -#' elements, defined in order: -#' \itemize{ -#' \item \code{name} - the parameter name. -#' \item \code{min} - the minimum value of the parameter. \code{-Inf} is -#' allowed. -#' \item \code{max} - the maximum value of the parameter. \code{Inf} is -#' allowed. -#' } -#' There following arguments are also optional: -#' \itemize{ -#' \item \code{init} - the initial value of the parameter. If running -#' multiple chains a vector of initial values can be used to specify distinct -#' values for each chain. -#' \item \code{block} - which likelihood block(s) this parameter belongs to. -#' See vignettes for instructions on using likelihood blocks. -#' } -#' -#' @param ... a series of named input arguments. -#' -#' -#' @export -#' @examples -#' define_params(name = "mu", min = -10, max = 10, init = 0, -#' name = "sigma", min = 0, max = 5, init = c(1, 2)) -#' -#' define_params(name = "mu1", min = -10, max = 10, init = 0, block = 1, -#' name = "mu2", min = -10, max = 10, init = 0, block = 2, -#' name = "sigma", min = 0, max = 5, init = 1, block = c(1, 2)) - -define_params <- function(...) { - x <- list(...) - - # check input format of arguments - assert_gr(length(x), 0, message = "input cannot be empty") - assert_in(names(x), c("name", "min", "max", "init", "block")) - use_init <- ("init" %in% names(x)) - use_block <- ("block" %in% names(x)) - n_cols <- 3 + use_init + use_block - if ((length(x) %% n_cols) != 0) { - stop("must have the same number of inputs per parameter") - } - n_param <- length(x) / n_cols - arg_names <-c("name", "min", "max") - if (use_init) { - arg_names <- c(arg_names, "init") - } - if (use_block) { - arg_names <- c(arg_names, "block") - } - assert_eq(names(x), rep(arg_names, n_param)) - - # create params dataframe - v <- n_cols*(0:(n_param - 1)) - ret <- data.frame(name = unlist(x[1 + v]), - min = unlist(x[2 + v]), - max = unlist(x[3 + v])) - if (use_init) { - ret$init <- x[which(arg_names == "init") + v] - } - if (use_block) { - ret$block <- x[which(arg_names == "block") + v] - } - - # run checks and standardise format - ret <- check_params(ret) - - return(ret) -} - -#------------------------------------------------ -# Check that params dataframe is formatted correctly, and return in standardised -# format (init and block coerced to list) -#' @noRd -check_params <- function(x) { - - # check dataframe has correct elements - assert_dataframe(x) - assert_in(c("name", "min", "max"), names(x), - message = "df_params must contain the columns 'name', 'min', 'max'") - if (any(duplicated(x$name))) { - stop("parameter names must be unique") - } - use_init <- ("init" %in% names(x)) - use_block <- ("block" %in% names(x)) - - # coerce init and block to list - if (use_init) { - if (!is.list(x$init)) { - x$init <- as.list(x$init) - } - } - if (use_block) { - if (!is.list(x$block)) { - x$block <- as.list(x$block) - } - } - - # check each row in turn - for (i in seq_len(nrow(x))) { - - # check format - assert_single_string(x$name[i], message = "parameter names must be character strings") - assert_single_numeric(x$min[i], message = "min values must be single values") - assert_single_numeric(x$max[i], message = "min values must be single values") - if (use_init) { - assert_vector_numeric(x$init[[i]], message = "init values must be numeric") - } - if (use_block) { - assert_vector_numeric(x$block[[i]], message = "block values must be numeric") - } - - # check order - assert_leq(x$min[i], x$max[i], message = "min values must be less than or equal to max values") - if (use_init) { - assert_greq(x$init[[i]], x$min[i], message = "init values must be greater than or equal to min values") - assert_leq(x$init[[i]], x$max[i], message = "init values must be less than or equal to max values") - } - } - - return(x) -} - -#------------------------------------------------ -#' @title Run drjacoby MCMC -#' -#' @description Run MCMC either with or without parallel tempering turned on. -#' Minimum inputs include a data object, a data.frame of parameters, a -#' log-likelihood function and a log-prior function. Produces an object of -#' class \code{drjacoby_output}, which contains all MCMC output along with -#' some diagnostics and a record of inputs. -#' -#' @details Note that both \code{data} and \code{misc} are passed into -#' log-likelihood/log-prior functions *by reference*. This means if you modify -#' these objects inside the functions then any changes will persist. -#' -#' @param data a named list or data frame or data values. -#' @param df_params a data.frame of parameters (see \code{?define_params}). -#' @param misc optional list object passed to likelihood and prior. This can be -#' useful for passing values that are not strictly data, for example passing a -#' lookup table to the prior function. -#' @param loglike,logprior the log-likelihood and log-prior functions used in -#' the MCMC. Can either be passed in as R functions (not in quotes), or as -#' character strings naming compiled C++ functions (in quotes). -#' @param burnin the number of burn-in iterations. Automatic tuning of proposal -#' standard deviations is only active during the burn-in period. -#' @param samples the number of sampling iterations. -#' @param rungs the number of temperature rungs used in the parallel tempering -#' method. By default, \eqn{\beta} values are equally spaced between 0 and 1, -#' i.e. \eqn{\beta[i]=}\code{(i-1)/(rungs-1)} for \code{i} in \code{1:rungs}. -#' The likelihood for the \out{ith} heated chain is raised to the -#' power \eqn{\beta[i]^\alpha}, meaning we can use the \eqn{\alpha} parameter -#' to concentrate rungs towards the start or the end of the interval (see the -#' \code{alpha} argument). -#' @param chains the number of independent replicates of the MCMC to run. If a -#' \code{cluster} object is defined then these chains are run in parallel, -#' otherwise they are run in serial. -#' @param beta_manual vector of manually defined \eqn{\beta} values used in the -#' parallel tempering approach. If defined, this overrides the spacing defined -#' by \code{rungs}. Note that even manually defined \eqn{\beta} values are -#' raised to the power \eqn{\alpha} internally, hence you should set -#' \code{alpha = 1} if you want to fix \eqn{\beta} values exactly. -#' @param alpha the likelihood for the \out{ith} heated chain is -#' raised to the power \eqn{\beta[i]^\alpha}, meaning we can use the -#' \eqn{\alpha} parameter to concentrate rungs towards the start or the end of -#' the temperature scale. -#' @param target_acceptance Target acceptance rate. Should be between 0 and 1. -#' Default of 0.44, set as optimum for unvariate proposal distributions. -#' @param cluster option to pass in a cluster environment, allowing chains to be -#' run in parallel (see package "parallel"). -#' @param coupling_on whether to implement Metropolis-coupling over temperature -#' rungs. The option of deactivating coupling has been retained for general -#' interest and debugging purposes only. If this parameter is \code{FALSE} -#' then parallel tempering will have no impact on MCMC mixing. -#' @param pb_markdown whether to run progress bars in markdown mode, meaning -#' they are only updated when they reach 100\% to avoid large amounts of output -#' being printed to markdown files. -#' @param save_data if \code{TRUE} (the default) the raw input data is stored -#' for reference in the project output. This allows complete reproducibility -#' from a project, but may be undesirable when datasets are very large. -#' @param save_hot_draws if \code{TRUE} the parameter draws relating to the hot -#' chains are also stored inside the \code{pt} element of the project output. -#' If \code{FALSE} (the default) only log-likelihoods and log-priors are -#' stored from heated chains. -#' @param silent whether to suppress all console output. -#' -#' @importFrom utils txtProgressBar -#' @importFrom stats setNames var runif -#' @export - -run_mcmc <- function(data, - df_params, - misc = list(), - loglike, - logprior, - burnin = 1e3, - samples = 1e4, - rungs = 1, - chains = 5, - beta_manual = NULL, - alpha = 1.0, - target_acceptance = 0.44, - cluster = NULL, - coupling_on = TRUE, - pb_markdown = FALSE, - save_data = TRUE, - save_hot_draws = FALSE, - silent = FALSE) { - - # declare variables to avoid "no visible binding" issues - phase <- rung <- value <- chain <- link <- NULL - - # Cleanup pointers on exit - on.exit(gc()) - - # ---------- check inputs ---------- - - # check data - assert_list_named(data) - - # check misc - assert_list(misc) - - # check loglikelihood and logprior functions - assert_class(loglike, c("function", "character")) - assert_class(logprior, c("function", "character")) - - # check MCMC parameters - assert_single_pos_int(burnin, zero_allowed = FALSE) - assert_single_pos_int(samples, zero_allowed = FALSE) - assert_single_pos_int(rungs, zero_allowed = FALSE) - assert_single_pos_int(chains, zero_allowed = FALSE) - assert_single_logical(coupling_on) - assert_single_pos(alpha) - assert_bounded(target_acceptance, 0, 1) - - # check df_params - df_params <- check_params(df_params) - use_init <- ("init" %in% names(df_params)) - use_block <- ("block" %in% names(df_params)) - if (use_init) { - for (i in 1:nrow(df_params)) { - if (length(df_params$init[[i]]) != 1) { - assert_length(df_params$init[[i]], chains, message = paste0("must define one df_params$init value per parameter, ", - "or alternatively a list of values one for each chain")) - } - } - } - - # calculate/check final temperature vector - if (is.null(beta_manual)) { - beta_manual <- rev(seq(1, 0, l = rungs)) - } - rungs <- length(beta_manual) - assert_vector_bounded(beta_manual) - assert_increasing(beta_manual) - assert_eq(beta_manual[rungs], 1.0) - - # check misc parameters - if (!is.null(cluster)) { - assert_class(cluster, "cluster") - } - assert_single_logical(pb_markdown) - assert_single_logical(save_data) - assert_single_logical(save_hot_draws) - assert_single_logical(silent) - - - # ---------- pre-processing ---------- - - # calculate transformation type for each parameter - # 0 = [-Inf,Inf] -> phi = theta - # 1 = [-Inf,b] -> phi = log(b - theta) - # 2 = [a,Inf] -> phi = log(theta - a) - # 3 = [a,b] -> phi = log((theta - a)/(b - theta)) - df_params$trans_type <- 2*is.finite(df_params$min) + is.finite(df_params$max) - - # flag to skip over fixed parameters - skip_param <- (df_params$min == df_params$max) - - # define default init values - if (!use_init) { - init_list <- list() - for (i in 1:nrow(df_params)) { - p <- runif(chains) - if (df_params$trans_type[i] == 0) { - init_list[[i]] <- log(p) - log(1 - p) - } else if (df_params$trans_type[i] == 1) { - init_list[[i]] <- log(p) + df_params$max[i] - } else if (df_params$trans_type[i] == 2) { - init_list[[i]] <- df_params$min[i] - log(p) - } else if (df_params$trans_type[i] == 3) { - init_list[[i]] <- df_params$min[i] + (df_params$max[i] - df_params$min[i])*p - } - } - df_params$init <- init_list - } - - # define default blocks - if (!use_block) { - df_params$block <- as.list(rep(1, nrow(df_params))) - } - - # get initial values into matrix. Rows for parameters, columns for chains - init_mat <- do.call(rbind, mapply(function(x) { - if (length(x) == 1) { - rep(x, chains) - } else { - x - } - }, df_params$init, SIMPLIFY = FALSE)) - - # flag whether likelihood and/or prior are C++ functions - loglike_use_cpp <- inherits(loglike, "character") - logprior_use_cpp <- inherits(logprior, "character") - - # raise temperature vector to power - beta_raised <- beta_manual^alpha - - # make sure "block" is not an element of misc already being used, and if not - # create dummy element for storing current block - if (length(misc) > 0) { - assert_not_in("block", names(misc), message = "'block' is a reserved name within misc object") - } - misc$block <- -1 - - - # ---------- define argument lists ---------- - - # parameters to pass to C++ - args_params <- list(x = data, - misc = misc, - loglike_use_cpp = loglike_use_cpp, - logprior_use_cpp = logprior_use_cpp, - theta_min = df_params$min, - theta_max = df_params$max, - block = df_params$block, - n_block = max(unlist(df_params$block)), - trans_type = df_params$trans_type, - skip_param = skip_param, - burnin = burnin, - samples = samples, - rungs = rungs, - coupling_on = coupling_on, - beta_raised = beta_raised, - save_hot_draws = save_hot_draws, - pb_markdown = pb_markdown, - silent = silent, - target_acceptance = target_acceptance) - - # functions to pass to C++ - args_functions <- list(loglike = loglike, - logprior = logprior, - test_convergence = test_convergence, - update_progress = update_progress) - - # complete list of arguments - args <- list(args_params = args_params, - args_functions = args_functions) - - # create distinct argument sets over chains - parallel_args <- replicate(chains, args, simplify = FALSE) - for (i in 1:chains) { - parallel_args[[i]]$args_params$chain <- i - - # create named vector object for passing internally within C++ functions. - # Initial values defined separately for each chain - parallel_args[[i]]$args_params$theta_vector <- setNames(init_mat[,i], df_params$name) - } - - - # ---------- run MCMC ---------- - - # split into parallel and serial implementations - if (!is.null(cluster)) { - - # run in parallel - parallel::clusterEvalQ(cluster, library(drjacoby)) - output_raw <- parallel::clusterApplyLB(cl = cluster, parallel_args, deploy_chain) - - } else { - - # run in serial - output_raw <- lapply(parallel_args, deploy_chain) - } - - # print total runtime - chain_runtimes <- mapply(function(x) x$t_diff, output_raw) - if (!silent) { - message(sprintf("total MCMC run-time: %s seconds", signif(sum(chain_runtimes), 3))) - } - - - # ---------- process output ---------- - - # define names - chain_names <- 1:chains - rung_names <- 1:rungs - param_names <- df_params$name - - # get parameter draws into dataframe. This will be over all rungs if - # save_hot_draws is TRUE, otherwise it will only be over the cold chain - df_theta <- do.call(rbind, mapply(function(j) { - do.call(rbind, mapply(function(i) { - - theta_burnin <- do.call(rbind, output_raw[[j]]$theta_burnin[[i]]) %>% - as.data.frame() %>% - magrittr::set_colnames(param_names) %>% - dplyr::mutate(chain = chain_names[j], - rung = rung_names[i], - phase = "burnin", .before = 1) - - theta_sampling <- do.call(rbind, output_raw[[j]]$theta_sampling[[i]]) %>% - as.data.frame() %>% - magrittr::set_colnames(param_names) %>% - dplyr::mutate(chain = chain_names[j], - rung = rung_names[i], - phase = "sampling", .before = 1) - - ret <- theta_burnin %>% - dplyr::bind_rows(theta_sampling) %>% - dplyr::mutate(iteration = seq_along(phase), .after = "phase") - - return(ret) - }, seq_along(output_raw[[j]]$theta_burnin), SIMPLIFY = FALSE)) - }, seq_along(output_raw), SIMPLIFY = FALSE)) - - # fix rungs field if save_hot_draws is FALSE - if (!save_hot_draws) { - df_theta$rung <- rungs - } - - # get likelihoods and priors over all rungs - df_pt <- do.call(rbind, mapply(function(j) { - do.call(rbind, mapply(function(i) { - - pt_burnin <- data.frame(chain = chain_names[j], - rung = rung_names[i], - phase = "burnin", - logprior = output_raw[[j]]$logprior_burnin[[i]], - loglikelihood = output_raw[[j]]$loglike_burnin[[i]]) - - pt_sampling <- data.frame(chain = chain_names[j], - rung = rung_names[i], - phase = "sampling", - logprior = output_raw[[j]]$logprior_sampling[[i]], - loglikelihood = output_raw[[j]]$loglike_sampling[[i]]) - - ret <- pt_burnin %>% - dplyr::bind_rows(pt_sampling) %>% - dplyr::mutate(iteration = seq_along(phase), .after = "phase") - - return(ret) - }, seq_along(output_raw[[j]]$logprior_burnin), SIMPLIFY = FALSE)) - }, seq_along(output_raw), SIMPLIFY = FALSE)) - - # merge loglike and logprior for cold chain into main output - df_theta <- df_theta %>% - dplyr::left_join(df_pt, by = c("chain", "rung", "phase", "iteration")) - - # if save_hot_draws = TRUE then merge theta values back into pt output - if (save_hot_draws) { - df_pt <- df_pt %>% - dplyr::left_join(dplyr::select(df_theta, -.data$loglikelihood, -.data$logprior), by = c("chain", "rung", "phase", "iteration")) - } - - # drop rungs field from main output - df_output <- df_theta %>% - dplyr::filter(.data$rung == max(rungs)) %>% - dplyr::select(-.data$rung) - - # check for bad values in output - if (!all(is.finite(unlist(df_output[, param_names])))) { - stop("output contains non-finite values") - } - - # append to output list - output_processed <- list(output = df_output, - pt = df_pt) - - ## Diagnostics - output_processed$diagnostics <- list() - - # run-times - run_time <- data.frame(chain = chain_names, - seconds = chain_runtimes) - output_processed$diagnostics$run_time <- run_time - - # Rhat (Gelman-Rubin diagnostic) - if (chains > 1) { - rhat_est <- c() - for (p in seq_along(param_names)) { - rhat_est[p] <- df_output %>% - dplyr::filter(phase == "sampling") %>% - dplyr::select(chain, param_names[p]) %>% - gelman_rubin(chains = chains, samples = samples) - } - rhat_est[skip_param] <- NA - names(rhat_est) <- param_names - output_processed$diagnostics$rhat <- rhat_est - } - - # ESS - ess_est <- df_output %>% - dplyr::filter(phase == "sampling") %>% - dplyr::select(param_names) %>% - apply(2, coda::effectiveSize) - ess_est[skip_param] <- NA - output_processed$diagnostics$ess <- ess_est - - # Thermodynamic power - output_processed$diagnostics$rung_details <- data.frame(rung = 1:rungs, - thermodynamic_power = beta_raised) - - # Metropolis-coupling - # store acceptance rates between pairs of rungs (links) - mc_accept <- NA - if (rungs > 1) { - - # MC accept - mc_accept <- expand.grid(link = seq_len(rungs - 1), chain = chain_names) - mc_accept_burnin <- unlist(lapply(output_raw, function(x){x$mc_accept_burnin})) / burnin - mc_accept_sampling <- unlist(lapply(output_raw, function(x){x$mc_accept_sampling})) / samples - mc_accept <- rbind(cbind(mc_accept, phase = "burnin", value = mc_accept_burnin), - cbind(mc_accept, phase = "sampling", value = mc_accept_sampling)) - - } - output_processed$diagnostics$mc_accept <- mc_accept - - # DIC - DIC <- df_pt %>% - dplyr::filter(.data$phase == "sampling" & .data$rung == rungs) %>% - dplyr::select(.data$loglikelihood) %>% - dplyr::mutate(deviance = -2*.data$loglikelihood) %>% - dplyr::summarise(DIC = mean(.data$deviance) + 0.5*var(.data$deviance)) %>% - dplyr::pull(.data$DIC) - output_processed$diagnostics$DIC_Gelman <- DIC - - ## Parameters - data_store <- NULL - if (save_data) { - data_store <- data - } - output_processed$parameters <- list(data = data_store, - df_params = df_params, - loglike = loglike, - logprior = logprior, - burnin = burnin, - samples = samples, - rungs = rungs, - chains = chains, - coupling_on = coupling_on, - alpha = alpha, - beta_manual = beta_manual) - - # save output as custom class - class(output_processed) <- "drjacoby_output" - - # return - return(output_processed) -} - -#------------------------------------------------ -# deploy main_mcmc for this chain -#' @noRd -deploy_chain <- function(args) { - - # Specify pointers to cpp functions - if (args$args_params$loglike_use_cpp) { - args$args_functions$loglike <- create_xptr(args$args_functions$loglike) - } - if (args$args_params$logprior_use_cpp) { - args$args_functions$logprior <- create_xptr(args$args_functions$logprior) - } - - # get parameters - burnin <- args$args_params$burnin - samples <- args$args_params$samples - - # make progress bars - pb_burnin <- txtProgressBar(min = 0, max = burnin, initial = NA, style = 3) - pb_samples <- txtProgressBar(min = 0, max = samples, initial = NA, style = 3) - args$args_progress <- list(pb_burnin = pb_burnin, - pb_samples = pb_samples) - - # run C++ function - ret <- main_cpp(args) - - # remove arguments - rm(args) - - return(ret) -} - -#------------------------------------------------ -# update progress bar -# pb_list = list of progress bar objects -# name = name of this progress bar -# i = new value of bar -# max_i = max value of bar (close when reach this value) -# close = whether to close when reach end -#' @importFrom utils setTxtProgressBar -#' @noRd -update_progress <- function(pb_list, name, i, max_i, close = TRUE) { - setTxtProgressBar(pb_list[[name]], i) - if (i == max_i & close) { - close(pb_list[[name]]) - } -} - -# Deal with user input cpp not being defined -globalVariables(c("create_xptr")) + +#------------------------------------------------ +#' @title Check that drjacoby package has loaded successfully +#' +#' @description Simple function to check that drjacoby package has loaded +#' successfully. Prints "drjacoby loaded successfully!" if so. +#' +#' @export + +check_drjacoby_loaded <- function() { + message("drjacoby loaded successfully!") +} + +#------------------------------------------------ +#' @title Define parameters dataframe +#' +#' @description Provides a convenient way of defining parameters in the format +#' required by \code{run_mcmc()}. Each parameter must have the following three +#' elements, defined in order: +#' \itemize{ +#' \item \code{name} - the parameter name. +#' \item \code{min} - the minimum value of the parameter. \code{-Inf} is +#' allowed. +#' \item \code{max} - the maximum value of the parameter. \code{Inf} is +#' allowed. +#' } +#' There following arguments are also optional: +#' \itemize{ +#' \item \code{init} - the initial value of the parameter. If running +#' multiple chains a vector of initial values can be used to specify distinct +#' values for each chain. +#' \item \code{block} - which likelihood block(s) this parameter belongs to. +#' See vignettes for instructions on using likelihood blocks. +#' } +#' +#' @param ... a series of named input arguments. +#' +#' +#' @export +#' @examples +#' define_params(name = "mu", min = -10, max = 10, init = 0, +#' name = "sigma", min = 0, max = 5, init = c(1, 2)) +#' +#' define_params(name = "mu1", min = -10, max = 10, init = 0, block = 1, +#' name = "mu2", min = -10, max = 10, init = 0, block = 2, +#' name = "sigma", min = 0, max = 5, init = 1, block = c(1, 2)) + +define_params <- function(...) { + x <- list(...) + + # check input format of arguments + assert_gr(length(x), 0, message = "input cannot be empty") + assert_in(names(x), c("name", "min", "max", "init", "block")) + use_init <- ("init" %in% names(x)) + use_block <- ("block" %in% names(x)) + n_cols <- 3 + use_init + use_block + if ((length(x) %% n_cols) != 0) { + stop("must have the same number of inputs per parameter") + } + n_param <- length(x) / n_cols + arg_names <-c("name", "min", "max") + if (use_init) { + arg_names <- c(arg_names, "init") + } + if (use_block) { + arg_names <- c(arg_names, "block") + } + assert_eq(names(x), rep(arg_names, n_param)) + + # create params dataframe + v <- n_cols*(0:(n_param - 1)) + ret <- data.frame(name = unlist(x[1 + v]), + min = unlist(x[2 + v]), + max = unlist(x[3 + v])) + if (use_init) { + ret$init <- x[which(arg_names == "init") + v] + } + if (use_block) { + ret$block <- x[which(arg_names == "block") + v] + } + + # run checks and standardise format + ret <- check_params(ret) + + return(ret) +} + +#------------------------------------------------ +# Check that params dataframe is formatted correctly, and return in standardised +# format (init and block coerced to list) +#' @noRd +check_params <- function(x) { + + # check dataframe has correct elements + assert_dataframe(x) + assert_in(c("name", "min", "max"), names(x), + message = "df_params must contain the columns 'name', 'min', 'max'") + if (any(duplicated(x$name))) { + stop("parameter names must be unique") + } + use_init <- ("init" %in% names(x)) + use_block <- ("block" %in% names(x)) + + # coerce init and block to list + if (use_init) { + if (!is.list(x$init)) { + x$init <- as.list(x$init) + } + } + if (use_block) { + if (!is.list(x$block)) { + x$block <- as.list(x$block) + } + } + + # check each row in turn + for (i in seq_len(nrow(x))) { + + # check format + assert_single_string(x$name[i], message = "parameter names must be character strings") + assert_single_numeric(x$min[i], message = "min values must be single values") + assert_single_numeric(x$max[i], message = "min values must be single values") + if (use_init) { + assert_vector_numeric(x$init[[i]], message = "init values must be numeric") + } + if (use_block) { + assert_vector_numeric(x$block[[i]], message = "block values must be numeric") + } + + # check order + assert_leq(x$min[i], x$max[i], message = "min values must be less than or equal to max values") + if (use_init) { + assert_greq(x$init[[i]], x$min[i], message = "init values must be greater than or equal to min values") + assert_leq(x$init[[i]], x$max[i], message = "init values must be less than or equal to max values") + } + } + + return(x) +} + +#------------------------------------------------ +#' @title Run drjacoby MCMC +#' +#' @description Run MCMC either with or without parallel tempering turned on. +#' Minimum inputs include a data object, a data.frame of parameters, a +#' log-likelihood function and a log-prior function. Produces an object of +#' class \code{drjacoby_output}, which contains all MCMC output along with +#' some diagnostics and a record of inputs. +#' +#' @details Note that both \code{data} and \code{misc} are passed into +#' log-likelihood/log-prior functions *by reference*. This means if you modify +#' these objects inside the functions then any changes will persist. +#' +#' @param data a named list or data frame or data values. +#' @param df_params a data.frame of parameters (see \code{?define_params}). +#' @param misc optional list object passed to likelihood and prior. This can be +#' useful for passing values that are not strictly data, for example passing a +#' lookup table to the prior function. +#' @param loglike,logprior the log-likelihood and log-prior functions used in +#' the MCMC. Can either be passed in as R functions (not in quotes), or as +#' character strings naming compiled C++ functions (in quotes). +#' @param burnin the number of burn-in iterations. Automatic tuning of proposal +#' standard deviations is only active during the burn-in period. +#' @param samples the number of sampling iterations. +#' @param rungs the number of temperature rungs used in the parallel tempering +#' method. By default, \eqn{\beta} values are equally spaced between 0 and 1, +#' i.e. \eqn{\beta[i]=}\code{(i-1)/(rungs-1)} for \code{i} in \code{1:rungs}. +#' The likelihood for the \out{ith} heated chain is raised to the +#' power \eqn{\beta[i]^\alpha}, meaning we can use the \eqn{\alpha} parameter +#' to concentrate rungs towards the start or the end of the interval (see the +#' \code{alpha} argument). +#' @param chains the number of independent replicates of the MCMC to run. If a +#' \code{cluster} object is defined then these chains are run in parallel, +#' otherwise they are run in serial. +#' @param beta_manual vector of manually defined \eqn{\beta} values used in the +#' parallel tempering approach. If defined, this overrides the spacing defined +#' by \code{rungs}. Note that even manually defined \eqn{\beta} values are +#' raised to the power \eqn{\alpha} internally, hence you should set +#' \code{alpha = 1} if you want to fix \eqn{\beta} values exactly. +#' @param alpha the likelihood for the \out{ith} heated chain is +#' raised to the power \eqn{\beta[i]^\alpha}, meaning we can use the +#' \eqn{\alpha} parameter to concentrate rungs towards the start or the end of +#' the temperature scale. +#' @param target_acceptance Target acceptance rate. Should be between 0 and 1. +#' Default of 0.44, set as optimum for unvariate proposal distributions. +#' @param cluster option to pass in a cluster environment, allowing chains to be +#' run in parallel (see package "parallel"). +#' @param coupling_on whether to implement Metropolis-coupling over temperature +#' rungs. The option of deactivating coupling has been retained for general +#' interest and debugging purposes only. If this parameter is \code{FALSE} +#' then parallel tempering will have no impact on MCMC mixing. +#' @param pb_markdown whether to run progress bars in markdown mode, meaning +#' they are only updated when they reach 100\% to avoid large amounts of output +#' being printed to markdown files. +#' @param save_data if \code{TRUE} (the default) the raw input data is stored +#' for reference in the project output. This allows complete reproducibility +#' from a project, but may be undesirable when datasets are very large. +#' @param save_hot_draws if \code{TRUE} the parameter draws relating to the hot +#' chains are also stored inside the \code{pt} element of the project output. +#' If \code{FALSE} (the default) only log-likelihoods and log-priors are +#' stored from heated chains. +#' @param silent whether to suppress all console output. +#' +#' @importFrom utils txtProgressBar +#' @importFrom stats setNames var runif +#' @export + +run_mcmc <- function(data, + df_params, + misc = list(), + loglike, + logprior, + burnin = 1e3, + samples = 1e4, + rungs = 1, + chains = 5, + beta_manual = NULL, + alpha = 1.0, + target_acceptance = 0.44, + cluster = NULL, + coupling_on = TRUE, + pb_markdown = FALSE, + save_data = TRUE, + save_hot_draws = FALSE, + silent = FALSE) { + + # declare variables to avoid "no visible binding" issues + phase <- rung <- value <- chain <- link <- NULL + + # Cleanup pointers on exit + on.exit(gc()) + + # ---------- check inputs ---------- + + # check data + assert_list_named(data) + + # check misc + assert_list(misc) + + # check loglikelihood and logprior functions + assert_class(loglike, c("function", "character")) + assert_class(logprior, c("function", "character")) + + # check MCMC parameters + assert_single_pos_int(burnin, zero_allowed = FALSE) + assert_single_pos_int(samples, zero_allowed = FALSE) + assert_single_pos_int(rungs, zero_allowed = FALSE) + assert_single_pos_int(chains, zero_allowed = FALSE) + assert_single_logical(coupling_on) + assert_single_pos(alpha) + assert_bounded(target_acceptance, 0, 1) + + # check df_params + df_params <- check_params(df_params) + use_init <- ("init" %in% names(df_params)) + use_block <- ("block" %in% names(df_params)) + if (use_init) { + for (i in 1:nrow(df_params)) { + if (length(df_params$init[[i]]) != 1) { + assert_length(df_params$init[[i]], chains, message = paste0("must define one df_params$init value per parameter, ", + "or alternatively a list of values one for each chain")) + } + } + } + + # calculate/check final temperature vector + if (is.null(beta_manual)) { + beta_manual <- rev(seq(1, 0, l = rungs)) + } + rungs <- length(beta_manual) + assert_vector_bounded(beta_manual) + assert_increasing(beta_manual) + assert_eq(beta_manual[rungs], 1.0) + + # check misc parameters + if (!is.null(cluster)) { + assert_class(cluster, "cluster") + } + assert_single_logical(pb_markdown) + assert_single_logical(save_data) + assert_single_logical(save_hot_draws) + assert_single_logical(silent) + + + # ---------- pre-processing ---------- + + # calculate transformation type for each parameter + # 0 = [-Inf,Inf] -> phi = theta + # 1 = [-Inf,b] -> phi = log(b - theta) + # 2 = [a,Inf] -> phi = log(theta - a) + # 3 = [a,b] -> phi = log((theta - a)/(b - theta)) + df_params$trans_type <- 2*is.finite(df_params$min) + is.finite(df_params$max) + + # flag to skip over fixed parameters + skip_param <- (df_params$min == df_params$max) + + # define default init values + if (!use_init) { + init_list <- list() + for (i in 1:nrow(df_params)) { + p <- runif(chains) + if (df_params$trans_type[i] == 0) { + init_list[[i]] <- log(p) - log(1 - p) + } else if (df_params$trans_type[i] == 1) { + init_list[[i]] <- log(p) + df_params$max[i] + } else if (df_params$trans_type[i] == 2) { + init_list[[i]] <- df_params$min[i] - log(p) + } else if (df_params$trans_type[i] == 3) { + init_list[[i]] <- df_params$min[i] + (df_params$max[i] - df_params$min[i])*p + } + } + df_params$init <- init_list + } + + # define default blocks + if (!use_block) { + df_params$block <- as.list(rep(1, nrow(df_params))) + } + + # get initial values into matrix. Rows for parameters, columns for chains + init_mat <- do.call(rbind, mapply(function(x) { + if (length(x) == 1) { + rep(x, chains) + } else { + x + } + }, df_params$init, SIMPLIFY = FALSE)) + + # flag whether likelihood and/or prior are C++ functions + loglike_use_cpp <- inherits(loglike, "character") + logprior_use_cpp <- inherits(logprior, "character") + + # raise temperature vector to power + beta_raised <- beta_manual^alpha + + # make sure "block" is not an element of misc already being used, and if not + # create dummy element for storing current block + if (length(misc) > 0) { + assert_not_in("block", names(misc), message = "'block' is a reserved name within misc object") + } + misc$block <- -1 + + + # ---------- define argument lists ---------- + + # parameters to pass to C++ + args_params <- list(x = data, + misc = misc, + loglike_use_cpp = loglike_use_cpp, + logprior_use_cpp = logprior_use_cpp, + theta_min = df_params$min, + theta_max = df_params$max, + block = df_params$block, + n_block = max(unlist(df_params$block)), + trans_type = df_params$trans_type, + skip_param = skip_param, + burnin = burnin, + samples = samples, + rungs = rungs, + coupling_on = coupling_on, + beta_raised = beta_raised, + save_hot_draws = save_hot_draws, + pb_markdown = pb_markdown, + silent = silent, + target_acceptance = target_acceptance) + + # functions to pass to C++ + args_functions <- list(loglike = loglike, + logprior = logprior, + test_convergence = test_convergence, + update_progress = update_progress) + + # complete list of arguments + args <- list(args_params = args_params, + args_functions = args_functions) + + # create distinct argument sets over chains + parallel_args <- replicate(chains, args, simplify = FALSE) + for (i in 1:chains) { + parallel_args[[i]]$args_params$chain <- i + + # create named vector object for passing internally within C++ functions. + # Initial values defined separately for each chain + parallel_args[[i]]$args_params$theta_vector <- setNames(init_mat[,i], df_params$name) + } + + + # ---------- run MCMC ---------- + + # split into parallel and serial implementations + if (!is.null(cluster)) { + + # run in parallel + parallel::clusterEvalQ(cluster, library(drjacoby)) + output_raw <- parallel::clusterApplyLB(cl = cluster, parallel_args, deploy_chain) + + } else { + + # run in serial + output_raw <- lapply(parallel_args, deploy_chain) + } + + # print total runtime + chain_runtimes <- mapply(function(x) x$t_diff, output_raw) + if (!silent) { + message(sprintf("total MCMC run-time: %s seconds", signif(sum(chain_runtimes), 3))) + } + + + # ---------- process output ---------- + + # define names + chain_names <- 1:chains + rung_names <- 1:rungs + param_names <- df_params$name + + # get parameter draws into data.frame. This will be over all rungs if + # save_hot_draws is TRUE, otherwise it will only be over the cold chain + df_theta <- do.call(rbind, mapply(function(j) { + do.call(rbind, mapply(function(i) { + + theta_burnin <- do.call(rbind, output_raw[[j]]$theta_burnin[[i]]) |> + as.data.frame() |> + setNames(param_names) |> + mutate(chain = chain_names[j], + rung = rung_names[i], + phase = "burnin", .before = 1) + + theta_sampling <- do.call(rbind, output_raw[[j]]$theta_sampling[[i]]) |> + as.data.frame() |> + setNames(param_names) |> + mutate(chain = chain_names[j], + rung = rung_names[i], + phase = "sampling", .before = 1) + + ret <- theta_burnin |> + bind_rows(theta_sampling) |> + mutate(iteration = row_number(), .after = "phase") + + return(ret) + }, seq_len(ifelse(save_hot_draws, rungs, 1)), SIMPLIFY = FALSE)) + }, seq_len(chains), SIMPLIFY = FALSE)) + + # fix rungs field if save_hot_draws is FALSE + if (!save_hot_draws) { + df_theta$rung <- rungs + } + + # get likelihoods and priors over all rungs + df_pt <- do.call(rbind, mapply(function(j) { + do.call(rbind, mapply(function(i) { + + pt_burnin <- data.frame(chain = chain_names[j], + rung = rung_names[i], + phase = "burnin", + logprior = output_raw[[j]]$logprior_burnin[[i]], + loglikelihood = output_raw[[j]]$loglike_burnin[[i]]) + + pt_sampling <- data.frame(chain = chain_names[j], + rung = rung_names[i], + phase = "sampling", + logprior = output_raw[[j]]$logprior_sampling[[i]], + loglikelihood = output_raw[[j]]$loglike_sampling[[i]]) + + ret <- pt_burnin |> + bind_rows(pt_sampling) |> + mutate(iteration = row_number(), .after = "phase") + + return(ret) + }, seq_len(rungs), SIMPLIFY = FALSE)) + }, seq_len(chains), SIMPLIFY = FALSE)) + + # merge loglike and logprior for cold chain into main output + df_theta <- df_theta %>% + left_join(df_pt, by = c("chain", "rung", "phase", "iteration")) + + # if save_hot_draws = TRUE then merge theta values back into pt output + if (save_hot_draws) { + df_pt <- df_pt %>% + left_join(dplyr::select(df_theta, -.data$loglikelihood, -.data$logprior), by = c("chain", "rung", "phase", "iteration")) + } + + # drop rungs field from main output + df_output <- df_theta %>% + dplyr::filter(.data$rung == max(rungs)) %>% + dplyr::select(-.data$rung) + + # check for bad values in output + if (!all(is.finite(unlist(df_output[, param_names])))) { + stop("output contains non-finite values") + } + + # append to output list + output_processed <- list(output = df_output, + pt = df_pt) + + ## Diagnostics + output_processed$diagnostics <- list() + + # run-times + run_time <- data.frame(chain = chain_names, + seconds = chain_runtimes) + output_processed$diagnostics$run_time <- run_time + + # Rhat (Gelman-Rubin diagnostic) + if (chains > 1) { + rhat_est <- c() + for (p in seq_along(param_names)) { + rhat_est[p] <- df_output %>% + dplyr::filter(phase == "sampling") %>% + dplyr::select(chain, param_names[p]) %>% + gelman_rubin(chains = chains, samples = samples) + } + rhat_est[skip_param] <- NA + names(rhat_est) <- param_names + output_processed$diagnostics$rhat <- rhat_est + } + + # ESS + ess_est <- df_output %>% + dplyr::filter(phase == "sampling") %>% + dplyr::select(param_names) %>% + apply(2, coda::effectiveSize) + ess_est[skip_param] <- NA + output_processed$diagnostics$ess <- ess_est + + # Thermodynamic power + output_processed$diagnostics$rung_details <- data.frame(rung = 1:rungs, + thermodynamic_power = beta_raised) + + # Metropolis-coupling + # store acceptance rates between pairs of rungs (links) + mc_accept <- NA + if (rungs > 1) { + + # MC accept + mc_accept <- expand.grid(link = seq_len(rungs - 1), chain = chain_names) + mc_accept_burnin <- unlist(lapply(output_raw, function(x){x$mc_accept_burnin})) / burnin + mc_accept_sampling <- unlist(lapply(output_raw, function(x){x$mc_accept_sampling})) / samples + mc_accept <- rbind(cbind(mc_accept, phase = "burnin", value = mc_accept_burnin), + cbind(mc_accept, phase = "sampling", value = mc_accept_sampling)) + + } + output_processed$diagnostics$mc_accept <- mc_accept + + # DIC + DIC <- df_pt %>% + dplyr::filter(.data$phase == "sampling" & .data$rung == rungs) %>% + dplyr::select(.data$loglikelihood) %>% + dplyr::mutate(deviance = -2*.data$loglikelihood) %>% + dplyr::summarise(DIC = mean(.data$deviance) + 0.5*var(.data$deviance)) %>% + dplyr::pull(.data$DIC) + output_processed$diagnostics$DIC_Gelman <- DIC + + ## Parameters + data_store <- NULL + if (save_data) { + data_store <- data + } + output_processed$parameters <- list(data = data_store, + df_params = df_params, + loglike = loglike, + logprior = logprior, + burnin = burnin, + samples = samples, + rungs = rungs, + chains = chains, + coupling_on = coupling_on, + alpha = alpha, + beta_manual = beta_manual) + + # save output as custom class + class(output_processed) <- "drjacoby_output" + + # return + return(output_processed) +} + +#------------------------------------------------ +# deploy main_mcmc for this chain +#' @noRd +deploy_chain <- function(args) { + + # Specify pointers to cpp functions + if (args$args_params$loglike_use_cpp) { + args$args_functions$loglike <- create_xptr(args$args_functions$loglike) + } + if (args$args_params$logprior_use_cpp) { + args$args_functions$logprior <- create_xptr(args$args_functions$logprior) + } + + # get parameters + burnin <- args$args_params$burnin + samples <- args$args_params$samples + + # make progress bars + pb_burnin <- txtProgressBar(min = 0, max = burnin, initial = NA, style = 3) + pb_samples <- txtProgressBar(min = 0, max = samples, initial = NA, style = 3) + args$args_progress <- list(pb_burnin = pb_burnin, + pb_samples = pb_samples) + + # run C++ function + ret <- main_cpp(args) + + # remove arguments + rm(args) + + return(ret) +} + +#------------------------------------------------ +# update progress bar +# pb_list = list of progress bar objects +# name = name of this progress bar +# i = new value of bar +# max_i = max value of bar (close when reach this value) +# close = whether to close when reach end +#' @importFrom utils setTxtProgressBar +#' @noRd +update_progress <- function(pb_list, name, i, max_i, close = TRUE) { + setTxtProgressBar(pb_list[[name]], i) + if (i == max_i & close) { + close(pb_list[[name]]) + } +} + +# Deal with user input cpp not being defined +globalVariables(c("create_xptr")) diff --git a/R/plot.R b/R/plot.R index 595d96c..0f6772f 100644 --- a/R/plot.R +++ b/R/plot.R @@ -235,7 +235,7 @@ plot_autocorrelation <- function(x, lag = 20, par = NULL, chain = 1, phase = "sa } #------------------------------------------------ -#' @title Plot parameter estimates +#' @title Plot parameter trace #' #' @description Produce a series of plots corresponding to each parameter, #' including the raw trace, the posterior histogram and an autocorrelation @@ -256,9 +256,9 @@ plot_autocorrelation <- function(x, lag = 20, par = NULL, chain = 1, phase = "sa #' #' @export -plot_par <- function(x, show = NULL, hide = NULL, lag = 20, - downsample = TRUE, phase = "sampling", - chain = NULL, display = TRUE) { +plot_trace <- function(x, show = NULL, hide = NULL, lag = 20, + downsample = TRUE, phase = "sampling", + chain = NULL, display = TRUE) { # check inputs and define defaults assert_class(x, "drjacoby_output") @@ -373,20 +373,21 @@ plot_par <- function(x, show = NULL, hide = NULL, lag = 20, } #------------------------------------------------ -#' @title Plot parameter correlation +#' @title Produce bivariate scatterplot #' -#' @description Plots correlation between two parameters +#' @description Produces scatterplot between two named parameters. #' #' @inheritParams plot_rung_loglike #' @param parameter1 name of parameter first parameter. #' @param parameter2 name of parameter second parameter. -#' @param downsample whether to downsample output to speed up plotting. +#' @param downsample whether to downsample output to 200 values max to speed up +#' plotting. #' #' @export -plot_cor <- function(x, parameter1, parameter2, - downsample = TRUE, phase = "sampling", - chain = NULL) { +plot_scatter <- function(x, parameter1, parameter2, + downsample = TRUE, phase = "sampling", + chain = NULL) { # check inputs assert_class(x, "drjacoby_output") @@ -552,3 +553,90 @@ plot_cor_mat <- function(x, show = NULL, phase = "sampling", param_names = NULL) ggplot2::xlab("") + ggplot2::ylab("") } + +#------------------------------------------------ +#' @title Produce scatterplots between multiple parameters +#' +#' @description Uses \code{ggpairs} function from the \code{GGally} package to +#' produce scatterplots between all named parameters. +#' +#' @inheritParams plot_trace +#' +#' @importFrom GGally ggpairs wrap +#' @export +plot_pairs <- function(x, show = NULL, hide = NULL) { + + # check inputs + assert_class(x, "drjacoby_output") + + # avoid "no visible binding" note + chain <- NULL + + # subsample posterior draws + param_draws <- sample_chains(x, sample_n = 1e3, keep_chain_index = TRUE) + + # get parameter names and apply show and hide conditions + param_names <- setdiff(names(param_draws), c("chain", "sample")) + if (!is.null(show)) { + assert_string(show) + param_names <- intersect(param_names, show) + } + if (!is.null(hide)) { + assert_string(hide) + param_names <- setdiff(param_names, hide) + } + if (length(param_names) == 0) { + stop("no parameters remaining after applying show and hide") + } + + # produce plot + param_draws |> + ggpairs(aes(col = as.factor(chain)), + columns = param_names, + lower = list(continuous = wrap("points", size = 0.5))) + theme_bw() +} + +#------------------------------------------------ +#' @title Produce density plots +#' +#' @description Density plots of all parameters. Use \code{show} and \code{hide} +#' to be more specific about which parameters to plot. +#' +#' @inheritParams plot_trace +#' +#' @importFrom tidyr pivot_longer +#' @export +plot_density <- function(x, show = NULL, hide = NULL) { + + # avoid "no visible binding" notes + value <- NULL + + # check inputs + assert_class(x, "drjacoby_output") + + # subsample posterior draws + param_draws <- sample_chains(x, sample_n = 1e3, keep_chain_index = FALSE) + + # get parameter names and apply show and hide conditions + param_names <- setdiff(names(param_draws), c("chain", "sample")) + if (!is.null(show)) { + assert_string(show) + param_names <- intersect(param_names, show) + } + if (!is.null(hide)) { + assert_string(hide) + param_names <- setdiff(param_names, hide) + } + if (length(param_names) == 0) { + stop("no parameters remaining after applying show and hide") + } + + # produce plot + param_draws |> + select(all_of(param_names)) |> + pivot_longer(cols = everything()) |> + ggplot(aes(x = value)) + theme_bw() + + geom_density(fill = "blue", alpha = 0.5) + + facet_wrap(~name, scales = "free") + + xlab("Parameter value") + ylab("Probability density") +} diff --git a/R/samples.R b/R/samples.R index 98f14fe..583e6e5 100644 --- a/R/samples.R +++ b/R/samples.R @@ -1,41 +1,47 @@ -#------------------------------------------------ -# return 95% quantile -#' @importFrom stats quantile -#' @noRd -quantile_95 <- function(x) { - ret <- quantile(x, probs = c(0.025, 0.5, 0.975)) - names(ret) <- c("Q2.5", "Q50", "Q97.5") - return(ret) -} - -#------------------------------------------------ -#' Sample N draws from all available chains -#' -#' @param x an object of class \code{drjacoby_output} -#' @param sample_n An integer number of samples -#' -#' @return A data.frame of posterior samples -#' @export -sample_chains <- function(x, sample_n) { - - # check inputs - assert_class(x, "drjacoby_output") - assert_pos_int(sample_n, zero_allowed = FALSE) - - # Join chains - all_chains <- dplyr::filter(x$output, .data$phase == "sampling") %>% - dplyr::select(-.data$chain, -.data$iteration, -.data$phase, -.data$logprior, -.data$loglikelihood) - assert_leq(sample_n, nrow(all_chains)) - - # Sample chains - sampled_chains <- all_chains[seq.int(1, nrow(all_chains), length.out = sample_n),, drop = FALSE] - sampled_chains$sample <- 1:nrow(sampled_chains) - - # ESS - ess_est_sampled <- round(apply(sampled_chains[,1:(ncol(sampled_chains) - 1), drop = FALSE], 2, coda::effectiveSize)) - message("Effective sample size of sample has range: ", min(ess_est_sampled), - " to ", max(ess_est_sampled), ". See function ess to estimate.") - - return(sampled_chains) -} - +#------------------------------------------------ +# return 95% quantile +#' @importFrom stats quantile +#' @noRd +quantile_95 <- function(x) { + ret <- quantile(x, probs = c(0.025, 0.5, 0.975)) + names(ret) <- c("Q2.5", "Q50", "Q97.5") + return(ret) +} + +#------------------------------------------------ +#' Sample posterior draws from all available chains +#' +#' @param x an object of class \code{drjacoby_output}. +#' @param sample_n An integer number of samples. +#' @param keep_chain_index if \code{TRUE} then the column giving the chain is +#' retained. +#' +#' @return A data.frame of posterior samples +#' @export +sample_chains <- function(x, sample_n, keep_chain_index = FALSE) { + + # avoid "no visible binding" note + phase <- chain <- iteration <- logprior <- loglikelihood <- NULL + + # check inputs + assert_class(x, "drjacoby_output") + assert_pos_int(sample_n, zero_allowed = FALSE) + assert_single_logical(keep_chain_index) + + # join chains + all_chains <- x$output |> + filter(phase == "sampling") |> + select(-c(iteration, phase, logprior, loglikelihood)) + if (!keep_chain_index) { + all_chains <- all_chains |> + select(-chain) + } + assert_leq(sample_n, nrow(all_chains), message = sprintf("sample_n cannot exceed the total number of samples over all chains (%s)", nrow(all_chains))) + + # sample chains + sampled_chains <- all_chains[seq.int(1, nrow(all_chains), length.out = sample_n),, drop = FALSE] + sampled_chains$sample <- 1:nrow(sampled_chains) + + return(sampled_chains) +} + diff --git a/R_ignore/deploy.R b/R_ignore/deploy.R index 93e27b3..b8d0a05 100644 --- a/R_ignore/deploy.R +++ b/R_ignore/deploy.R @@ -44,4 +44,6 @@ mcmc <- run_mcmc(data = list(x = x), burnin = 1e3, samples = 1e3) -plot_par(mcmc, show = "mu", phase = "burnin") +plot_trace(mcmc, show = "mu", phase = "burnin") + +plot_density(mcmc) diff --git a/docs/404.html b/docs/404.html index 8516f01..c5204d0 100644 --- a/docs/404.html +++ b/docs/404.html @@ -1,66 +1,27 @@ - - - - + + + + - Page not found (404) • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - -
-
- + +
+ + + - - -
+
+
-
+ + - - diff --git a/docs/LICENSE-text.html b/docs/LICENSE-text.html index 8771165..b155550 100644 --- a/docs/LICENSE-text.html +++ b/docs/LICENSE-text.html @@ -1,66 +1,12 @@ - - - - - - - -License • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -License • drjacoby - - - - + + -
-
- -
- -
+
-
MIT License
-
-Copyright (c) 2019 MRC Centre for Outbreak Analysis and Modelling
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+
YEAR: 2024
+COPYRIGHT HOLDER: MRC Centre for Outbreak Analysis and Modelling
 
+
-
- - + + diff --git a/docs/LICENSE.html b/docs/LICENSE.html new file mode 100644 index 0000000..a9fdeec --- /dev/null +++ b/docs/LICENSE.html @@ -0,0 +1,124 @@ + +NA • drjacoby + + +
+
+ + + +
+
+ + + +

MIT License

+

Copyright (c) 2024 MRC Centre for Outbreak Analysis and Modelling

+

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

+

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

+

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ + +
+ + + +
+ + + +
+ + + + + + + + diff --git a/docs/articles/blocks.html b/docs/articles/blocks.html index dfa0a2a..7965775 100644 --- a/docs/articles/blocks.html +++ b/docs/articles/blocks.html @@ -6,7 +6,7 @@ Using Likelihood Blocks • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

Once we are familiar with the basics, there are some more advanced drjacoby techniques that can be useful when running computationally intensive likelihoods, or when fitting multi-level models. These rely on the fact that drjacoby works by breaking the likelihood down into a series of independent blocks, which are combined to produce the overall likelihood.

-

This vignette demonstrates the use of likelihood blocks to fit a simple random-effects model.

-
-

-Problem motivation

-

For this example we will work with the chickwts dataset (one of the datasets that comes with R), which lists the weights of various chicks broken down by the feed they were given.

-
-head(chickwts, 12)
-#>    weight      feed
-#> 1     179 horsebean
-#> 2     160 horsebean
-#> 3     136 horsebean
-#> 4     227 horsebean
-#> 5     217 horsebean
-#> 6     168 horsebean
-#> 7     108 horsebean
-#> 8     124 horsebean
-#> 9     143 horsebean
-#> 10    140 horsebean
-#> 11    309   linseed
-#> 12    229   linseed
-

We will pass this into drjacoby as a list over groups:

+
#> Registered S3 method overwritten by 'GGally':
+#>   method from   
+#>   +.gg   ggplot2
+

Once we are familiar with the +basics, there are some more advanced drjacoby techniques +that can be useful when running computationally intensive likelihoods, +or when fitting multi-level models. These rely on the fact that +drjacoby works by breaking the likelihood down into a series of +independent blocks, which are combined to produce the overall +likelihood.

+

This vignette demonstrates the use of likelihood blocks to fit a +simple random-effects model.

+
+

Problem motivation +

+

For this example we will work with the chickwts dataset +(one of the datasets that comes with R), which lists the weights of +various chicks broken down by the feed they were given.

-data_list <- split(chickwts$weight, f = chickwts$feed)
-data_list
-#> $casein
-#>  [1] 368 390 379 260 404 318 352 359 216 222 283 332
-#> 
-#> $horsebean
-#>  [1] 179 160 136 227 217 168 108 124 143 140
-#> 
-#> $linseed
-#>  [1] 309 229 181 141 260 203 148 169 213 257 244 271
-#> 
-#> $meatmeal
-#>  [1] 325 257 303 315 380 153 263 242 206 344 258
-#> 
-#> $soybean
-#>  [1] 243 230 248 327 329 250 193 271 316 267 199 171 158 248
-#> 
-#> $sunflower
-#>  [1] 423 340 392 339 341 226 320 295 334 322 297 318
-

There are 6 feed groups in total, and we will assume that values within a group are normally distributed each with a distinct unknown mean, leading to 6 parameters \(\mu_1\) to \(\mu_6\). For simplicity we will assume the same standard deviation \(\sigma\) in all groups. To make this a random effects framework, we will assume that \(\mu_1\) to \(\mu_6\) are themselves random draws from a normal distribution with mean \(\phi\) and standard deviation \(\tau\). Finally, we will put diffuse priors on \(\sigma\), \(\phi\) and \(\tau\) so that the data can “speak for itself”. For the more mathematically minded, the complete model can be written as follows:

+head(chickwts, 12) +#> weight feed +#> 1 179 horsebean +#> 2 160 horsebean +#> 3 136 horsebean +#> 4 227 horsebean +#> 5 217 horsebean +#> 6 168 horsebean +#> 7 108 horsebean +#> 8 124 horsebean +#> 9 143 horsebean +#> 10 140 horsebean +#> 11 309 linseed +#> 12 229 linseed
+

We will pass this into drjacoby as a list over groups:

+
+data_list <- split(chickwts$weight, f = chickwts$feed)
+data_list
+#> $casein
+#>  [1] 368 390 379 260 404 318 352 359 216 222 283 332
+#> 
+#> $horsebean
+#>  [1] 179 160 136 227 217 168 108 124 143 140
+#> 
+#> $linseed
+#>  [1] 309 229 181 141 260 203 148 169 213 257 244 271
+#> 
+#> $meatmeal
+#>  [1] 325 257 303 315 380 153 263 242 206 344 258
+#> 
+#> $soybean
+#>  [1] 243 230 248 327 329 250 193 271 316 267 199 171 158 248
+#> 
+#> $sunflower
+#>  [1] 423 340 392 339 341 226 320 295 334 322 297 318
+

There are 6 feed groups in total, and we will assume that values +within a group are normally distributed each with a distinct unknown +mean, leading to 6 parameters \(\mu_1\) +to \(\mu_6\). For simplicity we will +assume the same standard deviation \(\sigma\) in all groups. To make this a +random effects framework, we will assume that \(\mu_1\) to \(\mu_6\) are themselves random draws from a +normal distribution with mean \(\phi\) +and standard deviation \(\tau\). +Finally, we will put diffuse priors on \(\sigma\), \(\phi\) and \(\tau\) so that the data can “speak for +itself”. For the more mathematically minded, the complete model can be +written as follows:

\[ \begin{aligned} - x_{i,j} &\sim Normal(\mu_i, \sigma^2) \hspace{5mm} \textrm{for } i \in 1:6, \\ - \mu_i &\sim Normal(\phi, \tau^2) \hspace{5mm} \textrm{for } i \in 1:6, \\ + x_{i,j} &\sim Normal(\mu_i, \sigma^2) \hspace{5mm} \textrm{for } i +\in 1:6, \\ + \mu_i &\sim Normal(\phi, \tau^2) \hspace{5mm} \textrm{for } i \in +1:6, \\ \sigma &\sim Gamma(0.01, 100), \\ \phi &\sim Normal(0, 1000), \\ \tau &\sim Gamma(0.01, 100) \end{aligned} -\] One way to fit this model in drjacoby is to write a likelihood function that simply loops through all the data and calculates the probability as a function of the parameters. However, this is very wasteful - if we are updating the parameter \(\mu_1\) then we only need to know the observed chick weights in feed group 1, along with the current estimates of \(\sigma\), \(\phi\) and \(\tau\). The same is true for all other \(\mu_i\) values, which only depend on a small subset of the data. In this example the inefficiency probably doesn’t matter too much, because the dataset is small and our likelihood is fast enough that we can brute force the problem, but it’s easy to imagine situations where this wouldn’t be the case. A better way to approach this problem is through likelihood blocks.

+\] One way to fit this model in drjacoby is to write a +likelihood function that simply loops through all the data and +calculates the probability as a function of the parameters. However, +this is very wasteful - if we are updating the parameter \(\mu_1\) then we only need to know the +observed chick weights in feed group 1, along with the current estimates +of \(\sigma\), \(\phi\) and \(\tau\). The same is true for all other +\(\mu_i\) values, which only depend on +a small subset of the data. In this example the inefficiency probably +doesn’t matter too much, because the dataset is small and our likelihood +is fast enough that we can brute force the problem, but it’s easy to +imagine situations where this wouldn’t be the case. A better way to +approach this problem is through likelihood blocks.

-
-

-Defining blocks

-

You can imagine likelihood blocks as a vector of values, each of which gives the log-likelihood of one small part of the model. The overall log-likelihood is simply the sum over this vector. We are free to define as many blocks as we like, and the exact number we need will depend on the model design. A good way of figuring out how many blocks you need is to sketch out a “conditional dependence” table like the one shown below.

+
+

Defining blocks +

+

You can imagine likelihood blocks as a vector of values, each of +which gives the log-likelihood of one small part of the model. The +overall log-likelihood is simply the sum over this vector. We are free +to define as many blocks as we like, and the exact number we need will +depend on the model design. A good way of figuring out how many blocks +you need is to sketch out a “conditional dependence” table like the one +shown below.

-

In the first column we list all the data and free parameters of our model, and in the second column we list every parameter on which the first column depends (“depends” in this context means we need to know this parameter value in order to write down the probability). Don’t put data or fixed values in the second column, only free parameters, i.e. those we are trying to infer. We need one block for every unique combination of parameters in the second column. For example, \(\mu_1\) and \(\mu_2\) depend on the same combination of parameters (\(\phi\) and \(\tau\)) therefore they belong in the same block. We find that we need seven blocks in this example.

-

We define blocks in drjacoby using the optional “block” column of the parameters dataframe. For each parameter you should list all the blocks in which it can be found - for example \(\mu_1\) can be found in blocks 1 and 7 in the table above:

-
-# define parameters dataframe
-df_params <- define_params(name = "mu1", min = 0, max = Inf, init = 1, block = c(1, 7),
-                           name = "mu2", min = 0, max = Inf, init = 1, block = c(2, 7),
-                           name = "mu3", min = 0, max = Inf, init = 1, block = c(3, 7),
-                           name = "mu4", min = 0, max = Inf, init = 1, block = c(4, 7),
-                           name = "mu5", min = 0, max = Inf, init = 1, block = c(5, 7),
-                           name = "mu6", min = 0, max = Inf, init = 1, block = c(6, 7),
-                           name = "sigma", min = 0, max = Inf, init = 1, block = 1:6,
-                           name = "phi", min = -Inf, max = Inf, init = 0, block = 7,
-                           name = "tau", min = 0, max = Inf, init = 1, block = 7)
-
-df_params
-#>    name  min max init            block
-#> 1   mu1    0 Inf    1             1, 7
-#> 2   mu2    0 Inf    1             2, 7
-#> 3   mu3    0 Inf    1             3, 7
-#> 4   mu4    0 Inf    1             4, 7
-#> 5   mu5    0 Inf    1             5, 7
-#> 6   mu6    0 Inf    1             6, 7
-#> 7 sigma    0 Inf    1 1, 2, 3, 4, 5, 6
-#> 8   phi -Inf Inf    0                7
-#> 9   tau    0 Inf    1                7
+

In the first column we list all the data and free parameters of our +model, and in the second column we list every parameter on which the +first column depends (“depends” in this context means we need to know +this parameter value in order to write down the probability). Don’t put +data or fixed values in the second column, only free parameters, +i.e. those we are trying to infer. We need one block for every +unique combination of parameters in the second column. For +example, \(\mu_1\) and \(\mu_2\) depend on the same combination of +parameters (\(\phi\) and \(\tau\)) therefore they belong in the same +block. We find that we need seven blocks in this example.

+

We define blocks in drjacoby using the optional “block” +column of the parameters dataframe. For each parameter you should list +all the blocks in which it can be found - for example \(\mu_1\) can be found in blocks 1 and 7 in +the table above:

+
+# define parameters dataframe
+df_params <- define_params(name = "mu1", min = 0, max = Inf, init = 1, block = c(1, 7),
+                           name = "mu2", min = 0, max = Inf, init = 1, block = c(2, 7),
+                           name = "mu3", min = 0, max = Inf, init = 1, block = c(3, 7),
+                           name = "mu4", min = 0, max = Inf, init = 1, block = c(4, 7),
+                           name = "mu5", min = 0, max = Inf, init = 1, block = c(5, 7),
+                           name = "mu6", min = 0, max = Inf, init = 1, block = c(6, 7),
+                           name = "sigma", min = 0, max = Inf, init = 1, block = 1:6,
+                           name = "phi", min = -Inf, max = Inf, init = 0, block = 7,
+                           name = "tau", min = 0, max = Inf, init = 1, block = 7)
+
+df_params
+#>    name  min max init            block
+#> 1   mu1    0 Inf    1             1, 7
+#> 2   mu2    0 Inf    1             2, 7
+#> 3   mu3    0 Inf    1             3, 7
+#> 4   mu4    0 Inf    1             4, 7
+#> 5   mu5    0 Inf    1             5, 7
+#> 6   mu6    0 Inf    1             6, 7
+#> 7 sigma    0 Inf    1 1, 2, 3, 4, 5, 6
+#> 8   phi -Inf Inf    0                7
+#> 9   tau    0 Inf    1                7
-
-

-The Likelihood

-

Next we need to access these blocks inside the likelihood function. The current block is stored within the misc object, inside an element called “block”. We can access this value in a C++ function using misc["block"], or in an R function using misc$block. Once we know the current block we can vary the likelihood accordingly. Our objective is to calculate and return just the log-likelihood of this block.

-

Take a look at the following likelihood function, which takes this approach:

+
+

The Likelihood +

+

Next we need to access these blocks inside the likelihood function. +The current block is stored within the misc object, inside +an element called “block”. We can access this value in a C++ function +using misc["block"], or in an R function using +misc$block. Once we know the current block we can vary the +likelihood accordingly. Our objective is to calculate and return just +the log-likelihood of this block.

+

Take a look at the following likelihood function, which takes this +approach:

    -
  1. Unpack each of the parameters from the params object
  2. +
  3. Unpack each of the parameters from the params +object
  4. Find out which block we are in using misc["block"]
  5. Split based on block:
      -
    1. For blocks 1:6, extract data corresponding to this block and calculate likelihood
    2. -
    3. For block 7, calculate likelihood over all mu parameters
    4. +
    5. For blocks 1:6, extract data corresponding to this block and +calculate likelihood
    6. +
    7. For block 7, calculate likelihood over all mu +parameters
@@ -294,25 +360,38 @@

stop("cpp function %i not found", function_name); } -

If we were to run this likelihood for blocks 1 through 7 and sum the values, we would obtain the full model log-likelihood.

-

For the prior function we don’t need to worry about blocks, we just need to apply the appropriate distributions:

+

If we were to run this likelihood for blocks 1 through 7 and sum the +values, we would obtain the full model log-likelihood.

+

For the prior function we don’t need to worry about blocks, we just +need to apply the appropriate distributions:

We can then run the MCMC as normal:

-
-# run MCMC
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e4,
-                 chains = 5,
-                 silent = TRUE)
-

Assuming we are happy with MCMC performance (convergence, ESS etc.), we can explore the posterior credible intervals of all parameters:

+# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e4, + chains = 5, + silent = TRUE)

+

Assuming we are happy with MCMC performance (convergence, ESS etc.), +we can explore the posterior credible intervals of all parameters:

+

-

Here we obtain a different mean estimate for each group, with the global mean (phi) being somewhere near the centre. If you compare the mu estimates to the raw mean of the data in each group you will find that they are “shrunk” towards the global mean. This is the impact of the random effects model - by sharing information between groups we end up pulling everything towards the centre.

-

Likelihood blocks take some getting used to, but for complex models they can make life much easier and be much more efficient. In reality drjacoby always uses blocks internally, it just places all parameters into block 1 if not specified by the user. There are no checks performed on the blocks that you define, so make sure your blocks are independent and cover the complete likelihood of the model.

+

Here we obtain a different mean estimate for each group, with the +global mean (phi) being somewhere near the centre. If you +compare the mu estimates to the raw mean of the data in +each group you will find that they are “shrunk” towards the global mean. +This is the impact of the random effects model - by sharing information +between groups we end up pulling everything towards the centre.

+

Likelihood blocks take some getting used to, but for complex models +they can make life much easier and be much more efficient. In reality +drjacoby always uses blocks internally, it just places all +parameters into block 1 if not specified by the user. There are no +checks performed on the blocks that you define, so make sure your blocks +are independent and cover the complete likelihood of the model.

@@ -327,11 +406,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -340,5 +421,7 @@

+ + diff --git a/docs/articles/blocks_files/figure-html/unnamed-chunk-9-1.png b/docs/articles/blocks_files/figure-html/unnamed-chunk-9-1.png index e7c629f..8fe0721 100644 Binary files a/docs/articles/blocks_files/figure-html/unnamed-chunk-9-1.png and b/docs/articles/blocks_files/figure-html/unnamed-chunk-9-1.png differ diff --git a/docs/articles/checks_double_well.html b/docs/articles/checks_double_well.html index ba58d59..bafaf6f 100644 --- a/docs/articles/checks_double_well.html +++ b/docs/articles/checks_double_well.html @@ -6,7 +6,7 @@ Double well • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

Purpose: to compare drjacoby results for a challenging problem involving a multimodal posterior, both with and without temperature rungs.

-
-

-Model

-

We assume a single parameter mu drawn from a double well potential distribution, defined by the formula:

+
## Registered S3 method overwritten by 'GGally':
+##   method from   
+##   +.gg   ggplot2
+

Purpose: to compare drjacoby results for a +challenging problem involving a multimodal posterior, both with and +without temperature rungs.

+
+

Model +

+

We assume a single parameter mu drawn from a double well +potential distribution, defined by the formula:

\[ \begin{aligned} \mu &\propto exp\left(-\gamma(\mu^2 - 1)^2\right) \end{aligned} -\] where \(\gamma\) is a parameter that defines the strength of the well (higher \(\gamma\) leads to a deeper valley and hence more challenging problem). NB, there is no data in this example, as the likelihood is defined exactly by these parameters.

+\] where \(\gamma\) is a +parameter that defines the strength of the well (higher \(\gamma\) leads to a deeper valley and hence +more challenging problem). NB, there is no data in this example, as the +likelihood is defined exactly by these parameters.

Likelihood and prior:

Parameters dataframe:

-
-L <- 2
-gamma <- 30
-df_params <- define_params(name = "mu", min = -L, max = L,
-                           name = "gamma", min = gamma, max = gamma)
-
-
-

-Single temperature rung (no Metropolis coupling)

-mcmc <- run_mcmc(data = list(x = -1),
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e5,
-                 chains = 1,
-                 rungs = 1,
-                 silent = TRUE)
+L <- 2 +gamma <- 30 +df_params <- define_params(name = "mu", min = -L, max = L, + name = "gamma", min = gamma, max = gamma)
+
+
+

Single temperature rung (no Metropolis coupling) +

-# trace plot
-plot_par(mcmc, show = "mu")
-

+mcmc <- run_mcmc(data = list(x = -1), + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e5, + chains = 1, + rungs = 1, + silent = TRUE)
-# extract posterior draws
-output_sub <- subset(mcmc$output, phase == "sampling")
-mu_draws <- output_sub$mu
-
-# get analytical solution
-x <- seq(-L, L, l = 1001)
-fx <- exp(-gamma*(x^2 - 1)^2)
-fx <- fx / sum(fx) * 1/(x[2]-x[1])
-
-# overlay plots
-hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black")
-lines(x, fx, col = 2, lwd = 2)
+# trace plot +plot_trace(mcmc, show = "mu")
+

+
+# extract posterior draws
+output_sub <- subset(mcmc$output, phase == "sampling")
+mu_draws <- output_sub$mu
+
+# get analytical solution
+x <- seq(-L, L, l = 1001)
+fx <- exp(-gamma*(x^2 - 1)^2)
+fx <- fx / sum(fx) * 1/(x[2]-x[1])
+
+# overlay plots
+hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black")
+lines(x, fx, col = 2, lwd = 2)

-
-

-Multiple temperature rungs

-
-mcmc <- run_mcmc(data = list(x = -1),
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e5,
-                 chains = 1,
-                 rungs = 11,
-                 alpha = 2,
-                 pb_markdown = TRUE)
-
## MCMC chain 1
-## burn-in
-## 
-  |                                                                            
-  |======================================================================| 100%
-## acceptance rate: 21.7%
-## sampling phase
-## 
-  |                                                                            
-  |======================================================================| 100%
-## acceptance rate: 22%
-## chain completed in 4.397968 seconds
-
## total MCMC run-time: 4.4 seconds
-
-# trace plot
-plot_par(mcmc, show = "mu")
-

+
+

Multiple temperature rungs +

+
+mcmc <- run_mcmc(data = list(x = -1),
+                 df_params = df_params,
+                 loglike = "loglike",
+                 logprior = "logprior",
+                 burnin = 1e3,
+                 samples = 1e5,
+                 chains = 1,
+                 rungs = 11,
+                 alpha = 2,
+                 pb_markdown = TRUE)
+
## MCMC chain 1
+## burn-in
+##   |                                                                              |======================================================================| 100%
+## acceptance rate: 21.7%
+## sampling phase
+##   |                                                                              |======================================================================| 100%
+## acceptance rate: 22%
+## chain completed in 2.187709 seconds
+
## total MCMC run-time: 2.19 seconds
-# coupling acceptance plot
-plot_mc_acceptance(mcmc)
-

+# trace plot +plot_trace(mcmc, show = "mu")
+

-# extract posterior draws
-output_sub <- subset(mcmc$output, phase == "sampling")
-mu_draws <- output_sub$mu
-
-# overlay plots
-hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black")
-lines(x, fx, col = 2, lwd = 2)
+# coupling acceptance plot +plot_mc_acceptance(mcmc)
+

+
+# extract posterior draws
+output_sub <- subset(mcmc$output, phase == "sampling")
+mu_draws <- output_sub$mu
+
+# overlay plots
+hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black")
+lines(x, fx, col = 2, lwd = 2)

@@ -229,11 +237,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -242,5 +252,7 @@

+ + diff --git a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-5-1.png b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-5-1.png index f6a71a8..543c4fc 100644 Binary files a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-5-1.png and b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-5-1.png differ diff --git a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-6-1.png b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-6-1.png index 7983936..9e74a7f 100644 Binary files a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-6-1.png and b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-6-1.png differ diff --git a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-1.png b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-1.png index 79cca73..2c626c7 100644 Binary files a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-1.png and b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-1.png differ diff --git a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-2.png b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-2.png index fb7090b..f19ebd9 100644 Binary files a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-2.png and b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-8-2.png differ diff --git a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-9-1.png b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-9-1.png index e5aaa97..a3f721d 100644 Binary files a/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-9-1.png and b/docs/articles/checks_double_well_files/figure-html/unnamed-chunk-9-1.png differ diff --git a/docs/articles/checks_multilevel_blocks.html b/docs/articles/checks_multilevel_blocks.html index 3415a2c..3f13d5c 100644 --- a/docs/articles/checks_multilevel_blocks.html +++ b/docs/articles/checks_multilevel_blocks.html @@ -6,7 +6,7 @@ Multilevel example with blocks • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

Purpose: to compare drjacoby infered posterior distribution against the exact analytical solution in a multi-level model implemented via likelihood blocks.

-
-

-Model

-

There are g groups with means mu_1 to mu_g. These component means are drawn from a normal distribution with mean 0 and sd 1.0. Within each group there are n observations drawn around each component mean from normal disribution with sd 1.0.

-
-g <- 5
-mu <- rnorm(g)
-
-n <- 5
-data_list <- list()
-for (i in 1:g) {
-  data_list[[i]] <- rnorm(n, mean = mu[i])
-}
-names(data_list) <- sprintf("group%s", 1:g)
+
## Registered S3 method overwritten by 'GGally':
+##   method from   
+##   +.gg   ggplot2
+

Purpose: to compare drjacoby infered +posterior distribution against the exact analytical solution in a +multi-level model implemented via likelihood blocks.

+
+

Model +

+

There are g groups with means mu_1 to +mu_g. These component means are drawn from a normal +distribution with mean 0 and sd 1.0. Within each group there are +n observations drawn around each component mean from normal +disribution with sd 1.0.

+
+g <- 5
+mu <- rnorm(g)
+
+n <- 5
+data_list <- list()
+for (i in 1:g) {
+  data_list[[i]] <- rnorm(n, mean = mu[i])
+}
+names(data_list) <- sprintf("group%s", 1:g)

Likelihood and prior:

Parameters dataframe:

-
-L <- 5
-df_params <- define_params(name = "mu_1", min = -L, max = L, block = c(1, 6),
-                           name = "mu_2", min = -L, max = L, block = c(2, 6),
-                           name = "mu_3", min = -L, max = L, block = c(3, 6),
-                           name = "mu_4", min = -L, max = L, block = c(4, 6),
-                           name = "mu_5", min = -L, max = L, block = c(5, 6))
-
-
-

-MCMC

-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e5,
-                 chains = 10,
-                 silent = TRUE)
+L <- 5 +df_params <- define_params(name = "mu_1", min = -L, max = L, block = c(1, 6), + name = "mu_2", min = -L, max = L, block = c(2, 6), + name = "mu_3", min = -L, max = L, block = c(3, 6), + name = "mu_4", min = -L, max = L, block = c(4, 6), + name = "mu_5", min = -L, max = L, block = c(5, 6))
+
+
+

MCMC +

+
+mcmc <- run_mcmc(data = data_list,
+                 df_params = df_params,
+                 loglike = "loglike",
+                 logprior = "logprior",
+                 burnin = 1e3,
+                 samples = 1e5,
+                 chains = 10,
+                 silent = TRUE)
-
-

-Plots

+
+

Plots +

Black = posterior draws

Red = analytical solution to multi-level model

-

Green = analytical solution assuming independent groups (no second level)

-
-# extract sampling draws
-output_sub <- subset(mcmc$output, phase == "sampling")
-
-for (i in 1:5) {
-  # get posterior draws
-  mu_draws <- output_sub[[sprintf("mu_%s", i)]]
-  
-  # get analytical solution for this group
-  x <- seq(-L, L, l = 1001)
-  m <- mean(data_list[[i]])
-  fx <- dnorm(x, mean = m * n/(n + 1), sd = sqrt(1/(n + 1)))
-  
-  # get analytical solution if no multi-level model
-  fx2 <- dnorm(x, mean = m, sd = sqrt(1/n))
-  
-  # overlay plots
-  hist(mu_draws, breaks = seq(-L, L, l = 1001), probability = TRUE, col = "black",
-       main = sprintf("mu_%s", i))
-  lines(x, fx, col = 2, lwd = 4)
-  lines(x, fx2, col = 3, lwd = 4)
-}
+

Green = analytical solution assuming independent groups (no second +level)

+
+# extract sampling draws
+output_sub <- subset(mcmc$output, phase == "sampling")
+
+for (i in 1:5) {
+  # get posterior draws
+  mu_draws <- output_sub[[sprintf("mu_%s", i)]]
+  
+  # get analytical solution for this group
+  x <- seq(-L, L, l = 1001)
+  m <- mean(data_list[[i]])
+  fx <- dnorm(x, mean = m * n/(n + 1), sd = sqrt(1/(n + 1)))
+  
+  # get analytical solution if no multi-level model
+  fx2 <- dnorm(x, mean = m, sd = sqrt(1/n))
+  
+  # overlay plots
+  hist(mu_draws, breaks = seq(-L, L, l = 1001), probability = TRUE, col = "black",
+       main = sprintf("mu_%s", i))
+  lines(x, fx, col = 2, lwd = 4)
+  lines(x, fx2, col = 3, lwd = 4)
+}

@@ -202,11 +215,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -215,5 +230,7 @@

+ + diff --git a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-1.png b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-1.png index eda989f..8cf251a 100644 Binary files a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-1.png and b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-1.png differ diff --git a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-2.png b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-2.png index f67d737..d9e48e3 100644 Binary files a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-2.png and b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-2.png differ diff --git a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-3.png b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-3.png index 56081b4..28e3935 100644 Binary files a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-3.png and b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-3.png differ diff --git a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-4.png b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-4.png index 6d1d5fe..eb491c4 100644 Binary files a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-4.png and b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-4.png differ diff --git a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-5.png b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-5.png index 46910dc..a481718 100644 Binary files a/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-5.png and b/docs/articles/checks_multilevel_blocks_files/figure-html/unnamed-chunk-5-5.png differ diff --git a/docs/articles/checks_normal_model.html b/docs/articles/checks_normal_model.html index 6bdd22d..8309c2f 100644 --- a/docs/articles/checks_normal_model.html +++ b/docs/articles/checks_normal_model.html @@ -6,7 +6,7 @@ Normal model • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

Purpose: to compare drjacoby infered posterior distribution against the exact analytical solution for a very simple model.

-
-

-Model

-

A vector of data x drawn from a normal distribution with unknown mean mu and known variance of 1.0.

-
-n <- 1e2
-data_list <- list(x = rnorm(n))
+
## Registered S3 method overwritten by 'GGally':
+##   method from   
+##   +.gg   ggplot2
+

Purpose: to compare drjacoby infered +posterior distribution against the exact analytical solution for a very +simple model.

+
+

Model +

+

A vector of data x drawn from a normal distribution with +unknown mean mu and known variance of 1.0.

+
+n <- 1e2
+data_list <- list(x = rnorm(n))

Likelihood and prior:

Parameters dataframe:

-
-df_params <- define_params(name = "mu", min = -1, max = 1)
-
-
-

-MCMC

-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e5,
-                 chains = 10,
-                 silent = TRUE)
+df_params <- define_params(name = "mu", min = -1, max = 1)
-
-

-Posterior plots

+
+

MCMC +

-# extract sampling draws
-mu_draws <- subset(mcmc$output, phase = "sampling")$mu
-
-# calculate analytical solution
-x <- seq(-1, 1, l = 1001)
-fx <- dnorm(x, mean = mean(data_list$x), sd = sqrt(1/n))
-
-# histogram and overlay analytical
-hist(mu_draws, breaks = seq(-1, 1, 0.01), probability = TRUE, col = "black")
-lines(x, fx, col = 2, lwd = 4)
+mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e5, + chains = 10, + silent = TRUE)
+
+
+

Posterior plots +

+
+# extract sampling draws
+mu_draws <- subset(mcmc$output, phase = "sampling")$mu
+
## Warning: In subset.data.frame(mcmc$output, phase = "sampling") :
+##  extra argument 'phase' will be disregarded
+
+# calculate analytical solution
+x <- seq(-1, 1, l = 1001)
+fx <- dnorm(x, mean = mean(data_list$x), sd = sqrt(1/n))
+
+# histogram and overlay analytical
+hist(mu_draws, breaks = seq(-1, 1, 0.01), probability = TRUE, col = "black")
+lines(x, fx, col = 2, lwd = 4)

@@ -176,11 +187,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -189,5 +202,7 @@

+ + diff --git a/docs/articles/checks_normal_model_files/figure-html/unnamed-chunk-5-1.png b/docs/articles/checks_normal_model_files/figure-html/unnamed-chunk-5-1.png index af484c1..483ec12 100644 Binary files a/docs/articles/checks_normal_model_files/figure-html/unnamed-chunk-5-1.png and b/docs/articles/checks_normal_model_files/figure-html/unnamed-chunk-5-1.png differ diff --git a/docs/articles/checks_return_prior.html b/docs/articles/checks_return_prior.html index 3fa863d..6aea24a 100644 --- a/docs/articles/checks_return_prior.html +++ b/docs/articles/checks_return_prior.html @@ -6,7 +6,7 @@ Return prior • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

Purpose: to check that drjacoby returns the prior distribution when no likelihood is used.

-
-

-Model

-

Four parameters, each representing a different one of the four possible parameter transformations internally. Each parameter is given a suitable informative prior over it’s domain.

+
## Registered S3 method overwritten by 'GGally':
+##   method from   
+##   +.gg   ggplot2
+

Purpose: to check that drjacoby returns the +prior distribution when no likelihood is used.

+
+

Model +

+

Four parameters, each representing a different one of the four +possible parameter transformations internally. Each parameter is given a +suitable informative prior over it’s domain.

Parameters dataframe:

-
-df_params <- define_params(name = "real_line", min = -Inf, max = Inf,
-                           name = "neg_line", min = -Inf, max = 0,
-                           name = "pos_line", min = 0, max = Inf,
-                           name = "unit_interval", min = 0, max = 1)
-

Likelihood and prior:

-
-
-

-Run MCMC

-mcmc <- run_mcmc(data = list(x = -1),
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e5,
-                 chains = 10,
-                 silent = TRUE)
+df_params <- define_params(name = "real_line", min = -Inf, max = Inf, + name = "neg_line", min = -Inf, max = 0, + name = "pos_line", min = 0, max = Inf, + name = "unit_interval", min = 0, max = 1)
+

Likelihood and prior:

-
-

-Plots

+
+

Run MCMC +

-output_sub <- subset(mcmc$output, phase == "sampling")
-
-# real_line
-hist(output_sub$real_line, breaks = 100, probability = TRUE, col = "black", main = "real_line")
-x <- seq(-5, 5, l = 1001)
-lines(x, dnorm(x), col = 2, lwd = 2)
-

+mcmc <- run_mcmc(data = list(x = -1), + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e5, + chains = 10, + silent = TRUE)
+
+
+

Plots +

-# neg_line
-hist(output_sub$neg_line, breaks = 100, probability = TRUE, col = "black", main = "neg_line")
-x <- seq(-100, 0, l = 1001)
-lines(x, dgamma(-x, shape = 5, scale = 5), col = 2, lwd = 2)
-

+output_sub <- subset(mcmc$output, phase == "sampling") + +# real_line +hist(output_sub$real_line, breaks = 100, probability = TRUE, col = "black", main = "real_line") +x <- seq(-5, 5, l = 1001) +lines(x, dnorm(x), col = 2, lwd = 2)
+

-# pos_line
-hist(output_sub$pos_line, breaks = 100, probability = TRUE, col = "black", main = "pos_line")
-x <- seq(0, 100, l = 1001)
-lines(x, dgamma(x, shape = 5, scale = 5), col = 2, lwd = 2)
-

+# neg_line +hist(output_sub$neg_line, breaks = 100, probability = TRUE, col = "black", main = "neg_line") +x <- seq(-100, 0, l = 1001) +lines(x, dgamma(-x, shape = 5, scale = 5), col = 2, lwd = 2)
+

-# unit_interval
-hist(output_sub$unit_interval, breaks = seq(0, 1, 0.01), probability = TRUE, col = "black", main = "unit_interval")
-x <- seq(0, 10, l = 1001)
-lines(x, dbeta(x, shape1 = 3, shape2 = 3), col = 2, lwd = 2)
+# pos_line +hist(output_sub$pos_line, breaks = 100, probability = TRUE, col = "black", main = "pos_line") +x <- seq(0, 100, l = 1001) +lines(x, dgamma(x, shape = 5, scale = 5), col = 2, lwd = 2)
+

+
+# unit_interval
+hist(output_sub$unit_interval, breaks = seq(0, 1, 0.01), probability = TRUE, col = "black", main = "unit_interval")
+x <- seq(0, 10, l = 1001)
+lines(x, dbeta(x, shape1 = 3, shape2 = 3), col = 2, lwd = 2)

@@ -190,11 +199,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -203,5 +214,7 @@

+ + diff --git a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-1.png b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-1.png index 55a3f4e..e2b38a5 100644 Binary files a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-1.png and b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-1.png differ diff --git a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-2.png b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-2.png index c336b84..48b09c7 100644 Binary files a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-2.png and b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-2.png differ diff --git a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-3.png b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-3.png index 18c1c53..5e50902 100644 Binary files a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-3.png and b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-3.png differ diff --git a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-4.png b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-4.png index 5681f5d..c7649d2 100644 Binary files a/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-4.png and b/docs/articles/checks_return_prior_files/figure-html/unnamed-chunk-5-4.png differ diff --git a/docs/articles/example.html b/docs/articles/example.html index 36cf8f8..483296a 100644 --- a/docs/articles/example.html +++ b/docs/articles/example.html @@ -6,7 +6,7 @@ Basic Example • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

The likelihood and prior distributions that go into drjacoby can be specified by the user either as R functions or as C++ functions. This vignette demonstrates a basic MCMC implementation using both the R and C++ methods, and compares the two in terms of speed.

-
-

-Setup

+
#> Registered S3 method overwritten by 'GGally':
+#>   method from   
+#>   +.gg   ggplot2
+

The likelihood and prior distributions that go into drjacoby +can be specified by the user either as R functions or as C++ functions. +This vignette demonstrates a basic MCMC implementation using both the R +and C++ methods, and compares the two in terms of speed.

+
+

Setup +

We need the following elements to run drjacoby:

  1. Some data
  2. @@ -132,29 +141,46 @@

  3. A likelihood function
  4. A prior function
-

Starting with the data, let’s assume that our observations consist of a series of draws from a normal distribution with a given mean (mu_true) and standard deviation (sigma_true). We can generate some random data to play with:

-
-# set random seed
-set.seed(1)
-
-# define true parameter values
-mu_true <- 3
-sigma_true <- 2
-
-# draw example data
-data_list <- list(x = rnorm(10, mean = mu_true, sd = sigma_true))
-

Notice that data needs to be defined as a named list. For our example MCMC we will assume that we know the correct distribution of the data (i.e. we know the data is normally distributed), and we know that the mean is no smaller than -10 and no larger than 10, but otherwise the parameters of the distribution are unknown. Parameters within drjacoby are defined in dataframe format, where we specify minimum and maximum values of all parameters. The function define_params() makes it slightly easier to define this dataframe, but standard methods for making dataframes are also fine:

+

Starting with the data, let’s assume that our observations consist of +a series of draws from a normal distribution with a given mean +(mu_true) and standard deviation (sigma_true). +We can generate some random data to play with:

-# define parameters dataframe
-df_params <- define_params(name = "mu", min = -10, max = 10,
-                           name = "sigma", min = 0, max = Inf)
-
-print(df_params)
-#>    name min max
-#> 1    mu -10  10
-#> 2 sigma   0 Inf
-

In this example we have one parameter (mu) that occupies a finite range [-10, 10], and another parameter (sigma) that can take any positive value. drjacoby deals with different parameter ranges using reparameterisation, which all occurs internally meaning we don’t need to worry about these constraints affecting our inference.

-

Next, we need a likelihood function. This must have the following three input arguments, specified in this order:

+# set random seed +set.seed(1) + +# define true parameter values +mu_true <- 3 +sigma_true <- 2 + +# draw example data +data_list <- list(x = rnorm(10, mean = mu_true, sd = sigma_true))
+

Notice that data needs to be defined as a named list. For our example +MCMC we will assume that we know the correct distribution of the data +(i.e. we know the data is normally distributed), and we know that the +mean is no smaller than -10 and no larger than 10, but otherwise the +parameters of the distribution are unknown. Parameters within +drjacoby are defined in dataframe format, where we specify +minimum and maximum values of all parameters. The function +define_params() makes it slightly easier to define this +dataframe, but standard methods for making dataframes are also fine:

+
+# define parameters dataframe
+df_params <- define_params(name = "mu", min = -10, max = 10,
+                           name = "sigma", min = 0, max = Inf)
+
+print(df_params)
+#>    name min max
+#> 1    mu -10  10
+#> 2 sigma   0 Inf
+

In this example we have one parameter (mu) that occupies +a finite range [-10, 10], and another parameter (sigma) +that can take any positive value. drjacoby deals with different +parameter ranges using reparameterisation, which all occurs internally +meaning we don’t need to worry about these constraints affecting our +inference.

+

Next, we need a likelihood function. This must have +the following three input arguments, specified in this order:

  1. params - a named vector of parameter values
  2. @@ -163,164 +189,232 @@

  3. misc - a named list of miscellaneous values
-

Finally, the function must return a single value for the likelihood in log space. These constraints on the format of the likelihood function might seem a bit restrictive, but they are needed in order for drjacoby to know how to use the function internally. The issue of taking logs is particularly important, as the MCMC will still run even if we forget to take logs, but the results produced will be nonsense!

-
-

Do not underestimate the importance of taking logs.

+

Finally, the function must return a single value for +the likelihood in log space. These constraints on the +format of the likelihood function might seem a bit restrictive, but they +are needed in order for drjacoby to know how to use the +function internally. The issue of taking logs is particularly important, +as the MCMC will still run even if we forget to take logs, but the +results produced will be nonsense!

+
+Do not underestimate the importance of taking logs.
Do not underestimate the importance of taking +logs.
-

Inside the likelihood function we can extract individual parameter values from the input vector and then use these values to calculate the probability of the data. In our example, the likelihood function is quite simple thanks to the dnorm() function which can return the density of the normal distribution already in log space:

-
-# define log-likelihood function
-r_loglike <- function(params, data, misc) {
-  
-  # extract parameter values
-  mu <- params["mu"]
-  sigma <- params["sigma"]
-  
-  # calculate log-probability of data
-  ret <- sum(dnorm(data$x, mean = mu, sd = sigma, log = TRUE))
-  
-  # return
-  return(ret)
-}
-

Notice that we don’t use the misc object at all here, which is perfectly fine. Finally, we need a prior function. This must have the params and misc arguments as above, and it must return a single value for the prior probability of those parameters in log space. Again, this strict format is required for drjacoby to know how to use the prior internally. In our case we will assume a uniform prior on mu, and a log-normal prior on sigma:

+

Inside the likelihood function we can extract individual parameter +values from the input vector and then use these values to calculate the +probability of the data. In our example, the likelihood function is +quite simple thanks to the dnorm() function which can +return the density of the normal distribution already in log space:

-# define log-prior function
-r_logprior <- function(params, misc) {
-  
-  # extract parameter values
-  mu <- params["mu"]
-  sigma <- params["sigma"]
-  
-  # calculate log-prior
-  ret <- dunif(mu, min = -10, max = 10, log = TRUE) +
-    dlnorm(sigma, meanlog = 0, sdlog = 1.0, log = TRUE)
-  
-  # return
-  return(ret)
-}
-

Be careful to ensure that your prior is defined over the same range as specified in the df_params dataframe. For example, here our uniform prior for mu ranges from -10 to 10, and our log-normal prior for sigma ranges from 0 to infinity.

+# define log-likelihood function +r_loglike <- function(params, data, misc) { + + # extract parameter values + mu <- params["mu"] + sigma <- params["sigma"] + + # calculate log-probability of data + ret <- sum(dnorm(data$x, mean = mu, sd = sigma, log = TRUE)) + + # return + return(ret) +}
+

Notice that we don’t use the misc object at all here, +which is perfectly fine. Finally, we need a prior function. This +must have the params and misc +arguments as above, and it must return a single value +for the prior probability of those parameters in log +space. Again, this strict format is required for +drjacoby to know how to use the prior internally. In our case +we will assume a uniform prior on mu, and a log-normal +prior on sigma:

+
+# define log-prior function
+r_logprior <- function(params, misc) {
+  
+  # extract parameter values
+  mu <- params["mu"]
+  sigma <- params["sigma"]
+  
+  # calculate log-prior
+  ret <- dunif(mu, min = -10, max = 10, log = TRUE) +
+    dlnorm(sigma, meanlog = 0, sdlog = 1.0, log = TRUE)
+  
+  # return
+  return(ret)
+}
+

Be careful to ensure that your prior is defined over the same range +as specified in the df_params dataframe. For example, here +our uniform prior for mu ranges from -10 to 10, and our +log-normal prior for sigma ranges from 0 to infinity.

-
-

-Running the MCMC

-

Once we have all the elements above it is straightforward to run a basic MCMC in drjacoby. We simply input the four elements, along with the number of burn-in and sampling iterations. By default drjacoby prints progress bars to the console to keep you updated on the progress of the MCMC. When running in R markdown we can use the option pb_markdown = TRUE to print progress bars in a markdown-friendly way, although you will probably want to leave this option turned off when running interactively (simply delete this argument).

-
# run MCMC
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = r_loglike,
-                 logprior = r_logprior,
-                 burnin = 1e3,
-                 samples = 1e3,
-                 pb_markdown = TRUE)
-#> MCMC chain 1
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.6%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44.1%
-#> chain completed in 0.153058 seconds
-#> MCMC chain 2
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.1%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.9%
-#> chain completed in 0.133369 seconds
-#> MCMC chain 3
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44.3%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.7%
-#> chain completed in 0.182436 seconds
-#> MCMC chain 4
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.2%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44.2%
-#> chain completed in 0.151080 seconds
-#> MCMC chain 5
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.8%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.8%
-#> chain completed in 0.169111 seconds
-#> total MCMC run-time: 0.789 seconds
-

The output returned by the MCMC function has four parts: 1) an “output” dataframe containing raw posterior draws and other key elements at each iteration of the MCMC, 2) a “pt” dataframe containing output related to parallel tempering (if not used this just contains the log-likelihood and log-prior at every iteration), 3) a “diagnostics” object containing useful summaries such as the effective sample size of each parameter, 3) a “parameters” object containing a record of the exact parameters used to run the MCMC. We can take a peek at the first of these outputs:

+
+

Running the MCMC +

+

Once we have all the elements above it is straightforward to run a +basic MCMC in drjacoby. We simply input the four elements, +along with the number of burn-in and sampling iterations. By default +drjacoby prints progress bars to the console to keep you +updated on the progress of the MCMC. When running in R markdown we can +use the option pb_markdown = TRUE to print progress bars in +a markdown-friendly way, although you will probably want to leave this +option turned off when running interactively (simply delete this +argument).

-head(mcmc$output)
-#>   chain  phase iteration       mu     sigma  logprior loglikelihood
-#> 1     1 burnin         1 8.694105 0.9516224 -3.866313    -183.58134
-#> 2     1 burnin         2 8.638174 0.9516224 -3.866313    -180.24515
-#> 3     1 burnin         3 8.638174 0.9516224 -3.866313    -180.24515
-#> 4     1 burnin         4 8.638174 1.3907698 -4.298931     -92.80611
-#> 5     1 burnin         5 8.638174 2.8227402 -5.490798     -39.06412
-#> 6     1 burnin         6 6.175764 2.9388368 -5.573742     -26.14634
-

We can see that both mu and sigma changed values throughout the burn-in period, as we would expect, and the loglikelihood generally increased.

-
-
-

-Exploring outputs and checking MCMC performance

-

Before we can draw any conclusions from our MCMC results there are some basic checks that we should carry out. First, we can examine trace plots of all parameters to see how they are moving. This can be done using the plot_par() function, shown here for the burn-in phase:

+# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e3, + samples = 1e3, + pb_markdown = TRUE) +#> MCMC chain 1 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 43.6% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 44.1% +#> chain completed in 0.020962 seconds +#> MCMC chain 2 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 43.1% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 43.9% +#> chain completed in 0.017915 seconds +#> MCMC chain 3 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 44.3% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 43.7% +#> chain completed in 0.018442 seconds +#> MCMC chain 4 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 43.2% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 44.2% +#> chain completed in 0.020117 seconds +#> MCMC chain 5 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 43.8% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 43.8% +#> chain completed in 0.018404 seconds +#> total MCMC run-time: 0.0958 seconds
+

The output returned by the MCMC function has four parts: 1) an +“output” dataframe containing raw posterior draws and other key elements +at each iteration of the MCMC, 2) a “pt” dataframe containing output +related to parallel tempering (if not used this just contains the +log-likelihood and log-prior at every iteration), 3) a “diagnostics” +object containing useful summaries such as the effective sample size of +each parameter, 3) a “parameters” object containing a record of the +exact parameters used to run the MCMC. We can take a peek at the first +of these outputs:

-plot_par(mcmc, show = "mu", phase = "burnin")
-

+head(mcmc$output) +#> chain phase iteration mu sigma logprior loglikelihood +#> 1 1 burnin 1 8.694105 0.9516224 -3.866313 -183.58134 +#> 2 1 burnin 2 8.638174 0.9516224 -3.866313 -180.24515 +#> 3 1 burnin 3 8.638174 0.9516224 -3.866313 -180.24515 +#> 4 1 burnin 4 8.638174 1.3907698 -4.298931 -92.80611 +#> 5 1 burnin 5 8.638174 2.8227402 -5.490798 -39.06412 +#> 6 1 burnin 6 6.175764 2.9388368 -5.573742 -26.14634
+

We can see that both mu and sigma changed +values throughout the burn-in period, as we would expect, and the +loglikelihood generally increased.

+
+
+

Exploring outputs and checking MCMC performance +

+

Before we can draw any conclusions from our MCMC results there are +some basic checks that we should carry out. First, we can examine trace +plots of all parameters to see how they are moving. This can be done +using the plot_trace() function, shown here for the burn-in +phase:

-plot_par(mcmc, show = "sigma", phase = "burnin")
-

-

Notice that for all five chains both mu and sigma move quickly from their initial values to stable levels at around 3 and 2, respectively. This is a visual indication that the MCMC has burned in for an appropriate number of iterations. We can check this more rigorously by looking at the rhat object within the MCMC diagnostics, which gives the value of the Gelman-Rubin convergence diagnostic.

+plot_trace(mcmc, show = "mu", phase = "burnin")
+

-mcmc$diagnostics$rhat
-#>     mu  sigma 
-#> 1.0019 1.0034
-

Values close to 1 indicate that the MCMC has converged (typically the threshold <1.1 is used). This output uses the variance between chains, and therefore is only availble when running multiple chains.

-

By setting phase = "sampling" we can look at trace plots from the sampling phase only:

+plot_trace(mcmc, show = "sigma", phase = "burnin")
+

+

Notice that for all five chains both mu and +sigma move quickly from their initial values to stable +levels at around 3 and 2, respectively. This is a visual indication that +the MCMC has burned in for an appropriate number of iterations. We can +check this more rigorously by looking at the rhat object +within the MCMC diagnostics, which gives the value of the Gelman-Rubin +convergence diagnostic.

-plot_par(mcmc, show = "mu", phase = "sampling")
-

+mcmc$diagnostics$rhat +#> mu sigma +#> 1.0019 1.0034
+

Values close to 1 indicate that the MCMC has converged (typically the +threshold <1.1 is used). This output uses the variance between +chains, and therefore is only availble when running multiple chains.

+

By setting phase = "sampling" we can look at trace plots +from the sampling phase only:

-plot_par(mcmc, show = "sigma", phase = "sampling")
-

-

Here we can see that the MCMC continues to move freely after the burn-in phase, and that all chains appear to be exploring the same part of the parameter space. The final plot shows the autocorrelation of the chains, which in this case falls off rapidly with samples being approximately independent at around 10 lags. This is an indication that the MCMC is mixing well. The marginal histogram shows a single clear peak, although this is still a bit rough so we may want to re-run the MCMC with a larger number of sampling iterations to get a smoother result. We can also explore this by looking at the effective sample size (ESS), which is stored within the MCMC diagnostics:

+plot_trace(mcmc, show = "mu", phase = "sampling")
+

-mcmc$diagnostics$ess
-#>        mu     sigma 
-#> 1023.8251  942.6932
-

We can see that despite running the MCMC for 1000 sampling iterations, the actual number of effectively independent samples accounting for autocorrelation is lower. When doing any calculation that relies on the number of samples we should use the ESS rather than the raw number of sampling iterations. For example if we want to know the standard error of our estimate of mu we should do sd(mu)/sqrt(ESS). But just as importantly, when producing any summary of the posterior that does not make direct use of the number of samples - for example when producing posterior histograms - we should use all posterior draws, and we should not thin samples to reduce autocorrelation. This is because a histogram produced from all samples is more accurate than one produced from thinned samples, even if the samples are autocorrelated.

-

The final question is how confident can we be that our MCMC has actually explored the space well? Although the trace plot above looks good, it is possible to get results that look like this from very pathological MCMC runs. This is a more complex problem that is dealt with in another vignette.

+plot_trace(mcmc, show = "sigma", phase = "sampling")

+

+

Here we can see that the MCMC continues to move freely after the +burn-in phase, and that all chains appear to be exploring the same part +of the parameter space. The final plot shows the autocorrelation of the +chains, which in this case falls off rapidly with samples being +approximately independent at around 10 lags. This is an indication that +the MCMC is mixing well. The marginal histogram shows a single clear +peak, although this is still a bit rough so we may want to re-run the +MCMC with a larger number of sampling iterations to get a smoother +result. We can also explore this by looking at the effective sample size +(ESS), which is stored within the MCMC diagnostics:

+
+mcmc$diagnostics$ess
+#>        mu     sigma 
+#> 1023.8251  942.6932
+

We can see that despite running the MCMC for 1000 sampling iterations +over 5 chains (5000 in total), the actual number of effectively +independent samples accounting for autocorrelation is lower. When doing +any calculation that relies on the number of samples we should use the +ESS rather than the raw number of sampling iterations. For example if we +want to know the standard error of our estimate of mu we +should do sd(mu)/sqrt(ESS). But just as importantly, when +producing any summary of the posterior that does not make direct use of +the number of samples - for example when producing posterior histograms +- we should use all posterior draws, and we should not thin +samples to reduce autocorrelation. This is because a histogram +produced from all samples is more accurate than one produced from +thinned samples, even if the samples are autocorrelated.

+

The final question is how confident can we be that our MCMC has +actually explored the space well? Although the trace plot above looks +good, it is possible to get results that look like this from very +pathological MCMC runs. This is a more complex problem that is dealt +with in another +vignette.

-
-

-Using C++ functions

-

Although drjacoby is an R package, under the hood it is running C++ through the fantastic Rcpp package. When we pass an R likelihood function to run_mcmc(), as in the example above, the code is forced to jump out of C++ into R to evaluate the likelihood before jumping back into C++. This creates a computational overhead that can be avoided by specifying functions directly within C++.

-

To use C++ a function within drjacoby we first write it out as stand-alone file. The example used here can be found inside the inst/extdata folder of the package, and reads as follows:

+
+

Using C++ functions +

+

Although drjacoby is an R package, under the hood it is +running C++ through the fantastic Rcpp +package. When we pass an R likelihood function to +run_mcmc(), as in the example above, the code is forced to +jump out of C++ into R to evaluate the likelihood before jumping back +into C++. This creates a computational overhead that can be avoided by +specifying functions directly within C++.

+

To use C++ a function within drjacoby we first write it out +as stand-alone file. The example used here can be found inside the +inst/extdata folder of the package, and reads as follows:

#include <Rcpp.h>
 using namespace Rcpp;
 
@@ -371,8 +465,19 @@ 

stop("cpp function %i not found", function_name); }

-

The logic of this function is identical to the R version above, just written in the language of C++. To allow drjacoby to talk to your C++ function(s) we need to have a linking function in this C++ file, in this case called create_xptr(). With this set up all we need to do is source our C++ file with Rccp::sourcecpp() then simply pass the function names to run_mcmc(). To make life easy when setting up a new C++ model, you can create a template for your cpp file with the cpp_template() function - then all you need to do is fill in the internals of the loglikehood and logprior.

-

As before, there are some constraints on what this function must look like. It must take the same three input arguments described in the previous section, defined in the following formats:

+

The logic of this function is identical to the R version above, just +written in the language of C++. To allow drjacoby to talk to your C++ +function(s) we need to have a linking function in this C++ file, in this +case called create_xptr(). With this set up all we need to +do is source our C++ file with Rccp::sourcecpp() then +simply pass the function names to run_mcmc(). To +make life easy when setting up a new C++ model, you can create a +template for your cpp file with the cpp_template() function +- then all you need to do is fill in the internals of the loglikehood +and logprior.

+

As before, there are some constraints on what this function must look +like. It must take the same three input arguments +described in the previous section, defined in the following formats:

  1. params must be an Rcpp::NumericVector @@ -384,80 +489,83 @@

    misc must be an Rcpp::List

-

All parameter values will be coerced to double within the code, hence parameters consisting of integer or boolean values should be dealt with as though they are continuous (for example TRUE = 1.0, FALSE = 0.0). Second, the function must return an object of class SEXP. The easiest way to achieve this is to calculate the raw return value as a double, and then to use the Rcpp::wrap() function to transform to SEXP. As before, the value returned should be the likelihood evaluated in log space.

-

Even though we are working in C++, we still have access to most of R’s nice distribution functions through the R:: namespace. For example, the dnorm() function can be accessed within C++ using R::dnorm(). A list of probability distributions available within Rcpp can be found here.

-

With these two functions in hand we can run the MCMC exactly the same as before, passing in the new functions:

-
# run MCMC
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e3,
-                 pb_markdown = TRUE)
-#> MCMC chain 1
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 42.8%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44.7%
-#> chain completed in 0.018050 seconds
-#> MCMC chain 2
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44.1%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.8%
-#> chain completed in 0.007956 seconds
-#> MCMC chain 3
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.4%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44.3%
-#> chain completed in 0.006985 seconds
-#> MCMC chain 4
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 42.9%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44.1%
-#> chain completed in 0.006987 seconds
-#> MCMC chain 5
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.4%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.8%
-#> chain completed in 0.002949 seconds
-#> total MCMC run-time: 0.0429 seconds
-

You should see that this MCMC runs considerably faster than the previous version that used R functions. On the other hand, writing C++ functions is arguably more error-prone and difficult to debug than simple R functions. Hence, if efficiency is your goal then C++ versions of both the likelihood and prior should be used, whereas if ease of programming is more important then R functions might be a better choice.

-

This was a very easy problem, and so required no fancy MCMC tricks. The next vignette demonstrates how drjacoby can be applied to a more challenging problem.

+

All parameter values will be coerced to double within +the code, hence parameters consisting of integer or boolean values +should be dealt with as though they are continuous (for example +TRUE = 1.0, FALSE = 0.0). Second, the function +must return an object of class SEXP. The +easiest way to achieve this is to calculate the raw return value as a +double, and then to use the Rcpp::wrap() function to +transform to SEXP. As before, the value returned should be +the likelihood evaluated in log space.

+

Even though we are working in C++, we still have access to most of +R’s nice distribution functions through the R:: namespace. +For example, the dnorm() function can be accessed within +C++ using R::dnorm(). A list of probability distributions +available within Rcpp can be found here.

+

With these two functions in hand we can run the MCMC exactly the same +as before, passing in the new functions:

+
+# run MCMC
+mcmc <- run_mcmc(data = data_list,
+                 df_params = df_params,
+                 loglike = "loglike",
+                 logprior = "logprior",
+                 burnin = 1e3,
+                 samples = 1e3,
+                 pb_markdown = TRUE)
+#> MCMC chain 1
+#> burn-in
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 42.8%
+#> sampling phase
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 44.7%
+#> chain completed in 0.003751 seconds
+#> MCMC chain 2
+#> burn-in
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 44.1%
+#> sampling phase
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 43.8%
+#> chain completed in 0.003537 seconds
+#> MCMC chain 3
+#> burn-in
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 43.4%
+#> sampling phase
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 44.3%
+#> chain completed in 0.003486 seconds
+#> MCMC chain 4
+#> burn-in
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 42.9%
+#> sampling phase
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 44.1%
+#> chain completed in 0.003568 seconds
+#> MCMC chain 5
+#> burn-in
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 43.4%
+#> sampling phase
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 43.8%
+#> chain completed in 0.003476 seconds
+#> total MCMC run-time: 0.0178 seconds
+

You should see that this MCMC runs considerably faster than the +previous version that used R functions. On the other hand, writing C++ +functions is arguably more error-prone and difficult to debug than +simple R functions. Hence, if efficiency is your goal then C++ versions +of both the likelihood and prior should be used, whereas if ease of +programming is more important then R functions might be a better +choice.

+

This was a very easy problem, and so required no fancy MCMC tricks. +The next +vignette demonstrates how drjacoby can be applied to a more +challenging problem.

@@ -472,11 +580,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -485,5 +595,7 @@

+ + diff --git a/docs/articles/example_files/figure-html/unnamed-chunk-10-1.png b/docs/articles/example_files/figure-html/unnamed-chunk-10-1.png index 3702a00..82c55bc 100644 Binary files a/docs/articles/example_files/figure-html/unnamed-chunk-10-1.png and b/docs/articles/example_files/figure-html/unnamed-chunk-10-1.png differ diff --git a/docs/articles/example_files/figure-html/unnamed-chunk-10-2.png b/docs/articles/example_files/figure-html/unnamed-chunk-10-2.png index c21e1cf..80869f8 100644 Binary files a/docs/articles/example_files/figure-html/unnamed-chunk-10-2.png and b/docs/articles/example_files/figure-html/unnamed-chunk-10-2.png differ diff --git a/docs/articles/example_files/figure-html/unnamed-chunk-8-1.png b/docs/articles/example_files/figure-html/unnamed-chunk-8-1.png index 234fc1e..b64b69d 100644 Binary files a/docs/articles/example_files/figure-html/unnamed-chunk-8-1.png and b/docs/articles/example_files/figure-html/unnamed-chunk-8-1.png differ diff --git a/docs/articles/example_files/figure-html/unnamed-chunk-8-2.png b/docs/articles/example_files/figure-html/unnamed-chunk-8-2.png index a9eecb7..0da6582 100644 Binary files a/docs/articles/example_files/figure-html/unnamed-chunk-8-2.png and b/docs/articles/example_files/figure-html/unnamed-chunk-8-2.png differ diff --git a/docs/articles/getting_model_fits.html b/docs/articles/getting_model_fits.html index 62396f7..ccab7f9 100644 --- a/docs/articles/getting_model_fits.html +++ b/docs/articles/getting_model_fits.html @@ -6,7 +6,7 @@ Getting Model Fits • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

Sometimes our likelihood function involves calculating a series of intermediate values. A good example is when we have a model that describes our fit to the data, which may be combined with an error term to capture random noise around this prediction. Once the MCMC has finished, we may want to extract this model fit so that we can see how good it really is. Having already written down the code for calculating the model fit once within the likelihood, we don’t want to have to write it down again when visualising results as this duplication of work is not only a waste of time but also can introduce errors.

-

This vignette demonstrates how intermediate values can be obtained from MCMC output using a single likelihood function.

-
-

-A simple model of population growth

-

For this example we will use some data on population growth and we will attempt to fit a simple curve through these points. The data can be loaded directly from within the drjacoby package, and takes the form of a simple data.frame of sampling times and population sizes:

-
-# load example data
-data("population_data")
-head(population_data)
-#>   pop time
-#> 1  14   33
-#> 2  26   37
-#> 3  36   41
-#> 4  30   43
-#> 5  37   44
-#> 6  57   48
-
-# plot points
-plot(population_data$time, population_data$pop, ylim = c(0, 120), pch = 20,
-     xlab = "Time (days)", ylab = "Population size")
+
#> Registered S3 method overwritten by 'GGally':
+#>   method from   
+#>   +.gg   ggplot2
+

Sometimes our likelihood function involves calculating a series of +intermediate values. A good example is when we have a model that +describes our fit to the data, which may be combined with an error term +to capture random noise around this prediction. Once the MCMC has +finished, we may want to extract this model fit so that we can see how +good it really is. Having already written down the code for calculating +the model fit once within the likelihood, we don’t want to have to write +it down again when visualising results as this duplication of work is +not only a waste of time but also can introduce errors.

+

This vignette demonstrates how intermediate values can be obtained +from MCMC output using a single likelihood function.

+
+

A simple model of population growth +

+

For this example we will use some data on population growth and we +will attempt to fit a simple curve through these points. The data can be +loaded directly from within the drjacoby package, and takes the +form of a simple data.frame of sampling times and population sizes:

+
+# load example data
+data("population_data")
+head(population_data)
+#>   pop time
+#> 1  14   33
+#> 2  26   37
+#> 3  36   41
+#> 4  30   43
+#> 5  37   44
+#> 6  57   48
+
+# plot points
+plot(population_data$time, population_data$pop, ylim = c(0, 120), pch = 20,
+     xlab = "Time (days)", ylab = "Population size")

-

For our model we will assume a discrete-time logistic growth model, in which the population starts at size \(N_0\) and grows at a rate \(r\) each timestep, but also experiences density-dependent death causing it to plateau out a maximum population size \(N_{max}\). If we use \(N_t\) to denote the population size at time \(t\) then we can write this model as follows:

+

For our model we will assume a discrete-time logistic growth model, +in which the population starts at size \(N_0\) and grows at a rate \(r\) each timestep, but also experiences +density-dependent death causing it to plateau out at a maximum +population size \(N_{max}\). If we use +\(N_t\) to denote the population size +at time \(t\) then we can write this +model as follows:

\[ \begin{align} - N_{t+1} = rN_t(1 - N_t / K), \hspace{10mm} \mbox{where } K = N_{\mbox{max}}\left(\frac{r}{r-1}\right). + N_{t+1} = rN_t(1 - N_t / K), \hspace{10mm} \mbox{where } K = +N_{\mbox{max}}\left(\frac{r}{r-1}\right). \end{align} \]

-

There are three parameters in this model, and so our parameters dataframe should contain the three with suitable ranges as follows:

-
-# define parameters dataframe
-df_params <- define_params(name = "N_0", min = 1, max = 100,
-                           name = "r", min = 1, max = 2,
-                           name = "N_max", min = 50, max = 200)
-

Within our likelihood function we need to calculate our expected population growth curve. This curve is perfectly smooth, but the real data is noisy so we will use a Poisson distribution to link the data to the model.

-

Crucially, when defining the likelihood function we will create an option to return the model prediction, rather than the log-likelihood. We use the misc input to the function to control whether this option is active or not. If we set misc$output_type = 1 then we will obtain the log-likelihood, but if we set misc$output_type = 2 then we will obtain the complete curve of model-predicted values:

+

There are three parameters in this model, and so our parameters +dataframe should contain the three variables with suitable ranges as +follows:

-# define log-likelihood function
-loglike <- function(params, data, misc) {
-  
-  # extract parameter values
-  N_0 <- params["N_0"]
-  r <- params["r"]
-  N_max <- params["N_max"]
-  
-  # calculate expected population growth
-  K <- N_max * r / (r - 1)
-  x <- rep(0, 100)
-  x[1] <- N_0
-  for (i in 2:length(x)) {
-    x[i] <- r * x[i-1] * (1 - x[i-1] / K)
-  }
-  
-  # option to return model prediction rather than log-likelihood
-  if (misc$output_type == 2) {
-    return(x)
-  }
-  
-  # calculate log-likelihood
-  ret <- sum(dpois(data$pop, lambda = x[data$time], log = TRUE))
-  
-  # return
-  return(ret)
-}
-
-# define R logprior function
-logprior <- function(params, misc) {
-  return(0)
-}
-

When running the MCMC we want to make sure misc$output_type = 1:

-
# run MCMC
-mcmc <- run_mcmc(data = population_data,
-                 df_params = df_params,
-                 misc = list(output_type = 1),
-                 loglike = loglike,
-                 logprior = logprior,
-                 burnin = 1e3,
-                 samples = 1e4,
-                 chains = 3,
-                 pb_markdown = TRUE)
-#> MCMC chain 1
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 42.5%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.9%
-#> chain completed in 4.281182 seconds
-#> MCMC chain 2
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.1%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44%
-#> chain completed in 4.096652 seconds
-#> MCMC chain 3
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 42.9%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44%
-#> chain completed in 4.112060 seconds
-#> total MCMC run-time: 12.5 seconds
-

Once we have MCMC output we should perform our usual checks to make sure that everything has converged nicely. Assuming everything is OK, we can move on to exploring model fits. We will do this by running the same likelihood function but with misc$output_type = 2, using the posterior parameter draws as inputs. We may not want to use every posterior parameter draw, in which case we can use the sample_chains() function to sub-sample the output down as needed:

+# define parameters dataframe +df_params <- define_params(name = "N_0", min = 1, max = 100, + name = "r", min = 1, max = 2, + name = "N_max", min = 50, max = 200)
+

Within our likelihood function we need to calculate our expected +population growth curve. This curve is perfectly smooth, but the real +data is noisy so we will use a Poisson distribution to link the data to +the model.

+

Crucially, when defining the likelihood function we will create an +option to return the model prediction, rather than the log-likelihood. +We use the misc input to the function to control whether +this option is active or not. If we set +misc$output_type = 1 then we will obtain the +log-likelihood, but if we set misc$output_type = 2 then we +will obtain the complete curve of model-predicted values:

+
+# define log-likelihood function
+loglike <- function(params, data, misc) {
+  
+  # extract parameter values
+  N_0 <- params["N_0"]
+  r <- params["r"]
+  N_max <- params["N_max"]
+  
+  # calculate expected population growth
+  K <- N_max * r / (r - 1)
+  x <- rep(0, 100)
+  x[1] <- N_0
+  for (i in 2:length(x)) {
+    x[i] <- r * x[i-1] * (1 - x[i-1] / K)
+  }
+  
+  # option to return model prediction rather than log-likelihood
+  if (misc$output_type == 2) {
+    return(x)
+  }
+  
+  # calculate log-likelihood
+  ret <- sum(dpois(data$pop, lambda = x[data$time], log = TRUE))
+  
+  # return
+  return(ret)
+}
+
+# define R logprior function
+logprior <- function(params, misc) {
+  dunif(params["N_0"], 1, 100, log = TRUE) +
+    dunif(params["r"], 1, 2, log = TRUE) +
+    dunif(params["N_max"], 50, 200, log = TRUE)
+}
+

When running the MCMC we want to make sure +misc$output_type = 1:

-# sample from posterior
-param_draws <- sample_chains(mcmc, 1000)
-#> Effective sample size of sample has range: 169 to 553. See function ess to estimate.
-
-# get matrix of model predictions
-model_matrix <- apply(param_draws, 1, function(x) {
-  loglike(params = x, data = population_data, misc = list(output_type = 2))
-})
-

Plotting these against the data we can see that - reassuringly - the posterior model fits make a smooth curve through the data points:

+# run MCMC +mcmc <- run_mcmc(data = population_data, + df_params = df_params, + misc = list(output_type = 1), + loglike = loglike, + logprior = logprior, + burnin = 1e3, + samples = 1e4, + chains = 3, + pb_markdown = TRUE) +#> MCMC chain 1 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 42.5% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 43.9% +#> chain completed in 1.800967 seconds +#> MCMC chain 2 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 43.1% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 44% +#> chain completed in 1.669812 seconds +#> MCMC chain 3 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 42.9% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 44% +#> chain completed in 1.647409 seconds +#> total MCMC run-time: 5.12 seconds
+

Once we have MCMC output we should perform our usual checks to make +sure that everything has converged nicely (we will skip this step here +in the interest of time). Assuming everything is OK, we can move on to +exploring model fits. We will do this by running the same likelihood +function but with misc$output_type = 2, using the posterior +parameter draws as inputs. We may not want to use every posterior +parameter draw, in which case we can use the +sample_chains() function to sub-sample the output down as +needed:

-matplot(model_matrix, type = 'l', lty = 1, col = "#99000010", ylim = c(0, 120),
-        xlab = "Time (days)", ylab = "Population size")
-points(population_data$time, population_data$pop, pch = 20)
+# sample from posterior +param_draws <- sample_chains(mcmc, 1000) + +# get matrix of model predictions +model_matrix <- apply(param_draws, 1, function(x) { + loglike(params = x, data = population_data, misc = list(output_type = 2)) +})
+

Plotting these against the data we can see that - reassuringly - the +posterior model fits make a smooth curve through the data points:

+
+matplot(model_matrix, type = 'l', lty = 1, col = "#99000010", ylim = c(0, 120),
+        xlab = "Time (days)", ylab = "Population size")
+points(population_data$time, population_data$pop, pch = 20)

-

Notice that we only had to define the model fitting computation once in this pipeline, as the same likelihood function was used for both inference and obtaining the fit. There are more complicated versions of this approach that may be useful in some settings, for example using switch functions or multiple if statements to allow for several different types of output from the likelihood function, or even varying aspects of the core model such as the error distribution using flags contained in misc.

+

Notice that we only had to define the model fitting computation once +in this pipeline, as the same likelihood function was used for both +inference and obtaining the fit. There are more complicated versions of +this approach that may be useful in some settings, for example using +switch functions or multiple if statements to +allow for several different types of output from the likelihood +function, or even varying aspects of the core model such as the error +distribution using flags contained in misc.

@@ -269,11 +312,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -282,5 +327,7 @@

+ + diff --git a/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-3-1.png b/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-3-1.png index 6ac1e0f..87d5077 100644 Binary files a/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-3-1.png and b/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-3-1.png differ diff --git a/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-8-1.png b/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-8-1.png index 306879d..fc635de 100644 Binary files a/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-8-1.png and b/docs/articles/getting_model_fits_files/figure-html/unnamed-chunk-8-1.png differ diff --git a/docs/articles/index.html b/docs/articles/index.html index b8482f0..fc5c022 100644 --- a/docs/articles/index.html +++ b/docs/articles/index.html @@ -1,66 +1,12 @@ - - - - - - - -Articles • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Articles • drjacoby + + - - - - -
-
- -
- - -
- +
- - + + diff --git a/docs/articles/installation.html b/docs/articles/installation.html index f1b25dd..5650887 100644 --- a/docs/articles/installation.html +++ b/docs/articles/installation.html @@ -6,7 +6,7 @@ Installing drjacoby • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-
-

-Installing Rcpp

-

drjacoby relies on the Rcpp package, which requires the following OS-specific steps:

+
+

Installing Rcpp +

+

drjacoby relies on the Rcpp +package, which requires the following OS-specific steps:

  • Windows
      -
    • Download and install the appropriate version of Rtools for your version of R. On installation, ensure you check the box to arrange your system PATH as recommended by Rtools
    • +
    • Download and install the appropriate version of Rtools for your +version of R. On installation, ensure you check the box to arrange your +system PATH as recommended by Rtools
  • Mac OS X
      -
    • Download and install XCode +
    • Download and install XCode
    • -
    • Within XCode go to Preferences : Downloads and install the Command Line Tools
    • +
    • Within XCode go to Preferences : Downloads and install the Command +Line Tools
  • Linux (Debian/Ubuntu)
    • -

      Install the core software development utilities required for R package development as well as LaTeX by executing

      -
      sudo apt-get install r-base-dev texlive-full
      +

      Install g++ with

      +
      sudo apt-get update
      +sudo apt-get install g++
-
-
-

-Installing and loading drjacoby -

-

Next, in R, ensure that you have the devtools package installed by running

+

Irrespective of which system you use above, you should then install +and load Rcpp with

-install.packages("devtools", repos='http://cran.us.r-project.org')
-

Then install the drjacoby package directly from GitHub by running

+install.packages("Rcpp") +library(Rcpp)
+

You can check the version number to make sure it has properly +installed

-devtools::install_github("mrc-ide/drjacoby")
-

If you have any problems installing then please raise an issue on github.

-

Assuming everything installed correctly, we need to load the package:

+packageVersion("Rcpp")
+
+
+

Installing and loading drjacoby +

+

Next, in R, ensure that you have the devtools +package installed by running

-library(drjacoby)
-

You can test that the package is loaded and working by running the following command, which should produce this output:

+install.packages("devtools", repos='http://cran.us.r-project.org')
+

Then install the drjacoby package directly from GitHub by +running

-check_drjacoby_loaded()
-#> drjacoby loaded successfully!
+devtools::install_github("mrc-ide/drjacoby")
+

If you have any problems installing then please raise an issue on +github.

+

Assuming everything installed correctly, we need to load the +package:

+
+library(drjacoby)
+#> Registered S3 method overwritten by 'GGally':
+#>   method from   
+#>   +.gg   ggplot2
+

You can test that the package is loaded and working by running the +following command, which should produce this output:

+
+check_drjacoby_loaded()
+#> drjacoby loaded successfully!
@@ -180,11 +204,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -193,5 +219,7 @@

+ + diff --git a/docs/articles/metropolis_coupling.html b/docs/articles/metropolis_coupling.html index 1324396..ea5afb3 100644 --- a/docs/articles/metropolis_coupling.html +++ b/docs/articles/metropolis_coupling.html @@ -6,7 +6,7 @@ Parallel Tempering • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

MCMC becomes considerably harder when the posterior distribution is 1) highly correlated, and/or 2) highly multimodal. For exampe, if your posterior has Twin Peaks then ordinary Metropolis-Hastings might not be enough. Parallel tempering tends to mitigate these problems and requires nothing more than some extra heated chains.

-

This vignette demonstrates how parallel tempering can be implemented within drjacoby to solve a deliberately awkward MCMC problem.

-
-

-Setup

-

For this example we will start by writing the likelihood and prior functions in C++. If this sounds unfamiliar to you, check out the earlier vignette for a simple example. Our basic model will assume that our data are normally distributed with mean alpha^2*beta. The alpha^2 term here means that both positive and negative values of alpha map to the same value, thereby creating a multimodal distribution, and the *beta term ensures that alpha and beta are highly correlated. We will also use a third parameter epsilon to represent some random noise that we want to integrate over. While this is a highly contrived example, it does have the advantage of being horrendously awkward!

-

For our prior we assume that alpha is uniform [-10,10], beta is uniform [0,10], and epsilon is normally distributed with mean 0 and standard deviation 1.

+
#> Registered S3 method overwritten by 'GGally':
+#>   method from   
+#>   +.gg   ggplot2
+

MCMC becomes considerably harder when the posterior distribution is +1) highly correlated, and/or 2) highly multimodal. For exampe, if your +posterior has Twin Peaks then ordinary Metropolis-Hastings might not be +enough. Parallel tempering tends to mitigate these problems and requires +nothing more than some extra heated chains.

+

This vignette demonstrates how parallel tempering can be implemented +within drjacoby to solve a deliberately awkward MCMC +problem.

+
+

Setup +

+

For this example we will start by writing the likelihood and prior +functions in C++. If this sounds unfamiliar to you, check out the earlier +vignette for a simple example. Our basic model will assume that our +data are normally distributed with mean alpha^2*beta. The +alpha^2 term here means that both positive and negative +values of alpha map to the same value, thereby creating a +multimodal distribution. The *beta term ensures that +alpha and beta are highly correlated. We will +also use a third parameter epsilon to represent some random +noise that we want to integrate over. While this is a highly contrived +example, it does have the advantage of being horrendously awkward!

+

For our prior we assume that alpha is uniform [-10,10], +beta is uniform [0,10], and epsilon is +normally distributed with mean 0 and standard deviation 1.

#include <Rcpp.h>
 using namespace Rcpp;
 
@@ -180,152 +204,223 @@ 

stop("cpp function %i not found", function_name); }

-

As always, we need to define a dataframe of parameters so that drjacoby knows what parameter ranges to expect:

-
-# define parameters dataframe
-df_params <- define_params(name = "alpha", min = -10, max = 10, init = 5,
-                           name = "beta", min = 0, max = 10, init = 5,
-                           name = "epsilon", min = -Inf, max = Inf, init = 0)
-

Finally, we need some data. For simplicity we will use a series of values drawn from a normal distribution with mean 10. These are obviously not drawn from the true model, but will have the advantage of creating a horribly awkward posterior.

+

As always, we need to define a dataframe of parameters so that +drjacoby knows what parameter ranges to expect:

-# draw example data
-data_list <- list(x = rnorm(100, mean = 10))
-
-
-

-Running the MCMC

-

First, we will try running the basic MCMC without parallel tempering. The following block of code repeats the same MCMC analysis nine times, each time producing a plot of posterior alpha against beta:

+# define parameters dataframe +df_params <- define_params(name = "alpha", min = -10, max = 10, init = 5, + name = "beta", min = 0, max = 10, init = 5, + name = "epsilon", min = -Inf, max = Inf, init = 0)
+

Finally, we need some data. For simplicity we will use a series of +values drawn from a normal distribution with mean 10. These are +obviously not drawn from the true model, but will have the advantage of +creating a horribly awkward posterior.

-plot_list <- list()
-for (i in seq_len(9)) {
-  
-  # run MCMC
-  mcmc <- run_mcmc(data = data_list,
-                   df_params = df_params,
-                   loglike = "loglike",
-                   logprior = "logprior",
-                   burnin = 1e3,
-                   samples = 1e4,
-                   chains = 1,
-                   silent = TRUE)
-  
-  # create plot of alpha against beta
-  plot_list[[i]] <- plot_cor(mcmc, "alpha", "beta") +
-    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
-}
-
-# plot grid
-gridExtra::grid.arrange(grobs = plot_list)
+# draw example data +data_list <- list(x = rnorm(100, mean = 10))
+
+
+

Running the MCMC +

+

First, we will try running the basic MCMC without parallel tempering. +The following block of code repeats the same MCMC analysis nine times, +each time producing a plot of posterior alpha against +beta:

+
+plot_list <- list()
+for (i in seq_len(9)) {
+  
+  # run MCMC
+  mcmc <- run_mcmc(data = data_list,
+                   df_params = df_params,
+                   loglike = "loglike",
+                   logprior = "logprior",
+                   burnin = 1e3,
+                   samples = 1e4,
+                   chains = 1,
+                   silent = TRUE)
+  
+  # create plot of alpha against beta
+  plot_list[[i]] <- plot_scatter(mcmc, "alpha", "beta") +
+    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
+}
+
+# plot grid
+gridExtra::grid.arrange(grobs = plot_list)

-

Clearly this MCMC is not mixing well! By looking over all nine plots we can get a rough idea of what the distribution should look like, but no single MCMC run has captured it adequately. You can experiment with increasing the number of samples - you should get better results for more samples, but a very large number of samples are needed before we get good mixing between the left and right sides of the distribution.

-

Parallel tempering is perfectly designed to solve this kind of problem. To activate parallel tempering we need to specify an additional argument to the run_mcmc() function - the number of rungs. Each rung represents a different MCMC chain arranged in a “temperature ladder”, with the hotter chains being more free to explore the parameter space. Every iteration, chains have the option of swapping parameter values between rungs, thereby allowing the freely-moving hot chains to propose values to the more constrained colder chains. When looking at the output, we tend to focus exclusively on the coldest chain which can be interpreted the same way as a single chain from regular MCMC.

+

Clearly this MCMC is not mixing well! By looking over all nine plots +we can get a rough idea of what the distribution should look +like, but no single MCMC run has captured it adequately. You can +experiment with increasing the number of samples - you should get better +results for more samples, but a very large number of samples +are needed before we get good mixing between the left and right sides of +the distribution.

+

Parallel tempering is perfectly designed to solve this kind of +problem. To activate parallel tempering we need to specify an additional +argument to the run_mcmc() function - the number of +rungs. Each rung represents a different MCMC chain arranged +in a “temperature ladder”, with the hotter chains being more free to +explore the parameter space. Every iteration, chains have the option of +swapping parameter values between rungs, thereby allowing the +freely-moving hot chains to propose values to the more constrained +colder chains. When looking at the output, we tend to focus exclusively +on the coldest chain which can be interpreted the same way as a single +chain from regular MCMC.

In this example we use 20 rungs:

-
# run MCMC
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e4,
-                 rungs = 20,
-                 chains = 1,
-                 pb_markdown = TRUE)
-#> MCMC chain 1
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 42.5%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44%
-#> chain completed in 4.817843 seconds
-#> total MCMC run-time: 4.82 seconds
-
-# create plot of alpha against beta
-plot_cor(mcmc, "alpha", "beta") +
-    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
+
+# run MCMC
+mcmc <- run_mcmc(data = data_list,
+                 df_params = df_params,
+                 loglike = "loglike",
+                 logprior = "logprior",
+                 burnin = 1e3,
+                 samples = 1e4,
+                 rungs = 20,
+                 chains = 1,
+                 pb_markdown = TRUE)
+#> MCMC chain 1
+#> burn-in
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 42.5%
+#> sampling phase
+#>   |                                                                              |======================================================================| 100%
+#> acceptance rate: 44%
+#> chain completed in 1.126265 seconds
+#> total MCMC run-time: 1.13 seconds
+
+# create plot of alpha against beta
+plot_scatter(mcmc, "alpha", "beta") +
+    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")

-

You should see a much better characterisation of the posterior distribution. The run time scales approximately linearly with the number of rungs, so there is a computational cost to using this method, but on the other hand our results are far better than we would obtain by simply increasing the number of samples by a factor of 20.

+

You should see a much better characterisation of the posterior +distribution. The run time scales approximately linearly with the number +of rungs, so there is a computational cost to using this method, but on +the other hand our results are far better than we would obtain by simply +increasing the number of samples by a factor of 20.

-
-

-How many rungs to use?

-

Parallel tempering in drjacoby works by proposing swaps between adjacent rungs in the temperature ladder, which are accepted or rejected according to the standard Metropolis-Hastings ratio. If, for example, a hot rung happens upon a set of parameter values that have high likelihood then there is a good chance these values will be swapped up to the next rung. In this way, information can effectively bubble-sort its way from the prior (the hottest chain) to the posterior (the coldest chain). The average chance of a swap being accepted depends on the two adjacent rungs being similar enough in distribution that values drawn from one have a high likelihood in the other. This, in turn, means that swaps have a higher chance of being accepted when we have a large number of densely packed rungs.

-

To explore this, we start by re-running the above MCMC with a small number of rungs and a small number of samples to highlight the problem. We can use the plot_mc_acceptance() function to plot the acceptance rate between rungs:

-
-# run MCMC
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e3,
-                 rungs = 10,
-                 chains = 1,
-                 silent = TRUE)
-
-# plot coupling acceptance rates
-plot_mc_acceptance(mcmc, x_axis_type = 2)
-

-

The 10 vertical grey bars in the above plot show the positions of our 10 rungs in the thermodynamic interval [0,1], and the red dots between them show the proportion of swaps that were accepted between rungs (this information is available via mcmc$diagnostics$mc_accept). Notice that the acceptance rate between the hottest and second-hottest rung is close to zero, meaning very little of the information in the hottest rung made it up the ladder to the colder rungs. We can see the problems this causes when we plot the posterior draws:

+
+

How many rungs to use? +

+

Parallel tempering in drjacoby works by proposing swaps +between adjacent rungs in the temperature ladder, which are accepted or +rejected according to the standard Metropolis-Hastings ratio. If, for +example, a hot rung happens upon a set of parameter values that have +high likelihood then there is a good chance these values will be swapped +up to the next rung. In this way, information can effectively +bubble-sort its way from the prior (the hottest chain) to the posterior +(the coldest chain). The average chance of a swap being accepted depends +on the two adjacent rungs being similar enough in distribution that +values drawn from one have a high likelihood in the other. This, in +turn, means that swaps have a higher chance of being accepted when we +have a large number of densely packed rungs.

+

To explore this, we start by re-running the above MCMC with a small +number of rungs and a small number of samples to highlight the problem. +We can use the plot_mc_acceptance() function to plot the +acceptance rate between rungs:

-plot_cor(mcmc, "alpha", "beta") +
-    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
-

-

Although we have some mixing between the left and right sides of the distribution, overall sampling is still very patchy.

-

One way of improving mixing between rungs is simply to increase the number of rungs so the distance between distributions is smaller:

+# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + rungs = 10, + chains = 1, + silent = TRUE) + +# plot coupling acceptance rates +plot_mc_acceptance(mcmc, x_axis_type = 2)
+

+

The 10 vertical grey bars in the above plot show the positions of our +10 rungs in the thermodynamic interval [0,1], and the red dots between +them show the proportion of swaps that were accepted between rungs (this +information is available via mcmc$diagnostics$mc_accept). +Notice that the acceptance rate between the hottest and second-hottest +rung is close to zero, meaning very little of the information in the +hottest rung made it up the ladder to the colder rungs. We can see the +problems this causes when we plot the posterior draws:

-# run MCMC
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e3,
-                 rungs = 50,
-                 chains = 1,
-                 silent = TRUE)
-
-# plot coupling acceptance rates
-plot_mc_acceptance(mcmc, x_axis_type = 2)
-

+plot_scatter(mcmc, "alpha", "beta") + + ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
+

+

Although we have some mixing between the left and right sides of the +distribution, overall sampling is still very patchy.

+

One way of improving mixing between rungs is simply to increase the +number of rungs so the distance between distributions is smaller:

-
-# create plot of alpha against beta
-plot_cor(mcmc, "alpha", "beta") +
-    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
-

-

This mitigates the problem to some extent, as we now have acceptance rates going “deeper” towards the prior, however the hottest rung is still not passing information up the ladder. This strategy also comes at a high computational cost, as we are effectively running the MCMC algorithm 50 times. A more efficient method is usually to change the distribution of rungs so they are more concentrated at low thermodynamic powers (i.e. near the prior end of the spectrum). This can be achieved through the parameter alpha, which represents a power that the temperature ladder is raised to. Higher values of alpha therefore lead to rungs being more concentrated at the prior end of the spectrum. The previous two MCMCs had alpha set to 1.0 by default, leading to evenly distributed rungs between [0,1]. Now we repeat the MCMC with just 20 rungs and alpha = 5.0:

+# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + rungs = 50, + chains = 1, + silent = TRUE) + +# plot coupling acceptance rates +plot_mc_acceptance(mcmc, x_axis_type = 2)
+

-# run MCMC
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e3,
-                 rungs = 20,
-                 chains = 1,
-                 alpha = 5.0,
-                 silent = TRUE)
-
-# plot coupling acceptance rates
-# we could also use the option x_axis_type = 1 if we wanted to see these
-# points spread out evenly, rather than at their true position in the
-# thermodynamic interval
-plot_mc_acceptance(mcmc, x_axis_type = 2)
-

+ +# create plot of alpha against beta +plot_scatter(mcmc, "alpha", "beta") + + ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
+

+

This has helped, as we now have acceptance rates going “deeper” +towards the prior, however the hottest rung is still not passing +information up the ladder. This strategy also comes at a high +computational cost, as we are effectively running the MCMC algorithm 50 +times. A more efficient method is usually to change the distribution of +rungs so they are more concentrated at low thermodynamic powers +(i.e. near the prior end of the spectrum). This can be achieved through +the parameter alpha, which represents a power that the +temperature ladder is raised to. Higher values of alpha +therefore lead to rungs being more concentrated at the prior end of the +spectrum. The previous two MCMCs had alpha set to 1.0 by +default, leading to evenly distributed rungs between [0,1]. Now we +repeat the MCMC with just 20 rungs and alpha = 5.0:

-
-# create plot of alpha against beta
-plot_cor(mcmc, parameter1 = "alpha", "beta") +
-    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")
+# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + rungs = 20, + chains = 1, + alpha = 5.0, + silent = TRUE) + +# plot coupling acceptance rates +# we could also use the option x_axis_type = 1 if we wanted to see these +# points spread out evenly, rather than at their true position in the +# thermodynamic interval +plot_mc_acceptance(mcmc, x_axis_type = 2)
+

+
+
+# create plot of alpha against beta
+plot_scatter(mcmc, parameter1 = "alpha", "beta") +
+    ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none")

-

We find that with just 20 rungs we now have decent acceptance rates all the way through the temperature ladder. If we really wanted to fine-tune the mixing we could use the beta_manual argument inside run_mcmc() to define the temperature ladder exactly as a vector of values between 0 and 1. Note, this ladder is always raised to the power alpha, so we should set alpha=1.0 if we don’t want any additional transformation applied.

-

In summary, the correct number of rungs to use in parallel tempered MCMC is whatever number provides decent acceptance rates all the way through our temperature ladder. When this is the case we can be condifent that the prior is “talking to” the posterior, making it extremely unlikely (but not impossible) that the MCMC is still missing large parts of the posterior distribution.

+

We find that with just 20 rungs we now have decent acceptance rates +all the way through the temperature ladder. If we really wanted to +fine-tune the mixing we could use the beta_manual argument +inside run_mcmc() to define the temperature ladder exactly +as a vector of values between 0 and 1. Note, this ladder is always +raised to the power alpha, so we should set +alpha=1.0 if we don’t want any additional transformation +applied.

+

In summary, the correct number of rungs to use in parallel tempered +MCMC is whatever number provides decent acceptance rates all the way +through our temperature ladder. When this is the case we can be +condifent that the prior is “talking to” the posterior, making it +extremely unlikely (but not impossible) that the MCMC is still +missing large parts of the posterior distribution.

@@ -340,11 +435,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -353,5 +450,7 @@

+ + diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-1.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-1.png index 10a687f..d6092c9 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-1.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-1.png differ diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-2.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-2.png index 49503e2..593e004 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-2.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-10-2.png differ diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-5-1.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-5-1.png index 57e8cc8..32cdd70 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-5-1.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-5-1.png differ diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-6-1.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-6-1.png index 77966f1..a3b7203 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-6-1.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-6-1.png differ diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-7-1.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-7-1.png index 7a806eb..b3fed9e 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-7-1.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-7-1.png differ diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-8-1.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-8-1.png index c1cd375..67f5afd 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-8-1.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-8-1.png differ diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-1.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-1.png index 42b1f6c..2886efb 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-1.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-1.png differ diff --git a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-2.png b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-2.png index 0711219..da34830 100644 Binary files a/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-2.png and b/docs/articles/metropolis_coupling_files/figure-html/unnamed-chunk-9-2.png differ diff --git a/docs/articles/parallel.html b/docs/articles/parallel.html index 1da540f..8b97331 100644 --- a/docs/articles/parallel.html +++ b/docs/articles/parallel.html @@ -6,7 +6,7 @@ Running in Parallel • drjacoby - + @@ -19,6 +19,8 @@ + +
+
-

Running multiple chains is a good way of checking that our MCMC is working well. Each chain is completely independent of all others, and so this qualifies as an embarrassingly parallel problem.

-

This vignette will demonstrate how to run drjacoby with multiple chains, first in serial and then in parallel over multiple cores.

-
-

-Setup

-

As always, we require some data, some parameters, and some functions to work with (see earlier examples). The underlying model is not our focus here, so we will use a very basic setup

-
-# define data
-data_list <- list(x = rnorm(10))
-
-# define parameters dataframe
-df_params <- data.frame(name = "mu", min = -10, max = 10, init = 0)
-
-# define loglike function
-r_loglike <- function(params, data, misc) {
-  sum(dnorm(data$x, mean = params["mu"], sd = 1.0, log = TRUE))
-}
-
-# define logprior function
-r_logprior <- function(params, misc) {
-  dunif(params["mu"], min = -10, max = 10, log = TRUE)
-}
+
#> Registered S3 method overwritten by 'GGally':
+#>   method from   
+#>   +.gg   ggplot2
+

Running multiple chains is a good way of checking that our MCMC is +working well. Each chain is completely independent of all others, and so +this qualifies as an embarrassingly +parallel problem.

+

This vignette will demonstrate how to run drjacoby with +multiple chains, first in serial and then in parallel over multiple +cores.

+
+

Setup +

+

As always, we require some data, some parameters, and some functions +to work with (see earlier +examples). The underlying model is not our focus here, so we will +use a very basic setup

+
+# define data
+data_list <- list(x = rnorm(10))
+
+# define parameters dataframe
+df_params <- data.frame(name = "mu", min = -10, max = 10, init = 0)
+
+# define loglike function
+r_loglike <- function(params, data, misc) {
+  sum(dnorm(data$x, mean = params["mu"], sd = 1.0, log = TRUE))
+}
+
+# define logprior function
+r_logprior <- function(params, misc) {
+  dunif(params["mu"], min = -10, max = 10, log = TRUE)
+}
-
-

-Running multiple chains

-

Whenever the input argument cluster is NULL, chains will run in serial. This is true by default, so running multiple chains in serial is simply a case of specifying the chains argument:

-
# run MCMC in serial
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = r_loglike,
-                 logprior = r_logprior,
-                 burnin = 1e3,
-                 samples = 1e3,
-                 chains = 2,
-                 pb_markdown = TRUE)
-#> MCMC chain 1
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 44%
-#> chain completed in 0.062929 seconds
-#> MCMC chain 2
-#> burn-in
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.8%
-#> sampling phase
-#> 
-  |                                                                            
-  |======================================================================| 100%
-#> acceptance rate: 43.7%
-#> chain completed in 0.068930 seconds
-#> total MCMC run-time: 0.132 seconds
-

When we look at our MCMC output (using the plot_par() function) we can see that there are 2 chains, each of which contains a series of draws from the posterior. If we used multiple temperature rungs then these would also be duplicated over chains.

+
+

Running multiple chains +

+

Whenever the input argument cluster is +NULL, chains will run in serial. This is true by default, +so running multiple chains in serial is simply a case of specifying the +chains argument:

-# summarise output
-mcmc
-#> drjacoby output:
-#> 2 chains
-#> 1 rungs
-#> 1000 burn-in iterations
-#> 1000 sampling iterations
-#> 1 parameters
-
-# compare mu over both chains
-plot_par(mcmc, "mu", phase = "both")
-

-

Running in parallel is only slightly more complex. Before running anything we need to know how many cores our machine has. You may know this number already, but if you don’t then the parallel package has a handy function for detecting the number of cores for you:

+# run MCMC in serial +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e3, + samples = 1e3, + chains = 2, + pb_markdown = TRUE) +#> MCMC chain 1 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 43% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 44% +#> chain completed in 0.010007 seconds +#> MCMC chain 2 +#> burn-in +#> | |======================================================================| 100% +#> acceptance rate: 43.8% +#> sampling phase +#> | |======================================================================| 100% +#> acceptance rate: 43.7% +#> chain completed in 0.005991 seconds +#> total MCMC run-time: 0.016 seconds
+

When we look at our MCMC output (using the plot_trace() +function) we can see that there are 2 chains, each of which contains a +series of draws from the posterior. If we used multiple temperature +rungs then these would also be duplicated over chains.

-cores <- parallel::detectCores()
-

Next we make a cluster object, which creates multiple copies of R running in parallel over different cores. Here we are using all available cores, but if you want to hold some back for other intensive tasks then simply use a smaller number of cores when specifying this cluster.

+# summarise output +mcmc +#> drjacoby output: +#> 2 chains +#> 1 rungs +#> 1000 burn-in iterations +#> 1000 sampling iterations +#> 1 parameters + +# compare mu over both chains +plot_trace(mcmc, "mu", phase = "both")
+

+

Running in parallel is only slightly more complex. Before running +anything we need to know how many cores our machine has. You may know +this number already, but if you don’t then the parallel +package has a handy function for detecting the number of cores for +you:

-cl <- parallel::makeCluster(cores)
-

We then run the usual run_mcmc() function, this time passing in the cluster object as an argument. This causes drjacoby to use a clusterApplyLB() call rather than an ordinary lapply() call over different chains. Each chain is added to a queue over the specified number of cores - when the first job completes, the next job is placed on the node that has become available and this continues until all jobs are complete.

-

Note that output is supressed when running in parallel to avoid sending print commands to multiple cores, so you will not see the usual progress bars.

+cores <- parallel::detectCores()
+

Next we make a cluster object, which creates multiple copies of R +running in parallel over different cores. Here we are using all +available cores, but if you want to hold some back for other intensive +tasks then simply use a smaller number of cores when specifying this +cluster.

-# run MCMC in parallel
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = r_loglike,
-                 logprior = r_logprior,
-                 burnin = 1e3,
-                 samples = 1e3,
-                 chains = 2,
-                 cluster = cl,
-                 pb_markdown = TRUE)
-

Finally, it is good practice to shut down the workers once we are finished:

+cl <- parallel::makeCluster(cores)
+

We then run the usual run_mcmc() function, this time +passing in the cluster object as an argument. This causes +drjacoby to use a clusterApplyLB() call rather +than an ordinary lapply() call over different chains. Each +chain is added to a queue over the specified number of cores - when the +first job completes, the next job is placed on the node that has become +available and this continues until all jobs are complete.

+

Note that output is supressed when running in parallel to avoid +sending print commands to multiple cores, so you will not see the usual +progress bars.

-parallel::stopCluster(cl)
-
-
-

-Running multiple chains using C++ log likelihood or log prior functions

-

To run the MCMC in parallel with C++ log-likelihood or log-prior functions there is on additional step to take when setting up the cluster. Each node must be able to access a version of the compiled C++ functions, so after initialising the cluster with:

+# run MCMC in parallel +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e3, + samples = 1e3, + chains = 2, + cluster = cl, + pb_markdown = TRUE)
+

Finally, it is good practice to shut down the workers once we are +finished:

-cl <- parallel::makeCluster(cores)
-

we must also make the C++ functions accessible, by running the Rcpp::sourceCPP command for our C++ file for each node:

+parallel::stopCluster(cl)
+
+
+

Running multiple chains using C++ log likelihood or log prior +functions +

+

To run the MCMC in parallel with C++ log-likelihood or log-prior +functions there is on additional step to take when setting up the +cluster. Each node must be able to access a version of the compiled C++ +functions, so after initialising the cluster with:

-cl_cpp <- parallel::clusterEvalQ(cl, Rcpp::sourceCpp("my_cpp.cpp"))
-

After which we can run the mcmc in the same way:

+cl <- parallel::makeCluster(cores)
+

we must also make the C++ functions accessible, by running the +Rcpp::sourceCPP command for our C++ file for each node:

-# run MCMC in parallel
-mcmc <- run_mcmc(data = data_list,
-                 df_params = df_params,
-                 loglike = "loglike",
-                 logprior = "logprior",
-                 burnin = 1e3,
-                 samples = 1e3,
-                 chains = 2,
-                 cluster = cl,
-                 pb_markdown = TRUE)
-parallel::stopCluster(cl)
+cl_cpp <- parallel::clusterEvalQ(cl, Rcpp::sourceCpp("my_cpp.cpp"))

+

After which we can run the mcmc in the same way:

+
+# run MCMC in parallel
+mcmc <- run_mcmc(data = data_list,
+                 df_params = df_params,
+                 loglike = "loglike",
+                 logprior = "logprior",
+                 burnin = 1e3,
+                 samples = 1e3,
+                 chains = 2,
+                 cluster = cl,
+                 pb_markdown = TRUE)
+parallel::stopCluster(cl)
@@ -255,11 +290,13 @@

-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -268,5 +305,7 @@

+ + diff --git a/docs/articles/parallel_files/figure-html/unnamed-chunk-4-1.png b/docs/articles/parallel_files/figure-html/unnamed-chunk-4-1.png index 5a1b36e..40b9f27 100644 Binary files a/docs/articles/parallel_files/figure-html/unnamed-chunk-4-1.png and b/docs/articles/parallel_files/figure-html/unnamed-chunk-4-1.png differ diff --git a/docs/authors.html b/docs/authors.html index 983befb..f06f000 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -1,66 +1,12 @@ - - - - - - - -Authors • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Authors and Citation • drjacoby - - + + - - - -
-
-
- -
+
- @@ -169,22 +123,20 @@

Authors

-
- +
- - + + diff --git a/docs/index.html b/docs/index.html index 58a9cfb..67753bf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,22 +6,21 @@ Flexible Markov Chain Monte Carlo via Reparameterization • drjacoby - + - + + +
-

Site built with pkgdown 1.6.1.

+

+

Site built with pkgdown 2.0.9.

@@ -171,5 +181,7 @@

Developers

+ + diff --git a/docs/pkgdown.css b/docs/pkgdown.css index 1273238..80ea5b8 100644 --- a/docs/pkgdown.css +++ b/docs/pkgdown.css @@ -56,8 +56,10 @@ img.icon { float: right; } -img { +/* Ensure in-page images don't run outside their container */ +.contents img { max-width: 100%; + height: auto; } /* Fix bug in bootstrap (only seen in firefox) */ @@ -78,11 +80,10 @@ dd { /* Section anchors ---------------------------------*/ a.anchor { - margin-left: -30px; - display:inline-block; - width: 30px; - height: 30px; - visibility: hidden; + display: none; + margin-left: 5px; + width: 20px; + height: 20px; background-image: url(./link.svg); background-repeat: no-repeat; @@ -90,17 +91,15 @@ a.anchor { background-position: center center; } -.hasAnchor:hover a.anchor { - visibility: visible; -} - -@media (max-width: 767px) { - .hasAnchor:hover a.anchor { - visibility: hidden; - } +h1:hover .anchor, +h2:hover .anchor, +h3:hover .anchor, +h4:hover .anchor, +h5:hover .anchor, +h6:hover .anchor { + display: inline-block; } - /* Fixes for fixed navbar --------------------------*/ .contents h1, .contents h2, .contents h3, .contents h4 { @@ -264,31 +263,26 @@ table { /* Syntax highlighting ---------------------------------------------------- */ -pre { - word-wrap: normal; - word-break: normal; - border: 1px solid #eee; -} - -pre, code { +pre, code, pre code { background-color: #f8f8f8; color: #333; } +pre, pre code { + white-space: pre-wrap; + word-break: break-all; + overflow-wrap: break-word; +} -pre code { - overflow: auto; - word-wrap: normal; - white-space: pre; +pre { + border: 1px solid #eee; } -pre .img { +pre .img, pre .r-plt { margin: 5px 0; } -pre .img img { +pre .img img, pre .r-plt img { background-color: #fff; - display: block; - height: auto; } code a, pre a { @@ -305,9 +299,8 @@ a.sourceLine:hover { .kw {color: #264D66;} /* keyword */ .co {color: #888888;} /* comment */ -.message { color: black; font-weight: bolder;} -.error { color: orange; font-weight: bolder;} -.warning { color: #6A0366; font-weight: bolder;} +.error {font-weight: bolder;} +.warning {font-weight: bolder;} /* Clipboard --------------------------*/ @@ -365,3 +358,27 @@ mark { content: ""; } } + +/* Section anchors --------------------------------- + Added in pandoc 2.11: https://github.com/jgm/pandoc-templates/commit/9904bf71 +*/ + +div.csl-bib-body { } +div.csl-entry { + clear: both; +} +.hanging-indent div.csl-entry { + margin-left:2em; + text-indent:-2em; +} +div.csl-left-margin { + min-width:2em; + float:left; +} +div.csl-right-inline { + margin-left:2em; + padding-left:1em; +} +div.csl-indent { + margin-left: 2em; +} diff --git a/docs/pkgdown.js b/docs/pkgdown.js index 7e7048f..6f0eee4 100644 --- a/docs/pkgdown.js +++ b/docs/pkgdown.js @@ -80,7 +80,7 @@ $(document).ready(function() { var copyButton = ""; - $(".examples, div.sourceCode").addClass("hasCopyButton"); + $("div.sourceCode").addClass("hasCopyButton"); // Insert copy buttons: $(copyButton).prependTo(".hasCopyButton"); @@ -91,7 +91,7 @@ // Initialize clipboard: var clipboardBtnCopies = new ClipboardJS('[data-clipboard-copy]', { text: function(trigger) { - return trigger.parentNode.textContent; + return trigger.parentNode.textContent.replace(/\n#>[^\n]*/g, ""); } }); diff --git a/docs/pkgdown.yml b/docs/pkgdown.yml index 9763326..bdef2cb 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -1,5 +1,5 @@ -pandoc: 2.11.4 -pkgdown: 1.6.1 +pandoc: 3.1.11 +pkgdown: 2.0.9 pkgdown_sha: ~ articles: blocks: blocks.html @@ -12,5 +12,5 @@ articles: installation: installation.html metropolis_coupling: metropolis_coupling.html parallel: parallel.html -last_built: 2021-12-16T09:33Z +last_built: 2024-06-26T00:19Z diff --git a/docs/reference/acf_data.html b/docs/reference/acf_data.html index 4cba9a7..4764469 100644 --- a/docs/reference/acf_data.html +++ b/docs/reference/acf_data.html @@ -1,67 +1,12 @@ - - - - - - - -Estimate autocorrelation — acf_data • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Estimate autocorrelation — acf_data • drjacoby - - + + - - -
-
- -
- -
+
@@ -159,47 +93,42 @@

Estimate autocorrelation

Estimate autocorrelation

-
acf_data(x, lag)
+
+
acf_data(x, lag)
+
+ +
+

Arguments

+
x
+

Single chain.

+ -

Arguments

- - - - - - - - - - -
x

Single chain.

lag

maximum lag. Must be an integer between 1 and 500.

+
lag
+

maximum lag. Must be an integer between 1 and 500.

+
+
-
- +
- - + + diff --git a/docs/reference/check_drjacoby_loaded.html b/docs/reference/check_drjacoby_loaded.html index 0fc6858..053219e 100644 --- a/docs/reference/check_drjacoby_loaded.html +++ b/docs/reference/check_drjacoby_loaded.html @@ -1,68 +1,13 @@ - - - - - - - -Check that drjacoby package has loaded successfully — check_drjacoby_loaded • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Check that drjacoby package has loaded successfully — check_drjacoby_loaded • drjacoby - + + - - - -
-
- -
- -
+
@@ -161,35 +95,32 @@

Check that drjacoby package has loaded successfully

successfully. Prints "drjacoby loaded successfully!" if so.

-
check_drjacoby_loaded()
- +
+
check_drjacoby_loaded()
+
+
-
- +
- - + + diff --git a/docs/reference/cpp_template.html b/docs/reference/cpp_template.html index 35be7d2..9c0516b 100644 --- a/docs/reference/cpp_template.html +++ b/docs/reference/cpp_template.html @@ -1,67 +1,12 @@ - - - - - - - -Create template for cpp — cpp_template • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Create template for cpp — cpp_template • drjacoby - + + - - - -
-
- -
- -
+
@@ -159,43 +93,38 @@

Create template for cpp

Create template for cpp

-
cpp_template(save_as)
+
+
cpp_template(save_as)
+
-

Arguments

- - - - - - -
save_as

Path of (.cpp) file to create, relative to root of active project.

+
+

Arguments

+
save_as
+

Path of (.cpp) file to create, relative to root of active project.

+
+
-
- +

- - + + diff --git a/docs/reference/define_params.html b/docs/reference/define_params.html index 9a741f9..05abfcf 100644 --- a/docs/reference/define_params.html +++ b/docs/reference/define_params.html @@ -1,46 +1,5 @@ - - - - - - - -Define parameters dataframe — define_params • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Define parameters dataframe — define_params • drjacoby - - - - - - - - - - + + - - - -
-
- -
- -
+

Provides a convenient way of defining parameters in the format - required by run_mcmc(). Each parameter must have the following three - elements, defined in order:

    -
  • name - the parameter name.

  • + required by run_mcmc(). Each parameter must have the following three + elements, defined in order:

    • name - the parameter name.

    • min - the minimum value of the parameter. -Inf is allowed.

    • max - the maximum value of the parameter. Inf is allowed.

    • -

    There following arguments are also optional:

      -
    • init - the initial value of the parameter. If running +

    There following arguments are also optional:

    • init - the initial value of the parameter. If running multiple chains a vector of initial values can be used to specify distinct values for each chain.

    • block - which likelihood block(s) this parameter belongs to. See vignettes for instructions on using likelihood blocks.

    • -
    +
+
+
define_params(...)
-
define_params(...)
- -

Arguments

- - - - - - -
...

a series of named input arguments.

- - -

Examples

-
define_params(name = "mu", min = -10, max = 10, init = 0, - name = "sigma", min = 0, max = 5, init = c(1, 2)) -
#> name min max init -#> 1 mu -10 10 0 -#> 2 sigma 0 5 1, 2
-define_params(name = "mu1", min = -10, max = 10, init = 0, block = 1, - name = "mu2", min = -10, max = 10, init = 0, block = 2, - name = "sigma", min = 0, max = 5, init = 1, block = c(1, 2)) -
#> name min max init block -#> 1 mu1 -10 10 0 1 -#> 2 mu2 -10 10 0 2 -#> 3 sigma 0 5 1 1, 2
+
+

Arguments

+
...
+

a series of named input arguments.

+ +
+ +
+

Examples

+
define_params(name = "mu", min = -10, max = 10, init = 0,
+              name = "sigma", min = 0, max = 5, init = c(1, 2))
+#>    name min max init
+#> 1    mu -10  10    0
+#> 2 sigma   0   5 1, 2
+              
+define_params(name = "mu1", min = -10, max = 10, init = 0, block = 1,
+              name = "mu2", min = -10, max = 10, init = 0, block = 2,
+              name = "sigma", min = 0, max = 5, init = 1, block = c(1, 2))
+#>    name min max init block
+#> 1   mu1 -10  10    0     1
+#> 2   mu2 -10  10    0     2
+#> 3 sigma   0   5    1  1, 2
+
+
+
-
- +
- - + + diff --git a/docs/reference/drjacoby.html b/docs/reference/drjacoby.html index 2ab5349..090d5cf 100644 --- a/docs/reference/drjacoby.html +++ b/docs/reference/drjacoby.html @@ -1,68 +1,14 @@ - - - - - - - -Flexible Markov Chain Monte Carlo via Reparameterization — drjacoby • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Flexible Markov Chain Monte Carlo via Reparameterization — drjacoby • drjacoby - + + - - - -
-
- -
- -
+

Flexible Markov chain monte carlo via reparameterization using the Jacobean matrix.

+

_PACKAGE

-
+
-
- +
- - + + diff --git a/docs/reference/gelman_rubin.html b/docs/reference/gelman_rubin.html index a440a93..58b1097 100644 --- a/docs/reference/gelman_rubin.html +++ b/docs/reference/gelman_rubin.html @@ -1,68 +1,13 @@ - - - - - - - -Gelman-Rubin statistic — gelman_rubin • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Gelman-Rubin statistic — gelman_rubin • drjacoby - + + - - - -
-
- -
- -
+
@@ -161,60 +95,59 @@

Gelman-Rubin statistic

across multiple chains. Basic method, assuming all chains are of equal length

-
gelman_rubin(par_matrix, chains, samples)
- -

Arguments

- - - - - - - - - - - - - - -
par_matrix

Matrix (interations x chains)

chains

number of chains

samples

number of samples

- -

Value

- -

Gelman-Rubin statistic

-

References

+
+
gelman_rubin(par_matrix, chains, samples)
+
+ +
+

Arguments

+
par_matrix
+

Matrix (interations x chains)

+ +
chains
+

number of chains

+ + +
samples
+

number of samples

+ +
+
+

Value

+ + +

Gelman-Rubin statistic

+
+
+

References

Gelman, A., and D. B. Rubin. 1992. Inference from Iterative Simulation Using Multiple Sequences. Statistical Science 7: 457–511.

-

https://astrostatistics.psu.edu/RLectures/diagnosticsMCMC.pdf

+

https://astrostatistics.psu.edu/RLectures/diagnosticsMCMC.pdf

+
+
-
- +

- - + + diff --git a/docs/reference/index.html b/docs/reference/index.html index 01600ae..bc7d1ed 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -1,66 +1,12 @@ - - - - - - - -Function reference • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Function reference • drjacoby - + + - - - -
-
- -
- -
+
- - - - - - - - - - - +
-

All functions

+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + + - - - - - - - - - -
+

All functions

+

acf_data()

Estimate autocorrelation

+

check_drjacoby_loaded()

Check that drjacoby package has loaded successfully

+

cpp_template()

Create template for cpp

+

define_params()

Define parameters dataframe

+

drjacoby

Flexible Markov Chain Monte Carlo via Reparameterization

+

gelman_rubin()

Gelman-Rubin statistic

+

plot_autocorrelation()

Plot autocorrelation

-

plot_cor()

-

Plot parameter correlation

+

plot_cor_mat()

Plot posterior correlation matrix

+

plot_credible()

Plot 95% credible intervals

+

plot_mc_acceptance()

Plot Metropolis coupling acceptance rates

-

plot_par()

+
+

plot_pairs()

Plot parameter estimates

+

Produce scatterplots between multiple parameters

plot_rung_loglike()

Plot loglikelihood 95% credible intervals

+
+

plot_scatter()

+

Produce bivariate scatterplot

+

plot_trace()

+

Plot parameter trace

population_data

Population data.

+

run_mcmc()

Run drjacoby MCMC

+

sample_chains()

Sample N draws from all available chains

- +

Sample posterior draws from all available chains

+
-
- +
- - + + diff --git a/docs/reference/plot_autocorrelation.html b/docs/reference/plot_autocorrelation.html index 8b054e7..fba971d 100644 --- a/docs/reference/plot_autocorrelation.html +++ b/docs/reference/plot_autocorrelation.html @@ -1,67 +1,12 @@ - - - - - - - -Plot autocorrelation — plot_autocorrelation • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Plot autocorrelation — plot_autocorrelation • drjacoby - + + - - - -
-
- -
- -
+
@@ -159,60 +93,55 @@

Plot autocorrelation

Plot autocorrelation for specified parameters

-
plot_autocorrelation(x, lag = 20, par = NULL, chain = 1, phase = "sampling")
- -

Arguments

- - - - - - - - - - - - - - - - - - - - - - -
x

an object of class drjacoby_output

lag

maximum lag. Must be an integer between 1 and 500.

par

vector of parameter names. If NULL all parameters are -plotted.

chain

which chain to plot.

phase

which phase to plot. Must be either "burnin" or "sampling".

+
+
plot_autocorrelation(x, lag = 20, par = NULL, chain = 1, phase = "sampling")
+
+ +
+

Arguments

+
x
+

an object of class drjacoby_output

+
lag
+

maximum lag. Must be an integer between 1 and 500.

+ + +
par
+

vector of parameter names. If NULL all parameters are +plotted.

+ + +
chain
+

which chain to plot.

+ + +
phase
+

which phase to plot. Must be either "burnin" or "sampling".

+ +
+
+
-
- +
- - + + diff --git a/docs/reference/plot_cor_mat.html b/docs/reference/plot_cor_mat.html index 445ee0b..c01d296 100644 --- a/docs/reference/plot_cor_mat.html +++ b/docs/reference/plot_cor_mat.html @@ -1,68 +1,13 @@ - - - - - - - -Plot posterior correlation matrix — plot_cor_mat • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Plot posterior correlation matrix — plot_cor_mat • drjacoby + + - - - - -
-
- -
- -
+
@@ -161,55 +95,50 @@

Plot posterior correlation matrix

from posterior draws.

-
plot_cor_mat(x, show = NULL, phase = "sampling", param_names = NULL)
- -

Arguments

- - - - - - - - - - - - - - - - - - -
x

an object of class drjacoby_output

show

Vector of parameter names to plot.

phase

which phase to plot. Must be either "burnin" or "sampling".

param_names

Optional vector of names to replace the default parameter names.

+
+
plot_cor_mat(x, show = NULL, phase = "sampling", param_names = NULL)
+
+
+

Arguments

+
x
+

an object of class drjacoby_output

+ + +
show
+

Vector of parameter names to plot.

+ + +
phase
+

which phase to plot. Must be either "burnin" or "sampling".

+ + +
param_names
+

Optional vector of names to replace the default parameter names.

+ +
+
-
- +
- - + + diff --git a/docs/reference/plot_credible.html b/docs/reference/plot_credible.html index 6b188cc..39371e9 100644 --- a/docs/reference/plot_credible.html +++ b/docs/reference/plot_credible.html @@ -1,68 +1,13 @@ - - - - - - - -Plot 95% credible intervals — plot_credible • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Plot 95% credible intervals — plot_credible • drjacoby + + - - - - -
-
- -
- -
+
@@ -161,55 +95,50 @@

Plot 95% credible intervals

parameters (defauls to all parameters).

-
plot_credible(x, show = NULL, phase = "sampling", param_names = NULL)
- -

Arguments

- - - - - - - - - - - - - - - - - - -
x

an object of class drjacoby_output

show

vector of parameter names to plot.

phase

which phase to plot. Must be either "burnin" or "sampling".

param_names

optional vector of names to replace the default parameter names.

+
+
plot_credible(x, show = NULL, phase = "sampling", param_names = NULL)
+
+
+

Arguments

+
x
+

an object of class drjacoby_output

+ + +
show
+

vector of parameter names to plot.

+ + +
phase
+

which phase to plot. Must be either "burnin" or "sampling".

+ + +
param_names
+

optional vector of names to replace the default parameter names.

+ +
+
-
- - + + diff --git a/docs/reference/plot_mc_acceptance.html b/docs/reference/plot_mc_acceptance.html index 4b86e06..c4b4d9f 100644 --- a/docs/reference/plot_mc_acceptance.html +++ b/docs/reference/plot_mc_acceptance.html @@ -1,67 +1,12 @@ - - - - - - - -Plot Metropolis coupling acceptance rates — plot_mc_acceptance • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Plot Metropolis coupling acceptance rates — plot_mc_acceptance • drjacoby + + - - - - -
-
- -
- -
+
@@ -159,56 +93,51 @@

Plot Metropolis coupling acceptance rates

Plot Metropolis coupling acceptance rates between all rungs.

-
plot_mc_acceptance(x, chain = NULL, phase = "sampling", x_axis_type = 1)
- -

Arguments

- - - - - - - - - - - - - - - - - - -
x

an object of class drjacoby_output

chain

which chain to plot. If NULL then plot all chains.

phase

which phase to plot. Must be either "burnin" or "sampling".

x_axis_type

how to format the x-axis. 1 = integer rungs, 2 = values of -the thermodynamic power.

+
+
plot_mc_acceptance(x, chain = NULL, phase = "sampling", x_axis_type = 1)
+
+
+

Arguments

+
x
+

an object of class drjacoby_output

+ + +
chain
+

which chain to plot. If NULL then plot all chains.

+ + +
phase
+

which phase to plot. Must be either "burnin" or "sampling".

+ + +
x_axis_type
+

how to format the x-axis. 1 = integer rungs, 2 = values of +the thermodynamic power.

+ +
+
- - - + + diff --git a/docs/reference/plot_pairs.html b/docs/reference/plot_pairs.html new file mode 100644 index 0000000..6f84275 --- /dev/null +++ b/docs/reference/plot_pairs.html @@ -0,0 +1,142 @@ + +Produce scatterplots between multiple parameters — plot_pairs • drjacoby + + +
+
+ + + +
+
+ + +
+

Uses ggpairs function from the GGally package to + produce scatterplots between all named parameters.

+
+ +
+
plot_pairs(x, show = NULL, hide = NULL)
+
+ +
+

Arguments

+
x
+

an object of class drjacoby_output

+ + +
show
+

optional vector of parameter names to plot. Parameters matching +show will be included.

+ + +
hide
+

optional vector of parameter names to filter out. Parameters +matching hide will be hidden.

+ +
+ +
+ +
+ + +
+ + + + + + + + diff --git a/docs/reference/plot_rung_loglike.html b/docs/reference/plot_rung_loglike.html index fec5f5a..d926058 100644 --- a/docs/reference/plot_rung_loglike.html +++ b/docs/reference/plot_rung_loglike.html @@ -1,67 +1,12 @@ - - - - - - - -Plot loglikelihood 95% credible intervals — plot_rung_loglike • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Plot loglikelihood 95% credible intervals — plot_rung_loglike • drjacoby - + + - - - -
-
- -
- -
+
@@ -159,67 +93,62 @@

Plot loglikelihood 95% credible intervals

Plot loglikelihood 95% credible intervals.

-
plot_rung_loglike(
-  x,
-  chain = 1,
-  phase = "sampling",
-  x_axis_type = 1,
-  y_axis_type = 1
-)
- -

Arguments

- - - - - - - - - - - - - - - - - - - - - - -
x

an object of class drjacoby_output

chain

which chain to plot.

phase

which phase to plot. Must be either "burnin" or "sampling".

x_axis_type

how to format the x-axis. 1 = integer rungs, 2 = values of -the thermodynamic power.

y_axis_type

how to format the y-axis. 1 = raw values, 2 = truncated at -auto-chosen lower limit. 3 = double-log scale.

+
+
plot_rung_loglike(
+  x,
+  chain = 1,
+  phase = "sampling",
+  x_axis_type = 1,
+  y_axis_type = 1
+)
+
+ +
+

Arguments

+
x
+

an object of class drjacoby_output

+
chain
+

which chain to plot.

+ + +
phase
+

which phase to plot. Must be either "burnin" or "sampling".

+ + +
x_axis_type
+

how to format the x-axis. 1 = integer rungs, 2 = values of +the thermodynamic power.

+ + +
y_axis_type
+

how to format the y-axis. 1 = raw values, 2 = truncated at +auto-chosen lower limit. 3 = double-log scale.

+ +
+
+
- - - + + diff --git a/docs/reference/plot_scatter.html b/docs/reference/plot_scatter.html new file mode 100644 index 0000000..fed4842 --- /dev/null +++ b/docs/reference/plot_scatter.html @@ -0,0 +1,158 @@ + +Produce bivariate scatterplot — plot_scatter • drjacoby + + +
+
+ + + +
+
+ + +
+

Produces scatterplot between two named parameters.

+
+ +
+
plot_scatter(
+  x,
+  parameter1,
+  parameter2,
+  downsample = TRUE,
+  phase = "sampling",
+  chain = NULL
+)
+
+ +
+

Arguments

+
x
+

an object of class drjacoby_output

+ + +
parameter1
+

name of parameter first parameter.

+ + +
parameter2
+

name of parameter second parameter.

+ + +
downsample
+

whether to downsample output to 200 values max to speed up +plotting.

+ + +
phase
+

which phase to plot. Must be either "burnin" or "sampling".

+ + +
chain
+

which chain to plot.

+ +
+ +
+ +
+ + +
+ + + + + + + + diff --git a/docs/reference/plot_trace.html b/docs/reference/plot_trace.html new file mode 100644 index 0000000..bc077c7 --- /dev/null +++ b/docs/reference/plot_trace.html @@ -0,0 +1,177 @@ + +Plot parameter trace — plot_trace • drjacoby + + +
+
+ + + +
+
+ + +
+

Produce a series of plots corresponding to each parameter, + including the raw trace, the posterior histogram and an autocorrelation + plot. Plotting objects can be cycled through interactively, or can be + returned as an object allowing them to be viewed/edited by the user.

+
+ +
+
plot_trace(
+  x,
+  show = NULL,
+  hide = NULL,
+  lag = 20,
+  downsample = TRUE,
+  phase = "sampling",
+  chain = NULL,
+  display = TRUE
+)
+
+ +
+

Arguments

+
x
+

an object of class drjacoby_output

+ + +
show
+

optional vector of parameter names to plot. Parameters matching +show will be included.

+ + +
hide
+

optional vector of parameter names to filter out. Parameters +matching hide will be hidden.

+ + +
lag
+

maximum lag. Must be an integer between 1 and 500.

+ + +
downsample
+

boolean. Whether to downsample chain to make plotting more +efficient.

+ + +
phase
+

which phase to plot. Must be either "burnin", "sampling" or "both".

+ + +
chain
+

which chain to plot.

+ + +
display
+

boolean. Whether to show plots, if FALSE then plotting +objects are returned without displaying.

+ +
+ +
+ +
+ + +
+ + + + + + + + diff --git a/docs/reference/population_data.html b/docs/reference/population_data.html index ec821d1..f6cfbba 100644 --- a/docs/reference/population_data.html +++ b/docs/reference/population_data.html @@ -1,67 +1,12 @@ - - - - - - - -Population data. — population_data • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Population data. — population_data • drjacoby - + + - - - -
-
- -
- -
+
@@ -159,44 +93,43 @@

Population data.

Example population growth data.

-
population_data
+
+
population_data
+
+
+

Format

+

A data frame with 20 rows and 2 variables:

pop
+

population size

-

Format

+
time
+

time

-

A data frame with 20 rows and 2 variables:

-
pop

population size

-
time

time

... -
- +
+
- - - + + diff --git a/docs/reference/run_mcmc.html b/docs/reference/run_mcmc.html index 217c8ac..e66e9dd 100644 --- a/docs/reference/run_mcmc.html +++ b/docs/reference/run_mcmc.html @@ -1,71 +1,16 @@ - - - - - - - -Run drjacoby MCMC — run_mcmc • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Run drjacoby MCMC — run_mcmc • drjacoby - - - - - - - - - - + + - - - -
-
- -
- -
+
@@ -167,166 +101,159 @@

Run drjacoby MCMC

some diagnostics and a record of inputs.

-
run_mcmc(
-  data,
-  df_params,
-  misc = list(),
-  loglike,
-  logprior,
-  burnin = 1000,
-  samples = 10000,
-  rungs = 1,
-  chains = 5,
-  beta_manual = NULL,
-  alpha = 1,
-  target_acceptance = 0.44,
-  cluster = NULL,
-  coupling_on = TRUE,
-  pb_markdown = FALSE,
-  save_data = TRUE,
-  save_hot_draws = FALSE,
-  silent = FALSE
-)
- -

Arguments

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
data

a named list of numeric data values. When using C++ likelihood -and/or prior these values are treated internally as doubles, so while -integer and Boolean values can be used, keep in mind that these will be -recast as doubles in the likelihood (i.e. TRUE = 1.0).

df_params

a data.frame of parameters (see ?define_params).

misc

optional list object passed to likelihood and prior. This can be +

+
run_mcmc(
+  data,
+  df_params,
+  misc = list(),
+  loglike,
+  logprior,
+  burnin = 1000,
+  samples = 10000,
+  rungs = 1,
+  chains = 5,
+  beta_manual = NULL,
+  alpha = 1,
+  target_acceptance = 0.44,
+  cluster = NULL,
+  coupling_on = TRUE,
+  pb_markdown = FALSE,
+  save_data = TRUE,
+  save_hot_draws = FALSE,
+  silent = FALSE
+)
+
+ +
+

Arguments

+
data
+

a named list or data frame or data values.

+ + +
df_params
+

a data.frame of parameters (see ?define_params).

+ + +
misc
+

optional list object passed to likelihood and prior. This can be useful for passing values that are not strictly data, for example passing a -lookup table to the prior function.

loglike, logprior

the log-likelihood and log-prior functions used in +lookup table to the prior function.

+ + +
loglike, logprior
+

the log-likelihood and log-prior functions used in the MCMC. Can either be passed in as R functions (not in quotes), or as -character strings naming compiled C++ functions (in quotes).

burnin

the number of burn-in iterations. Automatic tuning of proposal -standard deviations is only active during the burn-in period.

samples

the number of sampling iterations.

rungs

the number of temperature rungs used in the parallel tempering +character strings naming compiled C++ functions (in quotes).

+ + +
burnin
+

the number of burn-in iterations. Automatic tuning of proposal +standard deviations is only active during the burn-in period.

+ + +
samples
+

the number of sampling iterations.

+ + +
rungs
+

the number of temperature rungs used in the parallel tempering method. By default, \(\beta\) values are equally spaced between 0 and 1, i.e. \(\beta[i]=\)(i-1)/(rungs-1) for i in 1:rungs. The likelihood for the ith heated chain is raised to the power \(\beta[i]^\alpha\), meaning we can use the \(\alpha\) parameter to concentrate rungs towards the start or the end of the interval (see the -alpha argument).

chains

the number of independent replicates of the MCMC to run. If a +alpha argument).

+ + +
chains
+

the number of independent replicates of the MCMC to run. If a cluster object is defined then these chains are run in parallel, -otherwise they are run in serial.

beta_manual

vector of manually defined \(\beta\) values used in the +otherwise they are run in serial.

+ + +
beta_manual
+

vector of manually defined \(\beta\) values used in the parallel tempering approach. If defined, this overrides the spacing defined by rungs. Note that even manually defined \(\beta\) values are raised to the power \(\alpha\) internally, hence you should set -alpha = 1 if you want to fix \(\beta\) values exactly.

alpha

the likelihood for the ith heated chain is +alpha = 1 if you want to fix \(\beta\) values exactly.

+ + +
alpha
+

the likelihood for the ith heated chain is raised to the power \(\beta[i]^\alpha\), meaning we can use the \(\alpha\) parameter to concentrate rungs towards the start or the end of -the temperature scale.

target_acceptance

Target acceptance rate. Should be between 0 and 1. -Default of 0.44, set as optimum for unvariate proposal distributions.

cluster

option to pass in a cluster environment, allowing chains to be -run in parallel (see package "parallel").

coupling_on

whether to implement Metropolis-coupling over temperature +the temperature scale.

+ + +
target_acceptance
+

Target acceptance rate. Should be between 0 and 1. +Default of 0.44, set as optimum for unvariate proposal distributions.

+ + +
cluster
+

option to pass in a cluster environment, allowing chains to be +run in parallel (see package "parallel").

+ + +
coupling_on
+

whether to implement Metropolis-coupling over temperature rungs. The option of deactivating coupling has been retained for general interest and debugging purposes only. If this parameter is FALSE -then parallel tempering will have no impact on MCMC mixing.

pb_markdown

whether to run progress bars in markdown mode, meaning +then parallel tempering will have no impact on MCMC mixing.

+ + +
pb_markdown
+

whether to run progress bars in markdown mode, meaning they are only updated when they reach 100% to avoid large amounts of output -being printed to markdown files.

save_data

if TRUE (the default) the raw input data is stored +being printed to markdown files.

+ + +
save_data
+

if TRUE (the default) the raw input data is stored for reference in the project output. This allows complete reproducibility -from a project, but may be undesirable when datasets are very large.

save_hot_draws

if TRUE the parameter draws relating to the hot +from a project, but may be undesirable when datasets are very large.

+ + +
save_hot_draws
+

if TRUE the parameter draws relating to the hot chains are also stored inside the pt element of the project output. If FALSE (the default) only log-likelihoods and log-priors are -stored from heated chains.

silent

whether to suppress all console output.

+stored from heated chains.

-

Details

+
silent
+

whether to suppress all console output.

+ +
+
+

Details

Note that both data and misc are passed into log-likelihood/log-prior functions *by reference*. This means if you modify these objects inside the functions then any changes will persist.

+
+
- - - + + diff --git a/docs/reference/sample_chains.html b/docs/reference/sample_chains.html index 939eccf..f828216 100644 --- a/docs/reference/sample_chains.html +++ b/docs/reference/sample_chains.html @@ -1,67 +1,12 @@ - - - - - - - -Sample N draws from all available chains — sample_chains • drjacoby - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Sample posterior draws from all available chains — sample_chains • drjacoby - - + + - - -
-
- -
- -
+
-

Sample N draws from all available chains

+

Sample posterior draws from all available chains

+
+ +
+
sample_chains(x, sample_n, keep_chain_index = FALSE)
-
sample_chains(x, sample_n)
+
+

Arguments

+
x
+

an object of class drjacoby_output.

+ + +
sample_n
+

An integer number of samples.

-

Arguments

- - - - - - - - - - -
x

an object of class drjacoby_output

sample_n

An integer number of samples

-

Value

+
keep_chain_index
+

if TRUE then the column giving the chain is +retained.

-

A data.frame of posterior samples

+
+
+

Value

+ + +

A data.frame of posterior samples

+
+
-
- - + + diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 0000000..09abb5f --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,144 @@ + + + + /404.html + + + /LICENSE-text.html + + + /LICENSE.html + + + /articles/blocks.html + + + /articles/checks_double_well.html + + + /articles/checks_multilevel_blocks.html + + + /articles/checks_normal_model.html + + + /articles/checks_return_prior.html + + + /articles/example.html + + + /articles/getting_model_fits.html + + + /articles/index.html + + + /articles/installation.html + + + /articles/metropolis_coupling.html + + + /articles/parallel.html + + + /authors.html + + + /index.html + + + /reference/acf_data.html + + + /reference/check_drjacoby_loaded.html + + + /reference/check_likelihood_compilation.html + + + /reference/check_prior_compilation.html + + + /reference/cpp_function_get.html + + + /reference/cpp_template.html + + + /reference/define_default.html + + + /reference/define_params.html + + + /reference/drjacoby.html + + + /reference/drjacoby_cols.html + + + /reference/drjacoby_file.html + + + /reference/dummy1.html + + + /reference/ess.html + + + /reference/gelman_rubin.html + + + /reference/get_theta_rungs.html + + + /reference/index.html + + + /reference/plot_autocorrelation.html + + + /reference/plot_contour.html + + + /reference/plot_cor.html + + + /reference/plot_cor_mat.html + + + /reference/plot_credible.html + + + /reference/plot_mc_acceptance.html + + + /reference/plot_pairs.html + + + /reference/plot_par.html + + + /reference/plot_rung_loglike.html + + + /reference/plot_scatter.html + + + /reference/plot_trace.html + + + /reference/population_data.html + + + /reference/run_mcmc.html + + + /reference/sample_chains.html + + + /reference/set_rhat.html + + diff --git a/man/plot_density.Rd b/man/plot_density.Rd new file mode 100644 index 0000000..06625a9 --- /dev/null +++ b/man/plot_density.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot.R +\name{plot_density} +\alias{plot_density} +\title{Produce density plots} +\usage{ +plot_density(x, show = NULL, hide = NULL) +} +\arguments{ +\item{x}{an object of class \code{drjacoby_output}} + +\item{show}{optional vector of parameter names to plot. Parameters matching +show will be included.} + +\item{hide}{optional vector of parameter names to filter out. Parameters +matching hide will be hidden.} +} +\description{ +Density plots of all parameters. Use \code{show} and \code{hide} + to be more specific about which parameters to plot. +} diff --git a/man/plot_pairs.Rd b/man/plot_pairs.Rd new file mode 100644 index 0000000..d06431b --- /dev/null +++ b/man/plot_pairs.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot.R +\name{plot_pairs} +\alias{plot_pairs} +\title{Produce scatterplots between multiple parameters} +\usage{ +plot_pairs(x, show = NULL, hide = NULL) +} +\arguments{ +\item{x}{an object of class \code{drjacoby_output}} + +\item{show}{optional vector of parameter names to plot. Parameters matching +show will be included.} + +\item{hide}{optional vector of parameter names to filter out. Parameters +matching hide will be hidden.} +} +\description{ +Uses \code{ggpairs} function from the \code{GGally} package to + produce scatterplots between all named parameters. +} diff --git a/man/plot_cor.Rd b/man/plot_scatter.Rd similarity index 68% rename from man/plot_cor.Rd rename to man/plot_scatter.Rd index 9361776..b0f1c88 100644 --- a/man/plot_cor.Rd +++ b/man/plot_scatter.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/plot.R -\name{plot_cor} -\alias{plot_cor} -\title{Plot parameter correlation} +\name{plot_scatter} +\alias{plot_scatter} +\title{Produce bivariate scatterplot} \usage{ -plot_cor( +plot_scatter( x, parameter1, parameter2, @@ -20,12 +20,13 @@ plot_cor( \item{parameter2}{name of parameter second parameter.} -\item{downsample}{whether to downsample output to speed up plotting.} +\item{downsample}{whether to downsample output to 200 values max to speed up +plotting.} \item{phase}{which phase to plot. Must be either "burnin" or "sampling".} \item{chain}{which chain to plot.} } \description{ -Plots correlation between two parameters +Produces scatterplot between two named parameters. } diff --git a/man/plot_par.Rd b/man/plot_trace.Rd similarity index 93% rename from man/plot_par.Rd rename to man/plot_trace.Rd index 62e977c..19a0b72 100644 --- a/man/plot_par.Rd +++ b/man/plot_trace.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/plot.R -\name{plot_par} -\alias{plot_par} -\title{Plot parameter estimates} +\name{plot_trace} +\alias{plot_trace} +\title{Plot parameter trace} \usage{ -plot_par( +plot_trace( x, show = NULL, hide = NULL, diff --git a/man/sample_chains.Rd b/man/sample_chains.Rd index a133097..6730b0a 100644 --- a/man/sample_chains.Rd +++ b/man/sample_chains.Rd @@ -2,18 +2,21 @@ % Please edit documentation in R/samples.R \name{sample_chains} \alias{sample_chains} -\title{Sample N draws from all available chains} +\title{Sample posterior draws from all available chains} \usage{ -sample_chains(x, sample_n) +sample_chains(x, sample_n, keep_chain_index = FALSE) } \arguments{ -\item{x}{an object of class \code{drjacoby_output}} +\item{x}{an object of class \code{drjacoby_output}.} -\item{sample_n}{An integer number of samples} +\item{sample_n}{An integer number of samples.} + +\item{keep_chain_index}{if \code{TRUE} then the column giving the chain is +retained.} } \value{ A data.frame of posterior samples } \description{ -Sample N draws from all available chains +Sample posterior draws from all available chains } diff --git a/tests/testthat/test-plot.R b/tests/testthat/test-plot.R index b20ee63..05c5892 100644 --- a/tests/testthat/test-plot.R +++ b/tests/testthat/test-plot.R @@ -1,78 +1,78 @@ - -#------------------------------------------------ -test_that("plots do not produce errors", { - - # define example data - # (use small series so that loglikelihood comes back +ve in some cases, which - # forces down a particular path in some plotting functions) - data_list <- list(x = (1:10)*1e-2) - - # define parameters dataframe - df_params <- rbind.data.frame(list("mu", -10, 10, 5), - list("sigma", 0, 20, 1)) - names(df_params) <- c("name", "min", "max", "init") - - # log likelihood - r_loglike <- function(params, data, misc) { - sum(dnorm(data$x, mean = params["mu"], sd = params["sigma"], log = TRUE)) - } - - # log prior - r_logprior <- function(params, misc) { - dnorm(params["mu"], log = TRUE) + dlnorm(params["sigma"]) - } - - # run MCMC - mcmc_out <- run_mcmc(data = data_list, - df_params = df_params, - loglike = r_loglike, - logprior = r_logprior, - burnin = 1e2, - samples = 3e3, - rungs = 2, - chains = 2, - silent = TRUE) - - # expect no output (messages or warnings) in all standard plotting functions - expect_silent(plot_autocorrelation(mcmc_out)) - - expect_silent(plot_mc_acceptance(mcmc_out)) - expect_silent(plot_mc_acceptance(mcmc_out, chain = 1)) - expect_silent(plot_mc_acceptance(mcmc_out, x_axis_type = 2)) - - expect_silent(plot_par(mcmc_out, display = FALSE)) - expect_silent(plot_par(mcmc_out, show = "mu", display = FALSE)) - expect_silent(plot_par(mcmc_out, hide = "mu", display = FALSE)) - expect_silent(plot_par(mcmc_out, phase = "both", display = FALSE)) - - expect_silent(plot_cor(mcmc_out, parameter1 = "mu", parameter2 = "sigma")) - expect_silent(plot_cor(mcmc_out, parameter1 = "mu", parameter2 = "sigma", phase = "both")) - - expect_silent(plot_cor_mat(mcmc_out)) - expect_silent(plot_cor_mat(mcmc_out, show = c("mu", "sigma"))) - expect_silent(plot_cor_mat(mcmc_out, phase = "both")) - - expect_silent(plot_credible(mcmc_out)) - expect_silent(plot_credible(mcmc_out, show = "mu")) - expect_silent(plot_credible(mcmc_out, phase = "both")) - - expect_silent(plot_rung_loglike(mcmc_out)) - expect_silent(plot_rung_loglike(mcmc_out, x_axis_type = 2)) - expect_silent(plot_rung_loglike(mcmc_out, y_axis_type = 2)) - expect_silent(plot_rung_loglike(mcmc_out, y_axis_type = 3)) - - # repeat run with single rung - mcmc_out <- run_mcmc(data = data_list, - df_params = df_params, - loglike = r_loglike, - logprior = r_logprior, - burnin = 1e2, - samples = 3e3, - rungs = 1, - chains = 2, - silent = TRUE) - - # further tests - expect_error(plot_mc_acceptance(mcmc_out)) - -}) + +#------------------------------------------------ +test_that("plots do not produce errors", { + + # define example data + # (use small series so that loglikelihood comes back +ve in some cases, which + # forces down a particular path in some plotting functions) + data_list <- list(x = (1:10)*1e-2) + + # define parameters dataframe + df_params <- rbind.data.frame(list("mu", -10, 10, 5), + list("sigma", 0, 20, 1)) + names(df_params) <- c("name", "min", "max", "init") + + # log likelihood + r_loglike <- function(params, data, misc) { + sum(dnorm(data$x, mean = params["mu"], sd = params["sigma"], log = TRUE)) + } + + # log prior + r_logprior <- function(params, misc) { + dnorm(params["mu"], log = TRUE) + dlnorm(params["sigma"]) + } + + # run MCMC + mcmc_out <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e2, + samples = 3e3, + rungs = 2, + chains = 2, + silent = TRUE) + + # expect no output (messages or warnings) in all standard plotting functions + expect_silent(plot_autocorrelation(mcmc_out)) + + expect_silent(plot_mc_acceptance(mcmc_out)) + expect_silent(plot_mc_acceptance(mcmc_out, chain = 1)) + expect_silent(plot_mc_acceptance(mcmc_out, x_axis_type = 2)) + + expect_silent(plot_trace(mcmc_out, display = FALSE)) + expect_silent(plot_trace(mcmc_out, show = "mu", display = FALSE)) + expect_silent(plot_trace(mcmc_out, hide = "mu", display = FALSE)) + expect_silent(plot_trace(mcmc_out, phase = "both", display = FALSE)) + + expect_silent(plot_scatter(mcmc_out, parameter1 = "mu", parameter2 = "sigma")) + expect_silent(plot_scatter(mcmc_out, parameter1 = "mu", parameter2 = "sigma", phase = "both")) + + expect_silent(plot_cor_mat(mcmc_out)) + expect_silent(plot_cor_mat(mcmc_out, show = c("mu", "sigma"))) + expect_silent(plot_cor_mat(mcmc_out, phase = "both")) + + expect_silent(plot_credible(mcmc_out)) + expect_silent(plot_credible(mcmc_out, show = "mu")) + expect_silent(plot_credible(mcmc_out, phase = "both")) + + expect_silent(plot_rung_loglike(mcmc_out)) + expect_silent(plot_rung_loglike(mcmc_out, x_axis_type = 2)) + expect_silent(plot_rung_loglike(mcmc_out, y_axis_type = 2)) + expect_silent(plot_rung_loglike(mcmc_out, y_axis_type = 3)) + + # repeat run with single rung + mcmc_out <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e2, + samples = 3e3, + rungs = 1, + chains = 2, + silent = TRUE) + + # further tests + expect_error(plot_mc_acceptance(mcmc_out)) + +}) diff --git a/vignettes/checks_double_well.Rmd b/vignettes/checks_double_well.Rmd index 5636958..736f673 100644 --- a/vignettes/checks_double_well.Rmd +++ b/vignettes/checks_double_well.Rmd @@ -1,119 +1,119 @@ ---- -title: "Double well" -author: "Bob Verity and Pete Winskill" -date: "`r Sys.Date()`" -output: rmarkdown::html_vignette -vignette: > - %\VignetteIndexEntry{Double well} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - -```{r setup, include=FALSE} -knitr::opts_chunk$set(echo = TRUE) -``` - -```{r, echo = FALSE} -# set random seed -set.seed(1) - -# load the drjacoby package -library(drjacoby) -``` - -**Purpose:** to compare *drjacoby* results for a challenging problem involving a multimodal posterior, both with and without temperature rungs. - -## Model - -We assume a single parameter `mu` drawn from a double well potential distribution, defined by the formula: - -$$ -\begin{aligned} - \mu &\propto exp\left(-\gamma(\mu^2 - 1)^2\right) -\end{aligned} -$$ -where $\gamma$ is a parameter that defines the strength of the well (higher $\gamma$ leads to a deeper valley and hence more challenging problem). NB, there is no data in this example, as the likelihood is defined exactly by these parameters. - -Likelihood and prior: - -```{r, echo = FALSE, comment = ''} -Rcpp::sourceCpp(system.file("extdata/checks/", "doublewell_loglike_logprior.cpp", package = 'drjacoby', mustWork = TRUE)) -``` - -Parameters dataframe: - -```{r} -L <- 2 -gamma <- 30 -df_params <- define_params(name = "mu", min = -L, max = L, - name = "gamma", min = gamma, max = gamma) -``` - - -## Single temperature rung (no Metropolis coupling) - -```{r} -mcmc <- run_mcmc(data = list(x = -1), - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e5, - chains = 1, - rungs = 1, - silent = TRUE) -``` - -```{r} -# trace plot -plot_par(mcmc, show = "mu") -``` - -```{r} -# extract posterior draws -output_sub <- subset(mcmc$output, phase == "sampling") -mu_draws <- output_sub$mu - -# get analytical solution -x <- seq(-L, L, l = 1001) -fx <- exp(-gamma*(x^2 - 1)^2) -fx <- fx / sum(fx) * 1/(x[2]-x[1]) - -# overlay plots -hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black") -lines(x, fx, col = 2, lwd = 2) -``` - - -## Multiple temperature rungs - -```{r} -mcmc <- run_mcmc(data = list(x = -1), - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e5, - chains = 1, - rungs = 11, - alpha = 2, - pb_markdown = TRUE) -``` - -```{r, fig.width=6, fig.height=5} -# trace plot -plot_par(mcmc, show = "mu") - -# coupling acceptance plot -plot_mc_acceptance(mcmc) -``` - -```{r, fig.width=6, fig.height=5} -# extract posterior draws -output_sub <- subset(mcmc$output, phase == "sampling") -mu_draws <- output_sub$mu - -# overlay plots -hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black") -lines(x, fx, col = 2, lwd = 2) -``` +--- +title: "Double well" +author: "Bob Verity and Pete Winskill" +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Double well} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE) +``` + +```{r, echo = FALSE} +# set random seed +set.seed(1) + +# load the drjacoby package +library(drjacoby) +``` + +**Purpose:** to compare *drjacoby* results for a challenging problem involving a multimodal posterior, both with and without temperature rungs. + +## Model + +We assume a single parameter `mu` drawn from a double well potential distribution, defined by the formula: + +$$ +\begin{aligned} + \mu &\propto exp\left(-\gamma(\mu^2 - 1)^2\right) +\end{aligned} +$$ +where $\gamma$ is a parameter that defines the strength of the well (higher $\gamma$ leads to a deeper valley and hence more challenging problem). NB, there is no data in this example, as the likelihood is defined exactly by these parameters. + +Likelihood and prior: + +```{r, echo = FALSE, comment = ''} +Rcpp::sourceCpp(system.file("extdata/checks/", "doublewell_loglike_logprior.cpp", package = 'drjacoby', mustWork = TRUE)) +``` + +Parameters dataframe: + +```{r} +L <- 2 +gamma <- 30 +df_params <- define_params(name = "mu", min = -L, max = L, + name = "gamma", min = gamma, max = gamma) +``` + + +## Single temperature rung (no Metropolis coupling) + +```{r} +mcmc <- run_mcmc(data = list(x = -1), + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e5, + chains = 1, + rungs = 1, + silent = TRUE) +``` + +```{r} +# trace plot +plot_trace(mcmc, show = "mu") +``` + +```{r} +# extract posterior draws +output_sub <- subset(mcmc$output, phase == "sampling") +mu_draws <- output_sub$mu + +# get analytical solution +x <- seq(-L, L, l = 1001) +fx <- exp(-gamma*(x^2 - 1)^2) +fx <- fx / sum(fx) * 1/(x[2]-x[1]) + +# overlay plots +hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black") +lines(x, fx, col = 2, lwd = 2) +``` + + +## Multiple temperature rungs + +```{r} +mcmc <- run_mcmc(data = list(x = -1), + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e5, + chains = 1, + rungs = 11, + alpha = 2, + pb_markdown = TRUE) +``` + +```{r, fig.width=6, fig.height=5} +# trace plot +plot_trace(mcmc, show = "mu") + +# coupling acceptance plot +plot_mc_acceptance(mcmc) +``` + +```{r, fig.width=6, fig.height=5} +# extract posterior draws +output_sub <- subset(mcmc$output, phase == "sampling") +mu_draws <- output_sub$mu + +# overlay plots +hist(mu_draws, breaks = seq(-L, L, l = 201), probability = TRUE, main = "", col = "black") +lines(x, fx, col = 2, lwd = 2) +``` diff --git a/vignettes/example.Rmd b/vignettes/example.Rmd index a50ed4e..54fd512 100644 --- a/vignettes/example.Rmd +++ b/vignettes/example.Rmd @@ -1,217 +1,217 @@ ---- -title: "Basic Example" -author: "Bob Verity and Pete Winskill" -date: "`r Sys.Date()`" -output: rmarkdown::html_vignette -vignette: > - %\VignetteIndexEntry{Basic Example} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - -```{r setup, include = FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -``` - -```{r, echo = FALSE} -# set random seed -set.seed(1) - -# load the drjacoby package -library(drjacoby) -``` - -The likelihood and prior distributions that go into *drjacoby* can be specified by the user either as R functions or as C++ functions. This vignette demonstrates a basic MCMC implementation using both the R and C++ methods, and compares the two in terms of speed. - - -## Setup - -We need the following elements to run *drjacoby*: - -1. Some data -2. Some parameters -3. A likelihood function -4. A prior function - -Starting with the data, let's assume that our observations consist of a series of draws from a normal distribution with a given mean (`mu_true`) and standard deviation (`sigma_true`). We can generate some random data to play with: - -```{r} -# set random seed -set.seed(1) - -# define true parameter values -mu_true <- 3 -sigma_true <- 2 - -# draw example data -data_list <- list(x = rnorm(10, mean = mu_true, sd = sigma_true)) -``` - -Notice that data needs to be defined as a named list. For our example MCMC we will assume that we know the correct distribution of the data (i.e. we know the data is normally distributed), and we know that the mean is no smaller than -10 and no larger than 10, but otherwise the parameters of the distribution are unknown. Parameters within *drjacoby* are defined in dataframe format, where we specify minimum and maximum values of all parameters. The function `define_params()` makes it slightly easier to define this dataframe, but standard methods for making dataframes are also fine: - -```{r} -# define parameters dataframe -df_params <- define_params(name = "mu", min = -10, max = 10, - name = "sigma", min = 0, max = Inf) - -print(df_params) -``` - -In this example we have one parameter (`mu`) that occupies a finite range [-10, 10], and another parameter (`sigma`) that can take any positive value. *drjacoby* deals with different parameter ranges using reparameterisation, which all occurs internally meaning we don't need to worry about these constraints affecting our inference. - -Next, we need a likelihood function. This **must** have the following three input arguments, specified in this order: - -1. `params` - a named vector of parameter values -3. `data` - a named list of data -4. `misc` - a named list of miscellaneous values - -Finally, the function **must** return a single value for the likelihood **in log space**. These constraints on the format of the likelihood function might seem a bit restrictive, but they are needed in order for *drjacoby* to know how to use the function internally. The issue of taking logs is particularly important, as the MCMC will still run even if we forget to take logs, but the results produced will be nonsense! - -![Do not underestimate the importance of taking logs.](https://raw.githubusercontent.com/mrc-ide/drjacoby/master/R_ignore/images/loglady.jpg) - - -Inside the likelihood function we can extract individual parameter values from the input vector and then use these values to calculate the probability of the data. In our example, the likelihood function is quite simple thanks to the `dnorm()` function which can return the density of the normal distribution already in log space: - -```{r} -# define log-likelihood function -r_loglike <- function(params, data, misc) { - - # extract parameter values - mu <- params["mu"] - sigma <- params["sigma"] - - # calculate log-probability of data - ret <- sum(dnorm(data$x, mean = mu, sd = sigma, log = TRUE)) - - # return - return(ret) -} -``` - -Notice that we don't use the `misc` object at all here, which is perfectly fine. Finally, we need a prior function. This **must** have the `params` and `misc` arguments as above, and it **must** return a single value for the prior probability of those parameters **in log space**. Again, this strict format is required for *drjacoby* to know how to use the prior internally. In our case we will assume a uniform prior on `mu`, and a log-normal prior on `sigma`: - -```{r} -# define log-prior function -r_logprior <- function(params, misc) { - - # extract parameter values - mu <- params["mu"] - sigma <- params["sigma"] - - # calculate log-prior - ret <- dunif(mu, min = -10, max = 10, log = TRUE) + - dlnorm(sigma, meanlog = 0, sdlog = 1.0, log = TRUE) - - # return - return(ret) -} -``` - -Be careful to ensure that your prior is defined over the same range as specified in the `df_params` dataframe. For example, here our uniform prior for `mu` ranges from -10 to 10, and our log-normal prior for `sigma` ranges from 0 to infinity. - - -## Running the MCMC - -Once we have all the elements above it is straightforward to run a basic MCMC in *drjacoby*. We simply input the four elements, along with the number of burn-in and sampling iterations. By default *drjacoby* prints progress bars to the console to keep you updated on the progress of the MCMC. When running in R markdown we can use the option `pb_markdown = TRUE` to print progress bars in a markdown-friendly way, although you will probably want to leave this option turned off when running interactively (simply delete this argument). - -```{r} -# run MCMC -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = r_loglike, - logprior = r_logprior, - burnin = 1e3, - samples = 1e3, - pb_markdown = TRUE) -``` - -The output returned by the MCMC function has four parts: 1) an "output" dataframe containing raw posterior draws and other key elements at each iteration of the MCMC, 2) a "pt" dataframe containing output related to parallel tempering (if not used this just contains the log-likelihood and log-prior at every iteration), 3) a "diagnostics" object containing useful summaries such as the effective sample size of each parameter, 3) a "parameters" object containing a record of the exact parameters used to run the MCMC. We can take a peek at the first of these outputs: - -```{r} -head(mcmc$output) -``` - -We can see that both `mu` and `sigma` changed values throughout the burn-in period, as we would expect, and the loglikelihood generally increased. - - -## Exploring outputs and checking MCMC performance - -Before we can draw any conclusions from our MCMC results there are some basic checks that we should carry out. First, we can examine trace plots of all parameters to see how they are moving. This can be done using the `plot_par()` function, shown here for the burn-in phase: - -```{r, fig.width=8, fig.height=4} -plot_par(mcmc, show = "mu", phase = "burnin") -plot_par(mcmc, show = "sigma", phase = "burnin") -``` - -Notice that for all five chains both `mu` and `sigma` move quickly from their initial values to stable levels at around 3 and 2, respectively. This is a visual indication that the MCMC has burned in for an appropriate number of iterations. We can check this more rigorously by looking at the `rhat` object within the MCMC diagnostics, which gives the value of the [Gelman-Rubin convergence diagnostic](https://www.rdocumentation.org/packages/coda/versions/0.19-4/topics/gelman.diag). - -```{r} -mcmc$diagnostics$rhat -``` - -Values close to 1 indicate that the MCMC has converged (typically the threshold <1.1 is used). This output uses the variance between chains, and therefore is only availble when running multiple chains. - -By setting `phase = "sampling"` we can look at trace plots from the sampling phase only: - -```{r, fig.width=8, fig.height=4} -plot_par(mcmc, show = "mu", phase = "sampling") -plot_par(mcmc, show = "sigma", phase = "sampling") -``` - -Here we can see that the MCMC continues to move freely after the burn-in phase, and that all chains appear to be exploring the same part of the parameter space. The final plot shows the autocorrelation of the chains, which in this case falls off rapidly with samples being approximately independent at around 10 lags. This is an indication that the MCMC is mixing well. The marginal histogram shows a single clear peak, although this is still a bit rough so we may want to re-run the MCMC with a larger number of sampling iterations to get a smoother result. We can also explore this by looking at the effective sample size (ESS), which is stored within the MCMC diagnostics: - -```{r} -mcmc$diagnostics$ess -``` - -We can see that despite running the MCMC for 1000 sampling iterations, the actual number of effectively independent samples accounting for autocorrelation is lower. When doing any calculation that relies on the number of samples we should use the ESS rather than the raw number of sampling iterations. For example if we want to know the standard error of our estimate of `mu` we should do `sd(mu)/sqrt(ESS)`. But just as importantly, when producing any summary of the posterior that does not make direct use of the number of samples - for example when producing posterior histograms - we should *use all posterior draws*, and we should [not thin samples](https://doi.org/10.1111/j.2041-210X.2011.00131.x) to reduce autocorrelation. This is because a histogram produced from all samples is more accurate than one produced from thinned samples, even if the samples are autocorrelated. - -The final question is how confident can we be that our MCMC has actually explored the space well? Although the trace plot above looks good, it is possible to get results that look like this from very pathological MCMC runs. This is a more complex problem that is dealt with in [another vignette](https://mrc-ide.github.io/drjacoby/articles/metropolis_coupling.html). - - -## Using C++ functions - -Although *drjacoby* is an R package, under the hood it is running C++ through the fantastic [Rcpp](https://cran.r-project.org/web/packages/Rcpp/index.html) package. When we pass an R likelihood function to `run_mcmc()`, as in the example above, the code is forced to jump out of C++ into R to evaluate the likelihood before jumping back into C++. This creates a computational overhead that can be avoided by specifying functions directly within C++. - -To use C++ a function within *drjacoby* we first write it out as stand-alone file. The example used here can be found inside the *inst/extdata* folder of the package, and reads as follows: - -```{r, echo = FALSE, comment = ''} -likelihood_filename <- system.file("extdata", "example_loglike_logprior.cpp", package = 'drjacoby', mustWork = TRUE) -cat(readLines(likelihood_filename), sep = '\n') -``` - -The logic of this function is identical to the R version above, just written in the language of C++. To allow drjacoby to talk to your C++ function(s) we need to have a linking function in this C++ file, in this case called `create_xptr()`. With this set up all we need to do is source our C++ file with `Rccp::sourcecpp()` then simply pass the function *names* to `run_mcmc()`. To make life easy when setting up a new C++ model, you can create a template for your cpp file with the `cpp_template()` function - then all you need to do is fill in the internals of the loglikehood and logprior. - -As before, there are some constraints on what this function must look like. It **must** take the same three input arguments described in the previous section, defined in the following formats: - -1. `params` must be an `Rcpp::NumericVector` -3. `data` must be an `Rcpp::List` -4. `misc` must be an `Rcpp::List` - -All parameter values will be coerced to `double` within the code, hence parameters consisting of integer or boolean values should be dealt with as though they are continuous (for example `TRUE = 1.0`, `FALSE = 0.0`). Second, the function **must** return an object of class `SEXP`. The easiest way to achieve this is to calculate the raw return value as a double, and then to use the `Rcpp::wrap()` function to transform to `SEXP`. As before, the value returned should be the likelihood evaluated in **log space**. - -Even though we are working in C++, we still have access to most of R's nice distribution functions through the `R::` namespace. For example, the `dnorm()` function can be accessed within C++ using `R::dnorm()`. A list of probability distributions available within Rcpp can be found [here](https://teuder.github.io/rcpp4everyone_en/220_dpqr_functions.html#gamma-distribution). - -With these two functions in hand we can run the MCMC exactly the same as before, passing in the new functions: - -```{r, echo = FALSE, comment = '', warning=FALSE} -Rcpp::sourceCpp(likelihood_filename) -``` - -```{r} -# run MCMC -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e3, - pb_markdown = TRUE) -``` - -You should see that this MCMC runs considerably faster than the previous version that used R functions. On the other hand, writing C++ functions is arguably more error-prone and difficult to debug than simple R functions. Hence, if efficiency is your goal then C++ versions of both the likelihood and prior should be used, whereas if ease of programming is more important then R functions might be a better choice. - -This was a very easy problem, and so required no fancy MCMC tricks. The [next vignette](https://mrc-ide.github.io/drjacoby/articles/metropolis_coupling.html) demonstrates how *drjacoby* can be applied to a more challenging problem. +--- +title: "Basic Example" +author: "Bob Verity and Pete Winskill" +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Basic Example} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r setup, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r, echo = FALSE} +# set random seed +set.seed(1) + +# load the drjacoby package +library(drjacoby) +``` + +The likelihood and prior distributions that go into *drjacoby* can be specified by the user either as R functions or as C++ functions. This vignette demonstrates a basic MCMC implementation using both the R and C++ methods, and compares the two in terms of speed. + + +## Setup + +We need the following elements to run *drjacoby*: + +1. Some data +2. Some parameters +3. A likelihood function +4. A prior function + +Starting with the data, let's assume that our observations consist of a series of draws from a normal distribution with a given mean (`mu_true`) and standard deviation (`sigma_true`). We can generate some random data to play with: + +```{r} +# set random seed +set.seed(1) + +# define true parameter values +mu_true <- 3 +sigma_true <- 2 + +# draw example data +data_list <- list(x = rnorm(10, mean = mu_true, sd = sigma_true)) +``` + +Notice that data needs to be defined as a named list. For our example MCMC we will assume that we know the correct distribution of the data (i.e. we know the data is normally distributed), and we know that the mean is no smaller than -10 and no larger than 10, but otherwise the parameters of the distribution are unknown. Parameters within *drjacoby* are defined in dataframe format, where we specify minimum and maximum values of all parameters. The function `define_params()` makes it slightly easier to define this dataframe, but standard methods for making dataframes are also fine: + +```{r} +# define parameters dataframe +df_params <- define_params(name = "mu", min = -10, max = 10, + name = "sigma", min = 0, max = Inf) + +print(df_params) +``` + +In this example we have one parameter (`mu`) that occupies a finite range [-10, 10], and another parameter (`sigma`) that can take any positive value. *drjacoby* deals with different parameter ranges using reparameterisation, which all occurs internally meaning we don't need to worry about these constraints affecting our inference. + +Next, we need a likelihood function. This **must** have the following three input arguments, specified in this order: + +1. `params` - a named vector of parameter values +3. `data` - a named list of data +4. `misc` - a named list of miscellaneous values + +Finally, the function **must** return a single value for the likelihood **in log space**. These constraints on the format of the likelihood function might seem a bit restrictive, but they are needed in order for *drjacoby* to know how to use the function internally. The issue of taking logs is particularly important, as the MCMC will still run even if we forget to take logs, but the results produced will be nonsense! + +![Do not underestimate the importance of taking logs.](https://raw.githubusercontent.com/mrc-ide/drjacoby/master/R_ignore/images/loglady.jpg) + + +Inside the likelihood function we can extract individual parameter values from the input vector and then use these values to calculate the probability of the data. In our example, the likelihood function is quite simple thanks to the `dnorm()` function which can return the density of the normal distribution already in log space: + +```{r} +# define log-likelihood function +r_loglike <- function(params, data, misc) { + + # extract parameter values + mu <- params["mu"] + sigma <- params["sigma"] + + # calculate log-probability of data + ret <- sum(dnorm(data$x, mean = mu, sd = sigma, log = TRUE)) + + # return + return(ret) +} +``` + +Notice that we don't use the `misc` object at all here, which is perfectly fine. Finally, we need a prior function. This **must** have the `params` and `misc` arguments as above, and it **must** return a single value for the prior probability of those parameters **in log space**. Again, this strict format is required for *drjacoby* to know how to use the prior internally. In our case we will assume a uniform prior on `mu`, and a log-normal prior on `sigma`: + +```{r} +# define log-prior function +r_logprior <- function(params, misc) { + + # extract parameter values + mu <- params["mu"] + sigma <- params["sigma"] + + # calculate log-prior + ret <- dunif(mu, min = -10, max = 10, log = TRUE) + + dlnorm(sigma, meanlog = 0, sdlog = 1.0, log = TRUE) + + # return + return(ret) +} +``` + +Be careful to ensure that your prior is defined over the same range as specified in the `df_params` dataframe. For example, here our uniform prior for `mu` ranges from -10 to 10, and our log-normal prior for `sigma` ranges from 0 to infinity. + + +## Running the MCMC + +Once we have all the elements above it is straightforward to run a basic MCMC in *drjacoby*. We simply input the four elements, along with the number of burn-in and sampling iterations. By default *drjacoby* prints progress bars to the console to keep you updated on the progress of the MCMC. When running in R markdown we can use the option `pb_markdown = TRUE` to print progress bars in a markdown-friendly way, although you will probably want to leave this option turned off when running interactively (simply delete this argument). + +```{r} +# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e3, + samples = 1e3, + pb_markdown = TRUE) +``` + +The output returned by the MCMC function has four parts: 1) an "output" dataframe containing raw posterior draws and other key elements at each iteration of the MCMC, 2) a "pt" dataframe containing output related to parallel tempering (if not used this just contains the log-likelihood and log-prior at every iteration), 3) a "diagnostics" object containing useful summaries such as the effective sample size of each parameter, 3) a "parameters" object containing a record of the exact parameters used to run the MCMC. We can take a peek at the first of these outputs: + +```{r} +head(mcmc$output) +``` + +We can see that both `mu` and `sigma` changed values throughout the burn-in period, as we would expect, and the loglikelihood generally increased. + + +## Exploring outputs and checking MCMC performance + +Before we can draw any conclusions from our MCMC results there are some basic checks that we should carry out. First, we can examine trace plots of all parameters to see how they are moving. This can be done using the `plot_trace()` function, shown here for the burn-in phase: + +```{r, fig.width=8, fig.height=4} +plot_trace(mcmc, show = "mu", phase = "burnin") +plot_trace(mcmc, show = "sigma", phase = "burnin") +``` + +Notice that for all five chains both `mu` and `sigma` move quickly from their initial values to stable levels at around 3 and 2, respectively. This is a visual indication that the MCMC has burned in for an appropriate number of iterations. We can check this more rigorously by looking at the `rhat` object within the MCMC diagnostics, which gives the value of the [Gelman-Rubin convergence diagnostic](https://www.rdocumentation.org/packages/coda/versions/0.19-4/topics/gelman.diag). + +```{r} +mcmc$diagnostics$rhat +``` + +Values close to 1 indicate that the MCMC has converged (typically the threshold <1.1 is used). This output uses the variance between chains, and therefore is only availble when running multiple chains. + +By setting `phase = "sampling"` we can look at trace plots from the sampling phase only: + +```{r, fig.width=8, fig.height=4} +plot_trace(mcmc, show = "mu", phase = "sampling") +plot_trace(mcmc, show = "sigma", phase = "sampling") +``` + +Here we can see that the MCMC continues to move freely after the burn-in phase, and that all chains appear to be exploring the same part of the parameter space. The final plot shows the autocorrelation of the chains, which in this case falls off rapidly with samples being approximately independent at around 10 lags. This is an indication that the MCMC is mixing well. The marginal histogram shows a single clear peak, although this is still a bit rough so we may want to re-run the MCMC with a larger number of sampling iterations to get a smoother result. We can also explore this by looking at the effective sample size (ESS), which is stored within the MCMC diagnostics: + +```{r} +mcmc$diagnostics$ess +``` + +We can see that despite running the MCMC for 1000 sampling iterations over 5 chains (5000 in total), the actual number of effectively independent samples accounting for autocorrelation is lower. When doing any calculation that relies on the number of samples we should use the ESS rather than the raw number of sampling iterations. For example if we want to know the standard error of our estimate of `mu` we should do `sd(mu)/sqrt(ESS)`. But just as importantly, when producing any summary of the posterior that does not make direct use of the number of samples - for example when producing posterior histograms - we should *use all posterior draws*, and we should [not thin samples](https://doi.org/10.1111/j.2041-210X.2011.00131.x) to reduce autocorrelation. This is because a histogram produced from all samples is more accurate than one produced from thinned samples, even if the samples are autocorrelated. + +The final question is how confident can we be that our MCMC has actually explored the space well? Although the trace plot above looks good, it is possible to get results that look like this from very pathological MCMC runs. This is a more complex problem that is dealt with in [another vignette](https://mrc-ide.github.io/drjacoby/articles/metropolis_coupling.html). + + +## Using C++ functions + +Although *drjacoby* is an R package, under the hood it is running C++ through the fantastic [Rcpp](https://cran.r-project.org/web/packages/Rcpp/index.html) package. When we pass an R likelihood function to `run_mcmc()`, as in the example above, the code is forced to jump out of C++ into R to evaluate the likelihood before jumping back into C++. This creates a computational overhead that can be avoided by specifying functions directly within C++. + +To use C++ a function within *drjacoby* we first write it out as stand-alone file. The example used here can be found inside the *inst/extdata* folder of the package, and reads as follows: + +```{r, echo = FALSE, comment = ''} +likelihood_filename <- system.file("extdata", "example_loglike_logprior.cpp", package = 'drjacoby', mustWork = TRUE) +cat(readLines(likelihood_filename), sep = '\n') +``` + +The logic of this function is identical to the R version above, just written in the language of C++. To allow drjacoby to talk to your C++ function(s) we need to have a linking function in this C++ file, in this case called `create_xptr()`. With this set up all we need to do is source our C++ file with `Rccp::sourcecpp()` then simply pass the function *names* to `run_mcmc()`. To make life easy when setting up a new C++ model, you can create a template for your cpp file with the `cpp_template()` function - then all you need to do is fill in the internals of the loglikehood and logprior. + +As before, there are some constraints on what this function must look like. It **must** take the same three input arguments described in the previous section, defined in the following formats: + +1. `params` must be an `Rcpp::NumericVector` +3. `data` must be an `Rcpp::List` +4. `misc` must be an `Rcpp::List` + +All parameter values will be coerced to `double` within the code, hence parameters consisting of integer or boolean values should be dealt with as though they are continuous (for example `TRUE = 1.0`, `FALSE = 0.0`). Second, the function **must** return an object of class `SEXP`. The easiest way to achieve this is to calculate the raw return value as a double, and then to use the `Rcpp::wrap()` function to transform to `SEXP`. As before, the value returned should be the likelihood evaluated in **log space**. + +Even though we are working in C++, we still have access to most of R's nice distribution functions through the `R::` namespace. For example, the `dnorm()` function can be accessed within C++ using `R::dnorm()`. A list of probability distributions available within Rcpp can be found [here](https://teuder.github.io/rcpp4everyone_en/220_dpqr_functions.html#gamma-distribution). + +With these two functions in hand we can run the MCMC exactly the same as before, passing in the new functions: + +```{r, echo = FALSE, comment = '', warning=FALSE} +Rcpp::sourceCpp(likelihood_filename) +``` + +```{r} +# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + pb_markdown = TRUE) +``` + +You should see that this MCMC runs considerably faster than the previous version that used R functions. On the other hand, writing C++ functions is arguably more error-prone and difficult to debug than simple R functions. Hence, if efficiency is your goal then C++ versions of both the likelihood and prior should be used, whereas if ease of programming is more important then R functions might be a better choice. + +This was a very easy problem, and so required no fancy MCMC tricks. The [next vignette](https://mrc-ide.github.io/drjacoby/articles/metropolis_coupling.html) demonstrates how *drjacoby* can be applied to a more challenging problem. diff --git a/vignettes/getting_model_fits.Rmd b/vignettes/getting_model_fits.Rmd index 5169150..9c98344 100644 --- a/vignettes/getting_model_fits.Rmd +++ b/vignettes/getting_model_fits.Rmd @@ -1,140 +1,142 @@ ---- -title: "Getting Model Fits" -author: "Bob Verity and Pete Winskill" -date: "`r Sys.Date()`" -output: rmarkdown::html_vignette -vignette: > - %\VignetteIndexEntry{Getting Model Fits} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - -```{r, include = FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -``` - -```{r, echo = FALSE} -# set random seed -set.seed(1) - -# load the drjacoby package -library(drjacoby) - -# load other packages -library(ggplot2) -``` - -Sometimes our likelihood function involves calculating a series of intermediate values. A good example is when we have a model that describes our fit to the data, which may be combined with an error term to capture random noise around this prediction. Once the MCMC has finished, we may want to extract this model fit so that we can see how good it really is. Having already written down the code for calculating the model fit once within the likelihood, we don't want to have to write it down again when visualising results as this duplication of work is not only a waste of time but also can introduce errors. - -This vignette demonstrates how intermediate values can be obtained from MCMC output using a single likelihood function. - - -## A simple model of population growth - -For this example we will use some data on population growth and we will attempt to fit a simple curve through these points. The data can be loaded directly from within the *drjacoby* package, and takes the form of a simple data.frame of sampling times and population sizes: - -```{r, width = 10, height = 5} -# load example data -data("population_data") -head(population_data) - -# plot points -plot(population_data$time, population_data$pop, ylim = c(0, 120), pch = 20, - xlab = "Time (days)", ylab = "Population size") -``` - -For our model we will assume a discrete-time logistic growth model, in which the population starts at size $N_0$ and grows at a rate $r$ each timestep, but also experiences density-dependent death causing it to plateau out a maximum population size $N_{max}$. If we use $N_t$ to denote the population size at time $t$ then we can write this model as follows: - -$$ -\begin{align} - N_{t+1} = rN_t(1 - N_t / K), \hspace{10mm} \mbox{where } K = N_{\mbox{max}}\left(\frac{r}{r-1}\right). -\end{align} -$$ - -There are three parameters in this model, and so our parameters dataframe should contain the three with suitable ranges as follows: - -```{r} -# define parameters dataframe -df_params <- define_params(name = "N_0", min = 1, max = 100, - name = "r", min = 1, max = 2, - name = "N_max", min = 50, max = 200) -``` - -Within our likelihood function we need to calculate our expected population growth curve. This curve is perfectly smooth, but the real data is noisy so we will use a Poisson distribution to link the data to the model. - -Crucially, when defining the likelihood function we will create an option to return the model prediction, rather than the log-likelihood. We use the `misc` input to the function to control whether this option is active or not. If we set `misc$output_type = 1` then we will obtain the log-likelihood, but if we set `misc$output_type = 2` then we will obtain the complete curve of model-predicted values: - -```{r} -# define log-likelihood function -loglike <- function(params, data, misc) { - - # extract parameter values - N_0 <- params["N_0"] - r <- params["r"] - N_max <- params["N_max"] - - # calculate expected population growth - K <- N_max * r / (r - 1) - x <- rep(0, 100) - x[1] <- N_0 - for (i in 2:length(x)) { - x[i] <- r * x[i-1] * (1 - x[i-1] / K) - } - - # option to return model prediction rather than log-likelihood - if (misc$output_type == 2) { - return(x) - } - - # calculate log-likelihood - ret <- sum(dpois(data$pop, lambda = x[data$time], log = TRUE)) - - # return - return(ret) -} - -# define R logprior function -logprior <- function(params, misc) { - return(0) -} -``` - -When running the MCMC we want to make sure `misc$output_type = 1`: - -```{r} -# run MCMC -mcmc <- run_mcmc(data = population_data, - df_params = df_params, - misc = list(output_type = 1), - loglike = loglike, - logprior = logprior, - burnin = 1e3, - samples = 1e4, - chains = 3, - pb_markdown = TRUE) -``` - -Once we have MCMC output we should perform our usual checks to make sure that everything has converged nicely. Assuming everything is OK, we can move on to exploring model fits. We will do this by running the same likelihood function but with `misc$output_type = 2`, using the posterior parameter draws as inputs. We may not want to use every posterior parameter draw, in which case we can use the `sample_chains()` function to sub-sample the output down as needed: - -```{r} -# sample from posterior -param_draws <- sample_chains(mcmc, 1000) - -# get matrix of model predictions -model_matrix <- apply(param_draws, 1, function(x) { - loglike(params = x, data = population_data, misc = list(output_type = 2)) -}) -``` - -Plotting these against the data we can see that - reassuringly - the posterior model fits make a smooth curve through the data points: - -```{r} -matplot(model_matrix, type = 'l', lty = 1, col = "#99000010", ylim = c(0, 120), - xlab = "Time (days)", ylab = "Population size") -points(population_data$time, population_data$pop, pch = 20) -``` - -Notice that we only had to define the model fitting computation once in this pipeline, as the same likelihood function was used for both inference and obtaining the fit. There are more complicated versions of this approach that may be useful in some settings, for example using `switch` functions or multiple `if` statements to allow for several different types of output from the likelihood function, or even varying aspects of the core model such as the error distribution using flags contained in `misc`. +--- +title: "Getting Model Fits" +author: "Bob Verity and Pete Winskill" +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Getting Model Fits} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r, echo = FALSE} +# set random seed +set.seed(1) + +# load the drjacoby package +library(drjacoby) + +# load other packages +library(ggplot2) +``` + +Sometimes our likelihood function involves calculating a series of intermediate values. A good example is when we have a model that describes our fit to the data, which may be combined with an error term to capture random noise around this prediction. Once the MCMC has finished, we may want to extract this model fit so that we can see how good it really is. Having already written down the code for calculating the model fit once within the likelihood, we don't want to have to write it down again when visualising results as this duplication of work is not only a waste of time but also can introduce errors. + +This vignette demonstrates how intermediate values can be obtained from MCMC output using a single likelihood function. + + +## A simple model of population growth + +For this example we will use some data on population growth and we will attempt to fit a simple curve through these points. The data can be loaded directly from within the *drjacoby* package, and takes the form of a simple data.frame of sampling times and population sizes: + +```{r, width = 10, height = 5} +# load example data +data("population_data") +head(population_data) + +# plot points +plot(population_data$time, population_data$pop, ylim = c(0, 120), pch = 20, + xlab = "Time (days)", ylab = "Population size") +``` + +For our model we will assume a discrete-time logistic growth model, in which the population starts at size $N_0$ and grows at a rate $r$ each timestep, but also experiences density-dependent death causing it to plateau out at a maximum population size $N_{max}$. If we use $N_t$ to denote the population size at time $t$ then we can write this model as follows: + +$$ +\begin{align} + N_{t+1} = rN_t(1 - N_t / K), \hspace{10mm} \mbox{where } K = N_{\mbox{max}}\left(\frac{r}{r-1}\right). +\end{align} +$$ + +There are three parameters in this model, and so our parameters dataframe should contain the three variables with suitable ranges as follows: + +```{r} +# define parameters dataframe +df_params <- define_params(name = "N_0", min = 1, max = 100, + name = "r", min = 1, max = 2, + name = "N_max", min = 50, max = 200) +``` + +Within our likelihood function we need to calculate our expected population growth curve. This curve is perfectly smooth, but the real data is noisy so we will use a Poisson distribution to link the data to the model. + +Crucially, when defining the likelihood function we will create an option to return the model prediction, rather than the log-likelihood. We use the `misc` input to the function to control whether this option is active or not. If we set `misc$output_type = 1` then we will obtain the log-likelihood, but if we set `misc$output_type = 2` then we will obtain the complete curve of model-predicted values: + +```{r} +# define log-likelihood function +loglike <- function(params, data, misc) { + + # extract parameter values + N_0 <- params["N_0"] + r <- params["r"] + N_max <- params["N_max"] + + # calculate expected population growth + K <- N_max * r / (r - 1) + x <- rep(0, 100) + x[1] <- N_0 + for (i in 2:length(x)) { + x[i] <- r * x[i-1] * (1 - x[i-1] / K) + } + + # option to return model prediction rather than log-likelihood + if (misc$output_type == 2) { + return(x) + } + + # calculate log-likelihood + ret <- sum(dpois(data$pop, lambda = x[data$time], log = TRUE)) + + # return + return(ret) +} + +# define R logprior function +logprior <- function(params, misc) { + dunif(params["N_0"], 1, 100, log = TRUE) + + dunif(params["r"], 1, 2, log = TRUE) + + dunif(params["N_max"], 50, 200, log = TRUE) +} +``` + +When running the MCMC we want to make sure `misc$output_type = 1`: + +```{r} +# run MCMC +mcmc <- run_mcmc(data = population_data, + df_params = df_params, + misc = list(output_type = 1), + loglike = loglike, + logprior = logprior, + burnin = 1e3, + samples = 1e4, + chains = 3, + pb_markdown = TRUE) +``` + +Once we have MCMC output we should perform our usual checks to make sure that everything has converged nicely (we will skip this step here in the interest of time). Assuming everything is OK, we can move on to exploring model fits. We will do this by running the same likelihood function but with `misc$output_type = 2`, using the posterior parameter draws as inputs. We may not want to use every posterior parameter draw, in which case we can use the `sample_chains()` function to sub-sample the output down as needed: + +```{r} +# sample from posterior +param_draws <- sample_chains(mcmc, 1000) + +# get matrix of model predictions +model_matrix <- apply(param_draws, 1, function(x) { + loglike(params = x, data = population_data, misc = list(output_type = 2)) +}) +``` + +Plotting these against the data we can see that - reassuringly - the posterior model fits make a smooth curve through the data points: + +```{r} +matplot(model_matrix, type = 'l', lty = 1, col = "#99000010", ylim = c(0, 120), + xlab = "Time (days)", ylab = "Population size") +points(population_data$time, population_data$pop, pch = 20) +``` + +Notice that we only had to define the model fitting computation once in this pipeline, as the same likelihood function was used for both inference and obtaining the fit. There are more complicated versions of this approach that may be useful in some settings, for example using `switch` functions or multiple `if` statements to allow for several different types of output from the likelihood function, or even varying aspects of the core model such as the error distribution using flags contained in `misc`. diff --git a/vignettes/installation.Rmd b/vignettes/installation.Rmd index ba658bd..ed26420 100644 --- a/vignettes/installation.Rmd +++ b/vignettes/installation.Rmd @@ -1,63 +1,77 @@ ---- -title: "Installing drjacoby" -author: "Bob Verity and Pete Winskill" -date: "`r Sys.Date()`" -output: rmarkdown::html_vignette -vignette: > - %\VignetteIndexEntry{Installing drjacoby} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - -```{r setup, include = FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -``` - -## Installing Rcpp - -*drjacoby* relies on the [Rcpp](https://cran.r-project.org/web/packages/Rcpp/index.html) package, which requires the following OS-specific steps: - -* Windows - - Download and install the appropriate version of [Rtools](https://cran.rstudio.com/bin/windows/Rtools/) for your version of R. On installation, ensure you check the box to arrange your system PATH as recommended by Rtools -* Mac OS X - - Download and install [XCode](http://itunes.apple.com/us/app/xcode/id497799835?mt=12) - - Within XCode go to Preferences : Downloads and install the Command Line Tools -* Linux (Debian/Ubuntu) - - Install the core software development utilities required for R package development as well as LaTeX by executing - - ```{} - sudo apt-get install r-base-dev texlive-full - ``` - -## Installing and loading *drjacoby* - -Next, in R, ensure that you have the [devtools](https://www.rstudio.com/products/rpackages/devtools/) package installed by running - -```{r, eval = FALSE} -install.packages("devtools", repos='http://cran.us.r-project.org') -``` - -Then install the *drjacoby* package directly from GitHub by running - -```{r, eval = FALSE} -devtools::install_github("mrc-ide/drjacoby") -``` - -If you have any problems installing then please [raise an issue](https://github.com/mrc-ide/drjacoby/issues) on github. - -Assuming everything installed correctly, we need to load the package: - -```{r} -library(drjacoby) -``` - -You can test that the package is loaded and working by running the following command, which should produce this output: - -```{r} -check_drjacoby_loaded() -``` - - +--- +title: "Installing drjacoby" +author: "Bob Verity and Pete Winskill" +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Installing drjacoby} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r setup, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## Installing Rcpp + +*drjacoby* relies on the [Rcpp](https://cran.r-project.org/web/packages/Rcpp/index.html) package, which requires the following OS-specific steps: + +* Windows + - Download and install the appropriate version of [Rtools](https://cran.rstudio.com/bin/windows/Rtools/) for your version of R. On installation, ensure you check the box to arrange your system PATH as recommended by Rtools +* Mac OS X + - Download and install [XCode](http://itunes.apple.com/us/app/xcode/id497799835?mt=12) + - Within XCode go to Preferences : Downloads and install the Command Line Tools +* Linux (Debian/Ubuntu) + - Install g++ with + + ```{} + sudo apt-get update + sudo apt-get install g++ + ``` + +Irrespective of which system you use above, you should then install and load Rcpp with + +```{r, eval = FALSE} +install.packages("Rcpp") +library(Rcpp) +``` + +You can check the version number to make sure it has properly installed +```{r, eval = FALSE} +packageVersion("Rcpp") +``` + + +## Installing and loading *drjacoby* + +Next, in R, ensure that you have the [devtools](https://www.rstudio.com/products/rpackages/devtools/) package installed by running + +```{r, eval = FALSE} +install.packages("devtools", repos='http://cran.us.r-project.org') +``` + +Then install the *drjacoby* package directly from GitHub by running + +```{r, eval = FALSE} +devtools::install_github("mrc-ide/drjacoby") +``` + +If you have any problems installing then please [raise an issue](https://github.com/mrc-ide/drjacoby/issues) on github. + +Assuming everything installed correctly, we need to load the package: + +```{r} +library(drjacoby) +``` + +You can test that the package is loaded and working by running the following command, which should produce this output: + +```{r} +check_drjacoby_loaded() +``` + + diff --git a/vignettes/metropolis_coupling.Rmd b/vignettes/metropolis_coupling.Rmd index 280fcfd..9ca5cea 100644 --- a/vignettes/metropolis_coupling.Rmd +++ b/vignettes/metropolis_coupling.Rmd @@ -1,199 +1,199 @@ ---- -title: "Parallel Tempering" -author: "Bob Verity and Pete Winskill" -date: "`r Sys.Date()`" -output: rmarkdown::html_vignette -vignette: > - %\VignetteIndexEntry{Parallel Tempering} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - -```{r setup, include = FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -``` - -```{r, echo = FALSE} -# set random seed -set.seed(1) - -# load the drjacoby package -library(drjacoby) - -# load other packages -library(ggplot2) -library(gridExtra) -``` - -MCMC becomes considerably harder when the posterior distribution is 1) highly correlated, and/or 2) highly multimodal. For exampe, if your posterior has Twin Peaks then ordinary Metropolis-Hastings might not be enough. Parallel tempering tends to mitigate these problems and requires nothing more than some extra heated chains. - -This vignette demonstrates how parallel tempering can be implemented within *drjacoby* to solve a deliberately awkward MCMC problem. - - -## Setup - -For this example we will start by writing the likelihood and prior functions in C++. If this sounds unfamiliar to you, check out the [earlier vignette](https://mrc-ide.github.io/drjacoby/articles/example.html) for a simple example. Our basic model will assume that our data are normally distributed with mean `alpha^2*beta`. The `alpha^2` term here means that both positive and negative values of `alpha` map to the same value, thereby creating a multimodal distribution, and the `*beta` term ensures that `alpha` and `beta` are highly correlated. We will also use a third parameter `epsilon` to represent some random noise that we want to integrate over. While this is a highly contrived example, it does have the advantage of being horrendously awkward! - -For our prior we assume that `alpha` is uniform [-10,10], `beta` is uniform [0,10], and `epsilon` is normally distributed with mean 0 and standard deviation 1. - -```{r, echo = FALSE, comment = '', warning=FALSE} -cpp_funcs <- system.file("extdata/", "coupling_loglike_logprior.cpp", package = 'drjacoby', mustWork = TRUE) -Rcpp::sourceCpp(cpp_funcs) -cat(readLines(cpp_funcs), sep = '\n') -``` - -As always, we need to define a dataframe of parameters so that *drjacoby* knows what parameter ranges to expect: - -```{r} -# define parameters dataframe -df_params <- define_params(name = "alpha", min = -10, max = 10, init = 5, - name = "beta", min = 0, max = 10, init = 5, - name = "epsilon", min = -Inf, max = Inf, init = 0) -``` - -Finally, we need some data. For simplicity we will use a series of values drawn from a normal distribution with mean 10. These are obviously not drawn from the true model, but will have the advantage of creating a horribly awkward posterior. - -```{r} -# draw example data -data_list <- list(x = rnorm(100, mean = 10)) -``` - - -## Running the MCMC - -First, we will try running the basic MCMC without parallel tempering. The following block of code repeats the same MCMC analysis nine times, each time producing a plot of posterior `alpha` against `beta`: - -```{r, fig.width=8, fig.height=7} -plot_list <- list() -for (i in seq_len(9)) { - - # run MCMC - mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e4, - chains = 1, - silent = TRUE) - - # create plot of alpha against beta - plot_list[[i]] <- plot_cor(mcmc, "alpha", "beta") + - ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") -} - -# plot grid -gridExtra::grid.arrange(grobs = plot_list) -``` - -Clearly this MCMC is not mixing well! By looking over all nine plots we can get a rough idea of what the distribution *should* look like, but no single MCMC run has captured it adequately. You can experiment with increasing the number of samples - you should get better results for more samples, but a *very* large number of samples are needed before we get good mixing between the left and right sides of the distribution. - -Parallel tempering is perfectly designed to solve this kind of problem. To activate parallel tempering we need to specify an additional argument to the `run_mcmc()` function - the number of `rungs`. Each rung represents a different MCMC chain arranged in a "temperature ladder", with the hotter chains being more free to explore the parameter space. Every iteration, chains have the option of swapping parameter values between rungs, thereby allowing the freely-moving hot chains to propose values to the more constrained colder chains. When looking at the output, we tend to focus exclusively on the coldest chain which can be interpreted the same way as a single chain from regular MCMC. - -In this example we use 20 rungs: - -```{r, fig.width=5, fig.height=4} -# run MCMC -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e4, - rungs = 20, - chains = 1, - pb_markdown = TRUE) - -# create plot of alpha against beta -plot_cor(mcmc, "alpha", "beta") + - ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") -``` - -You should see a much better characterisation of the posterior distribution. The run time scales approximately linearly with the number of rungs, so there is a computational cost to using this method, but on the other hand our results are far better than we would obtain by simply increasing the number of samples by a factor of 20. - - -## How many rungs to use? - -Parallel tempering in *drjacoby* works by proposing swaps between adjacent rungs in the temperature ladder, which are accepted or rejected according to the standard Metropolis-Hastings ratio. If, for example, a hot rung happens upon a set of parameter values that have high likelihood then there is a good chance these values will be swapped up to the next rung. In this way, information can effectively bubble-sort its way from the prior (the hottest chain) to the posterior (the coldest chain). The average chance of a swap being accepted depends on the two adjacent rungs being similar enough in distribution that values drawn from one have a high likelihood in the other. This, in turn, means that swaps have a higher chance of being accepted when we have a large number of densely packed rungs. - -To explore this, we start by re-running the above MCMC with a small number of rungs and a small number of samples to highlight the problem. We can use the `plot_mc_acceptance()` function to plot the acceptance rate between rungs: - -```{r, fig.width=5, fig.height=4} -# run MCMC -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e3, - rungs = 10, - chains = 1, - silent = TRUE) - -# plot coupling acceptance rates -plot_mc_acceptance(mcmc, x_axis_type = 2) -``` - -The 10 vertical grey bars in the above plot show the positions of our 10 rungs in the thermodynamic interval [0,1], and the red dots between them show the proportion of swaps that were accepted between rungs (this information is available via `mcmc$diagnostics$mc_accept`). Notice that the acceptance rate between the hottest and second-hottest rung is close to zero, meaning very little of the information in the hottest rung made it up the ladder to the colder rungs. We can see the problems this causes when we plot the posterior draws: - -```{r, fig.width=5, fig.height=4} -plot_cor(mcmc, "alpha", "beta") + - ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") -``` - -Although we have some mixing between the left and right sides of the distribution, overall sampling is still very patchy. - -One way of improving mixing between rungs is simply to increase the number of rungs so the distance between distributions is smaller: - -```{r, fig.width=5, fig.height=4} -# run MCMC -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e3, - rungs = 50, - chains = 1, - silent = TRUE) - -# plot coupling acceptance rates -plot_mc_acceptance(mcmc, x_axis_type = 2) - -# create plot of alpha against beta -plot_cor(mcmc, "alpha", "beta") + - ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") -``` - -This mitigates the problem to some extent, as we now have acceptance rates going "deeper" towards the prior, however the hottest rung is still not passing information up the ladder. This strategy also comes at a high computational cost, as we are effectively running the MCMC algorithm 50 times. A more efficient method is usually to change the distribution of rungs so they are more concentrated at low thermodynamic powers (i.e. near the prior end of the spectrum). This can be achieved through the parameter `alpha`, which represents a power that the temperature ladder is raised to. Higher values of `alpha` therefore lead to rungs being more concentrated at the prior end of the spectrum. The previous two MCMCs had `alpha` set to 1.0 by default, leading to evenly distributed rungs between [0,1]. Now we repeat the MCMC with just 20 rungs and `alpha = 5.0`: - -```{r, fig.width=5, fig.height=4} -# run MCMC -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e3, - rungs = 20, - chains = 1, - alpha = 5.0, - silent = TRUE) - -# plot coupling acceptance rates -# we could also use the option x_axis_type = 1 if we wanted to see these -# points spread out evenly, rather than at their true position in the -# thermodynamic interval -plot_mc_acceptance(mcmc, x_axis_type = 2) - -# create plot of alpha against beta -plot_cor(mcmc, parameter1 = "alpha", "beta") + - ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") -``` - -We find that with just 20 rungs we now have decent acceptance rates all the way through the temperature ladder. If we really wanted to fine-tune the mixing we could use the `beta_manual` argument inside `run_mcmc()` to define the temperature ladder exactly as a vector of values between 0 and 1. Note, this ladder is always raised to the power `alpha`, so we should set `alpha=1.0` if we don't want any additional transformation applied. - -In summary, the correct number of rungs to use in parallel tempered MCMC is whatever number provides decent acceptance rates all the way through our temperature ladder. When this is the case we can be condifent that the prior is "talking to" the posterior, making it *extremely unlikely* (but not impossible) that the MCMC is still missing large parts of the posterior distribution. +--- +title: "Parallel Tempering" +author: "Bob Verity and Pete Winskill" +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Parallel Tempering} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r setup, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r, echo = FALSE} +# set random seed +set.seed(1) + +# load the drjacoby package +library(drjacoby) + +# load other packages +library(ggplot2) +library(gridExtra) +``` + +MCMC becomes considerably harder when the posterior distribution is 1) highly correlated, and/or 2) highly multimodal. For exampe, if your posterior has Twin Peaks then ordinary Metropolis-Hastings might not be enough. Parallel tempering tends to mitigate these problems and requires nothing more than some extra heated chains. + +This vignette demonstrates how parallel tempering can be implemented within *drjacoby* to solve a deliberately awkward MCMC problem. + + +## Setup + +For this example we will start by writing the likelihood and prior functions in C++. If this sounds unfamiliar to you, check out the [earlier vignette](https://mrc-ide.github.io/drjacoby/articles/example.html) for a simple example. Our basic model will assume that our data are normally distributed with mean `alpha^2*beta`. The `alpha^2` term here means that both positive and negative values of `alpha` map to the same value, thereby creating a multimodal distribution. The `*beta` term ensures that `alpha` and `beta` are highly correlated. We will also use a third parameter `epsilon` to represent some random noise that we want to integrate over. While this is a highly contrived example, it does have the advantage of being horrendously awkward! + +For our prior we assume that `alpha` is uniform [-10,10], `beta` is uniform [0,10], and `epsilon` is normally distributed with mean 0 and standard deviation 1. + +```{r, echo = FALSE, comment = '', warning=FALSE} +cpp_funcs <- system.file("extdata/", "coupling_loglike_logprior.cpp", package = 'drjacoby', mustWork = TRUE) +Rcpp::sourceCpp(cpp_funcs) +cat(readLines(cpp_funcs), sep = '\n') +``` + +As always, we need to define a dataframe of parameters so that *drjacoby* knows what parameter ranges to expect: + +```{r} +# define parameters dataframe +df_params <- define_params(name = "alpha", min = -10, max = 10, init = 5, + name = "beta", min = 0, max = 10, init = 5, + name = "epsilon", min = -Inf, max = Inf, init = 0) +``` + +Finally, we need some data. For simplicity we will use a series of values drawn from a normal distribution with mean 10. These are obviously not drawn from the true model, but will have the advantage of creating a horribly awkward posterior. + +```{r} +# draw example data +data_list <- list(x = rnorm(100, mean = 10)) +``` + + +## Running the MCMC + +First, we will try running the basic MCMC without parallel tempering. The following block of code repeats the same MCMC analysis nine times, each time producing a plot of posterior `alpha` against `beta`: + +```{r, fig.width=8, fig.height=7} +plot_list <- list() +for (i in seq_len(9)) { + + # run MCMC + mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e4, + chains = 1, + silent = TRUE) + + # create plot of alpha against beta + plot_list[[i]] <- plot_scatter(mcmc, "alpha", "beta") + + ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") +} + +# plot grid +gridExtra::grid.arrange(grobs = plot_list) +``` + +Clearly this MCMC is not mixing well! By looking over all nine plots we can get a rough idea of what the distribution *should* look like, but no single MCMC run has captured it adequately. You can experiment with increasing the number of samples - you should get better results for more samples, but a *very* large number of samples are needed before we get good mixing between the left and right sides of the distribution. + +Parallel tempering is perfectly designed to solve this kind of problem. To activate parallel tempering we need to specify an additional argument to the `run_mcmc()` function - the number of `rungs`. Each rung represents a different MCMC chain arranged in a "temperature ladder", with the hotter chains being more free to explore the parameter space. Every iteration, chains have the option of swapping parameter values between rungs, thereby allowing the freely-moving hot chains to propose values to the more constrained colder chains. When looking at the output, we tend to focus exclusively on the coldest chain which can be interpreted the same way as a single chain from regular MCMC. + +In this example we use 20 rungs: + +```{r, fig.width=5, fig.height=4} +# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e4, + rungs = 20, + chains = 1, + pb_markdown = TRUE) + +# create plot of alpha against beta +plot_scatter(mcmc, "alpha", "beta") + + ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") +``` + +You should see a much better characterisation of the posterior distribution. The run time scales approximately linearly with the number of rungs, so there is a computational cost to using this method, but on the other hand our results are far better than we would obtain by simply increasing the number of samples by a factor of 20. + + +## How many rungs to use? + +Parallel tempering in *drjacoby* works by proposing swaps between adjacent rungs in the temperature ladder, which are accepted or rejected according to the standard Metropolis-Hastings ratio. If, for example, a hot rung happens upon a set of parameter values that have high likelihood then there is a good chance these values will be swapped up to the next rung. In this way, information can effectively bubble-sort its way from the prior (the hottest chain) to the posterior (the coldest chain). The average chance of a swap being accepted depends on the two adjacent rungs being similar enough in distribution that values drawn from one have a high likelihood in the other. This, in turn, means that swaps have a higher chance of being accepted when we have a large number of densely packed rungs. + +To explore this, we start by re-running the above MCMC with a small number of rungs and a small number of samples to highlight the problem. We can use the `plot_mc_acceptance()` function to plot the acceptance rate between rungs: + +```{r, fig.width=5, fig.height=4} +# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + rungs = 10, + chains = 1, + silent = TRUE) + +# plot coupling acceptance rates +plot_mc_acceptance(mcmc, x_axis_type = 2) +``` + +The 10 vertical grey bars in the above plot show the positions of our 10 rungs in the thermodynamic interval [0,1], and the red dots between them show the proportion of swaps that were accepted between rungs (this information is available via `mcmc$diagnostics$mc_accept`). Notice that the acceptance rate between the hottest and second-hottest rung is close to zero, meaning very little of the information in the hottest rung made it up the ladder to the colder rungs. We can see the problems this causes when we plot the posterior draws: + +```{r, fig.width=5, fig.height=4} +plot_scatter(mcmc, "alpha", "beta") + + ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") +``` + +Although we have some mixing between the left and right sides of the distribution, overall sampling is still very patchy. + +One way of improving mixing between rungs is simply to increase the number of rungs so the distance between distributions is smaller: + +```{r, fig.width=5, fig.height=4} +# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + rungs = 50, + chains = 1, + silent = TRUE) + +# plot coupling acceptance rates +plot_mc_acceptance(mcmc, x_axis_type = 2) + +# create plot of alpha against beta +plot_scatter(mcmc, "alpha", "beta") + + ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") +``` + +This has helped, as we now have acceptance rates going "deeper" towards the prior, however the hottest rung is still not passing information up the ladder. This strategy also comes at a high computational cost, as we are effectively running the MCMC algorithm 50 times. A more efficient method is usually to change the distribution of rungs so they are more concentrated at low thermodynamic powers (i.e. near the prior end of the spectrum). This can be achieved through the parameter `alpha`, which represents a power that the temperature ladder is raised to. Higher values of `alpha` therefore lead to rungs being more concentrated at the prior end of the spectrum. The previous two MCMCs had `alpha` set to 1.0 by default, leading to evenly distributed rungs between [0,1]. Now we repeat the MCMC with just 20 rungs and `alpha = 5.0`: + +```{r, fig.width=5, fig.height=4} +# run MCMC +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + rungs = 20, + chains = 1, + alpha = 5.0, + silent = TRUE) + +# plot coupling acceptance rates +# we could also use the option x_axis_type = 1 if we wanted to see these +# points spread out evenly, rather than at their true position in the +# thermodynamic interval +plot_mc_acceptance(mcmc, x_axis_type = 2) + +# create plot of alpha against beta +plot_scatter(mcmc, parameter1 = "alpha", "beta") + + ggplot2::ylim(0, 10) + ggplot2::xlim(-10, 10) + ggplot2::theme(legend.position = "none") +``` + +We find that with just 20 rungs we now have decent acceptance rates all the way through the temperature ladder. If we really wanted to fine-tune the mixing we could use the `beta_manual` argument inside `run_mcmc()` to define the temperature ladder exactly as a vector of values between 0 and 1. Note, this ladder is always raised to the power `alpha`, so we should set `alpha=1.0` if we don't want any additional transformation applied. + +In summary, the correct number of rungs to use in parallel tempered MCMC is whatever number provides decent acceptance rates all the way through our temperature ladder. When this is the case we can be condifent that the prior is "talking to" the posterior, making it *extremely unlikely* (but not impossible) that the MCMC is still missing large parts of the posterior distribution. diff --git a/vignettes/parallel.Rmd b/vignettes/parallel.Rmd index 1a437f0..d94246b 100644 --- a/vignettes/parallel.Rmd +++ b/vignettes/parallel.Rmd @@ -1,146 +1,146 @@ ---- -title: "Running in Parallel" -author: "Bob Verity and Pete Winskill" -date: "`r Sys.Date()`" -output: rmarkdown::html_vignette -vignette: > - %\VignetteIndexEntry{Running in Parallel} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - -```{r setup, include = FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -``` - -```{r, echo = FALSE} -# set random seed -set.seed(1) - -# load the drjacoby package -library(drjacoby) -``` - -Running multiple chains is a good way of checking that our MCMC is working well. Each chain is completely independent of all others, and so this qualifies as an [embarrassingly parallel](https://en.wikipedia.org/wiki/Embarrassingly_parallel) problem. - -This vignette will demonstrate how to run *drjacoby* with multiple chains, first in serial and then in parallel over multiple cores. - -## Setup - -As always, we require some data, some parameters, and some functions to work with (see [earlier examples](https://mrc-ide.github.io/drjacoby/articles/example.html)). The underlying model is not our focus here, so we will use a very basic setup - - -```{r} -# define data -data_list <- list(x = rnorm(10)) - -# define parameters dataframe -df_params <- data.frame(name = "mu", min = -10, max = 10, init = 0) - -# define loglike function -r_loglike <- function(params, data, misc) { - sum(dnorm(data$x, mean = params["mu"], sd = 1.0, log = TRUE)) -} - -# define logprior function -r_logprior <- function(params, misc) { - dunif(params["mu"], min = -10, max = 10, log = TRUE) -} -``` - -## Running multiple chains - -Whenever the input argument `cluster` is `NULL`, chains will run in serial. This is true by default, so running multiple chains in serial is simply a case of specifying the `chains` argument: - -```{r} -# run MCMC in serial -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = r_loglike, - logprior = r_logprior, - burnin = 1e3, - samples = 1e3, - chains = 2, - pb_markdown = TRUE) -``` - -When we look at our MCMC output (using the `plot_par()` function) we can see that there are 2 chains, each of which contains a series of draws from the posterior. If we used multiple [temperature rungs](https://mrc-ide.github.io/drjacoby/articles/metropolis_coupling.html) then these would also be duplicated over chains. - -```{r, fig.width=10, fig.height=4} -# summarise output -mcmc - -# compare mu over both chains -plot_par(mcmc, "mu", phase = "both") -``` - -Running in parallel is only slightly more complex. Before running anything we need to know how many cores our machine has. You may know this number already, but if you don't then the `parallel` package has a handy function for detecting the number of cores for you: - -```{r, eval = FALSE} -cores <- parallel::detectCores() -``` - -Next we make a cluster object, which creates multiple copies of R running in parallel over different cores. Here we are using all available cores, but if you want to hold some back for other intensive tasks then simply use a smaller number of cores when specifying this cluster. - -```{r, eval = FALSE} -cl <- parallel::makeCluster(cores) -``` - -We then run the usual `run_mcmc()` function, this time passing in the cluster object as an argument. This causes *drjacoby* to use a `clusterApplyLB()` call rather than an ordinary `lapply()` call over different chains. Each chain is added to a queue over the specified number of cores - when the first job completes, the next job is placed on the node that has become available and this continues until all jobs are complete. - -Note that output is supressed when running in parallel to avoid sending print commands to multiple cores, so you will not see the usual progress bars. - -```{r, eval = FALSE} -# run MCMC in parallel -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = r_loglike, - logprior = r_logprior, - burnin = 1e3, - samples = 1e3, - chains = 2, - cluster = cl, - pb_markdown = TRUE) -``` - -Finally, it is good practice to shut down the workers once we are finished: - -```{r, eval = FALSE} -parallel::stopCluster(cl) -``` - - -## Running multiple chains using C++ log likelihood or log prior functions - -To run the MCMC in parallel with C++ log-likelihood or log-prior functions there is on additional step to take when setting up -the cluster. Each node must be able to access a version of the compiled C++ functions, so after initialising the cluster with: - -```{r, eval = FALSE} -cl <- parallel::makeCluster(cores) -``` - -we must also make the C++ functions accessible, by running the Rcpp::sourceCPP command for our C++ file for each node: - -```{r, eval = FALSE} -cl_cpp <- parallel::clusterEvalQ(cl, Rcpp::sourceCpp("my_cpp.cpp")) -``` - -After which we can run the mcmc in the same way: - -```{r, eval = FALSE} -# run MCMC in parallel -mcmc <- run_mcmc(data = data_list, - df_params = df_params, - loglike = "loglike", - logprior = "logprior", - burnin = 1e3, - samples = 1e3, - chains = 2, - cluster = cl, - pb_markdown = TRUE) -parallel::stopCluster(cl) -``` - +--- +title: "Running in Parallel" +author: "Bob Verity and Pete Winskill" +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Running in Parallel} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r setup, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r, echo = FALSE} +# set random seed +set.seed(1) + +# load the drjacoby package +library(drjacoby) +``` + +Running multiple chains is a good way of checking that our MCMC is working well. Each chain is completely independent of all others, and so this qualifies as an [embarrassingly parallel](https://en.wikipedia.org/wiki/Embarrassingly_parallel) problem. + +This vignette will demonstrate how to run *drjacoby* with multiple chains, first in serial and then in parallel over multiple cores. + +## Setup + +As always, we require some data, some parameters, and some functions to work with (see [earlier examples](https://mrc-ide.github.io/drjacoby/articles/example.html)). The underlying model is not our focus here, so we will use a very basic setup + + +```{r} +# define data +data_list <- list(x = rnorm(10)) + +# define parameters dataframe +df_params <- data.frame(name = "mu", min = -10, max = 10, init = 0) + +# define loglike function +r_loglike <- function(params, data, misc) { + sum(dnorm(data$x, mean = params["mu"], sd = 1.0, log = TRUE)) +} + +# define logprior function +r_logprior <- function(params, misc) { + dunif(params["mu"], min = -10, max = 10, log = TRUE) +} +``` + +## Running multiple chains + +Whenever the input argument `cluster` is `NULL`, chains will run in serial. This is true by default, so running multiple chains in serial is simply a case of specifying the `chains` argument: + +```{r} +# run MCMC in serial +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e3, + samples = 1e3, + chains = 2, + pb_markdown = TRUE) +``` + +When we look at our MCMC output (using the `plot_trace()` function) we can see that there are 2 chains, each of which contains a series of draws from the posterior. If we used multiple [temperature rungs](https://mrc-ide.github.io/drjacoby/articles/metropolis_coupling.html) then these would also be duplicated over chains. + +```{r, fig.width=10, fig.height=4} +# summarise output +mcmc + +# compare mu over both chains +plot_trace(mcmc, "mu", phase = "both") +``` + +Running in parallel is only slightly more complex. Before running anything we need to know how many cores our machine has. You may know this number already, but if you don't then the `parallel` package has a handy function for detecting the number of cores for you: + +```{r, eval = FALSE} +cores <- parallel::detectCores() +``` + +Next we make a cluster object, which creates multiple copies of R running in parallel over different cores. Here we are using all available cores, but if you want to hold some back for other intensive tasks then simply use a smaller number of cores when specifying this cluster. + +```{r, eval = FALSE} +cl <- parallel::makeCluster(cores) +``` + +We then run the usual `run_mcmc()` function, this time passing in the cluster object as an argument. This causes *drjacoby* to use a `clusterApplyLB()` call rather than an ordinary `lapply()` call over different chains. Each chain is added to a queue over the specified number of cores - when the first job completes, the next job is placed on the node that has become available and this continues until all jobs are complete. + +Note that output is supressed when running in parallel to avoid sending print commands to multiple cores, so you will not see the usual progress bars. + +```{r, eval = FALSE} +# run MCMC in parallel +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = r_loglike, + logprior = r_logprior, + burnin = 1e3, + samples = 1e3, + chains = 2, + cluster = cl, + pb_markdown = TRUE) +``` + +Finally, it is good practice to shut down the workers once we are finished: + +```{r, eval = FALSE} +parallel::stopCluster(cl) +``` + + +## Running multiple chains using C++ log likelihood or log prior functions + +To run the MCMC in parallel with C++ log-likelihood or log-prior functions there is on additional step to take when setting up +the cluster. Each node must be able to access a version of the compiled C++ functions, so after initialising the cluster with: + +```{r, eval = FALSE} +cl <- parallel::makeCluster(cores) +``` + +we must also make the C++ functions accessible, by running the Rcpp::sourceCPP command for our C++ file for each node: + +```{r, eval = FALSE} +cl_cpp <- parallel::clusterEvalQ(cl, Rcpp::sourceCpp("my_cpp.cpp")) +``` + +After which we can run the mcmc in the same way: + +```{r, eval = FALSE} +# run MCMC in parallel +mcmc <- run_mcmc(data = data_list, + df_params = df_params, + loglike = "loglike", + logprior = "logprior", + burnin = 1e3, + samples = 1e3, + chains = 2, + cluster = cl, + pb_markdown = TRUE) +parallel::stopCluster(cl) +``` +