MetaInsight v7

Making a comprehensive app for network meta-analysis reproducible

Simon Smart

20th April 2026

Overview of the seminar

  • What is network meta-analysis?
  • History of MetaInsight
  • Demonstration of new features
  • How challenges were addressed

About me

  • Software developer in Biostatistics group at University of Leicester
  • Background in agricultural science
  • Developing Shiny apps since 2018

Network meta-analysis can help determine the ‘best’ treatment option

Network of six treatments where there are different numbers of trials that compared each pair

MetaInsight provides frequentist and Bayesian methods

Summary forest plot summarising frequentist results

MetaInsight provides frequentist and Bayesian methods

Line graph showing the output of an analysis where each treatment is ranked by outcome

Sensitivity analyses can be run in parallel to the main analysis

Baseline risk meta-regression considers the initial health of participants

  • Compare reference treatment arms

Forest plot showing the treatment effects after controlling for baseline risk

Covariate meta-regression evaluates the effect of covariates

  • Does the age of participants affect the outcome?

Regression plot showing the effect of age on the response. The lines are horizontal indicating there is no effect.

Complex meta-analyses require extensive statistical and programming knowledge

  • BUGSnet, bnma, coda, gemtc, meta, metafor, netmeta
  • JAGS may be hard to install
  • Require data in different formats and use different terminology

Examples of different terminology

  • netmeta requires data in a ‘wide’ format while gemtc requires it in a ‘long’ format
  • netmeta uses random and common while gemtc uses random and fixed
  • bnma uses common and independent when gemtc uses shared and unrelated

Shiny apps enable anybody to access the power of R


ui <- fluidPage(
  numericInput("number", "Enter a number", value = 5),
  textOutput("answer")
)

server <- function(input, output) {
  output$answer <- renderText(input$number * 10)
}

shinyApp(ui, server)


A screenshot of a basic shiny app displaying an input box to enter a number in and an output displaying the inputted number multiplied by ten.

Shiny removes barriers for accessing cutting-edge methods

  • MetaInsight was developed to make NMA more accessible
  • Originally developed by statisticians, but increasingly in collaboration with developers

Line graph showing cumulative citations of Owen 2019 since 2020, now a total of 300

A history of MetaInsight

  • 2017: Development begins as a 3 month PhD placement
  • 2018: App launched featuring frequentist methods
  • 2019: Bayesian analysis and ‘long’ data formats added
  • 2020: Binary and continuous outcomes merged into a single app
  • 2021: Code moved to GitHub
  • 2023: Bayesian ranking panel added and code reorganised
  • 2024: Meta-regression added

MetaInsight has a global userbase

Screenshot of the existing app

The ‘black box’ nature of apps can limit uptake

  • Open science principles require that code can be rerun
  • NICE require that NMAs are reproducible

Screenshot of NICE report showing WinBUGS code

shinyscholar was developed to address this

  • shinyscholar was forked from wallace to make development of reproducible apps easier

Hex logo of shinyscholar showing a black and white mortar cap

shinyscholar was developed to address this

Screenshot of the updated app

Aims for version 7

  • Make analyses reproducible
  • Upload risk of bias and integrate with CiNeMA
  • Produce a downloadable report
  • Improve quality and consistency of downloaded plots
  • Add more automated tests
  • Maintain user experience

Screenshot of the existing app

Screenshot of the existing app

Various adjustments were made to the shinyscholar template

  • Multiple parallel analyses rather than a single linear analysis
  • Needed to maintain the functionality of excluding studies at any point
  • Opportunities to reuse code within and between modules

Functions manipulate data, modules control when functions are called

  • Convert core functionality into functions and package
  • App becomes an interface to the functions, dealing with interactivity
  • Separates the what from when
  • Functions are easier to test

The setup tab is equivalent to the ‘Load Data’ tab

  • setup_load: checks that data is valid
  • setup_configure: choose fixed options
  • setup_exclude: rerun with studies excluded

Once configured results can be generated

  • Summary and Frequentist use the configured data directly
  • Bayesian and Meta-regression tabs have a model function
  • Model results are then used to generate outputs

Incorporating risk of bias scores improves sensitivity analyses

  • During reviews, risk of bias information can be collected e.g.
    • Randomisation, blinding, missing data
  • Scores can guide sensitivity analyses

Risk of data can now be uploaded as part of the dataset

  • Name columns as rob.<name> or rob and indirectness
  • New plot summarises study results and RoB data

Forest plot summarising study results and showing risk of bias scores

Integration with CINeMA helps to evaluate confidence in findings

Cinema logo

  • Uses risk of bias scores for studies to evaluate evidence for treatments

Network plot as in the first slide but with the nodes between each treatment pair coloured according to the risk of bias

Uploaded datasets can create difficulties

  • Contain errors which could crash the app
  • Vary in the number of studies and treatments
  • Treatment and study names can vary in length

Uploaded datasets can create difficulties

Screenshot showing existing interface for excluding studies from an analysis

Previously studies were excluded by clicking in the sidebar

Screenshot showing existing interface for excluding studies from an analysis

A new graphical interface has been developed to exclude studies

  • The table has been replaced with the summary forest plot but which is interactive
  • Clicking on a row excludes or includes the study

Reactivity is a blessing and a curse

  • Reactivity controls when code is rerun
  • As complexity grows, it can be difficult to control and code can rerun unnecessarily
  • Previously Bayesian models had to be rerun when manually when settings changed

Reproducibility requires explicit decisions on what is run

  • Modules run when button is pressed, either with the mouse or Enter
  • Once pressed, they update automatically
  • Within the sections, a run all option runs all of them
  • If they can’t yet be run, an error message takes you to the relevant module

Slow-running tasks run in the background

  • Fitting models can block the app for other users
  • Spinner appears whilst waiting
  • Can be cancelled
  • Uses shiny::ExtendedTask() and mirai::mirai()

Producing publication-ready figures

  • Ideally plots can be downloaded and used without editing
  • Reuse plots from other packages, others custom-built
  • Mixture of base plots, grid and ggplot2

Screenshot of the existing app

Screenshot of the existing app

Screenshot of the existing app

Screenshot showing forest plots that are cutoff on small screens

Some downloaded plots had wide margins and low resolution

Forest plot downloaded from existing app showing wide white borders and low resolution

Adjusting plots to always fit is challenging

  • Dimensions need to be adjusted depending on dataset
  • Space available to display depends on screen size
  • Information in plot margins takes up a fixed number of pixels

Plots are now all produced using a consistent workflow

  • Functions generate SVGs
  • In app they squash and stretch freely
  • Enables fullscreen viewing
  • Can all be rendered to png, pdf or saved as svg
  • Enables testing of reproducibility

Reproducibility relies on a strict structure

  • Each module has an id made up of the component and module summary_network
  • Each calls a synonymous function summary_network()
  • Input values are stored in common$meta$summary_network$<input id>
  • Values are knitted into an .Rmd chunk and combined to create a .qmd

Reproducibility relies on a strict structure

```{asis, echo = {{summary_network_knit}, eval = {{summary_network_knit}}, include = {{summary_network_knit}}}}
### Display the networks for the original data and data with excluded studies.
{r,  results = 'asis'}
```
```{r, echo = {{summary_network_knit}, include = {{summary_network_knit}}}}
summary_network(configured_data,
                {{summary_network_style}}, 
                {{summary_network_label_all}}, 
                "Network plot of all studies")
```
### Display the networks for the original data and data with excluded studies.

```{r, results = 'asis'}
summary_network(configured_data, 
                "netplot", 
                1, 
                "Network plot of all studies")
```

Reproducibility also enables improved reporting

  • Use as the basis for writing a publication
  • Rendered in the app to produce an html report

Screenshot of the an html report produced by the app

Reproducibility also enables improved reporting

---
title: |
 ![](https://raw.githubusercontent.com/CRSU-Apps/MetaInsight/main/www/images/MetaInsightLogo.png){width=100px} MetaInsight v7.0.0 Session 2026-04-19
format:
  html:
    embed-resources: true
    code-overflow: wrap
    code-fold: true
    code-summary: "View code chunk"
    page-layout: article
    grid:
      body-width: 1200px
fig-format: svg
execute:
  warning: false
  message: false
theme: spacelab
---

::: {.panel-tabset}

## Introduction

Please find below the R code history from your *metainsight* v7.0.0
session.

You can reproduce your session results by running this quarto file in
RStudio.

Each code block is called a “chunk”, and you can run them either
one-by-one or all at once by choosing an option in the “Run” menu at the
top-right corner of the “Source” pane in RStudio. The file can also be
rendered into an html file using `quarto::quarto_render()` which will
contain all of the outputs alongside the code used to generate them.

For more detailed information see <https://quarto.org/>.

### Package installation

metainsight is an R package and it must be installed and loaded before
running any code. This can take a considerable time depending on your
computing environment. Some analyses use JAGS and you should install
this before installing the package: <https://mcmc-jags.sourceforge.io/>

```{r}
if(!require("metainsight")) remotes::install_github("CRSU-Apps/MetaInsight", ref = "shinyscholar")
library(metainsight)
```

The *metainsight* session code .qmd file is composed of a chain of
module functions that are internal to *metainsight*. Each of these
functions corresponds to a single module that the user ran during the
session. Users are encouraged to write custom code in the .qmd directly
to modify their analysis, and even modify the module function code to
further customize. To see the source code for any module function, just
type its name into the R console and press Return.

Your analyses can be found in the tabs above.

------------------------------------------------------------------------

## Setup

### Read the data, assemble into loaded data in and display a table

```{r}
raw_data <- structure(list(Study = c("Study01", "Study01", "Study02", "Study02", "Study03", "Study03", "Study04", "Study04", "Study05", "Study05", "Study06", "Study06", "Study07", "Study07", "Study08", "Study08", "Study09", "Study09", "Study10", "Study10", "Study11", "Study11", "Study12", "Study12", "Study13", "Study13", "Study14", "Study14", "Study15", "Study15", "Study16", "Study16", "Study17", "Study17", "Study18", "Study18", "Study19", "Study19", "Study20", "Study20", "Study21", "Study21", "Study22", "Study22", "Study23", "Study23", "Study24", "Study24", "Study25", "Study25", "Study26", "Study26", "Study27", "Study27", "Study28", "Study28", "Study29", "Study29", "Study30", "Study30", "Study31", "Study31", "Study32", "Study32", "Study33", "Study33", "Study34", "Study34", "Study35", "Study35", "Study36", "Study36", "Study37", "Study37", "Study38", "Study38", "Study39", "Study39", "Study40", "Study40", "Study41", "Study41", "Study42", "Study42", "Study43", "Study43", "Study44",
"Study44", "Study45", "Study45"), T = c("Placebo", "Glucocorticoids", "Placebo", "Glucocorticoids", "Placebo", "Glucocorticoids", "Placebo", "Glucocorticoids", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Ketamine", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo",
"Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids", "Placebo", "Gabapentinoids"), Mean = c(2.2, 1.9, 2, 2, 1.4, 1.4, 2.9, 2.6, 5.4, 3, 1, 1, 0, 0, 0, 0, 4, 4, 0.3, 1.1, 0.5, 2, 1.3, 1.4, 0, 0, 1.5, 0.3, 1, 0.7, 4, 4.3, 2, 2, 1.5, 0, 3.4, 2.4, 1, 0.5, 0.1, 0.2, 0.8, 0.8, 3.4, 3.1, 0, 0, 0.6, 0.2, 0.6, 0.5, 2.2, 2.6, 2.5, 3, 1.2, 0.5, 2.2, 2.1, 0.2, 0.1, 2, 1, 1.1, 0.9, 3, 2, 2, 1.8, 3.4, 2.5, 1.9, 1.5, 0, 0, 3, 2, 1.5, 1.1, 0.5, 1, 0, 0, 1.5, 1.3, 0.03, 0.4, 1.2, 1.4), SD = c(2.5, 2.4, 3.7, 2.8, 1.6, 1.7, 2.5, 2.4, 3.6, 3.5, 1.5, 1.5, 0.2, 0.7, 0.1, 0.3, 3.3, 2.2, 0.7, 2.1, 3.7, 1.7, 1.5, 1.4, 1.9, 0.7, 4.4, 3, 1.7, 1.4, 2.1, 2.5, 1.5, 1.5, 2.2, 1.5, 3, 1.1, 1.8, 1.7, 0.7, 0.7, 1.1, 0.9, 1.6, 1.7, 0.7, 0.7, 0.9, 0.4, 0.1, 0.9, 1.4, 1.7, 1, 0.8, 1.5, 0.9, 0.9, 1.2, 0.1, 0.1, 1.5, 0.6, 1.5, 1.2, 1.9, 1.5, 2.5, 2, 1.4,
1.2, 2, 1.9, 0.7, 2.1, 0.8, 0.5, 2, 1.6, 1.5, 1.5, 0.7, 0.7, 2.8, 2.2, 0.2, 0.9, 1.7, 1.7), N = c(50L, 62L, 45L, 92L, 88L, 78L, 47L, 46L, 52L, 43L, 84L, 84L, 34L, 33L, 35L, 34L, 66L, 73L, 19L, 18L, 6L, 8L, 47L, 31L, 24L, 25L, 17L, 15L, 35L, 33L, 42L, 49L, 62L, 118L, 22L, 22L, 7L, 8L, 25L, 55L, 38L, 39L, 30L, 31L, 10L, 21L, 53L, 65L, 20L, 20L, 35L, 27L, 22L, 23L, 4L, 2L, 30L, 60L, 50L, 50L, 20L, 20L, 29L, 30L, 24L, 17L, 49L, 50L, 28L, 83L, 100L, 100L, 24L, 22L, 48L, 52L, 31L, 62L, 20L, 18L, 18L, 16L, 46L, 46L, 27L, 27L, 35L, 70L, 76L, 75L), covar.age = c(45, 45, 75.5, 75.5, 71.3, 71.3, 54.5, 54.5, 56, 56, 50.2, 50.2, 64.5, 64.5, 60.2, 60.2, 66.5, 66.5, 60, 60, 55.3, 55.3, 49.5, 49.5, 38.5, 38.5, 68.8, 68.8, 64.8, 64.8, 47.6, 47.6, 57, 57, 66, 66, 61.7, 61.7, 52.5, 52.5, 48.3, 48.3, 63.8, 63.8, 60.2, 60.2, 60.2, 60.2, 60.1, 60.1, 41.5, 41.5, 65, 65, 34.5, 34.5, 47.7, 47.7, 43.5, 43.5, 56.9, 56.9, 24, 24, 40, 40, 62.4, 62.4, 66.5, 66.5, 50.9, 50.9, 43.5, 43.5, 50.5, 50.5, 67.1, 67.1,
39.1, 39.1, 70.3, 70.3, 68.5, 68.5, 52.1, 52.1, 43.1, 43.1, 62.9, 62.9), rob = c(3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 2L, 2L, 3L, 3L, 3L, 3L, 2L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 2L, 2L, 3L, 3L, 3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 2L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 2L, 2L, 2L, 2L, 2L, 2L, 3L, 3L, 3L, 3L), indirectness = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), rob.randomisation = c(1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L,
1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), rob.allocation_concealment = c(1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L), `rob.blinding_(participants_and_personnel)` = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 1L, 1L,
1L, 1L), `rob.blinding_(outcomes)` = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), rob.attrition = c(3L, 3L, 1L, 1L, 1L, 1L, 3L, 3L, 3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 1L, 1L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 3L, 3L, 1L, 1L, 3L, 3L), rob.reporting = c(3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 3L, 3L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L,
2L, 2L, 3L, 3L, 1L, 1L, 2L, 2L, 3L, 3L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 3L, 3L, 2L, 2L, 2L, 2L, 2L, 2L, 3L, 3L, 2L, 2L, 2L, 2L, 3L, 3L, 3L, 3L, 2L, 2L, 2L, 2L), rob.other_bias = c(1L, 1L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 1L, 1L, 3L, 3L, 3L, 3L, 1L, 1L, 3L, 3L, 3L, 3L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 3L, 3L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 3L, 3L, 3L, 3L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 3L, 3L, 2L, 2L, 3L, 3L, 2L, 2L, 1L, 1L, 3L, 3L)), row.names = c(NA, 90L), class = "data.frame")
treatments <- structure(list(Number = 1:4, Label = c("Placebo", "Glucocorticoids", "Ketamine", "Gabapentinoids")), class = "data.frame", row.names = c(NA, -4L))
outcome <- "continuous"

loaded_data <- list(data = raw_data,
                    treatments = treatments,
                    outcome = outcome,
                    is_data_valid = TRUE)
class(loaded_data) <- "loaded_data"

DT::datatable(raw_data, 
              options = list(
                scrollX = TRUE,
                autoWidth = FALSE)
              )
```

### Configure the analysis and process the data into suitable formats for analysis

```{r}
uncleaned_reference_treatment <- "Placebo"
ranking_option <- "good"
outcome_measure <- "MD"
model_effects <- "random"
seed <- 370

configured_data <- setup_configure(loaded_data, 
                                   uncleaned_reference_treatment, 
                                   model_effects,
                                   outcome_measure,
                                   ranking_option,
                                   seed)

knitr::kable(setup_configure_table(configured_data),
             row.names = TRUE,
             col.names = NULL,
             caption = "Analysis configuration")
```

### Exclude selected studies from the analysis

```{r}
excluded_studies <- c("Study09", "Study13", "Study16", "Study19", "Study21", "Study28", "Study30", "Study26", "Study24")
subsetted_data <- setup_exclude(configured_data, excluded_studies)
```

### Display summary forest plot showing excluded studies

```{r, results = 'asis'}

setup_exclude_plot(configured_data, excluded_studies)
```

## Summary

### Display the networks for the original data and data with excluded studies.

```{r, results = 'asis', out.extra='style=“height: 400px;”'}

network_style <- "netplot"

summary_network(configured_data, 
                network_style, 
                1, 
                "Network plot of all studies") 
```

```{r, results = 'asis'}

summary_network(subsetted_data, 
                network_style, 
                1, 
                "Network plot with selected \n studies excluded") 
```

### Display a table summarising the network characteristics of the original data and data with excluded studies.

```{r}
network_df_all <- make_netconnect(configured_data$freq)
network_df_sub <- make_netconnect(subsetted_data$freq)
network_df_compare <- data.frame(network_df_all, network_df_sub)
colnames(network_df_compare) <- c("All studies", "With selected studies excluded")
rownames(network_df_compare) <- c("Studies", "Pairwise comparisons", "Treatments", "Designs", "Subnetworks")

knitr::kable(network_df_compare, row.names = TRUE, caption = "Comparison of network characteristics")
```

## Frequentist

### Generate forest plots

```{r, results = 'asis'}

freq_forest(configured_data,
            -0.7,
            0.4,
            "Results for all studies") 
```

```{r, results = 'asis'}

freq_forest(subsetted_data,
            -0.7,
            0.5,
            "Results with selected studies excluded") 
```

,

## Bayesian

### Fit the Bayesian models

```{r}
bayes_model_all <- bayes_model(configured_data)
bayes_model_sub <- bayes_model(subsetted_data)
```

```{r}
dic_table(bayes_model_all$dic, "all")
```

```{r}
dic_table(bayes_model_sub$dic, "sub")
```

### Produce forest plots of the Bayesian models

```{r, results = 'asis'}

bayes_forest(bayes_model_all, 
             "All studies",
             xmin = -0.7,
             xmax = 0.5)
```

```{r, results = 'asis'}

bayes_forest(bayes_model_sub, 
             "With selected studies excluded",
             xmin = -0.8,
             xmax = 0.5)
```

## Baseline risk

### Fit the baseline risk model

```{r}
baseline_model <- baseline_model(get("configured_data"), "shared")

dic_table(baseline_model$dic)
```

### Produce forest plot of the baseline risk model

```{r, results = 'asis'}

baseline_forest(baseline_model, 
                xmin = -0.5,
                xmax = 0.4,
                title = "Baseline risk regression analysis")
```

## Covariate

### Fit the covariate model

```{r}
covariate_value <- 55
covariate_regressor <- "shared"
covariate_model <- covariate_model(get("configured_data"), 
                                   covariate_value, 
                                   covariate_regressor)

dic_table(covariate_model$dic)
```

### Produce forest plot of the covariate model

```{r, results = 'asis'}

covariate_forest(covariate_model, 
                 xmin = -0.8,
                 xmax = 0.5,
                 title = "Covariate regression analysis")
```

Reproducibility also enables improved reporting

Automated tests give us confidence that everything is working

  • As complexity increases, updating code can have unintended consequences
  • Already a lot of testing in place
  • Now the app is also run and all the buttons pressed on GitHub

Unit tests check that functions perform as expected

result <- setup_exclude(configured_data_con, c("Leo"))
expect_false("Leo" %in% result$connected_data$Study)

End-to-end tests that the app functions

app <- shinytest2::AppDriver$new(system.file("shiny", package = "metainsight"))
...
click_setup_exclude(app, "Leo")
exclusions <- app$get_value(input = "setup_exclude-exclusions")
expect_equal(exclusions, c("Leo"))

Analyses can also be saved to a file

  • Can be restored later on
  • Share with colleagues
  • Access the data / models for further analysis
  • Potential to spin up exact version of app in the future

The package will soon be available on CRAN

  • Simpler to install and run locally
  • Can use various tools for checking code
  • We will learn in advance if developers of the analytical packages make breaking changes

The app can also be run locally

install.packages("metainsight")
library(metainsight)
run_metainsight()

run_metainsight(load_file = "saved_file.rds")

Analyses can also be conducted without the app

configured_data <- setup_load("my_data.csv", outcome = "continuous") |>
  setup_configure(reference_treatment = "Placebo",
                  effects = "random",
                  outcome_measure = "MD",
                  ranking_option = "good",
                  seed = 123)

Analyses can also be conducted without the app

freq_forest(configured_data) |> 
  write_plot("frequentist_forest_plot.pdf")

bayes_model(configured_data) |> 
  bayes_forest() |>
  write_plot("bayesian_forest_plot.pdf")
  • Explained further in vignette

User feedback is important for developing new features

Github screenshot of user making feature request

Future plans

  • Incorporate guidance
  • Bulk downloads
  • More options for customisation - models and plots
  • More options for exclusions
  • Multiple outcomes or covariates

Acknowledgments

  • Naomi Bradbury, Ryan Field, Tom Morris, Clareece Nevill, Janion Nevill, Alex Sutton, Nicola Cooper, Suzanne Freeman
  • Wellcome (via Chan Zuckerburg Initiative)
  • NIHR
  • Email, Github, Bluesky

CRSU logo Wellcome logo NIHR logo