ggplot2でヒストグラムと箱ひげ図を同時に描く&グラフを一括出力する

ggplot2でヒストグラムと箱ひげ図を同時に描く&グラフを一括出力する

すでに他の方々が紹介された内容の繰り返しばかりですが,勉強を兼ねた自分用のメモです。
自分用ではありますが,同じくらいのR歴の方のお役に立てばいいなと思います。
間違いなど、お気づきの場合は@imuyaoyiにご指摘いただければ幸いです。

● 使用するパッケージ

library(ggplot2)
library(dplyr)
library(gridExtra)
library(scales)
library(grid)
library(pforeach)

● ヒストグラムと箱ひげ図を同時に描く

・ 複数のグラフを1枚に描く

  1. ggplotグラフを複数作成する
  2. gridExtraパッケージgrid.arrange関数を使って1枚に描写する
Hist1 <- ggplot(iris, aes(x = Sepal.Length))+
  geom_histogram()

Box1 <- ggplot(iris, aes(x = 1, y = Sepal.Length))+
  geom_boxplot()+
  coord_flip()  # グラフを横向きにする
 
grid.arrange(Hist1, Box1, ncol = 1,  # 1列に
                          nrow = 2) # 2行描く

・ 見た目を整える

  • 全体
    • ヒストグラムと箱ひげ図のx軸を一致させたい
    • ヒストグラムを大きく,箱ひげ図を小さくしたい
    • グラフタイトルをつけたい
    • グラフ全体に外枠をつけたい
    • 「count」とか軸ラベル全部いらない
    • x軸の両サイドを最大最小よりちょっと長くしたい
    • フォントサイズ大きくしたい
  • ヒストグラム
    • 平均値の線を引きたい
    • 階級数を好きに決めたい
    • 背景色は消してx軸とy軸だけ線引きたい
    • でもグリッド線はほしい
  • 箱ひげ図
    • y軸の数値ラベル消したい
    • 背景も軸もいらないグリッド線だけいる

箱ひげ図のy軸ラベルを丸っきり消してしまうと,ヒストグラムと揃えたはずのx軸がズレるので軸ラベルの色を白にして見えなくしました。
グラフ全体の外枠をつけるのに使用したgrid.rect関数はgridパッケージに含まれる関数です。

V <- iris$Sepal.Length
Bin = diff(range(V)/20)  # 変数の値の幅を指定したい階級数で割って階級幅を求める
Mean = mean(V)           # ヒストグラム用の平均値
Lim = c(min(V)-sd(V)/2, max(V)+sd(V)/2) # x軸の範囲(標準偏差の1/2分ながくする)

Hist2 <- ggplot(iris, aes(x = Sepal.Length))+
  geom_histogram(binwidth = Bin, fill = "darkgray")+ # 階級幅 # ヒストグラムの色
  geom_vline(xintercept = Mean, color = "black", size = 1)+ #  平均値の線を引く
  coord_cartesian(xlim = Lim)+ # x軸範囲
  theme_classic(base_size = 18)+ # 背景や軸を全体のテーマで設定 # フォントサイズ
  theme(panel.grid.major = element_line(color = "lightgray"))+ # グリッドだけ個別に指定
  labs(title = "Sepal.Length", x = "", y = "") # グラフタイトルと軸ラベル

Box2 <- ggplot(iris, aes(x = 1, y = Sepal.Length))+
  geom_boxplot()+
  coord_flip(ylim = Lim)+ # x軸範囲 #グラフの向きを変えたのでylimで指定する
  theme_minimal(base_size = 18)+
  theme(axis.text.y = element_text(color = "white"))+  # y軸ラベルのフォント色を白にして見えなくする
  labs(x = "", y = "")

grid.arrange(Hist2, Box2, ncol = 1, nrow = 2, heights = c(4,1)) # heightsでグラフサイズの比率を指定
grid.rect(gp = gpar(fill = NA, lwd = 1, col = "gray")) # 外枠をつける

● ggplotグラフを一括出力する

前にplot関数でやったことをggplot関数を使ってやります。

↑をHiRoshima.R#4で紹介した際,

  • プロットする関数を自作する
  • for関数ではなく並列処理ができるパッケージ及び関数を使う
  • if文の分岐を使って離散変数と連続変数の作図を1つのプログラムでかく

ということなどをご指摘いただきましたので,それらを踏まえて次の手順で作業しました。

  1. ggplot2を含む作図関数を自作する
  2. 並列処理にかける

1.ggplot2を含む作図関数を自作する

ggplot2を含む作図関数を自作するとき,次の2つのポイントに注意が必要です。

  1. 列名ではなく列番号で変数を指定する。
  2. aes()ではなく,aes_string()を使って変数を指定する。

a.については,前回のplot関数を使った場合と同じで,ループ変数iに読み込ませるリストとして
列名よりも列番号で指定する方が扱いやすいため*このようにしています。

ggplot(iris, aes(x = Sepal.Length))+
  geom_histogram()
 
ggplot(iris, aes(x = iris[,1]))+ # 列番号による変数指定
  geom_histogram()

b.は,通常の,""のついていない変数名で指定するaes()ではなく,""のついた文字列形式の変数名で指定するaes_string()を使うということです。

ggplot(iris, aes(x = Sepal.Length))+
  geom_histogram()
 
ggplot(iris, aes_string(x = "Sepal.Length"))+ # 文字列で変数指定
  geom_histogram()

ggplotを含む関数を自作する場合,aes_string()を使ってに変数指定をしないとうまくいきませんでした。

# Fun1 <- function(data, i){
#  ggplot(data, aes(x = data[,i]))+
#    geom_histogram()
# }
# Fun1(iris, 1) 
# Error in data[, i] : object of type 'closure' is not subsettable # エラーになる

Fun2 <- function(data, i){
  ggplot(data, aes_string(x = colnames(data)[i]))+
    geom_histogram()
}

Fun2(iris, 2) # OK!

2.並列処理にかける

pforeachパッケージnpforeach関数を使って一括出力します。

ポイントは以下3つです。

  • pforeachではなくnpforeachを使う
  • オプションで「.final = invisible」を指定する
  • グラフが表示されない場合はprint()をつける

pforeachパッケージを作成されたhoxo_mさんによる解説はこちらです。

Data <- iris[,1:4]

npforeach(i = 1:length(Data), .final = invisible)({
  print(Fun2(Data, i))
})

for関数やforeachパッケージを使うとこんなかんじです。参考までに。。。

for(i in rep(1:length(Data),10)){
  print(Fun3(Data, i))
  }

foreach(i = rep(1:length(Data),10))%do%{
  print(Fun3(Data, i))
  }

● 実践

・ 関数作成

HistBox <- function(data, i){
  V = data[,i]
  VName = colnames(data)[i]
  Bin = diff(range(V))/20
  Mean = mean(V)
  Lim = c(min(V)-sd(V)/2, max(V)+sd(V)/2)
  
  Hist <- ggplot(data, aes_string(x = VName))+
    geom_histogram(binwidth = Bin, fill = "darkgray")+
    geom_vline(xintercept = Mean, color = "black", size = 1)+
    coord_cartesian(xlim = Lim)+
    scale_y_continuous(labels = comma)+  # scalesパッケージを使用して
    scale_x_continuous(labels = comma)+  # 数値を区切りカンマを入れる
    theme_classic(base_size = 18)+
    theme(panel.grid.major = element_line(color = "lightgray"))+
    labs(title = VName, x = "", y = "")
  
  Box <- ggplot(data, aes_string(x = 1, y = VName))+
    geom_boxplot()+
    coord_flip(ylim = Lim)+
    scale_y_continuous(labels = comma)+
    theme_minimal(base_size = 18)+
    theme(axis.text.y = element_text(color = "lightgray"))+
    labs(x = "", y = "")
  
  grid.arrange(Hist, Box, ncol = 1, nrow = 2, heights = c(4,1))
  grid.rect(gp = gpar(fill = NA, lwd = 1, col = "gray"))
  
}
Barplot <- function(data, i){
  
  V = colnames(data)[i] 
  
  data %>%
    dplyr::select_(V) %>%
    dplyr::filter_(!is.na(V)) %>%
    table %>% as.data.frame() %>%
    ggplot(aes(x = . , y = Freq))+
    geom_bar(stat = "identity")+
    geom_text(aes(x = ., y = Freq, label = Freq, vjust = -0.5), size = 5)+
    theme_classic(base_size = 18)+
    theme(panel.grid.major = element_line(color = "white"),
          plot.background = element_rect(color = "gray", size = 1))+
    labs(title = V ,x = "", y = "")
}

・ irisデータ

  npforeach(i = 1:ncol(iris) ,.final = invisible)({
    if(is.factor(iris[,i])){
      print(Barplot(iris,i))  # print()をつけないと出力できない
    } else if(is.numeric(iris[,i])){
      HistBox(iris, i)
    }
  })

・ diamondsデータ

dat <- as.data.frame(diamonds)
  npforeach(i = 1:ncol(dat), .final = invisible)({
    if(is.factor(dat[,i])){
      dat[,i] <- as.factor(dat[,i])
      print(Barplot(dat,i))
      } else if(is.integer(dat[,i])|is.numeric(dat[,i])){
        HistBox(dat, i)
        }
    })

● 反省と雑感

  • 観測度数の桁数が大きくなるとヒストグラムと箱ひげ図のx軸がずれる
    diamondsデータのように観測度数の桁数が大きくなると,ヒストグラムだけy数値軸の幅が広くなり
    グラフが右側にずれてしまうため,箱ひげ図とのx軸の位置がずれてしまいます。。。
    y数値軸の幅を指定して固定するか,箱ひげ図のy数値軸をヒストグラムの方の桁数に合わせて値を大きくするか
    とか考えたけど,できるかわからないし,いい方法が浮かびません(´・x・`)。。。

  • 自作関数Barplotの方が複雑なのに説明をスルーした
    dplyr組み込んだり,aes()じゃなくてaes_string()使ったり,
    途中でtable()使って,さらにdata.frameに変換したりとカオスなのに
    説明あきらめた(´・x・`)。。。 きっともっとうまいやり方があるに違いない。。

  • 一度理想的な作図関数作って方法を確立してしまえば今後楽できると思ったけど,簡単にはいかないー

● 参考にさせていただきました