StatsFragments

Python, R, Rust, 統計, 機械学習とか

R {ggplot2} で独自の geom を手軽に作りたい


重要 このエントリは {ggplot2} 1.1.0 以前の情報です。v2.0.0 以降の方法は vignettes "Extending ggplot2" を読んでください。


はじめに

{ggplot2} を使っていると、新しい描画図形 (geom) を作りたいなという場合がたまにある。その方法は {ggplot2} の Wiki に書いてあり、手順は、

  1. 描画図形の実体を grid::Grob として定義する (リンク先の例では fieldGrob )
  2. 定義した Grob を 呼び出す描画用クラスを ggplot2:::Geom を継承して作る (リンク先の例では GeomField )
  3. 描画用関数 geom_xxx を作る (リンク先の例では geom_field )

手順のうち 新しい Grob を作るのは少し面倒な感じだ。が、作りたい geom が 既存 {ggplot2} 関数へのちょっとした追加処理や 組み合わせで表現できる場合には新しい Grob を作る必要もなく、わりと手軽にできる。ここではその方法を書く。

やりたいこと

ggplot2::geom_ribbon を階段状に描画したい。背景はこちら。

1. 既存の描画用クラス (Geom) を継承して作る

ribbon を階段状に描画するには、geom_ribbon の描画直前に データを階段状に変換する必要がある。他は geom_ribbon 同様に動けばよいので、描画用のクラスは ggplot2:::Geom ではなく ggplot2:::GeomRibbon を継承してつくればよさそうだ。

補足 データを階段状に変更する処理は ggplot2 の geom-path-step.R のものを多少変更して作成。

library(ggplot2)
library(proto)

# 描画用クラスを定義
GeomConfint <- proto(ggplot2:::GeomRibbon, {
  objname <- "confint1"
  required_aes <- c("x", "ymin", "ymax")
  
  draw <- function(., data, scales, coordinates, na.rm = FALSE, ...) {
    if (na.rm) data <- data[complete.cases(data[required_aes]), ]
    data <- data[order(data$group, data$x), ]
    # ここでデータを階段状に変換
    data <- .$stairstep_confint(data)  
    ggplot2:::GeomRibbon$draw(data, scales, coordinates, na.rm = FALSE, ...)
  }
  
  stairstep_confint <- function (., data) {
    # データを階段状に変換するメソッド
    data <- as.data.frame(data)[order(data$x), ]
    n <- nrow(data)
    ys <- rep(1:n, each = 2)[-2 * n]
    xs <- c(1, rep(2:n, each = 2))
    data.frame(x = data$x[xs], ymin = data$ymin[ys], ymax = data$ymax[ys],
               data[xs, setdiff(names(data), c("x", "ymin", "ymax"))])
  }
})

# 描画用クラスを呼び出す描画関数を定義
geom_confint1 <- function (mapping = NULL, data = NULL, stat = "identity",
                           position = "identity", na.rm = FALSE, ...) {
  GeomConfint$new(mapping = mapping, data = data, stat = stat, 
                  position = position, na.rm = na.rm, ...)
}

このとき、GeomConfint$drawdata としては、以下のように描画用に変換された data.frame が渡される。そのため、描画用データに対して処理を行う場合はこの方法が便利。

#   x ymin ymax PANEL group colour   fill size linetype alpha
# 1 1   -1    1     1     1     NA grey20  0.5        1   0.5
# 2 2   -2    2     1     1     NA grey20  0.5        1   0.5
# 3 3   -3    3     1     1     NA grey20  0.5        1   0.5
# 4 4   -4    4     1     1     NA grey20  0.5        1   0.5

描画してみる。上で定義した描画関数 geom_confint1 がそのまま geom として使える。

df <- data.frame(x = c(1, 2, 3, 4),
                 upper = c(1, 2, 3, 4),
                 lower = c(-1, -2, -3, -4))
ggplot(data = df) + geom_confint1(aes(x = x, ymin = lower, ymax = upper), alpha = 0.5)

f:id:sinhrks:20150328082021p:plain

2. 描画用関数 geom_xxx だけを定義して作る

また、今回の場合 塗りつぶしが必要なければ 複数geom_step の組み合わせでも描ける。このときは描画関数を以下のように定義すればよい。

geom_confint2 <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity", 
                           na.rm = FALSE, ...) {
  # 上側 / 下側の階段関数 geom_step に渡す mapping を作成
  mapping1 <- mapping
  mapping1['y'] <- mapping['ymax']
  
  mapping2 <- mapping
  mapping2['y'] <- mapping['ymin']
  
  g1 <- geom_step(mapping = mapping1, data = data, stat = stat, 
                  position = position, na.rm = na.rm, ...)
  g2 <- geom_step(mapping = mapping2, data = data, stat = stat, 
                  position = position, na.rm = na.rm, ...)
  # 複数の geom はリストで返す
  list(g1, g2)
}

描画する。

ggplot(data = df) + geom_confint2(aes(x = x, ymin = lower, ymax = upper))

f:id:sinhrks:20150328082034p:plain

補足 ただし、上の例では mapping の変換を伴うため、以下の書式では動かない。

ggplot(data = df, mapping = aes(x = x, ymin = lower, ymax = upper)) +
  geom_confint2()
# Error:  引数に異なる列数のデータフレームが含まれています: 7, 0 

まとめ

独自の geom は正当な方法以外でもわりと手軽に作れる。

  • 既存の描画用クラス (Geom) を継承して作る
  • 描画用関数 geom_xxx だけを定義して作る