Kilim plot

Multi-outcome absolute effects table plot

NMA
A table of treatments by outcomes showing absolute effects, with cell colors for the strength and direction of evidence.
NMAProposed

Kilim plot example

Kilim plot for the antidepressant network: events per 1,000 patients (95% CI) for each treatment and outcome, assuming the median placebo-arm risk, with color showing the strength of evidence (two-sided p-value) for benefit (green) or harm (red) against placebo. Custom implementation. Data: netmeta::Linde2015.
Family
Treatment ranking
Purpose
Present absolute benefits and harms across outcomes with their statistical support.
Inputs
Relative effects and their uncertainty per outcome, and baseline risks.
Software
Custom ggplot2 (shown); original R code from the authors

What it shows

Seo and colleagues designed the Kilim plot, named after the patterned rugs it resembles, to report NMA results for many outcomes without resorting to rankings. Each cell gives an absolute effect (for example, events per 1,000 patients) with its interval, and its color encodes both the direction of the effect and how strong the evidence is. Readers weigh benefits against harms on the natural scale.

How to read it

  • Rows: treatments (here sorted by P-score for response).
  • Columns: outcomes.
  • Cell text: absolute risk per 1,000 with 95% CI.
  • Color: green for benefit, red for harm, intensity for strength of evidence (569Xlvalue against the reference).

Interpretation

Hypericum gives about 562 responders per 1,000 against 392 on placebo, with no excess dropout due to adverse events (23 per 1,000, the same as placebo). TCA, SSRI, and SNRI improve response and remission similarly, but roughly double dropout due to adverse events (43 to 54 per 1,000). NRI increases both dropout outcomes.

Pitfalls

  • Absolute effects depend on the assumed baseline risk; state it and consider several.
  • Coloring by p-value mixes effect size and precision; a large imprecise effect and a small precise one can look the same.
  • Multiple testing across many cells is implicit.

Code

library(netmeta)

# Antidepressants in primary care (Linde et al. 2015): arm-level binary outcomes
data(Linde2015)
d <- Linde2015
nma_for <- function(v, small) {
  p <- pairwise(treat = list(d$treatment1, d$treatment2, d$treatment3),
                event = list(d[[paste0(v, 1)]], d[[paste0(v, 2)]], d[[paste0(v, 3)]]),
                n = list(d$n1, d$n2, d$n3), studlab = d$id, sm = "OR", allstudies = TRUE)
  netmeta(p, common = FALSE, reference.group = "Placebo", small.values = small)
}
library(ggplot2)

outs <- list(Response = list(v = "resp", small = "undesirable", good = "high"),
             Remission = list(v = "remi", small = "undesirable", good = "high"),
             Dropout = list(v = "loss", small = "desirable", good = "low"),
             `Dropout (AE)` = list(v = "loss.ae", small = "desirable", good = "low"))

# Absolute risk per 1,000 on each treatment, assuming the median placebo-arm
# risk, and the p-value for each treatment versus placebo
cells <- do.call(rbind, lapply(names(outs), function(o) {
  s <- outs[[o]]
  net <- nma_for(s$v, s$small)
  arms <- data.frame(t = c(d$treatment1, d$treatment2, d$treatment3),
                     r = c(d[[paste0(s$v, 1)]], d[[paste0(s$v, 2)]], d[[paste0(s$v, 3)]]),
                     n = c(d$n1, d$n2, d$n3))
  arms <- arms[arms$t %in% "Placebo" & !is.na(arms$r), ]
  p0 <- median(arms$r / arms$n)
  lor <- net$TE.random[, "Placebo"]; se <- net$seTE.random[, "Placebo"]
  risk <- function(x) 1000 * plogis(qlogis(p0) + x)
  data.frame(outcome = o, treatment = names(lor),
             text = sprintf("%.0f\n(%.0f to %.0f)", risk(lor), risk(lor - 1.96 * se), risk(lor + 1.96 * se)),
             p = 2 * pnorm(-abs(lor / se)),
             better = if (s$good == "high") lor > 0 else lor < 0)
}))
cells$strength <- ifelse(cells$treatment == "Placebo", 0,
                         pmin(-log10(cells$p), 3) * ifelse(cells$better, 1, -1))
cells$text[cells$treatment == "Placebo"] <- sub("\n.*", "\n(reference)", cells$text[cells$treatment == "Placebo"])
cells$outcome <- factor(cells$outcome, levels = names(outs))
ord <- netrank(nma_for("resp", "undesirable"))$ranking.random
cells$treatment <- factor(cells$treatment, levels = names(sort(ord)))

ggplot(cells, aes(outcome, treatment, fill = strength)) +
  geom_tile(colour = "white", linewidth = 1) +
  geom_text(aes(label = text), size = 2.9, lineheight = 0.9) +
  scale_fill_gradient2(low = "#b5452b", mid = "#f7f5f0", high = "#2a7f62", limits = c(-3, 3),
                       breaks = c(-3, -2, -1.3, 0, 1.3, 2, 3),
                       labels = c("p<.001 worse", ".01", ".05", "", ".05", ".01", "p<.001 better"),
                       name = "Evidence vs placebo") +
  scale_x_discrete(position = "top") +
  labs(x = NULL, y = NULL, title = "Kilim plot",
       subtitle = "Events per 1,000 patients (95% CI); color shows strength and direction of evidence") +
  theme(panel.grid = element_blank(), legend.position = "right",
        axis.text = element_text(size = 10, colour = "#1b1f24"))

References

  • Seo M, Furukawa TA, Veroniki AA, et al. The Kilim plot: a tool for visualizing network meta-analysis results for multiple outcomes. Res Synth Methods. 2021;12:86-95. doi:10.1002/jrsm.1428