2020/03/21

R - 研究編 2 日付・時刻に関するデータ構造と演算

大抵のプログラミング言語で、日付・時刻に関するデータ構造と演算の事情は、歴史的経緯と互換性の影響で混沌としている。R もやはりややこしい状況になっている。
R に用意されている日付・時刻のためのクラスが複数あるので、それぞれの機能を紹介したいと思う。

Date クラス

Date クラスは、日付を保持するためのクラスである。内部的には要素数 1 の数値ベクトルで、1970年1月1日を起点 (0) とした日数を持っている。負値も持てるので、意味を為すかどうかは別として、日数が double の精度で表現できる範囲の日付が保持できる。ただし表示の都合を考えると、0年1月1日から9999年12月31日のデータが扱えると考えた方が良い。この範囲の外にある値は、表示がおかしくなる。

Date オブジェクトは as.Date() 関数で生成する。代表的な使い方は以下の通り。

文字列からの生成

> as.Date("130117", "%y%m%d")
[1] "2013-01-17"
> as.Date("2013-01-17")
[1] "2013-01-17"
> as.Date("2013/01/17")
[1] "2013-01-17"
> as.Date(20, as.Date("2013-01-17"))
[1] "2013-02-06"

最初の例は、文字列と書式文字列を指定して生成している。書式文字列は POSIX 準拠。
%Y-%m-%d か %Y/%m/%d 形式の文字列であれば、書式文字列は省略できる (2つ目、3つ目の例)。文字列のかわりに因子を与えると、その水準名の文字列を対象として同様の生成を行う (例は省略)。
数値と日付の2つの引数を与えると、その日付を起点として数値の示す日数分ずらした日付の Date オブジェクトが生成される。

その他、as.Date(x) の x が後述の各種日付・時刻関連クラスのオブジェクトであれば、その日付の Date オブジェクトが生成できる。

Date オブジェクトの演算

Date クラスの +, - 演算子はオーバーライドされており、第2オペランドに数値や後述の difftime クラスのオブジェクトを渡すことで日付計算ができる。
> dt <- as.Date("2013-01-01")
> dt + 16
[1] "2013-01-17"

この計算にあたっては、1日未満の端数も内部で保持しており、結果は累積する。
> dt <- dt + 0.5
> dt
[1] "2013-01-01"
> dt <- dt + 0.5
> dt
[1] "2013-01-02"

しかし、生成時に時刻まで含めた情報を与えても、その時刻は内部には保持されていない。
> dt <- as.Date("2013-01-01 12:00:00", "%Y-%m-%d %H:%M:%S")
> dt + 0.5
[1] "2013-01-01"
> unclass(dt)
[1] 15706.00000000000000000

unclass() してみると端数を持っていないことがわかる。

Date クラスの seq() はオーバーライドされており、拡張された by パラメータが使える。by に数値を与えたときは通常の seq() の動作に従うので、1日ずつ足していくが、文字列で "days", "weeks", "months", "years" を指定することができる。例えば months を指定すれば、1ヶ月ずつ足されたシーケンスが取得できる。
> seq(as.Date("2013-01-01"), by="months", length=12)
 [1] "2013-01-01" "2013-02-01" "2013-03-01" "2013-04-01" "2013-05-01" "2013-06-01" "2013-07-01" "2013-08-01" "2013-09-01"
[10] "2013-10-01" "2013-11-01" "2013-12-01"

また、by には difftime オブジェクトを渡すこともできる。

Date オブジェクトの文字列化

Date オブジェクトを文字列に変換するには format() が使える。POSIX 書式文字列を用いて文字列化できる。

POSIXt クラス、POSIXct クラス、POSIXlt クラス

これらは、POSIX 仕様の日付・時刻データ構造に相当するクラスである。POSIXct は POSIX/C99 の time_t 型に対応し、POSIXlt は struct tm 型に対応するものである。この2つのクラスの共通の親クラスとして POSIXt が定義されている。

POSIXct

POSIXct の内部構造は、1970年1月1日0時0分0秒 (GMT) からの通算秒数を表す数値と、タイムゾーンを示す属性 tzone となっている。Date と同様 as.POSIXct() により生成できる。C の time_t 型は long 型であることが多く、秒単位の精度となるが、R では後述の例のようにマイクロ秒の精度まで保持できる。
> options(digits.secs=6)
> ct <- as.POSIXct("2013-01-17 15:24:24.123456", format="%Y-%m-%d %H:%M:%OS")
> ct
[1] "2013-01-17 15:24:24.123456 JST"

%OS は、秒の小数表記に対応しており、小数点以下に digits.secs オプションに指定された桁数を想定する。digits.secs は 0 ~ 6 の値が指定可能。

unclass() して内部構造を確認すると、以下のようになっている。
> unclass(ct)
[1] 1358403864.123456001282
attr(,"tzone")
[1] ""

POSIXlt

POSIXlt の内部構造は、sec (秒), min (分), hour (時), mday (日), mon (月/0:1月~11:12月), year (西暦 - 1900), wday (曜日/0:日曜日~6:土曜日), yday (当年1月1日からの相対日数/0~), isdst (サマータイム適用フラグ/0, 1) の各要素を持つリストである。C の struct tm 型に対応しているが、sec には秒未満の値も入る。
> lt <- as.POSIXlt("2013-01-17 15:24:24.123456", format="%Y-%m-%d %H:%M:%OS")
> lt
[1] "2013-01-17 15:24:24.123456"
> unclass(lt)
$sec
[1] 24.12345600000000089835

$min
[1] 24

$hour
[1] 15

$mday
[1] 17

$mon
[1] 0

$year
[1] 113

$wday
[1] 4

$yday
[1] 16

$isdst
[1] 0

POSIXct と POSIXlt の特性比較

基本的には計算や比較には POSIXct が適しており、表示やフィールド別の操作には POSIXlt が適しているので、用途に応じて相互に変換しながら用いることが多い。内部的にも変換がよく行われている。例えば、+, - 演算子は POSIXt 型でオーバーライドされていて、秒単位の加減算ができるようになっているが、これは第1オペランドが POSIXlt 型の場合でも POSIXct 型に変換してから計算されていて、戻り値は POSIXct 型になっている (そこは POSIXlt 返せよという気もするが…)。
POSIXct, POSIXlt 共に as.Date() で Date に変換することができるが、日未満の情報は欠落する。

difftime クラス

difftime は、2つの日付・時刻間の差を保持するためのクラスである。直接 difftime オブジェクトを生成することもできるが、- 演算子で Date 同士または POSIXt 同士の差をとった結果として生成されたものを見掛けることの方が多いかと思う。

difftime の内部構造と生成

difftime クラスの内部構造は、本体は差を示す数値で、その単位を示す属性 units とタイムゾーンを示す属性 tzone を持つ形となっている。
units は secs, mins, hours, days, weeks の値を取る。- 演算子で Date 型同士の差を取った場合は、units は days となる。POSIXt の差の場合は、結果に応じて適宜 secs~days の units が選ばれる。
関数 difftime() を用いれば、as.POSIXct() で POSIXct に変換できるオブジェクトであれば何でも差分を取ることができる。

difftime オブジェクトを直接生成するには、as.difftime() を用いる。
数値と単位を直接指定して作ると以下のようになる。
> as.difftime(0.001, units="secs")
Time difference of 0.001000000000000000020817 secs

文字列から作ることもできる。デフォルトでは %X に対応する文字列で、0:0:0 ~ 23:59:60 の範囲で指定可能。
> as.difftime("01:30:00")
Time difference of 1.5 hours

書式文字列を指定できるので、例えば2日と12時間を指定するのに以下のようにできないかと試してみたが、だめだった。
> as.difftime("2 days 12:00:00", "%d days %H:%M:%OS")
Time difference of -14.50000000000000000000 days

なぜかというと、as.difftime(tim, format) は、difftime(strptime(tim, format=format), strptime("0:0:0", format="%X"), units="auto") の結果を返すようになっており、上記の指定だと第1引数は現在年、現在月の2日12時を示す値になり、第2引数は現在年、現在月、現在日の0時を示す値になるためだ。普通に文字列を指定しただけでも strptime() が2度呼ばれているから、その2度の呼び出しの間で日付が変わってしまうとおかしなことになってしまいそうだ。怖くて使えない。

date クラス

date パッケージによって提供されているクラスである。内部的には1970年1月1日を起点とする相対日数を示す数値ベクトルである。
Date クラスはネイティブ関数を活用して駆動するが、date は純粋に R 上で駆動するようになっている。Date の方が高機能なので、あまり使う必要はないだろう。

chron クラス、dates クラス、times クラス

chron パッケージによって提供されているクラスである。内部的には、相対日数の数値ベクトルが本体で、起点の年月日を示す属性 origin (month, day, year の3要素のベクトル) や、書式を示す属性 format を持っている。
times は時間を表すクラスで、1日を1とした数値が本体になっており、origin は持っていない。dates は日付を表すクラスで、origin を起点とした相対日数を持っている。chron は日時を表すクラスで、やはり origin を起点とした相対日数を持っており、format は日付部分と時刻部分の2つの書式文字列をベクトルで持っている。

POSIXct, POSIXlt クラスはネイティブ関数を使用して動作しているが、こちらは純粋に R 上で駆動するようになっている。閏秒やサマータイムの考慮などはないようだが、is.holiday() という関数があり、.Holidays の内容に応じて休日判定をさせることができるようだ。とはいえ、こちらもあまり使うことはないだろう。