gghighlight
 作者です。
すべて
 お話しします。

@yutannihilation

ドーモ!

Hiroaki Yutani

  • Twitter: @yutannihilation
  • 好きな言語:R、忍殺語
  • ggplot2 のメンテナ
  • 「Rユーザのための RStudio[実践]入門」のdplyr/tidyrの章を担当

ggplot2のメンテナ

あまり大したことしてないですが、細かいバグを直したりとかしてます(最近あんまやれてない)

Rユーザのための RStudio[実践]入門

あれから5年…

伝説の Hadley Wickham 緊急来日回

あれから5年…

  • そういえば日本語で発表したことがなかった!
  • あと、英語力と時間の問題で簡単な機能紹介だけだったので、今日はフルバージョンでお届けします!

今日話すこと

  • gghighlightの使い方
  • gghighlightを使わずにやるとどうなるか

※表記について

このスライドでは合字フォントを使っています。 慣れていないと読みづらいかもです。すみません!

表示 実際の文字
<= < = (小なりイコール)
<- < - (代入)
|> | > (パイプ演算子)

gghighlightの使い方

gghighlightとは

  • 条件に当てはまるデータをハイライトするためのRパッケージ
    • 条件は dplyr::filter() と同じルールで評価される1
  • あのHadley公式ggplot2本(第三版)2にも紹介されている定番パッケージ

🍝

Code
set.seed(2)
data <- purrr::map_dfr(
  letters,
  ~ data.frame(
      x = 1:400,
      y = cumsum(runif(400, -1, 1)),
      type = .,
      flag = sample(c(TRUE, FALSE), size = 400, replace = TRUE),
      stringsAsFactors = FALSE
    )
)

library(ggplot2)
library(gghighlight)

ggplot(data, aes(x, y, colour = type)) +
  geom_line()

🍝

  • 色が多すぎて見分けられない
    • 色は6色くらいが望ましい、とされている 1
  • でも、値の分布を示すためにも、データを絞り込みたくはない

→ gghighlightの出番です!

ggplot(data, aes(x, y, colour = type)) +
  geom_line()

ggplot(data, aes(x, y, colour = type)) +
  geom_line() +
  gghighlight(max(y) >= 20)

インストール

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

install.packages("gghighlight")

基本的な使い方

  • 通常のggplotのオブジェクトに+で足すだけ
  • だいたいのGeomに使える
  • 条件には任意の表現が書ける
  • 条件は複数指定できる
ggplotのオブジェクト +
  gghighlight(条件1, 条件2, ...)

条件の例

  • yの最大値が20以上の線をハイライトする
gghighlight(max(y) >= 20)
  • yの最大値が20以上、かつ最小値が0以上の線
gghighlight(max(y) >= 20, min(y) >= 0)
  • レコード数が100以上の線
gghighlight(n() > 100)

geom_bar() の例

priceの平均が4000以上をハイライト

ggplot(diamonds, aes(cut, fill = cut)) +
  geom_bar() +
  gghighlight(mean(price) >= 4000)

geom_point() の例

dispが200以上をハイライト

mtcars$cyl <- factor(mtcars$cyl)
ggplot(mtcars, aes(wt, mpg, colour = cyl)) +
  geom_point(alpha = 0.85) +
  gghighlight(disp >= 200)

geom_sf() の例

AREAが0.20以上をハイライト

ggplot(nc) +
  geom_sf(aes(fill = AREA)) +
  gghighlight(AREA >= 0.20)

どういうGeomに使えるの?

  • だいたい使えるはず
  • 正確には、「同じデータからは必ず同じ図形が描かれる」なら使える
    • ダメな例: position_dodge()position_jitter()

geom_boxplot() の例

ひとつのboxplotの幅は、その目盛りで横並びになるべきカテゴリの数で決まる。 絞り込まれてカテゴリ数が変わると、幅が違ってうまく重ならない。

mpg$cyl <- factor(mpg$cyl)
ggplot(mpg, aes(class, hwy, colour = cyl)) +
  geom_boxplot(alpha = 0.3) +
  gghighlight(
    max(hwy) > 40,
    unhighlighted_params = list(
      colour = "grey40"
    )
  )

geom_boxplot() の例

geom_jitter() の例

ランダムにずれるので重ならない

d_jitter <- data.frame(x = rep(1, 20))

set.seed(1)
ggplot(d_jitter, aes(x, x)) +
  geom_jitter(size = 10) +
  gghighlight()

geom_jitter() の例

geom_jitter() の例

seedを固定すると大丈夫

ggplot(d_jitter, aes(x, x)) +
  geom_point(
    size = 10,
    position = position_jitter(seed = 1)
  ) +
  gghighlight()

geom_jitter() の例

条件の種類

  • geom_point()geom_sf()など、1つのレコードが1つの図形を描くGeom

→ レコードごとに計算される条件(例:disp >= 200AREA >= 0.20

  • geom_line()geom_bar()など、複数のレコードから1つの図形を描くGeom

→ グループごとに計算される条件(例:mean(price) >= 4000n() > 100

条件の種類

  • えっ、難しくてどっちを選べばいいかわからない…><

gghighlight()は自動でどっちの計算も試して、うまくいった方を採用するので、雰囲気で使って大丈夫!

use_group_by

  • 自動で計算してくれるのはありがたいんですが、この警告メッセージがうざいんですけど
#> Warning message:
#> Tried to calculate with group_by(), but the calculation failed.
#> Falling back to ungrouped filter operation... 

use_group_byを明示的に指定しよう

  gghighlight(disp >= 200,
    use_group_by = FALSE
  )

TRUE / FALSE じゃなくても OK

ここまで、わかりやすいように「条件」と書いてきたが、実はTRUE/FALSEでなくてもいい

  • 結果が数字や文字列の場合は、その値でデータを並べ替えて上位のレコード/グループをハイライトする
  • ハイライトする数は、max_highlightで変えられる(デフォルトは5)
mtcars$cyl <- factor(mtcars$cyl)
ggplot(mtcars, aes(wt, mpg, colour = cyl)) +
  geom_point() +
  gghighlight(disp, max_highlight = 3)

TRUE / FALSE じゃなくても OK

結果のカスタマイズ

ポイント

  1. gghighlight()の結果は通常のggplotオブジェクトなので、好きなようにいじれる
  2. ハイライトのされ方を変えるには、unhighlighted_params
  3. gghighlight()の対象から外したいレイヤーは、gghighlight()の後に重ねる

余談:個人的に思ってること

  • gghighlightのメインの目的はデータの探索
  • gghighlightはただのショートカットで、gghighlightでできることはgghighlightを使わなくてもできる

→ 複雑な図をつくりたいのであれば、gghighlightを使わないでやった方がいいかも…

gghighlight() の結果は ggplot

+でテーマを変えたり、patchworkしたりできる。

p1 <- ggplot(data, aes(x, y, colour = type)) +
  geom_line()

p2 <- p1 + gghighlight(max(y) > 19) +
  theme_minimal() +
  ggtitle("変わり果てた姿")

patchwork::wrap_plots(p1, p2)

gghighlight() の結果は ggplot

facet が便利

グレーになった部分は全facetに表示される

p2 + facet_wrap(vars(type))

facet が便利

なんなら、このためだけに空のgghighlight()を使うまである

mtcars$cyl <- factor(mtcars$cyl)
ggplot(mpg, aes(displ, hwy, colour = cyl)) +
  geom_point() + 
  gghighlight() + 
  facet_wrap(vars(cyl))

facet が便利

unhighlighted_params

グレーアウト部分の任意のパラメータを上書きできる

  1. 元のlinewidthは太目に
  2. グレーアウト部分のlinewidthは細目で上書きする
ggplot(data, aes(x, y, colour = type)) +
  geom_line(linewidth = 3, alpha = 0.7) +
  gghighlight(max(y) > 19,
    unhighlighted_params = list(
      linewidth = 1
    )
  )

unhighlighted_params

unhighlighted_params

NULLを指定してグレーアウトを取り消すこともできる

ggplot(data, aes(x, y, colour = type)) +
  geom_line(linewidth = 1.3) +
  gghighlight(max(y) > 19,
    unhighlighted_params = list(
1      colour = NULL,
2      alpha = 0.2
    ),
3    keep_scales = TRUE
  )
1
色は元のままにする
2
ただし透明度を上げる
3
(次に説明)

unhighlighted_params

keep_scales

gghighlight()は、絞り込んだ後のデータに色を割り当てるので、元のプロットの色とは一致しない。 keep_scales=TRUEを指定すれば、元の色と合わせることができる。

mtcars$cyl <- factor(mtcars$cyl)
A <- ggplot(mtcars, aes(wt, mpg, colour = cyl)) +
  geom_point()

B <- A + gghighlight(disp)

C <- A +
  gghighlight(disp, keep_scales = TRUE)

keep_scales

A: オリジナル、B: 通常、C: keep_scales=TRUE

gghighlight() は順序が重要

gghighlight()は通常のggplotの関数と同じく+で足せるが、中身はぜんぜん違う

  • ggplotのレイヤーは、そのプロットを表示する直前に評価されるが、gghighlight()+された時点で処理が実行される
  • なので、gghighlight()より後に+されたレイヤーに対してはgghighlight()の力は及ばない
  • ただし、ggplotオブジェクトが保持するデータは条件を満たすもののみに絞り込まれているので、元のデータを参照するにはdata引数に元のデータを渡す必要がある

gghighlight() は順序が重要

data2 <- tibble::tibble(
  x = rep(1:3, each = 3),
  type = rep(1:3, times = 3),
  y = x * type
)
data2$type <- factor(data2$type)

ggplot(data2, aes(x, y, colour = type)) +
  geom_line() +
  gghighlight(max(y) >= 9) +
  geom_point(data = data2)

geom_point()はカラフルなまま

gghighlight() は順序が重要

ラベルのカスタマイズ小ネタ集

  • gghighlightは、なるべく凡例の代わりに直接ラベルを付けるようにするが、use_direct_label=FALSEにすると常に凡例を表示する
  • ラベルはlabel_paramsでカスタマイズできる
  • geom_line()へのラベルのつけ方はline_label_typeで変えられる

“ggrepel_label”(デフォルト)

ggrepelパッケージを使う

ggplot(data, aes(x, y, colour = type)) +
  geom_line() +
  gghighlight(max(y) >= 20,
    line_label_type = "ggrepel_label"
  )

“ggrepel_label”(デフォルト)

“sec_axis”

secondary axisをおしゃれに使う

  • 欠点:値が近いところにあると、重なってしまって読みづらい
ggplot(data, aes(x, y, colour = type)) +
  geom_line() +
  gghighlight(max(y) >= 20,
    line_label_type = "sec_axis"
  )

“sec_axis”

“text_path”

geomtextpathパッケージを使う

  • 欠点:線が曲がりくねったり重なったりすると読みづらい
ggplot(data, aes(x, y, colour = type)) +
  geom_line() +
  gghighlight(max(y) >= 20,
    line_label_type = "text_path"
  )

“text_path”

gghighlightを支える技術

(ここからはマニア向けなので気軽に聞いてください)

gghighlight を使わずにやってみる

まず、条件を満たさない値をNAで置き換える。

library(dplyr)

data_tweak <- data |>
  mutate(
    flag = max(y) >= 20,
    bleached = if_else(flag, type, NA),
1    .by = type
  )
1
typeごとに集計

gghighlight を使わずにやってみる

NAにグレーを割り当てる。

ggplot(data_tweak) +
1  aes(x, y, colour = bleached, group = type) +
  geom_line() +
  scale_colour_discrete(
2    na.value = alpha("grey", 0.7)
  )
1
colour=bleachedだけだとNAで1本の線になってしまう。groupを明示的に指定する。
2
NAの値はna.valueで指定できる。

gghighlight を使わずにやってみる

gghighlight を使わずにやってみる

このタイプの方法(色の塗り分けで対応)は、わかりやすいが、facetできないという欠点がある。

last_plot() +
  facet_wrap(vars(bleached))

facet できるようにする

facet_*()は、引数に指定した名前の列が含まれるデータは分割するが、含まれないデータは全facetに描く。 これを利用するため、分割されないように列名を変えたバージョンをつくる。

bleached <- data |>
  rename(TYPE = type)

また、ハイライトするデータに絞ったバージョンもつくる。

colourful <- data |>
  filter(max(y) >= 20, .by = type)

facet できるようにする

この2つのデータを別々のgeom_line()として重ね合わせる。

ggplot(NULL, aes(x, y)) +
  geom_line(
    mapping = aes(group = TYPE),
    data = bleached,
    colour = alpha("grey", 0.7)
  ) +
  geom_line(
    mapping = aes(colour = type),
    data = colourful
  )

facet できるようにする

facet できるようにする

今度はうまくfacetできている。

last_plot() +
  facet_wrap(vars(type))

別解

あまり知られていないが、geom_*()data引数には関数を指定することもできる。 その場合、ggplot()に指定されているデータにその関数を適用した結果が使われる。

なので、例えば、こういうデータを絞り込む関数を作る関数を定義しておいて、

build_filter <- function(...) {
  function(data) {
    data |> filter(..., .by = type)
  }
}

別解

ハイライトする方のレイヤーのdata引数に指定してもいい。

ggplot(data, aes(x, y)) +
  geom_line(
    mapping = aes(group = TYPE),
    data = bleached,
    colour = alpha("grey", 0.7)
  ) +
  geom_line(
    mapping = aes(colour = type),
    data = build_filter(max(y) >= 20)
  )

別解

別解

こうしておくと、ちょっと条件を変えたくなってもここだけ書き換えればよくて便利

ggplot(data, aes(x, y)) +
  geom_line(
    mapping = aes(group = TYPE),
    data = bleached,
    colour = alpha("grey", 0.7)
  ) +
  geom_line(
    mapping = aes(colour = type),
    data = build_filter(max(y) >= 20)
  )

別解

こうしておくと、ちょっと条件を変えたくなってもここだけ書き換えればよくて便利

ggplot(data, aes(x, y)) +
  geom_line(
    mapping = aes(group = TYPE),
    data = bleached,
    colour = alpha("grey", 0.7)
  ) +
  geom_line(
    mapping = aes(colour = type),
    data = build_filter(max(y) >= 19)
  )

別解

こうしておくと、ちょっと条件を変えたくなってもここだけ書き換えればよくて便利

ggplot(data, aes(x, y)) +
  geom_line(
    mapping = aes(group = TYPE),
    data = bleached,
    colour = alpha("grey", 0.7)
  ) +
  geom_line(
    mapping = aes(colour = type),
    data = build_filter(max(y) <= 100)
  )

別解

(ま、こういう反復を手早くやりたくてgghighlight()をつくったんですけどね!)

keep_scales

keep_scales=TRUEを再現するには、expand_limits()という便利な関数がある。 任意のaesthetic variable(xycolourfill…)のスケールの範囲を拡大してくれる。

データには含まれないが凡例にはラベルを表示したい、みたいなときに便利。

last_plot() +
  expand_limits(
    colour = unique(data$type)
  )

keep_scales

ggrepel パッケージ

かぶらないように自動で位置を調整してくれる版のgeom_text()geom_label()

geomtextpath パッケージ

パスに従ってテキストを配置してくれるパッケージ

まとめ

まとめ

  • + gghighlight()でお手軽にハイライトできるのでぜひ使ってください
  • gghighlightはただのショートカットなので別に使わなくても同じことできます

Thank you!

References