ggplotで別々のデータを使った2軸グラフを使いたい
ggplot2で異なるグラフを重ねて、yの値が左右で異なるグラフを2軸グラフという。 イメージ的には、下記の2グラフ(平均価格をpoint、平均カラット数を棒グラフ)を1画面に収めるイメージ(※棒グラフの数値テキストは、以降での値を確かめる用なので除く)。
~ 個人的に2軸グラフは見づらくなるしゴチャゴチャしやすいので書くべきではないと思いますが、他からの指示で出さないといけないときがあるんや・・・。 ~
library(tidyverse) library(patchwork) # データ作成 df <- sample_n(diamonds,1000) %>% group_by(clarity) %>% summarise(mean_price = mean(price), mean_carat = mean(carat)) %>% ungroup() # clarity mean_price mean_carat # <ord> <dbl> <dbl> # 1 I1 2705. 1.00 # 2 SI2 5076. 1.09 # 3 SI1 4192. 0.876 # 4 VS2 3580. 0.729 # 5 VS1 4027. 0.785 # 6 VVS2 2611. 0.559 # 7 VVS1 2141. 0.476 # 8 IF 2848. 0.466 point <- df %>% ggplot(aes(color = clarity, fill = clarity, group = clarity)) + geom_point(aes(clarity, mean_price),color = "red", size = 5) + labs(x = "clarity", y = "mean price") bar <- df %>% ggplot(aes(color = clarity, fill = clarity, group = clarity)) + geom_bar(aes(clarity, mean_carat), stat = "identity") + geom_text(aes(clarity, mean_carat, label = round(mean_carat,2)),color="black") + labs(x = "clarity", y = "mean carat") point | bar
どうやって別データへの2軸グラフを作るか
単純な方法
scale_y_...にsec.axisという、2軸目に対して目盛を振る方法があるのでそれを利用する。 ggplot2: Secondary Y axis
sec.axisでは1軸目の数値「.」を用いた計算式マッピングで目盛を記載する。
例えば、1軸目が0~1000で100刻みであるなら、~ ./100
を計算式に書くと2軸目は目盛が0~10で1刻みとなる。
2軸目に0から1.2までで目盛を取りたいのであれば、1軸目の最大値で割った値に取りたい最大値をかければいい。つまり、 ~./1000 * 1.2
を計算させる。
今回は、1軸目が0~4500、2軸目を0~1.2で取りたいので計算式は ~/4500 * 1.2
となる。
注意点として、sec.axisはあくまで2軸目に数値をプロットしているだけなので2軸目と対応させたいグラフのスケールは1軸目に対応している。下図では、2軸目の目盛はうまくいっているが対応させたい棒グラフ自体は1軸目準拠でスケーリングされるので、どれも0~1.2程度なことからほぼ0の棒グラフになっている。
df %>% ggplot(aes(color = clarity, fill = clarity, group = clarity)) + geom_point(aes(clarity, mean_price), color = "red", size = 5) + geom_bar(aes(clarity, mean_carat), stat = "identity") + # 2軸設定 scale_y_continuous(sec.axis = sec_axis(~./4500 * 1.2, #2軸目の一番上を1.2にする name = "mean carat")) + labs(x = "clarity", y = "mean price")
これを避けるためには、棒グラフの数値自体を2軸目のスケーリングに合わせることで上手くいく。
最大値が2軸目は1.2で、1軸目は4500なので1.2で割り4500をかけると棒グラフの数値は1軸目と同じスケーリングになる。
df %>% ggplot(aes(color = clarity, fill = clarity, group = clarity)) + geom_point(aes(clarity, mean_price),color = "red", size = 5) + geom_bar(aes(clarity, mean_carat / 1.2 * 4500 ), stat = "identity") + # 2軸目の数値を1軸目にスケーリングする # 2軸設定 scale_y_continuous(sec.axis = sec_axis(~./4500 * 1.2, #2軸目の一番上を1.2にする name = "mean carat")) + labs(x = "clarity", y = "mean price")
点と棒グラフが重なって鬱陶しい場合は棒グラフの数値を1/2にし、軸目盛の最大値を2倍にする。
df %>% ggplot(aes(color = clarity, fill = clarity, group = clarity)) + geom_point(aes(clarity, mean_price),color = "red", size = 5) + geom_bar(aes(clarity, (mean_carat / 1.2 * 4500 )/2), stat = "identity") + # 2軸目の数値を1軸目にスケーリングする # 2軸設定 scale_y_continuous(sec.axis = sec_axis(~(./4500 * 1.2) * 2, #2軸目の一番上を1.2 * 2にする name = "mean carat")) + labs(x = "clarity", y = "mean price")
scales::rescale()関数の利用
上記の方法は、1軸目の数値⇔2軸目の数値の変換があまりスマートではない。
scales::rescale()は任意の範囲に対応したマッピングをおこなってくれる。
rescale function | R Documentation
df$mean_carat # 1.0009091 1.0887931 0.8755349 0.7294000 0.7846452 0.5590110 0.4756716 0.4662162 scales::rescale( x = df$mean_carat, # スケールしたい値 to = c(0,4500) # スケールする範囲 #4500.0000 3327.0853 2052.9968 2085.9671 1236.9952 888.3146 107.5510 0.0000
また、range関数は引数の値の最小値、最大値を返すため、スケールする範囲はrange関数を用いると便利とのこと。
ggplot2 2.2.0で2軸グラフを描くときのメモ - Technically, technophobic.
ただ、上手くいかない。
df %>% # priceのスケールに合わせたcaratの値を持つ mutate(scaled_mean_carat = scales::rescale(x = mean_carat, ## scaleしたい値 to = mean_price_range ## スケールする範囲(price範囲) )) %>% ggplot(aes(color = clarity, fill = clarity, group = clarity)) + geom_point(aes(clarity, mean_price),color = "red", size = 5) + # スケールを合わせたcarat値で棒グラフを書く geom_bar(aes(clarity, scaled_mean_carat), stat = "identity") + # 2軸設定 # 目盛の数値を元のcarat値にするために戻す # おそらく、0~ ではないのでなんかおかしくなる scale_y_continuous(sec.axis = sec_axis(~scales::rescale(x = ., #1軸目 to = mean_carat_range), name = "mean carat")) + labs(x = "clarity", y = "mean price")
おそらく、マッピングの関係で2軸目の目盛範囲が0.518 ~ 1.156、つまり0からではないのが原因と考えられる。
> scales::rescale(x = df$mean_price, #1軸目 + to = mean_carat_range) [1] 0.7877944 1.1374783 0.8538765 1.1565000 0.8536595 0.9455982 0.5189286 0.5648220 > min(df$mean_carat) [1] 0.5189286
ただ、sec.axis = c(0,1.1565)としても上手く行かなかった。sec_axis関数の式記述部分の挙動の理解がもうちょい必要そう。
そのうち追記します。