MBNMAtime for time-course Model-Based (Network) Meta-Analysis

Hugo Pedder



This vignette demonstrates how to use MBNMAtime to perform meta-analysis of studies with multiple follow-up measurements in order to account for time-course relationships within single or multiple treatment comparisons. This can be performed by conducting Model-Based (Network) Meta-Analysis (MBNMA) to pool relative treatment effects.

Including all available follow-up measurements within a study makes use of all the available evidence in a way that maintains connectivity between treatments and explains how the response of the treatment changes over time, thus accounting for heterogeneity and inconsistency that may be present from “lumping” together different time points in a standard Network Meta-Analysis (NMA). All models and analyses are implemented in a Bayesian framework, following an extension of the standard NMA methodology presented by (Lu and Ades 2004) and are run in JAGS (version 4.3.0 or later is required) (JAGS Computer Program 2017). For full details of time-course MBNMA methodology see Pedder et al. (2019), and a simulation study exploring the statistical properties of the method is reported in Pedder et al. (2020).

This package has been developed alongside MBNMAdose, a package that allows users to perform dose-response MBNMA to allow for modelling of dose-response relationships between different agents within a network. However, they should not be loaded into R at the same time as there are a number of functions with shared names that perform similar tasks yet are specific to dealing with either time-course or dose-response data.

Within the vignette, some models have not been evaluated, or have been run with fewer iterations than would be necessary to achieve convergence and produce valid results in practice. This has been done to speed up computation and rendering of the vignette.

Workflow within the package

Functions within MBNMAtime follow a clear pattern of use:

  1. Load your data into the correct format using mb.network()
  2. Specify a suitable time-course function and analyse your data using mb.run()
  3. Test for consistency using functions like mb.nodesplit()
  4. Examine model results using forest plots and treatment rankings
  5. Use your model to predict responses using predict()

At each of these stages there are a number of informative graphs that can be generated to help understand the data and make decisions regarding model fitting.

Datasets Included in the Package

Pain relief in osteoarthritis

osteopain is from a systematic review of treatments for pain in osteoarthritis, used previously in Pedder et al. (2019). The outcome is pain measured on a continuous scale, and aggregate data responses correspond to the mean WOMAC pain score at different follow-up times. The dataset includes 30 Randomised-Controlled Trials (RCTs), comparing 29 different treatments (including placebo). osteopain is a data frame in long format (one row per time point, arm and study), with the variables studyID, time, y, se, treatment and arm.

studyID time y se treatment arm treatname
Baerwald 2010 0 6.55 0.09 Pl_0 1 Placebo_0
Baerwald 2010 2 5.40 0.09 Pl_0 1 Placebo_0
Baerwald 2010 6 4.97 0.10 Pl_0 1 Placebo_0
Baerwald 2010 13 4.75 0.11 Pl_0 1 Placebo_0
Baerwald 2010 0 6.40 0.13 Na_1000 2 Naproxen_1000
Baerwald 2010 2 4.03 0.13 Na_1000 2 Naproxen_1000

Alogliptin for lowering blood glucose concentration in type II diabetes

alog_pcfb is from a systematic review of Randomised-Controlled Trials (RCTs) comparing different doses of alogliptin with placebo (Langford et al. 2016). The systematic review was simply performed and was intended to provide data to illustrate a statistical methodology rather than for clinical inference. Alogliptin is a treatment aimed at reducing blood glucose concentration in type II diabetes. The outcome is continuous, and aggregate data responses correspond to the mean change in HbA1c from baseline to follow-up in studies of at least 12 weeks follow-up. The dataset includes 14 Randomised-Controlled Trials (RCTs), comparing 5 different doses of alogliptin with placebo (6 different treatments in total). alog_pcfb is a data frame in long format (one row per time point, arm and study), with the variables studyID, clinicaltrialGov_ID, agent, dose, treatment, time, y, se, and N.

studyID clinicaltrialGov_ID agent dose treatment time y se N
1 NCT01263470 alogliptin 0.00 placebo 2 0.00 0.02 75
1 NCT01263470 alogliptin 6.25 alog_6.25 2 -0.16 0.02 79
1 NCT01263470 alogliptin 12.50 alog_12.5 2 -0.17 0.02 84
1 NCT01263470 alogliptin 25.00 alog_25 2 -0.16 0.02 79
1 NCT01263470 alogliptin 50.00 alog_50 2 -0.15 0.02 79
1 NCT01263470 alogliptin 0.00 placebo 4 -0.01 0.04 75

Tiotropium, Aclidinium and Placebo for maintenance treatment of moderate to severe chronic obstructive pulmonary disease

A dataset from a systematic review of Randomised-Controlled Trials (RCTs) for maintenance treatment of moderate to severe chronic obstructive pulmonary disease (COPD) (Karabis et al. 2013). Data are extracted from (Tallarita, De lorio, and Baio 2019). SEs were imputed for three studies, and number of patients randomised were imputed for one study (LAS 39) in which they were missing, using the median standard deviation calculated from other studies in the dataset. The outcome is trough Forced Expiratory Volume in 1 second (FEV1), measured in litres and reported in each study arm as mean change from baseline to follow-up. The dataset includes 13 RCTs, comparing 2 treatments (Tiotropium and Aclidinium) and placebo. copd is a data frame in long format (one row per observation, arm and study), with the variables studyID, time, y, se, treatment, and n.

studyID time y se treatment n
ACCORD I 1 -0.01 0.01 Placebo 187
ACCORD I 4 -0.01 0.01 Placebo 187
ACCORD I 8 -0.01 0.01 Placebo 187
ACCORD I 12 -0.02 0.01 Placebo 187
ACCORD I 1 0.10 0.01 Aclidinium 190
ACCORD I 4 0.11 0.01 Aclidinium 190

Body weight reduction in obesity patients

obesityBW_CFB is from a systematic review of pharmacological treatments for obesity. The outcome measured is change from baseline in body weight (kg) at different follow-up times. 35 RCTs are included that investigate 26 different treatments (16 agents/agent combinations compared at different doses). obesityBW_CFB is a dataset in long format (one row per time point, arm and study), with the variables studyID, time, y, se, N, treatment, arm, treatname, agent and class.

class is the class of a particular agent (e.g. Lipase inhibitor)

studyID time y se N treatment treatname agent class
27 Apfelbaum 1999 4.35 -1.00 0.39 78 plac placebo placebo Placebo
28 Apfelbaum 1999 4.35 -1.59 0.38 81 sibu_10MG sibutramine 10MG sibutramine SNRI
29 Apfelbaum 1999 8.70 -1.59 0.40 78 plac placebo placebo Placebo
30 Apfelbaum 1999 8.70 -3.01 0.39 81 sibu_10MG sibutramine 10MG sibutramine SNRI
31 Apfelbaum 1999 13.04 -2.25 0.41 78 plac placebo placebo Placebo
32 Apfelbaum 1999 13.04 -4.76 0.40 81 sibu_10MG sibutramine 10MG sibutramine SNRI

Serum uric acid concentration in gout

goutSUA_CFB is from a systematic review of interventions for lowering Serum Uric Acid (SUA) concentration in patients with gout [not published previously]. The outcome is continuous, and aggregate data responses correspond to the mean change from baseline in SUA in mg/dL at different follow-up times. The dataset includes 28 RCTs, comparing 41 treatments (8 agents compared at different doses). goutSUA_CFB is a data frame in long format (one row per arm and study), with the variables studyID, time, y, se, treatment, arm, class and treatname.

studyID time y se treatment treatname class
1102 1 0.07 0.25 RDEA_100 RDEA594_100 RDEA
1102 1 0.02 0.18 RDEA_200 RDEA594_200 RDEA
1102 1 0.06 0.25 RDEA_400 RDEA594_400 RDEA
1102 2 -0.53 0.25 RDEA_100 RDEA594_100 RDEA
1102 2 -1.37 0.18 RDEA_200 RDEA594_200 RDEA
1102 2 -1.73 0.25 RDEA_400 RDEA594_400 RDEA

Inspecting the data

Before embarking on an analysis, the first step is to have a look at the raw data. Two features (network connectivity and time-course relationship) are particularly important for MBNMA. To investigate these we must first get our dataset into the right format for the package. We can do this using mb.network(). This requires specifying the desired treatment to use for the network reference treatment, though one will automatically be specified if not given.

# Using the pain dataset
network.pain <- mb.network(osteopain, reference = "Pl_0")
#> description :
#> [1] "Network"
#> data.ab :
#> # A tibble: 417 x 10
#> # Groups:   studyID, time [116]
#>    studyID    time     y     se treatment   arm treatname   fupcount  fups  narm
#>    <fct>     <dbl> <dbl>  <dbl>     <dbl> <int> <fct>          <int> <int> <int>
#>  1 Baerwald~     0  6.55 0.0861         1     1 Placebo_0          1     4     3
#>  2 Baerwald~     0  6.40 0.127         15     2 Naproxen_1~        1     4     3
#>  3 Baerwald~     0  6.62 0.0900        16     3 Naproxcino~        1     4     3
#>  4 Baerwald~     2  5.40 0.0932         1     1 Placebo_0          2     4     3
#>  5 Baerwald~     2  4.03 0.133         15     2 Naproxen_1~        2     4     3
#>  6 Baerwald~     2  4.43 0.0926        16     3 Naproxcino~        2     4     3
#>  7 Baerwald~     6  4.97 0.0997         1     1 Placebo_0          3     4     3
#>  8 Baerwald~     6  3.72 0.139         15     2 Naproxen_1~        3     4     3
#>  9 Baerwald~     6  4.08 0.0965        16     3 Naproxcino~        3     4     3
#> 10 Baerwald~    13  4.75 0.109          1     1 Placebo_0          4     4     3
#> # ... with 407 more rows
#> treatments :
#>  [1] "Pl_0"    "Ce_100"  "Ce_200"  "Ce_400"  "Du_90"   "Et_10"   "Et_30"  
#>  [8] "Et_5"    "Et_60"   "Et_90"   "Lu_100"  "Lu_200"  "Lu_400"  "Lu_NA"  
#> [15] "Na_1000" "Na_1500" "Na_250"  "Na_750"  "Ox_44"   "Ro_12"   "Ro_125" 
#> [22] "Ro_25"   "Tr_100"  "Tr_200"  "Tr_300"  "Tr_400"  "Va_10"   "Va_20"  
#> [29] "Va_5"

This takes a dataset with the columns:

Additional columns can be included in the dataset. These will simply be added to the mb.network object, though will not affect the classification of the data.

mb.network then performs the following checks on the data:

And finally it converts the data into an object of class("mb.network"), which contains indices for study arms and follow-up measurements, and generates numeric values for treatments and classes. By convention, treatments are numbered alphabetically, though if the original data for treatments is provided as a factor then the factor codes will be used. This then contains all the necessary information for subsequent MBNMAtime functions.

Network connectivity

Looking at how the evidence in the network is connected and identifying which studies compare which treatments helps to understand which treatment effects can be estimated and what information will be helping to inform those estimates. A network plot can be generated which shows which treatments have been compared in head-to-head trials. Typically the thickness of connecting lines (“edges”) is proportional to the number of studies that make a particular comparison and the size of treatment nodes (“vertices”) is proportional to the total number of patients in the network who were randomised to a given treatment (provided N is included as a variable in the original dataset for mb.network()).

In MBNMAtime these plots are generated using igraph, and can be plotted by calling plot(). The generated plots are objects of class("igraph") meaning that, in addition to the options specified in plot(), various igraph functions can be used to make more detailed edits to them.

# Prepare data using the alogliptin dataset
network.alog <- mb.network(alog_pcfb, reference = "placebo")

# Plot network

Within these network plots, treatments are automatically aligned in a circle (as the default) and can be tidied by shifting the label distance away from the nodes.

# Draw network plot in a star layout using the gout dataset
network.gout <- mb.network(goutSUA_CFB, reference = "Plac")
plot(network.gout, layout=igraph::as_star(), label.distance = 5)

This command returns a warning stating that some treatments are not connected to the network reference treatment through any pathway of head-to-head evidence. The nodes that are coloured white represent these treatments. This means that it will not be possible to estimate relative effects for these treatments versus the network reference treatment (or any treatments connected to it). Several options exist to allow for inclusion of these treatments in an analysis which we will discuss in more detail later, but one approach is to assume a shared effect among treatments within the same class/agent. We can generate a network plot at the class level to examine this more closely, and can see that the network is connected at the class level.

plot(network.gout, level = "class", remove.loops = TRUE, label.distance = 5)

It is also possible to plot a network at the treatment level but to colour the treatments by the class that they belong to.

plot(network.gout, level = "treatment", v.color = "class", label.distance = 5)

Examining the time-course relationship

In order to consider which functional forms may be appropriate for modelling the time-course relationship, it is important to look at the responses in each arm plotted over time. This can easily be done using the timeplot() function on an object of class("mb.network")

# Prepare data using the pain dataset
network.pain <- mb.network(osteopain, reference="Pl_0")

# Draw plot of raw study responses over time

As the mean response for all treatments shows a rapid reduction in pain score followed by a levelling out after 2-5 weeks, an exponential decay time-course function might be a reasonable fit for this dataset. More complex alternatives could be Emax models (with or without a Hill parameter), fractional polynomials or a spline function.

Responses can also be plotted grouped by class rather than by treatment, and the relative effects between each treatment/class can be plotted instead of the absolute treatment responses. Since the MBNMA framework models the time-course on relative effects (Pedder et al. 2019) this can in fact make interpretation of the plots easier with regards to identifying a best-fitting time-course function.

# Draw plot of within-study relative effects over time grouped by class
network.gout <- mb.network(goutSUA_CFBcomb)
timeplot(network.gout, level="class", plotby="rel")

Many of the profiles here appear to be quite different within the same class, which would suggest modelling class effects may be inappropriate for this dataset.

Analysis using mb.run()

MBNMA models are fitted using mb.run(). This can just as easily be performed on datasets with many different treatments (network meta-analysis) as it can on datasets comparing only two treatments (pairwise meta-analysis) - the syntax is the same.

An object or class("mb.network") must be provided as the data for mb.run(). The key arguments within mb.run() involve specifying the functional form used to model the time-course, and the time-course parameters that comprise that functional form.

Time-course functions

A number of different time-course functions can be fitted within MBNMAtime and the specific forms of the time-course parameters are defined by arguments within these functions, and this allows for a wide variety of parameterizations and time-course shapes. For further details check the help files for each function (e.g. ?tloglin()). These functions, are then used as inputs for the fun argument in mb.run().

Time-course parameters within time-course functions are each defined by two arguments:

pool is used to define the approach used for the pooling of a given time-course parameter and can either of:

method is used to define the model used for meta-analysis for a given time-course parameter and can take either of:

Specifying pooling relative effects on all time-course parameters would imply performing a contrast-based synthesis, whereas specifying pooling absolute effects on all of them would imply performing an arm-based synthesis. There has been substantial discussion in the literature regarding the strengths and limitations of both these approaches (Dias and Ades 2016; Hong et al. 2016; Karahalios et al. 2017).

Additional arguments within the function may also be used to specify the degree (e.g. for polynomials) or the number of knots or knot placement for splines.


mb.run() returns an object of class(c("mbnma", "rjags")). summary() provides summary estimates of posterior densities for different parameters in the model, with some explanation regarding the way in which the model has been defined. Estimates are automatically reported for parameters of interest depending on the model specification (unless otherwise specified in parameters.to.save). Nodes that are automatically monitored (if present in the model) have the following interpretation:

Parameters modelled using relative effects

If pooling is relative (e.g. pool.1="rel") for a given parameter then the named parameter (e.g. emax) or a numbered d parameter (e.g. d.1) corresponds to the pooled relative effect for a given treatment compared to the network reference treatment for this time-course parameter.

sd. followed by a named (e.g. emax, beta.1) is the between-study SD (heterogeneity) for relative effects, reported if pooling for a time-course parameter is relative (e.g. pool.1="rel") and the method for synthesis is random (e.g. method.1="random).

If class effects are modelled, parameters for classes are represented by the upper case name of the time-course parameter they correspond to. For example if class.effect=list(emax="random"), relative class effects will be represented by EMAX. The SD of the class effect (e.g. sd.EMAX, sd.BETA.1) is the SD of treatments within a class for the time-course parameter they correspond to.

Parameters modelled using absolute effects

If pooling is absolute (e.g. pool.1="abs") for a given parameter then the named parameter (e.g. emax) or a numbered beta parameter (e.g. beta.1) corresponds to the estimated absolute effect for this time-course parameter.

For an absolute time-course parameter if the corresponding method is common (e.g. method.1="common") the parameter corresponds to a single common parameter estimated across all studies and treatments. If the corresponding method is random (e.g. method.1="random") then parameter is a mean effect around which the study-level absolute effects vary with SD corresponding to sd. followed by the named parameter (e.g. sd.emax, sd.beta.1).

Other model parameters

rho is the correlation coefficient for correlation between time-points. Its interpretation will differ depending on the covariance structure specified in covar.

totresdev is residual deviance of the model and deviance is the deviance of the model. Model fit statistics for pD (effective number of parameters) and DIC (Deviance Information Criterion) are also reported, with an explanation as to how they have been calculated.


An example MBNMA of the alogliptin dataset using a linear time-course function and common treatment effects that pool relative effects and assumes consistency between direct and indirect evidence can be performed as follows:

# Run a linear time-course MBNMA
mbnma <- mb.run(network.alog, fun=tpoly(degree=1, pool.1="rel", method.1="common"))
#> module glm loaded
#> ========================================
#> Time-course MBNMA
#> ========================================
#> Time-course function: poly (degree = 1)
#> Data modelled with intercept
#> beta.1 parameter
#> Pooling: relative effects
#> Method: common treatment effects
#> |Treatment |Parameter |  Median|    2.5%|   97.5%|
#> |:---------|:---------|-------:|-------:|-------:|
#> |placebo   |d.1[1]    |  0.0000|  0.0000|  0.0000|
#> |alog_6.25 |d.1[2]    | -0.0365| -0.0394| -0.0337|
#> |alog_12.5 |d.1[3]    | -0.0418| -0.0436| -0.0400|
#> |alog_25   |d.1[4]    | -0.0449| -0.0467| -0.0431|
#> |alog_50   |d.1[5]    | -0.0507| -0.0535| -0.0480|
#> |alog_100  |d.1[6]    | -0.0488| -0.0663| -0.0301|
#> Correlation between time points
#> Covariance structure: varadj 
#> Rho assigned a numeric value: 0
#> #### Model Fit Statistics ####
#> Effective number of parameters:
#> pD (pV) calculated using the rule, pD = var(deviance)/2 = 34
#> Deviance = 403
#> Residual deviance = 1350
#> Deviance Information Criterion (DIC) = 437

For this model, the d.1 parameters correspond to the 1st polynomial coefficient, and therefore are the linear gradient of the response over time for each treatment versus placebo. However, note that the residual deviance of the model is very high, suggesting (as we might expect) that this linear time-course function is a poor fit.

We may want to fit a more complex time-course function with two time-course parameters, such as an Emax function, yet limitations in the data might require that we make an assumption that one of the parameters does not vary by treatment. We can specify this by setting pool to be equal to "abs" for any parameters we choose.

# Run an Emax time-course MBNMA with two parameters
mbnma <- mb.run(network.alog, fun=temax(
  pool.emax = "rel", method.emax="common",
  pool.et50 = "abs", method.et50="common"
#> 'et50' parameters are on exponential scale to ensure they take positive values on the natural scale
#> ========================================
#> Time-course MBNMA
#> ========================================
#> Time-course function: emax
#> Data modelled with intercept
#> emax parameter
#> Pooling: relative effects
#> Method: common treatment effects
#> |Treatment |Parameter |  Median|    2.5%|   97.5%|
#> |:---------|:---------|-------:|-------:|-------:|
#> |placebo   |emax[1]   |  0.0000|  0.0000|  0.0000|
#> |alog_6.25 |emax[2]   | -0.6477| -0.7280| -0.5723|
#> |alog_12.5 |emax[3]   | -0.8052| -0.8723| -0.7458|
#> |alog_25   |emax[4]   | -0.8802| -0.9515| -0.8146|
#> |alog_50   |emax[5]   | -0.9988| -1.0970| -0.9103|
#> |alog_100  |emax[6]   | -0.8685| -1.1453| -0.5879|
#> et50 parameter
#> Pooling: absolute effects
#> Method: common treatment effects
#> Parameter modelled on exponential scale to ensure it takes positive values on the natural scale
#> |Treatment |Parameter | Median|   2.5%|  97.5%|
#> |:---------|:---------|------:|------:|------:|
#> |placebo   |et50      | 1.7227| 1.5664| 1.8826|
#> |alog_6.25 |et50      | 1.7227| 1.5664| 1.8826|
#> |alog_12.5 |et50      | 1.7227| 1.5664| 1.8826|
#> |alog_25   |et50      | 1.7227| 1.5664| 1.8826|
#> |alog_50   |et50      | 1.7227| 1.5664| 1.8826|
#> |alog_100  |et50      | 1.7227| 1.5664| 1.8826|
#> Correlation between time points
#> Covariance structure: varadj 
#> Rho assigned a numeric value: 0
#> #### Model Fit Statistics ####
#> Effective number of parameters:
#> pD (pV) calculated using the rule, pD = var(deviance)/2 = 34
#> Deviance = -439
#> Residual deviance = 508
#> Deviance Information Criterion (DIC) = -404

In this case, the parameters are named following the Emax function specification. emax corresponds to the maximum relative effect for each treatment versus place, whereas et50 is the log of the time at which 50% of the maximum response is achieved, across all treatments in the network. This assumes conditional constancy of absolute effects for this time-course parameter, which is typically a strong assumption. However, if there were limited data with which to inform this parameter (e.g. at earlier time-points) then such an assumption might be necessary, with the caveat that interpolation of response at time-points informed by this parameter may be more susceptible to bias. Further exploration of the degree of data required for reliable estimation of time-course parameters is given in Pedder et al. (2020).

Additional model specification with mb.run()

Correlation between time points

Within-study correlation between time points can easily be modelled using mb.run(), though this requires some additional considerations. The simplest approach is to incorporate correlation by using a variance adjustment (Jansen, Vieira, and Cope 2015). This avoids the need to use a multivariate normal likelihood (which is slow to run), and it assumes a common correlation between neighbouring time-points. This is achieved by using the argument covar="varadj", which is the default in mb.run().

There are two alternative covariance structures can be modelled, though these require fitting a multivariate normal likelihood and therefore take longer to run. covar="CS" specifies fitting a Compound Symmetry covariance structure, whilst covar="AR1" specifies fitting an autoregressive AR1 covariance structure to the multivariate normal likelihood used for modelling the correlation between multiple time points within a study (PennState Eberly College of Science, n.d.).

However, in addition to this, it’s also necessary to specify a value for rho, and this can be assigned in one of two ways:

  • Given as string representing a JAGS prior distribution (Plummer 2017), which indicates that the correlation should be estimated from the data. For example, to specify a prior that the correlation between time-points will be between 0 and 1 with equal probability you could set rho="dunif(0,1)".
  • Given as a single numeric value, which indicates that the correlation should be fixed to that value. For example, this value could be estimated externally from another study using Individual Participant Data. This could also be used to run a deterministic sensitivity analysis using different fixed values of rho.
# Using the COPD dataset
network.copd <- mb.network(copd)

# Run an log-linear time-course MBNMA 
# that accounts for correlation between time points using variance adjustment
mbnma <- mb.run(network.copd, 
                fun=tloglin(pool.rate="rel", method.rate="random"),
                rho="dunif(0,1)", covar="varadj")

It is important to note that the covariance matrix must be positive semi-definite. This may mean that in order to satisfy this requirement for particular covariance matrix structures, the values that rho can take are limited. rho must always be bounded by -1 and 1, but even within this range some negative values for rho can result in a non positive matrix, which can lead to an error in the evaluation of the multivariate likelihood. If so, it may be necessary to further restrict the prior distribution.

Class effects

Shared effects between treatments within the network can be modelled using class effects. This requires assuming that different treatments have some sort of shared class effect, perhaps due to different (yet clinically similar) doses of the same agent or different treatments with a similar mechanism of action. One advantage of this is that class effects can be used to connect relative effects between treatments in a network that would be disconnected at the treatment level, but can be connected via classes at the class level. However, it is important to ensure that such an effect is clinically justifiable, as making these assumptions risks introducing heterogeneity/inconsistency.

Class effects can only be applied to time-course parameters which vary by treatment (pool="rel"), and class effects are modelled separately for each time-course parameter.

In mb.run() class effects are specified as a list, in which each element is named by the time-course parameter on which it should be modelled. The class effect for each time-course parameter can be either "common", in which the effects for each treatment within the same class are constrained to a common class effect, or "random", in which the effects for each treatment within the same class are assumed to be randomly distributed around a shared class mean.

# Run a B-spline time-course MBNMA with a knot at 0.2 times the max follow-up
# Common class effect on beta.2, the 2nd spline coefficient
mbnma <- mb.run(network.gout, 
                fun=tspline(type="bs", knots=c(0.2),
                            pool.1 = "rel", method.1="common",
                            pool.2="rel", method.2="random"),
                class.effect = list(beta.2="common"))
#> ========================================
#> Time-course MBNMA
#> ========================================
#> Time-course function: B-spline (knots = 0.2; degree = 1)
#> Data modelled with intercept
#> beta.1 parameter
#> Pooling: relative effects
#> Method: common treatment effects
#> |Treatment |Parameter |   Median|     2.5%|    97.5%|
#> |:---------|:---------|--------:|--------:|--------:|
#> |Plac      |d.1[1]    |   0.0000|   0.0000|   0.0000|
#> |Allo_100  |d.1[2]    |  -1.5932|  -3.0459|  -0.0849|
#> |Allo_200  |d.1[3]    |  -3.1365|  -3.3959|  -2.8857|
#> |Allo_289  |d.1[4]    |  -4.4078|  -4.5717|  -4.2367|
#> |Allo_400  |d.1[5]    | -12.4440| -14.0334| -10.9217|
#> |Arha_NA   |d.1[6]    |  -6.8517|  -7.5575|  -6.1069|
#> |BCX4_140  |d.1[7]    |  -4.6051|  -4.9666|  -4.2345|
#> |BCX4_18.5 |d.1[8]    |  -2.4991|  -2.9553|  -2.0596|
#> |BCX4_240  |d.1[9]    |  -5.9009|  -6.3620|  -5.4527|
#> |BCX4_80   |d.1[10]   |  -3.6243|  -4.0995|  -3.1752|
#> |Benz_NA   |d.1[11]   | -15.0427| -16.2538| -13.9201|
#> |Febu_140  |d.1[12]   |  -6.9461|  -7.1171|  -6.7678|
#> |Febu_210  |d.1[13]   |  -9.0091|  -9.1153|  -8.9007|
#> |Febu_25   |d.1[14]   |  -4.0053|  -4.1695|  -3.8418|
#> |Febu_72.5 |d.1[15]   |  -5.9425|  -6.1132|  -5.7669|
#> |RDEA_100  |d.1[16]   |  -2.2728|  -2.8326|  -1.7272|
#> |RDEA_200  |d.1[17]   |  -4.0424|  -4.3907|  -3.6734|
#> |RDEA_400  |d.1[18]   |  -5.1579|  -5.5053|  -4.8083|
#> |RDEA_600  |d.1[19]   |  -8.0515|  -8.4898|  -7.6161|
#> beta.2 parameter
#> Pooling: relative effects
#> Method: random treatment effects
#> Class effects modelled for this parameter
#> |Treatment |Parameter |  Median|     2.5%|   97.5%|
#> |:---------|:---------|-------:|--------:|-------:|
#> |Plac      |d.2[1]    |  0.0000|   0.0000|  0.0000|
#> |Allo_100  |d.2[2]    | 15.0165|  -0.9267| 29.5792|
#> |Allo_200  |d.2[3]    | 15.0165|  -0.9267| 29.5792|
#> |Allo_289  |d.2[4]    | 15.0165|  -0.9267| 29.5792|
#> |Allo_400  |d.2[5]    | 15.0165|  -0.9267| 29.5792|
#> |Arha_NA   |d.2[6]    | -0.4155| -62.4791| 61.8483|
#> |BCX4_140  |d.2[7]    | 10.7825|  -3.1657| 24.9763|
#> |BCX4_18.5 |d.2[8]    | 10.7825|  -3.1657| 24.9763|
#> |BCX4_240  |d.2[9]    | 10.7825|  -3.1657| 24.9763|
#> |BCX4_80   |d.2[10]   | 10.7825|  -3.1657| 24.9763|
#> |Benz_NA   |d.2[11]   | 15.5054| -14.6098| 43.7460|
#> |Febu_140  |d.2[12]   | 15.9188|   2.8667| 27.7989|
#> |Febu_210  |d.2[13]   | 15.9188|   2.8667| 27.7989|
#> |Febu_25   |d.2[14]   | 15.9188|   2.8667| 27.7989|
#> |Febu_72.5 |d.2[15]   | 15.9188|   2.8667| 27.7989|
#> |RDEA_100  |d.2[16]   | 20.1915|  -0.2510| 38.5101|
#> |RDEA_200  |d.2[17]   | 20.1915|  -0.2510| 38.5101|
#> |RDEA_400  |d.2[18]   | 20.1915|  -0.2510| 38.5101|
#> |RDEA_600  |d.2[19]   | 20.1915|  -0.2510| 38.5101|
#> Between-study SD modelled for this parameter:
#> |Parameter |  Median|    2.5%|   97.5%|
#> |:---------|-------:|-------:|-------:|
#> |sd.beta.2 | 13.8466| 10.1003| 19.9032|
#> Class Effects
#> Class effects for beta.2
#> Common (fixed) class effects
#> |Class |Parameter |  Median|     2.5%|   97.5%|
#> |:-----|:---------|-------:|--------:|-------:|
#> |Plac  |D.2[1]    |  0.0000|   0.0000|  0.0000|
#> |Allo  |D.2[2]    | 15.0165|  -0.9267| 29.5792|
#> |Arha  |D.2[3]    | -0.4155| -62.4791| 61.8483|
#> |BCX4  |D.2[4]    | 10.7825|  -3.1657| 24.9763|
#> |Benz  |D.2[5]    | 15.5054| -14.6098| 43.7460|
#> |Febu  |D.2[6]    | 15.9188|   2.8667| 27.7989|
#> |RDEA  |D.2[7]    | 20.1915|  -0.2510| 38.5101|
#> Correlation between time points
#> Covariance structure: varadj 
#> Rho assigned a numeric value: 0
#> #### Model Fit Statistics ####
#> Effective number of parameters:
#> pD (pV) calculated using the rule, pD = var(deviance)/2 = 99
#> Deviance = 13311
#> Residual deviance = 14005
#> Deviance Information Criterion (DIC) = 13410

Mean class effects are given in the output as D.2 parameters. These can be interpreted as the relative effect of each class versus the Plac (Placebo), for the 2nd spline coefficient (beta.2). Note the number of D.2 parameters is therefore equal to the number of classes defined in the dataset.

Additional arguments

Several additional arguments can be given to mb.run() that require further explanation.


Default vague priors for the model are as follows:

\[ \begin{aligned} &\alpha_{i} \sim N(0,10000)\\ &\boldsymbol{\mu}_{i} \sim MVN(0,M_{i})\\ &\boldsymbol{d}_{t} \sim MVN(0,\Sigma_{t})\\ &beta_{\phi} \sim N(0,10000)\\ &D_{\phi,c} \sim N(0,1000)\\ &\tau_{\phi} \sim N(0,400) \text{ limited to } x \in [0,\infty]\\ &\tau^D_{\phi} \sim N(0,400) \text{ limited to } x \in [0,\infty]\\ &M_{i} \sim Wish^{-1}(\Omega,k)\\ &\Sigma_{t} \sim Wish^{-1}(\Omega,k)\\ \end{aligned} \]

  • \(\alpha_i\) is the response at time=0 in study \(i\)
  • \(\mu_i\) is a vector of study reference effects for each time-course parameter in study \(i\). Where only a single time-course parameter is modelled using relative effects the prior is defined as \(\mu_{i} \sim N(0,10000)\).
  • \(\boldsymbol{d}_{t}\) is a vector of pooled relative effects for treatment \(t\) whose length is the number of time-course parameters in the model. Where only a single time-course parameter is modelled using relative effects the prior is defined as \(d_{t} \sim N(0,10000)\).
  • \(\beta_{\phi}\) is the absolute effect for time-course parameter \(\phi\) modelled independently of treatment
  • \(D_{\phi,c}\) is the class relative effect for time-course parameter \(\phi\) in class \(c\)
  • \(\tau_{\phi}\) is the between-study SD for time-course parameter \(\phi\)
  • \(\tau^D_{\phi}\) is the within-class SD for time-course parameter \(\phi\)
  • \(\Omega\) is a diagonal scale matrix specified in var.scale
  • \(k\) is the degrees of freedom for the inverse-Wishart distribution, equal to the number of time-course parameters

Users may wish to change these, perhaps in order to use more/less informative priors, but also because the default prior distributions in some models may lead to errors when compiling/updating models.

This can be more likely for certain types of models. For example some prior distributions may generate results that are too extreme for JAGS to compute, such as for time-course parameters that are powers (e.g. Emax functions with a Hill parameter or power parameters in fractional polynomials).

If the model fails during compilation/updating (i.e. due to a problem in JAGS), mb.run() will generate an error and return a list of arguments that mb.run() used to generate the model. Within this (as within a model that has run successfully), the priors used by the model (in JAGS syntax) are stored within "model.arg".

In this way a model can first be run with vague priors and then rerun with different priors, perhaps to allow successful computation, perhaps to provide more informative priors, or perhaps to run a sensitivity analysis with different priors.

To change priors within a model, a list of replacements can be provided to priors in mb.run(). The name of each element is the name of the parameter to change (without indices) and the value of the element is the JAGS distribution to use for the prior. See the JAGS Manual (2017) for syntax details regarding specifying distributions. This can include censoring or truncation if desired. Only the priors to be changed need to be specified - priors for parameters that aren’t specified will take default values. Note that in JAGS, normal distributions are specified using precision (1/variance) rather than SD.

For example, we may wish to specify a tighter prior for the between-study SD:

mbnma <- mb.run(network.copd, 
                fun=tloglin(pool.rate="rel", method.rate="random"),
                priors=list(rate="dnorm(0,2) T(0,)"))

pD (effective number of parameters)

The default value for pd in mb.run() is pd="pv", which uses the rapid approach automatically calculated in the R2jags package as pv = var(deviance)/2. Whilst this is easy to calculate, it is only an approximation to the effective number of parameters, and may be numerically unstable (Gelman et al. 2003). However, it has been shown to be reliable for model comparison in time-course MBNMA models in a simulation study (Pedder et al. 2020).

A more reliable method for estimating pd is pd="pd.kl", which uses the Kullback-Leibler divergence (Plummer 2008). This is more reliable than the default method used in R2jags for calculating the effective number of parameters from non-linear models. The disadvantage of this approach is that it requires running additional MCMC iterations, so can be slightly slower to calculate.

A commonly-used approach in Bayesian models for calculating pD is the plug-in method (pd="plugin") (Spiegelhalter et al. 2002). However, this can sometimes result in negative non-sensical values due to skewed posterior distributions for deviance contributions that can arise when fitting non-linear models.

Finally, pD can also be calculated using an optimism adjustment (pd="popt") which allows for calculation of the penalized expected deviance (Plummer 2008). This adjustment allows for the fact that data used to estimate the model is the same as that used to assess its parsimony. As for pd="pd.kl", it also requires running additional MCMC iterations.

Correlation between time-course parameters

mb.run() automatically models correlation between time-course parameters modelled using relative effects. Time-course parameters are typically correlated and this allows information on each parameter to help inform the other(s). The correlation is modelled using a multivariate normal distribution with a vague inverse-Wishart prior on the covariance matrix \(\Sigma_t\). This can be made more informative by indicating the scale matrix for the parameters that are modelled using relative effects, and by increasing the degrees of freedom of the inverse-Wishart prior \(\Sigma_t\).

omega can be used as an argument in mb.run() to represent this scale matrix. If specified, it must be a symmetric positive definite matrix with dimensions equal to the number of time-course parameters modelled using relative effects (pool="rel"). If left as NULL (the default) a diagonal matrix with elements equal to 1 is used. The degrees of freedom can then be changed within the priors argument of mb.run() to make the prior more informative.

For example, with the osteoarthritis dataset we might expect that for a piecewise linear time-course function, the parameter values (in this model the relative different in gradient vs placebo) for the first coefficient might be 10 times larger than for the second coefficient:

mbnma <- mb.run(network.pain,
                fun=tspline(type="ls", knots=1,
                            pool.1="rel", method.1="random",
                            pool.2="rel", method.2="common"),
                omega=matrix(c(10,3,3,1), nrow=2))

Arguments to be sent to JAGS

In addition to the arguments specific to mb.run() it is also possible to use any arguments to be sent to R2jags::jags(). Most of these relate to improving the performance of MCMC simulations in JAGS. Some of the key arguments that may be of interest are:

  • n.chains The number of Markov chains to run (default is 3)
  • n.iter The total number of iterations per MCMC chain
  • n.burnin The number of iterations that are discarded to ensure iterations are only saved once chains have converged
  • n.thin The thinning rate which ensures that results are only saved for 1 in every n.thin iterations per chain. This can be increased to reduce autocorrelation in MCMC samples

Model Convergence

MBNMAtime is run using JAGS, which performs Bayesian inference using Gibbs sampling (JAGS Computer Program 2017). This samples from the posterior distribution to obtain posterior densities for monitored parameters of interest. However, convergence of the MCMC algorithm on the posterior density is therefore critical for obtaining robust results.

Multiple MCMC chains should always be run (default in MBNMAtime is 3) as this allows for traces of each chain to be examined. Consistent overlap between the traces in different chains, as well as low autocorrelation in MCMC samples suggests that convergence is likely to have been successful.

A simple output for assessing convergence is the Gelman-Rubin \(\hat{R}\) statistic, which is the ratio of between- and within-chain standard deviations (Gelman et al. 2003). Values of <1.05 are widely accepted as implying convergence for practical values, though some functions in MBNMAtime alerts users if \(\hat{R}>1.02\).

Rhat values for each parameter are calculated automatically in R2jags and can be shown if print() is called on an object of class("mbnma"):

#> Inference for Bugs model at "C:\Users\hp17602\AppData\Local\Temp\RtmpUR7Fjg\file4c5c4a54e72", fit using jags,
#>  3 chains, each with 20000 iterations (first 10000 discarded), n.thin = 10
#>  n.sims = 3000 iterations saved
#>             mu.vect sd.vect      2.5%       25%       50%       75%     97.5%
#> D.2[1]        0.000   0.000     0.000     0.000     0.000     0.000     0.000
#> D.2[2]       14.782   7.706    -0.927     9.978    15.016    19.844    29.579
#> D.2[3]       -0.064  31.874   -62.479   -21.943    -0.415    22.150    61.848
#> D.2[4]       10.833   7.248    -3.166     5.832    10.782    15.484    24.976
#> D.2[5]       15.111  14.549   -14.610     5.781    15.505    24.534    43.746
#> D.2[6]       15.729   6.410     2.867    11.537    15.919    20.040    27.799
#> D.2[7]       19.929   9.760    -0.251    13.763    20.191    26.341    38.510
#> d.1[1]        0.000   0.000     0.000     0.000     0.000     0.000     0.000
#> d.1[2]       -1.595   0.744    -3.046    -2.097    -1.593    -1.096    -0.085
#> d.1[3]       -3.137   0.127    -3.396    -3.221    -3.136    -3.050    -2.886
#> d.1[4]       -4.408   0.088    -4.572    -4.467    -4.408    -4.350    -4.237
#> d.1[5]      -12.454   0.780   -14.033   -12.970   -12.444   -11.926   -10.922
#> d.1[6]       -6.843   0.372    -7.558    -7.096    -6.852    -6.587    -6.107
#> d.1[7]       -4.603   0.188    -4.967    -4.736    -4.605    -4.475    -4.234
#> d.1[8]       -2.502   0.227    -2.955    -2.654    -2.499    -2.348    -2.060
#> d.1[9]       -5.901   0.235    -6.362    -6.058    -5.901    -5.742    -5.453
#> d.1[10]      -3.625   0.242    -4.100    -3.787    -3.624    -3.452    -3.175
#> d.1[11]     -15.054   0.585   -16.254   -15.444   -15.043   -14.669   -13.920
#> d.1[12]      -6.944   0.091    -7.117    -7.005    -6.946    -6.883    -6.768
#> d.1[13]      -9.008   0.053    -9.115    -9.044    -9.009    -8.972    -8.901
#> d.1[14]      -4.005   0.086    -4.170    -4.064    -4.005    -3.947    -3.842
#> d.1[15]      -5.942   0.089    -6.113    -6.002    -5.942    -5.884    -5.767
#> d.1[16]      -2.276   0.281    -2.833    -2.468    -2.273    -2.084    -1.727
#> d.1[17]      -4.040   0.182    -4.391    -4.162    -4.042    -3.920    -3.673
#> d.1[18]      -5.160   0.173    -5.505    -5.271    -5.158    -5.046    -4.808
#> d.1[19]      -8.055   0.223    -8.490    -8.206    -8.052    -7.903    -7.616
#> d.2[1]        0.000   0.000     0.000     0.000     0.000     0.000     0.000
#> d.2[2]       14.782   7.706    -0.927     9.978    15.016    19.844    29.579
#> d.2[3]       14.782   7.706    -0.927     9.978    15.016    19.844    29.579
#> d.2[4]       14.782   7.706    -0.927     9.978    15.016    19.844    29.579
#> d.2[5]       14.782   7.706    -0.927     9.978    15.016    19.844    29.579
#> d.2[6]       -0.064  31.874   -62.479   -21.943    -0.415    22.150    61.848
#> d.2[7]       10.833   7.248    -3.166     5.832    10.782    15.484    24.976
#> d.2[8]       10.833   7.248    -3.166     5.832    10.782    15.484    24.976
#> d.2[9]       10.833   7.248    -3.166     5.832    10.782    15.484    24.976
#> d.2[10]      10.833   7.248    -3.166     5.832    10.782    15.484    24.976
#> d.2[11]      15.111  14.549   -14.610     5.781    15.505    24.534    43.746
#> d.2[12]      15.729   6.410     2.867    11.537    15.919    20.040    27.799
#> d.2[13]      15.729   6.410     2.867    11.537    15.919    20.040    27.799
#> d.2[14]      15.729   6.410     2.867    11.537    15.919    20.040    27.799
#> d.2[15]      15.729   6.410     2.867    11.537    15.919    20.040    27.799
#> d.2[16]      19.929   9.760    -0.251    13.763    20.191    26.341    38.510
#> d.2[17]      19.929   9.760    -0.251    13.763    20.191    26.341    38.510
#> d.2[18]      19.929   9.760    -0.251    13.763    20.191    26.341    38.510
#> d.2[19]      19.929   9.760    -0.251    13.763    20.191    26.341    38.510
#> rho           0.000   0.000     0.000     0.000     0.000     0.000     0.000
#> sd.beta.2    14.150   2.472    10.100    12.404    13.847    15.555    19.903
#> totresdev 14005.435  14.059 13979.653 13995.610 14004.881 14014.440 14034.654
#> deviance  13311.247  14.059 13285.464 13301.422 13310.692 13320.251 13340.465
#>            Rhat n.eff
#> D.2[1]    1.000     1
#> D.2[2]    1.001  3000
#> D.2[3]    1.001  3000
#> D.2[4]    1.001  3000
#> D.2[5]    1.001  3000
#> D.2[6]    1.001  3000
#> D.2[7]    1.002  1800
#> d.1[1]    1.000     1
#> d.1[2]    1.001  3000
#> d.1[3]    1.002  2200
#> d.1[4]    1.001  3000
#> d.1[5]    1.003  1400
#> d.1[6]    1.001  3000
#> d.1[7]    1.001  3000
#> d.1[8]    1.001  3000
#> d.1[9]    1.001  3000
#> d.1[10]   1.001  3000
#> d.1[11]   1.005  1000
#> d.1[12]   1.001  3000
#> d.1[13]   1.003   800
#> d.1[14]   1.001  3000
#> d.1[15]   1.001  3000
#> d.1[16]   1.001  3000
#> d.1[17]   1.002  2000
#> d.1[18]   1.001  3000
#> d.1[19]   1.001  3000
#> d.2[1]    1.000     1
#> d.2[2]    1.001  3000
#> d.2[3]    1.001  3000
#> d.2[4]    1.001  3000
#> d.2[5]    1.001  3000
#> d.2[6]    1.001  3000
#> d.2[7]    1.001  3000
#> d.2[8]    1.001  3000
#> d.2[9]    1.001  3000
#> d.2[10]   1.001  3000
#> d.2[11]   1.001  3000
#> d.2[12]   1.001  3000
#> d.2[13]   1.001  3000
#> d.2[14]   1.001  3000
#> d.2[15]   1.001  3000
#> d.2[16]   1.002  1800
#> d.2[17]   1.002  1800
#> d.2[18]   1.002  1800
#> d.2[19]   1.002  1800
#> rho       1.000     1
#> sd.beta.2 1.002  1300
#> totresdev 1.001  3000
#> deviance  1.001  3000
#> For each parameter, n.eff is a crude measure of effective sample size,
#> and Rhat is the potential scale reduction factor (at convergence, Rhat=1).
#> DIC info (using the rule, pD = var(deviance)/2)
#> pD = 98.9 and DIC = 13409.6
#> DIC is an estimate of expected predictive error (lower deviance is better).

Two useful packages for investigating convergence of monitored parameters are the mcmcplots and coda packages. These can be used to generate plots that can provide information on MCMC chain overlap and autocorrelation:

# Traceplots
mcmcplots::traplot(mbnma, "sd.beta.1")

# Running mean plots
mcmcplots::rmeanplot(mbnma, "sd.beta.1")

# Posterior densities
mcmcplots::denplot(mbnma, "sd.beta.1")

# Autocorrelation plots

A single function (mcmcplots::mcmcplot()) can also be used to generate an HTLM with all these plots for all monitored parameters.

If there are problems with convergence, the first solution may be to run the model for more iterations (see Arguments to be sent to JAGS), which give the chains a greater number of calculations over which to converge. However, for models with a large number of parameters and relatively limited data, information may be too limited to allow convergence within a computationally reasonable number of iterations. In these situations there is not enough information in the data to support such a highly parameterised model, suggesting that a simpler model should be fitted. Alternatively, more informative priors can be used, but given the lack of information in the data relative to the model’s complexity, results are likely to be sensitive to these priors.

For a detailed review of MCMC convergence assessment see Sinharay (2003).


Deviance plots

To assess how well a model fits the data, it can be useful to look at a plot of the contributions of each data point to the total deviance or residual deviance. This can be done using devplot(). As individual deviance contributions are not automatically monitored in the model, this might require the model to be run for additional iterations.

Results can be plotted either as a scatter plot (plot.type="scatter") or a series of boxplots (plot.type="box").

# Run a first-order fractional polynomial time-course MBNMA
mbnma <- mb.run(network.pain, 
                          pool.1="rel", method.1="random",

# Plot a box-plot of deviance contributions (the default)
devplot(mbnma, n.iter=1000)
#> `dev` not monitored in mbnma$parameters.to.save.
#> additional iterations will be run in order to obtain results for `dev`

From these plots we can see that whilst the model fit is good at later time points, it frequently underestimates responses at earlier time points.

A function that appropriately captures the time-course shape should show a reasonably flat shape of deviance contributions (i.e. contributions should be similar across all time points).

If saved to an object, the output of devplot() contains the results for individual deviance contributions, and this can be used to identify any extreme outliers.

Fitted values

Another approach for assessing model fit can be to plot the fitted values, using fitplot(). As with devplot(), this may require running additional model iterations to monitor theta.

# Plot fitted and observed values with treatment labels
fitplot(mbnma, n.iter=1000)

Fitted values are plotted as connecting lines and observed values in the original dataset are plotted as points. These plots can be used to identify if the model fits the data well for different treatments and at different parts of the time-course.

Forest plots

Forest plots can be easily generated from MBNMA models using the plot() method on an "mbnma" object. By default this will plot a separate panel for each time-course parameter in the model. Forest plots can only be generated for parameters which vary by treatment/class.

# Run a quadratic time-course MBNMA using the alogliptin dataset
mbnma <- mb.run(network.alog, 
                          pool.1="rel", method.1="random",
                          pool.2="rel", method.2="common"



Rankings can be calculated for different time-course parameters from MBNMA models by using rank() on an "mbnma" object. Any parameter monitored in an MBNMA model that varies by treatment/class can be ranked. A vector of these is assigned to params. lower_better indicates whether negative scores should be ranked as “better” (TRUE) or “worse” (FALSE)

In addition, it is possible to rank the Area Under the Curve (AUC) for a particular treatment by adding "auc" to the vector of params (included as the default). This will calculate the area under the predicted response over time, and will therefore be a function of all the time-course parameters in the model simultaneously. However, it will be dependent on the range of times chosen to integrate over (int.range), and a different choice of time-frame may lead to different treatment rankings. "auc" can also not currently be calculated from MBNMA models with more complex time-course functions (piecewise, fractional polynomials), nor with MBNMA models that use class effects.

# Identify quantile for knot at 1 week
timequant <- 1/max(network.pain$data.ab$time)

# Run a piecewise linear time-course MBNMA with a knot at 1 week
mbnma <- mb.run(network.pain,
                fun=tspline(type="ls", knots = timequant,
                            pool.1 = "rel", method.1="common",
                            pool.2 = "rel", method.2="common"))

# Rank results based on AUC (calculated 0-10 weeks), more negative slopes considered to be "better"
ranks <- rank(mbnma, params=c("auc", "d.2"), 
                    int.range=c(0,10),  lower_better = TRUE, n.iter=1000)
#> ========================================
#> Treatment rankings
#> ======================================== 
#> d.2 ranking
#> |Treatment |  Mean| Median| 2.5%| 97.5%|
#> |:---------|-----:|------:|----:|-----:|
#> |Pl_0      |  8.69|      9|    5| 13.00|
#> |Ce_100    | 17.50|     18|    7| 26.00|
#> |Ce_200    | 16.27|     16|   11| 21.00|
#> |Ce_400    | 18.16|     19|    8| 26.00|
#> |Du_90     |  8.21|      3|    1| 29.00|
#> |Et_10     | 23.42|     27|    4| 29.00|
#> |Et_30     | 14.86|     15|    7| 23.00|
#> |Et_5      |  4.54|      3|    1| 22.03|
#> |Et_60     | 24.27|     25|   15| 29.00|
#> |Et_90     | 19.68|     23|    3| 29.00|
#> |Lu_100    | 12.71|     13|    6| 20.00|
#> |Lu_200    | 13.98|     14|    8| 21.00|
#> |Lu_400    | 14.68|     14|    7| 22.00|
#> |Lu_NA     |  7.08|      3|    1| 29.00|
#> |Na_1000   | 21.53|     22|   17| 26.00|
#> |Na_1500   | 18.97|     19|   11| 25.00|
#> |Na_250    | 20.79|     24|    3| 29.00|
#> |Na_750    | 22.19|     23|   12| 28.00|
#> |Ox_44     |  7.45|      4|    1| 26.00|
#> |Ro_12     | 21.81|     25|    4| 29.00|
#> |Ro_125    | 11.95|      8|    1| 29.00|
#> |Ro_25     | 24.88|     26|   13| 29.00|
#> |Tr_100    |  6.12|      6|    2| 13.00|
#> |Tr_200    |  7.53|      7|    3| 16.00|
#> |Tr_300    |  9.78|      9|    4| 19.00|
#> |Tr_400    |  7.04|      6|    2| 18.00|
#> |Va_10     | 21.25|     23|    8| 28.00|
#> |Va_20     | 16.10|     16|    4| 27.00|
#> |Va_5      | 13.54|     13|    3| 26.00|
#> auc ranking
#> |Treatment |  Mean| Median|  2.5%| 97.5%|
#> |:---------|-----:|------:|-----:|-----:|
#> |Pl_0      | 26.99|     27| 26.00| 28.00|
#> |Ce_100    | 22.35|     23| 16.00| 26.00|
#> |Ce_200    | 14.08|     14| 10.00| 18.02|
#> |Ce_400    | 13.28|     13|  6.00| 21.00|
#> |Du_90     | 17.68|     19|  6.00| 26.00|
#> |Et_10     | 27.48|     28| 20.00| 29.00|
#> |Et_30     |  6.04|      6|  3.00| 11.00|
#> |Et_5      | 12.78|     11|  2.00| 28.00|
#> |Et_60     |  3.51|      3|  2.00|  7.00|
#> |Et_90     |  5.83|      3|  1.00| 24.00|
#> |Lu_100    | 13.79|     14|  9.00| 19.00|
#> |Lu_200    | 15.75|     16| 10.00| 21.00|
#> |Lu_400    |  9.52|      9|  5.00| 16.00|
#> |Lu_NA     | 11.41|     11|  4.00| 20.00|
#> |Na_1000   |  7.68|      8|  4.00| 12.00|
#> |Na_1500   |  8.85|      9|  4.00| 17.00|
#> |Na_250    | 28.32|     29| 24.98| 29.00|
#> |Na_750    | 19.78|     20| 11.00| 25.00|
#> |Ox_44     |  4.63|      3|  1.00| 17.00|
#> |Ro_12     | 20.99|     24|  4.00| 28.00|
#> |Ro_125    |  1.63|      1|  1.00|  5.02|
#> |Ro_25     | 16.77|     18|  5.00| 26.00|
#> |Tr_100    | 24.18|     25| 20.00| 27.00|
#> |Tr_200    | 23.19|     23| 19.00| 26.00|
#> |Tr_300    | 15.66|     16|  8.00| 22.00|
#> |Tr_400    | 11.27|     11|  4.00| 21.00|
#> |Va_10     | 21.67|     22| 13.00| 27.00|
#> |Va_20     | 13.17|     13|  4.00| 22.00|
#> |Va_5      | 16.70|     18|  5.98| 25.00|

The output is an object of class("mb.rank"), containing a list for each ranked parameter in params, which consists of a summary table of rankings and raw information on treatment ranking and probabilities. The summary median ranks with 95% credible intervals can be simply displayed using print().

Histograms for ranking results can also be plotted using the plot() method, which takes the raw MCMC ranking results given in rank.matrix and plots the number of MCMC iterations the parameter value for each treatment was ranked a particular position.

# Ranking histograms for AUC
plot(ranks, params = "auc")


After performing an MBNMA, responses can be predicted from the parameter estimates using predict() on an "mbnma" object. A number of important parameters need to be identified to make robust predictions, though defaults can be used to generate a plot that gives a good indication of the time-course relationship assuming a reference treatment response of zero. For further information the help file can be accessed using ?predict.mbnma.

One key parameter is E0, which defines what value(s) to use for the predicted response at time = 0. A single numeric value can be given for this to indicate a deterministic value, or a function representing a random number generator (RNG) distribution in R (stochastic) (e.g. E0 = ~rnorm(n, 7, 0.2). These values can be identified for the population of interest from external data (e.g. observational/registry).

The more challenging parameter(s) to identify are those for the network reference treatment response, supplied to predict() in the ref.resp argument. Typically in an MBNMA, relative effects are estimated and the network reference effect is modelled as a nuisance parameter. Therefore we need to provide an input for this reference treatment effect for all time-course parameters modelled using pool="rel" so that we can apply the relative effects estimated in our model to it. There are two options for providing these values.

The first approach is to give values for each time-course parameter modelled using relative effects to ref.resp. This is given as a list, with a separate named element for each time-course parameter. Each element can take either a single numeric value (deterministic), or a function representing a random number generator distribution in R (stochastic).

# Run an Emax time-course MBNMA using the osteoarthritis dataset
mbnma <- mb.run(network.pain,
                fun=temax(pool.emax="rel", method.emax="common",
                          pool.et50="abs", method.et50="common"),
                rho="dunif(0,1)", covar="varadj")
# Specify placebo time-course parameters
ref.params <- list(emax=-2)

# Predict responses for a selection of treatments using a stochastic E0 and
# placebo parameters defined in ref.params to estimate the network reference treatment effect
pred <- predict(mbnma, treats=c("Pl_0", "Ce_200", "Du_90", "Et_60", 
                                        "Lu_400", "Na_1000", "Ox_44", "Ro_25",
                                        "Tr_300", "Va_20"),
                        E0=~rnorm(n, 8, 0.5), ref.resp=ref.params)


The second is to assign ref.resp a data frame composed of single-arm studies of the network reference treatment. A separate synthesis model for the reference treatment effect will then be run, and the values from this used as the prediction reference treatment effect. This dataset could be a series of observational studies measured at multiple follow-up times that closely match the population of interest for the prediction. Alternatively it could be a subset of data from the original RCT dataset used for the MBNMA model (though this may be less generalisable to the population of interest).

# Generate a dataset of network reference treatment responses over time
placebo.df <- network.pain$data.ab[network.pain$data.ab$treatment==1,]

# Predict responses for a selection of treatments using a deterministic E0 and 
#placebo.df to model the network reference treatment effect
pred <- predict(mbnma, treats=c("Pl_0", "Ce_200", "Du_90", "Et_60", 
                                        "Lu_400", "Na_1000", "Ox_44", "Ro_25",
                                        "Tr_300", "Va_20"),
                        E0=10, ref.resp=placebo.df)


It is also possible specify the time points for which to predict responses (times), given as a vector of positive numbers. If left as the default then the maximum follow-up in the dataset will be used as the upper limit for the range of predicted time-points.

An object of class "mb.predict" is returned, which is a list of summary tables and MCMC prediction matrices for each treatment, in addition to the original mbnma object. The summary() method can be used to print mean posterior predictions at each time point for each treatment.

Predicted responses can also be plotted using the plot() method on an object of class("mb.predict"). Within the default arguments, the median predicted network reference treatment response is overlaid on the predicted response for each treatment. Setting overlay.ref = FALSE prevents this and causes the network reference treatment predicted response to be plotted as a separate panel. Shaded counts of observations in the original dataset at each predicted time point can be plotted over the 95% CrI for each treatment by setting disp.obs = TRUE.

plot(pred, overlay.ref=TRUE, disp.obs=TRUE)

This can be used to identify any extrapolation/interpretation of the time-course that might be occurring for a particular treatment, and where predictions might therefore be problematic.

To illustrate a situation in which this could be very informative, we can look at predicted responses for a quadratic time-course function fitted to the Obesity dataset:

# Fit a quadratic time-course MBNMA to the Obesity dataset
network.obese <- mb.network(obesityBW_CFB, reference = "plac")

mbnma <- mb.run(network.obese,
                          pool.1 = "rel", method.1="common",
                          pool.2="rel", method.2="common"))

# Define stochastic values centred at zero for network reference treatment
ref.params <- list(beta.1=~rnorm(n, 0, 0.05), beta.2=~rnorm(n, 0, 0.0001))

# Predict responses over the
pred.obese <- predict(mbnma, times=c(0:50), E0=100, treats = c(1,4,15),

# Plot predictions
plot(pred.obese, disp.obs = TRUE)

As you can see, within the limits of the observed data the predicted responses appear reasonable. However, extrapolation beyond this for dexf_30MG leads to some rather strange results, suggesting an unrealistically huge increase in body weight after 50 weeks of treatment. On the other hand, the predicted response at 50 weeks follow-up in treatment 15 is within the limits of the observed data and so are likely to be more justifiable.

Plotting “lumped” NMA results

As a further addition to the plots of MBNMA predictions, it is possible to add predicted results from an NMA model. This is one in which time-points within an interval (specified in overlay.nma) are “lumped” together to allow for analysis using standard NMA approaches (Lu and Ades 2004). Either a "random" (the default) or "common" effects NMA can be specified, and model fit statistics are reported below the resulting plot.

This can be useful to assess if the MBNMA predictions are in agreement with predictions from a lumped NMA model over a specific set of time-points, and can be a general indicator of the fit of the time-course model. However, it is important to note that the NMA model is not necessarily the more robust model, since it ignores potential differences in treatment effects that may arise from lumping time-points together. The wider the range specified in overlay.nma, the greater the effect of lumping and the stronger the assumption of similarity between studies.

The NMA predictions are plotted over the range specified in overlay.nma as a horizontal line, with the 95%CrI shown by a grey rectangle. The NMA predictions represent those for any time-points within this range since they lump together data at all these time-points. Predictions for treatments that are disconnected from the network reference treatment at data points specified within overlay.nma cannot be estimated so are not included.

# Overlay predictions from lumped NMA between 8-10 weeks follow-up
plot(pred, overlay.nma=c(8,10), n.iter=20000)
#> Reference treatment in plots is Pl_0

Consistency Testing

When performing an MBNMA by pooling relative treatment effects (pool="rel"), the modelling approach assumes consistency between direct and indirect evidence within a network. This is an incredibly useful assumption as it allows us to improve precision on existing direct estimates, or to estimate relative effects between treatments that have not been compared in head-to-head trials, by making use of indirect evidence.

However, if this assumption does not hold it is extremely problematic for inference, so it is important to be able to test it. A number of different approaches exist to allow for this in standard Network Meta-Analysis (Dias et al. 2013). Two of these have been implemented within MBNMAtime. It is important to note that in some model specifications there is likely to be sharing of model parameters (e.g. heterogeneity parameters, correlation coefficients) across networks which will lead to more conservative tests for consistency, and may lead to an inflated type II error.

Consistency is also likely to differ depending on the model used. Failing to appropriately model the time-course function may in fact induce inconsistency in the data. “Lumping” together different time points from studies in standard NMA is known to be a potential cause of inconsistency, which is one of the reasons why accounting for time-course using MBNMA is important (Pedder et al. 2019). When performing MBNMA, this is why it is important to first try to identify the best model possible in terms of time-course and common/random effects, and then to test for consistency within that model, rather than testing for consistency in models that are known not be be a good fit to the data.

Consistency testing can only be performed in networks in which closed loops of treatment comparisons exist that are drawn from independent sources of evidence. In networks which do not have any such loops of evidence, consistency cannot be formally tested (though it may still be present). The mb.nodesplit.comparisons() function identifies loops of evidence that conform to this property, and identifies a treatment comparison within that loop for which direct and indirect evidence can be compared using node-splitting (see below).

# Loops of evidence within the alogliptin dataset
splits.alog <- mb.nodesplit.comparisons(network.alog)
#>   t1 t2    path
#> 8  3  4 3->1->4
#> 7  2  5 2->1->5
#> 6  2  4 2->1->4
#> 5  2  3 2->1->3

Unrelated Mean Effects (UME) models

To check for consistency using UME we fit a model that does not assume consistency relationships, and that only models the direct relative effects between each arm in a study and the study reference treatment. If the consistency assumption holds true then the results from the UME model and the MBNMA will be very similar. However, if there is a discrepancy between direct and indirect evidence in the network, then the consistency assumption may not be valid, and the UME results are likely differ in several ways:

UME can be fitted to any time-course parameter which has been modelled using relative effects (pool="rel"). UME can be specified for each time-course parameter in separate analyses, or can be modelled all at once in a single analysis.

# Identify quantile for knot at 0.5 weeks
timequant <- 0.5/max(network.pain$data.ab$time)

# Fit a B-spline MBNMA with common relative effects on slope.1 and slope.2
mbnma <- mb.run(network.pain,
                fun=tspline(type="bs", knots=timequant,
                            pool.1 = "rel", method.1="common",
                            pool.2 = "rel", method.2="common"

# Fit a UME model on both spline coefficients simultaneously
ume <- mb.run(network.pain,
                fun=tspline(type="bs", knots=timequant,
                            pool.1 = "rel", method.1="common",
                            pool.2 = "rel", method.2="common"

# Fit a UME model on the 1nd coefficient only
ume.slope.1 <- mb.run(network.pain,
                fun=tspline(type="bs", knots=timequant,
                            pool.1 = "rel", method.1="common",
                            pool.2 = "rel", method.2="common"

# Fit a UME model on the 2nd coefficient only
ume.slope.2 <- mb.run(network.pain,
                fun=tspline(type="bs", knots=timequant,
                            pool.1 = "rel", method.1="common",
                            pool.2 = "rel", method.2="common"
#> [1] "Deviance for mbnma: -110.54"
#> [1] "Deviance for ume on beta.1 and beta.2: -118.16"
#> [1] "Deviance for ume on beta.1: -117.51"
#> [1] "Deviance for uyme on beta.2: -118.04"

By comparing the deviance (or residual deviance) of models with UME fitted on different time-course parameters and the MBNMA model, we can see that there is some reduction in deviance in the different UME models. Given that deviance is lowest when UME is modelled only on beta.1 this is suggestive of inconsistency between direct and indirect evidence on beta.1, but perhaps also on beta.2 given that modelling UME on this also leads to a reduction in deviance.

Direct estimates from UME and MBNMA models can also be compared to examine in greater detail how inconsistency may be affecting results. However, it is important to note that whilst a discrepancy between UME and MBNMA results may be seen for a particular relative effect, the inconsistency is not exclusively applicable to that particular treatment comparison and may originate from other comparisons in the network. This is why consistency checking is so important, as a violation of the consistency assumption raises concerns about estimates for all treatments within the network.


Another approach for consistency checking is node-splitting. This splits contributions for a particular treatment comparison into direct and indirect evidence, and the two can then be compared to test their similarity. mb.nodesplit() takes similar arguments to mb.run() that define the underlying MBNMA model in which to test for consistency, and returns an object of class("mb.nodesplit"). There are two additional arguments required:

comparisons indicates on which treatment comparisons to perform a node-split. The default value for this is to automatically identify all comparisons for which both direct and indirect evidence contributions are available using mb.nodesplit.comparisons().

nodesplit.parameters indicates on which time-course parameters to perform a node-split. This can only take time-course parameters that have been assigned relative effects in the model (pool="rel"). Alternatively the default "all" can be used to split on all available time-course parameters in the model that have been pooled using relative effects.

As up to two models will need to be run for each treatment comparison to split, this function can take some time to run.

# Nodesplit using an Emax MBNMA
nodesplit <- mb.nodesplit(network.pain,
                          fun=temax(pool.emax="rel", method.emax = "random",
                                    pool.et50="abs", method.et50 = "common"),
#> ========================================
#> Node-splitting analysis of inconsistency
#> ========================================
#> emax
#> |Comparison        | p-value| Median|   2.5%| 97.5%|
#> |:-----------------|-------:|------:|------:|-----:|
#> |Ro_25 vs Ce_200   |   0.440|       |       |      |
#> |-> direct         |        |  0.368| -0.419| 1.183|
#> |-> indirect       |        |  0.466| -0.317| 1.217|
#> |                  |        |       |       |      |
#> |Na_1000 vs Ce_200 |   0.357|       |       |      |
#> |-> direct         |        |  0.219| -0.204| 0.688|
#> |-> indirect       |        |  0.403| -0.367| 1.176|
#> |                  |        |       |       |      |

Performing the print() method on an object of class("mb.nodesplit") prints a summary of the node-split results to the console, whilst the summary() method will return a data frame of posterior summaries for direct and indirect estimates for each split treatment comparison and each time-course parameter.

It is possible to generate different plots of each node-split comparison using plot():

# Plot forest plots of direct and indirect results for each node-split comparison
plot(nodesplit, plot.type="forest")

# Plot posterior densities of direct and indirect results for each node-split comparisons
plot(nodesplit, plot.type="density")

As a further example, if we use a different time-course function (exponential) that is a less good fit for the data, and perform a node-split on the rate time-course parameter, we find that there seems to be a strong discrepancy between direct and indirect estimates. This is strong evidence to reject the consistency assumption, and to either (as in this case) try to identify a better fitting model, or to re-examine the dataset to try to explain whether differences in studies making different comparisons may be causing this.

This highlights the importance of testing for consistency after identifying an appropriate time-course and common/random treatment effects model.

# Nodesplit on rate using an exponential MBNMA
ns.exp <- mb.nodesplit(network.pain,
                       fun=texp(pool.rate = "rel", method.rate="common"),
#> ========================================
#> Node-splitting analysis of inconsistency
#> ========================================
#> rate
#> |Comparison        | p-value| Median|   2.5%| 97.5%|
#> |:-----------------|-------:|------:|------:|-----:|
#> |Ro_25 vs Ce_200   |   0.152|       |       |      |
#> |-> direct         |        |  0.153| -0.200| 0.513|
#> |-> indirect       |        |  0.365|  0.150| 0.576|
#> |                  |        |       |       |      |
#> |Na_1000 vs Ce_200 |   0.000|       |       |      |
#> |-> direct         |        | -0.036| -0.176| 0.103|
#> |-> indirect       |        |  0.385|  0.200| 0.576|
#> |                  |        |       |       |      |

plot(ns.exp, plot.type="forest")


MBNMAtime provides a complete set of functions that allow for meta-analysis of longitudinal time-course data and plotting of a number of informative graphics. Functions are provided for ranking, prediction, and for assessing consistency when modelling using relative effects. By accounting for time-course in meta-analysis this can help to explain heterogeneity/inconsistency that may arise when lumping together different time-points using conventional NMA.

The package allows for flexible modelling of either relative or absolute effects interchangeably on different time-course parameters within the same analysis, whilst providing a straightforward syntax with which to define these models.


Dias, S., and A. E. Ades. 2016. “Absolute or Relative Effects? Arm-Based Synthesis of Trial Data.” Journal Article. Res Synth Methods 7 (1): 23–28. https://doi.org/10.1002/jrsm.1184.

Dias, S., N. J. Welton, A. J. Sutton, D. M. Caldwell, G. Lu, and A. E. Ades. 2013. “Evidence Synthesis for Decision Making 4: Inconsistency in Networks of Evidence Based on Randomized Controlled Trials.” Journal Article. Med Decis Making 33 (5): 641–56. https://doi.org/10.1177/0272989X12455847.

Gelman, A., J. B. Carlin, H. S. Stern, and D. B. Rubin. 2003. Bayesian Data Analysis. Book. 2nd ed. CRC Press.

Hong, H., H. Chu, J. Zhang, and B. P. Carlin. 2016. “A Bayesian Missing Data Framework for Generalized Multiple Outcome Mixed Treatment Comparisons.” Journal Article. Res Synth Methods 7 (1): 6–22. https://doi.org/10.1002/jrsm.1153.

JAGS Computer Program. 2017. https://mcmc-jags.sourceforge.io/.

Jansen, J. P., M. C. Vieira, and S. Cope. 2015. “Network Meta-Analysis of Longitudinal Data Using Fractional Polynomials.” Journal Article. Stat Med 34 (15): 2294–2311. https://doi.org/10.1002/sim.6492.

Karabis, A., L. Lindner, M. Mocarski, E. Huisman, and A. Greening. 2013. “Comparative Efficacy of Aclidinium Versus Glycopyrronium and Tiotropium, as Maintenance Treatment of Moderate to Severe Copd Patients: A Systematic Review and Network Meta-Analysis.” Journal Article. Int J Chron Obstruct Pulmon Dis 8: 405–23. https://doi.org/10.2147/COPD.S48967.

Karahalios, A. E., G. Salanti, S. L. Turner, G. P. Herbison, I. R. White, A. A. Veroniki, A. Nikolakopoulou, and J. E. McKenzie. 2017. “An Investigation of the Impact of Using Different Methods for Network Meta-Analysis: A Protocol for an Empirical Evaluation.” Journal Article. Syst Rev 6 (1): 119. https://doi.org/10.1186/s13643-017-0511-x.

Langford, O., J. K. Aronson, G. van Valkenhoef, and R. J. Stevens. 2016. “Methods for Meta-Analysis of Pharmacodynamic Dose-Response Data with Application to Multi-Arm Studies of Alogliptin.” Journal Article. Stat Methods Med Res. https://doi.org/10.1177/0962280216637093.

Lu, G., and A. E. Ades. 2004. “Combination of Direct and Indirect Evidence in Mixed Treatment Comparisons.” Journal Article. Stat Med 23 (20): 3105–24. https://doi.org/10.1002/sim.1875.

Pedder, H., M. Boucher, S. Dias, M. Bennetts, and N. J. Welton. 2020. “Performance of Model-Based Network Meta-Analysis (Mbnma) of Time-Course Relationships: A Simulation Study.” Journal Article. Research Synthesis Methods 11 (5): 678–97. https://doi.org/10.1002/jrsm.1432.

Pedder, H., S. Dias, M. Bennetts, M. Boucher, and N. J. Welton. 2019. “Modelling Time-Course Relationships with Multiple Treatments: Model-Based Network Meta-Analysis for Continuous Summary Outcomes.” Journal Article. Res Synth Methods 10 (2): 267–86.

PennState Eberly College of Science. n.d. “"Other Covariance Structures" Apr. 8, 2021.” https://online.stat.psu.edu/stat502/lesson/10/10.3.

Plummer, M. 2008. “Penalized Loss Functions for Bayesian Model Comparison.” Journal Article. Biostatistics 9 (3): 523–39. https://pubmed.ncbi.nlm.nih.gov/18209015/.

———. 2017. JAGS User Manual. Version 4.3.0. https://people.stat.sc.edu/hansont/stat740/jags_user_manual.pdf.

Sinharay, S. 2003. “Assessing Convergence of the Markov Chain Monte Carlo Algorithms: A Review.” RR-03-07. ETS Research Report Series. https://onlinelibrary.wiley.com/doi/pdf/10.1002/j.2333-8504.2003.tb01899.x.

Spiegelhalter, D. J., N. G. Best, B. P. Carlin, and A. van der Linde. 2002. “Bayesian Measures of Model Complexity and Fit.” Journal Article. J R Statistic Soc B 64 (4): 583–639.

Tallarita, M., M. De lorio, and G. Baio. 2019. “A Comparative Review of Network Meta-Analysis Models in Longitudinal Randomized Controlled Trial.” Journal Article. Statistics in Medicine 38 (16): 3053–72. https://doi.org/10.1002/sim.8169.