Heavy Watal

ggplot2 — きれいなグラフを簡単に合理的に

“The Grammer of Graphics” という体系に基づいて設計されたパッケージ。 単にいろんなグラフを「描ける」だけじゃなく「一貫性のある文法で合理的に描ける」。

Rのグラフ描画システムにはgraphicsgridの2つが存在しており、 R標準のboxplot()hist()などは前者の上に、 本項で扱うggplot2は後者の上に成り立っている。 使い方が全く異なるので、前者を知らずにいきなりggplot2から始めても大丈夫。

tidyverse に含まれているので、 install.packages("tidyverse") で一括インストール、 library(tidyverse) で一括ロード。

基本的な使い方

まずは手元のデータを整形して、1行が1観測、1列が1変数という形の 整然データ を用意することが重要。 言い換えると「この列がx軸、この列がy軸、この列によって色を変える」というように指示できる形。 例えばggplot2に付属の mpg データ:

library(conflicted)
library(tidyverse)
mpg
#     manufacturer  model displ  year   cyl      trans   drv   cty   hwy    fl   class
#            <chr>  <chr> <dbl> <int> <int>      <chr> <chr> <int> <int> <chr>   <chr>
#   1         audi     a4   1.8  1999     4   auto(l5)     f    18    29     p compact
#   2         audi     a4   1.8  1999     4 manual(m5)     f    21    29     p compact
#  --
# 233   volkswagen passat   2.8  1999     6 manual(m5)     f    18    26     p midsize
# 234   volkswagen passat   3.6  2008     6   auto(s6)     f    17    26     p midsize

このようなデータを渡して、指示をどんどん + していく:

ggplot(data = mpg) +              # mpgデータでキャンバス準備
  aes(x = displ, y = cty) +       # displ,cty列をx,y軸にmapping
  geom_point() +                  # 散布図を描く
  facet_wrap(vars(drv)) +         # drv列に応じてパネル分割
  theme_classic(base_size = 20)   # クラシックなテーマで

途中経過をオブジェクトとして取っておける:

p1 = ggplot(data = mpg)
p2 = p1 + aes(x = displ, y = cty)
p3 = p2 + geom_point()
p4 = p3 + facet_wrap(vars(drv))
p5 = p4 + theme_classic(base_size = 20)
print(p5)

画像ファイルに保存するところまできっちり書く:

ggsave("mpg.png", p5, width = 6, height = 4, dpi = 300)

Aesthetic mapping

データと見せ方を紐付ける。 aes(color = Species) のように aes() 内で列名を指定すると、 その変数に応じて色やサイズなどを変えることができる。 言い換えると、データの値を色やサイズに変換する(スケールする)ことに相当する。

# データによって点のサイズ・色・形を変える
p2 + geom_point(mapping = aes(color = drv, size = cyl))

# aes() の外で指定するとデータによらず全体に反映。
# 常に色はオレンジ、サイズは6、不透明度は0.4
p2 + geom_point(color = "darkorange", size = 6, alpha = 0.4)

Aestheticsまとめ

https://ggplot2.tidyverse.org/articles/ggplot2-specs.html

scale_*() で調整

変数をどうやって色やサイズに反映させるか、 各項目に対応する scale_*() 関数で調整する。

p1 + aes(x = displ, y = cty) + geom_point(aes(color = hwy)) + scale_color_fermenter()
  geom_point(mapping = aes(color = drv, size = cyl)) +
  scale_color_brewer(palette = "Set1") +
  scale_size_identity()
scale_color_gradient
連続値のデフォルト色スケール。
グラデーションの基準となる色を自由に指定できるけど、 後述のviridisのようによく計算されたスケールを使ったほうが無難。
scale_color_gradient(..., low, high, ...): 普通の連続値に
scale_color_gradient2(..., low, mid, high, midpoint = 0, ...): ある中間値を挟んで上下に分けたいとき
scale_color_gradientn(..., colors, values = NULL, ...): 多色のヒートマップなどに e.g., colors = c("#000000", "#0000FF", "#00FFFF", "#00FF00", "#FFFF00", "#FF0000")
離散的に塗るには scale_color_steps()
scale_color_viridis_c
色覚多様性が考慮されたパレットで、グレースケールでの明度変化も一定。 使用例と詳細説明はviridisパッケージのサイトを参照。
option = "viridis", "magma", "inferno", "plasma", "cividis", "mako", "rocket", "turbo"
連続値は _c、離散値は _d、連続値を離散的に塗るには _b
黄色が明るすぎて白背景で見にくい場合などは begin, end オプションで調整可能。
scale_color_hue
離散値のデフォルト色スケール。
scale_color_brewer
いい感じに考えられたパレット Colorbrewer から選んべるので楽ちん。 利用可能なパレットは RColorBrewer::display.brewer.all() でも一覧できる。
離散値は _brewer、連続値は _distiller、連続値を離散的に塗るには _fermenter
scale_*_identity
for color, fill, size, shape, linetype, alpha
色の名前やサイズなどを示す列が予めデータに含まれている場合にそのまま使う。
scale_*_manual
for color, fill, size, shape, linetype, alpha
対応関係を手動で指定する。 e.g., scale_color_manual(values = c("4" = "red", f = "green", r = "blue"))
scale_size
デフォルトではpointの面積を値にほぼ比例させるが、面積0にはならない。 値0に面積0を対応させるには scale_size_area() を使う。 半径を比例させるには scale_radius() があるけど要注意。

scale_alpha(..., range = c(0.1, 1))
scale_linetype
scale_shape

スケール共通オプション

値と見え方の対応関係が凡例(legend/colorbar)として表示される。 scale_*() 関数に以下のオプションを指定することで設定を変えられる。 連続値の場合離散値の場合 で微妙に意味が変わるけどだいたいこんな感じ:

変数によってパネルを分割する

多変量データを俯瞰するには、データに応じたパネル分割も便利。 色・サイズなどと合わせれば、x軸y軸プラス3次元程度はパッと可視化できることになる。

facet_wrap()
1変数で分割して並べる
facet_wrap(facets, nrow = NULL, ncol = NULL, scales = "fixed",
  shrink = TRUE, labeller = "label_value", as.table = TRUE,
  switch = NULL, drop = TRUE, dir = "h", strip.position = "top")

p3 + facet_wrap(vars(class), ncol = 4L) # new style
p3 + facet_wrap(~ class, ncol = 4L)     # old style
facet_grid()
2変数以上で分割して縦横に並べる
facet_grid(rows = NULL, cols = NULL, scales = "fixed", space = "fixed",
  shrink = TRUE, labeller = "label_value", as.table = TRUE,
  switch = NULL, drop = TRUE, margins = FALSE, facets = NULL)

p3 + facet_grid(vars(cyl), vars(class, drv)) # new style
p3 + facet_grid(cyl ~ class + drv)           # old style

1変数でいい場合は . ~ class のように片方をドットにする。

ファセットラベルの調整 labeller =
デフォルトでは値だけがfacetラベルに表示されるが、 変数名を同時に表示したり、数式を表示したりもできる。 それを変数ごとに指定したい場合は labeller() 関数を使う。e.g.,
labeller = labeller(cyl = label_both, class = label_value)
見た目の調整はテーマの strip.* で。

パネル同士が近すぎて axis.text が重なってしまう場合は theme(panel.spacing = grid::unit(1, "lines")) などとすれば間隔を空けられる。

内部変数を使う

ヒストグラムや箱ヒゲなどの表示に必要な計算は stat_*() を通して内部的に行われる。 そうした値の一部は after_stat() 関数を通じて参照し、 aes() 内で使うことができる。 例えば geom_histogram()geom_bar() の縦軸を生のカウントから密度に変えるには aes(y = after_stat(density)) のようにする。 (昔は ..density.. とか stat(density) のようにしていた。)

これらはggplotオブジェクトを作るときではなく、描画するときに計算されるらしい。 強制的にそこまで計算させて値を参照するには ggplot_build() を使う。 たとえば h = hist(mpg$cty) のようなものを取得したいときは:

p = ggplot(mpg) + aes(x = cty) + geom_histogram(bins = 6L)
ggplot_build(p)$data[[1L]]
# layer_data(p)

colorとfillを同色の透明度違いにする、とかやりたい場合も似たようなイメージで、 after_scale() を使って計算後・描画前に割り込める。

https://ggplot2-book.org/layers.html#generated-variables

座標軸やタイトルを変更

軸の区切りを変更したり対数にしたり
scale_x_continuous(breaks = seq(10, 100, by = 10))
scale_y_log10("Beer consumption")
scale_y_reverse()
上記のスケール共通オプションに加えて:
  • expand = expansion(mult, add): デフォルトでは値域よりも少し(連続値5%、離散値0.6個分)だけ広く描かれるので、 それをゼロにするとか、もっと広くとるとか。
  • trans: 数値の変換。exp, identity, log, log10, reciprocal, reverse など。 文字列変数の順序を変えたい場合は limits のほうを使う。
  • position: top, bottom, left, right
  • sec.axis: 第二軸
描画する範囲を指定
ylim(0, 42) + xlim("b", "c", "d")
coord_cartesian(xlim = NULL, ylim = NULL)
前者はデータそのものを切るが、後者はデータを変えずに描画領域だけ切る
ゼロを含むようにちょっと伸ばすだけとかなら expand_limits(x = 0) が便利。
X軸とY軸の比率を固定
coord_fixed(ratio = 1)
XY軸の転置
coord_flip()
geom_*(orientation = "y") も可。 geom_bar(), geom_histogram() では aes(y = にマッピングするだけでもいい。
極座標
パイチャートも作れるらしい
座標変換
coord_trans(x = "log10", y = "sqrt")
表示する座標を変換する。 stat前に適用される scale_x_* とは微妙に違う。
軸ラベルとタイトル
labs(x = "time", y = "weight", title = "growth", tag = "A")
xlab("time") + ylab("weight") + ggtitle("growth")
凡例
散布図なら点、折れ線なら線が凡例のキーとして自動的に選ばれるが、 geom_line(key_glyph = draw_key_rect) のように変更することもできる。

theme: 背景やラベルの調整

https://ggplot2.tidyverse.org/reference/ggtheme.html

既成テーマ

theme_gray(base_size = 11, base_family = "")
theme_grey()
灰色背景に白い格子。 ggplotらしいデフォルトだが、論文には使いにくい。
theme_bw()
黒枠白背景にうっすら灰色格子
theme_linedraw()
細いけど濃い色の panel.grid
theme_light()
それを薄くした感じ
theme_minimal()
外枠なしの theme_bw
theme_classic()
xy軸がL字に描かれているだけで枠もグリッドも無し
theme_dark()
濃灰色背景に黒い格子。黄色の点とかが見やすい。
theme_void()
完全に枠なし

これらをカッコ無しでコンソールに打ち込むと、 下記の各エレメントの設定方法やデフォルト値を知ることができる。

引数に base_family = "Helvetica Neue" などとしてフォントを指定できる。 テーマを構成する axis.text などはこの設定を継承するが、 geom_text()annotate("text", ...) などプロット内部の要素には影響しないことに注意。

どうしても日本語を表示しないといけない場合は https://ill-identified.hatenablog.com/entry/2020/10/03/200618 を参考にして設定する。

オプションとして base_line_size, base_rect_size もいじれるようになった。

ほかにもいろんなテーマが ggthemes というパッケージなどで提供されている。

設定項目

https://ggplot2.tidyverse.org/reference/theme.html

theme() 関数に項目と値を指定したものを、 ほかのレイヤーと同じようにどんどん足していける。 テーマは直線、長方形、文字の3種類のエレメントからなり、 後述の element_***()を介して変更していく。

## ベースとなるテーマを先に適用してから微調整
p3 + theme_bw() + theme(
  panel.background = element_rect(fill = "white"), # 箱
  panel.grid       = element_line(color = "blue"), # 線
  axis.title       = element_text(size = 32),      # 文字
  axis.text        = element_blank()               # 消す
)
全体
line: (element_line)
rect: (element_rect)
text: (element_text)
title: (element_text; inherits from text)
aspect.ratio:
軸タイトル、軸ラベル、目盛
axis.title: (element_text; inherits from text)
__.x, __.x.top, __.y, __.y.right
axis.text: (element_text; inherits from text)
__.x, __.x.top, __.y, __.y.right
axis.ticks: (element_line; inherits from line)
__.x, __.y
axis.ticks.length: (unit)
axis.line: (element_line; inherits from line)
__.x, __.y
凡例
legend.background: (element_rect; inherits from rect)
legend.margin: (margin)
legend.spacing:(unit)
__.x, __.y
legend.key: (element_rect; inherits from rect)
legend.key.size: (unit)
legend.key.height: (unit; inherits from legend.key.size)
legend.key.width: (unit; inherits from legend.key.size)
legend.text: (element_text; inherits from text)
legend.text.align: (number from 0 (left) to 1 (right))
legend.title: (element_text; inherits from title)
legend.title.align: (number from 0 (left) to 1 (right))
legend.position: ("left", "right", "bottom", "top", "none" c(0, 1))
legend.direction: ("horizontal" or "vertical")
legend.justification: ("center" or c(0, 1) のような数値でアンカー位置を指定)
legend.box: ("horizontal" or "vertical")
legend.box.just: ("top", "bottom", "left", or "right")
legend.box.margin: (margin)
legend.box.background: (element_rect; inherits from rect`)
legend.box.spacing:(unit)
プロット領域の背景、余白、格子
panel.background: (element_rect; inherits from rect)
panel.border: (element_rect; inherits from rect; should be used with fill = NA)
panel.spacing: (unit; facet_* の間隔)
__.x, __.y
panel.grid: (element_line; inherits from line)
panel.grid.major: (element_line; inherits from panel.grid)
__.x, __.y
panel.grid.minor: (element_line; inherits from panel.grid)
__.x, __.y
全体の背景、タイトル、余白
plot.background: (element_rect; inherits from rect)
plot.title: (element_text; inherits from title)
plot.subtitle: (element_text; inherits from title)
plot.caption: (element_text; inherits from title)
plot.margin: (unit with the sizes of the top, right, bottom, and left margins)
facet したときのラベル
strip.background: (element_rect; inherits from rect)
strip.placement: (“inside”, “outside”)
strip.text: (element_text; inherits from text)
__.x, __.y
その他
complete: 部分的な変更か、完全なテーマか (FALSE)
validate: 毎回チェックするか (TRUE)

エレメント

element_rect(fill, color, size, linetype, inherit.blank) — 長方形
fill: 塗りつぶしの色
color: 枠の色

element_line(color, size, linetype, lineend, arrow, inherit.blank) — 線

element_text(family, face, color, size, hjust, vjust, angle, lineheight, margin) — 文字
family: フォントファミリー。 空なら base_family を継承。
face: ("plain", "italic", "bold", "bold.italic")
hjust, vjust: 水平位置と垂直位置の寄せ方をそれぞれ [0, 1] の実数で。
angle: 角度 [0, 360]
margin: スペース調整を関数 margin(top, right, bottom, left) 越しに。
element_blank() — 空
消したい要素にはこれを指定する
rel(x)
デフォルトからの相対値で size 引数を指定したいときに。
grid::unit(x, units, data = NULL)
こちらは絶対指定。grid パッケージに入ってる。 units で使いそうなのは cm, mm, inches, points, lines, native
grid::arrow(angle, length, ends, type)
axis.lineelement_line() にこれを与えて軸を矢印にするとか。
margin(t = 0, r = 0, b = 0, l = 0, unit = "pt")
marginクラス
calc_element(element, theme, verbose = FALSE)
継承などを考慮した上でelementがどんな値にセットされるか確かめる。

ファイルに書き出す

RStudioやQuartzの保存ダイアログを利用して書き出すこともできるけど、 それだとサイズ調整やファイル名入力を手作業でやることになってしまう。 ggsave() をスクリプトに書いておけば何回でも同じ設定で出力できる。

ggsave(filename, plot = last_plot(), device = NULL, path = NULL,
       scale = 1,  width = NA, height = NA,
       units = c("in", "cm", "mm"), dpi = 300, limitsize = TRUE, ...)
# 7inch x 300dpi = 2100px四方 (デフォルト)
ggsave("mpg1.png", p3) # width = 7, height = 7, dpi = 300

# 4     x 300    = 1200  全体7/4倍ズーム
ggsave("mpg2.png", p3, width = 4, height = 4) # dpi = 300

# 2     x 600    = 1200  全体をさらに2倍ズーム
ggsave("mpg3.png", p3, width = 2, height = 2, dpi = 600)

# 4     x 300    = 1200  テーマを使って文字だけ拡大
ggsave("mpg4.png", p3 + theme_bw(base_size = 22), width = 4, height = 4)

プロットの種類

散布図
geom_point(size = 2, alpha = 0.3)
重なった点をランダムにばらかしたいときは geom_jitter()
点の形(shape)一覧
デフォルトの shape = 19 の丸は半透明にすると縁取りしたように見えてしまう。 shape = 16 の丸は均一なのでこっちを常に使うように設定できればいいんだけど。
折れ線グラフ
geom_path(size = 2, linetype = "dashed") データ順に結ぶ
geom_line() x軸上の順で結ぶ
geom_step() 階段状に結ぶ
線の種類(linetype)一覧
面グラフ
geom_ribbon() — yminからymaxの面
geom_area() — 0からyの面
ヒストグラム、密度曲線
geom_histogram() — 棒グラフ(連続値をstat_bin() で区切って)
geom_bar() — 棒グラフ(離散値をstat_count()で数えて)
geom_freqpoly() — 折れ線
geom_density() — 密度推定されたスムーズな線
geom_bin2d() — 二次元ヒストグラム
geom_hex() — 六角形版二次元ヒストグラム
棒グラフ
geom_col()
グループ分けする場合のオプション:
箱ひげ図とその代替案
geom_boxplot(): 生データの分布に関する情報量が落ちすぎるので要注意。
geom_violin(scale = "count"): 箱ひげ図よりは密度が分かりやすいけど、推定であることとスケールに注意。
ggridges::geom_density_ridges(): violinの片側を横にしたようなもの。
geom_dotplot() or ggbeeswarm::geom_beeswarm(): 生データの分布が読み取りやすい。
ヒートマップ
geom_tile(aes(fill = z))
geom_raster(aes(fill = z)) — 各タイルの大きさを揃える制約のため高速
エラーバー
geom_errorbar(aes(ymax = y + se, ymin = y - se), width = 0.1)
geom_linerange(...)
geom_pointrange(...)
関数
ggplot(data.frame(x = c(-4, 4))) + aes(x) + stat_function(fun = dnorm, args = c(0, 1), n = 200)
回帰曲線
geom_smooth(method = glm, method.args = list(family = poisson), se = FALSE)
始点と終点で曲線や矢印を描く
geom_curve(aes(x, y, xend, yend), curvature = -0.2)
geom_segment(aes(x, y, xend, yend), arrow = arrow(type = "closed"), linejoin = "mitre")
矢印の調整はgrid::arrow()。 普通の線より矢尻の分だけ長くなることに注意。
切片と傾きで直線を描く
geom_abline(intercept = 3, slope = 5)
geom_hline(yintercept = 7) + geom_vline(xintercept = 11)
これらは annotate() 的な位置づけであり、ほかの geom_ 関数とはかなり挙動が違う。 そのためか annotate("hline", yintercept = 7) なども思い通りの結果にはならない。
文字列や図形を書き加える
annotate("text", x = 1:4, y = 4:1, label = sprintf("x = %d", 1:4))
テーマの base_family は引き継がれないので family = で指定すべし。
数式を表示するには label = "italic(N[t])" のような文字列で渡して parse = TRUE
データ範囲によらず相対位置を指定したいとき、公式のオプションは無いが、 annotate("text", x = -Inf, y = Inf, hjust = 0, vjust = 1, label = "top-left") のように Inf とjustificationを駆使すれば端付近に置くことができる。
データ点に対応する文字列を添えるには geom_text(aes(label = foo)) のほうが適している。 オプションで nudge_x = 2, nudge_y = 2 などとすれば点と重ならないようにずらせる。 position_nudge()

Extensions

ggplotを拡張するための仕組みがversion 2.0から正式に導入され、 ユーザーが独自の stats や geom を作って登録することが容易になった。

gridExtra

https://github.com/baptiste/gridextra/wiki

データによって自動的にパネルを分割するには facet_grid()facet_wrap() を使えばよいが、 関係ない複数の図を1枚に描きたい場合は gridgtable の機能を使う必要がある。 gridExtra はそのへんの操作を手軽にできるようにしてくれるパッケージ。 残念ながら開発中止?

“grob” は “grid graphical object” の略。 ggplotオブジェクトと同じように ggsave() に渡して保存可能。

grob = gridExtra::arrangeGrob(p1, p2, nrow = 2, ncol = 1, bottom = "Time")
grid.newpage()
grid.draw(grob)

複数ページのPDFに書き出したい場合は purrr などを使って list of ggplots を作っておき、 marrangeGrob() に渡す。

.grobs = purrr::map(.dataframes, my_ggplot_func)
.gtable = gridExtra::marrangeGrob(.grobs, nrow = 4, ncol = 3)
ggsave("multi_page.pdf", .gtable, width = 7, height = 9.9)

cowplot

ggplotを学術論文向けにカスタマイズしやすくする。 主な利用目的はgridExtraと同じでggplotを並べる機能。 論文figureのようなA, B, Cラベルをオプションで簡単に付けられるのが良い。

cowplot::plot_grid()
facet_wrap()のように、ざっと並べるのに便利。 もちろん入れ子も可能。
cowplot::plot_grid(..., plotlist = NULL,
    align = c("none", "h", "v", "hv"),
    nrow = NULL, ncol = NULL,
    scale = 1, rel_widths = 1, rel_heights = 1,
    labels = NULL, label_size = 14,
    hjust = -0.5, vjust = 1.5)

さらに細やかな制御をしたいときは以下の関数を個別に重ねていく。

cowplot::ggdraw(plot = NULL, xlim = c(0, 1), ylim = c(0, 1))
これの後ろに + 演算子で draw_***() を足していく。
cowplot::get_legend(plot)
凡例が共通する図を並べるとき、代表のやつをこれで取っておいてあとで並べる。

draw_figure_label() draw_grob() draw_label() draw_line() draw_plot() draw_plot_label() draw_text()

theme_cowplot() ggsave2()

パッケージ読み込みと同時に勝手にテーマを変更したり、 ggsave() 関数を上書きしたりという問題が過去にはあったが、今は大丈夫。

できあがった図を並べるための新しいパッケージとして patchwork がすごくエレガントで期待大。 ただし、演算子を多用するスタイルは忘れやすく検索しにくい諸刃の剣。

関連書籍