データ系列が多すぎるとき、いい感じに一部をハイライトするためのパッケージgghighlightをつくりました

ggplot2で可視化しようとして、データ系列が多すぎてこんなもじゃもじゃになってしまう、みたいなことないでしょうか。

f:id:yutannihilation:20170929200917p:plain:w450

これを、一部だけを色付けしてこんな感じのプロットにしてくれるパッケージをつくりました。

f:id:yutannihilation:20170929201018p:plain:w450

インストール

GitHub上からインストールできます。

devtools::install_github("yutannihilation/gghighlight")

gghiglightがやっていること

gghiglightの説明をする前に、まずは上のグラフが何をしているのか、まずはふつうのtidyverseでやってみます。

データはこんな感じのやつです。

library(dplyr, warn.conflicts = FALSE)

set.seed(1)
d <- tibble(
  idx = 1:10000,
  value = runif(idx, -1, 1),
  type = sample(letters, size = length(idx), replace = TRUE)
) %>%
  group_by(type) %>%
  mutate(value = cumsum(value)) %>%
  ungroup()

これを、valueの最大値が20を超える系列だけに色を付けたいとすると、dplyr::filter()とかでそのデータだけ抜き出して、ggplot2のレイヤーを重ねるというのが正攻法でしょう。

# 色付けする用のデータだけを抜き出す
d_filtered <- d %>%
  dplyr::group_by(type) %>%
  dplyr::filter(max(value) > 20)

library(ggplot2)

ggplot() +
  # 元のデータはグレーで描画
  geom_line(aes(idx, value, group = type), d, colour = alpha("grey", 0.7)) +
  # 絞り込んだデータは色付きで描画
  geom_line(aes(idx, value, colour = type), d_filtered)

まあこれだけのコードではあるんですが、じゃあ最大値が10のやつにしよう、別の値で絞り込もう、とか思うとこのコードをまた上から打ち直すことになります。ちょっとめんどくさいなと思ってパッケージにしました。

gghighlightパッケージなら、上の処理を1行で書けます。

library(gghighlight)

gghighlight_line(d, aes(idx, value, colour = type), max(value) > 20)

f:id:yutannihilation:20170929201018p:plain

gghighlightが返すのはggplot2のオブジェクト

この結果はggplot2のオブジェクトなので、テーマを重ねたりfacetしたりできます。

library(ggplot2)

gghighlight_line(d, aes(idx, value, colour = type), max(value) > 20) +
  theme_minimal()

f:id:yutannihilation:20170929203547p:plain

gghighlight_line(d, aes(idx, value, colour = type), max(value) > 20) +
  facet_wrap(~ type)

f:id:yutannihilation:20170929203558p:plain

適切な閾値が思いつかない時

さて、上ではデータの絞り込みに、

max(value) > 20

という表現を使っていましたが、この20という閾値はどこからきたのでしょう。これは、私がデータを何度かグラフにしてみて「この辺かな?」というさじ加減で決めたものです。

このように、いい感じの閾値というのは試行錯誤の中で決まってくるものであって、可視化に先立って決められるものではなかったりします。

そこで、gghighlight_line()にはTRUE/FALSEを返す表現だけでなく数値や文字列を返す表現も指定することができます。結果の値が上位の方から、デフォルトだと5つのデータ系列をハイライトします。

gghighlight_line(d, aes(idx, value, colour = type), max(value))

f:id:yutannihilation:20170929204739p:plain

この系列数はmax_highlight引数で変えることもできます。

gghighlight_line(d, aes(idx, value, colour = type), max(value), max_highlight = 2L)

geomの種類

今のところ、サポートしているgeomは、

  • line
  • point

の2つです。

gghighlight_point()は以下のように使います。

d2 <- data.frame(
  x     = c( 1, 1, 1, 2, 2, 2, 3, 3, 3),
  y     = c( 1, 2, 3, 1, 2, 3, 1, 2, 3),
  value = c( 1, 2, 3, 9, 8, 4, 5, 7, 6),
  category = letters[1:9],
  stringsAsFactors = FALSE
)

gghighlight_point(d2, aes(x, y), value > 5)
#> Warning message:
#> In gghighlight_point(d2, aes(x, y), value > 5) :
#>   Using category as label for now, but please provide the label_key explicity!

f:id:yutannihilation:20170929205105p:plain

ラベルを明示的に指定しない場合は文字列型の列の一つが適当に選ばれますが、label_keyで明示的に指定しておくのがおすすめです(このデータの場合↓でも結果は同じ)。

gghighlight_point(d2, aes(x, y), value > 5, label_key = category)

Grouped vs ungrouped

上のgghighlight_point()の例で、gghighlight_line()との絞り込みの表現の違いに気づいたでしょうか?

gghighlight_line()に指定した表現ではmax()が使われていますが、これはgghighlight_line()ではグループ単位での計算(具体的には内部でdplyr::group_by()dplyr::summarise()を使っている)が行われるからです。グループごとに返す値が1つでないと、以下のようなエラーになります。

gghighlight_line(d, aes(idx, value, colour = type), value > 20)
#> Error in summarise_impl(.data, dots): Column `predicate..........` must be length 1 (a summary value), not 387

この挙動はuse_group_by引数で変えることができますが、グループ化されていない折れ線グラフに今のところ意味を見いだせていません。

gghighlight_line(d, aes(idx, value, colour = type), value > 20, use_group_by = FALSE)
#> Warning message:
#> In gghighlight_line(d, aes(idx, value, colour = type), value > 20,  :
#>   No grouped vars or label_key.
#> Falling back to a usual legend...

f:id:yutannihilation:20170929210704p:plain:w450

一方、gghighlight_point()の場合、デフォルトでは、グループ化されていない計算になっています。こちらもuse_group_by引数で挙動を変えることができます。折れ線グラフとは違って、こっちは意味のある可視化ができるかもしれません。

set.seed(10)
d2 <- sample_n(d, 50)

gghighlight_point(d2, aes(idx, value, colour = type), max(value), use_group_by = TRUE, label_key = type)

f:id:yutannihilation:20170929211002p:plain

注意点

gghiglightはこんな感じで便利っぽい(と個人的には思ってるんですがどうでしょう?)んですが、あまり効率的ではありません。データの絞り込みからggplotのビルドまでが毎回走ることになるからです。ある程度すばやく手元で可視化するのには便利ですが、見るべきデータのめどが付いたら自分でコードを書きましょう。

ggplot2は便利なので何でもその中でやりたくなっちゃうんですが、一応、以前にkohskeさんにいただいたこの警句をいつも心に留めているつもりです。

さいごに

基本的に自分の手元で使うためにつくったみたいなパッケージなので、他の人のユースケースと合うのかいまいちわからないんですが、気が向いたら使ってもらえると嬉しいです。

バグ報告、要望などあれば、Twitter(日本語)、このブログのコメント欄(日本語)、GitHubのissues(英語)までお願いします。