Content
補助で入っている授業で、学生には2要因2水準の実験データのグラフをExcelで作ってもらっています。Excelは重なりよけの位置ずらしができないので、誤差範囲については片側にして作ってもらっています。それをggplotでもやりたいという話です。
Problem
まず、データをテキトーに用意します。実験データを読み込んで集計した後だと思ってください。
test_df <-
tibble(
group_line = rep(letters[1:2], each = 2),
group_axis = rep(letters[3:4], times = 2),
y = c(3, 3, 4, 5),
sd = rep(1.5, 4),
ymin = y - sd,
ymax = y + sd
)
test_df# A tibble: 4 × 6
group_line group_axis y sd ymin ymax
<chr> <chr> <dbl> <dbl> <dbl> <dbl>
1 a c 3 1.5 1.5 4.5
2 a d 3 1.5 1.5 4.5
3 b c 4 1.5 2.5 5.5
4 b d 5 1.5 3.5 6.5
yminは誤差範囲の下側、ymaxは誤差範囲の上側の数値です。実際のデータではdplyr::reframe()やdplyr::summarise()の中で、ggplot2::mean_sdl(..., mult = 1)とすれば、平均値と平均値±1SDの値が一発で得られます。
ggplot2::mean_sdl()の例
iris |>
reframe(
check_mean = mean(Sepal.Length),
check_sd = sd(Sepal.Length),
ggplot2::mean_sdl(Sepal.Length, mult = 1),
.by = Species
) Species check_mean check_sd y ymin ymax
1 setosa 5.006 0.3524897 5.006 4.653510 5.358490
2 versicolor 5.936 0.5161711 5.936 5.419829 6.452171
3 virginica 6.588 0.6358796 6.588 5.952120 7.223880
mean_sdl()のHelpを見るとわかりますが、もともとはHmisc::smean.sdl()なのでその引数(mult、na.rm)が使えます。引数multは標準偏差を何倍して平均値に加えるかなので1でいいです。
普通にggplotで作るならこんな感じでしょうか。
test_df |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_errorbar(
aes(ymin = ymin, ymax = ymax),
width = .2,
alpha = .5,
position = position_dodge(.2)
) +
geom_line(position = position_dodge(.2)) +
geom_point(
aes(shape = group_line),
position = position_dodge(.2),
size = 3,
fill = "white"
) +
scale_shape_manual(
values = c(19, 21)
)
普段の自分で使う用のグラフならこれでいいです。ただ、誤差範囲が両側なのと、Excelでは(簡単に)できない位置ずらしがあるので、答え合わせ用としてはよくないですね。というわけで、位置ずらしせず誤差範囲を片側にしたグラフをggplotでも作りたいわけです。
Solution
まず、ggplot2::geom_errorbar()は要素としてyminとymaxが必須です。なので、例えば上下のどちらかを消そうとしてaes()の中で省いてしまうとエラーになります。
test_df |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
geom_errorbar(
aes(ymax = ymax)
)Error in `geom_errorbar()`:
! Problem while setting up geom.
ℹ Error occurred in the 1st layer.
Caused by error in `compute_geom_1()`:
! `geom_errorbar()` requires the following missing aesthetics: ymin or
xmin and xmax.
ということで、yminとymaxに適切な値を設定する必要があります。
まず、横軸の水準ごとに値が小さい方がTRUEを返す列を作り、値が小さい方はymaxがNAに、値が大きい方はyminがNAになるようにします。
test_df |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
1 .by = group_axis
) |>
mutate(
ymin = if_else(is_smaller, ymin, NA),
ymax = if_else(!is_smaller, ymax, NA)
)- 1
-
横軸の水準ごとになので、ここの
.byは必須。それかgroup_by() |> mutate() |> ungroup()。
# A tibble: 4 × 7
group_line group_axis y sd ymin ymax is_smaller
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <lgl>
1 a c 3 1.5 1.5 NA TRUE
2 a d 3 1.5 1.5 NA TRUE
3 b c 4 1.5 NA 5.5 FALSE
4 b d 5 1.5 NA 6.5 FALSE
この状態でgeom_errorbar()を使うとこうなります。
test_df |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin = if_else(is_smaller, ymin, NA),
ymax = if_else(!is_smaller, ymax, NA)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_errorbar(
aes(ymin = ymin, ymax = ymax),
width = .2,
alpha = .5
)
片側だけにひげができていますが、線分が消えてしまっています。ではNAの代わりにyの値を入れるとどうなるでしょうか。
test_df |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin = if_else(is_smaller, ymin, y),
ymax = if_else(!is_smaller, ymax, y)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_errorbar(
aes(ymin = ymin, ymax = ymax),
width = .2,
alpha = .5
)
線分の表示が戻りました。一見よさそうに見えるのですが、これをマーカーと合わせるとこうなります。
test_df |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin = if_else(is_smaller, ymin, y),
ymax = if_else(!is_smaller, ymax, y)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_errorbar(
aes(ymin = ymin, ymax = ymax),
width = .2,
alpha = .5
) +
geom_line() +
geom_point(
size = 3,
fill = "white"
)
マーカーにひげが重なってしまい見栄えが悪いです。ということで、geom::errorbar()にはyを渡すよりはNAを渡す方が作りたいグラフに近いことになります。では、同じことをgeom_linerange()でやるとどうなるでしょう。
test_df |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin = if_else(is_smaller, ymin, y),
ymax = if_else(!is_smaller, ymax, y)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_linerange(
aes(ymin = ymin, ymax = ymax),
alpha = .5
) +
geom_line() +
geom_point(
size = 3,
fill = "white"
)
こちらは線分が表示されていますね。
ということは、geom_errorbar()にNAを入れて片方のひげだけ作り、geom_linerange()にyを入れて線分を描いてもらえば解決しそうな気がします。というわけで、geom_errorbar()用の参照列とgeom_linerange()用の参照列を作ってグラフを描いてもらいます。
test_df |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin_lr = if_else(is_smaller, ymin, y),
ymin_eb = if_else(is_smaller, ymin, NA),
ymax_lr = if_else(!is_smaller, ymax, y),
ymax_eb = if_else(!is_smaller, ymax, NA)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_linerange(
aes(ymin = ymin_lr, ymax = ymax_lr),
) +
geom_errorbar(
aes(ymin = ymin_eb, ymax = ymax_eb),
width = .2,
alpha = .5
) +
geom_line() +
geom_point(size = 3)
求めていたものができました。後はいろいろ調整すれば完成です。
Code
test_df |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin_lr = if_else(is_smaller, ymin, y),
ymin_eb = if_else(is_smaller, ymin, NA),
ymax_lr = if_else(!is_smaller, ymax, y),
ymax_eb = if_else(!is_smaller, ymax, NA)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_linerange(
aes(ymin = ymin_lr, ymax = ymax_lr),
) +
geom_errorbar(
aes(ymin = ymin_eb, ymax = ymax_eb),
width = .2,
alpha = .5
) +
geom_line() +
geom_point(
aes(shape = group_line),
size = 3,
fill = "white"
) +
scale_shape_manual(
values = c(19, 21)
) +
scale_y_continuous(
limits = c(0, 8),
expand = expansion()
)
こんな風に微妙にクロスしているデータでもうまくいきます。
test_df_cross <-
tibble(
group_line = rep(letters[1:2], each = 2),
group_axis = rep(letters[3:4], times = 2),
y = c(2.1, 2, 2, 4),
sd = c(1.7, 1, .5, .2),
ymin = y - sd,
ymax = y + sd
)
test_df_cross# A tibble: 4 × 6
group_line group_axis y sd ymin ymax
<chr> <chr> <dbl> <dbl> <dbl> <dbl>
1 a c 2.1 1.7 0.4 3.8
2 a d 2 1 1 3
3 b c 2 0.5 1.5 2.5
4 b d 4 0.2 3.8 4.2
Code
test_df_cross |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin_lr = if_else(is_smaller, ymin, y),
ymin_eb = if_else(is_smaller, ymin, NA),
ymax_lr = if_else(!is_smaller, ymax, y),
ymax_eb = if_else(!is_smaller, ymax, NA)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_linerange(
aes(ymin = ymin_lr, ymax = ymax_lr),
alpha = .5
) +
geom_errorbar(
aes(ymin = ymin_eb, ymax = ymax_eb),
width = .2,
alpha = .5
) +
geom_line() +
geom_point(
aes(shape = group_line),
size = 3
)
ちなみに、Excelで誤差範囲を片側にしているときにデータがクロスしている場合、誤差範囲の値の片方を負の値にする必要があります。
| 平均値 | 水準c | 水準d |
|---|---|---|
| 条件A | 2.1 | 2 |
| 条件B | 2 | 4 |
| 標準偏差 | 水準c | 水準d |
|---|---|---|
| 条件A | 1.7 | 1 |
| 条件B | 0.5 | 0.2 |
こんな感じでグラフ作成用にデータを配置していた場合、以下のように標準偏差の方のどちらかの水準の列を負の値にすると、誤差範囲の向きが反転します。
| 標準偏差 | 水準c | 水準d |
|---|---|---|
| 条件A | -1.7 | 1 |
| 条件B | -0.5 | 0.2 |
横軸の要因が2要因以上あっても大丈夫です。
set.seed(20251026)
test_df_2x4 <-
tibble(
group_line = rep(letters[1:2], each = 4),
group_axis = rep(LETTERS[1:4], times = 2),
y = runif(8, min = 3, max = 5),
sd = runif(8, min = .5, max = 2),
ymin = y - sd,
ymax = y + sd
)
test_df_2x4# A tibble: 8 × 6
group_line group_axis y sd ymin ymax
<chr> <chr> <dbl> <dbl> <dbl> <dbl>
1 a A 3.65 1.89 1.76 5.54
2 a B 3.88 1.77 2.11 5.65
3 a C 4.90 0.816 4.08 5.71
4 a D 4.51 1.38 3.13 5.88
5 b A 3.98 1.25 2.73 5.22
6 b B 4.79 1.03 3.76 5.82
7 b C 3.30 1.39 1.91 4.70
8 b D 4.50 0.681 3.82 5.18
Code
test_df_2x4 |>
mutate(
is_smaller = rank(y, ties.method = "first") == 1,
.by = group_axis
) |>
mutate(
ymin_lr = if_else(is_smaller, ymin, y),
ymin_eb = if_else(is_smaller, ymin, NA),
ymax_lr = if_else(!is_smaller, ymax, y),
ymax_eb = if_else(!is_smaller, ymax, NA)
) |>
ggplot(aes(x = group_axis, y = y, group = group_line)) +
theme_classic() +
geom_linerange(
aes(ymin = ymin_lr, ymax = ymax_lr),
alpha = .5
) +
geom_errorbar(
aes(ymin = ymin_eb, ymax = ymax_eb),
width = .2,
alpha = .5
) +
geom_line() +
geom_point(
aes(shape = group_line),
size = 3
)