備忘ログ

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

Rの`{ggplot2}`でグラフを描画するときの、欠測値等のデータの一部が欠けた場合の積み上げ面グラフについて考えてみる

Rの{ggplot2}でグラフを描画するときの、欠測値等のデータの一部が欠けた場合の積み上げ面グラフについて考えてみる。

そもそも歯抜けの場合には面グラフが微妙な気がするという話は一旦おいておく。

サンプルデータは次の通り。

set.seed(123)
df <- data.frame(x = 1:12,
                 y_1 = c(rnorm(5, 4), rep(NA, 2), rnorm(5, 5)),
                 y_2 = c(rnorm(5, 4), rep(NA, 2), rnorm(5, 5)),
                 y_3 = c(rnorm(5, 4), rep(NA, 2), rnorm(5, 5)))

この積み上げ棒グラフを書いてみる。

# ggplot2で扱いやすいように縦持ちにしておく
df <- tidyr::pivot_longer(df, cols = dplyr::starts_with("y"),
                          names_to = "y")

library(ggplot2)
df |> 
  ggplot(aes(x = x, y = value, fill = y)) + 
  geom_bar(stat = "identity") +
  scale_x_continuous(breaks = seq(1, 12, 1))
## Warning: Removed 6 rows containing missing values (`position_stack()`).

これを同じようにgeom_area()で書くと欠測値の箇所が埋められて次の観測値までなめらかに繫がるように調整され、次のようになってしまう。

df |> 
  ggplot(aes(x = x, y = value, fill = y)) +
  geom_area(position = "stack") +
  scale_x_continuous(breaks = seq(1, 12, 1))
## Warning: Removed 6 rows containing non-finite values (`stat_align()`).

これはNAをを含む行を削除しても同じことになる。

df |> 
  tidyr::drop_na() |> 
  ggplot(aes(x = x, y = value, fill = y)) +
  geom_area(position = "stack") +
  scale_x_continuous(breaks = seq(1, 12, 1))

解決策としてはgeom_ribbon()を使えばよい。

geom_ribbon()は、面の上側(ymax)、下側(ymin)の値をそれぞれ与えなければならないので先に計算して値としてもっておく。

library(dplyr)
## 
##  次のパッケージを付け加えます: 'dplyr'

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

##  以下のオブジェクトは 'package:base' からマスクされています:
## 
##     intersect, setdiff, setequal, union
# ymaxとyminを計算する
df <- df |> 
  arrange(x, desc(y)) |>
  mutate(ymax = value |> 
           tidyr::replace_na(0) |> 
           cumsum(), 
         ymin = lag(ymax, default = 0),
         ymax = if_else(ymax == ymin, NA, ymax),
         .by = x) 

df |> 
  ggplot(aes(x = x, ymax = ymax, ymin = ymin, fill = y)) +
  geom_ribbon() +
  scale_x_continuous(breaks = seq(1, 12, 1))

みんな一斉に同じ期間データが欠測している場合にはこれで良さそう。

時々バラバラのタイミングで欠測値がある場合を考えてみる。

set.seed(123)
df <- data.frame(x = 1:12,
                 y_1 = c(rnorm(3, 10), rep(NA, 2), rnorm(4, 10), rep(NA, 3)),
                 y_2 = c(rep(NA, 3), rnorm(9, 10)),
                 y_3 = c(rep(NA, 2), rnorm(3, 10), rep(NA, 2), rnorm(5, 10)))

これの積み上げ棒グラフを適当にggplot2で書いてみる。

# ggplot2で扱いやすいように縦持ちにしておく
df <- tidyr::pivot_longer(df, cols = dplyr::starts_with("y"),
                          names_to = "y")

df |> 
  ggplot(aes(x = x, y = value, fill = y)) + 
  geom_bar(stat = "identity") +
  scale_x_continuous(breaks = seq(1, 12, 1))
## Warning: Removed 12 rows containing missing values (`position_stack()`).

これを積み上げ面グラフにするのにgeom_area()を使おうとすると次のようになる。

df |> 
  ggplot(aes(x = x, y = value, fill = y)) +
  geom_area(position = "stack") +
  scale_x_continuous(breaks = seq(1, 12, 1))
## Warning: Removed 12 rows containing non-finite values (`stat_align()`).

先程の例と同じでNAを含む行を除いても同じ結果になる。

先の例と同じように、上側と下側の値を計算してgeome_ribbon()を使うと次のようになる。

# ymaxとyminを計算する
df <- df |> 
  arrange(x, desc(y)) |>
  mutate(ymax = value |> 
           tidyr::replace_na(0) |> 
           cumsum(), 
         ymin = lag(ymax, default = 0),
         ymax = if_else(ymax == ymin, NA, ymax),
         .by = x) 

df |> 
  ggplot(aes(x = x, ymax = ymax, ymin = ymin, fill = y)) +
  geom_ribbon() +
  scale_x_continuous(breaks = seq(1, 12, 1))

たしかに、各xの箇所ででの値はそうなるし、そこからつなげるとそうなるのだけれど、面グラフとしてグラフの中に塗られていないスカスカの箇所があるのは見た目上違和感を感じる。

見た目を整えるという目的であれば暫定的な解決策の一つとしては、NA0で埋めてやると次のように層状にちゃんとなる。

df |> 
  tidyr::replace_na(list(value = 0)) |> 
  ggplot(aes(x = x, y = value, fill = y)) +
  geom_area() +
  scale_x_continuous(breaks = seq(1, 12, 1))

ただ、この場合、NAが0になっているのでじわっと減ったり増えたりしている印象をもたせるので、全体の総量の経過を経時的に見せたい場合以外には良くない気がする(NAが0という意味であれば問題ないが)。

上記の場合、グラフだけだと棒グラフも欠測値なのか0なのかはわかりかねるというのは変わらない。