備忘ログ

チラシの裏的備忘録&メモ

Rで`for`ループで大量に`{ggplot2}`でグラフを書きたい(`purrr::map()`も添えて)

Rでforループで大量に{ggplot2}でグラフを書き、Rmarkdown で書いたレポートに入れたいと思った。forループじゃなくpurrr::map()の例は最後に書く。

{ggplot2}で、例としてirisデータのSpeciesごとに、Sepal.LengthSepal.widthの散布図を書きたいとする。3つくらいならコードをその都度書いてもよいがforループで

データを取り出すために、{dplyr}filter()をつかってやりたいとする。

# 今回使うパッケージをロードする
library(ggplot2)
library(dplyr)
## 
##  次のパッケージを付け加えます: 'dplyr'

##  以下のオブジェクトは 'package:stats' からマスクされています: 
## 
##      filter, lag

##  以下のオブジェクトは 'package:base' からマスクされています: 
## 
##      intersect, setdiff, setequal, union

irisデータのSpeciesunique()で取り出す。

(描出されないけど)普通の書き方をすると次のようになると思う。

for(i in unique(iris$Species)){
  iris %>% 
    filter(Species == i) %>% 
    ggplot(aes(x = Sepal.Length, y = Sepal.Width)) +
    geom_point() +
    labs(title = i)
}

これは思ったようにグラフは描出されない。

すぐに思いつくのは、print()に最後にパイプで渡せば描出してくれそう(これも思ったように挙動しない)。

for(i in unique(iris$Species)){
  iris %>% 
    filter(Species == i) %>% 
    ggplot(aes(x = Sepal.Length, y = Sepal.Width)) +
    geom_point() +
    labs(title = i) %>% 
    print()
}
## $title
## [1] "setosa"
## 
## attr(,"class")
## [1] "labels"
## $title
## [1] "versicolor"
## 
## attr(,"class")
## [1] "labels"
## $title
## [1] "virginica"
## 
## attr(,"class")
## [1] "labels"

グラフがほしいんですよ、グラフが。

次にちょっと手間だけれど適当な空オブジェクト(今回はfor.ggというオブジェクト)をつくってそれの中にループさせて作ったものをリストとして格納していく風にして、最後にループ外でオブジェクトに格納されたものを取り出してみる。

for.gg <- NULL
for(i in unique(iris$Species)){
  for.gg[[i]] <- iris %>% 
    filter(Species == i) %>% 
    ggplot(aes(x = Sepal.Length, y = Sepal.Width)) +
    geom_point() +
    labs(title = i)
}

これはgg[[1]]gg[[2]]gg[[3]]に各散布図のデータが格納されている。図を取り出すにはgg[[1]]gg$setosaとして1個ずつ必要なグラフを取り出すか、ggでまとめて取り出せる。

for.gg
## $setosa

token

## 
## $versicolor

token

## 
## $virginica

token

こうすると、for.gg内のグラフオブジェクトは更にgeome_*()とかで書き込める。

for.gg[[1]] +
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula 'y ~ x'

token

これは一つのメリットでもあるが、もう図に書き込みはしないし再利用(再掲)する予定もなければオブジェクト内に保持する必要もないのでforループ内にでグラフの描画まで完結させたい。

そうしたとき、グラフ描画部分のコードを()でくくって、パイプでprint()にわたすと実現する。

for(i in unique(iris$Species)){
  (iris %>% 
     filter(Species == i) %>% 
     ggplot(aes(x = Sepal.Length, y = Sepal.Width)) +
     geom_point() +
     labs(title = i)) %>% 
    print()
}

f:id:indenkun:20211112120307p:plain f:id:indenkun:20211112120309p:plain f:id:indenkun:20211112120311p:plain

forループのコードを走らせただけで書きたい図がすべて描出される。(゚д゚)ウマー

場合により使い分けするとよいかもしれない。

ところで{ggplot2}で作るオブジェクトはRStudioのGroval Environmentをみると分かる通りリスト形式になっているので{purrr}map()をつかっても同じことが実現できる。

これなら空のオブジェクトを作る必要はない。オブジェクトに格納する方法であれば次のように書ける。

map.gg <- purrr::map(unique(iris$Species), function(i){
  iris %>% 
    filter(Species == i) %>% 
    ggplot(aes(x = Sepal.Length, y = Sepal.Width)) +
    geom_point() +
    labs(title = i)
})

一つだけ取り出してみる。

map.gg[[1]]

f:id:indenkun:20211112120341p:plain

こちらのほうがmap()つかって処理しているのでなんとなくRっぽい。

これも先のforループでオブジェクトに格納したときと同様にいじることができる。

map.gg[[1]] + 
  geom_smooth(method = "lm")
## `geom_smooth()` using formula 'y ~ x'

token

オブジェクトに格納しないなら次のように書くこともできる。

purrr::map(unique(iris$Species), function(i){
  iris %>% 
    filter(Species == i) %>% 
    ggplot(aes(x = Sepal.Length, y = Sepal.Width)) +
    geom_point() +
    labs(title = i)
})
## [[1]]

token

## 
## [[2]]

token

## 
## [[3]]

token

2重以上での条件(条件1 * 条件2)の組み合わせでグラフを書きたいと思ったときはこの延長で入れ子構造にすると書ける。

# サンプルデータを作る
d <- tibble::tibble(v1 = rnorm(1000),
                    v2 = rnorm(1000),
                    type = sample(c("A", "B"), 1000, replace = TRUE),
                    group = sample(c("X", "Y"), 1000, replace = TRUE))

例えば、forループだと次のようにして書ける。

for(i in unique(d$type)){
  for(j in unique(d$group)){
    (d %>% 
       filter(type == i) %>% 
       filter(group == j) %>% 
       ggplot(aes(x = v1, y = v2)) +
       geom_point() +
       labs(title = paste0(i, "*", j))) %>% 
      print()
  }
}

f:id:indenkun:20211112120717p:plain f:id:indenkun:20211112120720p:plain f:id:indenkun:20211112120722p:plain f:id:indenkun:20211112120724p:plain

purrr::map()ならこんな感じ。

purrr::map(unique(d$type), function(i){
  purrr::map(unique(d$group), function(j){
    d %>% 
      filter(type == i) %>% 
      filter(group == j) %>%
      ggplot(aes(x = v1, y = v2)) +
      geom_point() +
      labs(title = paste0(i, "*", j))
  })
})
## [[1]]
## [[1]][[1]]

f:id:indenkun:20211112120742p:plain

## 
## [[1]][[2]]

f:id:indenkun:20211112120750p:plain

## 
## 
## [[2]]
## [[2]][[1]]

f:id:indenkun:20211112120802p:plain

## 
## [[2]][[2]]

f:id:indenkun:20211112120810p:plain

少ない量なら一つずつ書くのもいいしcolorで簡単に色分けするくらいで大丈夫だけれど、探索的にデータ見てるときに膨大な数になるとループ処理が活きてくる。