ill-identified diary

所属組織の見解などとは一切関係なく小難しい話しかしません

ggplot2 で日付・時刻データを扱うときは日付の型の違いに注意

この記事は最終更新日から3年以上が経過しています


概要

  • ggplot2 でx軸が日付・時刻のグラフを描いて, geom_vline() を使うときは値の型を DatePOSIXct に統一しないと正常に表示されないという話
  • lubridate なんかを使っているとこの辺の違いを忘れがちかもしれない
  • as.numeric() 変換で対処する」は今は不要

この問題はかなり昔から知られており, 関連しそうなキーワードで検索するだけでもトップに10年近く前のスタックオーバーフローの質問がヒットする. なので今あえて書く必要はないともいえるが, 昨日 R-Wakalang にこの話が投稿されたので日本語のページがすぐに見つからないと調べるのを打ち切る人が多いのかもしれないので書く意味があるのかもしれない. (実際, 今検索したらスタックオーバーフローの機械翻訳を表示するページばかりヒットして私も見る気が失せてしまった.1 なお以前紹介した Hearly の邦訳『データ可視化入門』にもこの話は書かれていない.)

私の R-wakalang の当初の私の回答もあまり正確でなかったことに注意. さらに言うならば, もう少し動作確認を進めると問題の原因が違うところにある可能性が出てきたので書く必要が増したように感じた.


詳細

話題にのぼったのは, X軸が時刻のとき, geom_vline() で特定の日付に垂直線を引けない, というもの.

発生する条件は「X軸に指定する変数と, geom_vline() で指定する日付・時刻が Date 型か POSIXct 型かどちらかに統一されていない」というもの2.

  1. X軸の変数と geom_vline() に与える日付・時刻の型を DatePOSIXct に統一する.
  2. (古い方法) (1) に加えさらにgeom_vline(xintercept = as.numeric(as.Date(...))) のように Numeric 型に変換して与える

現時点 (v3.3.5) では, (1) が最も素直なやり方だろう. 両者の型の違いは, 時刻やタイムゾーン情報を含むかどうかである. 古いバージョンでは ggplot2 の issue #1048スタックオーバーフローのこの質問に書かれているように単に POSIXct を使うだけでは不十分で, geom_vline() 内では as.numeric() が必要だったようだが, 現在はこれは不要になっている.3 しかし何らかの理由でパッケージを更新できないということもありえるので, もしかするとこちらの方法が必要なこともあるかもしれない.

なお, geom_vline 以外で日付の型が一致してない場合はそういう内容のエラー文が表示される.

2021/7/26追記: あまり使うことはなさそうだが, y軸と geom_hline()の組み合わせでも試したところ, これと同様な挙動だった.

実際に試してみる4. まずは問題が発生しない, いずれも Date 型の場合は, 特別なことを何もしなくとも表示できる.

require(ggplot2)
d <- data.frame(date = seq(as.Date("1945-01-01"), as.Date("1965-01-01"), by = "quarter"))
d$date_posix <- with(d, as.POSIXct(date, tz = "Asia/Tokyo"))
d$x <- 1:NROW(d) + 4 * sin(4 * 1:NROW(d))

ggplot(d, aes(x = date, y = x)) + geom_line() +
  geom_vline(xintercept = as.Date("1955-04-01"), linetype = 2, color = "red") +
  geom_vline(xintercept = as.Date("1960-04-01"), linetype = 2, color = "red")

f:id:ill-identified:20210723190134p:plain

POSIXct に統一しても表示できる.

ggplot(d, aes(x = date_posix, y = x)) + geom_line() +
  geom_vline(xintercept = as.POSIXct("1955-04-01"), linetype = 2, color = "red") +
  geom_vline(xintercept = as.POSIXct("1960-04-01"), linetype = 2, color = "red")

f:id:ill-identified:20210723190208p:plain

X軸が Date 型で xintercept に指定する値が POSIXct 型だと表示されない.

ggplot(d, aes(x = date, y = x)) + geom_line() +
  geom_vline(xintercept = as.Date("1955-04-01"), linetype = 2, color = "red") +
  geom_vline(xintercept = as.POSIXct("1960-04-01"), linetype = 2, color = "red")

f:id:ill-identified:20210723190230p:plain

逆も同様.

ggplot(d, aes(x = date_posix, y = x)) + geom_line() +
  geom_vline(xintercept = as.Date("1955-04-01"), linetype = 2, color = "red") +
  geom_vline(xintercept = as.POSIXct("1960-04-01"), linetype = 2, color = "red")

f:id:ill-identified:20210723190309p:plain

ちなみに複数の垂直線を引きたい場合は以下のように aes() を使うこともできる. この場合も型の統一が必要.

ggplot(d, aes(x = date_posix, y = x)) + geom_line() +
  geom_vline(
    aes(xintercept = date_posix),
    linetype = 2,
    color = "red",
    data = data.frame(date_posix = as.POSIXct(c(
      "1955-04-01", "1960-04-01"
    )))
  )

f:id:ill-identified:20210723190323p:plain

もう少し細かいことを言うと, geom_vline() 以外の関数では型がそろっていないとエラーで停止する. つまり「geom_vline() だけ不適切な入力に対して本来エラーを返すべきなのになされていない」という見方もできる.

ggplot(d, aes(x = date, y = x)) +
  geom_line(aes(color = "Date")) +
  geom_point(aes(x = date_posix, color = "POSIX"))
## Error: Invalid input: date_trans works with objects of class Date only


as.numeric による古い対処法

as.numeric() でも Dateas.POSIXct の変換はできない. つまり現在の最新バージョンでは as.numeric は無意味である.

ggplot(d, aes(x = date, y = x)) + geom_line() +
  geom_vline(xintercept = as.numeric(as.Date("1955-04-01")), linetype = 2, color = "red") +
  geom_vline(xintercept = as.numeric(as.POSIXct("1960-04-01")), linetype = 2, color = "red")

f:id:ill-identified:20210723190350p:plain

すでに上げたスタックオーバーフローの質問によると, 古いバージョンでは DatePOSIXct いずれも統一した場合でもこれが必要があるらしいが, 今回は実際に確認していない.


補足: lubridate パッケージ使用時の注意点

いろいろな時刻や日付のフォーマットを変換したり演算したりするのに lubridate パッケージが便利だが, 便利さゆえにこの Date 型と POSIXct 型の違いを忘れがちになるのかもしれない. lubridate パッケージはおおむね, 入力で両者の違いを意識せずにすむような作りになっているが, 出力は関数によって Date 型だったり POSIXct だったりするので注意が必要である. (POSIXlt 型もあるが ggplot2 が対応していないので説明略)

例えば ymd(), mdy() などの年月日までの入力を受け付ける関数はデフォルトで Date 型を返すが, タイムゾーンを指定した場合や ymd_h(), ymd_hm(), ymd_hms() などは POSIXct 型を返す5. 後者は Date 型で表現できないから当たり前なのだが, この辺のルールはヘルプをよく読まないと気づかない可能性がある. ちなみに tibble を使えばヘッダに <date>, <dttm> とそれぞれ表示されるので違いに気づきやすい. 最新バージョンでも当初の投稿のような現象がおこるなら, この辺を一度確認してみるといいだろう.




  1. 検索結果からノイズを減らすために私がいろいろなスクリプトでこの手のページを結果から弾いていることを考慮しても多い.↩︎

  2. もとの投稿では「Date 型であるのにうまくいかない」と書いているが, 両者ともに Date 型ならば問題は発生しないはずなので, 古いバージョンを使っている可能性がある. chagelog を流し読みしただけではいつごろ変更があったかはわからなかったが, https://github.com/r-lib/scales/pull/75 と関係があるなら2018年ごろには修正されているようだ.↩︎

  3. issue の議論では修正が難しいとコメントされて終わっているが, いつの間にか改善されている. change log を簡単に検索しただけではいつごろ修正されたのか分からなかった↩︎

  4. R 本体のバージョンは 4.1.0, ggplot2 は 3.3.5↩︎

  5. v1.7.10 時点の挙動↩︎