Introducing the {KMunicate} package

· 5 minutes read

A few weeks ago, Tim Morris launched a competition on Twitter challenging his followers to write some code to create KMunicate-style Kaplan-Meier plots. I (obviously) took on the challenge, and I must admit: I got slightly carried away… hence now introducing the {KMunicate} R package.

{KMunicate} is now on CRAN, and the development version lives on my GitHub profile. You can install the CRAN version as usual:

1
install.packages("KMunicate")

Alternatively, you can install the dev version of {KMunicate} from GitHub with:

1
2
# install.packages("devtools")
devtools::install_github("ellessenne/KMunicate-package")

What’s the KMunicate style?

KMunicate-style Kaplan-Meier plots include confidence intervals for each fitted curve and an extended table beneath the main plot including the number of individuals at risk at each time and the cumulative number of events and censoring events. Here’s an example from the KMunicate study itself:

Example of KMunicate-style plot from the KMunicate study

As you might imagine, that’s quite a lot of work to produce a plot like that.

Well, not anymore!

The {KMunicate} package lets you create such a plot with a single line of code. Isn’t that great?

Let’s illustrate the basic functionality of {KMunicate} with an example. We’ll be using once again data from the German breast cancer study, which is conveniently bundled with {KMunicate}:

1
2
data(brcancer, package = "KMunicate")
head(brcancer)
##   id hormon x1 x2 x3 x4 x5 x6  x7 rectime censrec x4a x4b        x5e
## 1  1      0 70  2 21  2  3 48  66    1814       1   1   0 0.69767630
## 2  2      1 56  2 12  2  7 61  77    2018       1   1   0 0.43171051
## 3  3      1 58  2 35  2  9 52 271     712       1   1   0 0.33959553
## 4  4      1 59  2 17  2  4 60  29    1807       1   1   0 0.61878341
## 5  5      0 73  2 35  2  1 26  65     772       1   1   0 0.88692045
## 6  6      0 32  1 57  3 24  0  13     448       1   1   1 0.05613476

The survival time is in rectime, and the event indicator variable is censrec; the treatment variable is hormon, a binary covariate.

First, we fit the survival curve by treatment arm using the Kaplan-Meier estimator:

1
2
3
library(survival)
fit <- survfit(Surv(rectime, censrec) ~ hormon, data = brcancer)
fit
## Call: survfit(formula = Surv(rectime, censrec) ~ hormon, data = brcancer)
## 
##            n events median 0.95LCL 0.95UCL
## hormon=0 440    205   1528    1296    1814
## hormon=1 246     94   2018    1918      NA

The plot that can be obtained via the plot method is ok but needs a bit of work to be good enough for a publication. For instance, this is the default:

1
plot(fit)

No bueno, right? Let’s improve it a bit:

1
2
3
4
5
plot(fit, col = 1:2, lty = 1:2, conf.int = TRUE)
legend("bottomleft",
  col = 1:2, lty = 1:2,
  legend = c("Control", "Treatment"), bty = "n"
)

This is better, but still not great: the area defined by the confidence intervals is not shaded, and there is still no risk table.

Here’s when the {KMunicate} package comes to the rescue. First, we need to define the breaks for the x-axis; the risk table with be computed at those breaks. Say we want breaks every year:

1
2
time_breaks <- seq(0, max(brcancer$rectime), by = 365)
time_breaks
## [1]    0  365  730 1095 1460 1825 2190 2555

Then, all we have to do is to

1
2
3
4
library(ggplot2)
library(KMunicate)

KMunicate(fit = fit, time_scale = time_breaks)

Easy peasy!

We might want to get proper arm labels too:

1
2
3
4
brcancer$hormon <- factor(brcancer$hormon, levels = 0:1, labels = c("Control", "Treatment"))
fit <- survfit(Surv(rectime, censrec) ~ hormon, data = brcancer)

KMunicate(fit = fit, time_scale = time_breaks)

Nice. Next, we’ll show how to customise the plot.

Customising KMunicate-style plots

First, we might want to customise colours to use a colour-blind friendly palette via the .color_scale and .fill_scale arguments:

1
2
3
4
5
6
KMunicate(
  fit = fit,
  time_scale = time_breaks,
  .color_scale = ggplot2::scale_colour_brewer(type = "qual", palette = "Dark2"),
  .fill_scale = ggplot2::scale_fill_brewer(type = "qual", palette = "Dark2")
)

Then, we might want to use a custom font, such as my latest obsession Victor Mono, via the .ff argument:

1
2
3
4
5
6
7
KMunicate(
  fit = fit,
  time_scale = time_breaks,
  .color_scale = ggplot2::scale_colour_brewer(type = "qual", palette = "Dark2"),
  .fill_scale = ggplot2::scale_fill_brewer(type = "qual", palette = "Dark2"),
  .ff = "Victor Mono"
)

Finally, we customise the overall theme using e.g. theme_minimal from the {ggplot2} package:

1
2
3
4
5
6
7
8
KMunicate(
  fit = fit,
  time_scale = time_breaks,
  .color_scale = ggplot2::scale_colour_brewer(type = "qual", palette = "Dark2"),
  .fill_scale = ggplot2::scale_fill_brewer(type = "qual", palette = "Dark2"),
  .ff = "Victor Mono",
  .theme = ggplot2::theme_minimal(base_family = "Victor Mono")
)

When overriding the default theme, we need to re-define the font for the main plot using the base_family argument of a theme_* component. Overall, I think this is a much better plot!

Exporting plots

The final step consists of exporting a plot for later use, e.g. in manuscripts or presentations. That’s straightforward, being the output of KMunicate() a ggplot2-type object: all we have to do is use the ggplot2::ggsave function, e.g. in the next block of code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
p <- KMunicate(
  fit = fit,
  time_scale = time_breaks,
  .color_scale = ggplot2::scale_colour_brewer(type = "qual", palette = "Dark2"),
  .fill_scale = ggplot2::scale_fill_brewer(type = "qual", palette = "Dark2"),
  .ff = "Victor Mono",
  .theme = ggplot2::theme_minimal(base_family = "Victor Mono")
)

ggplot2::ggsave(p, filename = "export.png", height = 6, width = 6, dpi = 300)

Closing remarks

Further details on {KMunicate} can be found on its website, with more examples and a better explanation of the different arguments and customisation options. Let me know if you find the package useful, and if you find any bug (I’m sure there’ll be some) please file an issue on GitHub.

And what about Tim’s challenge that led to the inception of {KMunicate}, you might ask? Well, I got myself a beautiful hand-crafted wooden spoon:

Hand-crafted wooden spoon

Isn’t that great!?